Skip to content

Commit 92be7fc

Browse files
authored
Added handling of fixtures that implements IAsyncLifetime, IAsyncDisposable (#6)
Added parallelization of initialization and destruction of fixtures.
1 parent a4ce050 commit 92be7fc

6 files changed

+315
-105
lines changed

src/AssemblyFixture.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>net452;netstandard2.1;netstandard2.0;net5.0;net6.0</TargetFrameworks>
5+
<LangVersion>latest</LangVersion>
56
<Version>0.1.0</Version>
67
<Authors>kzu, J.D. Cain</Authors>
78
<Product />

src/IAssemblyFixture.cs

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
using Xunit.Abstractions;
22

3-
namespace Xunit.Extensions.AssemblyFixture
4-
{
5-
/// <summary>
6-
/// Used to decorate xUnit.net test classes and collections to indicate a test which has
7-
/// per-assembly fixture data. An instance of the fixture data is initialized just before
8-
/// the first test in the assembly is run, and if it implements IDisposable, is disposed
9-
/// after the last test in the assembly is run. To gain access to the fixture data from
10-
/// inside the test, a constructor argument should be added to the test class which
11-
/// exactly matches the <typeparamref name="TFixture"/>.
12-
/// </summary>
13-
/// <typeparam name="TFixture">The type of the fixture.</typeparam>
14-
public interface IAssemblyFixture<TFixture> where TFixture : class { }
15-
}
3+
namespace Xunit.Extensions.AssemblyFixture
4+
{
5+
/// <summary>
6+
/// Used to decorate xUnit.net test classes and collections to indicate a test which has
7+
/// per-assembly fixture data. An instance of the fixture data is initialized just before
8+
/// the first test in the assembly is run, and if it implements IDisposable, is disposed
9+
/// after the last test in the assembly is run. To gain access to the fixture data from
10+
/// inside the test, a constructor argument should be added to the test class which
11+
/// exactly matches the <typeparamref name="TFixture"/>.
12+
/// </summary>
13+
/// <typeparam name="TFixture">The type of the fixture.</typeparam>
14+
public interface IAssemblyFixture<TFixture> where TFixture : class { }
15+
}

src/TestAssemblyRunner.cs

+107-32
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,115 @@
33
using System.Linq;
44
using System.Threading;
55
using System.Threading.Tasks;
6+
67
using Xunit.Abstractions;
78
using Xunit.Sdk;
9+
using System.Reflection;
10+
using System.Runtime.CompilerServices;
811

912
namespace Xunit.Extensions.AssemblyFixture
1013
{
11-
class TestAssemblyRunner : XunitTestAssemblyRunner
12-
{
13-
readonly Dictionary<Type, object> assemblyFixtureMappings = new Dictionary<Type, object>();
14-
15-
public TestAssemblyRunner(ITestAssembly testAssembly,
16-
IEnumerable<IXunitTestCase> testCases,
17-
IMessageSink diagnosticMessageSink,
18-
IMessageSink executionMessageSink,
19-
ITestFrameworkExecutionOptions executionOptions)
20-
: base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions)
21-
{
22-
}
23-
24-
protected override Task BeforeTestAssemblyFinishedAsync()
25-
{
26-
// Make sure we clean up everybody who is disposable, and use Aggregator.Run to isolate Dispose failures
27-
foreach (var disposable in assemblyFixtureMappings.Values.OfType<IDisposable>())
28-
Aggregator.Run(disposable.Dispose);
29-
30-
return base.BeforeTestAssemblyFinishedAsync();
31-
}
32-
33-
34-
protected override Task<RunSummary> RunTestCollectionAsync(IMessageBus messageBus,
35-
ITestCollection testCollection,
36-
IEnumerable<IXunitTestCase> testCases,
37-
CancellationTokenSource cancellationTokenSource)
38-
{
39-
return new TestCollectionRunner(assemblyFixtureMappings, testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();
40-
}
41-
}
42-
}
14+
class TestAssemblyRunner : XunitTestAssemblyRunner
15+
{
16+
#if NET5_0_OR_GREATER
17+
private readonly Dictionary<Type, object> _assemblyFixtureMappings = new();
18+
#else
19+
private readonly Dictionary<Type, object> _assemblyFixtureMappings = new Dictionary<Type, object>();
20+
21+
#endif
22+
23+
public TestAssemblyRunner(
24+
ITestAssembly testAssembly,
25+
IEnumerable<IXunitTestCase> testCases,
26+
IMessageSink diagnosticMessageSink,
27+
IMessageSink executionMessageSink,
28+
ITestFrameworkExecutionOptions executionOptions)
29+
: base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions)
30+
{ }
31+
32+
///<inheritdoc/>
33+
protected override async Task AfterTestAssemblyStartingAsync()
34+
{
35+
// Let everything initialize
36+
await base.AfterTestAssemblyStartingAsync().ConfigureAwait(false);
37+
38+
// Go find all the AssemblyFixtureAttributes adorned on the test assembly
39+
await Aggregator.RunAsync(async () =>
40+
{
41+
ISet<Type> assemblyFixtures = new HashSet<Type>(((IReflectionAssemblyInfo)TestAssembly.Assembly).Assembly
42+
.GetTypes()
43+
.Select(type => type.GetInterfaces())
44+
.SelectMany(x => x)
45+
.Where(@interface => @interface.IsAssignableToGenericType(typeof(IAssemblyFixture<>)))
46+
.ToArray());
47+
48+
// Instantiate all the fixtures
49+
foreach (Type fixtureAttribute in assemblyFixtures)
50+
{
51+
Type fixtureType = fixtureAttribute.GetGenericArguments()[0];
52+
var hasConstructorWithMessageSink = fixtureType.GetConstructor(new[] { typeof(IMessageSink) }) != null;
53+
_assemblyFixtureMappings[fixtureType] = hasConstructorWithMessageSink
54+
? Activator.CreateInstance(fixtureType, ExecutionMessageSink)
55+
: Activator.CreateInstance(fixtureType);
56+
}
57+
58+
// Initialize IAsyncLifetime fixtures
59+
foreach (IAsyncLifetime asyncLifetime in _assemblyFixtureMappings.Values.OfType<IAsyncLifetime>())
60+
{
61+
await Aggregator.RunAsync(async () => await asyncLifetime.InitializeAsync().ConfigureAwait(false)).ConfigureAwait(false);
62+
}
63+
}).ConfigureAwait(false);
64+
}
65+
66+
protected override async Task BeforeTestAssemblyFinishedAsync()
67+
{
68+
// Make sure we clean up everybody who is disposable, and use Aggregator.Run to isolate Dispose failures
69+
Parallel.ForEach(_assemblyFixtureMappings.Values.OfType<IDisposable>(),
70+
disposable => Aggregator.Run(disposable.Dispose));
71+
72+
73+
#if NETSTANDARD2_1 || NET5_0_OR_GREATER
74+
75+
#if NET6_0_OR_GREATER
76+
await Parallel.ForEachAsync(_assemblyFixtureMappings.Values.OfType<IAsyncDisposable>(),
77+
async (disposable, _) => await Aggregator.RunAsync(async () => await disposable.DisposeAsync().ConfigureAwait(false))
78+
.ConfigureAwait(false));
79+
#else
80+
Parallel.ForEach(_assemblyFixtureMappings.Values.OfType<IAsyncDisposable>(),
81+
async (disposable, _) => await Aggregator.RunAsync(async () => await disposable.DisposeAsync().ConfigureAwait(false)));
82+
83+
#endif
84+
85+
#endif
86+
87+
88+
#if NET6_0_OR_GREATER
89+
await Parallel.ForEachAsync(_assemblyFixtureMappings.Values.OfType<IAsyncLifetime>(),
90+
async (disposable, _) => await Aggregator.RunAsync(async () => await disposable.DisposeAsync().ConfigureAwait(false))
91+
.ConfigureAwait(false));
92+
#else
93+
Parallel.ForEach(_assemblyFixtureMappings.Values.OfType<IAsyncLifetime>(),
94+
async (disposable, _) => await Aggregator.RunAsync(async () => await disposable.DisposeAsync().ConfigureAwait(false)));
95+
96+
#endif
97+
98+
await base.BeforeTestAssemblyFinishedAsync().ConfigureAwait(false);
99+
}
100+
101+
protected override async Task<RunSummary> RunTestCollectionAsync(
102+
IMessageBus messageBus,
103+
ITestCollection testCollection,
104+
IEnumerable<IXunitTestCase> testCases,
105+
CancellationTokenSource cancellationTokenSource)
106+
=> await new TestCollectionRunner(
107+
_assemblyFixtureMappings,
108+
testCollection,
109+
testCases,
110+
DiagnosticMessageSink,
111+
messageBus,
112+
TestCaseOrderer,
113+
new ExceptionAggregator(Aggregator),
114+
cancellationTokenSource)
115+
.RunAsync().ConfigureAwait(false);
116+
}
117+
}

