Skip to content

Commit

Permalink
Merge pull request #1324 from lg2de/AsyncContext
Browse files Browse the repository at this point in the history
Keep synchronization context of executing thread when asserting asynchronous operations
  • Loading branch information
dennisdoomen committed Jul 20, 2020
2 parents d9cf0a0 + 3777b62 commit 4396c8f
Show file tree
Hide file tree
Showing 25 changed files with 609 additions and 1,077 deletions.
25 changes: 22 additions & 3 deletions Src/FluentAssertions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Threading.Tasks;
using System.Xml.Linq;
using FluentAssertions.Collections;
using FluentAssertions.Common;
#if !NETSTANDARD2_0
using FluentAssertions.Events;
#endif
Expand Down Expand Up @@ -126,7 +125,7 @@ public static ExecutionTime ExecutionTime(this Action action)
[MustUseReturnValue /* do not use Pure because this method executes the action before returning to the caller */]
public static ExecutionTime ExecutionTime(this Func<Task> action)
{
return new ExecutionTime(action.ExecuteInDefaultSynchronizationContext);
return new ExecutionTime(action);
}

/// <summary>
Expand Down Expand Up @@ -773,6 +772,8 @@ public static TTo As<TTo>(this object subject)
return subject is TTo to ? to : default;
}

#pragma warning disable AV1755 // "Name of async method ... should end with Async"; Async suffix is too noisy in fluent API

/// <summary>
/// Asserts that the thrown exception has a message that matches <paramref name="expectedWildcardPattern" />.
/// </summary>
Expand All @@ -787,7 +788,6 @@ public static TTo As<TTo>(this object subject)
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
/// </param>
#pragma warning disable AV1755 // Changing would be a breaking change
public static async Task<ExceptionAssertions<TException>> WithMessage<TException>(
this Task<ExceptionAssertions<TException>> task,
string expectedWildcardPattern,
Expand All @@ -797,6 +797,25 @@ public static TTo As<TTo>(this object subject)
{
return (await task).WithMessage(expectedWildcardPattern, because, becauseArgs);
}

/// <summary>
/// Asserts that the thrown exception contains an inner exception of type <typeparamref name="TInnerException" />.
/// </summary>
/// <typeparam name="TException">The expected type of the exception.</typeparam>
/// <typeparam name="TInnerException">The expected type of the inner exception.</typeparam>
/// <param name="task">The <see cref="ExceptionAssertions{TException}"/> containing the thrown exception.</param>
/// <param name="because">The reason why the inner exception should be of the supplied type.</param>
/// <param name="becauseArgs">The parameters used when formatting the <paramref name="because" />.</param>
public static async Task<ExceptionAssertions<TInnerException>> WithInnerException<TException, TInnerException>(
this Task<ExceptionAssertions<TException>> task,
string because = "",
params object[] becauseArgs)
where TException : Exception
where TInnerException : Exception
{
return (await task).WithInnerException<TInnerException>(because, becauseArgs);
}

#pragma warning restore AV1755
}
}
6 changes: 0 additions & 6 deletions Src/FluentAssertions/Common/Clock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
return Task.Delay(delay, cancellationToken);
}

public bool Wait(Task task, TimeSpan timeout)
{
using var _ = NoSynchronizationContextScope.Enter();
return task.Wait(timeout);
}

public ITimer StartTimer() => new StopwatchTimer();
}
}
8 changes: 0 additions & 8 deletions Src/FluentAssertions/Common/IClock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,6 @@ public interface IClock
/// <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);

/// <summary>
/// Creates a timer to measure the time to complete some arbitrary executions.
/// </summary>
Expand Down
30 changes: 0 additions & 30 deletions Src/FluentAssertions/Common/NoSynchronizationContextScope.cs

This file was deleted.

25 changes: 0 additions & 25 deletions Src/FluentAssertions/Common/TaskExtensions.cs

This file was deleted.

53 changes: 7 additions & 46 deletions Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace FluentAssertions.Specialized
/// Contains a number of methods to assert that an asynchronous method yields the expected result.
/// </summary>
[DebuggerNonUserCode]
public class AsyncFunctionAssertions<TTask, TAssertions> : DelegateAssertions<Func<TTask>, TAssertions>
public class AsyncFunctionAssertions<TTask, TAssertions> : DelegateAssertionsBase<Func<TTask>, TAssertions>
where TTask : Task
where TAssertions : AsyncFunctionAssertions<TTask, TAssertions>
{
Expand All @@ -27,43 +27,6 @@ public AsyncFunctionAssertions(Func<TTask> subject, IExtractExceptions extractor

protected override string Identifier => "async function";

private protected override bool CanHandleAsync => true;

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>
Expand All @@ -84,7 +47,7 @@ protected override void InvokeSubject()
.FailWith("Expected {context:task} to complete within {0}{reason}, but found <null>.", timeSpan);

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

Task completedTask =
await Task.WhenAny(task, Clock.DelayAsync(timeSpan, timeoutCancellationTokenSource.Token));
Expand Down Expand Up @@ -130,7 +93,7 @@ protected override void InvokeSubject()
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} to throw exactly {0}{reason}, but found <null>.", expectedType);

Exception exception = await InvokeWithInterceptionAsync(Subject.ExecuteInDefaultSynchronizationContext);
Exception exception = await InvokeWithInterceptionAsync(Subject);

Execute.Assertion
.ForCondition(exception != null)
Expand Down Expand Up @@ -161,7 +124,7 @@ protected override void InvokeSubject()
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} to throw {0}{reason}, but found <null>.", typeof(TException));

