Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
introduced assertions for Task and Task<T> with CompleteWithin checks
introduce ITimer to abstract real timing in the tests rework after review rework after review Another pass of review.
- Loading branch information
1 parent
9c87318
commit eae4a19
Showing
13 changed files
with
562 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using FluentAssertions.Common; | ||
using FluentAssertions.Specialized; | ||
|
||
namespace FluentAssertions | ||
{ | ||
public class AggregateExceptionExtractor : IExtractExceptions | ||
{ | ||
public IEnumerable<T> OfType<T>(Exception actualException) | ||
where T : Exception | ||
{ | ||
if (typeof(T).IsSameOrInherits(typeof(AggregateException))) | ||
{ | ||
return (actualException is T exception) ? new[] { exception } : Enumerable.Empty<T>(); | ||
} | ||
|
||
return GetExtractedExceptions<T>(actualException); | ||
} | ||
|
||
private static List<T> GetExtractedExceptions<T>(Exception actualException) | ||
where T : Exception | ||
{ | ||
var exceptions = new List<T>(); | ||
|
||
if (actualException is AggregateException aggregateException) | ||
{ | ||
var flattenedExceptions = aggregateException.Flatten(); | ||
|
||
exceptions.AddRange(flattenedExceptions.InnerExceptions.OfType<T>()); | ||
} | ||
else if (actualException is T genericException) | ||
{ | ||
exceptions.Add(genericException); | ||
} | ||
|
||
return exceptions; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace FluentAssertions.Common | ||
{ | ||
/// <summary> | ||
/// Represents an abstract timer that is used to make some of this library's timing dependent functionality better testable. | ||
/// </summary> | ||
public interface ITimer | ||
{ | ||
/// <summary> | ||
/// Creates a task that will complete after a time delay. | ||
/// </summary> | ||
/// <param name="delay">The time span to wait before completing the returned task</param> | ||
/// <param name="timeoutCancellationTokenSource"></param> | ||
/// <returns>A task that represents the time delay.</returns> | ||
/// <seealso cref="Task.Delay(TimeSpan)"/> | ||
Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Waits for the task for a specified time. | ||
/// </summary> | ||
/// <param name="task">The task to be waited for.</param> | ||
/// <param name="timeout">The time span to wait.</param> | ||
/// <returns><c>true</c> if the task completes before specified timeout.</returns> | ||
bool Wait(Task task, TimeSpan timeout); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace FluentAssertions.Common | ||
{ | ||
/// <summary> | ||
/// Default implementation for <see cref="ITimer"/> for production use. | ||
/// </summary> | ||
internal class TaskTimer : ITimer | ||
{ | ||
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken) | ||
{ | ||
return Task.Delay(delay, cancellationToken); | ||
} | ||
|
||
public bool Wait(Task task, TimeSpan timeout) | ||
{ | ||
using (NoSynchronizationContextScope.Enter()) | ||
{ | ||
return task.Wait(timeout); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using FluentAssertions.Common; | ||
using FluentAssertions.Execution; | ||
|
||
namespace FluentAssertions.Specialized | ||
{ | ||
public class GenericAsyncFunctionAssertions<TResult> : AsyncFunctionAssertions | ||
{ | ||
private readonly Func<Task<TResult>> subject; | ||
private readonly ITimer timer; | ||
|
||
public GenericAsyncFunctionAssertions(Func<Task<TResult>> subject, IExtractExceptions extractor, ITimer timer) : base(subject, extractor) | ||
{ | ||
this.subject = subject; | ||
this.timer = timer; | ||
} | ||
|
||
/// <summary> | ||
/// Asserts that the current <see cref="Task{T}"/> will complete within 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 <see cref="because" />. | ||
/// </param> | ||
public AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, Task<TResult>> CompleteWithin( | ||
TimeSpan timeSpan, string because = "", params object[] becauseArgs) | ||
{ | ||
Task<TResult> task = subject(); | ||
bool completed = timer.Wait(task, timeSpan); | ||
|
||
Execute.Assertion | ||
.ForCondition(completed) | ||
.BecauseOf(because, becauseArgs) | ||
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan); | ||
|
||
return new AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, Task<TResult>>(this, task); | ||
} | ||
|
||
/// <summary> | ||
/// Asserts that the current <see cref="Task{T}"/> will 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 <see cref="because" />. | ||
/// </param> | ||
public async Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, Task<TResult>>> CompleteWithinAsync( | ||
TimeSpan timeSpan, string because = "", params object[] becauseArgs) | ||
{ | ||
var timeoutCancellationTokenSource = new CancellationTokenSource(); | ||
|
||
Task<TResult> task = subject(); | ||
|
||
Task completedTask = | ||
await Task.WhenAny(task, timer.DelayAsync(timeSpan, timeoutCancellationTokenSource.Token)); | ||
|
||
if (completedTask == task) | ||
{ | ||
timeoutCancellationTokenSource.Cancel(); | ||
await completedTask; | ||
} | ||
|
||
Execute.Assertion | ||
.ForCondition(completedTask == task) | ||
.BecauseOf(because, becauseArgs) | ||
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan); | ||
|
||
return new AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, Task<TResult>>(this, task); | ||
} | ||
|
||
} | ||
} |
Oops, something went wrong.