src/TestCollectionRunner.cs

+60-60
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,58 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Reflection;
5-
using System.Threading;
6-
using System.Threading.Tasks;
7-
using Xunit.Abstractions;
8-
using Xunit.Sdk;
9-
10-
namespace Xunit.Extensions.AssemblyFixture
11-
{
12-
class TestCollectionRunner : XunitTestCollectionRunner
13-
{
14-
readonly Dictionary<Type, object> assemblyFixtureMappings;
15-
readonly IMessageSink diagnosticMessageSink;
16-
17-
public TestCollectionRunner(Dictionary<Type, object> assemblyFixtureMappings,
18-
ITestCollection testCollection,
19-
IEnumerable<IXunitTestCase> testCases,
20-
IMessageSink diagnosticMessageSink,
21-
IMessageBus messageBus,
22-
ITestCaseOrderer testCaseOrderer,
23-
ExceptionAggregator aggregator,
24-
CancellationTokenSource cancellationTokenSource)
25-
: base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
26-
{
27-
this.assemblyFixtureMappings = assemblyFixtureMappings;
28-
this.diagnosticMessageSink = diagnosticMessageSink;
29-
}
30-
31-
protected override Task<RunSummary> RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable<IXunitTestCase> testCases)
32-
{
33-
foreach (var fixtureType in @class.Type.GetTypeInfo().ImplementedInterfaces
34-
.Where(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IAssemblyFixture<>))
35-
.Select(i => i.GetTypeInfo().GenericTypeArguments.Single())
36-
// First pass at filtering out before locking
37-
.Where(i => !assemblyFixtureMappings.ContainsKey(i)))
38-
{
39-
// ConcurrentDictionary's GetOrAdd does not lock around the value factory call, so we need
40-
// to do it ourselves.
41-
lock (assemblyFixtureMappings)
42-
if (!assemblyFixtureMappings.ContainsKey(fixtureType))
43-
Aggregator.Run(() => assemblyFixtureMappings.Add(fixtureType, CreateAssemblyFixtureInstance(fixtureType)));
44-
}
45-
46-
// Don't want to use .Concat + .ToDictionary because of the possibility of overriding types,
47-
// so instead we'll just let collection fixtures override assembly fixtures.
48-
var combinedFixtures = new Dictionary<Type, object>(assemblyFixtureMappings);
49-
foreach (var kvp in CollectionFixtureMappings)
50-
combinedFixtures[kvp.Key] = kvp.Value;
51-
52-
// We've done everything we need, so let the built-in types do the rest of the heavy lifting
53-
return new XunitTestClassRunner(testClass, @class, testCases, diagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, combinedFixtures).RunAsync();
54-
}
55-
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Xunit.Abstractions;
8+
using Xunit.Sdk;
9+
10+
namespace Xunit.Extensions.AssemblyFixture
11+
{
12+
class TestCollectionRunner : XunitTestCollectionRunner
13+
{
14+
readonly Dictionary<Type, object> assemblyFixtureMappings;
15+
readonly IMessageSink diagnosticMessageSink;
16+
17+
public TestCollectionRunner(Dictionary<Type, object> assemblyFixtureMappings,
18+
ITestCollection testCollection,
19+
IEnumerable<IXunitTestCase> testCases,
20+
IMessageSink diagnosticMessageSink,
21+
IMessageBus messageBus,
22+
ITestCaseOrderer testCaseOrderer,
23+
ExceptionAggregator aggregator,
24+
CancellationTokenSource cancellationTokenSource)
25+
: base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
26+
{
27+
this.assemblyFixtureMappings = assemblyFixtureMappings;
28+
this.diagnosticMessageSink = diagnosticMessageSink;
29+
}
30+
31+
protected override Task<RunSummary> RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable<IXunitTestCase> testCases)
32+
{
33+
foreach (var fixtureType in @class.Type.GetTypeInfo().ImplementedInterfaces
34+
.Where(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IAssemblyFixture<>))
35+
.Select(i => i.GetTypeInfo().GenericTypeArguments.Single())
36+
// First pass at filtering out before locking
37+
.Where(i => !assemblyFixtureMappings.ContainsKey(i)))
38+
{
39+
// ConcurrentDictionary's GetOrAdd does not lock around the value factory call, so we need
40+
// to do it ourselves.
41+
lock (assemblyFixtureMappings)
42+
if (!assemblyFixtureMappings.ContainsKey(fixtureType))
43+
Aggregator.Run(() => assemblyFixtureMappings.Add(fixtureType, CreateAssemblyFixtureInstance(fixtureType)));
44+
}
45+
46+
// Don't want to use .Concat + .ToDictionary because of the possibility of overriding types,
47+
// so instead we'll just let collection fixtures override assembly fixtures.
48+
var combinedFixtures = new Dictionary<Type, object>(assemblyFixtureMappings);
49+
foreach (var kvp in CollectionFixtureMappings)
50+
combinedFixtures[kvp.Key] = kvp.Value;
51+
52+
// We've done everything we need, so let the built-in types do the rest of the heavy lifting
53+
return new XunitTestClassRunner(testClass, @class, testCases, diagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, combinedFixtures).RunAsync();
54+
}
55+
5656
private object CreateAssemblyFixtureInstance(Type fixtureType) {
5757
var constructors = fixtureType.GetConstructors();
5858

@@ -63,8 +63,8 @@ private object CreateAssemblyFixtureInstance(Type fixtureType) {
6363
if (constructors[0].GetParameters().Length == 0)
6464
return Activator.CreateInstance(fixtureType);
6565

66-
return Activator.CreateInstance(fixtureType, diagnosticMessageSink);
67-
}
68-
69-
}
70-
}
66+
return Activator.CreateInstance(fixtureType, diagnosticMessageSink);
67+
}
68+
69+
}
70+
}

