diff --git a/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs b/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs new file mode 100644 index 00000000..19097c80 --- /dev/null +++ b/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +#if NET6_0_OR_GREATER +using System.Collections.Generic; +using System.Runtime.CompilerServices; +#endif + +namespace Testably.Abstractions.Testing; + +/// +/// Extension methods on . +/// +public static class AwaitableCallbackExtensions +{ + /// + /// Blocks the current thread until the callback is executed. + /// + /// Throws a if the expired before the callback was + /// executed. + /// + /// The callback. + /// + /// (optional) The number of callbacks to wait.
+ /// Defaults to 1 + /// + /// + /// (optional) The timeout in milliseconds to wait on the callback.
+ /// Defaults to 30000ms (30 seconds). + /// + public static TValue[] Wait(this IAwaitableCallback callback, + int count = 1, + int timeout = 30000) + => callback.Wait(count, TimeSpan.FromMilliseconds(timeout)); + + /// + /// Waits asynchronously until the callback is executed. + /// + /// The callback. + /// + /// (optional) The number of callbacks to wait.
+ /// Defaults to 1 + /// + /// + /// (optional) The timeout in milliseconds to wait on the callback.
+ /// Defaults to 30000ms (30 seconds). + /// + /// + /// (optional) A to cancel waiting.
+ /// Throws a if the token was canceled before the callback was executed. + /// + public static Task WaitAsync(this IAwaitableCallback callback, + int count = 1, + int timeout = 30000, + CancellationToken? cancellationToken = null) + => callback.WaitAsync(count, TimeSpan.FromMilliseconds(timeout), cancellationToken); + +#if NET6_0_OR_GREATER + /// + /// Converts the to an that yields a + /// value each time the callback is executed. + /// + /// + /// Uses a default timeout of 30 seconds to prevent infinite waiting if the callback is never executed. + /// + public static IAsyncEnumerable ToAsyncEnumerable( + this IAwaitableCallback source, + CancellationToken cancellationToken = default) + => ToAsyncEnumerable(source, null, cancellationToken); + + /// + /// Converts the to an that yields a + /// value each time the callback is executed. + /// + /// + /// If no is specified (), a default timeout of 30 seconds is used + /// to prevent infinite waiting if the callback is never executed. + /// + public static async IAsyncEnumerable ToAsyncEnumerable( + this IAwaitableCallback source, + TimeSpan? timeout, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + using CancellationTokenSource cts = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(timeout ?? TimeSpan.FromSeconds(30)); + CancellationToken token = cts.Token; + + while (!token.IsCancellationRequested) + { + TValue item; + try + { + TValue[] items = await source.WaitAsync(cancellationToken: token); + if (items.Length == 0) + { + continue; + } + + item = items[0]; + } + catch (OperationCanceledException) + { + yield break; + } + + yield return item; + } + } + + /// + /// Converts the to an that yields a + /// value each time the callback is executed. + /// + public static IAsyncEnumerable ToAsyncEnumerable( + this IAwaitableCallback source, + int timeout, + CancellationToken cancellationToken = default) + => ToAsyncEnumerable(source, TimeSpan.FromMilliseconds(timeout), cancellationToken); +#endif +} diff --git a/Source/Testably.Abstractions.Testing/FileSystem/INotificationHandler.cs b/Source/Testably.Abstractions.Testing/FileSystem/INotificationHandler.cs index 3b0632e1..ae30b28b 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/INotificationHandler.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/INotificationHandler.cs @@ -11,7 +11,7 @@ public interface INotificationHandler : IFileSystemEntity /// Callback executed when any change in the matching the /// occurred. /// - /// The callback to execute after the change occurred. + /// (optional) The callback to execute after the change occurred. /// /// (optional) A predicate used to filter which callbacks should be notified.
/// If set to (default value) all callbacks are notified. diff --git a/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs b/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs index 6f027030..e39ca8d0 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs @@ -293,6 +293,9 @@ internal static TimeoutException TimeoutExpired(int timeoutMilliseconds) => new( $"The timeout of {timeoutMilliseconds}ms expired in the awaitable callback."); + internal static TimeoutException TimeoutExpired(TimeSpan timeout) + => new($"The timeout of {timeout} expired in the awaitable callback."); + internal static ArgumentOutOfRangeException TimerArgumentOutOfRange(string propertyName) => new(propertyName, "Number must be either non-negative and less than or equal to Int32.MaxValue or -1") diff --git a/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs b/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs index 5a071ce8..ad6673d0 100644 --- a/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs +++ b/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; namespace Testably.Abstractions.Testing; @@ -7,7 +9,7 @@ namespace Testably.Abstractions.Testing; /// - un-registering a callback by calling
/// - blocking for the callback to be executed /// -public interface IAwaitableCallback : IDisposable +public interface IAwaitableCallback : IDisposable { /// /// Blocks the current thread until the callback is executed. @@ -30,8 +32,47 @@ public interface IAwaitableCallback : IDisposable /// /// (optional) A callback to execute when waiting started. /// +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of the awaitable callback.")] +#endif void Wait(Func? filter = null, int timeout = 30000, int count = 1, Action? executeWhenWaiting = null); + + /// + /// Blocks the current thread until the callback is executed. + /// + /// Throws a if the expired before the callback was + /// executed. + /// + /// + /// (optional) The number of callbacks to wait.
+ /// Defaults to 1 + /// + /// + /// (optional) The timeout to wait on the callback.
+ /// If not specified (), defaults to 30 seconds. + /// + TValue[] Wait(int count, TimeSpan? timeout = null); + + /// + /// Waits asynchronously until the callback is executed. + /// + /// + /// (optional) The number of callbacks to wait.
+ /// Defaults to 1 + /// + /// + /// (optional) The timeout to wait on the callback.
+ /// If not specified (), defaults to 30 seconds. + /// + /// + /// (optional) A to cancel waiting.
+ /// Throws a if the token was canceled before the callback was executed. + /// + Task WaitAsync( + int count = 1, + TimeSpan? timeout = null, + CancellationToken? cancellationToken = null); } diff --git a/Source/Testably.Abstractions.Testing/Notification.cs b/Source/Testably.Abstractions.Testing/Notification.cs index 5e38aa39..557a8cd5 100644 --- a/Source/Testably.Abstractions.Testing/Notification.cs +++ b/Source/Testably.Abstractions.Testing/Notification.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; using Testably.Abstractions.Testing.Helpers; @@ -15,6 +17,9 @@ public static class Notification /// Executes the while waiting for the notification. ///
/// The callback. +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] +#endif public static IAwaitableCallback ExecuteWhileWaiting( this IAwaitableCallback awaitable, Action callback) { @@ -29,6 +34,9 @@ public static IAwaitableCallback ExecuteWhileWaiting( /// Executes the while waiting for the notification. /// /// The callback. +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] +#endif public static IAwaitableCallback ExecuteWhileWaiting( this IAwaitableCallback awaitable, Func callback) { @@ -75,15 +83,18 @@ private void UnRegisterCallback(Guid key) private sealed class CallbackWaiter : IAwaitableCallback { private readonly Action? _callback; - private int _count; + private readonly Channel _channel = Channel.CreateUnbounded(); private readonly NotificationFactory _factory; private Func? _filter; + private volatile bool _isDisposed; private readonly Guid _key; private readonly Func _predicate; private readonly ManualResetEventSlim _reset; + private readonly ChannelWriter _writer; public CallbackWaiter(NotificationFactory factory, - Guid key, Action? callback, + Guid key, + Action? callback, Func? predicate) { _factory = factory; @@ -91,6 +102,7 @@ public CallbackWaiter(NotificationFactory factory, _callback = callback; _predicate = predicate ?? (_ => true); _reset = new ManualResetEventSlim(); + _writer = _channel.Writer; } #region IAwaitableCallback Members @@ -99,7 +111,9 @@ public CallbackWaiter(NotificationFactory factory, public void Dispose() { _factory.UnRegisterCallback(_key); + _writer.TryComplete(); _reset.Dispose(); + _isDisposed = true; } /// @@ -108,22 +122,127 @@ public void Wait(Func? filter = null, int count = 1, Action? executeWhenWaiting = null) { - _count = count; + if (_isDisposed) + { + throw new ObjectDisposedException(null, "The awaitable callback is already disposed."); + } + _filter = filter; _reset.Reset(); - Task? task = null; if (executeWhenWaiting != null) { - task = Task.Run(executeWhenWaiting.Invoke); + executeWhenWaiting(); } + TValue[]? result = null; + _ = Task.Run(async () => + { + try + { + result = await WaitAsync(count, TimeSpan.FromMilliseconds(timeout)); + } + catch + { + // Ignore exceptions as they will be handled by the timeout or cancellation token + } + finally + { + _reset.Set(); + } + }); + if (!_reset.Wait(timeout) || - task?.Wait(timeout) == false) + result is null) { throw ExceptionFactory.TimeoutExpired(timeout); } } + /// + public TValue[] Wait(int count, TimeSpan? timeout = null) + { + if (_isDisposed) + { + throw new ObjectDisposedException(null, "The awaitable callback is already disposed."); + } + + _reset.Reset(); + + TValue[]? result = null; + Task task = Task.Run(async () => + { + try + { + result = await WaitAsync(count, timeout); + } + finally + { + _reset.Set(); + } + }); + + TimeSpan timeoutOrDefault = timeout ?? TimeSpan.FromSeconds(30); + if (!_reset.Wait(timeoutOrDefault) || + result is null) + { + throw task.Exception?.InnerException ?? + ExceptionFactory.TimeoutExpired(timeoutOrDefault); + } + + return result; + } + + /// + public async Task WaitAsync( + int count = 1, + TimeSpan? timeout = null, + CancellationToken? cancellationToken = null) + { + if (_isDisposed) + { + throw new ObjectDisposedException(null, "The awaitable callback is already disposed."); + } + + List values = []; + ChannelReader reader = _channel.Reader; + + CancellationTokenSource? cts = null; + if (cancellationToken is null) + { + cts = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(30)); + cancellationToken = cts.Token; + } + else if (timeout is not null) + { + cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value); + cts.CancelAfter(timeout.Value); + cancellationToken = cts.Token; + } + + try + { + do + { + TValue value = await reader.ReadAsync(cancellationToken.Value) + .ConfigureAwait(false); + if (_filter?.Invoke(value) != false) + { + values.Add(value); + if (--count <= 0) + { + break; + } + } + } while (!cancellationToken.Value.IsCancellationRequested); + } + finally + { + cts?.Dispose(); + } + + return values.ToArray(); + } + #endregion /// @@ -138,11 +257,7 @@ internal void Invoke(TValue value) } _callback?.Invoke(value); - if (_filter?.Invoke(value) != false && - Interlocked.Decrement(ref _count) <= 0) - { - _reset.Set(); - } + _writer.TryWrite(value); } } } @@ -161,7 +276,10 @@ IAwaitableCallback RegisterCallback( /// - un-registering a callback by calling
/// - blocking for the callback to be executed ///
- public interface IAwaitableCallback +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.")] +#endif + public interface IAwaitableCallback : IAwaitableCallback { /// @@ -193,6 +311,9 @@ public interface IAwaitableCallback Action? executeWhenWaiting = null); } +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.")] +#endif private sealed class CallbackWaiterWithValue : IAwaitableCallback { @@ -213,6 +334,10 @@ public void Dispose() => _awaitableCallback.Dispose(); /// +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete( + "Use another `Wait` or `WaitAsync` overload and move the filter to the creation of the awaitable callback.")] +#endif public TFunc Wait(Func? filter = null, int timeout = 30000, int count = 1, @@ -227,6 +352,21 @@ public TFunc Wait(Func? filter = null, return value; } + /// + public TValue[] Wait(int count, TimeSpan? timeout = null) + { + _valueProvider(); + return _awaitableCallback.Wait(count, timeout); + } + + /// + public Task WaitAsync(int count = 1, TimeSpan? timeout = null, + CancellationToken? cancellationToken = null) + { + _valueProvider(); + return _awaitableCallback.WaitAsync(count, timeout, cancellationToken); + } + /// void IAwaitableCallback.Wait(Func? filter, int timeout, 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 1158dfa7..b95e0ef5 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 @@ -2,82 +2,17 @@ [assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Testably.Abstractions.Testing.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001006104741100251820044d92b34b0519a1de0bccd80d6199aadbdcd5931d035462d42f70b0ae7a7db37bab63afb8a8ad0dc21392bb01f1243bfc51df4b5f1975b1b9746fecbed88913b783fccb69efc59e23b0e019e065abd38731711a2d6ac2569ab57d4b4d529f5903f5bee0f4388b2a5f4d5e0fddab6aac18d96aa78c2e73e0")] [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v10.0", FrameworkDisplayName=".NET 10.0")] -namespace Testably.Abstractions.Testing.FileSystem +namespace Testably.Abstractions.Testing { - public class ChangeDescription - { - public System.IO.WatcherChangeTypes ChangeType { get; } - public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } - public string? Name { get; } - public System.IO.NotifyFilters NotifyFilters { get; } - public string? OldName { get; } - public string? OldPath { get; } - public string Path { get; } - public override string ToString() { } - } - public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy - { - public DefaultAccessControlStrategy(System.Func callback) { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public DefaultSafeFileHandleStrategy(System.Func callback) { } - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public interface IAccessControlStrategy - { - bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); - } - public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); - } - public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); - } - public interface ISafeFileHandleStrategy + public static class AwaitableCallbackExtensions { - Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle); - } - public interface IUnixFileModeStrategy - { - 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 static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.AsyncIteratorStateMachine(typeof(Testably.Abstractions.Testing.AwaitableCallbackExtensions.d__3))] + public static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, System.TimeSpan? timeout, [System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken = default) { } + public static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, int timeout, System.Threading.CancellationToken cancellationToken = default) { } + public static TValue[] Wait(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000) { } + public static System.Threading.Tasks.Task WaitAsync(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000, System.Threading.CancellationToken? cancellationToken = default) { } } - public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy - { - public NullAccessControlStrategy() { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public NullSafeFileHandleStrategy() { } - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public class NullUnixFileModeStrategy : Testably.Abstractions.Testing.FileSystem.IUnixFileModeStrategy - { - public NullUnixFileModeStrategy() { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode, System.IO.FileAccess requestedAccess) { } - public void OnSetUnixFileMode(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode) { } - } - public class SafeFileHandleMock - { - public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } - public System.IO.FileMode Mode { get; } - public string Path { get; } - public System.IO.FileShare Share { get; } - } -} -namespace Testably.Abstractions.Testing -{ public static class FileSystemInitializerExtensions { public static Testably.Abstractions.Testing.Initializer.IFileSystemInitializer Initialize(this TFileSystem fileSystem, System.Action? options = null) @@ -99,9 +34,11 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { + TValue[] Wait(int count, System.TimeSpan? timeout = default); void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + System.Threading.Tasks.Task WaitAsync(int count = 1, System.TimeSpan? timeout = default, System.Threading.CancellationToken? cancellationToken = default); } public static class InterceptionHandlerExtensions { @@ -179,7 +116,7 @@ namespace Testably.Abstractions.Testing { public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback + public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } @@ -225,6 +162,80 @@ namespace Testably.Abstractions.Testing public static Testably.Abstractions.Testing.TimeSystem.ITimeProvider Use(System.DateTime time) { } } } +namespace Testably.Abstractions.Testing.FileSystem +{ + public class ChangeDescription + { + public System.IO.WatcherChangeTypes ChangeType { get; } + public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } + public string? Name { get; } + public System.IO.NotifyFilters NotifyFilters { get; } + public string? OldName { get; } + public string? OldPath { get; } + public string Path { get; } + public override string ToString() { } + } + public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy + { + public DefaultAccessControlStrategy(System.Func callback) { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public DefaultSafeFileHandleStrategy(System.Func callback) { } + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public interface IAccessControlStrategy + { + bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); + } + public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); + } + public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); + } + public interface ISafeFileHandleStrategy + { + Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle); + } + public interface IUnixFileModeStrategy + { + 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() { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public NullSafeFileHandleStrategy() { } + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public class NullUnixFileModeStrategy : Testably.Abstractions.Testing.FileSystem.IUnixFileModeStrategy + { + public NullUnixFileModeStrategy() { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode, System.IO.FileAccess requestedAccess) { } + public void OnSetUnixFileMode(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode) { } + } + public class SafeFileHandleMock + { + public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } + public System.IO.FileMode Mode { get; } + public string Path { get; } + public System.IO.FileShare Share { get; } + } +} namespace Testably.Abstractions.Testing.Initializer { public class DirectoryDescription : Testably.Abstractions.Testing.Initializer.FileSystemInfoDescription 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 9081cbde..5fe7bb85 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 @@ -2,71 +2,17 @@ [assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Testably.Abstractions.Testing.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001006104741100251820044d92b34b0519a1de0bccd80d6199aadbdcd5931d035462d42f70b0ae7a7db37bab63afb8a8ad0dc21392bb01f1243bfc51df4b5f1975b1b9746fecbed88913b783fccb69efc59e23b0e019e065abd38731711a2d6ac2569ab57d4b4d529f5903f5bee0f4388b2a5f4d5e0fddab6aac18d96aa78c2e73e0")] [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")] -namespace Testably.Abstractions.Testing.FileSystem +namespace Testably.Abstractions.Testing { - public class ChangeDescription - { - public System.IO.WatcherChangeTypes ChangeType { get; } - public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } - public string? Name { get; } - public System.IO.NotifyFilters NotifyFilters { get; } - public string? OldName { get; } - public string? OldPath { get; } - public string Path { get; } - public override string ToString() { } - } - public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy - { - public DefaultAccessControlStrategy(System.Func callback) { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public DefaultSafeFileHandleStrategy(System.Func callback) { } - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public interface IAccessControlStrategy - { - bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); - } - public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity + public static class AwaitableCallbackExtensions { - Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); + public static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.AsyncIteratorStateMachine(typeof(Testably.Abstractions.Testing.AwaitableCallbackExtensions.d__3))] + public static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, System.TimeSpan? timeout, [System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken = default) { } + public static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, int timeout, System.Threading.CancellationToken cancellationToken = default) { } + public static TValue[] Wait(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000) { } + public static System.Threading.Tasks.Task WaitAsync(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000, System.Threading.CancellationToken? cancellationToken = default) { } } - public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); - } - public interface ISafeFileHandleStrategy - { - 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() { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public NullSafeFileHandleStrategy() { } - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public class SafeFileHandleMock - { - public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } - public System.IO.FileMode Mode { get; } - public string Path { get; } - public System.IO.FileShare Share { get; } - } -} -namespace Testably.Abstractions.Testing -{ public static class FileSystemInitializerExtensions { public static Testably.Abstractions.Testing.Initializer.IFileSystemInitializer Initialize(this TFileSystem fileSystem, System.Action? options = null) @@ -88,9 +34,11 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { + TValue[] Wait(int count, System.TimeSpan? timeout = default); void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + System.Threading.Tasks.Task WaitAsync(int count = 1, System.TimeSpan? timeout = default, System.Threading.CancellationToken? cancellationToken = default); } public static class InterceptionHandlerExtensions { @@ -167,7 +115,7 @@ namespace Testably.Abstractions.Testing { public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback + public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } @@ -213,6 +161,69 @@ namespace Testably.Abstractions.Testing public static Testably.Abstractions.Testing.TimeSystem.ITimeProvider Use(System.DateTime time) { } } } +namespace Testably.Abstractions.Testing.FileSystem +{ + public class ChangeDescription + { + public System.IO.WatcherChangeTypes ChangeType { get; } + public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } + public string? Name { get; } + public System.IO.NotifyFilters NotifyFilters { get; } + public string? OldName { get; } + public string? OldPath { get; } + public string Path { get; } + public override string ToString() { } + } + public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy + { + public DefaultAccessControlStrategy(System.Func callback) { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public DefaultSafeFileHandleStrategy(System.Func callback) { } + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public interface IAccessControlStrategy + { + bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); + } + public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); + } + public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); + } + public interface ISafeFileHandleStrategy + { + 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() { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public NullSafeFileHandleStrategy() { } + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public class SafeFileHandleMock + { + public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } + public System.IO.FileMode Mode { get; } + public string Path { get; } + public System.IO.FileShare Share { get; } + } +} namespace Testably.Abstractions.Testing.Initializer { public class DirectoryDescription : Testably.Abstractions.Testing.Initializer.FileSystemInfoDescription 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 2164c13d..e311aeb0 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 @@ -2,82 +2,17 @@ [assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Testably.Abstractions.Testing.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001006104741100251820044d92b34b0519a1de0bccd80d6199aadbdcd5931d035462d42f70b0ae7a7db37bab63afb8a8ad0dc21392bb01f1243bfc51df4b5f1975b1b9746fecbed88913b783fccb69efc59e23b0e019e065abd38731711a2d6ac2569ab57d4b4d529f5903f5bee0f4388b2a5f4d5e0fddab6aac18d96aa78c2e73e0")] [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")] -namespace Testably.Abstractions.Testing.FileSystem +namespace Testably.Abstractions.Testing { - public class ChangeDescription - { - public System.IO.WatcherChangeTypes ChangeType { get; } - public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } - public string? Name { get; } - public System.IO.NotifyFilters NotifyFilters { get; } - public string? OldName { get; } - public string? OldPath { get; } - public string Path { get; } - public override string ToString() { } - } - public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy - { - public DefaultAccessControlStrategy(System.Func callback) { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public DefaultSafeFileHandleStrategy(System.Func callback) { } - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public interface IAccessControlStrategy - { - bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); - } - public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); - } - public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); - } - public interface ISafeFileHandleStrategy + public static class AwaitableCallbackExtensions { - Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle); - } - public interface IUnixFileModeStrategy - { - 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 static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.AsyncIteratorStateMachine(typeof(Testably.Abstractions.Testing.AwaitableCallbackExtensions.d__3))] + public static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, System.TimeSpan? timeout, [System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken = default) { } + public static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, int timeout, System.Threading.CancellationToken cancellationToken = default) { } + public static TValue[] Wait(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000) { } + public static System.Threading.Tasks.Task WaitAsync(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000, System.Threading.CancellationToken? cancellationToken = default) { } } - public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy - { - public NullAccessControlStrategy() { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public NullSafeFileHandleStrategy() { } - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public class NullUnixFileModeStrategy : Testably.Abstractions.Testing.FileSystem.IUnixFileModeStrategy - { - public NullUnixFileModeStrategy() { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode, System.IO.FileAccess requestedAccess) { } - public void OnSetUnixFileMode(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode) { } - } - public class SafeFileHandleMock - { - public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } - public System.IO.FileMode Mode { get; } - public string Path { get; } - public System.IO.FileShare Share { get; } - } -} -namespace Testably.Abstractions.Testing -{ public static class FileSystemInitializerExtensions { public static Testably.Abstractions.Testing.Initializer.IFileSystemInitializer Initialize(this TFileSystem fileSystem, System.Action? options = null) @@ -99,9 +34,11 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { + TValue[] Wait(int count, System.TimeSpan? timeout = default); void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + System.Threading.Tasks.Task WaitAsync(int count = 1, System.TimeSpan? timeout = default, System.Threading.CancellationToken? cancellationToken = default); } public static class InterceptionHandlerExtensions { @@ -179,7 +116,7 @@ namespace Testably.Abstractions.Testing { public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback + public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } @@ -225,6 +162,80 @@ namespace Testably.Abstractions.Testing public static Testably.Abstractions.Testing.TimeSystem.ITimeProvider Use(System.DateTime time) { } } } +namespace Testably.Abstractions.Testing.FileSystem +{ + public class ChangeDescription + { + public System.IO.WatcherChangeTypes ChangeType { get; } + public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } + public string? Name { get; } + public System.IO.NotifyFilters NotifyFilters { get; } + public string? OldName { get; } + public string? OldPath { get; } + public string Path { get; } + public override string ToString() { } + } + public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy + { + public DefaultAccessControlStrategy(System.Func callback) { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public DefaultSafeFileHandleStrategy(System.Func callback) { } + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public interface IAccessControlStrategy + { + bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); + } + public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); + } + public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); + } + public interface ISafeFileHandleStrategy + { + Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle); + } + public interface IUnixFileModeStrategy + { + 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() { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public NullSafeFileHandleStrategy() { } + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public class NullUnixFileModeStrategy : Testably.Abstractions.Testing.FileSystem.IUnixFileModeStrategy + { + public NullUnixFileModeStrategy() { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode, System.IO.FileAccess requestedAccess) { } + public void OnSetUnixFileMode(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode) { } + } + public class SafeFileHandleMock + { + public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } + public System.IO.FileMode Mode { get; } + public string Path { get; } + public System.IO.FileShare Share { get; } + } +} namespace Testably.Abstractions.Testing.Initializer { public class DirectoryDescription : Testably.Abstractions.Testing.Initializer.FileSystemInfoDescription 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 c23262e1..d57e60d7 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 @@ -2,82 +2,17 @@ [assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Testably.Abstractions.Testing.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001006104741100251820044d92b34b0519a1de0bccd80d6199aadbdcd5931d035462d42f70b0ae7a7db37bab63afb8a8ad0dc21392bb01f1243bfc51df4b5f1975b1b9746fecbed88913b783fccb69efc59e23b0e019e065abd38731711a2d6ac2569ab57d4b4d529f5903f5bee0f4388b2a5f4d5e0fddab6aac18d96aa78c2e73e0")] [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v9.0", FrameworkDisplayName=".NET 9.0")] -namespace Testably.Abstractions.Testing.FileSystem +namespace Testably.Abstractions.Testing { - public class ChangeDescription - { - public System.IO.WatcherChangeTypes ChangeType { get; } - public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } - public string? Name { get; } - public System.IO.NotifyFilters NotifyFilters { get; } - public string? OldName { get; } - public string? OldPath { get; } - public string Path { get; } - public override string ToString() { } - } - public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy - { - public DefaultAccessControlStrategy(System.Func callback) { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public DefaultSafeFileHandleStrategy(System.Func callback) { } - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public interface IAccessControlStrategy - { - bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); - } - public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); - } - public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); - } - public interface ISafeFileHandleStrategy + public static class AwaitableCallbackExtensions { - Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle); - } - public interface IUnixFileModeStrategy - { - 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 static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.AsyncIteratorStateMachine(typeof(Testably.Abstractions.Testing.AwaitableCallbackExtensions.d__3))] + public static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, System.TimeSpan? timeout, [System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken = default) { } + public static System.Collections.Generic.IAsyncEnumerable ToAsyncEnumerable(this Testably.Abstractions.Testing.IAwaitableCallback source, int timeout, System.Threading.CancellationToken cancellationToken = default) { } + public static TValue[] Wait(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000) { } + public static System.Threading.Tasks.Task WaitAsync(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000, System.Threading.CancellationToken? cancellationToken = default) { } } - public class NullAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy - { - public NullAccessControlStrategy() { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public NullSafeFileHandleStrategy() { } - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public class NullUnixFileModeStrategy : Testably.Abstractions.Testing.FileSystem.IUnixFileModeStrategy - { - public NullUnixFileModeStrategy() { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode, System.IO.FileAccess requestedAccess) { } - public void OnSetUnixFileMode(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode) { } - } - public class SafeFileHandleMock - { - public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } - public System.IO.FileMode Mode { get; } - public string Path { get; } - public System.IO.FileShare Share { get; } - } -} -namespace Testably.Abstractions.Testing -{ public static class FileSystemInitializerExtensions { public static Testably.Abstractions.Testing.Initializer.IFileSystemInitializer Initialize(this TFileSystem fileSystem, System.Action? options = null) @@ -99,9 +34,11 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { + TValue[] Wait(int count, System.TimeSpan? timeout = default); void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + System.Threading.Tasks.Task WaitAsync(int count = 1, System.TimeSpan? timeout = default, System.Threading.CancellationToken? cancellationToken = default); } public static class InterceptionHandlerExtensions { @@ -179,7 +116,7 @@ namespace Testably.Abstractions.Testing { public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback + public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } @@ -225,6 +162,80 @@ namespace Testably.Abstractions.Testing public static Testably.Abstractions.Testing.TimeSystem.ITimeProvider Use(System.DateTime time) { } } } +namespace Testably.Abstractions.Testing.FileSystem +{ + public class ChangeDescription + { + public System.IO.WatcherChangeTypes ChangeType { get; } + public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } + public string? Name { get; } + public System.IO.NotifyFilters NotifyFilters { get; } + public string? OldName { get; } + public string? OldPath { get; } + public string Path { get; } + public override string ToString() { } + } + public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy + { + public DefaultAccessControlStrategy(System.Func callback) { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public DefaultSafeFileHandleStrategy(System.Func callback) { } + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public interface IAccessControlStrategy + { + bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); + } + public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); + } + public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); + } + public interface ISafeFileHandleStrategy + { + Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle); + } + public interface IUnixFileModeStrategy + { + 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() { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public NullSafeFileHandleStrategy() { } + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification="SafeFileHandle cannot be unit tested.")] + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public class NullUnixFileModeStrategy : Testably.Abstractions.Testing.FileSystem.IUnixFileModeStrategy + { + public NullUnixFileModeStrategy() { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode, System.IO.FileAccess requestedAccess) { } + public void OnSetUnixFileMode(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility, System.IO.UnixFileMode mode) { } + } + public class SafeFileHandleMock + { + public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } + public System.IO.FileMode Mode { get; } + public string Path { get; } + public System.IO.FileShare Share { get; } + } +} namespace Testably.Abstractions.Testing.Initializer { public class DirectoryDescription : Testably.Abstractions.Testing.Initializer.FileSystemInfoDescription 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 24ffa35d..87ad3515 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 @@ -2,69 +2,13 @@ [assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Testably.Abstractions.Testing.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001006104741100251820044d92b34b0519a1de0bccd80d6199aadbdcd5931d035462d42f70b0ae7a7db37bab63afb8a8ad0dc21392bb01f1243bfc51df4b5f1975b1b9746fecbed88913b783fccb69efc59e23b0e019e065abd38731711a2d6ac2569ab57d4b4d529f5903f5bee0f4388b2a5f4d5e0fddab6aac18d96aa78c2e73e0")] [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] -namespace Testably.Abstractions.Testing.FileSystem +namespace Testably.Abstractions.Testing { - public class ChangeDescription - { - public System.IO.WatcherChangeTypes ChangeType { get; } - public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } - public string? Name { get; } - public System.IO.NotifyFilters NotifyFilters { get; } - public string? OldName { get; } - public string? OldPath { get; } - public string Path { get; } - public override string ToString() { } - } - public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy + public static class AwaitableCallbackExtensions { - public DefaultAccessControlStrategy(System.Func callback) { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + public static TValue[] Wait(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000) { } + public static System.Threading.Tasks.Task WaitAsync(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000, System.Threading.CancellationToken? cancellationToken = default) { } } - public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public DefaultSafeFileHandleStrategy(System.Func callback) { } - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public interface IAccessControlStrategy - { - bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); - } - public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); - } - public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); - } - public interface ISafeFileHandleStrategy - { - 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() { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public NullSafeFileHandleStrategy() { } - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public class SafeFileHandleMock - { - public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } - public System.IO.FileMode Mode { get; } - public string Path { get; } - public System.IO.FileShare Share { get; } - } -} -namespace Testably.Abstractions.Testing -{ public static class FileSystemInitializerExtensions { public static Testably.Abstractions.Testing.Initializer.IFileSystemInitializer Initialize(this TFileSystem fileSystem, System.Action? options = null) @@ -86,9 +30,11 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { + TValue[] Wait(int count, System.TimeSpan? timeout = default); void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + System.Threading.Tasks.Task WaitAsync(int count = 1, System.TimeSpan? timeout = default, System.Threading.CancellationToken? cancellationToken = default); } public static class InterceptionHandlerExtensions { @@ -164,7 +110,7 @@ namespace Testably.Abstractions.Testing { public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback + public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } @@ -210,6 +156,67 @@ namespace Testably.Abstractions.Testing public static Testably.Abstractions.Testing.TimeSystem.ITimeProvider Use(System.DateTime time) { } } } +namespace Testably.Abstractions.Testing.FileSystem +{ + public class ChangeDescription + { + public System.IO.WatcherChangeTypes ChangeType { get; } + public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } + public string? Name { get; } + public System.IO.NotifyFilters NotifyFilters { get; } + public string? OldName { get; } + public string? OldPath { get; } + public string Path { get; } + public override string ToString() { } + } + public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy + { + public DefaultAccessControlStrategy(System.Func callback) { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public DefaultSafeFileHandleStrategy(System.Func callback) { } + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public interface IAccessControlStrategy + { + bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); + } + public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); + } + public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); + } + public interface ISafeFileHandleStrategy + { + 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() { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public NullSafeFileHandleStrategy() { } + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public class SafeFileHandleMock + { + public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } + public System.IO.FileMode Mode { get; } + public string Path { get; } + public System.IO.FileShare Share { get; } + } +} namespace Testably.Abstractions.Testing.Initializer { public class DirectoryDescription : Testably.Abstractions.Testing.Initializer.FileSystemInfoDescription 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 aeacef40..9bf2c069 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 @@ -2,69 +2,13 @@ [assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Testably.Abstractions.Testing.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001006104741100251820044d92b34b0519a1de0bccd80d6199aadbdcd5931d035462d42f70b0ae7a7db37bab63afb8a8ad0dc21392bb01f1243bfc51df4b5f1975b1b9746fecbed88913b783fccb69efc59e23b0e019e065abd38731711a2d6ac2569ab57d4b4d529f5903f5bee0f4388b2a5f4d5e0fddab6aac18d96aa78c2e73e0")] [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName=".NET Standard 2.1")] -namespace Testably.Abstractions.Testing.FileSystem +namespace Testably.Abstractions.Testing { - public class ChangeDescription - { - public System.IO.WatcherChangeTypes ChangeType { get; } - public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } - public string? Name { get; } - public System.IO.NotifyFilters NotifyFilters { get; } - public string? OldName { get; } - public string? OldPath { get; } - public string Path { get; } - public override string ToString() { } - } - public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy + public static class AwaitableCallbackExtensions { - public DefaultAccessControlStrategy(System.Func callback) { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + public static TValue[] Wait(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000) { } + public static System.Threading.Tasks.Task WaitAsync(this Testably.Abstractions.Testing.IAwaitableCallback callback, int count = 1, int timeout = 30000, System.Threading.CancellationToken? cancellationToken = default) { } } - public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public DefaultSafeFileHandleStrategy(System.Func callback) { } - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public interface IAccessControlStrategy - { - bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); - } - public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); - } - public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity - { - Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); - } - public interface ISafeFileHandleStrategy - { - 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() { } - public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } - } - public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy - { - public NullSafeFileHandleStrategy() { } - public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } - } - public class SafeFileHandleMock - { - public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } - public System.IO.FileMode Mode { get; } - public string Path { get; } - public System.IO.FileShare Share { get; } - } -} -namespace Testably.Abstractions.Testing -{ public static class FileSystemInitializerExtensions { public static Testably.Abstractions.Testing.Initializer.IFileSystemInitializer Initialize(this TFileSystem fileSystem, System.Action? options = null) @@ -86,9 +30,11 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { + TValue[] Wait(int count, System.TimeSpan? timeout = default); void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + System.Threading.Tasks.Task WaitAsync(int count = 1, System.TimeSpan? timeout = default, System.Threading.CancellationToken? cancellationToken = default); } public static class InterceptionHandlerExtensions { @@ -164,7 +110,7 @@ namespace Testably.Abstractions.Testing { public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback + public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } @@ -210,6 +156,67 @@ namespace Testably.Abstractions.Testing public static Testably.Abstractions.Testing.TimeSystem.ITimeProvider Use(System.DateTime time) { } } } +namespace Testably.Abstractions.Testing.FileSystem +{ + public class ChangeDescription + { + public System.IO.WatcherChangeTypes ChangeType { get; } + public Testably.Abstractions.Testing.FileSystemTypes FileSystemType { get; } + public string? Name { get; } + public System.IO.NotifyFilters NotifyFilters { get; } + public string? OldName { get; } + public string? OldPath { get; } + public string Path { get; } + public override string ToString() { } + } + public class DefaultAccessControlStrategy : Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy + { + public DefaultAccessControlStrategy(System.Func callback) { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class DefaultSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public DefaultSafeFileHandleStrategy(System.Func callback) { } + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public interface IAccessControlStrategy + { + bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility); + } + public interface IInterceptionHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback Event(System.Action interceptionCallback, System.Func? predicate = null); + } + public interface INotificationHandler : System.IO.Abstractions.IFileSystemEntity + { + Testably.Abstractions.Testing.IAwaitableCallback OnEvent(System.Action? notificationCallback = null, System.Func? predicate = null); + } + public interface ISafeFileHandleStrategy + { + 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() { } + public bool IsAccessGranted(string fullPath, Testably.Abstractions.Helpers.IFileSystemExtensibility extensibility) { } + } + public class NullSafeFileHandleStrategy : Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy + { + public NullSafeFileHandleStrategy() { } + public Testably.Abstractions.Testing.FileSystem.SafeFileHandleMock MapSafeFileHandle(Microsoft.Win32.SafeHandles.SafeFileHandle fileHandle) { } + } + public class SafeFileHandleMock + { + public SafeFileHandleMock(string path, System.IO.FileMode mode = 3, System.IO.FileShare share = 0) { } + public System.IO.FileMode Mode { get; } + public string Path { get; } + public System.IO.FileShare Share { get; } + } +} namespace Testably.Abstractions.Testing.Initializer { public class DirectoryDescription : Testably.Abstractions.Testing.Initializer.FileSystemInfoDescription diff --git a/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs b/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs new file mode 100644 index 00000000..cfd49ebc --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs @@ -0,0 +1,166 @@ +using System.IO; +using System.Linq; +using System.Threading; +using Testably.Abstractions.Testing.FileSystem; + +namespace Testably.Abstractions.Testing.Tests; + +public class AwaitableCallbackExtensionsTests +{ +#if NET6_0_OR_GREATER + public sealed class ToAsyncEnumerableTests + { + [Fact] + public async Task ShouldReturnAllEvents() + { + MockFileSystem fileSystem = new(); + IAwaitableCallback sut = fileSystem.Notify.OnEvent(); + + fileSystem.Directory.CreateDirectory("Test"); + fileSystem.File.WriteAllText("Test/abc.txt", "foo"); + fileSystem.File.Delete("Test/abc.txt"); + fileSystem.Directory.Delete("Test"); + + ChangeDescription[] results = await sut + .ToAsyncEnumerable(cancellationToken: TestContext.Current.CancellationToken) + .Take(6) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); + + await That(results[0]).IsEquivalentTo(new + { + ChangeType = WatcherChangeTypes.Created, + FileSystemType = FileSystemTypes.Directory, + Name = "Test", + }); + await That(results[1]).IsEquivalentTo(new + { + ChangeType = WatcherChangeTypes.Created, + FileSystemType = FileSystemTypes.File, + Name = "Test/abc.txt", + }); + await That(results[4]).IsEquivalentTo(new + { + ChangeType = WatcherChangeTypes.Deleted, + FileSystemType = FileSystemTypes.File, + Name = "Test/abc.txt", + }); + await That(results[5]).IsEquivalentTo(new + { + ChangeType = WatcherChangeTypes.Deleted, + FileSystemType = FileSystemTypes.Directory, + Name = "Test", + }); + } + + [Fact] + public async Task WithCancelledToken_ShouldAbort() + { + MockFileSystem fileSystem = new(); + IAwaitableCallback sut = fileSystem.Notify + .OnEvent(predicate: p => p.FileSystemType == FileSystemTypes.Directory); + using CancellationTokenSource cts = new(); + CancellationToken token = cts.Token; + + _ = Task.Run(async () => + { + for (int i = 0; i < 10; i++) + { + fileSystem.Directory.CreateDirectory($"Test{i}"); + await Task.Delay(100, TestContext.Current.CancellationToken); + if (i == 5) + { + // ReSharper disable once AccessToDisposedClosure + cts.Cancel(); + } + } + }, TestContext.Current.CancellationToken); + + ChangeDescription[] results = await sut.ToAsyncEnumerable(cancellationToken: token) + .Take(10) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); + + await That(results.Length).IsEqualTo(6); + } + + [Fact] + public async Task WithFilter_ShouldOnlyReturnMatchingEvents() + { + MockFileSystem fileSystem = new(); + IAwaitableCallback sut = fileSystem.Notify + .OnEvent(predicate: p => p.FileSystemType == FileSystemTypes.Directory); + + fileSystem.Directory.CreateDirectory("Test"); + fileSystem.File.WriteAllText("Test/abc.txt", "foo"); + fileSystem.File.Delete("Test/abc.txt"); + fileSystem.Directory.Delete("Test"); + + ChangeDescription[] results = await sut + .ToAsyncEnumerable(cancellationToken: TestContext.Current.CancellationToken) + .Take(2) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); + + await That(results[0]).IsEquivalentTo(new + { + ChangeType = WatcherChangeTypes.Created, + FileSystemType = FileSystemTypes.Directory, + Name = "Test", + }); + await That(results[1]).IsEquivalentTo(new + { + ChangeType = WatcherChangeTypes.Deleted, + FileSystemType = FileSystemTypes.Directory, + Name = "Test", + }); + } + + [Fact] + public async Task WithIntTimeout_ShouldAbortAfterwards() + { + MockFileSystem fileSystem = new(); + IAwaitableCallback sut = fileSystem.Notify + .OnEvent(predicate: p => p.FileSystemType == FileSystemTypes.Directory); + + _ = Task.Run(async () => + { + for (int i = 0; i < 10; i++) + { + fileSystem.Directory.CreateDirectory($"Test{i}"); + await Task.Delay(100, TestContext.Current.CancellationToken); + } + }, TestContext.Current.CancellationToken); + + ChangeDescription[] results = await sut + .ToAsyncEnumerable(150, cancellationToken: TestContext.Current.CancellationToken) + .Take(10) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); + + await That(results.Length).IsLessThan(9); + } + + [Fact] + public async Task WithTimeout_ShouldAbortAfterwards() + { + MockFileSystem fileSystem = new(); + IAwaitableCallback sut = fileSystem.Notify + .OnEvent(predicate: p => p.FileSystemType == FileSystemTypes.Directory); + + _ = Task.Run(async () => + { + for (int i = 0; i < 10; i++) + { + fileSystem.Directory.CreateDirectory($"Test{i}"); + await Task.Delay(100, TestContext.Current.CancellationToken); + } + }, TestContext.Current.CancellationToken); + + ChangeDescription[] results = await sut + .ToAsyncEnumerable(TimeSpan.FromMilliseconds(150), + cancellationToken: TestContext.Current.CancellationToken) + .Take(10) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); + + await That(results.Length).IsLessThan(9); + } + } +#endif +} diff --git a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs index e2ec3568..4d775177 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs @@ -57,18 +57,16 @@ public async Task string path = FileSystem.Path.Combine(path1, path2, path3); int eventCount = 0; - FileSystem.Notify + using IAwaitableCallback onEvent = FileSystem.Notify .OnEvent(c => { testOutputHelper.WriteLine($"Received event {c}"); eventCount++; }, - c => c.ChangeType == WatcherChangeTypes.Created) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.CreateDirectory(path); - }) - .Wait(count: 3); + c => c.ChangeType == WatcherChangeTypes.Created); + FileSystem.Directory.CreateDirectory(path); + + onEvent.Wait(3); await That(eventCount).IsEqualTo(3); } @@ -86,15 +84,14 @@ public async Task ExecuteCallback_ShouldTriggerNotification( FileSystem.Initialize(); initialization?.Invoke(FileSystem, path); - FileSystem.Notify + using IAwaitableCallback onEvent = FileSystem.Notify .OnEvent(c => receivedPath = c.Path, c => c.ChangeType == expectedChangeType && - c.FileSystemType == expectedFileSystemType) - .ExecuteWhileWaiting(() => - { - callback.Invoke(FileSystem, path); - }) - .Wait(); + c.FileSystemType == expectedFileSystemType); + + callback.Invoke(FileSystem, path); + + onEvent.Wait(); await That(receivedPath).IsEqualTo(FileSystem.Path.GetFullPath(path)); } @@ -106,11 +103,13 @@ public async Task Watcher_ShouldNotTriggerWhenFileSystemWatcherDoesNotMatch() IFileSystemWatcher watcher = FileSystem.FileSystemWatcher.New("bar"); watcher.EnableRaisingEvents = true; - IAwaitableCallback onEvent = FileSystem.Watcher.OnTriggered(); + using IAwaitableCallback onEvent = FileSystem.Watcher.OnTriggered(); + + FileSystem.File.WriteAllText(@"foo.txt", "some-text"); void Act() => - onEvent.Wait(timeout: 100, - executeWhenWaiting: () => FileSystem.File.WriteAllText(@"foo.txt", "some-text")); + // ReSharper disable once AccessToDisposedClosure + onEvent.Wait(timeout: 100); await That(Act).Throws(); } @@ -124,10 +123,10 @@ public async Task Watcher_ShouldTriggerWhenFileSystemWatcherSendsNotification() watcher.Created += (_, _) => isTriggered = true; watcher.EnableRaisingEvents = true; - IAwaitableCallback onEvent = FileSystem.Watcher.OnTriggered(); + using IAwaitableCallback onEvent = FileSystem.Watcher.OnTriggered(); + FileSystem.File.WriteAllText(@"foo.txt", "some-text"); - onEvent.Wait(timeout: 5000, - executeWhenWaiting: () => FileSystem.File.WriteAllText(@"foo.txt", "some-text")); + onEvent.Wait(timeout: 5000); await That(isTriggered).IsTrue(); } diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.WithExecuteWhileWaiting.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.WithExecuteWhileWaiting.cs new file mode 100644 index 00000000..e0155462 --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.WithExecuteWhileWaiting.cs @@ -0,0 +1,680 @@ +namespace Testably.Abstractions.Testing.Tests; + +public partial class NotificationHandlerExtensionsTests +{ + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnChanged_File_OtherEvent_ShouldNotTrigger( + string path) + { + bool isNotified = false; + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.File.WriteAllText(path, null); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnChanged_File_ShouldConsiderBasePath(string path1, + string path2) + { + bool isNotified = false; + FileSystem.File.WriteAllText(path1, null); + FileSystem.File.WriteAllText(path2, null); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true, path2) + .ExecuteWhileWaiting(() => + { + FileSystem.File.AppendAllText(path1, "foo"); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [InlineData(".", "foo", "f*o", true)] + [InlineData(".", "foo", "*fo", false)] + [InlineData("bar", "foo", "f*o", true)] + [InlineData("bar", "foo", "baz/f*o", false)] + [InlineData("bar", "foo", "/f*o", false)] + [InlineData("bar", "foo", "**/f*o", true)] + public async Task WithExecuteWhileWaiting_OnChanged_File_ShouldConsiderGlobPattern( + string directoryPath, string fileName, string globPattern, bool expectedResult) + { + bool isNotified = false; + string filePath = FileSystem.Path.Combine(directoryPath, fileName); + FileSystem.Directory.CreateDirectory(directoryPath); + FileSystem.File.WriteAllText(filePath, null); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true, globPattern) + .ExecuteWhileWaiting(() => + { + FileSystem.File.AppendAllText(filePath, "foo"); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnChanged_File_ShouldNotifyWhenFileIsChanged( + string path) + { + bool isNotified = false; + FileSystem.File.WriteAllText(path, null); + + FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.File.AppendAllText(path, "foo"); + }) + .Wait(); + + await That(isNotified).IsTrue(); + } + + [Theory] + [InlineAutoData(false)] + [InlineAutoData(true)] + public async Task WithExecuteWhileWaiting_OnChanged_File_ShouldUsePredicate(bool expectedResult, + string path) + { + bool isNotified = false; + FileSystem.File.WriteAllText(path, null); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true, + predicate: _ => expectedResult) + .ExecuteWhileWaiting(() => + { + FileSystem.File.AppendAllText(path, "foo"); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnCreated_Directory_OtherEvent_ShouldNotTrigger( + string path) + { + bool isNotified = false; + FileSystem.Directory.CreateDirectory(path); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.Delete(path); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnCreated_Directory_ShouldConsiderBasePath( + string path1, string path2) + { + bool isNotified = false; + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true, path2) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.CreateDirectory(path1); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [InlineData(".", "foo", "f*o", true)] + [InlineData(".", "foo", "*fo", false)] + [InlineData("bar", "foo", "f*o", true)] + [InlineData("bar", "foo", "baz/f*o", false)] + [InlineData("bar", "foo", "/f*o", false)] + [InlineData("bar", "foo", "**/f*o", true)] + public async Task WithExecuteWhileWaiting_OnCreated_Directory_ShouldConsiderGlobPattern( + string directoryPath, string fileName, string globPattern, bool expectedResult) + { + string filePath = FileSystem.Path.Combine(directoryPath, fileName); + FileSystem.Directory.CreateDirectory(directoryPath); + bool isNotified = false; + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true, + globPattern) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.CreateDirectory(filePath); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } + + [Theory] + [AutoData] + public async Task + WithExecuteWhileWaiting_OnCreated_Directory_ShouldNotifyWhenDirectoryIsCreated(string path) + { + bool isNotified = false; + + FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.CreateDirectory(path); + }) + .Wait(); + + await That(isNotified).IsTrue(); + } + + [Theory] + [InlineAutoData(false)] + [InlineAutoData(true)] + public async Task WithExecuteWhileWaiting_OnCreated_Directory_ShouldUsePredicate( + bool expectedResult, string path) + { + bool isNotified = false; + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true, + predicate: _ => expectedResult) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.CreateDirectory(path); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnCreated_File_OtherEvent_ShouldNotTrigger( + string path) + { + bool isNotified = false; + FileSystem.File.WriteAllText(path, null); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.File.Delete(path); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnCreated_File_ShouldConsiderBasePath(string path1, + string path2) + { + bool isNotified = false; + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true, path2) + .ExecuteWhileWaiting(() => + { + FileSystem.File.WriteAllText(path1, null); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [InlineData(".", "foo", "f*o", true)] + [InlineData(".", "foo", "*fo", false)] + [InlineData("bar", "foo", "f*o", true)] + [InlineData("bar", "foo", "baz/f*o", false)] + [InlineData("bar", "foo", "/f*o", false)] + [InlineData("bar", "foo", "**/f*o", true)] + public async Task WithExecuteWhileWaiting_OnCreated_File_ShouldConsiderGlobPattern( + string directoryPath, string fileName, string globPattern, bool expectedResult) + { + string filePath = FileSystem.Path.Combine(directoryPath, fileName); + FileSystem.Directory.CreateDirectory(directoryPath); + bool isNotified = false; + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true, + globPattern) + .ExecuteWhileWaiting(() => + { + FileSystem.File.WriteAllText(filePath, null); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnCreated_File_ShouldNotifyWhenFileIsCreated( + string path) + { + bool isNotified = false; + + FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.File.WriteAllText(path, null); + }) + .Wait(); + + await That(isNotified).IsTrue(); + } + + [Theory] + [InlineAutoData(false)] + [InlineAutoData(true)] + public async Task WithExecuteWhileWaiting_OnCreated_File_ShouldUsePredicate(bool expectedResult, + string path) + { + bool isNotified = false; + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true, + predicate: _ => expectedResult) + .ExecuteWhileWaiting(() => + { + FileSystem.File.WriteAllText(path, null); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnDeleted_Directory_OtherEvent_ShouldNotTrigger( + string path) + { + bool isNotified = false; + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.CreateDirectory(path); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnDeleted_Directory_ShouldConsiderBasePath( + string path1, string path2) + { + bool isNotified = false; + FileSystem.Directory.CreateDirectory(path1); + FileSystem.Directory.CreateDirectory(path2); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true, path2) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.Delete(path1); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [InlineData(".", "foo", "f*o", true)] + [InlineData(".", "foo", "*fo", false)] + [InlineData("bar", "foo", "f*o", true)] + [InlineData("bar", "foo", "baz/f*o", false)] + [InlineData("bar", "foo", "/f*o", false)] + [InlineData("bar", "foo", "**/f*o", true)] + public async Task WithExecuteWhileWaiting_OnDeleted_Directory_ShouldConsiderGlobPattern( + string basePath, string directoryName, string globPattern, bool expectedResult) + { + bool isNotified = false; + string directoryPath = FileSystem.Path.Combine(basePath, directoryName); + FileSystem.Directory.CreateDirectory(basePath); + FileSystem.Directory.CreateDirectory(directoryPath); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true, + globPattern) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.Delete(directoryPath); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } + + [Theory] + [AutoData] + public async Task + WithExecuteWhileWaiting_OnDeleted_Directory_ShouldNotifyWhenDirectoryIsDeleted(string path) + { + bool isNotified = false; + FileSystem.Directory.CreateDirectory(path); + + FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.Delete(path); + }) + .Wait(); + + await That(isNotified).IsTrue(); + } + + [Theory] + [InlineAutoData(false)] + [InlineAutoData(true)] + public async Task WithExecuteWhileWaiting_OnDeleted_Directory_ShouldUsePredicate( + bool expectedResult, string path) + { + bool isNotified = false; + FileSystem.Directory.CreateDirectory(path); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true, + predicate: _ => expectedResult) + .ExecuteWhileWaiting(() => + { + FileSystem.Directory.Delete(path); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnDeleted_File_OtherEvent_ShouldNotTrigger( + string path) + { + bool isNotified = false; + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.File.WriteAllText(path, null); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnDeleted_File_ShouldConsiderBasePath(string path1, + string path2) + { + bool isNotified = false; + FileSystem.File.WriteAllText(path1, null); + FileSystem.File.WriteAllText(path2, null); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true, path2) + .ExecuteWhileWaiting(() => + { + FileSystem.File.Delete(path1); + }) + .Wait(timeout: 50); + }); + + await That(exception).IsExactly(); + await That(isNotified).IsFalse(); + } + + [Theory] + [InlineData(".", "foo", "f*o", true)] + [InlineData(".", "foo", "*fo", false)] + [InlineData("bar", "foo", "f*o", true)] + [InlineData("bar", "foo", "baz/f*o", false)] + [InlineData("bar", "foo", "/f*o", false)] + [InlineData("bar", "foo", "**/f*o", true)] + public async Task WithExecuteWhileWaiting_OnDeleted_File_ShouldConsiderGlobPattern( + string directoryPath, string fileName, string globPattern, bool expectedResult) + { + bool isNotified = false; + string filePath = FileSystem.Path.Combine(directoryPath, fileName); + FileSystem.Directory.CreateDirectory(directoryPath); + FileSystem.File.WriteAllText(filePath, null); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true, + globPattern) + .ExecuteWhileWaiting(() => + { + FileSystem.File.Delete(filePath); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } + + [Theory] + [AutoData] + public async Task WithExecuteWhileWaiting_OnDeleted_File_ShouldNotifyWhenFileIsDeleted( + string path) + { + bool isNotified = false; + FileSystem.File.WriteAllText(path, null); + + FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true) + .ExecuteWhileWaiting(() => + { + FileSystem.File.Delete(path); + }) + .Wait(); + + await That(isNotified).IsTrue(); + } + + [Theory] + [InlineAutoData(false)] + [InlineAutoData(true)] + public async Task WithExecuteWhileWaiting_OnDeleted_File_ShouldUsePredicate(bool expectedResult, + string path) + { + bool isNotified = false; + FileSystem.File.WriteAllText(path, null); + + Exception? exception = Record.Exception(() => + { + FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true, + predicate: _ => expectedResult) + .ExecuteWhileWaiting(() => + { + FileSystem.File.Delete(path); + }) + .Wait(timeout: expectedResult ? 30000 : 50); + }); + + if (expectedResult) + { + await That(exception).IsNull(); + } + else + { + await That(exception).IsExactly(); + } + + await That(isNotified).IsEqualTo(expectedResult); + } +} diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs index aa459c54..5d553fed 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs @@ -1,6 +1,8 @@ +using Testably.Abstractions.Testing.FileSystem; + namespace Testably.Abstractions.Testing.Tests; -public class NotificationHandlerExtensionsTests +public partial class NotificationHandlerExtensionsTests { #region Test Setup @@ -14,15 +16,15 @@ public async Task OnChanged_File_OtherEvent_ShouldNotTrigger(string path) { bool isNotified = false; + using IAwaitableCallback onChanged = + FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true); + + FileSystem.File.WriteAllText(path, null); Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnChanged(FileSystemTypes.File, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.File.WriteAllText(path, null); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onChanged.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -37,15 +39,13 @@ public async Task OnChanged_File_ShouldConsiderBasePath(string path1, string pat FileSystem.File.WriteAllText(path1, null); FileSystem.File.WriteAllText(path2, null); + using IAwaitableCallback onChanged = FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true, path2); + FileSystem.File.AppendAllText(path1, "foo"); Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnChanged(FileSystemTypes.File, _ => isNotified = true, path2) - .ExecuteWhileWaiting(() => - { - FileSystem.File.AppendAllText(path1, "foo"); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onChanged.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -66,16 +66,14 @@ public async Task OnChanged_File_ShouldConsiderGlobPattern( string filePath = FileSystem.Path.Combine(directoryPath, fileName); FileSystem.Directory.CreateDirectory(directoryPath); FileSystem.File.WriteAllText(filePath, null); + using IAwaitableCallback onChanged = FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true, globPattern); + FileSystem.File.AppendAllText(filePath, "foo"); Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnChanged(FileSystemTypes.File, _ => isNotified = true, globPattern) - .ExecuteWhileWaiting(() => - { - FileSystem.File.AppendAllText(filePath, "foo"); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onChanged.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -97,13 +95,12 @@ public async Task OnChanged_File_ShouldNotifyWhenFileIsChanged(string path) bool isNotified = false; FileSystem.File.WriteAllText(path, null); - FileSystem.Notify - .OnChanged(FileSystemTypes.File, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.File.AppendAllText(path, "foo"); - }) - .Wait(); + using IAwaitableCallback onChanged = FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true); + + FileSystem.File.AppendAllText(path, "foo"); + + onChanged.Wait(); await That(isNotified).IsTrue(); } @@ -116,16 +113,15 @@ public async Task OnChanged_File_ShouldUsePredicate(bool expectedResult, string bool isNotified = false; FileSystem.File.WriteAllText(path, null); + using IAwaitableCallback onChanged = FileSystem.Notify + .OnChanged(FileSystemTypes.File, _ => isNotified = true, + predicate: _ => expectedResult); + FileSystem.File.AppendAllText(path, "foo"); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnChanged(FileSystemTypes.File, _ => isNotified = true, - predicate: _ => expectedResult) - .ExecuteWhileWaiting(() => - { - FileSystem.File.AppendAllText(path, "foo"); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onChanged.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -147,15 +143,14 @@ public async Task OnCreated_Directory_OtherEvent_ShouldNotTrigger(string path) bool isNotified = false; FileSystem.Directory.CreateDirectory(path); + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true); + FileSystem.Directory.Delete(path); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnCreated(FileSystemTypes.Directory, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.Delete(path); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onCreated.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -168,15 +163,14 @@ public async Task OnCreated_Directory_ShouldConsiderBasePath(string path1, strin { bool isNotified = false; + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true, path2); + FileSystem.Directory.CreateDirectory(path1); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnCreated(FileSystemTypes.Directory, _ => isNotified = true, path2) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.CreateDirectory(path1); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onCreated.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -197,16 +191,15 @@ public async Task OnCreated_Directory_ShouldConsiderGlobPattern( FileSystem.Directory.CreateDirectory(directoryPath); bool isNotified = false; + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true, + globPattern); + FileSystem.Directory.CreateDirectory(filePath); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnCreated(FileSystemTypes.Directory, _ => isNotified = true, - globPattern) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.CreateDirectory(filePath); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onCreated.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -227,13 +220,11 @@ public async Task OnCreated_Directory_ShouldNotifyWhenDirectoryIsCreated(string { bool isNotified = false; - FileSystem.Notify - .OnCreated(FileSystemTypes.Directory, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.CreateDirectory(path); - }) - .Wait(); + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true); + FileSystem.Directory.CreateDirectory(path); + + onCreated.Wait(); await That(isNotified).IsTrue(); } @@ -245,16 +236,15 @@ public async Task OnCreated_Directory_ShouldUsePredicate(bool expectedResult, st { bool isNotified = false; + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.Directory, _ => isNotified = true, + predicate: _ => expectedResult); + FileSystem.Directory.CreateDirectory(path); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnCreated(FileSystemTypes.Directory, _ => isNotified = true, - predicate: _ => expectedResult) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.CreateDirectory(path); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onCreated.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -276,15 +266,14 @@ public async Task OnCreated_File_OtherEvent_ShouldNotTrigger(string path) bool isNotified = false; FileSystem.File.WriteAllText(path, null); + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true); + FileSystem.File.Delete(path); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnCreated(FileSystemTypes.File, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.File.Delete(path); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onCreated.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -297,15 +286,14 @@ public async Task OnCreated_File_ShouldConsiderBasePath(string path1, string pat { bool isNotified = false; + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true, path2); + FileSystem.File.WriteAllText(path1, null); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnCreated(FileSystemTypes.File, _ => isNotified = true, path2) - .ExecuteWhileWaiting(() => - { - FileSystem.File.WriteAllText(path1, null); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onCreated.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -326,16 +314,15 @@ public async Task OnCreated_File_ShouldConsiderGlobPattern( FileSystem.Directory.CreateDirectory(directoryPath); bool isNotified = false; + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true, + globPattern); + FileSystem.File.WriteAllText(filePath, null); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnCreated(FileSystemTypes.File, _ => isNotified = true, - globPattern) - .ExecuteWhileWaiting(() => - { - FileSystem.File.WriteAllText(filePath, null); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onCreated.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -356,13 +343,11 @@ public async Task OnCreated_File_ShouldNotifyWhenFileIsCreated(string path) { bool isNotified = false; - FileSystem.Notify - .OnCreated(FileSystemTypes.File, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.File.WriteAllText(path, null); - }) - .Wait(); + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true); + FileSystem.File.WriteAllText(path, null); + + onCreated.Wait(); await That(isNotified).IsTrue(); } @@ -374,16 +359,15 @@ public async Task OnCreated_File_ShouldUsePredicate(bool expectedResult, string { bool isNotified = false; + using IAwaitableCallback onCreated = FileSystem.Notify + .OnCreated(FileSystemTypes.File, _ => isNotified = true, + predicate: _ => expectedResult); + FileSystem.File.WriteAllText(path, null); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnCreated(FileSystemTypes.File, _ => isNotified = true, - predicate: _ => expectedResult) - .ExecuteWhileWaiting(() => - { - FileSystem.File.WriteAllText(path, null); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onCreated.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -404,15 +388,14 @@ public async Task OnDeleted_Directory_OtherEvent_ShouldNotTrigger(string path) { bool isNotified = false; + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true); + FileSystem.Directory.CreateDirectory(path); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.CreateDirectory(path); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onDeleted.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -427,15 +410,14 @@ public async Task OnDeleted_Directory_ShouldConsiderBasePath(string path1, strin FileSystem.Directory.CreateDirectory(path1); FileSystem.Directory.CreateDirectory(path2); + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true, path2); + FileSystem.Directory.Delete(path1); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true, path2) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.Delete(path1); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onDeleted.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -457,16 +439,15 @@ public async Task OnDeleted_Directory_ShouldConsiderGlobPattern( FileSystem.Directory.CreateDirectory(basePath); FileSystem.Directory.CreateDirectory(directoryPath); + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true, + globPattern); + FileSystem.Directory.Delete(directoryPath); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true, - globPattern) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.Delete(directoryPath); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onDeleted.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -488,13 +469,11 @@ public async Task OnDeleted_Directory_ShouldNotifyWhenDirectoryIsDeleted(string bool isNotified = false; FileSystem.Directory.CreateDirectory(path); - FileSystem.Notify - .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.Delete(path); - }) - .Wait(); + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true); + FileSystem.Directory.Delete(path); + + onDeleted.Wait(); await That(isNotified).IsTrue(); } @@ -507,16 +486,15 @@ public async Task OnDeleted_Directory_ShouldUsePredicate(bool expectedResult, st bool isNotified = false; FileSystem.Directory.CreateDirectory(path); + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true, + predicate: _ => expectedResult); + FileSystem.Directory.Delete(path); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnDeleted(FileSystemTypes.Directory, _ => isNotified = true, - predicate: _ => expectedResult) - .ExecuteWhileWaiting(() => - { - FileSystem.Directory.Delete(path); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onDeleted.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -537,15 +515,14 @@ public async Task OnDeleted_File_OtherEvent_ShouldNotTrigger(string path) { bool isNotified = false; + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true); + FileSystem.File.WriteAllText(path, null); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnDeleted(FileSystemTypes.File, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.File.WriteAllText(path, null); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onDeleted.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -560,15 +537,14 @@ public async Task OnDeleted_File_ShouldConsiderBasePath(string path1, string pat FileSystem.File.WriteAllText(path1, null); FileSystem.File.WriteAllText(path2, null); + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true, path2); + FileSystem.File.Delete(path1); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnDeleted(FileSystemTypes.File, _ => isNotified = true, path2) - .ExecuteWhileWaiting(() => - { - FileSystem.File.Delete(path1); - }) - .Wait(timeout: 50); + // ReSharper disable once AccessToDisposedClosure + onDeleted.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -590,16 +566,15 @@ public async Task OnDeleted_File_ShouldConsiderGlobPattern( FileSystem.Directory.CreateDirectory(directoryPath); FileSystem.File.WriteAllText(filePath, null); + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true, + globPattern); + FileSystem.File.Delete(filePath); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnDeleted(FileSystemTypes.File, _ => isNotified = true, - globPattern) - .ExecuteWhileWaiting(() => - { - FileSystem.File.Delete(filePath); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onDeleted.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -621,13 +596,11 @@ public async Task OnDeleted_File_ShouldNotifyWhenFileIsDeleted(string path) bool isNotified = false; FileSystem.File.WriteAllText(path, null); - FileSystem.Notify - .OnDeleted(FileSystemTypes.File, _ => isNotified = true) - .ExecuteWhileWaiting(() => - { - FileSystem.File.Delete(path); - }) - .Wait(); + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true); + FileSystem.File.Delete(path); + + onDeleted.Wait(); await That(isNotified).IsTrue(); } @@ -640,16 +613,15 @@ public async Task OnDeleted_File_ShouldUsePredicate(bool expectedResult, string bool isNotified = false; FileSystem.File.WriteAllText(path, null); + using IAwaitableCallback onDeleted = FileSystem.Notify + .OnDeleted(FileSystemTypes.File, _ => isNotified = true, + predicate: _ => expectedResult); + FileSystem.File.Delete(path); + Exception? exception = Record.Exception(() => { - FileSystem.Notify - .OnDeleted(FileSystemTypes.File, _ => isNotified = true, - predicate: _ => expectedResult) - .ExecuteWhileWaiting(() => - { - FileSystem.File.Delete(path); - }) - .Wait(timeout: expectedResult ? 30000 : 50); + // ReSharper disable once AccessToDisposedClosure + onDeleted.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs new file mode 100644 index 00000000..53f9f383 --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs @@ -0,0 +1,241 @@ +using System.Threading; + +namespace Testably.Abstractions.Testing.Tests; + +public partial class NotificationTests +{ + public sealed class WaitAsync + { + [Fact] + public async Task AwaitableCallback_Amount_ShouldOnlyReturnAfterNumberOfCallbacks() + { + MockTimeSystem timeSystem = new(); + int receivedCount = 0; + using IAwaitableCallback onThreadSleep = + timeSystem.On.ThreadSleep(t => + { + if (t.TotalMilliseconds > 0) + { + receivedCount++; + } + }); + + _ = Task.Run(async () => + { + await Task.Delay(10, TestContext.Current.CancellationToken); + for (int i = 1; i <= 10; i++) + { + timeSystem.Thread.Sleep(i); + await Task.Delay(1, TestContext.Current.CancellationToken); + } + }, TestContext.Current.CancellationToken); + + TimeSpan[] result = await onThreadSleep.WaitAsync(count: 7, + cancellationToken: TestContext.Current.CancellationToken); + await That(receivedCount).IsGreaterThanOrEqualTo(7); + await That(result.Length).IsEqualTo(7); + } + + [Fact] + public async Task + AwaitableCallback_CancellationTokenCancelled_ShouldThrowOperationCancelledException() + { + MockTimeSystem timeSystem = new(); + bool isCalled = false; + using ManualResetEventSlim ms = new(); + using IAwaitableCallback onThreadSleep = + timeSystem.On.ThreadSleep(_ => + { + isCalled = true; + }); + new Thread(() => + { + // ReSharper disable once AccessToDisposedClosure + try + { + // Delay larger than cancellation after 10ms + ms.Wait(TestContext.Current.CancellationToken); + timeSystem.Thread.Sleep(1); + } + catch (ObjectDisposedException) + { + // Ignore any ObjectDisposedException + } + }).Start(); + + Exception? exception = await Record.ExceptionAsync(async () => + { + using CancellationTokenSource cts = new(); + cts.CancelAfter(10); + await onThreadSleep.WaitAsync(cancellationToken: cts.Token); + }); + + await That(exception).IsExactly(); + await That(isCalled).IsFalse(); + ms.Set(); + } + + [Fact] + public async Task AwaitableCallback_ShouldWaitForCallbackExecution() + { + using ManualResetEventSlim ms = new(); + try + { + MockTimeSystem timeSystem = new(); + bool isCalled = false; + using IAwaitableCallback onThreadSleep = + timeSystem.On.ThreadSleep(_ => + { + isCalled = true; + }); + + _ = Task.Run(async () => + { + // ReSharper disable once AccessToDisposedClosure + try + { + while (!ms.IsSet) + { + timeSystem.Thread.Sleep(1); + await Task.Delay(1, TestContext.Current.CancellationToken); + } + } + catch (ObjectDisposedException) + { + // Ignore any ObjectDisposedException + } + }, TestContext.Current.CancellationToken); + + await onThreadSleep.WaitAsync( + cancellationToken: TestContext.Current.CancellationToken); + await That(isCalled).IsTrue(); + } + finally + { + ms.Set(); + } + } + + [Fact] + public async Task + AwaitableCallback_TimeoutExpired_ShouldThrowOperationCancelledException() + { + MockTimeSystem timeSystem = new(); + bool isCalled = false; + using ManualResetEventSlim ms = new(); + using IAwaitableCallback onThreadSleep = + timeSystem.On.ThreadSleep(_ => + { + isCalled = true; + }); + new Thread(() => + { + // ReSharper disable once AccessToDisposedClosure + try + { + // Delay larger than timeout of 10ms + ms.Wait(TestContext.Current.CancellationToken); + timeSystem.Thread.Sleep(1); + } + catch (ObjectDisposedException) + { + // Ignore any ObjectDisposedException + } + }).Start(); + + Exception? exception = await Record.ExceptionAsync(async () => + { + await onThreadSleep.WaitAsync(1, TimeSpan.FromMilliseconds(10), + TestContext.Current.CancellationToken); + }); + + await That(exception).IsExactly(); + await That(isCalled).IsFalse(); + ms.Set(); + } + + [Fact] + public async Task + AwaitableCallback_WaitAsync_AfterDispose_ShouldThrowObjectDisposedException() + { + MockTimeSystem timeSystem = new(); + IAwaitableCallback onThreadSleep = + timeSystem.On.ThreadSleep(); + + onThreadSleep.Dispose(); + + async Task Act() + { + await onThreadSleep.WaitAsync(timeout: 20000, + cancellationToken: TestContext.Current.CancellationToken); + } + + await That(Act).ThrowsExactly() + .WithMessage("The awaitable callback is already disposed."); + } + + [Fact] + public async Task AwaitableCallback_WaitedPreviously_ShouldWaitAgainForCallbackExecution() + { + int secondThreadMilliseconds = 42; + int firstThreadMilliseconds = secondThreadMilliseconds + 1; + using ManualResetEventSlim ms = new(); + MockTimeSystem timeSystem = new(); + using ManualResetEventSlim listening = new(); + using IAwaitableCallback onThreadSleep = + timeSystem.On.ThreadSleep(); + + _ = Task.Delay(50, TestContext.Current.CancellationToken) + .ContinueWith(_ => timeSystem.Thread.Sleep(firstThreadMilliseconds), + TestContext.Current.CancellationToken) + .ContinueWith( + async _ => await Task.Delay(20, TestContext.Current.CancellationToken), + TestContext.Current.CancellationToken) + .ContinueWith(_ => timeSystem.Thread.Sleep(secondThreadMilliseconds), + TestContext.Current.CancellationToken); + + TimeSpan[] result1 = + await onThreadSleep.WaitAsync( + cancellationToken: TestContext.Current.CancellationToken); + TimeSpan[] result2 = + await onThreadSleep.WaitAsync( + cancellationToken: TestContext.Current.CancellationToken); + await That(result1).HasSingle().Which + .Satisfies(f => f.Milliseconds == firstThreadMilliseconds); + await That(result2).HasSingle().Which + .Satisfies(f => f.Milliseconds == secondThreadMilliseconds); + } + + [Theory] + [AutoData] + public async Task Execute_ShouldBeExecutedBeforeWait(int milliseconds) + { + MockTimeSystem timeSystem = new(); + int receivedMilliseconds = -1; + + using IAwaitableCallback onThreadSleep = timeSystem.On + .ThreadSleep(t => + { + receivedMilliseconds = (int)t.TotalMilliseconds; + }); + timeSystem.Thread.Sleep(milliseconds); + await onThreadSleep.WaitAsync(cancellationToken: TestContext.Current.CancellationToken); + await That(receivedMilliseconds).IsEqualTo(milliseconds); + } + + [Fact] + public async Task ExecuteWhileWaiting_ShouldExecuteCallback() + { + MockTimeSystem timeSystem = new(); + + using IAwaitableCallback onThreadSleep = timeSystem.On + .ThreadSleep(); + + timeSystem.Thread.Sleep(10); + TimeSpan[] result = + await onThreadSleep.WaitAsync( + cancellationToken: TestContext.Current.CancellationToken); + await That(result).HasSingle().Which.Satisfies(f => f.Milliseconds == 10); + } + } +} diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs index ac1ff04b..a066814b 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs @@ -2,7 +2,7 @@ namespace Testably.Abstractions.Testing.Tests; -public class NotificationTests +public partial class NotificationTests { [Fact] public async Task AwaitableCallback_Amount_ShouldOnlyReturnAfterNumberOfCallbacks() @@ -28,7 +28,7 @@ public async Task AwaitableCallback_Amount_ShouldOnlyReturnAfterNumberOfCallback } }, TestContext.Current.CancellationToken); - wait.Wait(count: 7); + wait.Wait(7); await That(receivedCount).IsGreaterThanOrEqualTo(7); } @@ -60,8 +60,7 @@ public async Task AwaitableCallback_DisposeFromExecuteWhileWaiting_ShouldStopLis .ThreadSleep(_ => { isCalled = true; - }) - .ExecuteWhileWaiting(() => { }); + }); wait.Dispose(); @@ -71,6 +70,9 @@ public async Task AwaitableCallback_DisposeFromExecuteWhileWaiting_ShouldStopLis } [Fact] +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("TODO: Remove once the obsolete filter overload is removed from the public API.")] +#endif public async Task AwaitableCallback_Filter_ShouldOnlyUpdateAfterFilteredValue() { MockTimeSystem timeSystem = new(); @@ -207,20 +209,41 @@ public async Task AwaitableCallback_TimeoutExpired_ShouldThrowTimeoutException() } [Fact] - public async Task AwaitableCallback_Wait_AfterDispose_ShouldThrowObjectDisposedException() + public async Task + AwaitableCallback_Wait_AfterDispose_ShouldThrowObjectDisposedException() { MockTimeSystem timeSystem = new(); - IAwaitableCallback wait = + IAwaitableCallback onThreadSleep = timeSystem.On.ThreadSleep(); - wait.Dispose(); + onThreadSleep.Dispose(); - Exception? exception = Record.Exception(() => + void Act() { - wait.Wait(timeout: 100); - }); + onThreadSleep.Wait(); + } + + await That(Act).ThrowsExactly() + .WithMessage("The awaitable callback is already disposed."); + } + + [Fact] + public async Task + AwaitableCallback_Wait_WithCount_AfterDispose_ShouldThrowObjectDisposedException() + { + MockTimeSystem timeSystem = new(); + IAwaitableCallback onThreadSleep = + timeSystem.On.ThreadSleep(); + + onThreadSleep.Dispose(); + + void Act() + { + onThreadSleep.Wait(1, TimeSpan.FromSeconds(20)); + } - await That(exception).IsExactly(); + await That(Act).ThrowsExactly() + .WithMessage("The awaitable callback is already disposed."); } [Fact] @@ -240,17 +263,6 @@ public async Task AwaitableCallback_WaitedPreviously_ShouldWaitAgainForCallbackE { isCalledFromSecondThread = true; } - }).ExecuteWhileWaiting(() => - { - // ReSharper disable once AccessToDisposedClosure - try - { - listening.Set(); - } - catch (ObjectDisposedException) - { - // Ignore any ObjectDisposedException - } }); new Thread(() => { @@ -265,6 +277,7 @@ public async Task AwaitableCallback_WaitedPreviously_ShouldWaitAgainForCallbackE // Ignore any ObjectDisposedException } }).Start(); + listening.Set(); wait.Wait(); listening.Reset(); @@ -294,6 +307,9 @@ public async Task AwaitableCallback_WaitedPreviously_ShouldWaitAgainForCallbackE [Theory] [AutoData] +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("TODO: Remove once the obsolete ExecuteWhileWaiting method is removed from the public API.")] +#endif public async Task Execute_ShouldBeExecutedBeforeWait(int milliseconds) { MockTimeSystem timeSystem = new(); @@ -320,6 +336,9 @@ public async Task Execute_ShouldBeExecutedBeforeWait(int milliseconds) [Theory] [AutoData] +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("TODO: Remove once the obsolete ExecuteWhileWaiting method is removed from the public API.")] +#endif public async Task Execute_WithReturnValue_ShouldBeExecutedAndReturnValue( int milliseconds, string result) { @@ -348,6 +367,9 @@ public async Task Execute_WithReturnValue_ShouldBeExecutedAndReturnValue( } [Fact] +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("TODO: Remove once the obsolete ExecuteWhileWaiting method is removed from the public API.")] +#endif public async Task ExecuteWhileWaiting_ShouldExecuteCallback() { MockTimeSystem timeSystem = new(); @@ -367,6 +389,9 @@ public async Task ExecuteWhileWaiting_ShouldExecuteCallback() [Theory] [AutoData] +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("TODO: Remove once the obsolete ExecuteWhileWaiting method is removed from the public API.")] +#endif public async Task ExecuteWhileWaiting_WithReturnValue_ShouldExecuteCallback(int result) { MockTimeSystem timeSystem = new();