From 5ed72616db3f2baf59cd0f590d422cb022ac8da4 Mon Sep 17 00:00:00 2001 From: Robert Coltheart Date: Tue, 26 Jan 2021 21:57:43 +0800 Subject: [PATCH] Support for async/await specifications --- .../Example.Random/AsyncSpecifications.cs | 45 ++++++++ .../AsyncSpecificationsWithExceptions.cs | 28 +++++ .../Runner/AsyncDelegateRunnerSpecs.cs | 72 ++++++++++++ .../Machine.Specifications.csproj | 1 + .../Model/Specification.cs | 5 +- .../Runner/Impl/AsyncManualResetEvent.cs | 34 ++++++ .../Impl/AsyncSynchronizationContext.cs | 104 ++++++++++++++++++ .../Runner/Impl/DelegateRunner.cs | 47 ++++++++ .../Utility/RandomExtensionMethods.cs | 12 +- 9 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 src/Examples/Example.Random/AsyncSpecifications.cs create mode 100644 src/Examples/Example.Random/AsyncSpecificationsWithExceptions.cs create mode 100644 src/Machine.Specifications.Specs/Runner/AsyncDelegateRunnerSpecs.cs create mode 100644 src/Machine.Specifications/Runner/Impl/AsyncManualResetEvent.cs create mode 100644 src/Machine.Specifications/Runner/Impl/AsyncSynchronizationContext.cs create mode 100644 src/Machine.Specifications/Runner/Impl/DelegateRunner.cs diff --git a/src/Examples/Example.Random/AsyncSpecifications.cs b/src/Examples/Example.Random/AsyncSpecifications.cs new file mode 100644 index 000000000..6f2db2db6 --- /dev/null +++ b/src/Examples/Example.Random/AsyncSpecifications.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Machine.Specifications; + +namespace Example.Random +{ + public class AsyncSpecifications + { + public static bool establish_invoked; + + public static bool because_invoked; + + public static bool async_it_invoked; + + public static bool sync_it_invoked; + + public static bool cleanup_invoked; + + Establish context = async () => + { + establish_invoked = true; + await Task.Delay(10); + }; + + Because of = async () => + { + because_invoked = true; + await Task.Delay(10); + }; + + It should_invoke_sync = () => + sync_it_invoked = true; + + It should_invoke_async = async () => + { + async_it_invoked = true; + await Task.Delay(10); + }; + + Cleanup after = async () => + { + cleanup_invoked = true; + await Task.Delay(10); + }; + } +} diff --git a/src/Examples/Example.Random/AsyncSpecificationsWithExceptions.cs b/src/Examples/Example.Random/AsyncSpecificationsWithExceptions.cs new file mode 100644 index 000000000..0a0af03cc --- /dev/null +++ b/src/Examples/Example.Random/AsyncSpecificationsWithExceptions.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Machine.Specifications; + +namespace Example.Random +{ + public class AsyncSpecificationsWithExceptions + { + Because of = async () => + { + await Task.Delay(10); + + throw new InvalidOperationException("something went wrong"); + }; + + It should_invoke_sync = () => + { + throw new InvalidOperationException("something went wrong"); + }; + + It should_invoke_async = async () => + { + await Task.Delay(10); + + throw new InvalidOperationException("something went wrong"); + }; + } +} diff --git a/src/Machine.Specifications.Specs/Runner/AsyncDelegateRunnerSpecs.cs b/src/Machine.Specifications.Specs/Runner/AsyncDelegateRunnerSpecs.cs new file mode 100644 index 000000000..be3abfe88 --- /dev/null +++ b/src/Machine.Specifications.Specs/Runner/AsyncDelegateRunnerSpecs.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using Example.Random; +using FluentAssertions; +using Machine.Specifications.Factories; +using Machine.Specifications.Runner; +using Machine.Specifications.Runner.Impl; + +namespace Machine.Specifications.Specs.Runner +{ + [Subject("Async Delegate Runner")] + public class when_running_async_specifications : RunnerSpecs + { + Establish context = () => + { + AsyncSpecifications.establish_invoked = false; + AsyncSpecifications.because_invoked = false; + AsyncSpecifications.async_it_invoked = false; + AsyncSpecifications.sync_it_invoked = false; + AsyncSpecifications.cleanup_invoked = false; + }; + + Because of = () => + Run(); + + It should_call_establish = () => + AsyncSpecifications.establish_invoked.Should().BeTrue(); + + It should_call_because = () => + AsyncSpecifications.because_invoked.Should().BeTrue(); + + It should_call_async_spec = () => + AsyncSpecifications.async_it_invoked.Should().BeTrue(); + + It should_call_sync_spec = () => + AsyncSpecifications.sync_it_invoked.Should().BeTrue(); + + It should_call_cleanup = () => + AsyncSpecifications.cleanup_invoked.Should().BeTrue(); + } + + [Subject("Async Delegate Runner")] + public class when_running_async_specifications_with_exceptions : RunnerSpecs + { + static ContextFactory factory; + + static Result[] results; + + Establish context = () => + factory = new ContextFactory(); + + Because of = () => + { + var context = factory.CreateContextFrom(Activator.CreateInstance()); + + results = ContextRunnerFactory + .GetContextRunnerFor(context) + .Run(context, + new RunListenerBase(), + RunOptions.Default, + Array.Empty(), + Array.Empty()) + .ToArray(); + }; + + It should_run_two_specs = () => + results.Length.Should().Be(2); + + It should_have_failures = () => + results.Should().Match(x => x.All(y => !y.Passed)); + } +} diff --git a/src/Machine.Specifications/Machine.Specifications.csproj b/src/Machine.Specifications/Machine.Specifications.csproj index 92a5b7833..f008fee30 100644 --- a/src/Machine.Specifications/Machine.Specifications.csproj +++ b/src/Machine.Specifications/Machine.Specifications.csproj @@ -46,6 +46,7 @@ + diff --git a/src/Machine.Specifications/Model/Specification.cs b/src/Machine.Specifications/Model/Specification.cs index 9ae52df3d..943f8b144 100644 --- a/src/Machine.Specifications/Model/Specification.cs +++ b/src/Machine.Specifications/Model/Specification.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Text; +using Machine.Specifications.Utility; using Machine.Specifications.Utility.Internal; namespace Machine.Specifications.Model @@ -80,7 +81,7 @@ public bool IsExecutable protected virtual void InvokeSpecificationField() { - _it.DynamicInvoke(); + _it.InvokeAsync(); } } -} \ No newline at end of file +} diff --git a/src/Machine.Specifications/Runner/Impl/AsyncManualResetEvent.cs b/src/Machine.Specifications/Runner/Impl/AsyncManualResetEvent.cs new file mode 100644 index 000000000..ff1f83b87 --- /dev/null +++ b/src/Machine.Specifications/Runner/Impl/AsyncManualResetEvent.cs @@ -0,0 +1,34 @@ +#if !NET35 +using System.Threading.Tasks; + +namespace Machine.Specifications.Runner.Impl +{ + internal class AsyncManualResetEvent + { + private volatile TaskCompletionSource source = new TaskCompletionSource(); + + public AsyncManualResetEvent() + { + source.TrySetResult(true); + } + + public void Reset() + { + if (source.Task.IsCompleted) + { + source = new TaskCompletionSource(); + } + } + + public void Set() + { + source.TrySetResult(true); + } + + public void Wait() + { + source.Task.Wait(); + } + } +} +#endif diff --git a/src/Machine.Specifications/Runner/Impl/AsyncSynchronizationContext.cs b/src/Machine.Specifications/Runner/Impl/AsyncSynchronizationContext.cs new file mode 100644 index 000000000..994aeab45 --- /dev/null +++ b/src/Machine.Specifications/Runner/Impl/AsyncSynchronizationContext.cs @@ -0,0 +1,104 @@ +#if !NET35 +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Machine.Specifications.Runner.Impl +{ + internal class AsyncSynchronizationContext : SynchronizationContext + { + private readonly SynchronizationContext inner; + + private readonly AsyncManualResetEvent events = new AsyncManualResetEvent(); + + private int callCount; + + private Exception exception; + + public AsyncSynchronizationContext(SynchronizationContext inner) + { + this.inner = inner; + } + + private void Execute(SendOrPostCallback callback, object state) + { + try + { + callback(state); + } + catch (Exception ex) + { + exception = ex; + } + finally + { + OperationCompleted(); + } + } + + public override void OperationCompleted() + { + var count = Interlocked.Decrement(ref callCount); + + if (count == 0) + { + events.Set(); + } + } + + public override void OperationStarted() + { + Interlocked.Increment(ref callCount); + + events.Reset(); + } + + public override void Post(SendOrPostCallback d, object state) + { + OperationStarted(); + + try + { + if (inner == null) + { + ThreadPool.QueueUserWorkItem(_ => Execute(d, state)); + } + else + { + inner.Post(_ => Execute(d, state), null); + } + } + catch + { + // ignored + } + } + + public override void Send(SendOrPostCallback d, object state) + { + try + { + if (inner == null) + { + d(state); + } + else + { + inner.Send(d, state); + } + } + catch (Exception ex) + { + exception = ex; + } + } + + public Exception WaitAsync() + { + events.Wait(); + + return exception; + } + } +} +#endif diff --git a/src/Machine.Specifications/Runner/Impl/DelegateRunner.cs b/src/Machine.Specifications/Runner/Impl/DelegateRunner.cs new file mode 100644 index 000000000..2b5c8a062 --- /dev/null +++ b/src/Machine.Specifications/Runner/Impl/DelegateRunner.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading; + +namespace Machine.Specifications.Runner.Impl +{ + internal class DelegateRunner + { + private readonly Delegate target; + + private readonly object[] args; + + public DelegateRunner(Delegate target, params object[] args) + { + this.target = target; + this.args = args; + } + + public void Execute() + { +#if NET35 + target.DynamicInvoke(args); +#else + var currentContext = SynchronizationContext.Current; + + var context = new AsyncSynchronizationContext(currentContext); + + SynchronizationContext.SetSynchronizationContext(context); + + try + { + target.DynamicInvoke(args); + + var exception = context.WaitAsync(); + + if (exception != null) + { + throw exception; + } + } + finally + { + SynchronizationContext.SetSynchronizationContext(currentContext); + } +#endif + } + } +} diff --git a/src/Machine.Specifications/Utility/RandomExtensionMethods.cs b/src/Machine.Specifications/Utility/RandomExtensionMethods.cs index 52f01f938..b1870583d 100644 --- a/src/Machine.Specifications/Utility/RandomExtensionMethods.cs +++ b/src/Machine.Specifications/Utility/RandomExtensionMethods.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; - +using Machine.Specifications.Runner.Impl; using Machine.Specifications.Sdk; namespace Machine.Specifications.Utility @@ -19,7 +19,13 @@ public static void Each(this IEnumerable enumerable, Action action) internal static void InvokeAll(this IEnumerable contextActions, params object[] args) { - contextActions.AllNonNull().Select(x => () => x.DynamicInvoke(args)).InvokeAll(); + contextActions.AllNonNull().Select(x => () => x.InvokeAsync(args)).InvokeAll(); + } + + internal static void InvokeAsync(this Delegate target, params object[] args) + { + var runner = new DelegateRunner(target, args); + runner.Execute(); } static IEnumerable AllNonNull(this IEnumerable elements) where T : class @@ -77,4 +83,4 @@ internal static AttributeFullName GetCustomDelegateAttributeFullName(this Type t return null; } } -} \ No newline at end of file +}