Skip to content

Commit

Permalink
Refactored Task monitoring based on idea of @dennisdoomen
Browse files Browse the repository at this point in the history
  • Loading branch information
lg2de committed Aug 9, 2022
1 parent 12ceff5 commit 71e5477
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 79 deletions.
114 changes: 56 additions & 58 deletions Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,42 +46,25 @@ public AsyncFunctionAssertions(Func<TTask> subject, IExtractExceptions extractor
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}, but found <null>.", timeSpan);

if (!success)
if (success)
{
// subject is null, nothing to execute
return new AndConstraint<TAssertions>((TAssertions)this);
}

ITimer timer = Clock.StartTimer();
TTask task = Subject.Invoke();
TimeSpan remainingTime = timeSpan - timer.Elapsed;

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

if (!success)
{
// task already in timeout with sync portion
return new AndConstraint<TAssertions>((TAssertions)this);
}
(TTask task, TimeSpan remainingTime) = InvokeWithTimer(timeSpan, Clock);

using var timeoutCancellationTokenSource = new CancellationTokenSource();
Task completedTask =
await Task.WhenAny(task, Clock.DelayAsync(remainingTime, timeoutCancellationTokenSource.Token));
success = Execute.Assertion
.ForCondition(remainingTime >= TimeSpan.Zero)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan);

if (completedTask == task)
{
timeoutCancellationTokenSource.Cancel();
await completedTask;
if (success)
{
bool completesWithinTimeout = await CompletesWithinTimeoutAsync(task, remainingTime, Clock);
Execute.Assertion
.ForCondition(completesWithinTimeout)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {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 All @@ -104,36 +87,19 @@ public AsyncFunctionAssertions(Func<TTask> subject, IExtractExceptions extractor
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect {context:task} to complete within {0}{reason}, but found <null>.", timeSpan);

if (!success)
{
// subject is null, nothing to execute
return new AndConstraint<TAssertions>((TAssertions)this);
}

ITimer timer = Clock.StartTimer();
TTask task = Subject.Invoke();
TimeSpan remainingTime = timeSpan - timer.Elapsed;
if (remainingTime < TimeSpan.Zero)
if (success)
{
// expected timeout reached, done
return new AndConstraint<TAssertions>((TAssertions)this);
}

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

if (completedTask == task)
{
timeoutCancellationTokenSource.Cancel();
await completedTask;
(Task task, TimeSpan remainingTime) = InvokeWithTimer(timeSpan, Clock);
if (remainingTime >= TimeSpan.Zero)
{
bool completesWithinTimeout = await CompletesWithinTimeoutAsync(task, remainingTime, Clock);
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("Did not expect {context:task} to complete within {0}{reason}.", timeSpan);

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

Expand Down Expand Up @@ -327,6 +293,38 @@ async Task<AndConstraint<TAssertions>> AssertionTaskAsync()
}
}

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

return (result, remainingTime);
}

/// <summary>
/// Monitors the specified task whether it completes withing the remaining time span.
/// </summary>
protected async Task<bool> CompletesWithinTimeoutAsync(Task target, TimeSpan remainingTime, IClock clock)
{
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;
}

private static async Task<Exception> InvokeWithInterceptionAsync(Func<Task> action)
{
try
Expand Down
29 changes: 8 additions & 21 deletions Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,39 +40,26 @@ public GenericAsyncFunctionAssertions(Func<Task<TResult>> subject, IExtractExcep
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));
}

ITimer timer = Clock.StartTimer();
Task<TResult> task = Subject.Invoke();
TimeSpan remainingTime = timeSpan - timer.Elapsed;
(Task<TResult> task, TimeSpan remainingTime) = InvokeWithTimer(timeSpan, Clock);

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

if (!success)
{
// task already in timeout with sync portion
return new AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>(this, task.Result);
}

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

if (completedTask == task)
if (success)
{
timeoutCancellationTokenSource.Cancel();
await completedTask;
bool completesWithinTimeout = await CompletesWithinTimeoutAsync(task, remainingTime, Clock);
Execute.Assertion
.ForCondition(completesWithinTimeout)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {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 AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>(this, task.Result);
}

Expand Down

0 comments on commit 71e5477

Please sign in to comment.