From fa8fc34b776cdfa9451eae3eeb209f1b54b59085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Sun, 15 Feb 2026 07:47:22 +0100 Subject: [PATCH 1/7] feat: support async wait handlers --- .../AwaitableCallbackExtensions.cs | 53 +++ .../Helpers/ExceptionFactory.cs | 3 + .../IAwaitableCallback.cs | 43 ++- .../Notification.cs | 153 +++++++- .../Testably.Abstractions.Testing_net10.0.txt | 166 +++++---- .../Testably.Abstractions.Testing_net6.0.txt | 144 ++++---- .../Testably.Abstractions.Testing_net8.0.txt | 166 +++++---- .../Testably.Abstractions.Testing_net9.0.txt | 166 +++++---- ...ly.Abstractions.Testing_netstandard2.0.txt | 140 ++++---- ...ly.Abstractions.Testing_netstandard2.1.txt | 140 ++++---- .../FileSystem/ChangeHandlerTests.cs | 39 +- .../NotificationHandlerExtensionsTests.cs | 340 ++++++++---------- .../NotificationTests.WaitAsync.cs | 188 ++++++++++ .../NotificationTests.cs | 34 +- 14 files changed, 1111 insertions(+), 664 deletions(-) create mode 100644 Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs create mode 100644 Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs diff --git a/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs b/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs new file mode 100644 index 000000000..1d3673466 --- /dev/null +++ b/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +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); +} diff --git a/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs b/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs index 6f0270306..e39ca8d08 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 5a071ce8e..1538eb61d 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,45 @@ public interface IAwaitableCallback : IDisposable /// /// (optional) A callback to execute when waiting started. /// - void Wait(Func? filter = null, + [Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of the awaitable callback.")] + void Wait(Func? filter, 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 = 1, 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 5e38aa398..3a083df2e 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,7 @@ public static class Notification /// Executes the while waiting for the notification. ///
/// The callback. + [Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static IAwaitableCallback ExecuteWhileWaiting( this IAwaitableCallback awaitable, Action callback) { @@ -29,6 +32,7 @@ public static IAwaitableCallback ExecuteWhileWaiting( /// Executes the while waiting for the notification. /// /// The callback. + [Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static IAwaitableCallback ExecuteWhileWaiting( this IAwaitableCallback awaitable, Func callback) { @@ -75,15 +79,19 @@ private void UnRegisterCallback(Guid key) private sealed class CallbackWaiter : IAwaitableCallback { private readonly Action? _callback; + private readonly Channel _channel = Channel.CreateUnbounded(); private int _count; private readonly NotificationFactory _factory; private Func? _filter; + private 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 +99,7 @@ public CallbackWaiter(NotificationFactory factory, _callback = callback; _predicate = predicate ?? (_ => true); _reset = new ManualResetEventSlim(); + _writer = _channel.Writer; } #region IAwaitableCallback Members @@ -100,10 +109,11 @@ public void Dispose() { _factory.UnRegisterCallback(_key); _reset.Dispose(); + _isDisposed = true; } /// - public void Wait(Func? filter = null, + public void Wait(Func? filter, int timeout = 30000, int count = 1, Action? executeWhenWaiting = null) @@ -111,19 +121,121 @@ public void Wait(Func? filter = null, _count = count; _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 = 1, TimeSpan? timeout = null) + { + _count = count; + _reset.Reset(); + + TValue[]? result = null; + _ = Task.Run(async () => + { + try + { + result = await WaitAsync(count, timeout); + } + catch + { + // Ignore exceptions as they will be handled by the timeout or cancellation token + } + finally + { + _reset.Set(); + } + }); + + TimeSpan timeoutOrDefault = timeout ?? TimeSpan.FromSeconds(30); + if (!_reset.Wait(timeoutOrDefault) || + result is null) + { + throw ExceptionFactory.TimeoutExpired(timeoutOrDefault); + } + + return result; + } + + /// + public async Task WaitAsync( + int count = 1, + TimeSpan? timeout = null, + CancellationToken? cancellationToken = null) + { + if (_isDisposed) + { + throw new ObjectDisposedException( + "The awaitable callback is already disposed."); + } + + List values = []; + _count = count; + 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 (Interlocked.Decrement(ref _count) <= 0) + { + break; + } + } + } while (!cancellationToken.Value.IsCancellationRequested); + } + finally + { + cts?.Dispose(); + } + + return values.ToArray(); + } + #endregion /// @@ -138,11 +250,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 +269,8 @@ IAwaitableCallback RegisterCallback( /// - un-registering a callback by calling
/// - blocking for the callback to be executed ///
- public interface IAwaitableCallback + [Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", error: true)] + public interface IAwaitableCallback : IAwaitableCallback { /// @@ -187,12 +296,13 @@ public interface IAwaitableCallback /// /// (optional) A callback to execute when waiting started. /// - new TFunc Wait(Func? filter = null, + new TFunc Wait(Func? filter, int timeout = 30000, int count = 1, Action? executeWhenWaiting = null); } + [Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", error: true)] private sealed class CallbackWaiterWithValue : IAwaitableCallback { @@ -213,7 +323,9 @@ public void Dispose() => _awaitableCallback.Dispose(); /// - public TFunc Wait(Func? filter = null, + [Obsolete( + "Use another `Wait` or `WaitAsync` overload and move the filter to the creation of the awaitable callback.")] + public TFunc Wait(Func? filter, int timeout = 30000, int count = 1, Action? executeWhenWaiting = null) @@ -227,6 +339,21 @@ public TFunc Wait(Func? filter = null, return value; } + /// + public TValue[] Wait(int count = 1, 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 1158dfa7d..b07a9e22b 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,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(".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 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 +30,13 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { - void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); + [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + + " the awaitable callback.")] + void Wait(System.Func? filter, 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 { @@ -177,11 +112,14 @@ namespace Testably.Abstractions.Testing } public static class Notification { + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] 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 + [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] + 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); + TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions @@ -225,6 +163,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 9081cbde5..4b7ff4720 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,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(".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 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 +30,13 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { - void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); + [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + + " the awaitable callback.")] + void Wait(System.Func? filter, 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 { @@ -165,11 +111,14 @@ namespace Testably.Abstractions.Testing } public static class Notification { + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] 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 + [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] + 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); + TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions @@ -213,6 +162,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 2164c13d5..2e7bea78a 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,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(".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 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 +30,13 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { - void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); + [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + + " the awaitable callback.")] + void Wait(System.Func? filter, 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 { @@ -177,11 +112,14 @@ namespace Testably.Abstractions.Testing } public static class Notification { + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] 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 + [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] + 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); + TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions @@ -225,6 +163,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 c23262e1f..df78ebc15 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,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(".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 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 +30,13 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { - void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); + [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + + " the awaitable callback.")] + void Wait(System.Func? filter, 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 { @@ -177,11 +112,14 @@ namespace Testably.Abstractions.Testing } public static class Notification { + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] 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 + [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] + 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); + TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions @@ -225,6 +163,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 24ffa35d5..1eac3544f 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,13 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { - void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); + [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + + " the awaitable callback.")] + void Wait(System.Func? filter, 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 { @@ -162,11 +110,14 @@ namespace Testably.Abstractions.Testing } public static class Notification { + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] 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 + [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] + 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); + TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions @@ -210,6 +161,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 aeacef404..352960354 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,13 @@ namespace Testably.Abstractions.Testing File = 2, DirectoryOrFile = 3, } - public interface IAwaitableCallback : System.IDisposable + public interface IAwaitableCallback : System.IDisposable { - void Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); + [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + + " the awaitable callback.")] + void Wait(System.Func? filter, 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 { @@ -162,11 +110,14 @@ namespace Testably.Abstractions.Testing } public static class Notification { + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } + [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] 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 + [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] + 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); + TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions @@ -210,6 +161,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/FileSystem/ChangeHandlerTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs index e2ec3568b..fbe1f7ced 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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(5000)); await That(isTriggered).IsTrue(); } diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs index aa459c547..41c0f9c03 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs @@ -1,3 +1,5 @@ +using Testably.Abstractions.Testing.FileSystem; + namespace Testably.Abstractions.Testing.Tests; public class NotificationHandlerExtensionsTests @@ -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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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: TimeSpan.FromMilliseconds(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 000000000..cf9a99385 --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs @@ -0,0 +1,188 @@ +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_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_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 timeout of 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_WaitAsync_AfterDispose_ShouldThrowObjectDisposedException() + { + MockTimeSystem timeSystem = new(); + IAwaitableCallback onThreadSleep = + timeSystem.On.ThreadSleep(); + + onThreadSleep.Dispose(); + + Exception? exception = await Record.ExceptionAsync(async () => + { + await onThreadSleep.WaitAsync(cancellationToken: TestContext.Current.CancellationToken); + }); + + await That(exception).IsExactly(); + } + + [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) + .ContinueWith(_ => timeSystem.Thread.Sleep(secondThreadMilliseconds), TestContext.Current.CancellationToken); + + var result1 = await onThreadSleep.WaitAsync(cancellationToken: TestContext.Current.CancellationToken); + var 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 ac1ff04b9..e5e826931 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() @@ -60,8 +60,7 @@ public async Task AwaitableCallback_DisposeFromExecuteWhileWaiting_ShouldStopLis .ThreadSleep(_ => { isCalled = true; - }) - .ExecuteWhileWaiting(() => { }); + }); wait.Dispose(); @@ -71,6 +70,7 @@ public async Task AwaitableCallback_DisposeFromExecuteWhileWaiting_ShouldStopLis } [Fact] + [Obsolete("TODO: Remove once the obsolete filter overload is removed from the public API.")] public async Task AwaitableCallback_Filter_ShouldOnlyUpdateAfterFilteredValue() { MockTimeSystem timeSystem = new(); @@ -198,7 +198,7 @@ public async Task AwaitableCallback_TimeoutExpired_ShouldThrowTimeoutException() Exception? exception = Record.Exception(() => { - wait.Wait(timeout: 10); + wait.Wait(timeout: TimeSpan.FromMilliseconds(10)); }); await That(exception).IsExactly(); @@ -217,7 +217,7 @@ public async Task AwaitableCallback_Wait_AfterDispose_ShouldThrowObjectDisposedE Exception? exception = Record.Exception(() => { - wait.Wait(timeout: 100); + wait.Wait(timeout: TimeSpan.FromMilliseconds(100)); }); await That(exception).IsExactly(); @@ -240,17 +240,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 +254,7 @@ public async Task AwaitableCallback_WaitedPreviously_ShouldWaitAgainForCallbackE // Ignore any ObjectDisposedException } }).Start(); + listening.Set(); wait.Wait(); listening.Reset(); @@ -291,9 +281,10 @@ public async Task AwaitableCallback_WaitedPreviously_ShouldWaitAgainForCallbackE ms.Set(); await That(isCalledFromSecondThread).IsTrue(); } - + [Theory] [AutoData] + [Obsolete("TODO: Remove once the obsolete ExecuteWhileWaiting method is removed from the public API.")] public async Task Execute_ShouldBeExecutedBeforeWait(int milliseconds) { MockTimeSystem timeSystem = new(); @@ -309,7 +300,7 @@ public async Task Execute_ShouldBeExecutedBeforeWait(int milliseconds) { timeSystem.Thread.Sleep(milliseconds); }) - .Wait(executeWhenWaiting: () => + .Wait(null, executeWhenWaiting: () => { isExecuted = true; }); @@ -320,6 +311,7 @@ public async Task Execute_ShouldBeExecutedBeforeWait(int milliseconds) [Theory] [AutoData] + [Obsolete("TODO: Remove once the obsolete ExecuteWhileWaiting method is removed from the public API.")] public async Task Execute_WithReturnValue_ShouldBeExecutedAndReturnValue( int milliseconds, string result) { @@ -337,7 +329,7 @@ public async Task Execute_WithReturnValue_ShouldBeExecutedAndReturnValue( timeSystem.Thread.Sleep(milliseconds); return result; }) - .Wait(executeWhenWaiting: () => + .Wait(null, executeWhenWaiting: () => { isExecuted = true; }); @@ -348,6 +340,7 @@ public async Task Execute_WithReturnValue_ShouldBeExecutedAndReturnValue( } [Fact] + [Obsolete("TODO: Remove once the obsolete ExecuteWhileWaiting method is removed from the public API.")] public async Task ExecuteWhileWaiting_ShouldExecuteCallback() { MockTimeSystem timeSystem = new(); @@ -367,6 +360,7 @@ public async Task ExecuteWhileWaiting_ShouldExecuteCallback() [Theory] [AutoData] + [Obsolete("TODO: Remove once the obsolete ExecuteWhileWaiting method is removed from the public API.")] public async Task ExecuteWhileWaiting_WithReturnValue_ShouldExecuteCallback(int result) { MockTimeSystem timeSystem = new(); @@ -380,7 +374,7 @@ public async Task ExecuteWhileWaiting_WithReturnValue_ShouldExecuteCallback(int timeSystem.Thread.Sleep(10); return result; }) - .Wait(); + .Wait(null); await That(actualResult).IsEqualTo(result); await That(isExecuted).IsTrue(); From ef00753c0c918d49ec586a4a29e51ca019450c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 18 Feb 2026 13:54:49 +0100 Subject: [PATCH 2/7] Fix review issues --- .../Notification.cs | 30 +++++++++++-------- .../NotificationTests.WaitAsync.cs | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Source/Testably.Abstractions.Testing/Notification.cs b/Source/Testably.Abstractions.Testing/Notification.cs index 3a083df2e..f52891c90 100644 --- a/Source/Testably.Abstractions.Testing/Notification.cs +++ b/Source/Testably.Abstractions.Testing/Notification.cs @@ -80,10 +80,9 @@ private sealed class CallbackWaiter : IAwaitableCallback { private readonly Action? _callback; private readonly Channel _channel = Channel.CreateUnbounded(); - private int _count; private readonly NotificationFactory _factory; private Func? _filter; - private bool _isDisposed; + private volatile bool _isDisposed; private readonly Guid _key; private readonly Func _predicate; private readonly ManualResetEventSlim _reset; @@ -108,6 +107,7 @@ public CallbackWaiter(NotificationFactory factory, public void Dispose() { _factory.UnRegisterCallback(_key); + _writer.TryComplete(); _reset.Dispose(); _isDisposed = true; } @@ -118,7 +118,12 @@ public void Wait(Func? filter, int count = 1, Action? executeWhenWaiting = null) { - _count = count; + if (_isDisposed) + { + throw new ObjectDisposedException( + "The awaitable callback is already disposed."); + } + _filter = filter; _reset.Reset(); if (executeWhenWaiting != null) @@ -153,20 +158,21 @@ public void Wait(Func? filter, /// public TValue[] Wait(int count = 1, TimeSpan? timeout = null) { - _count = count; + if (_isDisposed) + { + throw new ObjectDisposedException( + "The awaitable callback is already disposed."); + } + _reset.Reset(); TValue[]? result = null; - _ = Task.Run(async () => + Task task = Task.Run(async () => { try { result = await WaitAsync(count, timeout); } - catch - { - // Ignore exceptions as they will be handled by the timeout or cancellation token - } finally { _reset.Set(); @@ -177,7 +183,8 @@ public TValue[] Wait(int count = 1, TimeSpan? timeout = null) if (!_reset.Wait(timeoutOrDefault) || result is null) { - throw ExceptionFactory.TimeoutExpired(timeoutOrDefault); + throw task.Exception?.InnerException ?? + ExceptionFactory.TimeoutExpired(timeoutOrDefault); } return result; @@ -196,7 +203,6 @@ public async Task WaitAsync( } List values = []; - _count = count; ChannelReader reader = _channel.Reader; CancellationTokenSource? cts = null; @@ -221,7 +227,7 @@ public async Task WaitAsync( if (_filter?.Invoke(value) != false) { values.Add(value); - if (Interlocked.Decrement(ref _count) <= 0) + if (--count <= 0) { break; } diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs index cf9a99385..8a82013ac 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs @@ -145,7 +145,7 @@ public async Task AwaitableCallback_WaitedPreviously_ShouldWaitAgainForCallbackE _ = Task.Delay(50, TestContext.Current.CancellationToken) .ContinueWith(_ => timeSystem.Thread.Sleep(firstThreadMilliseconds), TestContext.Current.CancellationToken) - .ContinueWith(async _ => await Task.Delay(20), TestContext.Current.CancellationToken) + .ContinueWith(async _ => await Task.Delay(20, TestContext.Current.CancellationToken), TestContext.Current.CancellationToken) .ContinueWith(_ => timeSystem.Thread.Sleep(secondThreadMilliseconds), TestContext.Current.CancellationToken); var result1 = await onThreadSleep.WaitAsync(cancellationToken: TestContext.Current.CancellationToken); From 0064039bd3ba68c523b976822f123b16156e728a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 18 Feb 2026 18:18:26 +0100 Subject: [PATCH 3/7] Add `ToAsyncEnumerable` extension methods for .NET6 or higher --- .../AwaitableCallbackExtensions.cs | 70 +++++++- .../FileSystem/INotificationHandler.cs | 2 +- .../Testably.Abstractions.Testing_net10.0.txt | 4 + .../Testably.Abstractions.Testing_net6.0.txt | 4 + .../Testably.Abstractions.Testing_net8.0.txt | 4 + .../Testably.Abstractions.Testing_net9.0.txt | 4 + .../AwaitableCallbackExtensionsTests.cs | 154 ++++++++++++++++++ 7 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs diff --git a/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs b/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs index 1d3673466..19097c800 100644 --- a/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs +++ b/Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs @@ -1,11 +1,15 @@ 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 . +/// Extension methods on . /// public static class AwaitableCallbackExtensions { @@ -50,4 +54,68 @@ public static Task WaitAsync(this IAwaitableCallback c 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 3b0632e13..ae30b28b2 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/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 b07a9e22b..81b400cb8 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 @@ -6,6 +6,10 @@ namespace Testably.Abstractions.Testing { public static class AwaitableCallbackExtensions { + 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) { } } 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 4b7ff4720..142647dee 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 @@ -6,6 +6,10 @@ namespace Testably.Abstractions.Testing { public static class AwaitableCallbackExtensions { + 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) { } } 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 2e7bea78a..f61d20687 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 @@ -6,6 +6,10 @@ namespace Testably.Abstractions.Testing { public static class AwaitableCallbackExtensions { + 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) { } } 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 df78ebc15..d6646ab97 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 @@ -6,6 +6,10 @@ namespace Testably.Abstractions.Testing { public static class AwaitableCallbackExtensions { + 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) { } } diff --git a/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs b/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs new file mode 100644 index 000000000..62fea13ed --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs @@ -0,0 +1,154 @@ +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().Take(6).ToArrayAsync(); + + 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); + if (i == 5) + { + // ReSharper disable once AccessToDisposedClosure + cts.Cancel(); + } + } + }); + + ChangeDescription[] results = await sut + .ToAsyncEnumerable(cancellationToken: token).Take(10).ToArrayAsync(); + + 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().Take(2).ToArrayAsync(); + + 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 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); + } + }); + + ChangeDescription[] results = await sut + .ToAsyncEnumerable(TimeSpan.FromMilliseconds(150)).Take(10).ToArrayAsync(); + + await That(results.Length).IsLessThan(9); + } + + [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); + } + }); + + ChangeDescription[] results = await sut + .ToAsyncEnumerable(150).Take(10).ToArrayAsync(); + + await That(results.Length).IsLessThan(9); + } + } +#endif +} From 02908063e179f69babf8d2418ae2b4dd86442574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 18 Feb 2026 18:53:33 +0100 Subject: [PATCH 4/7] Remove obsolete attribute --- .../IAwaitableCallback.cs | 2 ++ Source/Testably.Abstractions.Testing/Notification.cs | 12 ++++++++++-- .../Testably.Abstractions.Testing_net10.0.txt | 5 ----- .../Testably.Abstractions.Testing_net6.0.txt | 5 ----- .../Testably.Abstractions.Testing_net8.0.txt | 5 ----- .../Testably.Abstractions.Testing_net9.0.txt | 5 ----- .../Testably.Abstractions.Testing_netstandard2.0.txt | 5 ----- .../Testably.Abstractions.Testing_netstandard2.1.txt | 5 ----- .../NotificationTests.cs | 10 ++++++++++ 9 files changed, 22 insertions(+), 32 deletions(-) diff --git a/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs b/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs index 1538eb61d..a4032ac52 100644 --- a/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs +++ b/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs @@ -32,7 +32,9 @@ 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, int timeout = 30000, int count = 1, diff --git a/Source/Testably.Abstractions.Testing/Notification.cs b/Source/Testably.Abstractions.Testing/Notification.cs index f52891c90..1f0779a87 100644 --- a/Source/Testably.Abstractions.Testing/Notification.cs +++ b/Source/Testably.Abstractions.Testing/Notification.cs @@ -17,7 +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) { @@ -32,7 +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) { @@ -275,7 +279,9 @@ IAwaitableCallback RegisterCallback( /// - un-registering a callback by calling
/// - blocking for the callback to be executed /// - [Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", error: true)] +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.")] +#endif public interface IAwaitableCallback : IAwaitableCallback { @@ -308,7 +314,9 @@ public interface IAwaitableCallback Action? executeWhenWaiting = null); } - [Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", error: true)] +#if MarkExecuteWhileWaitingNotificationObsolete + [Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.")] +#endif private sealed class CallbackWaiterWithValue : IAwaitableCallback { 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 81b400cb8..b4d037c35 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 @@ -37,8 +37,6 @@ namespace Testably.Abstractions.Testing public interface IAwaitableCallback : System.IDisposable { TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + - " the awaitable callback.")] void Wait(System.Func? filter, 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); } @@ -116,11 +114,8 @@ namespace Testably.Abstractions.Testing } public static class Notification { - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt index 142647dee..63a992cc0 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 @@ -37,8 +37,6 @@ namespace Testably.Abstractions.Testing public interface IAwaitableCallback : System.IDisposable { TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + - " the awaitable callback.")] void Wait(System.Func? filter, 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); } @@ -115,11 +113,8 @@ namespace Testably.Abstractions.Testing } public static class Notification { - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt index f61d20687..045c53546 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 @@ -37,8 +37,6 @@ namespace Testably.Abstractions.Testing public interface IAwaitableCallback : System.IDisposable { TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + - " the awaitable callback.")] void Wait(System.Func? filter, 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); } @@ -116,11 +114,8 @@ namespace Testably.Abstractions.Testing } public static class Notification { - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt index d6646ab97..9199aa9ee 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 @@ -37,8 +37,6 @@ namespace Testably.Abstractions.Testing public interface IAwaitableCallback : System.IDisposable { TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + - " the awaitable callback.")] void Wait(System.Func? filter, 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); } @@ -116,11 +114,8 @@ namespace Testably.Abstractions.Testing } public static class Notification { - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt index 1eac3544f..dbe2b4ca9 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 @@ -33,8 +33,6 @@ namespace Testably.Abstractions.Testing public interface IAwaitableCallback : System.IDisposable { TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + - " the awaitable callback.")] void Wait(System.Func? filter, 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); } @@ -110,11 +108,8 @@ namespace Testably.Abstractions.Testing } public static class Notification { - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt index 352960354..2a940f455 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 @@ -33,8 +33,6 @@ namespace Testably.Abstractions.Testing public interface IAwaitableCallback : System.IDisposable { TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - [System.Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of" + - " the awaitable callback.")] void Wait(System.Func? filter, 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); } @@ -110,11 +108,8 @@ namespace Testably.Abstractions.Testing } public static class Notification { - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Action callback) { } - [System.Obsolete("Execute the callback before calling `Wait` or `WaitAsync` instead.")] public static Testably.Abstractions.Testing.Notification.IAwaitableCallback ExecuteWhileWaiting(this Testably.Abstractions.Testing.IAwaitableCallback awaitable, System.Func callback) { } - [System.Obsolete("Will be removed when `ExecuteWhileWaiting` is removed.", true)] public interface IAwaitableCallback : System.IDisposable, Testably.Abstractions.Testing.IAwaitableCallback { TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs index e5e826931..49739f9b4 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs @@ -70,7 +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(); @@ -284,7 +286,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(); @@ -311,7 +315,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) { @@ -340,7 +346,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(); @@ -360,7 +368,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(); From be6e3d80b6d7cd0de9a556907363f2eca18a2497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Wed, 18 Feb 2026 18:56:21 +0100 Subject: [PATCH 5/7] Fix sonar issues about missing use of CancellationToken in tests --- .../AwaitableCallbackExtensionsTests.cs | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs b/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs index 62fea13ed..cfd49ebc5 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/AwaitableCallbackExtensionsTests.cs @@ -21,7 +21,10 @@ public async Task ShouldReturnAllEvents() fileSystem.File.Delete("Test/abc.txt"); fileSystem.Directory.Delete("Test"); - ChangeDescription[] results = await sut.ToAsyncEnumerable().Take(6).ToArrayAsync(); + ChangeDescription[] results = await sut + .ToAsyncEnumerable(cancellationToken: TestContext.Current.CancellationToken) + .Take(6) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); await That(results[0]).IsEquivalentTo(new { @@ -63,17 +66,18 @@ public async Task WithCancelledToken_ShouldAbort() for (int i = 0; i < 10; i++) { fileSystem.Directory.CreateDirectory($"Test{i}"); - await Task.Delay(100); + 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(); + ChangeDescription[] results = await sut.ToAsyncEnumerable(cancellationToken: token) + .Take(10) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); await That(results.Length).IsEqualTo(6); } @@ -90,7 +94,10 @@ public async Task WithFilter_ShouldOnlyReturnMatchingEvents() fileSystem.File.Delete("Test/abc.txt"); fileSystem.Directory.Delete("Test"); - ChangeDescription[] results = await sut.ToAsyncEnumerable().Take(2).ToArrayAsync(); + ChangeDescription[] results = await sut + .ToAsyncEnumerable(cancellationToken: TestContext.Current.CancellationToken) + .Take(2) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); await That(results[0]).IsEquivalentTo(new { @@ -107,7 +114,7 @@ await That(results[1]).IsEquivalentTo(new } [Fact] - public async Task WithTimeout_ShouldAbortAfterwards() + public async Task WithIntTimeout_ShouldAbortAfterwards() { MockFileSystem fileSystem = new(); IAwaitableCallback sut = fileSystem.Notify @@ -118,18 +125,20 @@ public async Task WithTimeout_ShouldAbortAfterwards() for (int i = 0; i < 10; i++) { fileSystem.Directory.CreateDirectory($"Test{i}"); - await Task.Delay(100); + await Task.Delay(100, TestContext.Current.CancellationToken); } - }); + }, TestContext.Current.CancellationToken); ChangeDescription[] results = await sut - .ToAsyncEnumerable(TimeSpan.FromMilliseconds(150)).Take(10).ToArrayAsync(); + .ToAsyncEnumerable(150, cancellationToken: TestContext.Current.CancellationToken) + .Take(10) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); await That(results.Length).IsLessThan(9); } [Fact] - public async Task WithIntTimeout_ShouldAbortAfterwards() + public async Task WithTimeout_ShouldAbortAfterwards() { MockFileSystem fileSystem = new(); IAwaitableCallback sut = fileSystem.Notify @@ -140,12 +149,15 @@ public async Task WithIntTimeout_ShouldAbortAfterwards() for (int i = 0; i < 10; i++) { fileSystem.Directory.CreateDirectory($"Test{i}"); - await Task.Delay(100); + await Task.Delay(100, TestContext.Current.CancellationToken); } - }); + }, TestContext.Current.CancellationToken); ChangeDescription[] results = await sut - .ToAsyncEnumerable(150).Take(10).ToArrayAsync(); + .ToAsyncEnumerable(TimeSpan.FromMilliseconds(150), + cancellationToken: TestContext.Current.CancellationToken) + .Take(10) + .ToArrayAsync(cancellationToken: TestContext.Current.CancellationToken); await That(results.Length).IsLessThan(9); } From ffca77d61a574ee2e10ab1ad2a3718f5b87154c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Thu, 19 Feb 2026 20:25:11 +0100 Subject: [PATCH 6/7] Reduce change surface --- .../IAwaitableCallback.cs | 4 +-- .../Notification.cs | 12 ++++--- .../Testably.Abstractions.Testing_net10.0.txt | 6 ++-- .../Testably.Abstractions.Testing_net6.0.txt | 6 ++-- .../Testably.Abstractions.Testing_net8.0.txt | 6 ++-- .../Testably.Abstractions.Testing_net9.0.txt | 6 ++-- ...ly.Abstractions.Testing_netstandard2.0.txt | 6 ++-- ...ly.Abstractions.Testing_netstandard2.1.txt | 6 ++-- .../FileSystem/ChangeHandlerTests.cs | 4 +-- .../NotificationHandlerExtensionsTests.cs | 36 +++++++++---------- .../NotificationTests.cs | 6 ++-- 11 files changed, 50 insertions(+), 48 deletions(-) diff --git a/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs b/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs index a4032ac52..ad6673d07 100644 --- a/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs +++ b/Source/Testably.Abstractions.Testing/IAwaitableCallback.cs @@ -35,7 +35,7 @@ public interface IAwaitableCallback : IDisposable #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, + void Wait(Func? filter = null, int timeout = 30000, int count = 1, Action? executeWhenWaiting = null); @@ -54,7 +54,7 @@ void Wait(Func? filter, /// (optional) The timeout to wait on the callback.
/// If not specified (), defaults to 30 seconds. /// - TValue[] Wait(int count = 1, TimeSpan? timeout = null); + TValue[] Wait(int count, TimeSpan? timeout = null); /// /// Waits asynchronously until the callback is executed. diff --git a/Source/Testably.Abstractions.Testing/Notification.cs b/Source/Testably.Abstractions.Testing/Notification.cs index 1f0779a87..cd09d0f16 100644 --- a/Source/Testably.Abstractions.Testing/Notification.cs +++ b/Source/Testably.Abstractions.Testing/Notification.cs @@ -117,7 +117,7 @@ public void Dispose() } /// - public void Wait(Func? filter, + public void Wait(Func? filter = null, int timeout = 30000, int count = 1, Action? executeWhenWaiting = null) @@ -160,7 +160,7 @@ public void Wait(Func? filter, } /// - public TValue[] Wait(int count = 1, TimeSpan? timeout = null) + public TValue[] Wait(int count, TimeSpan? timeout = null) { if (_isDisposed) { @@ -308,7 +308,7 @@ public interface IAwaitableCallback /// /// (optional) A callback to execute when waiting started. /// - new TFunc Wait(Func? filter, + new TFunc Wait(Func? filter = null, int timeout = 30000, int count = 1, Action? executeWhenWaiting = null); @@ -337,9 +337,11 @@ 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.")] - public TFunc Wait(Func? filter, +#endif + public TFunc Wait(Func? filter = null, int timeout = 30000, int count = 1, Action? executeWhenWaiting = null) @@ -354,7 +356,7 @@ public TFunc Wait(Func? filter, } /// - public TValue[] Wait(int count = 1, TimeSpan? timeout = null) + public TValue[] Wait(int count, TimeSpan? timeout = null) { _valueProvider(); return _awaitableCallback.Wait(count, 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 b4d037c35..b95e0ef5e 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 @@ -36,8 +36,8 @@ namespace Testably.Abstractions.Testing } public interface IAwaitableCallback : System.IDisposable { - TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - void Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + 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 @@ -118,7 +118,7 @@ namespace Testably.Abstractions.Testing 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 { - TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions 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 63a992cc0..5fe7bb85e 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 @@ -36,8 +36,8 @@ namespace Testably.Abstractions.Testing } public interface IAwaitableCallback : System.IDisposable { - TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - void Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + 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 @@ -117,7 +117,7 @@ namespace Testably.Abstractions.Testing 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 { - TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions 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 045c53546..e311aeb0c 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 @@ -36,8 +36,8 @@ namespace Testably.Abstractions.Testing } public interface IAwaitableCallback : System.IDisposable { - TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - void Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + 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 @@ -118,7 +118,7 @@ namespace Testably.Abstractions.Testing 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 { - TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions 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 9199aa9ee..d57e60d7e 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 @@ -36,8 +36,8 @@ namespace Testably.Abstractions.Testing } public interface IAwaitableCallback : System.IDisposable { - TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - void Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + 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 @@ -118,7 +118,7 @@ namespace Testably.Abstractions.Testing 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 { - TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions 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 dbe2b4ca9..87ad3515b 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 @@ -32,8 +32,8 @@ namespace Testably.Abstractions.Testing } public interface IAwaitableCallback : System.IDisposable { - TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - void Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + 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 @@ -112,7 +112,7 @@ namespace Testably.Abstractions.Testing 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 { - TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions 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 2a940f455..9bf2c0691 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 @@ -32,8 +32,8 @@ namespace Testably.Abstractions.Testing } public interface IAwaitableCallback : System.IDisposable { - TValue[] Wait(int count = 1, System.TimeSpan? timeout = default); - void Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + 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 @@ -112,7 +112,7 @@ namespace Testably.Abstractions.Testing 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 { - TFunc Wait(System.Func? filter, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); + TFunc Wait(System.Func? filter = null, int timeout = 30000, int count = 1, System.Action? executeWhenWaiting = null); } } public static class NotificationHandlerExtensions diff --git a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs index fbe1f7ced..4d7751778 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/ChangeHandlerTests.cs @@ -109,7 +109,7 @@ public async Task Watcher_ShouldNotTriggerWhenFileSystemWatcherDoesNotMatch() void Act() => // ReSharper disable once AccessToDisposedClosure - onEvent.Wait(timeout: TimeSpan.FromMilliseconds(100)); + onEvent.Wait(timeout: 100); await That(Act).Throws(); } @@ -126,7 +126,7 @@ public async Task Watcher_ShouldTriggerWhenFileSystemWatcherSendsNotification() using IAwaitableCallback onEvent = FileSystem.Watcher.OnTriggered(); FileSystem.File.WriteAllText(@"foo.txt", "some-text"); - onEvent.Wait(timeout: TimeSpan.FromMilliseconds(5000)); + onEvent.Wait(timeout: 5000); await That(isTriggered).IsTrue(); } diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs index 41c0f9c03..5fa1c987c 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs @@ -24,7 +24,7 @@ public async Task OnChanged_File_OtherEvent_ShouldNotTrigger(string path) Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onChanged.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onChanged.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -45,7 +45,7 @@ public async Task OnChanged_File_ShouldConsiderBasePath(string path1, string pat Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onChanged.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onChanged.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -121,7 +121,7 @@ public async Task OnChanged_File_ShouldUsePredicate(bool expectedResult, string Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onChanged.Wait(timeout: TimeSpan.FromMilliseconds(expectedResult ? 30000 : 50)); + onChanged.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -150,7 +150,7 @@ public async Task OnCreated_Directory_OtherEvent_ShouldNotTrigger(string path) Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onCreated.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onCreated.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -170,7 +170,7 @@ public async Task OnCreated_Directory_ShouldConsiderBasePath(string path1, strin Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onCreated.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onCreated.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -244,7 +244,7 @@ public async Task OnCreated_Directory_ShouldUsePredicate(bool expectedResult, st Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onCreated.Wait(timeout: TimeSpan.FromMilliseconds(expectedResult ? 30000 : 50)); + onCreated.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -273,7 +273,7 @@ public async Task OnCreated_File_OtherEvent_ShouldNotTrigger(string path) Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onCreated.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onCreated.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -293,7 +293,7 @@ public async Task OnCreated_File_ShouldConsiderBasePath(string path1, string pat Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onCreated.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onCreated.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -322,7 +322,7 @@ public async Task OnCreated_File_ShouldConsiderGlobPattern( Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onCreated.Wait(timeout: TimeSpan.FromMilliseconds(expectedResult ? 30000 : 50)); + onCreated.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -367,7 +367,7 @@ public async Task OnCreated_File_ShouldUsePredicate(bool expectedResult, string Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onCreated.Wait(timeout: TimeSpan.FromMilliseconds(expectedResult ? 30000 : 50)); + onCreated.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -395,7 +395,7 @@ public async Task OnDeleted_Directory_OtherEvent_ShouldNotTrigger(string path) Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onDeleted.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onDeleted.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -417,7 +417,7 @@ public async Task OnDeleted_Directory_ShouldConsiderBasePath(string path1, strin Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onDeleted.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onDeleted.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -447,7 +447,7 @@ public async Task OnDeleted_Directory_ShouldConsiderGlobPattern( Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onDeleted.Wait(timeout: TimeSpan.FromMilliseconds(expectedResult ? 30000 : 50)); + onDeleted.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -494,7 +494,7 @@ public async Task OnDeleted_Directory_ShouldUsePredicate(bool expectedResult, st Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onDeleted.Wait(timeout: TimeSpan.FromMilliseconds(expectedResult ? 30000 : 50)); + onDeleted.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -522,7 +522,7 @@ public async Task OnDeleted_File_OtherEvent_ShouldNotTrigger(string path) Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onDeleted.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onDeleted.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -544,7 +544,7 @@ public async Task OnDeleted_File_ShouldConsiderBasePath(string path1, string pat Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onDeleted.Wait(timeout: TimeSpan.FromMilliseconds(50)); + onDeleted.Wait(timeout: 50); }); await That(exception).IsExactly(); @@ -574,7 +574,7 @@ public async Task OnDeleted_File_ShouldConsiderGlobPattern( Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onDeleted.Wait(timeout: TimeSpan.FromMilliseconds(expectedResult ? 30000 : 50)); + onDeleted.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) @@ -621,7 +621,7 @@ public async Task OnDeleted_File_ShouldUsePredicate(bool expectedResult, string Exception? exception = Record.Exception(() => { // ReSharper disable once AccessToDisposedClosure - onDeleted.Wait(timeout: TimeSpan.FromMilliseconds(expectedResult ? 30000 : 50)); + onDeleted.Wait(timeout: expectedResult ? 30000 : 50); }); if (expectedResult) diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs index 49739f9b4..757a61d37 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs @@ -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); } @@ -200,7 +200,7 @@ public async Task AwaitableCallback_TimeoutExpired_ShouldThrowTimeoutException() Exception? exception = Record.Exception(() => { - wait.Wait(timeout: TimeSpan.FromMilliseconds(10)); + wait.Wait(timeout: 10); }); await That(exception).IsExactly(); @@ -219,7 +219,7 @@ public async Task AwaitableCallback_Wait_AfterDispose_ShouldThrowObjectDisposedE Exception? exception = Record.Exception(() => { - wait.Wait(timeout: TimeSpan.FromMilliseconds(100)); + wait.Wait(timeout: 100); }); await That(exception).IsExactly(); From 955bbb7334a8cf87b3925999cbacf31795a66d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Thu, 19 Feb 2026 20:51:57 +0100 Subject: [PATCH 7/7] Add missing tests --- .../Notification.cs | 9 +- ...ExtensionsTests.WithExecuteWhileWaiting.cs | 680 ++++++++++++++++++ .../NotificationHandlerExtensionsTests.cs | 2 +- .../NotificationTests.WaitAsync.cs | 91 ++- .../NotificationTests.cs | 43 +- 5 files changed, 788 insertions(+), 37 deletions(-) create mode 100644 Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.WithExecuteWhileWaiting.cs diff --git a/Source/Testably.Abstractions.Testing/Notification.cs b/Source/Testably.Abstractions.Testing/Notification.cs index cd09d0f16..557a8cd5a 100644 --- a/Source/Testably.Abstractions.Testing/Notification.cs +++ b/Source/Testably.Abstractions.Testing/Notification.cs @@ -124,8 +124,7 @@ public void Wait(Func? filter = null, { if (_isDisposed) { - throw new ObjectDisposedException( - "The awaitable callback is already disposed."); + throw new ObjectDisposedException(null, "The awaitable callback is already disposed."); } _filter = filter; @@ -164,8 +163,7 @@ public TValue[] Wait(int count, TimeSpan? timeout = null) { if (_isDisposed) { - throw new ObjectDisposedException( - "The awaitable callback is already disposed."); + throw new ObjectDisposedException(null, "The awaitable callback is already disposed."); } _reset.Reset(); @@ -202,8 +200,7 @@ public async Task WaitAsync( { if (_isDisposed) { - throw new ObjectDisposedException( - "The awaitable callback is already disposed."); + throw new ObjectDisposedException(null, "The awaitable callback is already disposed."); } List values = []; 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 000000000..e01554622 --- /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 5fa1c987c..5d553fedd 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationHandlerExtensionsTests.cs @@ -2,7 +2,7 @@ namespace Testably.Abstractions.Testing.Tests; -public class NotificationHandlerExtensionsTests +public partial class NotificationHandlerExtensionsTests { #region Test Setup diff --git a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs index 8a82013ac..53f9f383c 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.WaitAsync.cs @@ -36,6 +36,45 @@ public async Task AwaitableCallback_Amount_ShouldOnlyReturnAfterNumberOfCallback 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() { @@ -67,7 +106,8 @@ public async Task AwaitableCallback_ShouldWaitForCallbackExecution() } }, TestContext.Current.CancellationToken); - await onThreadSleep.WaitAsync(cancellationToken: TestContext.Current.CancellationToken); + await onThreadSleep.WaitAsync( + cancellationToken: TestContext.Current.CancellationToken); await That(isCalled).IsTrue(); } finally @@ -77,7 +117,8 @@ public async Task AwaitableCallback_ShouldWaitForCallbackExecution() } [Fact] - public async Task AwaitableCallback_CancellationTokenCancelled_ShouldThrowOperationCancelledException() + public async Task + AwaitableCallback_TimeoutExpired_ShouldThrowOperationCancelledException() { MockTimeSystem timeSystem = new(); bool isCalled = false; @@ -104,9 +145,8 @@ public async Task AwaitableCallback_CancellationTokenCancelled_ShouldThrowOperat Exception? exception = await Record.ExceptionAsync(async () => { - using CancellationTokenSource cts = new(); - cts.CancelAfter(10); - await onThreadSleep.WaitAsync(cancellationToken: cts.Token); + await onThreadSleep.WaitAsync(1, TimeSpan.FromMilliseconds(10), + TestContext.Current.CancellationToken); }); await That(exception).IsExactly(); @@ -124,12 +164,14 @@ public async Task onThreadSleep.Dispose(); - Exception? exception = await Record.ExceptionAsync(async () => + async Task Act() { - await onThreadSleep.WaitAsync(cancellationToken: TestContext.Current.CancellationToken); - }); + await onThreadSleep.WaitAsync(timeout: 20000, + cancellationToken: TestContext.Current.CancellationToken); + } - await That(exception).IsExactly(); + await That(Act).ThrowsExactly() + .WithMessage("The awaitable callback is already disposed."); } [Fact] @@ -142,16 +184,26 @@ public async Task AwaitableCallback_WaitedPreviously_ShouldWaitAgainForCallbackE 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); - - var result1 = await onThreadSleep.WaitAsync(cancellationToken: TestContext.Current.CancellationToken); - var 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); + .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] @@ -181,7 +233,8 @@ public async Task ExecuteWhileWaiting_ShouldExecuteCallback() timeSystem.Thread.Sleep(10); TimeSpan[] result = - await onThreadSleep.WaitAsync(cancellationToken: TestContext.Current.CancellationToken); + 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 757a61d37..a066814b5 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/NotificationTests.cs @@ -209,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(); - await That(exception).IsExactly(); + onThreadSleep.Dispose(); + + void Act() + { + onThreadSleep.Wait(1, TimeSpan.FromSeconds(20)); + } + + await That(Act).ThrowsExactly() + .WithMessage("The awaitable callback is already disposed."); } [Fact] @@ -283,7 +304,7 @@ public async Task AwaitableCallback_WaitedPreviously_ShouldWaitAgainForCallbackE ms.Set(); await That(isCalledFromSecondThread).IsTrue(); } - + [Theory] [AutoData] #if MarkExecuteWhileWaitingNotificationObsolete @@ -304,7 +325,7 @@ public async Task Execute_ShouldBeExecutedBeforeWait(int milliseconds) { timeSystem.Thread.Sleep(milliseconds); }) - .Wait(null, executeWhenWaiting: () => + .Wait(executeWhenWaiting: () => { isExecuted = true; }); @@ -335,7 +356,7 @@ public async Task Execute_WithReturnValue_ShouldBeExecutedAndReturnValue( timeSystem.Thread.Sleep(milliseconds); return result; }) - .Wait(null, executeWhenWaiting: () => + .Wait(executeWhenWaiting: () => { isExecuted = true; }); @@ -384,7 +405,7 @@ public async Task ExecuteWhileWaiting_WithReturnValue_ShouldExecuteCallback(int timeSystem.Thread.Sleep(10); return result; }) - .Wait(null); + .Wait(); await That(actualResult).IsEqualTo(result); await That(isExecuted).IsTrue();