dotnet_runtime/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs

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;
}
}
}