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

Added assertions for non-generic TaskCompletionSource #1961

Merged
merged 2 commits into from Aug 15, 2022
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
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)
lg2de marked this conversation as resolved.
Show resolved Hide resolved
.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 @@ -2348,13 +2348,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 @@ -2466,13 +2467,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