Commit 00fffc58 authored by Michael Herndon's avatar Michael Herndon

WIP: AB#1 import the rest of the xunit runners and the executor.

parent 3df30278
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 Mettle.Xunit.Sdk
{
/// <summary>
/// The test case runner for xUnit.net v2 tests.
/// </summary>
public class MettleTestCaseRunner : TestCaseRunner<IXunitTestCase>
{
List<BeforeAfterTestAttribute> beforeAfterAttributes;
/// <summary>
/// Initializes a new instance of the <see cref="XunitTestCaseRunner"/> class.
/// </summary>
/// <param name="testCase">The test case to be run.</param>
/// <param name="displayName">The display name of the test case.</param>
/// <param name="skipReason">The skip reason, if the test is to be skipped.</param>
/// <param name="constructorArguments">The arguments to be passed to the test class constructor.</param>
/// <param name="testMethodArguments">The arguments to be passed to the test method.</param>
/// <param name="messageBus">The message bus to report run status to.</param>
/// <param name="aggregator">The exception aggregator used to run code and collect exceptions.</param>
/// <param name="cancellationTokenSource">The task cancellation token source, used to cancel the test run.</param>
public MettleTestCaseRunner(IXunitTestCase testCase,
string displayName,
string skipReason,
object[] constructorArguments,
object[] testMethodArguments,
IMessageBus messageBus,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(testCase, messageBus, aggregator, cancellationTokenSource)
{
DisplayName = displayName;
SkipReason = skipReason;
ConstructorArguments = constructorArguments;
TestClass = TestCase.TestMethod.TestClass.Class.ToRuntimeType();
TestMethod = TestCase.Method.ToRuntimeMethod();
var parameters = TestMethod.GetParameters();
var parameterTypes = new Type[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
parameterTypes[i] = parameters[i].ParameterType;
TestMethodArguments = Reflector.ConvertArguments(testMethodArguments, parameterTypes);
}
/// <summary>
/// Gets the list of <see cref="BeforeAfterTestAttribute"/>s that will be used for this test case.
/// </summary>
public IReadOnlyList<BeforeAfterTestAttribute> BeforeAfterAttributes
{
get
{
if (beforeAfterAttributes == null)
beforeAfterAttributes = GetBeforeAfterTestAttributes();
return beforeAfterAttributes;
}
}
/// <summary>
/// Gets or sets the arguments passed to the test class constructor
/// </summary>
protected object[] ConstructorArguments { get; set; }
/// <summary>
/// Gets or sets the display name of the test case
/// </summary>
protected string DisplayName { get; set; }
/// <summary>
/// Gets or sets the skip reason for the test, if set.
/// </summary>
protected string SkipReason { get; set; }
/// <summary>
/// Gets or sets the runtime type for the test class that the test method belongs to.
/// </summary>
protected Type TestClass { get; set; }
/// <summary>
/// Gets of sets the runtime method for the test method that the test case belongs to.
/// </summary>
protected MethodInfo TestMethod { get; set; }
/// <summary>
/// Gets or sets the arguments to pass to the test method when it's being invoked.
/// </summary>
protected object[] TestMethodArguments { get; set; }
/// <summary>
/// Creates the <see cref="ITest"/> instance for the given test case.
/// </summary>
protected virtual ITest CreateTest(IXunitTestCase testCase, string displayName)
=> new XunitTest(testCase, displayName);
/// <summary>
/// Creates the test runner used to run the given test.
/// </summary>
protected virtual XunitTestRunner CreateTestRunner(ITest test,
IMessageBus messageBus,
Type testClass,
object[] constructorArguments,
MethodInfo testMethod,
object[] testMethodArguments,
string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
=> new XunitTestRunner(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments,
skipReason, beforeAfterAttributes, new ExceptionAggregator(aggregator), cancellationTokenSource);
/// <summary>
/// Gets the list of <see cref="BeforeAfterTestAttribute"/> attributes that apply to this test case.
/// </summary>
protected virtual List<BeforeAfterTestAttribute> GetBeforeAfterTestAttributes()
{
IEnumerable<Attribute> beforeAfterTestCollectionAttributes;
if (TestCase.TestMethod.TestClass.TestCollection.CollectionDefinition is IReflectionTypeInfo collectionDefinition)
beforeAfterTestCollectionAttributes = collectionDefinition.Type.GetTypeInfo().GetCustomAttributes(typeof(BeforeAfterTestAttribute));
else
beforeAfterTestCollectionAttributes = Enumerable.Empty<Attribute>();
return beforeAfterTestCollectionAttributes.Concat(TestClass.GetTypeInfo().GetCustomAttributes(typeof(BeforeAfterTestAttribute)))
.Concat(TestMethod.GetCustomAttributes(typeof(BeforeAfterTestAttribute)))
.Cast<BeforeAfterTestAttribute>()
.ToList();
}
/// <inheritdoc/>
protected override Task<RunSummary> RunTestAsync()
=> CreateTestRunner(CreateTest(TestCase, DisplayName), MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments,
SkipReason, BeforeAfterAttributes, Aggregator, CancellationTokenSource).RunAsync();
}
}
This diff is collapsed.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Mettle.Xunit.Sdk
{
/// <summary>
/// The test collection runner for xUnit.net v2 tests.
/// </summary>
public class XunitTestCollectionRunner : TestCollectionRunner<IXunitTestCase>
{
/// <summary>
/// Initializes a new instance of the <see cref="XunitTestCollectionRunner"/> class.
/// </summary>
/// <param name="testCollection">The test collection that contains the tests to be run.</param>
/// <param name="testCases">The test cases to be run.</param>
/// <param name="diagnosticMessageSink">The message sink used to send diagnostic messages</param>
/// <param name="messageBus">The message bus to report run status to.</param>
/// <param name="testCaseOrderer">The test case orderer that will be used to decide how to order the test.</param>
/// <param name="aggregator">The exception aggregator used to run code and collect exceptions.</param>
/// <param name="cancellationTokenSource">The task cancellation token source, used to cancel the test run.</param>
public XunitTestCollectionRunner(ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
ITestCaseOrderer testCaseOrderer,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(testCollection, testCases, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
{
this.DiagnosticMessageSink = diagnosticMessageSink;
}
/// <summary>
/// Gets the fixture mappings that were created during <see cref="AfterTestCollectionStartingAsync"/>.
/// </summary>
protected Dictionary<Type, object> CollectionFixtureMappings { get; set; } = new Dictionary<Type, object>();
/// <summary>
/// Gets the message sink used to send diagnostic messages.
/// </summary>
protected IMessageSink DiagnosticMessageSink { get; private set; }
/// <inheritdoc/>
protected override async Task AfterTestCollectionStartingAsync()
{
await CreateCollectionFixturesAsync();
TestCaseOrderer = GetTestCaseOrderer() ?? TestCaseOrderer;
}
/// <inheritdoc/>
protected override async Task BeforeTestCollectionFinishedAsync()
{
var disposeAsyncTasks = CollectionFixtureMappings.Values.OfType<IAsyncLifetime>().Select(fixture => Aggregator.RunAsync(fixture.DisposeAsync)).ToList();
await Task.WhenAll(disposeAsyncTasks);
foreach (var fixture in CollectionFixtureMappings.Values.OfType<IDisposable>())
Aggregator.Run(fixture.Dispose);
}
/// <summary>
/// Creates the instance of a collection fixture type to be used by the test collection. If the fixture can be created,
/// it should be placed into the <see cref="CollectionFixtureMappings"/> dictionary; if it cannot, then the method
/// should record the error by calling <code>Aggregator.Add</code>.
/// </summary>
/// <param name="fixtureType">The type of the fixture to be created</param>
protected virtual void CreateCollectionFixture(Type fixtureType)
{
var ctors = fixtureType.GetTypeInfo()
.DeclaredConstructors
.Where(ci => !ci.IsStatic && ci.IsPublic)
.ToList();
if (ctors.Count != 1)
{
Aggregator.Add(new TestClassException($"Collection fixture type '{fixtureType.FullName}' may only define a single public constructor."));
return;
}
var ctor = ctors[0];
var missingParameters = new List<ParameterInfo>();
var ctorArgs = ctor.GetParameters().Select(p =>
{
object arg = null;
if (p.ParameterType == typeof(IMessageSink))
arg = DiagnosticMessageSink;
else
missingParameters.Add(p);
return arg;
}).ToArray();
if (missingParameters.Count > 0)
Aggregator.Add(new TestClassException(
$"Collection fixture type '{fixtureType.FullName}' had one or more unresolved constructor arguments: {string.Join(", ", missingParameters.Select(p => $"{p.ParameterType.Name} {p.Name}"))}"
));
else
{
Aggregator.Run(() => CollectionFixtureMappings[fixtureType] = ctor.Invoke(ctorArgs));
}
}
Task CreateCollectionFixturesAsync()
{
if (TestCollection.CollectionDefinition == null)
{
return Task.FromResult(0);
}
var declarationType = ((IReflectionTypeInfo)TestCollection.CollectionDefinition).Type;
foreach (var interfaceType in declarationType.GetTypeInfo().ImplementedInterfaces.Where(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollectionFixture<>)))
{
var fixtureType = interfaceType.GenericTypeArguments.Single();
CreateCollectionFixture(fixtureType);
}
var initializeAsyncTasks = CollectionFixtureMappings.Values.OfType<IAsyncLifetime>().Select(fixture => Aggregator.RunAsync(fixture.InitializeAsync)).ToList();
return Task.WhenAll(initializeAsyncTasks);
}
/// <summary>
/// Gives an opportunity to override test case orderer. By default, this method gets the
/// orderer from the collection definition. If this function returns <c>null</c>, the
/// test case orderer passed into the constructor will be used.
/// </summary>
protected virtual ITestCaseOrderer GetTestCaseOrderer()
{
if (TestCollection.CollectionDefinition != null)
{
var ordererAttribute = TestCollection.CollectionDefinition.GetCustomAttributes(typeof(TestCaseOrdererAttribute)).SingleOrDefault();
if (ordererAttribute != null)
{
try
{
var testCaseOrderer = ExtensibilityPointFactory.GetTestCaseOrderer(DiagnosticMessageSink, ordererAttribute);
if (testCaseOrderer != null)
return testCaseOrderer;
var args = ordererAttribute.GetConstructorArguments().Cast<string>().ToList();
DiagnosticMessageSink.OnMessage(new DiagnosticMessage($"Could not find type '{args[0]}' in {args[1]} for collection-level test case orderer on test collection '{TestCollection.DisplayName}'"));
}
catch (Exception ex)
{
var innerEx = ex.Unwrap();
var args = ordererAttribute.GetConstructorArguments().Cast<string>().ToList();
DiagnosticMessageSink.OnMessage(new DiagnosticMessage($"Collection-level test case orderer '{args[0]}' for test collection '{TestCollection.DisplayName}' threw '{innerEx.GetType().FullName}' during construction: {innerEx.Message}{Environment.NewLine}{innerEx.StackTrace}"));
}
}
}
return null;
}
/// <inheritdoc/>
protected override Task<RunSummary> RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable<IXunitTestCase> testCases)
=> new XunitTestClassRunner(testClass, @class, testCases, DiagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, CollectionFixtureMappings).RunAsync();
}
}
using System;
using System.Collections.Generic;
using System.Reflection;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Mettle.Xunit.Sdk
{
/// <summary>
/// The implementation of <see cref="ITestFrameworkExecutor"/> that supports execution
/// of unit tests linked against xunit.core.dll, using xunit.execution.dll.
/// </summary>
public class MettleTestFrameworkExecutor : TestFrameworkExecutor<IXunitTestCase>
{
readonly Lazy<XunitTestFrameworkDiscoverer> discoverer;
/// <summary>
/// Initializes a new instance of the <see cref="XunitTestFrameworkExecutor"/> class.
/// </summary>
/// <param name="assemblyName">Name of the test assembly.</param>
/// <param name="sourceInformationProvider">The source line number information provider.</param>
/// <param name="diagnosticMessageSink">The message sink to report diagnostic messages to.</param>
public MettleTestFrameworkExecutor(AssemblyName assemblyName,
ISourceInformationProvider sourceInformationProvider,
IMessageSink diagnosticMessageSink)
: base(assemblyName, sourceInformationProvider, diagnosticMessageSink)
{
string config = null;
#if NETFRAMEWORK
config = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
#endif
TestAssembly = new TestAssembly(AssemblyInfo, config, assemblyName.Version);
discoverer = new Lazy<XunitTestFrameworkDiscoverer>(() => new XunitTestFrameworkDiscoverer(AssemblyInfo, SourceInformationProvider, DiagnosticMessageSink));
}
/// <summary>
/// Gets the test assembly that contains the test.
/// </summary>
protected TestAssembly TestAssembly { get; set; }
/// <inheritdoc/>
protected override ITestFrameworkDiscoverer CreateDiscoverer()
=> discoverer.Value;
/// <inheritdoc/>
public override ITestCase Deserialize(string value)
{
if (value.Length > 3 && value.StartsWith(":F:"))
{
// Format from XunitTestFrameworkDiscoverer.Serialize: ":F:{typeName}:{methodName}:{defaultMethodDisplay}:{defaultMethodDisplayOptions}:{collectionId}"
// Colons in values are double-escaped, so we can't use String.Split
var parts = new List<string>();
var idx = 3;
var idxEnd = 3;
while (idxEnd < value.Length)
{
if (value[idxEnd] == ':')
{
if (idxEnd + 1 == value.Length || value[idxEnd + 1] != ':')
{
if (idx != idxEnd)
parts.Add(value.Substring(idx, idxEnd - idx).Replace("::", ":"));
idx = idxEnd + 1;
}
else if (value[idxEnd + 1] == ':')
++idxEnd;
}
++idxEnd;
}
if (idx != idxEnd)
parts.Add(value.Substring(idx, idxEnd - idx).Replace("::", ":"));
if (parts.Count > 4)
{
// TODO: create a mettle test discover and uncomment when complete.
/*
var typeInfo = discoverer.Value.AssemblyInfo.GetType(parts[0]);
var testCollectionUniqueId = Guid.Parse(parts[4]);
var testClass = discoverer.Value.CreateTestClass(typeInfo, testCollectionUniqueId);
var methodInfo = testClass.Class.GetMethod(parts[1], true);
var testMethod = new TestMethod(testClass, methodInfo);
var defaultMethodDisplay = (TestMethodDisplay)int.Parse(parts[2]);
var defaultMethodDisplayOptions = (TestMethodDisplayOptions)int.Parse(parts[3]);
return new XunitTestCase(DiagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod);
*/
}
}
return base.Deserialize(value);
}
/// <inheritdoc/>
protected override async void RunTestCases(IEnumerable<IXunitTestCase> testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
{
using (var assemblyRunner = new XunitTestAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions))
await assemblyRunner.RunAsync();
}
}
}
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Mettle.Xunit.Sdk
{
/// <summary>
/// The test method runner for xUnit.net v2 tests.
/// </summary>
public class MettleTestMethodRunner : TestMethodRunner<IXunitTestCase>
{
readonly object[] constructorArguments;
readonly IMessageSink diagnosticMessageSink;
/// <summary>
/// Initializes a new instance of the <see cref="MettleTestMethodRunner"/> class.
/// </summary>
/// <param name="testMethod">The test method to be run.</param>
/// <param name="class">The test class that contains the test method.</param>
/// <param name="method">The test method that contains the tests to be run.</param>
/// <param name="testCases">The test cases to be run.</param>
/// <param name="diagnosticMessageSink">The message sink used to send diagnostic messages to.</param>
/// <param name="messageBus">The message bus to report run status to.</param>
/// <param name="aggregator">The exception aggregator used to run code and collect exceptions.</param>
/// <param name="cancellationTokenSource">The task cancellation token source, used to cancel the test run.</param>
/// <param name="constructorArguments">The constructor arguments for the test class.</param>
public MettleTestMethodRunner(ITestMethod testMethod,
IReflectionTypeInfo @class,
IReflectionMethodInfo method,
IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
object[] constructorArguments)
: base(testMethod, @class, method, testCases, messageBus, aggregator, cancellationTokenSource)
{
this.constructorArguments = constructorArguments;
this.diagnosticMessageSink = diagnosticMessageSink;
}
/// <inheritdoc/>
protected override Task<RunSummary> RunTestCaseAsync(IXunitTestCase testCase)
=> testCase.RunAsync(diagnosticMessageSink, MessageBus, constructorArguments, new ExceptionAggregator(Aggregator), CancellationTokenSource);
}
}
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Mettle.Xunit.Sdk
{
/// <summary>
/// The test runner for xUnit.net v2 tests.
/// </summary>
public class MettleTestRunner : TestRunner<IXunitTestCase>
{
readonly IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes;
/// <summary>
/// Initializes a new instance of the <see cref="XunitTestRunner"/> class.
/// </summary>
/// <param name="test">The test that this invocation belongs to.</param>
/// <param name="messageBus">The message bus to report run status to.</param>
/// <param name="testClass">The test class that the test method belongs to.</param>
/// <param name="constructorArguments">The arguments to be passed to the test class constructor.</param>
/// <param name="testMethod">The test method that will be invoked.</param>
/// <param name="testMethodArguments">The arguments to be passed to the test method.</param>
/// <param name="skipReason">The skip reason, if the test is to be skipped.</param>
/// <param name="beforeAfterAttributes">The list of <see cref="BeforeAfterTestAttribute"/>s for this test.</param>
/// <param name="aggregator">The exception aggregator used to run code and collect exceptions.</param>
/// <param name="cancellationTokenSource">The task cancellation token source, used to cancel the test run.</param>
public MettleTestRunner(ITest test,
IMessageBus messageBus,
Type testClass,
object[] constructorArguments,
MethodInfo testMethod,
object[] testMethodArguments,
string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, aggregator, cancellationTokenSource)
{
this.beforeAfterAttributes = beforeAfterAttributes;
}
/// <summary>
/// Gets the list of <see cref="BeforeAfterTestAttribute"/>s for this test.
/// </summary>
protected IReadOnlyList<BeforeAfterTestAttribute> BeforeAfterAttributes
=> beforeAfterAttributes;
/// <inheritdoc/>
protected override async Task<Tuple<decimal, string>> InvokeTestAsync(ExceptionAggregator aggregator)
{
var output = string.Empty;
TestOutputHelper testOutputHelper = null;
foreach (object obj in ConstructorArguments)
{
testOutputHelper = obj as TestOutputHelper;
if (testOutputHelper != null)
break;
}
if (testOutputHelper != null)
testOutputHelper.Initialize(MessageBus, Test);
var executionTime = await InvokeTestMethodAsync(aggregator);
if (testOutputHelper != null)
{
output = testOutputHelper.Output;
testOutputHelper.Uninitialize();
}
return Tuple.Create(executionTime, output);
}
/// <summary>
/// Override this method to invoke the test method.
/// </summary>
/// <param name="aggregator">The exception aggregator used to run code and collect exceptions.</param>
/// <returns>Returns the execution time (in seconds) spent running the test method.</returns>
protected virtual Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
=> new MettleTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment