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
21 changes: 21 additions & 0 deletions Source/Mockolate/Exceptions/MockVerificationTimeoutException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace Mockolate.Exceptions;

/// <summary>
/// Represents a verification timeout error on the mock.
/// </summary>
internal class MockVerificationTimeoutException : MockException
{
/// <inheritdoc cref="MockVerificationException" />
public MockVerificationTimeoutException(TimeSpan? timeout, Exception innerException)
: base(timeout is null ? "it timed out" : $"it timed out after {timeout.Value}", innerException)
{
Timeout = timeout;
}

/// <summary>
/// The timeout that was reached during verification, if any.
/// </summary>
public TimeSpan? Timeout { get; }
}
18 changes: 18 additions & 0 deletions Source/Mockolate/Verify/IAsyncVerificationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Threading.Tasks;
using Mockolate.Interactions;

namespace Mockolate.Verify;

/// <summary>
/// An awaitable <see cref="VerificationResult{TVerify}" /> that uses the timeout or cancellation token to wait for the
/// expected interactions to occur.
/// </summary>
public interface IAsyncVerificationResult : IVerificationResult
{
/// <summary>
/// Asynchronously waits until the specified <paramref name="predicate" /> holds true for the current set of
/// interactions, or until the timeout or cancellation token is triggered.
/// </summary>
Task<bool> VerifyAsync(Func<IInteraction[], bool> predicate);
}
150 changes: 150 additions & 0 deletions Source/Mockolate/Verify/VerificationResult.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Mockolate.Exceptions;
using Mockolate.Interactions;

namespace Mockolate.Verify;
Expand Down Expand Up @@ -37,6 +40,153 @@ TVerify IVerificationResult<TVerify>.Object
internal VerificationResult<T> Map<T>(T mock)
=> new(mock, _interactions, _predicate, _expectation);

/// <summary>
/// Makes the verification result awaitable, using the specified <paramref name="timeout" /> to wait for the expected
/// interactions to occur.
/// </summary>
public virtual VerificationResult<TVerify> Within(TimeSpan timeout)
=> new Awaitable(this, timeout);

/// <summary>
/// Makes the verification result awaitable, using the specified <paramref name="cancellationToken" /> to wait for the
/// expected interactions to occur.
/// </summary>
public virtual VerificationResult<TVerify> WithCancellation(CancellationToken cancellationToken)
=> new Awaitable(this, cancellationToken);

/// <summary>
/// An awaitable <see cref="VerificationResult{TVerify}" /> that uses the timeout or cancellation token to wait for the
/// expected interactions to occur.
/// </summary>
internal class Awaitable : VerificationResult<TVerify>, IAsyncVerificationResult
{
private CancellationToken? _cancellationToken;
private TimeSpan? _timeout;

/// <summary>
/// An awaitable <see cref="VerificationResult{TVerify}" /> that uses the <paramref name="timeout" /> to wait for the
/// expected interactions to occur.
/// </summary>
public Awaitable(VerificationResult<TVerify> inner, TimeSpan timeout) : base(inner._verify, inner._interactions,
inner._predicate, inner._expectation)
{
_timeout = timeout;
}

/// <summary>
/// An awaitable <see cref="VerificationResult{TVerify}" /> that uses the <paramref name="cancellationToken" /> to wait
/// for the
/// expected interactions to occur.
/// </summary>
public Awaitable(VerificationResult<TVerify> inner, CancellationToken cancellationToken) : base(inner._verify,
inner._interactions, inner._predicate, inner._expectation)
{
_cancellationToken = cancellationToken;
}

/// <inheritdoc cref="IVerificationResult.Verify(Func{IInteraction[], Boolean})" />
bool IVerificationResult.Verify(Func<IInteraction[], bool> predicate)
{
IInteraction[] matchingInteractions = _interactions.Interactions.Where(_predicate).ToArray();
_interactions.Verified(matchingInteractions);
bool result = predicate(matchingInteractions);
if (result)
{
return true;
}

return VerifyAsync(predicate).ConfigureAwait(false).GetAwaiter().GetResult();
}
Comment thread
vbreuss marked this conversation as resolved.

/// <inheritdoc cref="IAsyncVerificationResult.VerifyAsync(Func{IInteraction[], Boolean})" />
public async Task<bool> VerifyAsync(Func<IInteraction[], bool> predicate)
{
IInteraction[] matchingInteractions = _interactions.Interactions.Where(_predicate).ToArray();
_interactions.Verified(matchingInteractions);
bool result = predicate(matchingInteractions);
Comment thread
vbreuss marked this conversation as resolved.
if (result)
{
return true;
}

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;
}

SemaphoreSlim semaphore = new(0, 1);
try
{
_interactions.InteractionAdded += OnInteractionAdded;
do
{
await semaphore.WaitAsync(token);

matchingInteractions = _interactions.Interactions.Where(_predicate).ToArray();
_interactions.Verified(matchingInteractions);
if (predicate(matchingInteractions))
{
return true;
}
} while (!token.IsCancellationRequested);

return false;
}
finally
{
_interactions.InteractionAdded -= OnInteractionAdded;
cts?.Dispose();
}

void OnInteractionAdded(object? sender, EventArgs eventArgs)
{
semaphore.Release();
}
}
catch (OperationCanceledException ex)
{
if (_cancellationToken?.IsCancellationRequested == true)
{
throw new MockVerificationTimeoutException(null, ex);
}

throw new MockVerificationTimeoutException(_timeout, ex);
}
}

/// <inheritdoc cref="VerificationResult{TVerify}.Within(TimeSpan)" />
public override VerificationResult<TVerify> Within(TimeSpan timeout)
{
_timeout = timeout;
return this;
}

/// <inheritdoc cref="VerificationResult{TVerify}.WithCancellation(CancellationToken)" />
public override VerificationResult<TVerify> WithCancellation(CancellationToken cancellationToken)
{
_cancellationToken = cancellationToken;
return this;
}
}

#region IVerificationResult

/// <inheritdoc cref="IVerificationResult.Expectation" />
Expand Down
Loading