Skip to content

Commit

Permalink
Merge pull request #1961 from lg2de/tcs
Browse files Browse the repository at this point in the history
Added assertions for non-generic TaskCompletionSource
  • Loading branch information
jnyrup committed Aug 15, 2022
2 parents 05f4f4b + 9fce45e commit 98fbfb2
Show file tree
Hide file tree
Showing 12 changed files with 483 additions and 218 deletions.
16 changes: 15 additions & 1 deletion Src/FluentAssertions/AssertionExtensions.cs
Expand Up @@ -919,6 +919,20 @@ public static IMonitor<T> Monitor<T>(this T eventSource, Func<DateTime> utcNow =
return new EventMonitor<T>(eventSource, utcNow ?? (() => DateTime.UtcNow));
}

#endif

#if NET6_0_OR_GREATER

/// <summary>
/// Returns a <see cref="TaskCompletionSourceAssertions"/> object that can be used to assert the
/// current <see cref="TaskCompletionSource"/>.
/// </summary>
[Pure]
public static TaskCompletionSourceAssertions Should(this TaskCompletionSource tcs)
{
return new TaskCompletionSourceAssertions(tcs);
}

#endif

/// <summary>
Expand Down Expand Up @@ -1038,7 +1052,7 @@ public static void Should<TAssertions>(this SimpleTimeSpanAssertions<TAssertions

/// <inheritdoc cref="Should(ExecutionTimeAssertions)" />
[Obsolete("You are asserting the 'AndConstraint' itself. Remove the 'Should()' method directly following 'And'", error: true)]
public static void Should<TSubject>(this TaskCompletionSourceAssertions<TSubject> _)
public static void Should(this TaskCompletionSourceAssertionsBase _)
{
InvalidShouldCall();
}
Expand Down
162 changes: 133 additions & 29 deletions Src/FluentAssertions/Specialized/TaskCompletionSourceAssertions.cs
Expand Up @@ -8,7 +8,89 @@ namespace FluentAssertions.Specialized;

#pragma warning disable CS0659 // Ignore not overriding Object.GetHashCode()
#pragma warning disable CA1065 // Ignore throwing NotSupportedException from Equals
public class TaskCompletionSourceAssertions<T>

#if NET6_0_OR_GREATER
public class TaskCompletionSourceAssertions : TaskCompletionSourceAssertionsBase
{
private readonly TaskCompletionSource subject;

public TaskCompletionSourceAssertions(TaskCompletionSource tcs)
: this(tcs, new Clock())
{
}

public TaskCompletionSourceAssertions(TaskCompletionSource tcs, IClock clock)
: base(clock)
{
subject = tcs;
}

/// <summary>
/// Asserts that the <see cref="Task"/> of the current <see cref="TaskCompletionSource"/> 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<TaskCompletionSourceAssertions>> CompleteWithinAsync(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
var success = Execute.Assertion
.ForCondition(subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} to complete within {0}{reason}, but found <null>.", timeSpan);

if (success)
{
bool completesWithinTimeout = await CompletesWithinTimeoutAsync(subject.Task, timeSpan);
Execute.Assertion
.ForCondition(completesWithinTimeout)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan);
}

return new AndConstraint<TaskCompletionSourceAssertions>(this);
}

/// <summary>
/// Asserts that the <see cref="Task"/> of the current <see cref="TaskCompletionSource"/> will not complete within the specified time.
/// </summary>
/// <param name="timeSpan">The time span to wait 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<TaskCompletionSourceAssertions>> NotCompleteWithinAsync(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
var success = Execute.Assertion
.ForCondition(subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} to not complete within {0}{reason}, but found <null>.", timeSpan);

if (success)
{
bool completesWithinTimeout = await CompletesWithinTimeoutAsync(subject.Task, timeSpan);
Execute.Assertion
.ForCondition(!completesWithinTimeout)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to not complete within {0}{reason}.", timeSpan);
}

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

#endif

