From 99f4739e85c9bb29ed782593d8298d42bbb41582 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 27 Apr 2023 23:38:46 -0400 Subject: [PATCH 1/4] Fix scenario when application attribute is not used --- src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs index 12f8515209..f98240d6d7 100644 --- a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs +++ b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs @@ -128,7 +128,8 @@ public sealed class HeadlessUnitTestSession : IDisposable if (s_session is not null) { - if (appBuilderEntryPointType != s_session.EntryPointType) + var hasNoAttribute = appBuilderEntryPointType == null && s_session.EntryPointType == typeof(Application); + if (!hasNoAttribute && appBuilderEntryPointType != s_session.EntryPointType) { // Avalonia doesn't support multiple Application instances. At least at the moment. throw new System.InvalidOperationException( From bb893b189c3e46b779d7d206eee156d0b584f29a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 28 Apr 2023 04:58:54 -0400 Subject: [PATCH 2/4] Make HeadlessUnitTestSession run individual frames per test --- .../AvaloniaTestMethodCommand.cs | 9 +- .../Avalonia.Headless.NUnit/ExecutionQueue.cs | 71 -------------- .../Avalonia.Headless.XUnit.csproj | 6 -- ...unner.cs => AvaloniaTestAssemblyRunner.cs} | 0 .../AvaloniaTestCase.cs | 12 +-- .../AvaloniaTestCaseRunner.cs | 98 +++++++++++++++++++ .../AvaloniaTheoryTestCase.cs | 8 +- .../HeadlessUnitTestSession.cs | 95 +++++++++++++++--- .../ThreadingTests.cs | 7 ++ 9 files changed, 200 insertions(+), 106 deletions(-) delete mode 100644 src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs rename src/Headless/Avalonia.Headless.XUnit/{AvaloniaTestRunner.cs => AvaloniaTestAssemblyRunner.cs} (100%) create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs index a7e461f76e..318005f0b0 100644 --- a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs +++ b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using System.Threading.Tasks; +using Avalonia.Threading; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal; using NUnit.Framework.Internal.Commands; @@ -30,12 +31,12 @@ internal class AvaloniaTestMethodCommand : DelegatingTestCommand { if (s_beforeTest.GetValue(beforeAndAfterTestCommand) is Action beforeTest) { - Action beforeAction = c => session.Dispatcher.Invoke(() => beforeTest(c)); + Action beforeAction = c => session.Dispatch(() => beforeTest(c), default); s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction); } if (s_afterTest.GetValue(beforeAndAfterTestCommand) is Action afterTest) { - Action afterAction = c => session.Dispatcher.Invoke(() => afterTest(c)); + Action afterAction = c => session.Dispatch(() => afterTest(c), default); s_afterTest.SetValue(beforeAndAfterTestCommand, afterAction); } } @@ -52,10 +53,10 @@ internal class AvaloniaTestMethodCommand : DelegatingTestCommand return command; } - + public override TestResult Execute(TestExecutionContext context) { - return _session.Dispatcher.InvokeOnQueue(() => ExecuteTestMethod(context)); + return _session.Dispatch(() => ExecuteTestMethod(context), default).GetAwaiter().GetResult(); } // Unfortunately, NUnit has issues with custom synchronization contexts, which means we need to add some hacks to make it work. diff --git a/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs b/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs deleted file mode 100644 index b42f126389..0000000000 --- a/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs +++ /dev/null @@ -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> 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 cb) - { - dispatcher.Post(() => - { - s_queue.Enqueue(cb); - TryExecuteNext(); - }); - } - - internal static Task ExecuteOnQueue(this Dispatcher dispatcher, Func> cb) - { - var tcs = new TaskCompletionSource(); - PostToTheQueue(dispatcher, async () => - { - try - { - var result = await cb(); - tcs.TrySetResult(result); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - }); - return tcs.Task; - } - - public static TResult InvokeOnQueue(this Dispatcher dispatcher, Func> cb, CancellationToken cancellationToken = default) - { - return dispatcher - .InvokeAsync(() => ExecuteOnQueue(dispatcher, cb), DispatcherPriority.Normal, cancellationToken) - .GetTask().Unwrap() - .GetAwaiter().GetResult(); - } - - public static Task InvokeOnQueueAsync(this Dispatcher dispatcher, Func> cb, CancellationToken cancellationToken = default) - { - return dispatcher - .InvokeAsync(() => ExecuteOnQueue(dispatcher, cb), DispatcherPriority.Normal, cancellationToken) - .GetTask().Unwrap(); - } -} diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj index c244a1a4cd..4ab70eb07d 100644 --- a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -13,12 +13,6 @@ - - - ExecutionQueue.cs - - - diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs similarity index 100% rename from src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs rename to src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs index b6c0cd11c4..092662745c 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs @@ -1,8 +1,8 @@ using System; using System.ComponentModel; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; -using Avalonia.Headless.NUnit; using Avalonia.Threading; using Xunit.Abstractions; using Xunit.Sdk; @@ -37,12 +37,10 @@ internal class AvaloniaTestCase : XunitTestCase // We need to block the XUnit thread to ensure its concurrency throttle is effective. // See https://github.com/AArnott/Xunit.StaFact/pull/55#issuecomment-826187354 for details. - var runSummary = session.Dispatcher.InvokeOnQueue(async () => - { - var runner = new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, - TestMethodArguments, messageBus, aggregator, cancellationTokenSource); - return await runner.RunAsync(); - }, cancellationTokenSource.Token); + var runSummary = AvaloniaTestCaseRunner + .RunTest(session, this, DisplayName, SkipReason, constructorArguments, + TestMethodArguments, messageBus, aggregator, cancellationTokenSource) + .GetAwaiter().GetResult(); return Task.FromResult(runSummary); } diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs new file mode 100644 index 0000000000..97fcfa2521 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs @@ -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 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 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 beforeAfterAttributes, ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments, + testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource) + { + _onAfterTestInvoked = onAfterTestInvoked; + } + + protected override Task 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 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); + } + } + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs index 4b56de6e6c..ea7e7abee4 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs @@ -2,7 +2,6 @@ using System.ComponentModel; using System.Threading; using System.Threading.Tasks; -using Avalonia.Headless.NUnit; using Xunit.Abstractions; using Xunit.Sdk; @@ -25,11 +24,8 @@ internal class AvaloniaTheoryTestCase : XunitTheoryTestCase { var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly); - return session.Dispatcher.InvokeOnQueueAsync(async () => - { - var runner = new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, + return AvaloniaTestCaseRunner + .RunTest(session, this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource); - return await runner.RunAsync(); - }, cancellationTokenSource.Token); } } diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs index f98240d6d7..81295f7fc3 100644 --- a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs +++ b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; @@ -18,9 +20,11 @@ namespace Avalonia.Headless; /// public sealed class HeadlessUnitTestSession : IDisposable { - private readonly CancellationTokenSource _cancellationToken; + private readonly CancellationTokenSource _cancellationTokenSource; private static HeadlessUnitTestSession? s_session; private static object s_lock = new(); + private readonly BlockingCollection _queue; + private readonly Task _dispatchTask; internal const DynamicallyAccessedMemberTypes DynamicallyAccessed = DynamicallyAccessedMemberTypes.PublicMethods | @@ -29,9 +33,11 @@ public sealed class HeadlessUnitTestSession : IDisposable private HeadlessUnitTestSession(Type entryPointType, Application application, SynchronizationContext synchronizationContext, - Dispatcher dispatcher, CancellationTokenSource cancellationToken) + Dispatcher dispatcher, CancellationTokenSource cancellationTokenSource, BlockingCollection queue, Task _dispatchTask) { - _cancellationToken = cancellationToken; + _cancellationTokenSource = cancellationTokenSource; + _queue = queue; + this._dispatchTask = _dispatchTask; EntryPointType = entryPointType; Dispatcher = dispatcher; Application = application; @@ -43,9 +49,63 @@ public sealed class HeadlessUnitTestSession : IDisposable public Dispatcher Dispatcher { get; } internal Type EntryPointType { get; } + public Task Dispatch(Action action, CancellationToken cancellationToken) + { + return Dispatch(() => { action(); return Task.FromResult(0); }, cancellationToken); + } + + public Task Dispatch(Func action, CancellationToken cancellationToken) + { + return Dispatch(() => Task.FromResult(action()), cancellationToken); + } + + public Task Dispatch(Func> action, CancellationToken cancellationToken) + { + if (_cancellationTokenSource.IsCancellationRequested) + { + throw new ObjectDisposedException("Session was already disposed."); + } + + var token = _cancellationTokenSource.Token; + + var tcs = new TaskCompletionSource(); + _queue.Add(() => + { + var cts = new CancellationTokenSource(); + using var globalCts = token.Register(s => ((CancellationTokenSource)s!).Cancel(), cts); + using var localCts = cancellationToken.Register(s => ((CancellationTokenSource)s!).Cancel(), cts); + + try + { + var task = action(); + task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts); + + if (cts.IsCancellationRequested) + { + return; + } + + var frame = new DispatcherFrame(); + using var innerCts = cts.Token.Register(() => frame.Continue = false); + Dispatcher.PushFrame(frame); + + var result = task.GetAwaiter().GetResult(); + tcs.TrySetResult(result); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }); + return tcs.Task; + } + public void Dispose() { - _cancellationToken.Cancel(); + _cancellationTokenSource.Cancel(); + _queue.CompleteAdding(); + _dispatchTask.Wait(); + _cancellationTokenSource.Dispose(); } /// @@ -73,9 +133,10 @@ public sealed class HeadlessUnitTestSession : IDisposable { var tcs = new TaskCompletionSource(); var cancellationTokenSource = new CancellationTokenSource(); + var queue = new BlockingCollection(); - Thread? thread = null; - thread = new Thread(() => + Task? task = null; + task = Task.Run(() => { try { @@ -93,10 +154,10 @@ public sealed class HeadlessUnitTestSession : IDisposable { throw new InvalidOperationException("Avalonia Headless platform has failed to initialize."); } - + // ReSharper disable once AccessToModifiedClosure tcs.SetResult(new HeadlessUnitTestSession(entryPointType, Application.Current!, - SynchronizationContext.Current!, Dispatcher.UIThread, cancellationTokenSource)); + SynchronizationContext.Current!, Dispatcher.UIThread, cancellationTokenSource, queue, task!)); } catch (Exception e) { @@ -104,13 +165,23 @@ public sealed class HeadlessUnitTestSession : IDisposable return; } - Dispatcher.UIThread.MainLoop(cancellationTokenSource.Token); - }) { IsBackground = true }; - thread.Start(); + while (!cancellationTokenSource.IsCancellationRequested) + { + try + { + var action = queue.Take(cancellationTokenSource.Token); + action(); + } + catch (OperationCanceledException) + { + + } + } + }); return tcs.Task.GetAwaiter().GetResult(); } - + /// /// Creates a session from AvaloniaTestApplicationAttribute attribute or reuses any existing. /// If AvaloniaTestApplicationAttribute doesn't exist, empty application is used. diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs index e26b902eb1..b6ba7de8c5 100644 --- a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -13,6 +13,13 @@ public class ThreadingTests Dispatcher.UIThread.VerifyAccess(); } + // This test should always fail, uncomment to test if it fails. + // [AvaloniaFact] + // public void Should_Fail_Test_On_Delayed_Post_When_FlushDispatcher() + // { + // Dispatcher.UIThread.Post(() => throw new InvalidOperationException(), DispatcherPriority.Default); + // } + [AvaloniaTheory] [InlineData(1)] [InlineData(10)] From 73d726a9f565642151b1602b567a81004260483c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 28 Apr 2023 04:59:07 -0400 Subject: [PATCH 3/4] Make AvaloniaTestAssemblyRunner use existing test case runners --- .../AvaloniaTestAssemblyRunner.cs | 100 +++++++++++++++++- .../AvaloniaTestFramework.cs | 2 +- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs index 86efe33f5c..4b1cf84914 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs @@ -9,11 +9,11 @@ using Xunit.Sdk; namespace Avalonia.Headless.XUnit; -internal class AvaloniaTestRunner : XunitTestAssemblyRunner +internal class AvaloniaTestAssemblyRunner : XunitTestAssemblyRunner { private HeadlessUnitTestSession? _session; - public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable testCases, + public AvaloniaTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions) @@ -22,8 +22,9 @@ internal class AvaloniaTestRunner : XunitTestAssemblyRunner protected override void SetupSyncContext(int maxParallelThreads) { - _session = HeadlessUnitTestSession.GetOrStartForAssembly(Assembly.Load(new AssemblyName(TestAssembly.Assembly.Name))); - SynchronizationContext.SetSynchronizationContext(_session.SynchronizationContext); + _session = HeadlessUnitTestSession.GetOrStartForAssembly( + Assembly.Load(new AssemblyName(TestAssembly.Assembly.Name))); + base.SetupSyncContext(1); } public override void Dispose() @@ -31,4 +32,95 @@ internal class AvaloniaTestRunner : XunitTestAssemblyRunner _session?.Dispose(); base.Dispose(); } + + protected override Task RunTestCollectionAsync( + IMessageBus messageBus, + ITestCollection testCollection, + IEnumerable 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 testCases, + IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, + ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection, + testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource) + { + _session = session; + } + + protected override Task RunTestClassAsync( + ITestClass testClass, + IReflectionTypeInfo @class, + IEnumerable 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 testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, + ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource, IDictionary collectionFixtureMappings) : + base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, + cancellationTokenSource, collectionFixtureMappings) + { + _session = session; + } + + protected override Task RunTestMethodAsync( + ITestMethod testMethod, + IReflectionMethodInfo method, + IEnumerable 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 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 RunTestCaseAsync(IXunitTestCase testCase) + { + return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason, + _constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator, + _cancellationTokenSource); + } + } } diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs index 2d45228c57..aa9b3e7e18 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs @@ -27,7 +27,7 @@ internal class AvaloniaTestFramework : XunitTestFramework IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) { - using (var assemblyRunner = new AvaloniaTestRunner( + using (var assemblyRunner = new AvaloniaTestAssemblyRunner( TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions)) await assemblyRunner.RunAsync(); } From 0547001f3b16be9a0386c923e9f209fd4c30340c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 28 Apr 2023 05:59:07 -0400 Subject: [PATCH 4/4] Cleanup --- .../HeadlessUnitTestSession.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs index 81295f7fc3..312064064f 100644 --- a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs +++ b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs @@ -31,22 +31,14 @@ public sealed class HeadlessUnitTestSession : IDisposable DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor; - private HeadlessUnitTestSession(Type entryPointType, Application application, - SynchronizationContext synchronizationContext, - Dispatcher dispatcher, CancellationTokenSource cancellationTokenSource, BlockingCollection queue, Task _dispatchTask) + private HeadlessUnitTestSession(Type entryPointType, CancellationTokenSource cancellationTokenSource, BlockingCollection queue, Task _dispatchTask) { _cancellationTokenSource = cancellationTokenSource; _queue = queue; this._dispatchTask = _dispatchTask; EntryPointType = entryPointType; - Dispatcher = dispatcher; - Application = application; - SynchronizationContext = synchronizationContext; } - - public Application Application { get; } - public SynchronizationContext SynchronizationContext { get; } - public Dispatcher Dispatcher { get; } + internal Type EntryPointType { get; } public Task Dispatch(Action action, CancellationToken cancellationToken) @@ -87,7 +79,7 @@ public sealed class HeadlessUnitTestSession : IDisposable var frame = new DispatcherFrame(); using var innerCts = cts.Token.Register(() => frame.Continue = false); - Dispatcher.PushFrame(frame); + Dispatcher.UIThread.PushFrame(frame); var result = task.GetAwaiter().GetResult(); tcs.TrySetResult(result); @@ -156,8 +148,7 @@ public sealed class HeadlessUnitTestSession : IDisposable } // ReSharper disable once AccessToModifiedClosure - tcs.SetResult(new HeadlessUnitTestSession(entryPointType, Application.Current!, - SynchronizationContext.Current!, Dispatcher.UIThread, cancellationTokenSource, queue, task!)); + tcs.SetResult(new HeadlessUnitTestSession(entryPointType, cancellationTokenSource, queue, task!)); } catch (Exception e) {