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

Support chaining NotThrowAfter and NotThrowAsync #1289

Merged
merged 1 commit into from Apr 10, 2020
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
4 changes: 2 additions & 2 deletions Src/FluentAssertions/AssertionExtensions.cs
Expand Up @@ -71,7 +71,7 @@ public static Func<Task> Awaiting<T>(this T subject, Func<T, Task> action)

/// <summary>
/// Invokes the specified action on a subject so that you can chain it
/// with any of the assertions from <see cref="AsyncFunctionAssertions"/>
/// with any of the assertions from <see cref="NonGenericAsyncFunctionAssertions"/>
/// </summary>
[Pure]
public static Func<Task> Awaiting<T>(this T subject, Func<T, ValueTask> action)
Expand All @@ -81,7 +81,7 @@ public static Func<Task> Awaiting<T>(this T subject, Func<T, ValueTask> action)

/// <summary>
/// Invokes the specified action on a subject so that you can chain it
/// with any of the assertions from <see cref="AsyncFunctionAssertions"/>
/// with any of the assertions from <see cref="GenericAsyncFunctionAssertions{TResult}"/>
/// </summary>
[Pure]
public static Func<Task<TResult>> Awaiting<T, TResult>(this T subject, Func<T, ValueTask<TResult>> action)
Expand Down
8 changes: 1 addition & 7 deletions Src/FluentAssertions/Specialized/ActionAssertions.cs
Expand Up @@ -8,22 +8,16 @@ namespace FluentAssertions.Specialized
/// Contains a number of methods to assert that an <see cref="Action"/> yields the expected result.
/// </summary>
[DebuggerNonUserCode]
public class ActionAssertions : DelegateAssertions<Action>
public class ActionAssertions : DelegateAssertions<Action, ActionAssertions>
{
public ActionAssertions(Action subject, IExtractExceptions extractor) : this(subject, extractor, new Clock())
{
}

public ActionAssertions(Action subject, IExtractExceptions extractor, IClock clock) : base(subject, extractor, clock)
{
Subject = subject;
}

/// <summary>
/// Gets the <see cref="Action"/> that is being asserted.
/// </summary>
public new Action Subject { get; }

protected override void InvokeSubject()
{
Subject();
Expand Down
101 changes: 87 additions & 14 deletions Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
Expand Up @@ -11,22 +11,18 @@ namespace FluentAssertions.Specialized
/// Contains a number of methods to assert that an asynchronous method yields the expected result.
/// </summary>
[DebuggerNonUserCode]
public class AsyncFunctionAssertions : DelegateAssertions<Func<Task>>
public class AsyncFunctionAssertions<TTask, TAssertions> : DelegateAssertions<Func<TTask>, TAssertions>
where TTask : Task
where TAssertions : AsyncFunctionAssertions<TTask, TAssertions>
{
public AsyncFunctionAssertions(Func<Task> subject, IExtractExceptions extractor) : this(subject, extractor, new Clock())
public AsyncFunctionAssertions(Func<TTask> subject, IExtractExceptions extractor) : this(subject, extractor, new Clock())
{
}

public AsyncFunctionAssertions(Func<Task> subject, IExtractExceptions extractor, IClock clock) : base(subject, extractor, clock)
public AsyncFunctionAssertions(Func<TTask> subject, IExtractExceptions extractor, IClock clock) : base(subject, extractor, clock)
{
Subject = subject;
}

/// <summary>
/// Gets the <see cref="Func{Task}"/> that is being asserted.
/// </summary>
public new Func<Task> Subject { get; }

protected override string Identifier => "async function";

private protected override bool CanHandleAsync => true;
Expand All @@ -36,6 +32,77 @@ protected override void InvokeSubject()
Subject.ExecuteInDefaultSynchronizationContext().GetAwaiter().GetResult();
}

/// <summary>
/// Asserts that the current <typeparamref name="TTask"/> 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 <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> CompleteWithin(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.ForCondition(Subject is object)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}, but found <null>.", timeSpan);

TTask task = Subject.ExecuteInDefaultSynchronizationContext();
bool completed = Clock.Wait(task, timeSpan);

Execute.Assertion
.ForCondition(completed)
.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 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)
{
Execute.Assertion
.ForCondition(Subject is object)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}, but found <null>.", timeSpan);

using (var timeoutCancellationTokenSource = new CancellationTokenSource())
{
TTask task = Subject.ExecuteInDefaultSynchronizationContext();

Task completedTask =
await Task.WhenAny(task, Clock.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 AndConstraint<TAssertions>((TAssertions)this);
}
}

/// <summary>
/// Asserts that the current <see cref="Func{Task}"/> throws an exception of the exact type <typeparamref name="TException"/> (and not a derived exception type).
/// </summary>
Expand Down Expand Up @@ -108,7 +175,7 @@ protected override void InvokeSubject()
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public async Task NotThrowAsync(string because = "", params object[] becauseArgs)
public async Task<AndConstraint<TAssertions>> NotThrowAsync(string because = "", params object[] becauseArgs)
{
Execute.Assertion
.ForCondition(Subject is object)
Expand All @@ -123,6 +190,8 @@ public async Task NotThrowAsync(string because = "", params object[] becauseArgs
{
NotThrow(exception, because, becauseArgs);
}

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

/// <summary>
Expand All @@ -135,7 +204,7 @@ public async Task NotThrowAsync(string because = "", params object[] becauseArgs
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public async Task NotThrowAsync<TException>(string because = "", params object[] becauseArgs)
public async Task<AndConstraint<TAssertions>> NotThrowAsync<TException>(string because = "", params object[] becauseArgs)
where TException : Exception
{
Execute.Assertion
Expand All @@ -151,6 +220,8 @@ public async Task NotThrowAsync<TException>(string because = "", params object[]
{
NotThrow<TException>(exception, because, becauseArgs);
}

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

/// <summary>
Expand All @@ -176,7 +247,7 @@ public async Task NotThrowAsync<TException>(string because = "", params object[]
/// 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 Task NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
public Task<AndConstraint<TAssertions>> NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
{
if (waitTime < TimeSpan.Zero)
{
Expand All @@ -198,7 +269,7 @@ public Task NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string

return assertionTask();

async Task assertionTask()
async Task<AndConstraint<TAssertions>> assertionTask()
{
TimeSpan? invocationEndTime = null;
Exception exception = null;
Expand All @@ -209,7 +280,7 @@ async Task assertionTask()
exception = await InvokeWithInterceptionAsync(wrappedSubject);
if (exception is null)
{
return;
return new AndConstraint<TAssertions>((TAssertions)this);
}

await Clock.DelayAsync(pollInterval, CancellationToken.None);
Expand All @@ -219,6 +290,8 @@ async Task assertionTask()
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);
}
}

Expand Down
27 changes: 11 additions & 16 deletions Src/FluentAssertions/Specialized/DelegateAssertions.cs
Expand Up @@ -4,15 +4,16 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using FluentAssertions.Common;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;

namespace FluentAssertions.Specialized
{
[DebuggerNonUserCode]
public abstract class DelegateAssertions<TDelegate> : ReferenceTypeAssertions<Delegate, DelegateAssertions<TDelegate>> where TDelegate : Delegate
public abstract class DelegateAssertions<TDelegate, TAssertions> : ReferenceTypeAssertions<TDelegate, DelegateAssertions<TDelegate, TAssertions>>
where TDelegate : Delegate
where TAssertions : DelegateAssertions<TDelegate, TAssertions>
{
private readonly IExtractExceptions extractor;

Expand All @@ -24,14 +25,8 @@ private protected DelegateAssertions(TDelegate @delegate, IExtractExceptions ext
{
this.extractor = extractor ?? throw new ArgumentNullException(nameof(extractor));
Clock = clock ?? throw new ArgumentNullException(nameof(clock));
Subject = @delegate;
}

/// <summary>
/// Gets the <typeparamref name="TDelegate"/> that is being asserted.
/// </summary>
public new TDelegate Subject { get; }

private protected IClock Clock { get; }

private protected virtual bool CanHandleAsync => false;
Expand Down Expand Up @@ -69,7 +64,7 @@ public ExceptionAssertions<TException> Throw<TException>(string because = "", pa
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<DelegateAssertions<TDelegate>> NotThrow<TException>(string because = "", params object[] becauseArgs)
public AndConstraint<TAssertions> NotThrow<TException>(string because = "", params object[] becauseArgs)
where TException : Exception
{
Execute.Assertion
Expand All @@ -92,7 +87,7 @@ public AndConstraint<DelegateAssertions<TDelegate>> NotThrow<TException>(string
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<DelegateAssertions<TDelegate>> NotThrow(string because = "", params object[] becauseArgs)
public AndConstraint<TAssertions> NotThrow(string because = "", params object[] becauseArgs)
{
Execute.Assertion
.ForCondition(Subject is object)
Expand Down Expand Up @@ -167,7 +162,7 @@ public AndConstraint<DelegateAssertions<TDelegate>> NotThrow(string because = ""
/// 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 AndConstraint<DelegateAssertions<TDelegate>> NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
public AndConstraint<TAssertions> NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
{
Execute.Assertion
.ForCondition(Subject is object)
Expand Down Expand Up @@ -206,7 +201,7 @@ public AndConstraint<DelegateAssertions<TDelegate>> NotThrowAfter(TimeSpan waitT
.ForCondition(exception is null)
.FailWith("Did not expect any exceptions after {0}{reason}, but found {1}.", waitTime, exception);

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

protected ExceptionAssertions<TException> Throw<TException>(Exception exception, string because, object[] becauseArgs)
Expand All @@ -231,25 +226,25 @@ protected ExceptionAssertions<TException> Throw<TException>(Exception exception,
return new ExceptionAssertions<TException>(expectedExceptions);
}

protected AndConstraint<DelegateAssertions<TDelegate>> NotThrow(Exception exception, string because, object[] becauseArgs)
protected AndConstraint<TAssertions> NotThrow(Exception exception, string because, object[] becauseArgs)
{
Execute.Assertion
.ForCondition(exception is null)
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect any exception{reason}, but found {0}.", exception);

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

protected AndConstraint<DelegateAssertions<TDelegate>> NotThrow<TException>(Exception exception, string because, object[] becauseArgs) where TException : Exception
protected AndConstraint<TAssertions> NotThrow<TException>(Exception exception, string because, object[] becauseArgs) where TException : Exception
{
IEnumerable<TException> exceptions = extractor.OfType<TException>(exception);
Execute.Assertion
.ForCondition(!exceptions.Any())
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect {0}{reason}, but found {1}.", typeof(TException), exception);

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

protected abstract void InvokeSubject();
Expand Down
64 changes: 64 additions & 0 deletions Src/FluentAssertions/Specialized/FunctionAssertionHelpers.cs
@@ -0,0 +1,64 @@
using System;
using FluentAssertions.Common;
using FluentAssertions.Execution;

namespace FluentAssertions.Specialized
{
internal class FunctionAssertionHelpers
{
internal static T NotThrow<T>(Func<T> subject, string because, object[] becauseArgs)
{
try
{
return subject();
}
catch (Exception exception)
{
Execute.Assertion
.ForCondition(exception is null)
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect any exception{reason}, but found {0}.", exception);

return default;
}
}

internal static TResult NotThrowAfter<TResult>(Func<TResult> subject, IClock clock, TimeSpan waitTime, TimeSpan pollInterval, string because, object[] becauseArgs)
{
if (waitTime < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(waitTime), $"The value of {nameof(waitTime)} must be non-negative.");
}

if (pollInterval < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(pollInterval), $"The value of {nameof(pollInterval)} must be non-negative.");
}

TimeSpan? invocationEndTime = null;
Exception exception = null;
ITimer timer = clock.StartTimer();

while (invocationEndTime is null || invocationEndTime < waitTime)
{
try
{
return subject();
}
catch (Exception ex)
{
exception = ex;
}

clock.Delay(pollInterval);
invocationEndTime = timer.Elapsed;
}

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

return default;
}
}
}