public class TaskCompletionSourceAssertions<T> : TaskCompletionSourceAssertionsBase
{
private readonly TaskCompletionSource<T> subject;

Expand All @@ -18,13 +100,11 @@ public TaskCompletionSourceAssertions(TaskCompletionSource<T> tcs)
}

public TaskCompletionSourceAssertions(TaskCompletionSource<T> tcs, IClock clock)
: base(clock)
{
subject = tcs;
Clock = clock ?? throw new ArgumentNullException(nameof(clock));
}

private protected IClock Clock { get; }

/// <summary>
/// Asserts that the <see cref="Task"/> of the current <see cref="TaskCompletionSource{T}"/> will complete within the specified time.
/// </summary>
Expand All @@ -39,27 +119,23 @@ public TaskCompletionSourceAssertions(TaskCompletionSource<T> tcs, IClock clock)
public async Task<AndWhichConstraint<TaskCompletionSourceAssertions<T>, T>> CompleteWithinAsync(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
Execute.Assertion
var success = Execute.Assertion
.ForCondition(subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context} to complete within {0}{reason}, but found <null>.", timeSpan);

using var timeoutCancellationTokenSource = new CancellationTokenSource();
Task completedTask = await Task.WhenAny(
subject.Task,
Clock.DelayAsync(timeSpan, timeoutCancellationTokenSource.Token));

if (completedTask == subject.Task)
if (!success)
{
timeoutCancellationTokenSource.Cancel();
await completedTask;
return new AndWhichConstraint<TaskCompletionSourceAssertions<T>, T>(this, default(T));
}

bool completesWithinTimeout = await CompletesWithinTimeoutAsync(subject.Task, timeSpan);
Execute.Assertion
.ForCondition(completedTask == subject.Task)
.ForCondition(completesWithinTimeout)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan);
return new AndWhichConstraint<TaskCompletionSourceAssertions<T>, T>(this, subject.Task.Result);
T result = subject.Task.IsCompleted ? subject.Task.Result : default;
return new AndWhichConstraint<TaskCompletionSourceAssertions<T>, T>(this, result);
}

