Browse Source

Use ExecutionQueue in test frameworks

pull/11169/head
Max Katz 3 years ago
parent
commit
ae04033c76
  1. 47
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTestCommand.cs
  2. 54
      src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs
  3. 6
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  4. 10
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs
  5. 9
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs
  6. 2
      tests/Avalonia.Headless.UnitTests/InputTests.cs

47
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<Task<TestResult>>(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<TestResult> 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;
}
}

54
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<Func<Task>> _queue = new();
static async void TryExecuteNext()
private static bool s_running;
private static Queue<Func<Task>> 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<Task> cb)
private static void PostToTheQueue(this Dispatcher dispatcher, Func<Task> cb)
{
dispatcher.Post(() =>
{
_queue.Enqueue(cb);
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()
.Result;
}
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();
}
}

6
src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj

@ -12,6 +12,12 @@
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Headless.NUnit\ExecutionQueue.cs">
<Link>ExecutionQueue.cs</Link>
</Compile>
</ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\NullableEnable.props" />

10
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<Task<RunSummary>>(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);
}
}

9
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<RunSummary> RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
public override Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
{
var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly);
var task = session.Dispatcher.InvokeAsync<Task<RunSummary>>(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);
}
}

2
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);
}
}

Loading…
Cancel
Save