Skip to content

Commit

Permalink
Merge pull request #1967 from lg2de/not-complete-within
Browse files Browse the repository at this point in the history
Added `NotCompleteWithinAsync` for Task assertions
  • Loading branch information
jnyrup committed Aug 13, 2022
2 parents 403b334 + 59398c8 commit 19f2bcd
Show file tree
Hide file tree
Showing 15 changed files with 403 additions and 249 deletions.
95 changes: 77 additions & 18 deletions Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
Expand Up @@ -41,36 +41,63 @@ public AsyncFunctionAssertions(Func<TTask> subject, IExtractExceptions extractor
public async Task<AndConstraint<TAssertions>> CompleteWithinAsync(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
Execute.Assertion
bool success = Execute.Assertion
.ForCondition(Subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}, but found <null>.", timeSpan);

ITimer timer = Clock.StartTimer();
TTask task = Subject.Invoke();
TimeSpan remainingTime = timeSpan - timer.Elapsed;
if (success)
{
(TTask task, TimeSpan remainingTime) = InvokeWithTimer(timeSpan);

success = Execute.Assertion
.ForCondition(remainingTime >= TimeSpan.Zero)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan);

if (success)
{
bool completesWithinTimeout = await CompletesWithinTimeoutAsync(task, remainingTime);
Execute.Assertion
.ForCondition(completesWithinTimeout)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan);
}
}

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that the current <typeparamref name="TTask"/> will not complete within the specified time.
/// </summary>
/// <param name="timeSpan">The allowed time span for the operation.</param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public async Task<AndConstraint<TAssertions>> NotCompleteWithinAsync(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
bool success = Execute.Assertion
.ForCondition(remainingTime >= TimeSpan.Zero)
.ForCondition(Subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan);
.FailWith("Did not expect {context:task} to complete within {0}{reason}, but found <null>.", timeSpan);

if (success)
{
using var timeoutCancellationTokenSource = new CancellationTokenSource();
Task completedTask =
await Task.WhenAny(task, Clock.DelayAsync(remainingTime, timeoutCancellationTokenSource.Token));

if (completedTask == task)
(Task task, TimeSpan remainingTime) = InvokeWithTimer(timeSpan);
if (remainingTime >= TimeSpan.Zero)
{
timeoutCancellationTokenSource.Cancel();
await completedTask;
bool completesWithinTimeout = await CompletesWithinTimeoutAsync(task, remainingTime);
Execute.Assertion
.ForCondition(!completesWithinTimeout)
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect {context:task} to complete within {0}{reason}.", timeSpan);
}

Execute.Assertion
.ForCondition(completedTask == task)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan);
}

return new AndConstraint<TAssertions>((TAssertions)this);
Expand Down Expand Up @@ -266,6 +293,38 @@ async Task<AndConstraint<TAssertions>> AssertionTaskAsync()
}
}

/// <summary>
/// Monitors the specified task whether it completes withing the remaining time span.
/// </summary>
protected async Task<bool> CompletesWithinTimeoutAsync(Task target, TimeSpan remainingTime)
{
using var timeoutCancellationTokenSource = new CancellationTokenSource();

Task completedTask =
await Task.WhenAny(target, Clock.DelayAsync(remainingTime, timeoutCancellationTokenSource.Token));

if (completedTask != target)
{
return false;
}

// cancel the clock
timeoutCancellationTokenSource.Cancel();
return true;
}

/// <summary>
/// Invokes the subject and measures the sync execution time.
/// </summary>
private protected (TTask result, TimeSpan remainingTime) InvokeWithTimer(TimeSpan timeSpan)
{
ITimer timer = Clock.StartTimer();
TTask result = Subject.Invoke();
TimeSpan remainingTime = timeSpan - timer.Elapsed;

return (result, remainingTime);
}

private static async Task<Exception> InvokeWithInterceptionAsync(Func<Task> action)
{
try
Expand Down
37 changes: 18 additions & 19 deletions Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs
Expand Up @@ -32,39 +32,36 @@ public GenericAsyncFunctionAssertions(Func<Task<TResult>> subject, IExtractExcep
public new async Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> CompleteWithinAsync(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
Execute.Assertion
bool success = Execute.Assertion
.ForCondition(Subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} to complete within {0}{reason}, but found <null>.", timeSpan);

ITimer timer = Clock.StartTimer();
Task<TResult> task = Subject.Invoke();
TimeSpan remainingTime = timeSpan - timer.Elapsed;
if (!success)
{
// subject is null, nothing to execute
// We need (currently) to return a default result as "Which" because actual result is not available.
return new AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>(this, default(TResult));
}

bool success = Execute.Assertion
(Task<TResult> task, TimeSpan remainingTime) = InvokeWithTimer(timeSpan);

success = Execute.Assertion
.ForCondition(remainingTime >= TimeSpan.Zero)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan);

if (success)
{
using var timeoutCancellationTokenSource = new CancellationTokenSource();
Task completedTask =
await Task.WhenAny(task, Clock.DelayAsync(remainingTime, timeoutCancellationTokenSource.Token));

if (completedTask == task)
{
timeoutCancellationTokenSource.Cancel();
await completedTask;
}

bool completesWithinTimeout = await CompletesWithinTimeoutAsync(task, remainingTime);
Execute.Assertion
.ForCondition(completedTask == task)
.ForCondition(completesWithinTimeout)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan);
}

return new AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>(this, task.Result);
TResult result = task.IsCompleted ? task.Result : default;
return new AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>(this, result);
}