/// <summary>
Expand All @@ -73,32 +149,60 @@ public TaskCompletionSourceAssertions(TaskCompletionSource<T> tcs, IClock clock)
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public async Task NotCompleteWithinAsync(
public async Task<AndConstraint<TaskCompletionSourceAssertions<T>>> NotCompleteWithinAsync(
TimeSpan timeSpan, string because = "", params object[] becauseArgs)
{
Execute.Assertion
var success = Execute.Assertion
.ForCondition(subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect {context} to complete within {0}{reason}, but found <null>.", timeSpan);

using var timeoutCancellationTokenSource = new CancellationTokenSource();
Task completedTask = await Task.WhenAny(
subject.Task,
Clock.DelayAsync(timeSpan, timeoutCancellationTokenSource.Token));

if (completedTask == subject.Task)
if (success)
{
timeoutCancellationTokenSource.Cancel();
await completedTask;
bool completesWithinTimeout = await CompletesWithinTimeoutAsync(subject.Task, timeSpan);
Execute.Assertion
.ForCondition(!completesWithinTimeout)
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect {context:task} to complete within {0}{reason}.", timeSpan);
}

Execute.Assertion
.ForCondition(completedTask != subject.Task)
.BecauseOf(because, becauseArgs)
.FailWith("Did not expect {context:task} to complete within {0}{reason}.", timeSpan);
return new AndConstraint<TaskCompletionSourceAssertions<T>>(this);
}
}

/// <summary>
/// Implements base functionality for assertions on TaskCompletionSource.
/// </summary>
public class TaskCompletionSourceAssertionsBase
{
protected TaskCompletionSourceAssertionsBase(IClock clock)
{
Clock = clock ?? throw new ArgumentNullException(nameof(clock));
}

private protected IClock Clock { get; }

/// <inheritdoc/>
public override bool Equals(object obj) =>
throw new NotSupportedException("Calling Equals on Assertion classes is not supported.");

/// <summary>
/// Monitors the specified task whether it completes withing the remaining time span.
/// </summary>
private protected async Task<bool> CompletesWithinTimeoutAsync(Task target, TimeSpan remainingTime)
{
using var timeoutCancellationTokenSource = new CancellationTokenSource();

Task completedTask =
await Task.WhenAny(target, Clock.DelayAsync(remainingTime, timeoutCancellationTokenSource.Token));

if (completedTask != target)
{
return false;
}

// cancel the clock
timeoutCancellationTokenSource.Cancel();
return true;
}
}
Expand Up @@ -43,6 +43,9 @@ namespace FluentAssertions
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
public static void Should(this FluentAssertions.Specialized.ExecutionTimeAssertions _) { }
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
public static void Should(this FluentAssertions.Specialized.TaskCompletionSourceAssertionsBase _) { }
public static FluentAssertions.Types.MethodInfoSelectorAssertions Should(this FluentAssertions.Types.MethodInfoSelector methodSelector) { }
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
Expand Down Expand Up @@ -135,9 +138,6 @@ namespace FluentAssertions
"ly following \'And\'", true)]
public static void Should<TAssertions>(this FluentAssertions.Primitives.SimpleTimeSpanAssertions<TAssertions> _)
where TAssertions : FluentAssertions.Primitives.SimpleTimeSpanAssertions<TAssertions> { }
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
public static void Should<TSubject>(this FluentAssertions.Specialized.TaskCompletionSourceAssertions<TSubject> _) { }
public static FluentAssertions.Collections.GenericCollectionAssertions<T> Should<T>(this System.Collections.Generic.IEnumerable<T> actualValue) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>(this System.Func<System.Threading.Tasks.Task<T>> action) { }
public static FluentAssertions.Specialized.FunctionAssertions<T> Should<T>(this System.Func<T> func) { }
Expand Down Expand Up @@ -2347,13 +2347,17 @@ namespace FluentAssertions.Specialized
public NonGenericAsyncFunctionAssertions(System.Func<System.Threading.Tasks.Task> subject, FluentAssertions.Specialized.IExtractExceptions extractor) { }
public NonGenericAsyncFunctionAssertions(System.Func<System.Threading.Tasks.Task> subject, FluentAssertions.Specialized.IExtractExceptions extractor, FluentAssertions.Common.IClock clock) { }
}
public class TaskCompletionSourceAssertions<T>
public class TaskCompletionSourceAssertionsBase
{
protected TaskCompletionSourceAssertionsBase(FluentAssertions.Common.IClock clock) { }
public override bool Equals(object obj) { }
}
public class TaskCompletionSourceAssertions<T> : FluentAssertions.Specialized.TaskCompletionSourceAssertionsBase
{
public TaskCompletionSourceAssertions(System.Threading.Tasks.TaskCompletionSource<T> tcs) { }
public TaskCompletionSourceAssertions(System.Threading.Tasks.TaskCompletionSource<T> tcs, FluentAssertions.Common.IClock clock) { }
public System.Threading.Tasks.Task<FluentAssertions.AndWhichConstraint<FluentAssertions.Specialized.TaskCompletionSourceAssertions<T>, T>> CompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public override bool Equals(object obj) { }
public System.Threading.Tasks.Task NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<FluentAssertions.Specialized.TaskCompletionSourceAssertions<T>>> NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
}
}
namespace FluentAssertions.Streams
Expand Down
Expand Up @@ -43,6 +43,9 @@ namespace FluentAssertions
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
public static void Should(this FluentAssertions.Specialized.ExecutionTimeAssertions _) { }
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
public static void Should(this FluentAssertions.Specialized.TaskCompletionSourceAssertionsBase _) { }
public static FluentAssertions.Types.MethodInfoSelectorAssertions Should(this FluentAssertions.Types.MethodInfoSelector methodSelector) { }
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
Expand Down Expand Up @@ -77,6 +80,7 @@ namespace FluentAssertions
public static FluentAssertions.Types.ConstructorInfoAssertions Should(this System.Reflection.ConstructorInfo constructorInfo) { }
public static FluentAssertions.Types.MethodInfoAssertions Should(this System.Reflection.MethodInfo methodInfo) { }
public static FluentAssertions.Types.PropertyInfoAssertions Should(this System.Reflection.PropertyInfo propertyInfo) { }
public static FluentAssertions.Specialized.TaskCompletionSourceAssertions Should(this System.Threading.Tasks.TaskCompletionSource tcs) { }
public static FluentAssertions.Primitives.TimeOnlyAssertions Should(this System.TimeOnly actualValue) { }
public static FluentAssertions.Primitives.NullableTimeOnlyAssertions Should(this System.TimeOnly? actualValue) { }
public static FluentAssertions.Primitives.SimpleTimeSpanAssertions Should(this System.TimeSpan actualValue) { }
Expand Down Expand Up @@ -147,9 +151,6 @@ namespace FluentAssertions
"ly following \'And\'", true)]
public static void Should<TAssertions>(this FluentAssertions.Primitives.TimeOnlyAssertions<TAssertions> _)
where TAssertions : FluentAssertions.Primitives.TimeOnlyAssertions<TAssertions> { }
[System.Obsolete("You are asserting the \'AndConstraint\' itself. Remove the \'Should()\' method direct" +
"ly following \'And\'", true)]
public static void Should<TSubject>(this FluentAssertions.Specialized.TaskCompletionSourceAssertions<TSubject> _) { }
public static FluentAssertions.Collections.GenericCollectionAssertions<T> Should<T>(this System.Collections.Generic.IEnumerable<T> actualValue) { }
public static FluentAssertions.Specialized.GenericAsyncFunctionAssertions<T> Should<T>(this System.Func<System.Threading.Tasks.Task<T>> action) { }
public static FluentAssertions.Specialized.FunctionAssertions<T> Should<T>(this System.Func<T> func) { }
Expand Down Expand Up @@ -2465,13 +2466,24 @@ namespace FluentAssertions.Specialized
public NonGenericAsyncFunctionAssertions(System.Func<System.Threading.Tasks.Task> subject, FluentAssertions.Specialized.IExtractExceptions extractor) { }
public NonGenericAsyncFunctionAssertions(System.Func<System.Threading.Tasks.Task> subject, FluentAssertions.Specialized.IExtractExceptions extractor, FluentAssertions.Common.IClock clock) { }
}
public class TaskCompletionSourceAssertions<T>
public class TaskCompletionSourceAssertions : FluentAssertions.Specialized.TaskCompletionSourceAssertionsBase
{
public TaskCompletionSourceAssertions(System.Threading.Tasks.TaskCompletionSource tcs) { }
public TaskCompletionSourceAssertions(System.Threading.Tasks.TaskCompletionSource tcs, FluentAssertions.Common.IClock clock) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<FluentAssertions.Specialized.TaskCompletionSourceAssertions>> CompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<FluentAssertions.Specialized.TaskCompletionSourceAssertions>> NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
}
public class TaskCompletionSourceAssertionsBase
{
protected TaskCompletionSourceAssertionsBase(FluentAssertions.Common.IClock clock) { }
public override bool Equals(object obj) { }
}
public class TaskCompletionSourceAssertions<T> : FluentAssertions.Specialized.TaskCompletionSourceAssertionsBase
{
public TaskCompletionSourceAssertions(System.Threading.Tasks.TaskCompletionSource<T> tcs) { }
public TaskCompletionSourceAssertions(System.Threading.Tasks.TaskCompletionSource<T> tcs, FluentAssertions.Common.IClock clock) { }
public System.Threading.Tasks.Task<FluentAssertions.AndWhichConstraint<FluentAssertions.Specialized.TaskCompletionSourceAssertions<T>, T>> CompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public override bool Equals(object obj) { }
public System.Threading.Tasks.Task NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
public System.Threading.Tasks.Task<FluentAssertions.AndConstraint<FluentAssertions.Specialized.TaskCompletionSourceAssertions<T>>> NotCompleteWithinAsync(System.TimeSpan timeSpan, string because = "", params object[] becauseArgs) { }
}
}
namespace FluentAssertions.Streams
Expand Down

0 comments on commit 98fbfb2

Please sign in to comment.