Exception exception = await InvokeWithInterceptionAsync(Subject.ExecuteInDefaultSynchronizationContext);
Exception exception = await InvokeWithInterceptionAsync(Subject);
return Throw<TException>(exception, because, becauseArgs);
}

Expand All @@ -184,7 +147,7 @@ public async Task<AndConstraint<TAssertions>> NotThrowAsync(string because = "",

try
{
await Subject.ExecuteInDefaultSynchronizationContext();
await Subject.Invoke();
}
catch (Exception exception)
{
Expand Down Expand Up @@ -214,7 +177,7 @@ public async Task<AndConstraint<TAssertions>> NotThrowAsync<TException>(string b

try
{
await Subject.ExecuteInDefaultSynchronizationContext();
await Subject.Invoke();
}
catch (Exception exception)
{
Expand Down Expand Up @@ -265,8 +228,6 @@ public Task<AndConstraint<TAssertions>> NotThrowAfterAsync(TimeSpan waitTime, Ti
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} not to throw any exceptions after {0}{reason}, but found <null>.", waitTime);

Func<Task> wrappedSubject = Subject.ExecuteInDefaultSynchronizationContext;

return AssertionTaskAsync();

async Task<AndConstraint<TAssertions>> AssertionTaskAsync()
Expand All @@ -277,7 +238,7 @@ async Task<AndConstraint<TAssertions>> AssertionTaskAsync()

while (invocationEndTime is null || invocationEndTime < waitTime)
{
exception = await InvokeWithInterceptionAsync(wrappedSubject);
exception = await InvokeWithInterceptionAsync(Subject);
if (exception is null)
{
return new AndConstraint<TAssertions>((TAssertions)this);
Expand Down
66 changes: 7 additions & 59 deletions Src/FluentAssertions/Specialized/DelegateAssertions.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,30 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using FluentAssertions.Common;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;

namespace FluentAssertions.Specialized
{
/// <summary>
/// Contains a number of methods to assert that a synchronous method yields the expected result.
/// </summary>
[DebuggerNonUserCode]
public abstract class DelegateAssertions<TDelegate, TAssertions> : ReferenceTypeAssertions<TDelegate, DelegateAssertions<TDelegate, TAssertions>>
public abstract class DelegateAssertions<TDelegate, TAssertions> : DelegateAssertionsBase<TDelegate, TAssertions>
where TDelegate : Delegate
where TAssertions : DelegateAssertions<TDelegate, TAssertions>
{
private readonly IExtractExceptions extractor;

protected DelegateAssertions(TDelegate @delegate, IExtractExceptions extractor)
: this(@delegate, extractor, new Clock())
: base(@delegate, extractor, new Clock())
{
}

private protected DelegateAssertions(TDelegate @delegate, IExtractExceptions extractor, IClock clock)
: base(@delegate)
: base(@delegate, extractor, clock)
{
this.extractor = extractor ?? throw new ArgumentNullException(nameof(extractor));
Clock = clock ?? throw new ArgumentNullException(nameof(clock));
}

private protected IClock Clock { get; }

private protected virtual bool CanHandleAsync => false;

/// <summary>
/// Asserts that the current <see cref="Delegate" /> throws an exception of type <typeparamref name="TException"/>.
/// </summary>
Expand Down Expand Up @@ -206,50 +198,6 @@ public AndConstraint<TAssertions> NotThrowAfter(TimeSpan waitTime, TimeSpan poll
return new AndConstraint<TAssertions>((TAssertions)this);
}

protected ExceptionAssertions<TException> Throw<TException>(Exception exception, string because, object[] becauseArgs)
where TException : Exception
{
TException[] expectedExceptions = extractor.OfType<TException>(exception).ToArray();

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected a <{0}> to be thrown{reason}, ", typeof(TException))
.ForCondition(exception != null)
.FailWith("but no exception was thrown.")
.Then
.ForCondition(expectedExceptions.Any())
.FailWith("but found <{0}>: {1}{2}.",
exception?.GetType(),
Environment.NewLine,
exception)
.Then
.ClearExpectation();

return new ExceptionAssertions<TException>(expectedExceptions);
}

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<TAssertions>((TAssertions)this);
}

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<TAssertions>((TAssertions)this);
}

protected abstract void InvokeSubject();

private Exception InvokeSubjectWithInterception()
Expand All @@ -270,7 +218,7 @@ private Exception InvokeSubjectWithInterception()

private void FailIfSubjectIsAsyncVoid()
{
if (!CanHandleAsync && Subject.GetMethodInfo().IsDecoratedWithOrInherit<AsyncStateMachineAttribute>())
if (Subject.GetMethodInfo().IsDecoratedWithOrInherit<AsyncStateMachineAttribute>())
{
throw new InvalidOperationException("Cannot use action assertions on an async void method. Assign the async method to a variable of type Func<Task> instead of Action so that it can be awaited.");
}
Expand Down

0 comments on commit 4396c8f

Please sign in to comment.