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
19 changes: 19 additions & 0 deletions Source/Mockolate/Verify/IAsyncVerificationResult.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Mockolate.Exceptions;
using Mockolate.Interactions;

namespace Mockolate.Verify;
Expand All @@ -15,4 +17,21 @@ public interface IAsyncVerificationResult : IVerificationResult
/// interactions, or until the timeout or cancellation token is triggered.
/// </summary>
Task<bool> VerifyAsync(Func<IInteraction[], bool> predicate);

/// <summary>
/// Asynchronously waits until the specified <paramref name="predicate" /> holds true for the current set of
/// interactions, or until the configured timeout or cancellation token, or the given
/// <paramref name="cancellationToken" /> is triggered.
/// </summary>
/// <remarks>
/// The <paramref name="cancellationToken" /> only applies to this call and leaves the configured timeout and
/// cancellation token unchanged.
/// </remarks>
/// <exception cref="MockVerificationTimeoutException">
/// Thrown when the configured timeout or cancellation token is triggered first.
/// </exception>
/// <exception cref="OperationCanceledException">
/// Thrown when the <paramref name="cancellationToken" /> is canceled.
/// </exception>
Task<bool> VerifyAsync(Func<IInteraction[], bool> predicate, CancellationToken cancellationToken);
}
72 changes: 31 additions & 41 deletions Source/Mockolate/Verify/VerificationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,11 @@ bool IVerificationResult.Verify(Func<IInteraction[], bool> predicate)
}

/// <inheritdoc cref="IAsyncVerificationResult.VerifyAsync(Func{IInteraction[], Boolean})" />
public async Task<bool> VerifyAsync(Func<IInteraction[], bool> predicate)
public Task<bool> VerifyAsync(Func<IInteraction[], bool> predicate)
=> VerifyAsync(predicate, CancellationToken.None);

/// <inheritdoc cref="IAsyncVerificationResult.VerifyAsync(Func{IInteraction[], Boolean}, CancellationToken)" />
public async Task<bool> VerifyAsync(Func<IInteraction[], bool> predicate, CancellationToken cancellationToken)
{
ThrowIfRecordingDisabled(_interactions);
IInteraction[] matchingInteractions = CollectMatching();
Expand All @@ -268,26 +272,7 @@ public async Task<bool> VerifyAsync(Func<IInteraction[], bool> predicate)

try
{
CancellationTokenSource? cts = null;
CancellationToken token;
if (_timeout is null)
{
token = _cancellationToken!.Value;
}
else
{
if (_cancellationToken is not null)
{
cts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken.Value);
}
else
{
cts = new CancellationTokenSource();
}

cts.CancelAfter(_timeout.Value);
token = cts.Token;
}
CancellationToken token = CreateWaitToken(cancellationToken, out CancellationTokenSource? cts);

SemaphoreSlim semaphore = new(0);
try
Expand Down Expand Up @@ -328,6 +313,12 @@ void OnInteractionAdded(object? sender, EventArgs eventArgs)
}
catch (OperationCanceledException ex)
{
if (cancellationToken.IsCancellationRequested)
{
// The wait was canceled on the linked token, but the caller expects its own token
throw new OperationCanceledException(ex.Message, ex, cancellationToken);
}

if (_cancellationToken?.IsCancellationRequested == true)
{
throw new MockVerificationTimeoutException(null, ex);
Expand All @@ -337,6 +328,24 @@ void OnInteractionAdded(object? sender, EventArgs eventArgs)
}
}

private CancellationToken CreateWaitToken(CancellationToken cancellationToken, out CancellationTokenSource? cts)
{
if (_timeout is null && !cancellationToken.CanBeCanceled)
{
cts = null;
return _cancellationToken!.Value;
}

cts = CancellationTokenSource.CreateLinkedTokenSource(
_cancellationToken ?? CancellationToken.None, cancellationToken);
if (_timeout is not null)
{
cts.CancelAfter(_timeout.Value);
}

return cts.Token;
}

