Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <inheritdoc cref="object.ToString()" />
public override string ToString()
=> $"{ChangeType} ({FileSystemType}) {Path} [{NotifyFilters}]";
Expand Down
84 changes: 76 additions & 8 deletions Source/Testably.Abstractions.Testing/FileSystem/ChangeHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using Testably.Abstractions.Testing.Storage;
Expand All @@ -23,13 +23,24 @@ private readonly Notification.INotificationFactory<ChangeDescription>

private readonly MockFileSystem _mockFileSystem;

private readonly Notification.INotificationFactory<ChangeDescription>
_watcherNotificationTriggeredCallbacks = Notification.CreateFactory<ChangeDescription>();
private readonly List<WatcherChangeDescription>? _watcherHistory;
#if NET9_0_OR_GREATER
private readonly System.Threading.Lock _watcherHistoryLock = new();
#else
private readonly object _watcherHistoryLock = new();
#endif

private readonly Notification.INotificationFactory<WatcherChangeDescription>
_watcherNotificationTriggeredCallbacks =
Notification.CreateFactory<WatcherChangeDescription>();

public ChangeHandler(MockFileSystem mockFileSystem, bool recordNotificationHistory)
{
_mockFileSystem = mockFileSystem;
_history = recordNotificationHistory ? new List<ChangeDescription>() : null;
_watcherHistory = recordNotificationHistory
? new List<WatcherChangeDescription>()
: null;
}

#region IInterceptionHandler Members
Expand Down Expand Up @@ -97,11 +108,50 @@ public IAwaitableCallback<ChangeDescription> OnEventOrReplay(
#region IWatcherTriggeredHandler Members

/// <inheritdoc cref="IWatcherTriggeredHandler.OnTriggered" />
public IAwaitableCallback<ChangeDescription> OnTriggered(
Action<ChangeDescription>? triggerCallback = null,
Func<ChangeDescription, bool>? predicate = null)
public IAwaitableCallback<WatcherChangeDescription> OnTriggered(
Action<WatcherChangeDescription>? triggerCallback = null,
Func<WatcherChangeDescription, bool>? predicate = null)
=> _watcherNotificationTriggeredCallbacks.RegisterCallback(triggerCallback, predicate);

/// <inheritdoc cref="IWatcherTriggeredHandler.OnTriggeredOrReplay" />
public IAwaitableCallback<WatcherChangeDescription> OnTriggeredOrReplay(
Action<WatcherChangeDescription>? triggerCallback = null,
Func<WatcherChangeDescription, bool>? 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<WatcherChangeDescription> 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)
Expand Down Expand Up @@ -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<WatcherChangeDescription> invoke;
lock (_watcherHistoryLock)
{
_watcherHistory.Add(watcherChange);
invoke = _watcherNotificationTriggeredCallbacks.SnapshotInvocations();
}

invoke(watcherChange);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ private void NotifyChange(ChangeDescription item)
TriggerRenameNotification(item);
}

_fileSystem.ChangeHandler.NotifyWatcherTriggeredChange(item);
_fileSystem.ChangeHandler.NotifyWatcherTriggeredChange(item, this);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;

namespace Testably.Abstractions.Testing.FileSystem;

Expand All @@ -16,8 +16,31 @@ public interface IWatcherTriggeredHandler : IFileSystemEntity
/// (optional) A predicate used to filter which callbacks should be notified.<br />
/// If set to <see langword="null" /> (default value) all callbacks are notified.
/// </param>
/// <returns>An <see cref="IAwaitableCallback{ChangeDescription}" /> to un-register the callback on dispose.</returns>
IAwaitableCallback<ChangeDescription> OnTriggered(
Action<ChangeDescription>? triggerCallback = null,
Func<ChangeDescription, bool>? predicate = null);
/// <returns>An <see cref="IAwaitableCallback{WatcherChangeDescription}" /> to un-register the callback on dispose.</returns>
IAwaitableCallback<WatcherChangeDescription> OnTriggered(
Action<WatcherChangeDescription>? triggerCallback = null,
Func<WatcherChangeDescription, bool>? predicate = null);
Comment thread
vbreuss marked this conversation as resolved.

