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