Skip to content

Commit

Permalink
Added handling of fixtures that implements IAsyncLifetime, `IAsyncD…
Browse files Browse the repository at this point in the history
…isposable` (#6)

Added parallelization of initialization and destruction of fixtures.
  • Loading branch information
candoumbe authored Dec 7, 2021
1 parent a4ce050 commit 92be7fc
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 105 deletions.
1 change: 1 addition & 0 deletions src/AssemblyFixture.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFrameworks>net452;netstandard2.1;netstandard2.0;net5.0;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.0</Version>
<Authors>kzu, J.D. Cain</Authors>
<Product />
Expand Down
26 changes: 13 additions & 13 deletions src/IAssemblyFixture.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using Xunit.Abstractions;

namespace Xunit.Extensions.AssemblyFixture
{
/// <summary>
/// Used to decorate xUnit.net test classes and collections to indicate a test which has
/// per-assembly fixture data. An instance of the fixture data is initialized just before
/// the first test in the assembly is run, and if it implements IDisposable, is disposed
/// after the last test in the assembly is run. To gain access to the fixture data from
/// inside the test, a constructor argument should be added to the test class which
/// exactly matches the <typeparamref name="TFixture"/>.
/// </summary>
/// <typeparam name="TFixture">The type of the fixture.</typeparam>
public interface IAssemblyFixture<TFixture> where TFixture : class { }
}
namespace Xunit.Extensions.AssemblyFixture
{
/// <summary>
/// Used to decorate xUnit.net test classes and collections to indicate a test which has
/// per-assembly fixture data. An instance of the fixture data is initialized just before
/// the first test in the assembly is run, and if it implements IDisposable, is disposed
/// after the last test in the assembly is run. To gain access to the fixture data from
/// inside the test, a constructor argument should be added to the test class which
/// exactly matches the <typeparamref name="TFixture"/>.
/// </summary>
/// <typeparam name="TFixture">The type of the fixture.</typeparam>
public interface IAssemblyFixture<TFixture> where TFixture : class { }
}
139 changes: 107 additions & 32 deletions src/TestAssemblyRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,115 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using Xunit.Abstractions;
using Xunit.Sdk;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace Xunit.Extensions.AssemblyFixture
{
class TestAssemblyRunner : XunitTestAssemblyRunner
{
readonly Dictionary<Type, object> assemblyFixtureMappings = new Dictionary<Type, object>();

public TestAssemblyRunner(ITestAssembly testAssembly,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
: base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions)
{
}

protected override Task BeforeTestAssemblyFinishedAsync()
{
// Make sure we clean up everybody who is disposable, and use Aggregator.Run to isolate Dispose failures
foreach (var disposable in assemblyFixtureMappings.Values.OfType<IDisposable>())
Aggregator.Run(disposable.Dispose);

return base.BeforeTestAssemblyFinishedAsync();
}


protected override Task<RunSummary> RunTestCollectionAsync(IMessageBus messageBus,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
CancellationTokenSource cancellationTokenSource)
{
return new TestCollectionRunner(assemblyFixtureMappings, testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();
}
}
}
class TestAssemblyRunner : XunitTestAssemblyRunner
{
#if NET5_0_OR_GREATER
private readonly Dictionary<Type, object> _assemblyFixtureMappings = new();
#else
private readonly Dictionary<Type, object> _assemblyFixtureMappings = new Dictionary<Type, object>();

#endif

public TestAssemblyRunner(
ITestAssembly testAssembly,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
: base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions)
{ }

///<inheritdoc/>
protected override async Task AfterTestAssemblyStartingAsync()
{
// Let everything initialize
await base.AfterTestAssemblyStartingAsync().ConfigureAwait(false);

// Go find all the AssemblyFixtureAttributes adorned on the test assembly
await Aggregator.RunAsync(async () =>
{
ISet<Type> assemblyFixtures = new HashSet<Type>(((IReflectionAssemblyInfo)TestAssembly.Assembly).Assembly
.GetTypes()
.Select(type => type.GetInterfaces())
.SelectMany(x => x)
.Where(@interface => @interface.IsAssignableToGenericType(typeof(IAssemblyFixture<>)))
.ToArray());
// Instantiate all the fixtures
foreach (Type fixtureAttribute in assemblyFixtures)
{
Type fixtureType = fixtureAttribute.GetGenericArguments()[0];
var hasConstructorWithMessageSink = fixtureType.GetConstructor(new[] { typeof(IMessageSink) }) != null;
_assemblyFixtureMappings[fixtureType] = hasConstructorWithMessageSink
? Activator.CreateInstance(fixtureType, ExecutionMessageSink)
: Activator.CreateInstance(fixtureType);
}
// Initialize IAsyncLifetime fixtures
foreach (IAsyncLifetime asyncLifetime in _assemblyFixtureMappings.Values.OfType<IAsyncLifetime>())
{
await Aggregator.RunAsync(async () => await asyncLifetime.InitializeAsync().ConfigureAwait(false)).ConfigureAwait(false);
}
}).ConfigureAwait(false);
}

protected override async Task BeforeTestAssemblyFinishedAsync()
{
// Make sure we clean up everybody who is disposable, and use Aggregator.Run to isolate Dispose failures
Parallel.ForEach(_assemblyFixtureMappings.Values.OfType<IDisposable>(),
disposable => Aggregator.Run(disposable.Dispose));


#if NETSTANDARD2_1 || NET5_0_OR_GREATER

#if NET6_0_OR_GREATER
await Parallel.ForEachAsync(_assemblyFixtureMappings.Values.OfType<IAsyncDisposable>(),
async (disposable, _) => await Aggregator.RunAsync(async () => await disposable.DisposeAsync().ConfigureAwait(false))
.ConfigureAwait(false));
#else
Parallel.ForEach(_assemblyFixtureMappings.Values.OfType<IAsyncDisposable>(),
async (disposable, _) => await Aggregator.RunAsync(async () => await disposable.DisposeAsync().ConfigureAwait(false)));

#endif

#endif


#if NET6_0_OR_GREATER
await Parallel.ForEachAsync(_assemblyFixtureMappings.Values.OfType<IAsyncLifetime>(),
async (disposable, _) => await Aggregator.RunAsync(async () => await disposable.DisposeAsync().ConfigureAwait(false))
.ConfigureAwait(false));
#else
Parallel.ForEach(_assemblyFixtureMappings.Values.OfType<IAsyncLifetime>(),
async (disposable, _) => await Aggregator.RunAsync(async () => await disposable.DisposeAsync().ConfigureAwait(false)));

#endif

await base.BeforeTestAssemblyFinishedAsync().ConfigureAwait(false);
}

protected override async Task<RunSummary> RunTestCollectionAsync(
IMessageBus messageBus,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
CancellationTokenSource cancellationTokenSource)
=> await new TestCollectionRunner(
_assemblyFixtureMappings,
testCollection,
testCases,
DiagnosticMessageSink,
messageBus,
TestCaseOrderer,
new ExceptionAggregator(Aggregator),
cancellationTokenSource)
.RunAsync().ConfigureAwait(false);
}
}
120 changes: 60 additions & 60 deletions src/TestCollectionRunner.cs
Original file line number Diff line number Diff line change
@@ -1,58 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Xunit.Extensions.AssemblyFixture
{
class TestCollectionRunner : XunitTestCollectionRunner
{
readonly Dictionary<Type, object> assemblyFixtureMappings;
readonly IMessageSink diagnosticMessageSink;

public TestCollectionRunner(Dictionary<Type, object> assemblyFixtureMappings,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
ITestCaseOrderer testCaseOrderer,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
{
this.assemblyFixtureMappings = assemblyFixtureMappings;
this.diagnosticMessageSink = diagnosticMessageSink;
}

protected override Task<RunSummary> RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable<IXunitTestCase> testCases)
{
foreach (var fixtureType in @class.Type.GetTypeInfo().ImplementedInterfaces
.Where(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IAssemblyFixture<>))
.Select(i => i.GetTypeInfo().GenericTypeArguments.Single())
// First pass at filtering out before locking
.Where(i => !assemblyFixtureMappings.ContainsKey(i)))
{
// ConcurrentDictionary's GetOrAdd does not lock around the value factory call, so we need
// to do it ourselves.
lock (assemblyFixtureMappings)
if (!assemblyFixtureMappings.ContainsKey(fixtureType))
Aggregator.Run(() => assemblyFixtureMappings.Add(fixtureType, CreateAssemblyFixtureInstance(fixtureType)));
}

// Don't want to use .Concat + .ToDictionary because of the possibility of overriding types,
// so instead we'll just let collection fixtures override assembly fixtures.
var combinedFixtures = new Dictionary<Type, object>(assemblyFixtureMappings);
foreach (var kvp in CollectionFixtureMappings)
combinedFixtures[kvp.Key] = kvp.Value;

// We've done everything we need, so let the built-in types do the rest of the heavy lifting
return new XunitTestClassRunner(testClass, @class, testCases, diagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, combinedFixtures).RunAsync();
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Xunit.Extensions.AssemblyFixture
{
class TestCollectionRunner : XunitTestCollectionRunner
{
readonly Dictionary<Type, object> assemblyFixtureMappings;
readonly IMessageSink diagnosticMessageSink;

public TestCollectionRunner(Dictionary<Type, object> assemblyFixtureMappings,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
ITestCaseOrderer testCaseOrderer,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
{
this.assemblyFixtureMappings = assemblyFixtureMappings;
this.diagnosticMessageSink = diagnosticMessageSink;
}

protected override Task<RunSummary> RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable<IXunitTestCase> testCases)
{
foreach (var fixtureType in @class.Type.GetTypeInfo().ImplementedInterfaces
.Where(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IAssemblyFixture<>))
.Select(i => i.GetTypeInfo().GenericTypeArguments.Single())
// First pass at filtering out before locking
.Where(i => !assemblyFixtureMappings.ContainsKey(i)))
{
// ConcurrentDictionary's GetOrAdd does not lock around the value factory call, so we need
// to do it ourselves.
lock (assemblyFixtureMappings)
if (!assemblyFixtureMappings.ContainsKey(fixtureType))
Aggregator.Run(() => assemblyFixtureMappings.Add(fixtureType, CreateAssemblyFixtureInstance(fixtureType)));
}

// Don't want to use .Concat + .ToDictionary because of the possibility of overriding types,
// so instead we'll just let collection fixtures override assembly fixtures.
var combinedFixtures = new Dictionary<Type, object>(assemblyFixtureMappings);
foreach (var kvp in CollectionFixtureMappings)
combinedFixtures[kvp.Key] = kvp.Value;

// We've done everything we need, so let the built-in types do the rest of the heavy lifting
return new XunitTestClassRunner(testClass, @class, testCases, diagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, combinedFixtures).RunAsync();
}

private object CreateAssemblyFixtureInstance(Type fixtureType) {
var constructors = fixtureType.GetConstructors();

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

return Activator.CreateInstance(fixtureType, diagnosticMessageSink);
}

}
}
return Activator.CreateInstance(fixtureType, diagnosticMessageSink);
}

}
}
55 changes: 55 additions & 0 deletions src/TypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace System
{
/// <summary>
/// Extension methods for <see cref="Type"/>.
/// </summary>
public static class TypeExtensions
{
/// <summary>
/// Checks if an instance of <paramref name="givenType"/> can be assigned to a type <paramref name="genericType"/>.
/// </summary>
/// <param name="givenType">The type under test</param>
/// <param name="genericType">The targeted type</param>
/// <returns><c>true</c> if <paramref name="genericType"/> is an ancestor of <paramref name="givenType"/> and <c>false</c> otherwise.</returns>
public static bool IsAssignableToGenericType(this Type givenType, Type genericType)
=> givenType is not null && genericType is not null
&& (givenType == genericType || givenType.MapsToGenericTypeDefinition(genericType)
|| givenType.HasInterfaceThatMapsToGenericTypeDefinition(genericType)
|| givenType.GetTypeInfo().BaseType.IsAssignableToGenericType(genericType));

private static bool HasInterfaceThatMapsToGenericTypeDefinition(this Type givenType, Type genericType)
=> givenType is not null && genericType is not null && givenType
.GetTypeInfo()
#if NETSTANDARD1_0 || NETSTANDARD1_1 || NETSTANDARD1_3

.ImplementedInterfaces
#else
.GetInterfaces()
#endif
.Where(it => it.GetTypeInfo().IsGenericType)
.Any(it => it.GetGenericTypeDefinition() == genericType);

private static bool MapsToGenericTypeDefinition(this Type givenType, Type genericType)
=> givenType is not null
&& genericType?.GetTypeInfo().IsGenericTypeDefinition == true
&& givenType.GetTypeInfo().IsGenericType
&& givenType.GetGenericTypeDefinition() == genericType;

/// <summary>
/// Tests if <paramref name="type"/> is an anonymous type
/// </summary>
/// <param name="type">The type under test</param>
/// <returns><c>true</c>if <paramref name="type"/> is an anonymous type and <c>false</c> otherwise</returns>
public static bool IsAnonymousType(this Type type)
{
bool hasCompilerGeneratedAttribute = type?.GetTypeInfo()?.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false)?.Any() ?? false;
bool nameContainsAnonymousType = type?.FullName.Contains("AnonymousType") ?? false;

return hasCompilerGeneratedAttribute && nameContainsAnonymousType;
}
}
}
Loading

0 comments on commit 92be7fc

Please sign in to comment.