Skip to content

Commit

Permalink
Converted AsyncFunctionAssertions into real base class (#2359)
Browse files Browse the repository at this point in the history
* Make AsyncFunctionAssertions a base class

* Move methods to concrete class to prevent overwriting with `new`

* Add some class documentation

* Updated release notes

* Disable Qudana rule "InvertIf"
  • Loading branch information
lg2de committed Oct 8, 2023
1 parent 6f1035a commit 3205e04
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 194 deletions.
155 changes: 4 additions & 151 deletions Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,67 +11,20 @@ namespace FluentAssertions.Specialized;
/// <summary>
/// Contains a number of methods to assert that an asynchronous method yields the expected result.
/// </summary>
/// <typeparam name="TTask">The type of <see cref="Task{T}"/> to be handled.</typeparam>
/// <typeparam name="TAssertions">The type of assertion to be returned.</typeparam>
[DebuggerNonUserCode]
public class AsyncFunctionAssertions<TTask, TAssertions> : DelegateAssertionsBase<Func<TTask>, TAssertions>
where TTask : Task
where TAssertions : AsyncFunctionAssertions<TTask, TAssertions>
{
[Obsolete("This class is intended as base class. This ctor is accidentally public and will be removed in Version 7.")]
public AsyncFunctionAssertions(Func<TTask> subject, IExtractExceptions extractor)
: this(subject, extractor, new Clock())
{
}

[Obsolete("This class is intended as base class. This ctor is accidentally public and will be made protected in Version 7.")]
public AsyncFunctionAssertions(Func<TTask> subject, IExtractExceptions extractor, IClock clock)
protected AsyncFunctionAssertions(Func<TTask> subject, IExtractExceptions extractor, IClock clock)
: base(subject, extractor, clock)
{
}

protected override string Identifier => "async function";

/// <summary>
/// Asserts that the current <typeparamref name="TTask"/> 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 <paramref name="because" />.
/// </param>
public async Task<AndConstraint<TAssertions>> CompleteWithinAsync(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
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);

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>
Expand Down Expand Up @@ -279,38 +232,6 @@ private async Task<Exception> InvokeWithInterceptionAsync(TimeSpan timeout)
}
}

/// <summary>
/// Asserts that the current <see cref="Func{Task}"/> does not throw any exception.
/// </summary>
/// <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>> NotThrowAsync(string because = "", params object[] becauseArgs)
{
bool success = Execute.Assertion
.ForCondition(Subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} not to throw{reason}, but found <null>.");

if (success)
{
try
{
await Subject!.Invoke();
}
catch (Exception exception)
{
return NotThrowInternal(exception, because, becauseArgs);
}
}

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

/// <summary>
/// Asserts that the current <see cref="Func{Task}"/> does not throw an exception of type <typeparamref name="TException"/>.
/// </summary>
Expand Down Expand Up @@ -345,74 +266,6 @@ public async Task<AndConstraint<TAssertions>> NotThrowAsync<TException>(string b
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that the current <see cref="Func{T}"/> stops throwing any exception
/// after a specified amount of time.
/// </summary>
/// <remarks>
/// The <see cref="Func{T}"/> is invoked. If it raises an exception,
/// the invocation is repeated until it either stops raising any exceptions
/// or the specified wait time is exceeded.
/// </remarks>
/// <param name="waitTime">
/// The time after which the <see cref="Func{T}"/> should have stopped throwing any exception.
/// </param>
/// <param name="pollInterval">
/// The time between subsequent invocations of the <see cref="Func{T}"/>.
/// </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>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="waitTime"/> or <paramref name="pollInterval"/> are negative.</exception>
public Task<AndConstraint<TAssertions>> NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because = "",
params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNegative(waitTime);
Guard.ThrowIfArgumentIsNegative(pollInterval);

bool success = Execute.Assertion
.ForCondition(Subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} not to throw any exceptions after {0}{reason}, but found <null>.", waitTime);

if (success)
{
return AssertionTaskAsync();

async Task<AndConstraint<TAssertions>> AssertionTaskAsync()
{
TimeSpan? invocationEndTime = null;
Exception exception = null;
ITimer timer = Clock.StartTimer();

while (invocationEndTime is null || invocationEndTime < waitTime)
{
exception = await InvokeWithInterceptionAsync(Subject);

if (exception is null)
{
return new AndConstraint<TAssertions>((TAssertions)this);
}

await Clock.DelayAsync(pollInterval, CancellationToken.None);
invocationEndTime = timer.Elapsed;
}

Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect any exceptions after {0}{reason}, but found {1}.", waitTime, exception);

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

return Task.FromResult(new AndConstraint<TAssertions>((TAssertions)this));
}

/// <summary>
/// Invokes the subject and measures the sync execution time.
/// </summary>
Expand Down Expand Up @@ -452,7 +305,7 @@ private protected async Task<bool> CompletesWithinTimeoutAsync(Task target, Time
return true;
}

private static async Task<Exception> InvokeWithInterceptionAsync(Func<Task> action)
private protected static async Task<Exception> InvokeWithInterceptionAsync(Func<Task> action)
{
try
{
Expand Down
18 changes: 13 additions & 5 deletions Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@

namespace FluentAssertions.Specialized;

/// <summary>
/// Contains a number of methods to assert that an asynchronous method yields the expected result.
/// </summary>
/// <typeparam name="TResult">The type returned in the <see cref="Task{T}"/>.</typeparam>
public class GenericAsyncFunctionAssertions<TResult>
: AsyncFunctionAssertions<Task<TResult>, GenericAsyncFunctionAssertions<TResult>>
{
/// <summary>
/// Initializes a new instance of the <see cref="GenericAsyncFunctionAssertions{TResult}"/> class.
/// </summary>
public GenericAsyncFunctionAssertions(Func<Task<TResult>> subject, IExtractExceptions extractor)
: this(subject, extractor, new Clock())
{
}

/// <summary>
/// Initializes a new instance of the <see cref="GenericAsyncFunctionAssertions{TResult}"/> class with custom <see cref="IClock"/>.
/// </summary>
public GenericAsyncFunctionAssertions(Func<Task<TResult>> subject, IExtractExceptions extractor, IClock clock)
#pragma warning disable CS0618 // is currently obsolete to make it protected in Version 7
: base(subject, extractor, clock)
#pragma warning restore CS0618
{
}

Expand All @@ -32,7 +40,7 @@ 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>> CompleteWithinAsync(
public async Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> CompleteWithinAsync(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
bool success = Execute.Assertion
Expand Down Expand Up @@ -78,7 +86,7 @@ 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(
public async Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> NotThrowAsync(
string because = "", params object[] becauseArgs)
{
bool success = Execute.Assertion
Expand Down Expand Up @@ -125,7 +133,7 @@ public GenericAsyncFunctionAssertions(Func<Task<TResult>> subject, IExtractExcep
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="waitTime"/> or <paramref name="pollInterval"/> are negative.</exception>
public new Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> NotThrowAfterAsync(
public Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> NotThrowAfterAsync(
TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNegative(waitTime);
Expand Down
Loading

0 comments on commit 3205e04

Please sign in to comment.