/// <inheritdoc cref="IFastVerifyCountResult.VerifyCount(Func{int, Boolean})" />
bool IFastVerifyCountResult.VerifyCount(Func<int, bool> countPredicate)
{
Expand Down Expand Up @@ -368,26 +377,7 @@ private async Task<bool> VerifyCountAsync(Func<int, bool> countPredicate)
{
try
{
CancellationTokenSource? cts = null;
CancellationToken token;
if (_timeout is null)
{
token = _cancellationToken!.Value;
}
else
{
if (_cancellationToken is not null)
{
cts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken.Value);
}
else
{
cts = new CancellationTokenSource();
}

cts.CancelAfter(_timeout.Value);
token = cts.Token;
}
CancellationToken token = CreateWaitToken(CancellationToken.None, out CancellationTokenSource? cts);

SemaphoreSlim semaphore = new(0);
try
Expand Down
1 change: 1 addition & 0 deletions Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3279,6 +3279,7 @@ namespace Mockolate.Verify
public interface IAsyncVerificationResult : Mockolate.Verify.IVerificationResult
{
System.Threading.Tasks.Task<bool> VerifyAsync(System.Func<Mockolate.Interactions.IInteraction[], bool> predicate);
System.Threading.Tasks.Task<bool> VerifyAsync(System.Func<Mockolate.Interactions.IInteraction[], bool> predicate, System.Threading.CancellationToken cancellationToken);
}
public interface IMockVerify<out T> : Mockolate.IInteractiveMock<T> { }
public interface IVerificationResult
Expand Down
1 change: 1 addition & 0 deletions Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2833,6 +2833,7 @@ namespace Mockolate.Verify
public interface IAsyncVerificationResult : Mockolate.Verify.IVerificationResult
{
System.Threading.Tasks.Task<bool> VerifyAsync(System.Func<Mockolate.Interactions.IInteraction[], bool> predicate);
System.Threading.Tasks.Task<bool> VerifyAsync(System.Func<Mockolate.Interactions.IInteraction[], bool> predicate, System.Threading.CancellationToken cancellationToken);
}
public interface IMockVerify<out T> : Mockolate.IInteractiveMock<T> { }
public interface IVerificationResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2756,6 +2756,7 @@ namespace Mockolate.Verify
public interface IAsyncVerificationResult : Mockolate.Verify.IVerificationResult
{
System.Threading.Tasks.Task<bool> VerifyAsync(System.Func<Mockolate.Interactions.IInteraction[], bool> predicate);
System.Threading.Tasks.Task<bool> VerifyAsync(System.Func<Mockolate.Interactions.IInteraction[], bool> predicate, System.Threading.CancellationToken cancellationToken);
}
public interface IMockVerify<out T> : Mockolate.IInteractiveMock<T> { }
public interface IVerificationResult
Expand Down
91 changes: 89 additions & 2 deletions Tests/Mockolate.Tests/Verify/VerificationResultTests.AsyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public async Task VerifyAsync_WhenAlreadySuccessful_ShouldReturnTrue()
VerificationResult<Mock.IMockVerifyForIChocolateDispenser> result = sut.Mock.Verify.Dispense(Match.AnyParameters())
.Within(500.Milliseconds());

await That(((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 0)).IsTrue();
await That(((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 0, CancellationToken.None)).IsTrue();
}

[Fact]
Expand All @@ -122,11 +122,98 @@ public async Task VerifyAsync_WhenMultipleIterationsAreNecessary_ShouldStopWhenS
}
}, token);

await That(((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 20)).IsTrue();
await That(((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 20, CancellationToken.None)).IsTrue();
cts.Cancel();
await backgroundTask;
}

[Fact]
public async Task VerifyAsync_WithCancellationToken_ShouldKeepConfiguredTimeout()
{
IChocolateDispenser sut = IChocolateDispenser.CreateMock();
using CancellationTokenSource cts = new(30.Seconds());

VerificationResult<Mock.IMockVerifyForIChocolateDispenser> result = sut.Mock.Verify.Dispense(Match.AnyParameters())
.Within(50.Milliseconds());

Task Act()
=> ((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 0, cts.Token);

await That(Act).Throws<MockVerificationTimeoutException>()
.Whose(e => e.Timeout, t => t.IsEqualTo(50.Milliseconds()));
}

[Fact]
public async Task VerifyAsync_WithCancellationToken_ShouldLeaveResultUnchanged()
{
IChocolateDispenser sut = IChocolateDispenser.CreateMock();
CancellationToken canceledToken = new(true);

VerificationResult<Mock.IMockVerifyForIChocolateDispenser> result = sut.Mock.Verify.Dispense(Match.AnyParameters())
.Within(50.Milliseconds());

Task ActWithToken()
=> ((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 0, canceledToken);

Task ActWithoutToken()
=> ((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 0, CancellationToken.None);

await That(ActWithToken).Throws<OperationCanceledException>();
await That(ActWithoutToken).Throws<MockVerificationTimeoutException>()
.Whose(e => e.Timeout, t => t.IsEqualTo(50.Milliseconds()));
}

[Fact]
public async Task VerifyAsync_WithCancellationToken_ShouldStopWaitingWhenConfiguredTokenIsCanceled()
{
IChocolateDispenser sut = IChocolateDispenser.CreateMock();
using CancellationTokenSource ownCts = new(50.Milliseconds());
using CancellationTokenSource cts = new(30.Seconds());

VerificationResult<Mock.IMockVerifyForIChocolateDispenser> result = sut.Mock.Verify.Dispense(Match.AnyParameters())
.WithCancellation(ownCts.Token);

Task Act()
=> ((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 0, cts.Token);

await That(Act).Throws<MockVerificationTimeoutException>()
.Whose(e => e.Timeout, t => t.IsNull());
}

[Fact]
public async Task VerifyAsync_WithCancellationToken_WhenCanceled_ShouldThrowOperationCanceledException()
{
IChocolateDispenser sut = IChocolateDispenser.CreateMock();
using CancellationTokenSource cts = new(50.Milliseconds());
CancellationToken token = cts.Token;

VerificationResult<Mock.IMockVerifyForIChocolateDispenser> result = sut.Mock.Verify.Dispense(Match.AnyParameters())
.Within(30.Seconds());

Task Act()
=> ((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 0, token);

await That(Act).Throws<OperationCanceledException>()
.Whose(e => e.CancellationToken, t => t.IsEqualTo(token));
}

[Fact]
public async Task VerifyAsync_WithCancellationToken_WhenCanceledWithoutTimeout_ShouldThrowOperationCanceledException()
{
IChocolateDispenser sut = IChocolateDispenser.CreateMock();
using CancellationTokenSource cts = new(50.Milliseconds());
CancellationToken token = cts.Token;

VerificationResult<Mock.IMockVerifyForIChocolateDispenser> result = sut.Mock.Verify.Dispense(Match.AnyParameters())
.WithCancellation(CancellationToken.None);

Task Act()
=> ((IAsyncVerificationResult)result).VerifyAsync(l => l.Length > 0, token);

await That(Act).Throws<OperationCanceledException>()
.Whose(e => e.CancellationToken, t => t.IsEqualTo(token));
}

[Fact]
public async Task WithCancellation_ShouldReturnAsyncVerificationResult()
{
Expand Down
Loading