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

Assertions for Task and Task<T> with CompleteWithin checks #1048

Merged
merged 1 commit into from May 28, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -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
This conversation was marked as resolved by jnyrup

This comment has been minimized.

Copy link
@jnyrup

jnyrup May 27, 2019

Collaborator

Should we make this internal?

This comment has been minimized.

Copy link
@dennisdoomen

dennisdoomen May 27, 2019

Author Member

We can only do that if we give the specs access to its internals using [InternalsVisibleTo

{
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;
}
}
}
@@ -1,16 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions.Common;
using FluentAssertions.Specialized;

namespace FluentAssertions
{
public static partial class AssertionExtensions
{
private static readonly AggregateExceptionExtractor extractor = new AggregateExceptionExtractor();

/// <summary>
/// Asserts that the <paramref name="actionAssertions"/> subject throws the exact exception (and not a derived exception type).
/// </summary>
@@ -88,38 +83,5 @@ public static partial class AssertionExtensions
exceptionAssertions.Which.GetType().Should().Be<TException>(because, becauseArgs);
return exceptionAssertions;
}

private 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;
}
}
}
}
@@ -25,6 +25,8 @@ namespace FluentAssertions
[DebuggerNonUserCode]
public static partial class AssertionExtensions
{
private static readonly AggregateExceptionExtractor extractor = new AggregateExceptionExtractor();

/// <summary>
/// Invokes the specified action on a subject so that you can chain it
/// with any of the assertions from <see cref="ActionAssertions"/>
@@ -662,19 +664,19 @@ public static ActionAssertions Should(this Action action)
/// current <see cref="System.Func{Task}"/> .
/// </summary>
[Pure]
public static AsyncFunctionAssertions Should(this Func<Task> action)
public static NonGenericAsyncFunctionAssertions Should(this Func<Task> action)
This conversation was marked as resolved by jnyrup

This comment has been minimized.

Copy link
@jnyrup

jnyrup May 27, 2019

Collaborator

Strictly speaking this is apparently a breaking change
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md#signatures

but those rules are also ultra strict as they need to be reflection and serialization compatible.
Since we're returning a more derived type, we should be covered?

This comment has been minimized.

Copy link
@dennisdoomen

dennisdoomen May 27, 2019

Author Member

From a compile-time perspective, it'll be fine indeed. But technically, this could cause a binary incompatibility of some other library relied on this method. I'm not sure I'm willing to be that strict.

This comment has been minimized.

Copy link
@dennisdoomen

dennisdoomen May 27, 2019

Author Member

Do you agree with this?

This comment has been minimized.

Copy link
@jnyrup

jnyrup May 27, 2019

Collaborator

Wouldn't that other library be able to match it against the base type AsyncFunctionAssertions?
Maybe I'm just to tired to review any more code for today.

This comment has been minimized.

Copy link
@jnyrup

jnyrup May 28, 2019

Collaborator

I was to apparently to tired to even answer your question.
I'm good with this change and the potential risk.

{
return new AsyncFunctionAssertions(action.ExecuteInDefaultSynchronizationContext, extractor);
return new NonGenericAsyncFunctionAssertions(action.ExecuteInDefaultSynchronizationContext, extractor, new TaskTimer());
}

/// <summary>
/// Returns a <see cref="AsyncFunctionAssertions"/> object that can be used to assert the
/// current <see><cref>System.Func{Task{T}}</cref></see>.
/// </summary>
[Pure]
public static AsyncFunctionAssertions Should<T>(this Func<Task<T>> action)
public static GenericAsyncFunctionAssertions<T> Should<T>(this Func<Task<T>> action)
{
return new AsyncFunctionAssertions(action.ExecuteInDefaultSynchronizationContext, extractor);
return new GenericAsyncFunctionAssertions<T>(action.ExecuteInDefaultSynchronizationContext, extractor, new TaskTimer());
}

/// <summary>
@@ -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);
}
}
@@ -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())
This conversation was marked as resolved by jnyrup

This comment has been minimized.

Copy link
@jnyrup

jnyrup May 27, 2019

Collaborator

Is there any problem in Tasks being nested twice in a NoSynchronizationContextScope.
First in Should and then again here?

This comment has been minimized.

Copy link
@dennisdoomen

dennisdoomen May 27, 2019

Author Member

It's fine if you nest them. It'll just get the null context, set it to null and then restore the null ;-)

{
return task.Wait(timeout);
}
}
}
}
@@ -52,7 +52,8 @@ public ExceptionAssertions<TException> Throw<TException>(string because = "", pa
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public async Task<ExceptionAssertions<TException>> ThrowAsync<TException>(string because = "", params object[] becauseArgs)
public async Task<ExceptionAssertions<TException>> ThrowAsync<TException>(string because = "",
params object[] becauseArgs)
where TException : Exception
{
Exception exception = await InvokeSubjectWithInterceptionAsync();
@@ -202,7 +203,8 @@ public void NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string becau

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

TimeSpan? invocationEndTime = null;
@@ -216,9 +218,11 @@ public void NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string becau
{
return;
}

Task.Delay(pollInterval).Wait();
invocationEndTime = watch.Elapsed;
}

Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect any exceptions after {0}{reason}, but found {1}.", waitTime, exception);
@@ -248,7 +252,7 @@ public void NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string becau
/// </param>
/// <exception cref="ArgumentOutOfRangeException">Throws if waitTime or pollInterval are negative.</exception>
public
Task NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
Task NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
{
if (waitTime < TimeSpan.Zero)
{
@@ -257,7 +261,8 @@ Task NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because

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

return assertionTask();
@@ -289,7 +294,7 @@ async Task assertionTask()
private ExceptionAssertions<TException> Throw<TException>(Exception exception, string because, object[] becauseArgs)
where TException : Exception
{
var exceptions = extractor.OfType<TException>(exception);
var exceptions = extractor.OfType<TException>(exception).ToArray();

Execute.Assertion
.ForCondition(exception != null)
@@ -0,0 +1,82 @@
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(

This comment has been minimized.

Copy link
@jnyrup

jnyrup May 28, 2019

Collaborator

Could we return TResult instead of Task<TResult>?
That would remove the need in tests to call .Result.

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)
{
using (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);
}
}
}
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.