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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions Source/Testably.Abstractions.Testing/AwaitableCallbackExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using System.Threading;
using System.Threading.Tasks;
#if NET6_0_OR_GREATER
using System.Collections.Generic;
using System.Runtime.CompilerServices;
#endif

namespace Testably.Abstractions.Testing;

/// <summary>
/// Extension methods on <see cref="IAwaitableCallback{TValue}" />.
/// </summary>
public static class AwaitableCallbackExtensions
{
/// <summary>
/// Blocks the current thread until the callback is executed.
/// <para />
/// Throws a <see cref="TimeoutException" /> if the <paramref name="timeout" /> expired before the callback was
/// executed.
/// </summary>
/// <param name="callback">The callback.</param>
/// <param name="count">
/// (optional) The number of callbacks to wait.<br />
/// Defaults to <c>1</c>
/// </param>
/// <param name="timeout">
/// (optional) The timeout in milliseconds to wait on the callback.<br />
/// Defaults to <c>30000</c>ms (30 seconds).
/// </param>
public static TValue[] Wait<TValue>(this IAwaitableCallback<TValue> callback,
int count = 1,
int timeout = 30000)
=> callback.Wait(count, TimeSpan.FromMilliseconds(timeout));

/// <summary>
/// Waits asynchronously until the callback is executed.
/// </summary>
/// <param name="callback">The callback.</param>
/// <param name="count">
/// (optional) The number of callbacks to wait.<br />
/// Defaults to <c>1</c>
/// </param>
/// <param name="timeout">
/// (optional) The timeout in milliseconds to wait on the callback.<br />
/// Defaults to <c>30000</c>ms (30 seconds).
/// </param>
/// <param name="cancellationToken">
/// (optional) A <see cref="CancellationToken" /> to cancel waiting.<br />
/// Throws a <see cref="OperationCanceledException" /> if the token was canceled before the callback was executed.
/// </param>
public static Task<TValue[]> WaitAsync<TValue>(this IAwaitableCallback<TValue> callback,
int count = 1,
int timeout = 30000,
CancellationToken? cancellationToken = null)
=> callback.WaitAsync(count, TimeSpan.FromMilliseconds(timeout), cancellationToken);

#if NET6_0_OR_GREATER
/// <summary>
/// Converts the <see cref="IAwaitableCallback{TValue}" /> to an <see cref="IAsyncEnumerable{TValue}" /> that yields a
/// value each time the callback is executed.
/// </summary>
/// <remarks>
/// Uses a default timeout of 30 seconds to prevent infinite waiting if the callback is never executed.
/// </remarks>
public static IAsyncEnumerable<TValue> ToAsyncEnumerable<TValue>(
this IAwaitableCallback<TValue> source,
CancellationToken cancellationToken = default)
=> ToAsyncEnumerable(source, null, cancellationToken);

/// <summary>
/// Converts the <see cref="IAwaitableCallback{TValue}" /> to an <see cref="IAsyncEnumerable{TValue}" /> that yields a
/// value each time the callback is executed.
/// </summary>
/// <remarks>
/// If no <paramref name="timeout" /> is specified (<see langword="null" />), a default timeout of 30 seconds is used
/// to prevent infinite waiting if the callback is never executed.
/// </remarks>
public static async IAsyncEnumerable<TValue> ToAsyncEnumerable<TValue>(
this IAwaitableCallback<TValue> 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;
}
}

/// <summary>
/// Converts the <see cref="IAwaitableCallback{TValue}" /> to an <see cref="IAsyncEnumerable{TValue}" /> that yields a
/// value each time the callback is executed.
/// </summary>
public static IAsyncEnumerable<TValue> ToAsyncEnumerable<TValue>(
this IAwaitableCallback<TValue> source,
int timeout,
CancellationToken cancellationToken = default)
=> ToAsyncEnumerable(source, TimeSpan.FromMilliseconds(timeout), cancellationToken);
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public interface INotificationHandler : IFileSystemEntity
/// Callback executed when any change in the <see cref="MockFileSystem" /> matching the <paramref name="predicate" />
/// occurred.
/// </summary>
/// <param name="notificationCallback">The callback to execute after the change occurred.</param>
/// <param name="notificationCallback">(optional) The callback to execute after the change occurred.</param>
/// <param name="predicate">
/// (optional) A predicate used to filter which callbacks should be notified.<br />
/// If set to <see langword="null" /> (default value) all callbacks are notified.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
43 changes: 42 additions & 1 deletion Source/Testably.Abstractions.Testing/IAwaitableCallback.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Testably.Abstractions.Testing;

Expand All @@ -7,7 +9,7 @@ namespace Testably.Abstractions.Testing;
/// - un-registering a callback by calling <see cref="IDisposable.Dispose()" /><br />
/// - blocking for the callback to be executed
/// </summary>
public interface IAwaitableCallback<out TValue> : IDisposable
public interface IAwaitableCallback<TValue> : IDisposable
{
/// <summary>
/// Blocks the current thread until the callback is executed.
Expand All @@ -30,8 +32,47 @@ public interface IAwaitableCallback<out TValue> : IDisposable
/// <param name="executeWhenWaiting">
/// (optional) A callback to execute when waiting started.
/// </param>
#if MarkExecuteWhileWaitingNotificationObsolete
[Obsolete("Use another `Wait` or `WaitAsync` overload and move the filter to the creation of the awaitable callback.")]
#endif
void Wait(Func<TValue, bool>? filter = null,
int timeout = 30000,
int count = 1,
Action? executeWhenWaiting = null);

/// <summary>
/// Blocks the current thread until the callback is executed.
/// <para />
/// Throws a <see cref="TimeoutException" /> if the <paramref name="timeout" /> expired before the callback was
/// executed.
/// </summary>
/// <param name="count">
/// (optional) The number of callbacks to wait.<br />
/// Defaults to <c>1</c>
/// </param>
/// <param name="timeout">
/// (optional) The timeout to wait on the callback.<br />
/// If not specified (<see langword="null" />), defaults to 30 seconds.
/// </param>
TValue[] Wait(int count, TimeSpan? timeout = null);

/// <summary>
/// Waits asynchronously until the callback is executed.
/// </summary>
/// <param name="count">
/// (optional) The number of callbacks to wait.<br />
/// Defaults to <c>1</c>
/// </param>
/// <param name="timeout">
/// (optional) The timeout to wait on the callback.<br />
/// If not specified (<see langword="null" />), defaults to 30 seconds.
/// </param>
/// <param name="cancellationToken">
/// (optional) A <see cref="CancellationToken" /> to cancel waiting.<br />
/// Throws a <see cref="OperationCanceledException" /> if the token was canceled before the callback was executed.
/// </param>
Task<TValue[]> WaitAsync(
int count = 1,
TimeSpan? timeout = null,
CancellationToken? cancellationToken = null);
}
Loading
Loading