Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added NotCompleteWithinAsync for Task assertions #1967

Merged
merged 4 commits into from Aug 13, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -2237,6 +2237,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 @@ -2355,6 +2355,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 @@ -2237,6 +2237,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 @@ -2237,6 +2237,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 @@ -2189,6 +2189,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 @@ -2237,6 +2237,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