Skip to content

Commit

Permalink
Support for async/await specifications
Browse files Browse the repository at this point in the history
  • Loading branch information
robertcoltheart committed Jan 26, 2021
1 parent f13ec7d commit 5ed7261
Show file tree
Hide file tree
Showing 9 changed files with 343 additions and 5 deletions.
45 changes: 45 additions & 0 deletions src/Examples/Example.Random/AsyncSpecifications.cs
Original file line number Diff line number Diff line change
@@ -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);
};
}
}
28 changes: 28 additions & 0 deletions src/Examples/Example.Random/AsyncSpecificationsWithExceptions.cs
Original file line number Diff line number Diff line change
@@ -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");
};
}
}
Original file line number Diff line number Diff line change
@@ -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<AsyncSpecifications>();

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<AsyncSpecificationsWithExceptions>());
results = ContextRunnerFactory
.GetContextRunnerFor(context)
.Run(context,
new RunListenerBase(),
RunOptions.Default,
Array.Empty<ICleanupAfterEveryContextInAssembly>(),
Array.Empty<ISupplementSpecificationResults>())
.ToArray();
};

It should_run_two_specs = () =>
results.Length.Should().Be(2);

It should_have_failures = () =>
results.Should().Match(x => x.All(y => !y.Passed));
}
}
1 change: 1 addition & 0 deletions src/Machine.Specifications/Machine.Specifications.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<PackageReference Include="System.Reflection" Version="4.1.0" />
<PackageReference Include="System.Reflection.Extensions" Version="4.0.1" />
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.1.0" />
<PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" />
</ItemGroup>

<PropertyGroup Condition=" '$(TargetFramework)' == 'net35' ">
Expand Down
5 changes: 3 additions & 2 deletions src/Machine.Specifications/Model/Specification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,7 +81,7 @@ public bool IsExecutable

protected virtual void InvokeSpecificationField()
{
_it.DynamicInvoke();
_it.InvokeAsync();
}
}
}
}
34 changes: 34 additions & 0 deletions src/Machine.Specifications/Runner/Impl/AsyncManualResetEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#if !NET35
using System.Threading.Tasks;

namespace Machine.Specifications.Runner.Impl
{
internal class AsyncManualResetEvent
{
private volatile TaskCompletionSource<bool> source = new TaskCompletionSource<bool>();

public AsyncManualResetEvent()
{
source.TrySetResult(true);
}

public void Reset()
{
if (source.Task.IsCompleted)
{
source = new TaskCompletionSource<bool>();
}
}

public void Set()
{
source.TrySetResult(true);
}

public void Wait()
{
source.Task.Wait();
}
}
}
#endif
104 changes: 104 additions & 0 deletions src/Machine.Specifications/Runner/Impl/AsyncSynchronizationContext.cs
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions src/Machine.Specifications/Runner/Impl/DelegateRunner.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
12 changes: 9 additions & 3 deletions src/Machine.Specifications/Utility/RandomExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,7 +19,13 @@ public static void Each<T>(this IEnumerable<T> enumerable, Action<T> action)

internal static void InvokeAll(this IEnumerable<Delegate> contextActions, params object[] args)
{
contextActions.AllNonNull().Select<Delegate, Action>(x => () => x.DynamicInvoke(args)).InvokeAll();
contextActions.AllNonNull().Select<Delegate, Action>(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<T> AllNonNull<T>(this IEnumerable<T> elements) where T : class
Expand Down Expand Up @@ -77,4 +83,4 @@ internal static AttributeFullName GetCustomDelegateAttributeFullName(this Type t
return null;
}
}
}
}

0 comments on commit 5ed7261

Please sign in to comment.