Skip to content

Commit 5ed7261

Browse files
Support for async/await specifications
1 parent f13ec7d commit 5ed7261

File tree

9 files changed

+343
-5
lines changed

9 files changed

+343
-5
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Threading.Tasks;
2+
using Machine.Specifications;
3+
4+
namespace Example.Random
5+
{
6+
public class AsyncSpecifications
7+
{
8+
public static bool establish_invoked;
9+
10+
public static bool because_invoked;
11+
12+
public static bool async_it_invoked;
13+
14+
public static bool sync_it_invoked;
15+
16+
public static bool cleanup_invoked;
17+
18+
Establish context = async () =>
19+
{
20+
establish_invoked = true;
21+
await Task.Delay(10);
22+
};
23+
24+
Because of = async () =>
25+
{
26+
because_invoked = true;
27+
await Task.Delay(10);
28+
};
29+
30+
It should_invoke_sync = () =>
31+
sync_it_invoked = true;
32+
33+
It should_invoke_async = async () =>
34+
{
35+
async_it_invoked = true;
36+
await Task.Delay(10);
37+
};
38+
39+
Cleanup after = async () =>
40+
{
41+
cleanup_invoked = true;
42+
await Task.Delay(10);
43+
};
44+
}
45+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Machine.Specifications;
4+
5+
namespace Example.Random
6+
{
7+
public class AsyncSpecificationsWithExceptions
8+
{
9+
Because of = async () =>
10+
{
11+
await Task.Delay(10);
12+
13+
throw new InvalidOperationException("something went wrong");
14+
};
15+
16+
It should_invoke_sync = () =>
17+
{
18+
throw new InvalidOperationException("something went wrong");
19+
};
20+
21+
It should_invoke_async = async () =>
22+
{
23+
await Task.Delay(10);
24+
25+
throw new InvalidOperationException("something went wrong");
26+
};
27+
}
28+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System;
2+
using System.Linq;
3+
using Example.Random;
4+
using FluentAssertions;
5+
using Machine.Specifications.Factories;
6+
using Machine.Specifications.Runner;
7+
using Machine.Specifications.Runner.Impl;
8+
9+
namespace Machine.Specifications.Specs.Runner
10+
{
11+
[Subject("Async Delegate Runner")]
12+
public class when_running_async_specifications : RunnerSpecs
13+
{
14+
Establish context = () =>
15+
{
16+
AsyncSpecifications.establish_invoked = false;
17+
AsyncSpecifications.because_invoked = false;
18+
AsyncSpecifications.async_it_invoked = false;
19+
AsyncSpecifications.sync_it_invoked = false;
20+
AsyncSpecifications.cleanup_invoked = false;
21+
};
22+
23+
Because of = () =>
24+
Run<AsyncSpecifications>();
25+
26+
It should_call_establish = () =>
27+
AsyncSpecifications.establish_invoked.Should().BeTrue();
28+
29+
It should_call_because = () =>
30+
AsyncSpecifications.because_invoked.Should().BeTrue();
31+
32+
It should_call_async_spec = () =>
33+
AsyncSpecifications.async_it_invoked.Should().BeTrue();
34+
35+
It should_call_sync_spec = () =>
36+
AsyncSpecifications.sync_it_invoked.Should().BeTrue();
37+
38+
It should_call_cleanup = () =>
39+
AsyncSpecifications.cleanup_invoked.Should().BeTrue();
40+
}
41+
42+
[Subject("Async Delegate Runner")]
43+
public class when_running_async_specifications_with_exceptions : RunnerSpecs
44+
{
45+
static ContextFactory factory;
46+
47+
static Result[] results;
48+
49+
Establish context = () =>
50+
factory = new ContextFactory();
51+
52+
Because of = () =>
53+
{
54+
var context = factory.CreateContextFrom(Activator.CreateInstance<AsyncSpecificationsWithExceptions>());
55+
56+
results = ContextRunnerFactory
57+
.GetContextRunnerFor(context)
58+
.Run(context,
59+
new RunListenerBase(),
60+
RunOptions.Default,
61+
Array.Empty<ICleanupAfterEveryContextInAssembly>(),
62+
Array.Empty<ISupplementSpecificationResults>())
63+
.ToArray();
64+
};
65+
66+
It should_run_two_specs = () =>
67+
results.Length.Should().Be(2);
68+
69+
It should_have_failures = () =>
70+
results.Should().Match(x => x.All(y => !y.Passed));
71+
}
72+
}

src/Machine.Specifications/Machine.Specifications.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<PackageReference Include="System.Reflection" Version="4.1.0" />
4747
<PackageReference Include="System.Reflection.Extensions" Version="4.0.1" />
4848
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.1.0" />
49+
<PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" />
4950
</ItemGroup>
5051

5152
<PropertyGroup Condition=" '$(TargetFramework)' == 'net35' ">

src/Machine.Specifications/Model/Specification.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Reflection;
55
using System.Text;
6+
using Machine.Specifications.Utility;
67
using Machine.Specifications.Utility.Internal;
78

89
namespace Machine.Specifications.Model
@@ -80,7 +81,7 @@ public bool IsExecutable
8081

8182
protected virtual void InvokeSpecificationField()
8283
{
83-
_it.DynamicInvoke();
84+
_it.InvokeAsync();
8485
}
8586
}
86-
}
87+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#if !NET35
2+
using System.Threading.Tasks;
3+
4+
namespace Machine.Specifications.Runner.Impl
5+
{
6+
internal class AsyncManualResetEvent
7+
{
8+
private volatile TaskCompletionSource<bool> source = new TaskCompletionSource<bool>();
9+
10+
public AsyncManualResetEvent()
11+
{
12+
source.TrySetResult(true);
13+
}
14+
15+
public void Reset()
16+
{
17+
if (source.Task.IsCompleted)
18+
{
19+
source = new TaskCompletionSource<bool>();
20+
}
21+
}
22+
23+
public void Set()
24+
{
25+
source.TrySetResult(true);
26+
}
27+
28+
public void Wait()
29+
{
30+
source.Task.Wait();
31+
}
32+
}
33+
}
34+
#endif
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#if !NET35
2+
using System;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
namespace Machine.Specifications.Runner.Impl
7+
{
8+
internal class AsyncSynchronizationContext : SynchronizationContext
9+
{
10+
private readonly SynchronizationContext inner;
11+
12+
private readonly AsyncManualResetEvent events = new AsyncManualResetEvent();
13+
14+
private int callCount;
15+
16+
private Exception exception;
17+
18+
public AsyncSynchronizationContext(SynchronizationContext inner)
19+
{
20+
this.inner = inner;
21+
}
22+
23+
private void Execute(SendOrPostCallback callback, object state)
24+
{
25+
try
26+
{
27+
callback(state);
28+
}
29+
catch (Exception ex)
30+
{
31+
exception = ex;
32+
}
33+
finally
34+
{
35+
OperationCompleted();
36+
}
37+
}
38+
39+
public override void OperationCompleted()
40+
{
41+
var count = Interlocked.Decrement(ref callCount);
42+
43+
if (count == 0)
44+
{
45+
events.Set();
46+
}
47+
}
48+
49+
public override void OperationStarted()
50+
{
51+
Interlocked.Increment(ref callCount);
52+
53+
events.Reset();
54+
}
55+
56+
public override void Post(SendOrPostCallback d, object state)
57+
{
58+
OperationStarted();
59+
60+
try
61+
{
62+
if (inner == null)
63+
{
64+
ThreadPool.QueueUserWorkItem(_ => Execute(d, state));
65+
}
66+
else
67+
{
68+
inner.Post(_ => Execute(d, state), null);
69+
}
70+
}
71+
catch
72+
{
73+
// ignored
74+
}
75+
}
76+
77+
public override void Send(SendOrPostCallback d, object state)
78+
{
79+
try
80+
{
81+
if (inner == null)
82+
{
83+
d(state);
84+
}
85+
else
86+
{
87+
inner.Send(d, state);
88+
}
89+
}
90+
catch (Exception ex)
91+
{
92+
exception = ex;
93+
}
94+
}
95+
96+
public Exception WaitAsync()
97+
{
98+
events.Wait();
99+
100+
return exception;
101+
}
102+
}
103+
}
104+
#endif
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Threading;
3+
4+
namespace Machine.Specifications.Runner.Impl
5+
{
6+
internal class DelegateRunner
7+
{
8+
private readonly Delegate target;
9+
10+
private readonly object[] args;
11+
12+
public DelegateRunner(Delegate target, params object[] args)
13+
{
14+
this.target = target;
15+
this.args = args;
16+
}
17+
18+
public void Execute()
19+
{
20+
#if NET35
21+
target.DynamicInvoke(args);
22+
#else
23+
var currentContext = SynchronizationContext.Current;
24+
25+
var context = new AsyncSynchronizationContext(currentContext);
26+
27+
SynchronizationContext.SetSynchronizationContext(context);
28+
29+
try
30+
{
31+
target.DynamicInvoke(args);
32+
33+
var exception = context.WaitAsync();
34+
35+
if (exception != null)
36+
{
37+
throw exception;
38+
}
39+
}
40+
finally
41+
{
42+
SynchronizationContext.SetSynchronizationContext(currentContext);
43+
}
44+
#endif
45+
}
46+
}
47+
}

src/Machine.Specifications/Utility/RandomExtensionMethods.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Reflection;
5-
5+
using Machine.Specifications.Runner.Impl;
66
using Machine.Specifications.Sdk;
77

88
namespace Machine.Specifications.Utility
@@ -19,7 +19,13 @@ public static void Each<T>(this IEnumerable<T> enumerable, Action<T> action)
1919

2020
internal static void InvokeAll(this IEnumerable<Delegate> contextActions, params object[] args)
2121
{
22-
contextActions.AllNonNull().Select<Delegate, Action>(x => () => x.DynamicInvoke(args)).InvokeAll();
22+
contextActions.AllNonNull().Select<Delegate, Action>(x => () => x.InvokeAsync(args)).InvokeAll();
23+
}
24+
25+
internal static void InvokeAsync(this Delegate target, params object[] args)
26+
{
27+
var runner = new DelegateRunner(target, args);
28+
runner.Execute();
2329
}
2430

2531
static IEnumerable<T> AllNonNull<T>(this IEnumerable<T> elements) where T : class
@@ -77,4 +83,4 @@ internal static AttributeFullName GetCustomDelegateAttributeFullName(this Type t
7783
return null;
7884
}
7985
}
80-
}
86+
}

0 commit comments

Comments
 (0)