Skip to content

Commit

Permalink
Add Should().NotThrowAfter assertion for actions (#942)
Browse files Browse the repository at this point in the history
Add `NotThrowAfter` for `Action`, `Func<T>` and `Task<T>`
  • Loading branch information
frederik-h authored and jnyrup committed Jan 11, 2019
1 parent 0702ef4 commit 6cf1d34
Show file tree
Hide file tree
Showing 7 changed files with 794 additions and 5 deletions.
64 changes: 63 additions & 1 deletion Src/FluentAssertions/Specialized/ActionAssertions.cs
Expand Up @@ -4,7 +4,9 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

#if !NETSTANDARD1_3 && !NETSTANDARD1_6
using System.Threading;
#endif
using FluentAssertions.Common;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
Expand Down Expand Up @@ -114,6 +116,66 @@ public void NotThrow(string because = "", params object[] becauseArgs)
}
}

#if !NETSTANDARD1_3 && !NETSTANDARD1_6
/// <summary>
/// Asserts that the current <see cref="Action"/> stops throwing any exception
/// after a specified amount of time.
/// </summary>
/// <remarks>
/// The <see cref="Action"/> 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="Action"/> should have stopped throwing any exception.
/// </param>
/// <param name="pollInterval">
/// The time between subsequent invocations of the <see cref="Action"/>.
/// </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>
/// <exception cref="ArgumentOutOfRangeException">Throws if waitTime or pollInterval are negative.</exception>
public void NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs)
{
FailIfSubjectIsAsyncVoid();

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;
var watch = Stopwatch.StartNew();

while (invocationEndTime is null || invocationEndTime < waitTime)
{
exception = InvokeSubjectWithInterception();
if (exception is null)
{
return;
}

Thread.Sleep(pollInterval);
invocationEndTime = watch.Elapsed;
}

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

private Exception InvokeSubjectWithInterception()
{
Exception actualException = null;
Expand Down
116 changes: 116 additions & 0 deletions Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
Expand Up @@ -174,6 +174,122 @@ private static void NotThrow(Exception exception, string because, object[] becau
}
}

/// <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 <see cref="because" />.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">Throws if waitTime or pollInterval are negative.</exception>
public void NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params 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;
var watch = Stopwatch.StartNew();

while (invocationEndTime is null || invocationEndTime < waitTime)
{
exception = InvokeSubjectWithInterception();
if (exception is null)
{
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);
}

/// <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 <see cref="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)
{
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.");
}

return assertionTask();

async Task assertionTask()
{
TimeSpan? invocationEndTime = null;
Exception exception = null;
var watch = Stopwatch.StartNew();

while (invocationEndTime is null || invocationEndTime < waitTime)
{
exception = await InvokeSubjectWithInterceptionAsync();
if (exception is null)
{
return;
}

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

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

private static Exception GetFirstNonAggregateException(Exception exception)
{
Exception nonAggregateException = exception;
Expand Down
68 changes: 66 additions & 2 deletions Src/FluentAssertions/Specialized/FunctionAssertions.cs
@@ -1,7 +1,9 @@
using System;
using System.Diagnostics;
using System.Linq;

#if !NETSTANDARD1_3 && !NETSTANDARD1_6
using System.Threading;
#endif
using FluentAssertions.Execution;
using FluentAssertions.Primitives;

Expand Down Expand Up @@ -79,7 +81,6 @@ public ExceptionAssertions<TException> ThrowExactly<TException>(string because =
{
NotThrow(exception, because, becauseArgs);
return null;

}
}

Expand Down Expand Up @@ -130,6 +131,69 @@ private static void NotThrow(Exception exception, string because, object[] becau
}
}

#if !NETSTANDARD1_3 && !NETSTANDARD1_6
/// <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 <see cref="because" />.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">Throws if waitTime or pollInterval are negative.</exception>
public AndWhichConstraint<FunctionAssertions<T>, T> NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params 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;
var watch = Stopwatch.StartNew();

while (invocationEndTime is null || invocationEndTime < waitTime)
{
try
{
T result = Subject();
return new AndWhichConstraint<FunctionAssertions<T>, T>(this, result);
}
catch (Exception e)
{
exception = e;
}

Thread.Sleep(pollInterval);
invocationEndTime = watch.Elapsed;
}

Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect any exceptions after {0}{reason}, but found {1}.", waitTime, exception);
return null; // never reached
}
#endif

private static Exception GetFirstNonAggregateException(Exception exception)
{
Exception nonAggregateException = exception;
Expand Down

0 comments on commit 6cf1d34

Please sign in to comment.