From ae04033c76f0cfab4e2183538104887d10ca1410 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 27 Apr 2023 00:48:00 -0400 Subject: [PATCH] Use ExecutionQueue in test frameworks --- .../AvaloniaTestCommand.cs | 47 ++++++++-------- .../Avalonia.Headless.NUnit/ExecutionQueue.cs | 54 +++++++++++++++---- .../Avalonia.Headless.XUnit.csproj | 6 +++ .../AvaloniaTestCase.cs | 10 ++-- .../AvaloniaTheoryTestCase.cs | 9 ++-- .../Avalonia.Headless.UnitTests/InputTests.cs | 2 +- 6 files changed, 86 insertions(+), 42 deletions(-) diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestCommand.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestCommand.cs index e5eabb612a..2e761616f6 100644 --- a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestCommand.cs +++ b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestCommand.cs @@ -17,28 +17,31 @@ internal class AvaloniaTestCommand : DelegatingTestCommand public override TestResult Execute(TestExecutionContext context) { - return _session.Dispatcher.InvokeAsync>(async () => + return _session.Dispatcher.InvokeOnQueue(() => ExecuteTestMethod(context)); + } + + // Unfortunately, NUnit has issues with custom synchronization contexts, which means we need to add some hacks to make it work. + private async Task ExecuteTestMethod(TestExecutionContext context) + { + var testMethod = innerCommand.Test.Method; + var methodInfo = testMethod!.MethodInfo; + + var result = methodInfo.Invoke(context.TestObject, innerCommand.Test.Arguments); + // Only Task, non generic ValueTask are supported in async context. No ValueTask<> nor F# tasks. + if (result is Task task) { - var testMethod = innerCommand.Test.Method; - var methodInfo = testMethod!.MethodInfo; - - var result = methodInfo.Invoke(context.TestObject, innerCommand.Test.Arguments); - // Only Task, non generic ValueTask are supported in async context. No ValueTask<> nor F# tasks. - if (result is Task task) - { - await task; - } - else if (result is ValueTask valueTask) - { - await valueTask; - } - - context.CurrentResult.SetResult(ResultState.Success); - - if (context.CurrentResult.AssertionResults.Count > 0) - context.CurrentResult.RecordTestCompletion(); - - return context.CurrentResult; - }).GetTask().Unwrap().Result; + await task; + } + else if (result is ValueTask valueTask) + { + await valueTask; + } + + context.CurrentResult.SetResult(ResultState.Success); + + if (context.CurrentResult.AssertionResults.Count > 0) + context.CurrentResult.RecordTestCompletion(); + + return context.CurrentResult; } } diff --git a/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs b/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs index 5488fc956a..ebaca54164 100644 --- a/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs +++ b/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs @@ -1,35 +1,71 @@ 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 { - static bool _running; - static Queue> _queue = new(); - static async void TryExecuteNext() + private static bool s_running; + private static Queue> s_queue = new(); + + private static async void TryExecuteNext() { - if (_running || _queue.Count == 0) return; + if (s_running || s_queue.Count == 0) return; try { - _running = true; - await _queue.Dequeue()(); + s_running = true; + await s_queue.Dequeue()(); } finally { - _running = false; + s_running = false; } TryExecuteNext(); } - static void ExecuteOnQueue(this Dispatcher dispatcher, Func cb) + private static void PostToTheQueue(this Dispatcher dispatcher, Func cb) { dispatcher.Post(() => { - _queue.Enqueue(cb); + 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() + .Result; + } + + 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 df5822dc01..c01dc5a350 100644 --- a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -12,6 +12,12 @@ + + + ExecutionQueue.cs + + + diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs index fd124aec75..b6c0cd11c4 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Threading; using System.Threading.Tasks; +using Avalonia.Headless.NUnit; using Avalonia.Threading; using Xunit.Abstractions; using Xunit.Sdk; @@ -34,16 +35,15 @@ internal class AvaloniaTestCase : XunitTestCase { var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly); - var task = session.Dispatcher.InvokeAsync>(async () => + // 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(); - }, default, cancellationTokenSource.Token).GetTask().Unwrap(); + }, cancellationTokenSource.Token); - // 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 = task.GetAwaiter().GetResult(); return Task.FromResult(runSummary); } } diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs index 454fe48e07..4b56de6e6c 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Threading; using System.Threading.Tasks; +using Avalonia.Headless.NUnit; using Xunit.Abstractions; using Xunit.Sdk; @@ -20,17 +21,15 @@ internal class AvaloniaTheoryTestCase : XunitTheoryTestCase { } - public override async Task RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) + public override Task RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) { var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly); - var task = session.Dispatcher.InvokeAsync>(async () => + return session.Dispatcher.InvokeOnQueueAsync(async () => { var runner = new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource); return await runner.RunAsync(); - }, default, cancellationTokenSource.Token).GetTask().Unwrap(); - - return await task; + }, cancellationTokenSource.Token); } } diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs index 3fcd509fe2..ebc3cd3e95 100644 --- a/tests/Avalonia.Headless.UnitTests/InputTests.cs +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -29,7 +29,7 @@ public class InputTests window.MouseDown(new Point(50, 50), MouseButton.Left); window.MouseUp(new Point(50, 50), MouseButton.Left); - + Assert.True(buttonClicked); } }