/// <summary>
Expand All @@ -77,7 +74,8 @@ public GenericAsyncFunctionAssertions(Func<Task<TResult>> subject, IExtractExcep
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public new async Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> NotThrowAsync(string because = "", params object[] becauseArgs)
public new async Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> NotThrowAsync(
string because = "", params object[] becauseArgs)
{
Execute.Assertion
.ForCondition(Subject is not null)
Expand Down Expand Up @@ -119,7 +117,8 @@ public GenericAsyncFunctionAssertions(Func<Task<TResult>> subject, IExtractExcep
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">Throws if waitTime or pollInterval are negative.</exception>
public new Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
public new Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> NotThrowAfterAsync(
TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
{
if (waitTime < TimeSpan.Zero)
{
Expand Down
Expand Up @@ -79,7 +79,7 @@ public TaskCompletionSourceAssertions(TaskCompletionSource<T> tcs, IClock clock)
Execute.Assertion
.ForCondition(subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} to not complete within {0}{reason}, but found <null>.", timeSpan);
.FailWith("Did not expect {context} to complete within {0}{reason}, but found <null>.", timeSpan);

using var timeoutCancellationTokenSource = new CancellationTokenSource();
Task completedTask = await Task.WhenAny(
Expand All @@ -95,7 +95,7 @@ public TaskCompletionSourceAssertions(TaskCompletionSource<T> tcs, IClock clock)
Execute.Assertion
.ForCondition(completedTask != subject.Task)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to not complete within {0}{reason}.", timeSpan);
.FailWith("Did not expect {context:task} to complete within {0}{reason}.", timeSpan);
}

/// <inheritdoc/>
Expand Down
Expand Up @@ -2242,6 +2242,8 @@ namespace FluentAssertions.Specialized
public AsyncFunctionAssertions(System.Func<TTask> subject, FluentAssertions.Specialized.IExtractExceptions extractor, FluentAssertions.Common.IClock clock) { }
protected override string Identifier { get; }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> CompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
protected System.Threading.Tasks.Task<bool> CompletesWithinTimeoutAsync(System.Threading.Tasks.Task target, System.TimeSpan remainingTime) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAfterAsync(System.TimeSpan waitTime, System.TimeSpan pollInterval, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync(string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync<TException>(string because = "", params object[] becauseArgs)
Expand Down
Expand Up @@ -2360,6 +2360,8 @@ namespace FluentAssertions.Specialized
public AsyncFunctionAssertions(System.Func<TTask> subject, FluentAssertions.Specialized.IExtractExceptions extractor, FluentAssertions.Common.IClock clock) { }
protected override string Identifier { get; }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> CompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
protected System.Threading.Tasks.Task<bool> CompletesWithinTimeoutAsync(System.Threading.Tasks.Task target, System.TimeSpan remainingTime) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAfterAsync(System.TimeSpan waitTime, System.TimeSpan pollInterval, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync(string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync<TException>(string because = "", params object[] becauseArgs)
Expand Down
Expand Up @@ -2242,6 +2242,8 @@ namespace FluentAssertions.Specialized
public AsyncFunctionAssertions(System.Func<TTask> subject, FluentAssertions.Specialized.IExtractExceptions extractor, FluentAssertions.Common.IClock clock) { }
protected override string Identifier { get; }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> CompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
protected System.Threading.Tasks.Task<bool> CompletesWithinTimeoutAsync(System.Threading.Tasks.Task target, System.TimeSpan remainingTime) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAfterAsync(System.TimeSpan waitTime, System.TimeSpan pollInterval, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync(string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync<TException>(string because = "", params object[] becauseArgs)
Expand Down
Expand Up @@ -2242,6 +2242,8 @@ namespace FluentAssertions.Specialized
public AsyncFunctionAssertions(System.Func<TTask> subject, FluentAssertions.Specialized.IExtractExceptions extractor, FluentAssertions.Common.IClock clock) { }
protected override string Identifier { get; }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> CompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
protected System.Threading.Tasks.Task<bool> CompletesWithinTimeoutAsync(System.Threading.Tasks.Task target, System.TimeSpan remainingTime) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAfterAsync(System.TimeSpan waitTime, System.TimeSpan pollInterval, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync(string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync<TException>(string because = "", params object[] becauseArgs)
Expand Down
Expand Up @@ -2194,6 +2194,8 @@ namespace FluentAssertions.Specialized
public AsyncFunctionAssertions(System.Func<TTask> subject, FluentAssertions.Specialized.IExtractExceptions extractor, FluentAssertions.Common.IClock clock) { }
protected override string Identifier { get; }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> CompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
protected System.Threading.Tasks.Task<bool> CompletesWithinTimeoutAsync(System.Threading.Tasks.Task target, System.TimeSpan remainingTime) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAfterAsync(System.TimeSpan waitTime, System.TimeSpan pollInterval, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync(string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync<TException>(string because = "", params object[] becauseArgs)
Expand Down
Expand Up @@ -2242,6 +2242,8 @@ namespace FluentAssertions.Specialized
public AsyncFunctionAssertions(System.Func<TTask> subject, FluentAssertions.Specialized.IExtractExceptions extractor, FluentAssertions.Common.IClock clock) { }
protected override string Identifier { get; }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> CompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
protected System.Threading.Tasks.Task<bool> CompletesWithinTimeoutAsync(System.Threading.Tasks.Task target, System.TimeSpan remainingTime) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAfterAsync(System.TimeSpan waitTime, System.TimeSpan pollInterval, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync(string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<TAssertions>> NotThrowAsync<TException>(string because = "", params object[] becauseArgs)
Expand Down
Expand Up @@ -71,7 +71,7 @@ public async Task When_should_is_passed_argument_context_should_still_be_found()

// Assert
await action.Should().ThrowAsync<XunitException>()
.WithMessage("Expected bob to not complete within 1s because test testArg.");
.WithMessage("Did not expect bob to complete within 1s because test testArg.");
}

[Fact]
Expand Down

0 comments on commit 19f2bcd

Please sign in to comment.