diff --git a/Source/aweXpect.Core/Core/Sources/DelegateSource.cs b/Source/aweXpect.Core/Core/Sources/DelegateSource.cs index 36d26d60c..e37319287 100644 --- a/Source/aweXpect.Core/Core/Sources/DelegateSource.cs +++ b/Source/aweXpect.Core/Core/Sources/DelegateSource.cs @@ -1,18 +1,34 @@ using System; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using aweXpect.Core.TimeSystem; namespace aweXpect.Core.Sources; -internal class DelegateSource(Action? action) : IValueSource +internal class DelegateSource : IValueSource { + private readonly Action? _action; + + public DelegateSource(Action? action) + { + ThrowIfAsyncVoid(action?.GetMethodInfo(), "Func"); + _action = action; + } + + public DelegateSource(Action? action) + { + ThrowIfAsyncVoid(action?.GetMethodInfo(), "Func"); + _action = action is null ? null : _ => action(); + } + #region IValueSource Members public Task GetValue(ITimeSystem timeSystem, CancellationToken cancellationToken) { - if (action is null) + if (_action is null) { return Task.FromResult(new DelegateValue(null, TimeSpan.Zero, true)); } @@ -21,7 +37,7 @@ public Task GetValue(ITimeSystem timeSystem, try { sw.Start(); - action(cancellationToken); + _action(cancellationToken); sw.Stop(); return Task.FromResult(new DelegateValue(null, sw.Elapsed)); } @@ -32,4 +48,14 @@ public Task GetValue(ITimeSystem timeSystem, } #endregion + + private static void ThrowIfAsyncVoid(MethodInfo? method, string replaceType) + { + if (method is not null && + Attribute.IsDefined(method, typeof(AsyncStateMachineAttribute), true)) + { + throw new InvalidOperationException( + $"Cannot use aweXpect on an async void method: Use {replaceType} instead."); + } + } } diff --git a/Source/aweXpect.Core/Expect.cs b/Source/aweXpect.Core/Expect.cs index ab65122b6..3e26cc57e 100644 --- a/Source/aweXpect.Core/Expect.cs +++ b/Source/aweXpect.Core/Expect.cs @@ -55,7 +55,7 @@ public static ThatDelegate.WithoutValue That(Action @delegate, [CallerArgumentExpression("delegate")] string doNotPopulateThisValue = "") => new(new ExpectationBuilder( // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - new DelegateSource(@delegate is null ? null : _ => @delegate()), doNotPopulateThisValue)); + new DelegateSource(@delegate), doNotPopulateThisValue)); /// /// Specify expectations for the current . diff --git a/Tests/aweXpect.Internal.Tests/ThatTests/DelegateTests.cs b/Tests/aweXpect.Internal.Tests/ThatTests/DelegateTests.cs index c8683e45d..924167357 100644 --- a/Tests/aweXpect.Internal.Tests/ThatTests/DelegateTests.cs +++ b/Tests/aweXpect.Internal.Tests/ThatTests/DelegateTests.cs @@ -5,6 +5,64 @@ namespace aweXpect.Internal.Tests.ThatTests; public sealed class DelegateTests { + [Fact] + public async Task ForAsyncVoidAction_WhenVerifyingDoesNotThrow_ShouldThrowInvalidOperationException() + { + Task incompleteTask = new TaskCompletionSource().Task; + // ReSharper disable once AsyncVoidLambda + Action @delegate = async () => await incompleteTask; + + async Task Act() + => await That(@delegate).DoesNotThrow(); + + await That(Act).Throws() + .WithMessage("Cannot use aweXpect on an async void method: Use Func instead."); + } + + [Fact] + public async Task + ForAsyncVoidAction_WithCancellationToken_WhenVerifyingDoesNotThrow_ShouldThrowInvalidOperationException() + { + Task incompleteTask = new TaskCompletionSource().Task; + // ReSharper disable once AsyncVoidLambda + Action @delegate = async _ => await incompleteTask; + + async Task Act() + => await That(@delegate).DoesNotThrow(); + + await That(Act).Throws() + .WithMessage("Cannot use aweXpect on an async void method: Use Func instead."); + } + + [Fact] + public async Task ForAsyncVoidAction_WhenVerifyingDoesThrow_ShouldThrowInvalidOperationException() + { + Task incompleteTask = new TaskCompletionSource().Task; + // ReSharper disable once AsyncVoidLambda + Action @delegate = async () => await incompleteTask; + + async Task Act() + => await That(@delegate).ThrowsException(); + + await That(Act).Throws() + .WithMessage("Cannot use aweXpect on an async void method: Use Func instead."); + } + + [Fact] + public async Task + ForAsyncVoidAction_WithCancellationToken_WhenVerifyingDoesThrow_ShouldThrowInvalidOperationException() + { + Task incompleteTask = new TaskCompletionSource().Task; + // ReSharper disable once AsyncVoidLambda + Action @delegate = async _ => await incompleteTask; + + async Task Act() + => await That(@delegate).ThrowsException(); + + await That(Act).Throws() + .WithMessage("Cannot use aweXpect on an async void method: Use Func instead."); + } + [Theory] [AutoData] public async Task ShouldReturnValue_FuncTaskValue(int value)