diff --git a/Source/Testably.Abstractions.Testing/FileSystem/ChangeDescription.cs b/Source/Testably.Abstractions.Testing/FileSystem/ChangeDescription.cs index df654a10..675eb0e4 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/ChangeDescription.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/ChangeDescription.cs @@ -58,6 +58,17 @@ internal ChangeDescription(WatcherChangeTypes changeType, NotifyFilters = notifyFilters; } + internal ChangeDescription(ChangeDescription source) + { + Path = source.Path; + Name = source.Name; + OldPath = source.OldPath; + OldName = source.OldName; + ChangeType = source.ChangeType; + FileSystemType = source.FileSystemType; + NotifyFilters = source.NotifyFilters; + } + /// public override string ToString() => $"{ChangeType} ({FileSystemType}) {Path} [{NotifyFilters}]"; diff --git a/Source/Testably.Abstractions.Testing/FileSystem/ChangeHandler.cs b/Source/Testably.Abstractions.Testing/FileSystem/ChangeHandler.cs index cc737908..db120c4a 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/ChangeHandler.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/ChangeHandler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using Testably.Abstractions.Testing.Storage; @@ -23,13 +23,24 @@ private readonly Notification.INotificationFactory private readonly MockFileSystem _mockFileSystem; - private readonly Notification.INotificationFactory - _watcherNotificationTriggeredCallbacks = Notification.CreateFactory(); + private readonly List? _watcherHistory; +#if NET9_0_OR_GREATER + private readonly System.Threading.Lock _watcherHistoryLock = new(); +#else + private readonly object _watcherHistoryLock = new(); +#endif + + private readonly Notification.INotificationFactory + _watcherNotificationTriggeredCallbacks = + Notification.CreateFactory(); public ChangeHandler(MockFileSystem mockFileSystem, bool recordNotificationHistory) { _mockFileSystem = mockFileSystem; _history = recordNotificationHistory ? new List() : null; + _watcherHistory = recordNotificationHistory + ? new List() + : null; } #region IInterceptionHandler Members @@ -97,11 +108,50 @@ public IAwaitableCallback OnEventOrReplay( #region IWatcherTriggeredHandler Members /// - public IAwaitableCallback OnTriggered( - Action? triggerCallback = null, - Func? predicate = null) + public IAwaitableCallback OnTriggered( + Action? triggerCallback = null, + Func? predicate = null) => _watcherNotificationTriggeredCallbacks.RegisterCallback(triggerCallback, predicate); + /// + public IAwaitableCallback OnTriggeredOrReplay( + Action? triggerCallback = null, + Func? predicate = null) + { + if (_watcherHistory is null) + { + throw new InvalidOperationException( + $"{nameof(OnTriggeredOrReplay)} requires notification history, but it was disabled via " + + $"{nameof(MockFileSystem.MockFileSystemOptions)}." + + $"{nameof(MockFileSystem.MockFileSystemOptions.WithoutNotificationHistory)}. " + + $"Use {nameof(OnTriggered)} instead, or remove the opt-out."); + } + + IAwaitableCallback waiter; + WatcherChangeDescription[] snapshot; + lock (_watcherHistoryLock) + { + waiter = + _watcherNotificationTriggeredCallbacks.RegisterCallback(triggerCallback, predicate); + snapshot = _watcherHistory.ToArray(); + } + + try + { + foreach (WatcherChangeDescription past in snapshot) + { + _watcherNotificationTriggeredCallbacks.Replay(waiter, past); + } + } + catch + { + waiter.Dispose(); + throw; + } + + return waiter; + } + #endregion internal void NotifyCompletedChange(ChangeDescription? fileSystemChange) @@ -140,6 +190,24 @@ internal ChangeDescription NotifyPendingChange(WatcherChangeTypes changeType, return fileSystemChange; } - internal void NotifyWatcherTriggeredChange(ChangeDescription fileSystemChange) - => _watcherNotificationTriggeredCallbacks.InvokeCallbacks(fileSystemChange); + internal void NotifyWatcherTriggeredChange(ChangeDescription fileSystemChange, + IFileSystemWatcher watcher) + { + WatcherChangeDescription watcherChange = new(fileSystemChange, watcher); + + if (_watcherHistory is null) + { + _watcherNotificationTriggeredCallbacks.InvokeCallbacks(watcherChange); + return; + } + + Action invoke; + lock (_watcherHistoryLock) + { + _watcherHistory.Add(watcherChange); + invoke = _watcherNotificationTriggeredCallbacks.SnapshotInvocations(); + } + + invoke(watcherChange); + } } diff --git a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs index 33068dec..1775afd2 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/FileSystemWatcherMock.cs @@ -528,7 +528,7 @@ private void NotifyChange(ChangeDescription item) TriggerRenameNotification(item); } - _fileSystem.ChangeHandler.NotifyWatcherTriggeredChange(item); + _fileSystem.ChangeHandler.NotifyWatcherTriggeredChange(item, this); } } diff --git a/Source/Testably.Abstractions.Testing/FileSystem/IWatcherTriggeredHandler.cs b/Source/Testably.Abstractions.Testing/FileSystem/IWatcherTriggeredHandler.cs index a155f99b..ba66b815 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/IWatcherTriggeredHandler.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/IWatcherTriggeredHandler.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Testably.Abstractions.Testing.FileSystem; @@ -16,8 +16,31 @@ public interface IWatcherTriggeredHandler : IFileSystemEntity /// (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); + /// An to un-register the callback on dispose. + IAwaitableCallback OnTriggered( + Action? triggerCallback = null, + Func? predicate = null); + + /// + /// Like , but the returned callback also replays any matching + /// watcher-triggered notifications that occurred before this call, in their original order. + /// + /// Notification history is enabled by default. If it has been disabled via + /// , calling + /// this method throws ; use + /// instead in that case. + /// + /// (optional) The callback to execute for each replayed and future notification. + /// + /// (optional) A predicate used to filter which notifications are replayed and which future notifications notify the callback.
+ /// If set to (default value) all notifications are considered. + /// + /// An to un-register the callback on dispose. + /// + /// Notification history was disabled via + /// . + /// + IAwaitableCallback OnTriggeredOrReplay( + Action? triggerCallback = null, + Func? predicate = null); } diff --git a/Source/Testably.Abstractions.Testing/FileSystem/WatcherChangeDescription.cs b/Source/Testably.Abstractions.Testing/FileSystem/WatcherChangeDescription.cs new file mode 100644 index 00000000..3f216a9d --- /dev/null +++ b/Source/Testably.Abstractions.Testing/FileSystem/WatcherChangeDescription.cs @@ -0,0 +1,20 @@ +namespace Testably.Abstractions.Testing.FileSystem; + +/// +/// Describes a change in the that was emitted by a specific +/// . The property allows +/// consumers to filter watcher-triggered notifications by the emitting watcher instance. +/// +public sealed class WatcherChangeDescription : ChangeDescription +{ + /// + /// The that emitted this change notification. + /// + public IFileSystemWatcher FileSystemWatcher { get; } + + internal WatcherChangeDescription(ChangeDescription source, IFileSystemWatcher fileSystemWatcher) + : base(source) + { + FileSystemWatcher = fileSystemWatcher; + } +} diff --git a/Source/Testably.Abstractions.Testing/MockFileSystem.cs b/Source/Testably.Abstractions.Testing/MockFileSystem.cs index 0e4a91e6..36bb8dee 100644 --- a/Source/Testably.Abstractions.Testing/MockFileSystem.cs +++ b/Source/Testably.Abstractions.Testing/MockFileSystem.cs @@ -316,7 +316,8 @@ public class MockFileSystemOptions /// /// Whether to record a history of completed change notifications so that - /// can replay past events. + /// and + /// can replay past events. /// internal bool RecordNotificationHistory { get; private set; } = true; @@ -378,10 +379,12 @@ public MockFileSystemOptions UseTimeSystem(ITimeSystem timeSystem) } /// - /// Disables recording the change notification history. With history disabled, - /// throws + /// Disables recording the change notification history for both the notify and + /// watcher-triggered streams. With history disabled, + /// and + /// throw /// ; use - /// instead. + /// or instead. /// public MockFileSystemOptions WithoutNotificationHistory() { 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 eddfa063..e2af3e6b 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 @@ -231,7 +231,8 @@ namespace Testably.Abstractions.Testing.FileSystem } public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity { - Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggeredOrReplay(System.Action? triggerCallback = null, System.Func? predicate = null); } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { @@ -257,6 +258,10 @@ namespace Testably.Abstractions.Testing.FileSystem public string Path { get; } public System.IO.FileShare Share { get; } } + public sealed class WatcherChangeDescription : Testably.Abstractions.Testing.FileSystem.ChangeDescription + { + public System.IO.Abstractions.IFileSystemWatcher FileSystemWatcher { get; } + } } namespace Testably.Abstractions.Testing.Initializer { 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 07ce3eb9..fa6ea6d7 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 @@ -224,7 +224,8 @@ namespace Testably.Abstractions.Testing.FileSystem } public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity { - Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggeredOrReplay(System.Action? triggerCallback = null, System.Func? predicate = null); } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { @@ -244,6 +245,10 @@ namespace Testably.Abstractions.Testing.FileSystem public string Path { get; } public System.IO.FileShare Share { get; } } + public sealed class WatcherChangeDescription : Testably.Abstractions.Testing.FileSystem.ChangeDescription + { + public System.IO.Abstractions.IFileSystemWatcher FileSystemWatcher { get; } + } } namespace Testably.Abstractions.Testing.Initializer { 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 d68c6252..fc9b2984 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 @@ -231,7 +231,8 @@ namespace Testably.Abstractions.Testing.FileSystem } public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity { - Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggeredOrReplay(System.Action? triggerCallback = null, System.Func? predicate = null); } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { @@ -257,6 +258,10 @@ namespace Testably.Abstractions.Testing.FileSystem public string Path { get; } public System.IO.FileShare Share { get; } } + public sealed class WatcherChangeDescription : Testably.Abstractions.Testing.FileSystem.ChangeDescription + { + public System.IO.Abstractions.IFileSystemWatcher FileSystemWatcher { get; } + } } namespace Testably.Abstractions.Testing.Initializer { 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 147f89e5..c724e81b 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 @@ -231,7 +231,8 @@ namespace Testably.Abstractions.Testing.FileSystem } public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity { - Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggeredOrReplay(System.Action? triggerCallback = null, System.Func? predicate = null); } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { @@ -257,6 +258,10 @@ namespace Testably.Abstractions.Testing.FileSystem public string Path { get; } public System.IO.FileShare Share { get; } } + public sealed class WatcherChangeDescription : Testably.Abstractions.Testing.FileSystem.ChangeDescription + { + public System.IO.Abstractions.IFileSystemWatcher FileSystemWatcher { get; } + } } namespace Testably.Abstractions.Testing.Initializer { 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 16b86b69..21b4bd4b 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 @@ -218,7 +218,8 @@ namespace Testably.Abstractions.Testing.FileSystem } public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity { - Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggeredOrReplay(System.Action? triggerCallback = null, System.Func? predicate = null); } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { @@ -237,6 +238,10 @@ namespace Testably.Abstractions.Testing.FileSystem public string Path { get; } public System.IO.FileShare Share { get; } } + public sealed class WatcherChangeDescription : Testably.Abstractions.Testing.FileSystem.ChangeDescription + { + public System.IO.Abstractions.IFileSystemWatcher FileSystemWatcher { get; } + } } namespace Testably.Abstractions.Testing.Initializer { 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 46c01a2f..e1953db2 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 @@ -218,7 +218,8 @@ namespace Testably.Abstractions.Testing.FileSystem } public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity { - Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggered(System.Action? triggerCallback = null, System.Func? predicate = null); + Testably.Abstractions.Testing.IAwaitableCallback OnTriggeredOrReplay(System.Action? triggerCallback = null, System.Func? predicate = null); } public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy { @@ -237,6 +238,10 @@ namespace Testably.Abstractions.Testing.FileSystem public string Path { get; } public System.IO.FileShare Share { get; } } + public sealed class WatcherChangeDescription : Testably.Abstractions.Testing.FileSystem.ChangeDescription + { + public System.IO.Abstractions.IFileSystemWatcher FileSystemWatcher { get; } + } } namespace Testably.Abstractions.Testing.Initializer { diff --git a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs index 5d33757a..4c38168d 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs @@ -94,7 +94,8 @@ public async Task ExecuteCallback_ShouldTriggerNotification( } public static - IEnumerable<(Action?, Action, WatcherChangeTypes, FileSystemTypes, string)> NotificationTriggeringMethods() + IEnumerable<(Action?, Action, WatcherChangeTypes, + FileSystemTypes, string)> NotificationTriggeringMethods() { yield return (null, (f, p) => f.Directory.CreateDirectory(p), WatcherChangeTypes.Created, FileSystemTypes.Directory, $"path_{Guid.NewGuid()}"); @@ -106,6 +107,21 @@ public static WatcherChangeTypes.Deleted, FileSystemTypes.File, $"path_{Guid.NewGuid()}"); } + [Test] + [AutoArguments] + public async Task OnEventOrReplay_CallbackThrowsDuringReplay_ShouldNotLeakWaiter(string path) + { + FileSystem.File.WriteAllText(path, null); + + void Subscribe() + => FileSystem.Notify.OnEventOrReplay(_ => throw new InvalidOperationException("boom")); + + await That(Subscribe).Throws().WithMessage("boom"); + + void TriggerAnotherChange() => FileSystem.File.WriteAllText(path, "more"); + await That(TriggerAnotherChange).DoesNotThrow(); + } + [Test] [AutoArguments] public async Task OnEventOrReplay_ShouldAlsoReceiveFutureEvents(string path1, string path2) @@ -168,18 +184,107 @@ await That(Act).Throws() } [Test] - [AutoArguments] - public async Task OnEventOrReplay_CallbackThrowsDuringReplay_ShouldNotLeakWaiter(string path) + public async Task OnTriggeredOrReplay_ShouldAlsoReceiveFutureEmissions() { - FileSystem.File.WriteAllText(path, null); + FileSystem.InitializeIn("."); + IFileSystemWatcher watcher = FileSystem.FileSystemWatcher.New("."); + watcher.EnableRaisingEvents = true; - void Subscribe() => FileSystem.Notify.OnEventOrReplay( - _ => throw new InvalidOperationException("boom")); + using IAwaitableCallback warmup = + FileSystem.Watcher.OnTriggered(); + FileSystem.File.WriteAllText("foo.txt", "some-text"); + warmup.Wait(timeout: 30000); - await That(Subscribe).Throws().WithMessage("boom"); + using IAwaitableCallback onEvent = FileSystem.Watcher + .OnTriggeredOrReplay(predicate: c => c.ChangeType == WatcherChangeTypes.Created); - void TriggerAnotherChange() => FileSystem.File.WriteAllText(path, "more"); - await That(TriggerAnotherChange).DoesNotThrow(); + FileSystem.File.WriteAllText("bar.txt", "more-text"); + + WatcherChangeDescription[] received = await onEvent.WaitAsync( + count: 2, + timeout: TimeSpan.FromSeconds(5)); + + await That(received.Length).IsEqualTo(2); + } + + [Test] + public async Task OnTriggeredOrReplay_ShouldFilterByEmittingWatcher() + { + FileSystem.InitializeIn("."); + IFileSystemWatcher watcher1 = FileSystem.FileSystemWatcher.New("."); + IFileSystemWatcher watcher2 = FileSystem.FileSystemWatcher.New("."); + watcher1.EnableRaisingEvents = true; + watcher2.EnableRaisingEvents = true; + + using IAwaitableCallback warmup = + FileSystem.Watcher.OnTriggered(); + FileSystem.File.WriteAllText("foo.txt", "some-text"); + warmup.Wait(count: 2, timeout: TimeSpan.FromSeconds(30)); + + using IAwaitableCallback onEvent = FileSystem.Watcher + .OnTriggeredOrReplay(predicate: c => + ReferenceEquals(c.FileSystemWatcher, watcher1)); + + WatcherChangeDescription[] replayed = + await onEvent.WaitAsync(timeout: TimeSpan.FromSeconds(5)); + + await That(replayed.Length).IsEqualTo(1); + await That(replayed[0].FileSystemWatcher).IsSameAs(watcher1); + } + + [Test] + public async Task OnTriggeredOrReplay_ShouldNotReplayEmissionsFilteredOutByPredicate() + { + FileSystem.InitializeIn("."); + IFileSystemWatcher watcher = FileSystem.FileSystemWatcher.New("."); + watcher.EnableRaisingEvents = true; + + using IAwaitableCallback warmup = + FileSystem.Watcher.OnTriggered(); + FileSystem.File.WriteAllText("foo.txt", "some-text"); + warmup.Wait(timeout: 30000); + + using IAwaitableCallback onEvent = FileSystem.Watcher + .OnTriggeredOrReplay(predicate: c => c.ChangeType == WatcherChangeTypes.Deleted); + + void Act() => + // ReSharper disable once AccessToDisposedClosure + onEvent.Wait(timeout: 50); + + await That(Act).Throws(); + } + + [Test] + public async Task OnTriggeredOrReplay_ShouldReplayPriorMatchingEmissions() + { + FileSystem.InitializeIn("."); + IFileSystemWatcher watcher = FileSystem.FileSystemWatcher.New("."); + watcher.EnableRaisingEvents = true; + + using IAwaitableCallback warmup = + FileSystem.Watcher.OnTriggered(); + FileSystem.File.WriteAllText("foo.txt", "some-text"); + warmup.Wait(timeout: 30000); + + using IAwaitableCallback onEvent = FileSystem.Watcher + .OnTriggeredOrReplay(predicate: c => c.ChangeType == WatcherChangeTypes.Created); + + WatcherChangeDescription[] replayed = + await onEvent.WaitAsync(timeout: TimeSpan.FromSeconds(5)); + + await That(replayed.Length).IsEqualTo(1); + await That(replayed[0].Path).IsEqualTo(FileSystem.Path.GetFullPath("foo.txt")); + } + + [Test] + public async Task OnTriggeredOrReplay_WithoutNotificationHistory_ShouldThrow() + { + MockFileSystem fileSystem = new(o => o.WithoutNotificationHistory()); + + void Act() => fileSystem.Watcher.OnTriggeredOrReplay(); + + await That(Act).Throws() + .WithMessage("*WithoutNotificationHistory*").AsWildcard(); } [Test] @@ -189,7 +294,8 @@ public async Task Watcher_ShouldNotTriggerWhenFileSystemWatcherDoesNotMatch() IFileSystemWatcher watcher = FileSystem.FileSystemWatcher.New("bar"); watcher.EnableRaisingEvents = true; - using IAwaitableCallback onEvent = FileSystem.Watcher.OnTriggered(); + using IAwaitableCallback onEvent = + FileSystem.Watcher.OnTriggered(); FileSystem.File.WriteAllText(@"foo.txt", "some-text"); @@ -209,7 +315,8 @@ public async Task Watcher_ShouldTriggerWhenFileSystemWatcherSendsNotification() watcher.Created += (_, _) => isTriggered = true; watcher.EnableRaisingEvents = true; - using IAwaitableCallback onEvent = FileSystem.Watcher.OnTriggered(); + using IAwaitableCallback onEvent = + FileSystem.Watcher.OnTriggered(); FileSystem.File.WriteAllText(@"foo.txt", "some-text"); onEvent.Wait(timeout: 30000);