diff --git a/Source/Testably.Abstractions.Testing/FileSystem/ChangeHandler.cs b/Source/Testably.Abstractions.Testing/FileSystem/ChangeHandler.cs index 3edcf4de..10f856ed 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/ChangeHandler.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/ChangeHandler.cs @@ -4,8 +4,8 @@ namespace Testably.Abstractions.Testing.FileSystem; -internal sealed class ChangeHandler : IInterceptionHandler, - INotificationHandler +internal sealed class ChangeHandler + : IInterceptionHandler, INotificationHandler, IWatcherTriggeredHandler { private readonly Notification.INotificationFactory _changeOccurredCallbacks = Notification.CreateFactory(); @@ -15,6 +15,9 @@ private readonly Notification.INotificationFactory private readonly MockFileSystem _mockFileSystem; + private readonly Notification.INotificationFactory + _watcherNotificationTriggeredCallbacks = Notification.CreateFactory(); + public ChangeHandler(MockFileSystem mockFileSystem) { _mockFileSystem = mockFileSystem; @@ -25,8 +28,7 @@ public ChangeHandler(MockFileSystem mockFileSystem) /// public IFileSystem FileSystem => _mockFileSystem; - /// + /// public IAwaitableCallback Event( Action interceptionCallback, Func? predicate = null) @@ -36,8 +38,7 @@ public IAwaitableCallback Event( #region INotificationHandler Members - /// + /// public IAwaitableCallback OnEvent( Action? notificationCallback = null, Func? predicate = null) @@ -45,6 +46,16 @@ public IAwaitableCallback OnEvent( #endregion + #region IWatcherTriggeredHandler Members + + /// + public IAwaitableCallback OnTriggered( + Action? triggerCallback = null, + Func? predicate = null) + => _watcherNotificationTriggeredCallbacks.RegisterCallback(triggerCallback, predicate); + + #endregion + internal void NotifyCompletedChange(ChangeDescription? fileSystemChange) { if (fileSystemChange != null) @@ -65,4 +76,7 @@ internal ChangeDescription NotifyPendingChange(WatcherChangeTypes changeType, _changeOccurringCallbacks.InvokeCallbacks(fileSystemChange); return fileSystemChange; } + + internal void NotifyWatcherTriggeredChange(ChangeDescription fileSystemChange) + => _watcherNotificationTriggeredCallbacks.InvokeCallbacks(fileSystemChange); } diff --git a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs index e5ea8974..5b5e70d3 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs @@ -446,6 +446,8 @@ private void NotifyChange(ChangeDescription item) { TriggerRenameNotification(item); } + + _fileSystem.ChangeHandler.NotifyWatcherTriggeredChange(item); } } @@ -664,7 +666,8 @@ private void TriggerMacRenameNotification(ChangeDescription item, RenamedContext { CheckRenamePremise(context); - if (context.ComesFromInside && TryMakeRenamedEventArgs(item, out RenamedEventArgs? eventArgs)) + if (context.ComesFromInside && + TryMakeRenamedEventArgs(item, out RenamedEventArgs? eventArgs)) { Renamed?.Invoke(this, eventArgs); return; diff --git a/Source/Testably.Abstractions.Testing/FileSystem/IWatcherTriggeredHandler.cs b/Source/Testably.Abstractions.Testing/FileSystem/IWatcherTriggeredHandler.cs new file mode 100644 index 00000000..a155f99b --- /dev/null +++ b/Source/Testably.Abstractions.Testing/FileSystem/IWatcherTriggeredHandler.cs @@ -0,0 +1,23 @@ +using System; + +namespace Testably.Abstractions.Testing.FileSystem; + +/// +/// The notification handler for triggered notifications from the . +/// +public interface IWatcherTriggeredHandler : IFileSystemEntity +{ + /// + /// Callback executed after the notified about a change in the + /// matching the . + /// + /// The callback to execute after the notification was triggered. + /// + /// (optional) A predicate used to filter which callbacks should be notified.
+ /// If set to (default value) all callbacks are notified. + /// + /// An to un-register the callback on dispose. + IAwaitableCallback OnTriggered( + Action? triggerCallback = null, + Func? predicate = null); +} diff --git a/Source/Testably.Abstractions.Testing/MockFileSystem.cs b/Source/Testably.Abstractions.Testing/MockFileSystem.cs index 3d5482ea..57c5697e 100644 --- a/Source/Testably.Abstractions.Testing/MockFileSystem.cs +++ b/Source/Testably.Abstractions.Testing/MockFileSystem.cs @@ -26,6 +26,11 @@ public sealed class MockFileSystem : IFileSystem /// public INotificationHandler Notify => ChangeHandler; + /// + /// Get notified of events after they were triggered by a . + /// + public IWatcherTriggeredHandler Watcher => ChangeHandler; + /// /// The used random system. /// @@ -223,7 +228,8 @@ public MockFileSystem WithDrive(string? drive, Action? driveCallback = null) { IStorageDrive driveInfoMock = - drive == null || string.Equals(drive, Execute.Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) + drive == null || string.Equals(drive, + Execute.Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) ? Storage.MainDrive : Storage.GetOrAddDrive(drive); driveCallback?.Invoke(driveInfoMock); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt index a8541142..1158dfa7 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt @@ -47,6 +47,10 @@ namespace Testably.Abstractions.Testing.FileSystem bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode, System.IO.FileAccess requestedAccess); void OnSetUnixFileMode(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode); } + public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { public NullAccessControlStrategy() { } @@ -124,6 +128,7 @@ namespace Testably.Abstractions.Testing public Testably.Abstractions.Testing.SimulationMode SimulationMode { get; } public Testably.Abstractions.Testing.Statistics.IFileSystemStatistics Statistics { get; } public Testably.Abstractions.ITimeSystem TimeSystem { get; } + public Testably.Abstractions.Testing.FileSystem.IWatcherTriggeredHandler Watcher { get; } public override string ToString() { } public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { } public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action? driveCallback = null) { } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt index ad1ec8f2..9081cbde 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt @@ -42,6 +42,10 @@ namespace Testably.Abstractions.Testing.FileSystem { Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle); } + public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { public NullAccessControlStrategy() { } @@ -113,6 +117,7 @@ namespace Testably.Abstractions.Testing public Testably.Abstractions.Testing.SimulationMode SimulationMode { get; } public Testably.Abstractions.Testing.Statistics.IFileSystemStatistics Statistics { get; } public Testably.Abstractions.ITimeSystem TimeSystem { get; } + public Testably.Abstractions.Testing.FileSystem.IWatcherTriggeredHandler Watcher { get; } public override string ToString() { } public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { } public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action? driveCallback = null) { } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt index 67d657c2..2164c13d 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt @@ -47,6 +47,10 @@ namespace Testably.Abstractions.Testing.FileSystem bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode, System.IO.FileAccess requestedAccess); void OnSetUnixFileMode(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode); } + public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { public NullAccessControlStrategy() { } @@ -124,6 +128,7 @@ namespace Testably.Abstractions.Testing public Testably.Abstractions.Testing.SimulationMode SimulationMode { get; } public Testably.Abstractions.Testing.Statistics.IFileSystemStatistics Statistics { get; } public Testably.Abstractions.ITimeSystem TimeSystem { get; } + public Testably.Abstractions.Testing.FileSystem.IWatcherTriggeredHandler Watcher { get; } public override string ToString() { } public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { } public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action? driveCallback = null) { } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt index 10c63c1f..c23262e1 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt @@ -47,6 +47,10 @@ namespace Testably.Abstractions.Testing.FileSystem bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode, System.IO.FileAccess requestedAccess); void OnSetUnixFileMode(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode); } + public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { public NullAccessControlStrategy() { } @@ -124,6 +128,7 @@ namespace Testably.Abstractions.Testing public Testably.Abstractions.Testing.SimulationMode SimulationMode { get; } public Testably.Abstractions.Testing.Statistics.IFileSystemStatistics Statistics { get; } public Testably.Abstractions.ITimeSystem TimeSystem { get; } + public Testably.Abstractions.Testing.FileSystem.IWatcherTriggeredHandler Watcher { get; } public override string ToString() { } public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { } public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action? driveCallback = null) { } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt index f9fff1a6..24ffa35d 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt @@ -41,6 +41,10 @@ namespace Testably.Abstractions.Testing.FileSystem { Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle); } + public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { public NullAccessControlStrategy() { } @@ -111,6 +115,7 @@ namespace Testably.Abstractions.Testing public Testably.Abstractions.Testing.SimulationMode SimulationMode { get; } public Testably.Abstractions.Testing.Statistics.IFileSystemStatistics Statistics { get; } public Testably.Abstractions.ITimeSystem TimeSystem { get; } + public Testably.Abstractions.Testing.FileSystem.IWatcherTriggeredHandler Watcher { get; } public override string ToString() { } public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { } public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action? driveCallback = null) { } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt index eb3d5ccd..aeacef40 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt @@ -41,6 +41,10 @@ namespace Testably.Abstractions.Testing.FileSystem { Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle); } + public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { public NullAccessControlStrategy() { } @@ -111,6 +115,7 @@ namespace Testably.Abstractions.Testing public Testably.Abstractions.Testing.SimulationMode SimulationMode { get; } public Testably.Abstractions.Testing.Statistics.IFileSystemStatistics Statistics { get; } public Testably.Abstractions.ITimeSystem TimeSystem { get; } + public Testably.Abstractions.Testing.FileSystem.IWatcherTriggeredHandler Watcher { get; } public override string ToString() { } public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { } public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action? driveCallback = null) { } diff --git a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs index 1a64a913..e2ec3568 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs @@ -1,5 +1,6 @@ using aweXpect.Synchronous; using System.IO; +using Testably.Abstractions.Testing.FileSystem; namespace Testably.Abstractions.Testing.Tests.FileSystem; @@ -98,6 +99,39 @@ public async Task ExecuteCallback_ShouldTriggerNotification( await That(receivedPath).IsEqualTo(FileSystem.Path.GetFullPath(path)); } + [Fact] + public async Task Watcher_ShouldNotTriggerWhenFileSystemWatcherDoesNotMatch() + { + FileSystem.Directory.CreateDirectory("bar"); + IFileSystemWatcher watcher = FileSystem.FileSystemWatcher.New("bar"); + watcher.EnableRaisingEvents = true; + + IAwaitableCallback onEvent = FileSystem.Watcher.OnTriggered(); + + void Act() => + onEvent.Wait(timeout: 100, + executeWhenWaiting: () => FileSystem.File.WriteAllText(@"foo.txt", "some-text")); + + await That(Act).Throws(); + } + + [Fact] + public async Task Watcher_ShouldTriggerWhenFileSystemWatcherSendsNotification() + { + bool isTriggered = false; + FileSystem.InitializeIn("."); + IFileSystemWatcher watcher = FileSystem.FileSystemWatcher.New("."); + watcher.Created += (_, _) => isTriggered = true; + watcher.EnableRaisingEvents = true; + + IAwaitableCallback onEvent = FileSystem.Watcher.OnTriggered(); + + onEvent.Wait(timeout: 5000, + executeWhenWaiting: () => FileSystem.File.WriteAllText(@"foo.txt", "some-text")); + + await That(isTriggered).IsTrue(); + } + #region Helpers public static