src/TypeExtensions.cs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System.Linq;
2+
using System.Reflection;
3+
using System.Runtime.CompilerServices;
4+
5+
namespace System
6+
{
7+
/// <summary>
8+
/// Extension methods for <see cref="Type"/>.
9+
/// </summary>
10+
public static class TypeExtensions
11+
{
12+
/// <summary>
13+
/// Checks if an instance of <paramref name="givenType"/> can be assigned to a type <paramref name="genericType"/>.
14+
/// </summary>
15+
/// <param name="givenType">The type under test</param>
16+
/// <param name="genericType">The targeted type</param>
17+
/// <returns><c>true</c> if <paramref name="genericType"/> is an ancestor of <paramref name="givenType"/> and <c>false</c> otherwise.</returns>
18+
public static bool IsAssignableToGenericType(this Type givenType, Type genericType)
19+
=> givenType is not null && genericType is not null
20+
&& (givenType == genericType || givenType.MapsToGenericTypeDefinition(genericType)
21+
|| givenType.HasInterfaceThatMapsToGenericTypeDefinition(genericType)
22+
|| givenType.GetTypeInfo().BaseType.IsAssignableToGenericType(genericType));
23+
24+
private static bool HasInterfaceThatMapsToGenericTypeDefinition(this Type givenType, Type genericType)
25+
=> givenType is not null && genericType is not null && givenType
26+
.GetTypeInfo()
27+
#if NETSTANDARD1_0 || NETSTANDARD1_1 || NETSTANDARD1_3
28+
29+
.ImplementedInterfaces
30+
#else
31+
.GetInterfaces()
32+
#endif
33+
.Where(it => it.GetTypeInfo().IsGenericType)
34+
.Any(it => it.GetGenericTypeDefinition() == genericType);
35+
36+
private static bool MapsToGenericTypeDefinition(this Type givenType, Type genericType)
37+
=> givenType is not null
38+
&& genericType?.GetTypeInfo().IsGenericTypeDefinition == true
39+
&& givenType.GetTypeInfo().IsGenericType
40+
&& givenType.GetGenericTypeDefinition() == genericType;
41+
42+
/// <summary>
43+
/// Tests if <paramref name="type"/> is an anonymous type
44+
/// </summary>
45+
/// <param name="type">The type under test</param>
46+
/// <returns><c>true</c>if <paramref name="type"/> is an anonymous type and <c>false</c> otherwise</returns>
47+
public static bool IsAnonymousType(this Type type)
48+
{
49+
bool hasCompilerGeneratedAttribute = type?.GetTypeInfo()?.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false)?.Any() ?? false;
50+
bool nameContainsAnonymousType = type?.FullName.Contains("AnonymousType") ?? false;
51+
52+
return hasCompilerGeneratedAttribute && nameContainsAnonymousType;
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)