11 changed files with 330 additions and 152 deletions
@ -1,71 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Headless.NUnit; |
|||
|
|||
// To force all tests (which might run in parallel) to be executed in a queue.
|
|||
internal static class ExecutionQueue |
|||
{ |
|||
private static bool s_running; |
|||
private static Queue<Func<Task>> s_queue = new(); |
|||
|
|||
private static async void TryExecuteNext() |
|||
{ |
|||
if (s_running || s_queue.Count == 0) return; |
|||
try |
|||
{ |
|||
s_running = true; |
|||
await s_queue.Dequeue()(); |
|||
} |
|||
finally |
|||
{ |
|||
s_running = false; |
|||
} |
|||
TryExecuteNext(); |
|||
} |
|||
|
|||
private static void PostToTheQueue(this Dispatcher dispatcher, Func<Task> cb) |
|||
{ |
|||
dispatcher.Post(() => |
|||
{ |
|||
s_queue.Enqueue(cb); |
|||
TryExecuteNext(); |
|||
}); |
|||
} |
|||
|
|||
internal static Task<TResult> ExecuteOnQueue<TResult>(this Dispatcher dispatcher, Func<Task<TResult>> cb) |
|||
{ |
|||
var tcs = new TaskCompletionSource<TResult>(); |
|||
PostToTheQueue(dispatcher, async () => |
|||
{ |
|||
try |
|||
{ |
|||
var result = await cb(); |
|||
tcs.TrySetResult(result); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
tcs.TrySetException(ex); |
|||
} |
|||
}); |
|||
return tcs.Task; |
|||
} |
|||
|
|||
public static TResult InvokeOnQueue<TResult>(this Dispatcher dispatcher, Func<Task<TResult>> cb, CancellationToken cancellationToken = default) |
|||
{ |
|||
return dispatcher |
|||
.InvokeAsync(() => ExecuteOnQueue(dispatcher, cb), DispatcherPriority.Normal, cancellationToken) |
|||
.GetTask().Unwrap() |
|||
.GetAwaiter().GetResult(); |
|||
} |
|||
|
|||
public static Task<TResult> InvokeOnQueueAsync<TResult>(this Dispatcher dispatcher, Func<Task<TResult>> cb, CancellationToken cancellationToken = default) |
|||
{ |
|||
return dispatcher |
|||
.InvokeAsync(() => ExecuteOnQueue(dispatcher, cb), DispatcherPriority.Normal, cancellationToken) |
|||
.GetTask().Unwrap(); |
|||
} |
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reflection; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Threading; |
|||
using Xunit.Abstractions; |
|||
using Xunit.Sdk; |
|||
|
|||
namespace Avalonia.Headless.XUnit; |
|||
|
|||
internal class AvaloniaTestAssemblyRunner : XunitTestAssemblyRunner |
|||
{ |
|||
private HeadlessUnitTestSession? _session; |
|||
|
|||
public AvaloniaTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases, |
|||
IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, |
|||
ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, |
|||
executionMessageSink, executionOptions) |
|||
{ |
|||
} |
|||
|
|||
protected override void SetupSyncContext(int maxParallelThreads) |
|||
{ |
|||
_session = HeadlessUnitTestSession.GetOrStartForAssembly( |
|||
Assembly.Load(new AssemblyName(TestAssembly.Assembly.Name))); |
|||
base.SetupSyncContext(1); |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
_session?.Dispose(); |
|||
base.Dispose(); |
|||
} |
|||
|
|||
protected override Task<RunSummary> RunTestCollectionAsync( |
|||
IMessageBus messageBus, |
|||
ITestCollection testCollection, |
|||
IEnumerable<IXunitTestCase> testCases, |
|||
CancellationTokenSource cancellationTokenSource) |
|||
{ |
|||
return new AvaloniaTestCollectionRunner(_session!, testCollection, testCases, DiagnosticMessageSink, messageBus, |
|||
TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); |
|||
} |
|||
|
|||
private class AvaloniaTestCollectionRunner : XunitTestCollectionRunner |
|||
{ |
|||
private readonly HeadlessUnitTestSession _session; |
|||
|
|||
public AvaloniaTestCollectionRunner(HeadlessUnitTestSession session, |
|||
ITestCollection testCollection, IEnumerable<IXunitTestCase> testCases, |
|||
IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, |
|||
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection, |
|||
testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource) |
|||
{ |
|||
_session = session; |
|||
} |
|||
|
|||
protected override Task<RunSummary> RunTestClassAsync( |
|||
ITestClass testClass, |
|||
IReflectionTypeInfo @class, |
|||
IEnumerable<IXunitTestCase> testCases) |
|||
{ |
|||
return new AvaloniaTestClassRunner(_session, testClass, @class, testCases, DiagnosticMessageSink, MessageBus, |
|||
TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, |
|||
CollectionFixtureMappings).RunAsync(); |
|||
} |
|||
} |
|||
|
|||
private class AvaloniaTestClassRunner : XunitTestClassRunner |
|||
{ |
|||
private readonly HeadlessUnitTestSession _session; |
|||
|
|||
public AvaloniaTestClassRunner(HeadlessUnitTestSession session, ITestClass testClass, |
|||
IReflectionTypeInfo @class, |
|||
IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, |
|||
ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, |
|||
CancellationTokenSource cancellationTokenSource, IDictionary<Type, object> collectionFixtureMappings) : |
|||
base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, |
|||
cancellationTokenSource, collectionFixtureMappings) |
|||
{ |
|||
_session = session; |
|||
} |
|||
|
|||
protected override Task<RunSummary> RunTestMethodAsync( |
|||
ITestMethod testMethod, |
|||
IReflectionMethodInfo method, |
|||
IEnumerable<IXunitTestCase> testCases, |
|||
object[] constructorArguments) |
|||
{ |
|||
return new AvaloniaTestMethodRunner(_session, testMethod, Class, method, testCases, DiagnosticMessageSink, |
|||
MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource, |
|||
constructorArguments).RunAsync(); |
|||
} |
|||
} |
|||
|
|||
private class AvaloniaTestMethodRunner : XunitTestMethodRunner |
|||
{ |
|||
private readonly HeadlessUnitTestSession _session; |
|||
private readonly IMessageBus _messageBus; |
|||
private readonly ExceptionAggregator _aggregator; |
|||
private readonly CancellationTokenSource _cancellationTokenSource; |
|||
private readonly object[] _constructorArguments; |
|||
|
|||
public AvaloniaTestMethodRunner(HeadlessUnitTestSession session, ITestMethod testMethod, |
|||
IReflectionTypeInfo @class, |
|||
IReflectionMethodInfo method, IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink, |
|||
IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, |
|||
object[] constructorArguments) : base(testMethod, @class, method, testCases, diagnosticMessageSink, |
|||
messageBus, aggregator, cancellationTokenSource, constructorArguments) |
|||
{ |
|||
_session = session; |
|||
_messageBus = messageBus; |
|||
_aggregator = aggregator; |
|||
_cancellationTokenSource = cancellationTokenSource; |
|||
_constructorArguments = constructorArguments; |
|||
} |
|||
|
|||
protected override Task<RunSummary> RunTestCaseAsync(IXunitTestCase testCase) |
|||
{ |
|||
return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason, |
|||
_constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator, |
|||
_cancellationTokenSource); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reflection; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Threading; |
|||
using Xunit.Abstractions; |
|||
using Xunit.Sdk; |
|||
|
|||
namespace Avalonia.Headless.XUnit; |
|||
|
|||
internal class AvaloniaTestCaseRunner : XunitTestCaseRunner |
|||
{ |
|||
private readonly Action? _onAfterTestInvoked; |
|||
|
|||
public AvaloniaTestCaseRunner( |
|||
Action? onAfterTestInvoked, |
|||
IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, |
|||
object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, |
|||
CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments, |
|||
testMethodArguments, messageBus, aggregator, cancellationTokenSource) |
|||
{ |
|||
_onAfterTestInvoked = onAfterTestInvoked; |
|||
} |
|||
|
|||
public static Task<RunSummary> RunTest(HeadlessUnitTestSession session, |
|||
IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, |
|||
object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, |
|||
CancellationTokenSource cancellationTokenSource) |
|||
{ |
|||
var afterTest = () => Dispatcher.UIThread.RunJobs(); |
|||
return session.Dispatch(async () => |
|||
{ |
|||
var runner = new AvaloniaTestCaseRunner(afterTest, testCase, displayName, |
|||
skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource); |
|||
return await runner.RunAsync(); |
|||
}, cancellationTokenSource.Token); |
|||
} |
|||
|
|||
protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass, |
|||
object[] constructorArguments, |
|||
MethodInfo testMethod, object[] testMethodArguments, string skipReason, |
|||
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, |
|||
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) |
|||
{ |
|||
return new AvaloniaTestRunner(_onAfterTestInvoked, test, messageBus, testClass, constructorArguments, |
|||
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource); |
|||
} |
|||
|
|||
private class AvaloniaTestRunner : XunitTestRunner |
|||
{ |
|||
private readonly Action? _onAfterTestInvoked; |
|||
|
|||
public AvaloniaTestRunner( |
|||
Action? onAfterTestInvoked, |
|||
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, beforeAfterAttributes, aggregator, cancellationTokenSource) |
|||
{ |
|||
_onAfterTestInvoked = onAfterTestInvoked; |
|||
} |
|||
|
|||
protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator) |
|||
{ |
|||
return new AvaloniaTestInvoker(_onAfterTestInvoked, Test, MessageBus, TestClass, ConstructorArguments, |
|||
TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync(); |
|||
} |
|||
} |
|||
|
|||
private class AvaloniaTestInvoker : XunitTestInvoker |
|||
{ |
|||
private readonly Action? _onAfterTestInvoked; |
|||
|
|||
public AvaloniaTestInvoker( |
|||
Action? onAfterTestInvoked, |
|||
ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, |
|||
object[] testMethodArguments, IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, |
|||
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(test, messageBus, |
|||
testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator, |
|||
cancellationTokenSource) |
|||
{ |
|||
_onAfterTestInvoked = onAfterTestInvoked; |
|||
} |
|||
|
|||
protected override async Task AfterTestMethodInvokedAsync() |
|||
{ |
|||
await base.AfterTestMethodInvokedAsync(); |
|||
|
|||
// Only here we can execute random code after the test, where exception will be properly handled by the XUnit.
|
|||
if (_onAfterTestInvoked is not null) |
|||
{ |
|||
Aggregator.Run(_onAfterTestInvoked); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,34 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reflection; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Threading; |
|||
using Xunit.Abstractions; |
|||
using Xunit.Sdk; |
|||
|
|||
namespace Avalonia.Headless.XUnit; |
|||
|
|||
internal class AvaloniaTestRunner : XunitTestAssemblyRunner |
|||
{ |
|||
private HeadlessUnitTestSession? _session; |
|||
|
|||
public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases, |
|||
IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, |
|||
ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, |
|||
executionMessageSink, executionOptions) |
|||
{ |
|||
} |
|||
|
|||
protected override void SetupSyncContext(int maxParallelThreads) |
|||
{ |
|||
_session = HeadlessUnitTestSession.GetOrStartForAssembly(Assembly.Load(new AssemblyName(TestAssembly.Assembly.Name))); |
|||
SynchronizationContext.SetSynchronizationContext(_session.SynchronizationContext); |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
_session?.Dispose(); |
|||
base.Dispose(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue