mirror of https://github.com/dotnet/runtime
584 lines
30 KiB
C#
584 lines
30 KiB
C#
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using Xunit;
|
|
using Xunit.Sdk;
|
|
using Xunit.Abstractions;
|
|
using System.Linq;
|
|
|
|
namespace System.IO.Tests
|
|
{
|
|
public abstract partial class FileSystemWatcherTest : FileCleanupTestBase
|
|
{
|
|
// Events are reported asynchronously by the OS, so allow an amount of time for
|
|
// them to arrive before testing an assertion. If we expect an event to occur,
|
|
// we can wait for it for a relatively long time, as if it doesn't arrive, we're
|
|
// going to fail the test. If we don't expect an event to occur, then we need
|
|
// to keep the timeout short, as in a successful run we'll end up waiting for
|
|
// the entire timeout specified.
|
|
public const int WaitForExpectedEventTimeout = 500; // ms to wait for an event to happen
|
|
public const int LongWaitTimeout = 50000; // ms to wait for an event that takes a longer time than the average operation
|
|
public const int SubsequentExpectedWait = 10; // ms to wait for checks that occur after the first.
|
|
public const int WaitForExpectedEventTimeout_NoRetry = 3000;// ms to wait for an event that isn't surrounded by a retry.
|
|
public const int WaitForUnexpectedEventTimeout = 150; // ms to wait for a non-expected event.
|
|
public const int DefaultAttemptsForExpectedEvent = 3; // Number of times an expected event should be retried if failing.
|
|
public const int DefaultAttemptsForUnExpectedEvent = 2; // Number of times an unexpected event should be retried if failing.
|
|
public const int RetryDelayMilliseconds = 500; // ms to wait when retrying after failure
|
|
|
|
/// <summary>
|
|
/// Watches the Changed WatcherChangeType and unblocks the returned AutoResetEvent when a
|
|
/// Changed event is thrown by the watcher.
|
|
/// </summary>
|
|
public static (AutoResetEvent EventOccurred, FileSystemEventHandler Handler) WatchChanged(FileSystemWatcher watcher, string[] expectedPaths = null)
|
|
{
|
|
AutoResetEvent eventOccurred = new AutoResetEvent(false);
|
|
|
|
FileSystemEventHandler changeHandler = (o, e) =>
|
|
{
|
|
Assert.Equal(WatcherChangeTypes.Changed, e.ChangeType);
|
|
|
|
if (expectedPaths == null || expectedPaths.Contains(e.FullPath))
|
|
{
|
|
eventOccurred.Set();
|
|
}
|
|
};
|
|
|
|
watcher.Changed += changeHandler;
|
|
return (eventOccurred, changeHandler);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Watches the Created WatcherChangeType and unblocks the returned AutoResetEvent when a
|
|
/// Created event is thrown by the watcher.
|
|
/// </summary>
|
|
public static (AutoResetEvent EventOccurred, FileSystemEventHandler Handler) WatchCreated(FileSystemWatcher watcher, string[] expectedPaths = null, ITestOutputHelper _output = null)
|
|
{
|
|
AutoResetEvent eventOccurred = new AutoResetEvent(false);
|
|
|
|
FileSystemEventHandler handler = (o, e) =>
|
|
{
|
|
if (e.ChangeType != WatcherChangeTypes.Created)
|
|
{
|
|
_output?.WriteLine("Unexpected event {0} while waiting for {1}", e.ChangeType, WatcherChangeTypes.Created);
|
|
Assert.Equal(WatcherChangeTypes.Created, e.ChangeType);
|
|
}
|
|
|
|
Assert.Equal(WatcherChangeTypes.Created, e.ChangeType);
|
|
|
|
if (expectedPaths == null || expectedPaths.Contains(e.FullPath))
|
|
{
|
|
eventOccurred.Set();
|
|
}
|
|
};
|
|
|
|
watcher.Created += handler;
|
|
return (eventOccurred, handler);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Watches the Renamed WatcherChangeType and unblocks the returned AutoResetEvent when a
|
|
/// Renamed event is thrown by the watcher.
|
|
/// </summary>
|
|
public static (AutoResetEvent EventOccurred, FileSystemEventHandler Handler) WatchDeleted(FileSystemWatcher watcher, string[] expectedPaths = null, ITestOutputHelper _output = null)
|
|
{
|
|
AutoResetEvent eventOccurred = new AutoResetEvent(false);
|
|
FileSystemEventHandler handler = (o, e) =>
|
|
{
|
|
if (e.ChangeType != WatcherChangeTypes.Deleted)
|
|
{
|
|
_output?.WriteLine("Unexpected event {0} while waiting for {1}", e.ChangeType, WatcherChangeTypes.Deleted);
|
|
Assert.Equal(WatcherChangeTypes.Deleted, e.ChangeType);
|
|
}
|
|
|
|
if (expectedPaths == null || expectedPaths.Contains(e.FullPath))
|
|
{
|
|
eventOccurred.Set();
|
|
}
|
|
};
|
|
|
|
watcher.Deleted += handler;
|
|
return (eventOccurred, handler);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Watches the Renamed WatcherChangeType and unblocks the returned AutoResetEvent when a
|
|
/// Renamed event is thrown by the watcher.
|
|
/// </summary>
|
|
public static (AutoResetEvent EventOccurred, RenamedEventHandler Handler) WatchRenamed(FileSystemWatcher watcher, string[] expectedPaths = null, ITestOutputHelper _output = null)
|
|
{
|
|
AutoResetEvent eventOccurred = new AutoResetEvent(false);
|
|
|
|
RenamedEventHandler handler = (o, e) =>
|
|
{
|
|
if (e.ChangeType != WatcherChangeTypes.Renamed)
|
|
{
|
|
_output?.WriteLine("Unexpected event {0} while waiting for {1}", e.ChangeType, WatcherChangeTypes.Renamed);
|
|
Assert.Equal(WatcherChangeTypes.Renamed, e.ChangeType);
|
|
}
|
|
|
|
if (expectedPaths == null || expectedPaths.Contains(e.FullPath))
|
|
{
|
|
eventOccurred.Set();
|
|
}
|
|
};
|
|
|
|
watcher.Renamed += handler;
|
|
return (eventOccurred, handler);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that the given handle will be signaled within the default timeout.
|
|
/// </summary>
|
|
public static void ExpectEvent(WaitHandle eventOccurred, string eventName_NoRetry)
|
|
{
|
|
string message = string.Format("Didn't observe a {0} event within {1}ms", eventName_NoRetry, WaitForExpectedEventTimeout_NoRetry);
|
|
Assert.True(eventOccurred.WaitOne(WaitForExpectedEventTimeout_NoRetry), message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does verification that the given watcher will throw exactly/only the events in "expectedEvents" when
|
|
/// "action" is executed.
|
|
/// </summary>
|
|
/// <param name="watcher">The FileSystemWatcher to test</param>
|
|
/// <param name="expectedEvents">All of the events that are expected to be raised by this action</param>
|
|
/// <param name="action">The Action that will trigger events.</param>
|
|
/// <param name="cleanup">Optional. Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
|
|
public static void ExpectEvent(FileSystemWatcher watcher, WatcherChangeTypes expectedEvents, Action action, Action cleanup = null)
|
|
{
|
|
ExpectEvent(watcher, expectedEvents, action, cleanup, (string[])null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does verification that the given watcher will throw exactly/only the events in "expectedEvents" when
|
|
/// "action" is executed.
|
|
/// </summary>
|
|
/// <param name="watcher">The FileSystemWatcher to test</param>
|
|
/// <param name="expectedEvents">All of the events that are expected to be raised by this action</param>
|
|
/// <param name="action">The Action that will trigger events.</param>
|
|
/// <param name="cleanup">Optional. Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
|
|
/// <param name="expectedPath">Optional. Adds path verification to all expected events.</param>
|
|
/// <param name="attempts">Optional. Number of times the test should be executed if it's failing.</param>
|
|
public static void ExpectEvent(FileSystemWatcher watcher, WatcherChangeTypes expectedEvents, Action action, Action cleanup = null, string expectedPath = null, int attempts = DefaultAttemptsForExpectedEvent, int timeout = WaitForExpectedEventTimeout)
|
|
{
|
|
ExpectEvent(watcher, expectedEvents, action, cleanup, expectedPath == null ? null : new string[] { expectedPath }, attempts, timeout);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does verification that the given watcher will throw exactly/only the events in "expectedEvents" when
|
|
/// "action" is executed.
|
|
/// </summary>
|
|
/// <param name="watcher">The FileSystemWatcher to test</param>
|
|
/// <param name="expectedEvents">All of the events that are expected to be raised by this action</param>
|
|
/// <param name="action">The Action that will trigger events.</param>
|
|
/// <param name="cleanup">Optional. Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
|
|
/// <param name="expectedPath">Optional. Adds path verification to all expected events.</param>
|
|
/// <param name="attempts">Optional. Number of times the test should be executed if it's failing.</param>
|
|
public static void ExpectEvent(FileSystemWatcher watcher, WatcherChangeTypes expectedEvents, Action action, Action cleanup = null, string[] expectedPaths = null, int attempts = DefaultAttemptsForExpectedEvent, int timeout = WaitForExpectedEventTimeout)
|
|
{
|
|
int attemptsCompleted = 0;
|
|
bool result = false;
|
|
FileSystemWatcher newWatcher = watcher;
|
|
while (!result && attemptsCompleted++ < attempts)
|
|
{
|
|
if (attemptsCompleted > 1)
|
|
{
|
|
// Re-create the watcher to get a clean iteration.
|
|
newWatcher = RecreateWatcher(newWatcher);
|
|
// Most intermittent failures in FSW are caused by either a shortage of resources (e.g. inotify instances)
|
|
// or by insufficient time to execute (e.g. CI gets bogged down). Immediately re-running a failed test
|
|
// won't resolve the first issue, so we wait a little while hoping that things clear up for the next run.
|
|
Thread.Sleep(RetryDelayMilliseconds);
|
|
}
|
|
|
|
result = ExecuteAndVerifyEvents(newWatcher, expectedEvents, action, attemptsCompleted == attempts, expectedPaths, timeout);
|
|
|
|
if (cleanup != null)
|
|
cleanup();
|
|
}
|
|
}
|
|
|
|
// Pasted from RetryHelper.cs in order to force FSW tests to log retries to the Helix console.
|
|
// We don't want to do that for tests in general.
|
|
// Once we've gotten enough data, delete this and go back to the regular RetryHelper.
|
|
private static readonly Func<int, int> s_defaultBackoffFunc = i => Math.Min(i * 100, 60_000);
|
|
private static readonly Predicate<Exception> s_defaultRetryWhenFunc = _ => true;
|
|
private static readonly bool s_debug = Environment.GetEnvironmentVariable("DEBUG_RETRYHELPER") == "1";
|
|
|
|
/// <summary>Executes the <paramref name="test"/> action up to a maximum of <paramref name="maxAttempts"/> times.</summary>
|
|
/// <param name="maxAttempts">The maximum number of times to invoke <paramref name="test"/>.</param>
|
|
/// <param name="test">The test to invoke.</param>
|
|
/// <param name="backoffFunc">After a failure, invoked to determine how many milliseconds to wait before the next attempt. It's passed the number of iterations attempted.</param>
|
|
/// <param name="retryWhen">Invoked to select the exceptions to retry on. If not set, any exception will trigger a retry.</param>
|
|
public static void Execute(Action test, int maxAttempts = 5, Func<int, int> backoffFunc = null, Predicate<Exception> retryWhen = null, [CallerMemberName] string? testName = null)
|
|
{
|
|
// Validate arguments
|
|
if (maxAttempts < 1)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(maxAttempts));
|
|
}
|
|
if (test == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(test));
|
|
}
|
|
|
|
retryWhen ??= s_defaultRetryWhenFunc;
|
|
|
|
// Execute the test until it either passes or we run it maxAttempts times
|
|
var exceptions = new List<Exception>();
|
|
for (int i = 1; i <= maxAttempts; i++)
|
|
{
|
|
Exception lastException;
|
|
try
|
|
{
|
|
test();
|
|
return;
|
|
}
|
|
catch (Exception e) when (retryWhen(e))
|
|
{
|
|
lastException = e;
|
|
exceptions.Add(e);
|
|
if (i == maxAttempts)
|
|
{
|
|
throw new AggregateException(exceptions);
|
|
}
|
|
}
|
|
|
|
if (PlatformDetection.IsInHelix || s_debug)
|
|
{
|
|
// Dump into the console output so we can mine it
|
|
Console.WriteLine($"RetryHelper: retrying {testName} {i}th time of {maxAttempts}: got {lastException.Message}");
|
|
}
|
|
|
|
if (s_debug)
|
|
{
|
|
Debug.WriteLine($"RetryHelper: retrying {testName} {i}th time of {maxAttempts}: got {lastException.Message}");
|
|
}
|
|
|
|
Thread.Sleep((backoffFunc ?? s_defaultBackoffFunc)(i));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does verification that the given watcher will not throw exactly/only the events in "expectedEvents" when
|
|
/// "action" is executed.
|
|
/// </summary>
|
|
/// <param name="watcher">The FileSystemWatcher to test</param>
|
|
/// <param name="unExpectedEvents">All of the events that are expected to be raised by this action</param>
|
|
/// <param name="action">The Action that will trigger events.</param>
|
|
/// <param name="cleanup">Optional. Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
|
|
/// <param name="expectedPath">Optional. Adds path verification to all expected events.</param>
|
|
public static void ExpectNoEvent(FileSystemWatcher watcher, WatcherChangeTypes unExpectedEvents, Action action, Action cleanup = null, string expectedPath = null, int timeout = WaitForExpectedEventTimeout)
|
|
{
|
|
bool result = ExecuteAndVerifyEvents(watcher, unExpectedEvents, action, false, expectedPath == null ? null : new string[] { expectedPath }, timeout);
|
|
Assert.False(result, "Expected Event occurred");
|
|
|
|
if (cleanup != null)
|
|
cleanup();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper for the ExpectEvent function.
|
|
/// </summary>
|
|
/// <param name="watcher">The FileSystemWatcher to test</param>
|
|
/// <param name="expectedEvents">All of the events that are expected to be raised by this action</param>
|
|
/// <param name="action">The Action that will trigger events.</param>
|
|
/// <param name="assertExpected">True if results should be asserted. Used if there is no retry.</param>
|
|
/// <param name="expectedPath"> Adds path verification to all expected events.</param>
|
|
/// <returns>True if the events raised correctly; else, false.</returns>
|
|
public static bool ExecuteAndVerifyEvents(FileSystemWatcher watcher, WatcherChangeTypes expectedEvents, Action action, bool assertExpected, string[] expectedPaths, int timeout)
|
|
{
|
|
bool result = true, verifyChanged = true, verifyCreated = true, verifyDeleted = true, verifyRenamed = true;
|
|
(AutoResetEvent EventOccurred, FileSystemEventHandler Handler) changed = default, created = default, deleted = default;
|
|
(AutoResetEvent EventOccurred, RenamedEventHandler Handler) renamed = default;
|
|
|
|
if (verifyChanged = ((expectedEvents & WatcherChangeTypes.Changed) > 0))
|
|
changed = WatchChanged(watcher, expectedPaths);
|
|
if (verifyCreated = ((expectedEvents & WatcherChangeTypes.Created) > 0))
|
|
created = WatchCreated(watcher, expectedPaths);
|
|
if (verifyDeleted = ((expectedEvents & WatcherChangeTypes.Deleted) > 0))
|
|
deleted = WatchDeleted(watcher, expectedPaths);
|
|
if (verifyRenamed = ((expectedEvents & WatcherChangeTypes.Renamed) > 0))
|
|
renamed = WatchRenamed(watcher, expectedPaths);
|
|
|
|
watcher.EnableRaisingEvents = true;
|
|
action();
|
|
|
|
// Verify Changed
|
|
if (verifyChanged)
|
|
{
|
|
bool Changed_expected = ((expectedEvents & WatcherChangeTypes.Changed) > 0);
|
|
bool Changed_actual = changed.EventOccurred.WaitOne(timeout);
|
|
watcher.Changed -= changed.Handler;
|
|
result = Changed_expected == Changed_actual;
|
|
if (assertExpected)
|
|
Assert.True(Changed_expected == Changed_actual, "Changed event did not occur as expected");
|
|
}
|
|
|
|
// Verify Created
|
|
if (verifyCreated)
|
|
{
|
|
bool Created_expected = ((expectedEvents & WatcherChangeTypes.Created) > 0);
|
|
bool Created_actual = created.EventOccurred.WaitOne(verifyChanged ? SubsequentExpectedWait : timeout);
|
|
watcher.Created -= created.Handler;
|
|
result = result && Created_expected == Created_actual;
|
|
if (assertExpected)
|
|
Assert.True(Created_expected == Created_actual, "Created event did not occur as expected");
|
|
}
|
|
|
|
// Verify Deleted
|
|
if (verifyDeleted)
|
|
{
|
|
bool Deleted_expected = ((expectedEvents & WatcherChangeTypes.Deleted) > 0);
|
|
bool Deleted_actual = deleted.EventOccurred.WaitOne(verifyChanged || verifyCreated ? SubsequentExpectedWait : timeout);
|
|
watcher.Deleted -= deleted.Handler;
|
|
result = result && Deleted_expected == Deleted_actual;
|
|
if (assertExpected)
|
|
Assert.True(Deleted_expected == Deleted_actual, "Deleted event did not occur as expected");
|
|
}
|
|
|
|
// Verify Renamed
|
|
if (verifyRenamed)
|
|
{
|
|
bool Renamed_expected = ((expectedEvents & WatcherChangeTypes.Renamed) > 0);
|
|
bool Renamed_actual = renamed.EventOccurred.WaitOne(verifyChanged || verifyCreated || verifyDeleted ? SubsequentExpectedWait : timeout);
|
|
watcher.Renamed -= renamed.Handler;
|
|
result = result && Renamed_expected == Renamed_actual;
|
|
if (assertExpected)
|
|
Assert.True(Renamed_expected == Renamed_actual, "Renamed event did not occur as expected");
|
|
}
|
|
|
|
watcher.EnableRaisingEvents = false;
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does verification that the given watcher will throw an Error when the given action is executed.
|
|
/// </summary>
|
|
/// <param name="watcher">The FileSystemWatcher to test</param>
|
|
/// <param name="action">The Action that will trigger a failure.</param>
|
|
/// <param name="cleanup">Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
|
|
/// <param name="attempts">Optional. Number of times the test should be executed if it's failing.</param>
|
|
public static void ExpectError(FileSystemWatcher watcher, Action action, Action cleanup, int attempts = DefaultAttemptsForExpectedEvent)
|
|
{
|
|
string message = string.Format("Did not observe an error event within {0}ms and {1} attempts.", WaitForExpectedEventTimeout, attempts);
|
|
Assert.True(TryErrorEvent(watcher, action, cleanup, attempts, expected: true), message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does verification that the given watcher will <b>not</b> throw an Error when the given action is executed.
|
|
/// </summary>
|
|
/// <param name="watcher">The FileSystemWatcher to test</param>
|
|
/// <param name="action">The Action that will not trigger a failure.</param>
|
|
/// <param name="cleanup">Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
|
|
/// <param name="attempts">Optional. Number of times the test should be executed if it's failing.</param>
|
|
public static void ExpectNoError(FileSystemWatcher watcher, Action action, Action cleanup, int attempts = DefaultAttemptsForUnExpectedEvent)
|
|
{
|
|
string message = string.Format("Should not observe an error event within {0}ms. Attempted {1} times and received the event each time.", WaitForExpectedEventTimeout, attempts);
|
|
Assert.False(TryErrorEvent(watcher, action, cleanup, attempts, expected: true), message);
|
|
}
|
|
|
|
/// /// <summary>
|
|
/// Helper method for the ExpectError/ExpectNoError functions.
|
|
/// </summary>
|
|
/// <param name="watcher">The FileSystemWatcher to test</param>
|
|
/// <param name="action">The Action to execute.</param>
|
|
/// <param name="cleanup">Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
|
|
/// <param name="attempts">Number of times the test should be executed if it's failing.</param>
|
|
/// <param name="expected">Whether it is expected that an error event will be arisen.</param>
|
|
/// <returns>True if an Error event was raised by the watcher when the given action was executed; else, false.</returns>
|
|
public static bool TryErrorEvent(FileSystemWatcher watcher, Action action, Action cleanup, int attempts, bool expected)
|
|
{
|
|
int attemptsCompleted = 0;
|
|
bool result = !expected;
|
|
while (result != expected && attemptsCompleted++ < attempts)
|
|
{
|
|
if (attemptsCompleted > 1)
|
|
{
|
|
// Re-create the watcher to get a clean iteration.
|
|
watcher = RecreateWatcher(watcher);
|
|
// Most intermittent failures in FSW are caused by either a shortage of resources (e.g. inotify instances)
|
|
// or by insufficient time to execute (e.g. CI gets bogged down). Immediately re-running a failed test
|
|
// won't resolve the first issue, so we wait a little while hoping that things clear up for the next run.
|
|
Thread.Sleep(500);
|
|
}
|
|
|
|
AutoResetEvent errorOccurred = new AutoResetEvent(false);
|
|
watcher.Error += (o, e) =>
|
|
{
|
|
errorOccurred.Set();
|
|
};
|
|
|
|
// Enable raising events but be careful with the possibility of the max user inotify instances being reached already.
|
|
if (attemptsCompleted <= attempts)
|
|
{
|
|
try
|
|
{
|
|
watcher.EnableRaisingEvents = true;
|
|
}
|
|
catch (IOException) // Max User INotify instances. Isn't the type of error we're checking for.
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
watcher.EnableRaisingEvents = true;
|
|
}
|
|
|
|
action();
|
|
result = errorOccurred.WaitOne(WaitForExpectedEventTimeout);
|
|
watcher.EnableRaisingEvents = false;
|
|
cleanup();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public static IEnumerable<object[]> FilterTypes()
|
|
{
|
|
foreach (NotifyFilters filter in Enum.GetValues(typeof(NotifyFilters)))
|
|
yield return new object[] { filter };
|
|
}
|
|
|
|
// Linux and OSX systems have less precise filtering systems than Windows, so most
|
|
// metadata filters are effectively equivalent to each other on those systems. For example
|
|
// there isn't a way to filter only LastWrite events on either system; setting
|
|
// Filters to LastWrite will allow events from attribute change, creation time
|
|
// change, size change, etc.
|
|
public const NotifyFilters LinuxFiltersForAttribute = NotifyFilters.Attributes |
|
|
NotifyFilters.CreationTime |
|
|
NotifyFilters.LastAccess |
|
|
NotifyFilters.LastWrite |
|
|
NotifyFilters.Security |
|
|
NotifyFilters.Size;
|
|
public const NotifyFilters LinuxFiltersForModify = NotifyFilters.LastAccess |
|
|
NotifyFilters.LastWrite |
|
|
NotifyFilters.Security |
|
|
NotifyFilters.Size;
|
|
public const NotifyFilters OSXFiltersForModify = NotifyFilters.Attributes |
|
|
NotifyFilters.CreationTime |
|
|
NotifyFilters.LastAccess |
|
|
NotifyFilters.LastWrite |
|
|
NotifyFilters.Size;
|
|
|
|
private static FileSystemWatcher RecreateWatcher(FileSystemWatcher watcher)
|
|
{
|
|
FileSystemWatcher newWatcher = new FileSystemWatcher()
|
|
{
|
|
IncludeSubdirectories = watcher.IncludeSubdirectories,
|
|
NotifyFilter = watcher.NotifyFilter,
|
|
Path = watcher.Path,
|
|
InternalBufferSize = watcher.InternalBufferSize,
|
|
SynchronizingObject = watcher.SynchronizingObject,
|
|
};
|
|
|
|
foreach (string filter in watcher.Filters)
|
|
{
|
|
newWatcher.Filters.Add(filter);
|
|
}
|
|
|
|
return newWatcher;
|
|
}
|
|
|
|
internal readonly struct FiredEvent
|
|
{
|
|
public FiredEvent(WatcherChangeTypes eventType, string dir1, string dir2 = "") => (EventType, Dir1, Dir2) = (eventType, dir1, dir2);
|
|
|
|
public readonly WatcherChangeTypes EventType;
|
|
public readonly string Dir1;
|
|
public readonly string Dir2;
|
|
|
|
public override bool Equals(object obj) => obj is FiredEvent evt && Equals(evt);
|
|
|
|
public bool Equals(FiredEvent other) => EventType == other.EventType &&
|
|
Dir1 == other.Dir1 &&
|
|
Dir2 == other.Dir2;
|
|
|
|
|
|
public override int GetHashCode() => EventType.GetHashCode() ^ Dir1.GetHashCode() ^ Dir2.GetHashCode();
|
|
|
|
public override string ToString() => $"{EventType} {Dir1} {Dir2}";
|
|
|
|
}
|
|
|
|
// Observe until an expected count of events is triggered, otherwise fail. Return all collected events.
|
|
internal static List<FiredEvent> ExpectEvents(FileSystemWatcher watcher, int expectedEvents, Action action)
|
|
{
|
|
using var eventsOccurred = new AutoResetEvent(false);
|
|
var eventsOrrures = 0;
|
|
|
|
var events = new List<FiredEvent>();
|
|
|
|
ErrorEventArgs error = null;
|
|
|
|
FileSystemEventHandler fileWatcherEvent = (_, e) => AddEvent(e.ChangeType, e.FullPath);
|
|
RenamedEventHandler renameWatcherEvent = (_, e) => AddEvent(e.ChangeType, e.FullPath, e.OldFullPath);
|
|
ErrorEventHandler errorHandler = (_, e) => error ??= e ?? new ErrorEventArgs(null);
|
|
|
|
watcher.Changed += fileWatcherEvent;
|
|
watcher.Created += fileWatcherEvent;
|
|
watcher.Deleted += fileWatcherEvent;
|
|
watcher.Renamed += renameWatcherEvent;
|
|
watcher.Error += errorHandler;
|
|
|
|
bool raisingEvent = watcher.EnableRaisingEvents;
|
|
watcher.EnableRaisingEvents = true;
|
|
|
|
try
|
|
{
|
|
action();
|
|
eventsOccurred.WaitOne(new TimeSpan(0, 0, 5));
|
|
}
|
|
finally
|
|
{
|
|
watcher.Changed -= fileWatcherEvent;
|
|
watcher.Created -= fileWatcherEvent;
|
|
watcher.Deleted -= fileWatcherEvent;
|
|
watcher.Renamed -= renameWatcherEvent;
|
|
watcher.Error -= errorHandler;
|
|
watcher.EnableRaisingEvents = raisingEvent;
|
|
}
|
|
|
|
if (error != null)
|
|
{
|
|
Assert.Fail($"Filewatcher error event triggered: { error.GetException()?.Message ?? "Unknow error" }");
|
|
}
|
|
|
|
return events;
|
|
|
|
void AddEvent(WatcherChangeTypes eventType, string dir1, string dir2 = "")
|
|
{
|
|
events.Add(new FiredEvent(eventType, dir1, dir2));
|
|
if (Interlocked.Increment(ref eventsOrrures) == expectedEvents)
|
|
{
|
|
eventsOccurred.Set();
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class TestISynchronizeInvoke : ISynchronizeInvoke
|
|
{
|
|
public bool BeginInvoke_Called;
|
|
public Delegate ExpectedDelegate;
|
|
|
|
public IAsyncResult BeginInvoke(Delegate method, object[] args)
|
|
{
|
|
if (ExpectedDelegate != null)
|
|
Assert.Equal(ExpectedDelegate, method);
|
|
|
|
BeginInvoke_Called = true;
|
|
method.DynamicInvoke(args[0], args[1]);
|
|
return null;
|
|
}
|
|
|
|
public bool InvokeRequired => true;
|
|
public object EndInvoke(IAsyncResult result) => null;
|
|
public object Invoke(Delegate method, object[] args) => null;
|
|
}
|
|
}
|
|
}
|