/// <summary>
/// Like <see cref="OnTriggered" />, but the returned callback also replays any matching
/// watcher-triggered notifications that occurred before this call, in their original order.
/// <para />
/// Notification history is enabled by default. If it has been disabled via
/// <see cref="MockFileSystem.MockFileSystemOptions.WithoutNotificationHistory" />, calling
/// this method throws <see cref="InvalidOperationException" />; use <see cref="OnTriggered" />
/// instead in that case.
/// </summary>
/// <param name="triggerCallback">(optional) The callback to execute for each replayed and future notification.</param>
/// <param name="predicate">
/// (optional) A predicate used to filter which notifications are replayed and which future notifications notify the callback.<br />
/// If set to <see langword="null" /> (default value) all notifications are considered.
/// </param>
/// <returns>An <see cref="IAwaitableCallback{WatcherChangeDescription}" /> to un-register the callback on dispose.</returns>
/// <exception cref="InvalidOperationException">
/// Notification history was disabled via
/// <see cref="MockFileSystem.MockFileSystemOptions.WithoutNotificationHistory" />.
/// </exception>
IAwaitableCallback<WatcherChangeDescription> OnTriggeredOrReplay(
Action<WatcherChangeDescription>? triggerCallback = null,
Func<WatcherChangeDescription, bool>? predicate = null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Testably.Abstractions.Testing.FileSystem;

/// <summary>
/// Describes a change in the <see cref="MockFileSystem" /> that was emitted by a specific
/// <see cref="IFileSystemWatcher" />. The <see cref="FileSystemWatcher" /> property allows
/// consumers to filter watcher-triggered notifications by the emitting watcher instance.
/// </summary>
public sealed class WatcherChangeDescription : ChangeDescription
{
/// <summary>
/// The <see cref="IFileSystemWatcher" /> that emitted this change notification.
/// </summary>
public IFileSystemWatcher FileSystemWatcher { get; }

internal WatcherChangeDescription(ChangeDescription source, IFileSystemWatcher fileSystemWatcher)
: base(source)
{
FileSystemWatcher = fileSystemWatcher;
}
}
11 changes: 7 additions & 4 deletions Source/Testably.Abstractions.Testing/MockFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ public class MockFileSystemOptions

/// <summary>
/// Whether to record a history of completed change notifications so that
/// <see cref="INotificationHandler.OnEventOrReplay" /> can replay past events.
/// <see cref="INotificationHandler.OnEventOrReplay" /> and
/// <see cref="IWatcherTriggeredHandler.OnTriggeredOrReplay" /> can replay past events.
/// </summary>
internal bool RecordNotificationHistory { get; private set; } = true;

Expand Down Expand Up @@ -378,10 +379,12 @@ public MockFileSystemOptions UseTimeSystem(ITimeSystem timeSystem)
}

/// <summary>
/// Disables recording the change notification history. With history disabled,
/// <see cref="INotificationHandler.OnEventOrReplay" /> throws
/// Disables recording the change notification history for both the notify and
/// watcher-triggered streams. With history disabled,
/// <see cref="INotificationHandler.OnEventOrReplay" /> and
/// <see cref="IWatcherTriggeredHandler.OnTriggeredOrReplay" /> throw
/// <see cref="InvalidOperationException" />; use <see cref="INotificationHandler.OnEvent" />
/// instead.
/// or <see cref="IWatcherTriggeredHandler.OnTriggered" /> instead.
/// </summary>
public MockFileSystemOptions WithoutNotificationHistory()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ namespace Testably.Abstractions.Testing.FileSystem
}
public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity
{
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.ChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.ChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.ChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggeredOrReplay(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
}
public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy
{
Expand All @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ namespace Testably.Abstractions.Testing.FileSystem
}
public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity
{
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.ChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.ChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.ChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggeredOrReplay(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
}
public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy
{
Expand All @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ namespace Testably.Abstractions.Testing.FileSystem
}
public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity
{
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.ChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.ChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.ChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggeredOrReplay(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
}
public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy
{
Expand All @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ namespace Testably.Abstractions.Testing.FileSystem
}
public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity
{
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.ChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.ChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.ChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggeredOrReplay(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
}
public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy
{
Expand All @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ namespace Testably.Abstractions.Testing.FileSystem
}
public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity
{
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.ChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.ChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.ChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggeredOrReplay(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
}
public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy
{
Expand All @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ namespace Testably.Abstractions.Testing.FileSystem
}
public interface IWatcherTriggeredHandler : System.IO.Abstractions.IFileSystemEntity
{
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.ChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.ChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.ChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggered(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription> OnTriggeredOrReplay(System.Action<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription>? triggerCallback = null, System.Func<Testably.Abstractions.Testing.FileSystem.WatcherChangeDescription, bool>? predicate = null);
}
public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy
{
Expand All @@ -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
{
Expand Down
Loading
Loading