Browse Source

Reset application after each test

pull/11146/head
Max Katz 3 years ago
parent
commit
a3df4ad3b4
  1. 21
      src/Avalonia.Controls/AppBuilder.cs
  2. 31
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs
  3. 126
      src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs

21
src/Avalonia.Controls/AppBuilder.cs

@ -288,17 +288,26 @@ namespace Avalonia
} }
s_setupWasAlreadyCalled = true; s_setupWasAlreadyCalled = true;
SetupUnsafe();
}
/// <summary>
/// Setup method that doesn't check for input initalizers being set.
/// Nor
/// </summary>
internal void SetupUnsafe()
{
_optionsInitializers?.Invoke(); _optionsInitializers?.Invoke();
RuntimePlatformServicesInitializer(); RuntimePlatformServicesInitializer?.Invoke();
RenderingSubsystemInitializer(); RenderingSubsystemInitializer?.Invoke();
WindowingSubsystemInitializer(); WindowingSubsystemInitializer?.Invoke();
AfterPlatformServicesSetupCallback(Self); AfterPlatformServicesSetupCallback?.Invoke(Self);
Instance = _appFactory(); Instance = _appFactory!();
Instance.ApplicationLifetime = _lifetime; Instance.ApplicationLifetime = _lifetime;
AvaloniaLocator.CurrentMutable.BindToSelf(Instance); AvaloniaLocator.CurrentMutable.BindToSelf(Instance);
Instance.RegisterServices(); Instance.RegisterServices();
Instance.Initialize(); Instance.Initialize();
AfterSetupCallback(Self); AfterSetupCallback?.Invoke(Self);
Instance.OnFrameworkInitializationCompleted(); Instance.OnFrameworkInitializationCompleted();
} }
} }

31
src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Threading; using Avalonia.Threading;
@ -11,6 +12,8 @@ namespace Avalonia.Headless.NUnit;
internal class AvaloniaTestMethodCommand : DelegatingTestCommand internal class AvaloniaTestMethodCommand : DelegatingTestCommand
{ {
private readonly HeadlessUnitTestSession _session; private readonly HeadlessUnitTestSession _session;
private readonly List<Action> _beforeTest;
private readonly List<Action> _afterTest;
private static FieldInfo s_innerCommand = typeof(DelegatingTestCommand) private static FieldInfo s_innerCommand = typeof(DelegatingTestCommand)
.GetField("innerCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; .GetField("innerCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
@ -19,24 +22,35 @@ internal class AvaloniaTestMethodCommand : DelegatingTestCommand
private static FieldInfo s_afterTest = typeof(BeforeAndAfterTestCommand) private static FieldInfo s_afterTest = typeof(BeforeAndAfterTestCommand)
.GetField("AfterTest", BindingFlags.Instance | BindingFlags.NonPublic)!; .GetField("AfterTest", BindingFlags.Instance | BindingFlags.NonPublic)!;
private AvaloniaTestMethodCommand(HeadlessUnitTestSession session, TestCommand innerCommand) private AvaloniaTestMethodCommand(
HeadlessUnitTestSession session,
TestCommand innerCommand,
List<Action> beforeTest,
List<Action> afterTest)
: base(innerCommand) : base(innerCommand)
{ {
_session = session; _session = session;
_beforeTest = beforeTest;
_afterTest = afterTest;
} }
public static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command) public static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command)
{
return ProcessCommand(session, command, new List<Action>(), new List<Action>());
}
private static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command, List<Action> before, List<Action> after)
{ {
if (command is BeforeAndAfterTestCommand beforeAndAfterTestCommand) if (command is BeforeAndAfterTestCommand beforeAndAfterTestCommand)
{ {
if (s_beforeTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> beforeTest) if (s_beforeTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> beforeTest)
{ {
Action<TestExecutionContext> beforeAction = c => session.Dispatch(() => beforeTest(c), default); Action<TestExecutionContext> beforeAction = c => before.Add(() => beforeTest(c));
s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction); s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction);
} }
if (s_afterTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> afterTest) if (s_afterTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> afterTest)
{ {
Action<TestExecutionContext> afterAction = c => session.Dispatch(() => afterTest(c), default); Action<TestExecutionContext> afterAction = c => after.Add(() => afterTest(c));
s_afterTest.SetValue(beforeAndAfterTestCommand, afterAction); s_afterTest.SetValue(beforeAndAfterTestCommand, afterAction);
} }
} }
@ -44,11 +58,11 @@ internal class AvaloniaTestMethodCommand : DelegatingTestCommand
if (command is DelegatingTestCommand delegatingTestCommand if (command is DelegatingTestCommand delegatingTestCommand
&& s_innerCommand.GetValue(delegatingTestCommand) is TestCommand inner) && s_innerCommand.GetValue(delegatingTestCommand) is TestCommand inner)
{ {
s_innerCommand.SetValue(delegatingTestCommand, ProcessCommand(session, inner)); s_innerCommand.SetValue(delegatingTestCommand, ProcessCommand(session, inner, before, after));
} }
else if (command is TestMethodCommand methodCommand) else if (command is TestMethodCommand methodCommand)
{ {
return new AvaloniaTestMethodCommand(session, methodCommand); return new AvaloniaTestMethodCommand(session, methodCommand, before, after);
} }
return command; return command;
@ -62,6 +76,8 @@ internal class AvaloniaTestMethodCommand : DelegatingTestCommand
// Unfortunately, NUnit has issues with custom synchronization contexts, which means we need to add some hacks to make it work. // 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) private async Task<TestResult> ExecuteTestMethod(TestExecutionContext context)
{ {
_beforeTest.ForEach(a => a());
var testMethod = innerCommand.Test.Method; var testMethod = innerCommand.Test.Method;
var methodInfo = testMethod!.MethodInfo; var methodInfo = testMethod!.MethodInfo;
@ -81,6 +97,11 @@ internal class AvaloniaTestMethodCommand : DelegatingTestCommand
if (context.CurrentResult.AssertionResults.Count > 0) if (context.CurrentResult.AssertionResults.Count > 0)
context.CurrentResult.RecordTestCompletion(); context.CurrentResult.RecordTestCompletion();
if (context.ExecutionStatus != TestExecutionStatus.AbortRequested)
{
_afterTest.ForEach(a => a());
}
return context.CurrentResult; return context.CurrentResult;
} }
} }

126
src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs

@ -1,10 +1,14 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Reactive;
using Avalonia.Rendering;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.Headless; namespace Avalonia.Headless;
@ -15,14 +19,12 @@ namespace Avalonia.Headless;
/// to keep execution flow on the UI thread. /// to keep execution flow on the UI thread.
/// Disposing unit test session stops internal dispatcher loop. /// Disposing unit test session stops internal dispatcher loop.
/// </summary> /// </summary>
/// <remarks>
/// As Avalonia supports only a single Application instance created, this session must be created only once as well.
/// </remarks>
public sealed class HeadlessUnitTestSession : IDisposable public sealed class HeadlessUnitTestSession : IDisposable
{ {
private static readonly ConcurrentDictionary<Assembly, HeadlessUnitTestSession> s_session = new();
private readonly AppBuilder _appBuilder;
private readonly CancellationTokenSource _cancellationTokenSource; private readonly CancellationTokenSource _cancellationTokenSource;
private static HeadlessUnitTestSession? s_session;
private static object s_lock = new();
private readonly BlockingCollection<Action> _queue; private readonly BlockingCollection<Action> _queue;
private readonly Task _dispatchTask; private readonly Task _dispatchTask;
@ -30,27 +32,41 @@ public sealed class HeadlessUnitTestSession : IDisposable
DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicParameterlessConstructor; DynamicallyAccessedMemberTypes.PublicParameterlessConstructor;
private HeadlessUnitTestSession(Type entryPointType, CancellationTokenSource cancellationTokenSource, BlockingCollection<Action> queue, Task _dispatchTask) private HeadlessUnitTestSession(AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource,
BlockingCollection<Action> queue, Task dispatchTask)
{ {
_appBuilder = appBuilder;
_cancellationTokenSource = cancellationTokenSource; _cancellationTokenSource = cancellationTokenSource;
_queue = queue; _queue = queue;
this._dispatchTask = _dispatchTask; _dispatchTask = dispatchTask;
EntryPointType = entryPointType;
} }
internal Type EntryPointType { get; } /// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
public Task Dispatch(Action action, CancellationToken cancellationToken) public Task Dispatch(Action action, CancellationToken cancellationToken)
{ {
return Dispatch(() => { action(); return Task.FromResult(0); }, cancellationToken); return Dispatch(() =>
{
action();
return Task.FromResult(0);
}, cancellationToken);
} }
/// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
public Task<TResult> Dispatch<TResult>(Func<TResult> action, CancellationToken cancellationToken) public Task<TResult> Dispatch<TResult>(Func<TResult> action, CancellationToken cancellationToken)
{ {
return Dispatch(() => Task.FromResult(action()), cancellationToken); return Dispatch(() => Task.FromResult(action()), cancellationToken);
} }
/// <summary>
/// Dispatch method queues an async operation on the dispatcher thread, creates a new application instance,
/// setting app avalonia services, and runs <see cref="action"/> parameter.
/// </summary>
/// <param name="action">Action to execute on the dispatcher thread with avalonia services.</param>
/// <param name="cancellationToken">Cancellation token to cancel execution.</param>
/// <exception cref="ObjectDisposedException">
/// If global session was already cancelled and thread killed, it's not possible to dispatch any actions again
/// </exception>
public Task<TResult> Dispatch<TResult>(Func<Task<TResult>> action, CancellationToken cancellationToken) public Task<TResult> Dispatch<TResult>(Func<Task<TResult>> action, CancellationToken cancellationToken)
{ {
if (_cancellationTokenSource.IsCancellationRequested) if (_cancellationTokenSource.IsCancellationRequested)
@ -63,6 +79,8 @@ public sealed class HeadlessUnitTestSession : IDisposable
var tcs = new TaskCompletionSource<TResult>(); var tcs = new TaskCompletionSource<TResult>();
_queue.Add(() => _queue.Add(() =>
{ {
using var application = EnsureApplication();
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
using var globalCts = token.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true); using var globalCts = token.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true);
using var localCts = cancellationToken.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true); using var localCts = cancellationToken.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true);
@ -92,7 +110,28 @@ public sealed class HeadlessUnitTestSession : IDisposable
}); });
return tcs.Task; return tcs.Task;
} }
private IDisposable EnsureApplication()
{
var scope = AvaloniaLocator.EnterScope();
try
{
Dispatcher.ResetForUnitTests();
_appBuilder.SetupUnsafe();
}
catch
{
scope.Dispose();
throw;
}
return Disposable.Create(() =>
{
scope.Dispose();
Dispatcher.ResetForUnitTests();
});
}
public void Dispose() public void Dispose()
{ {
_cancellationTokenSource.Cancel(); _cancellationTokenSource.Cancel();
@ -101,19 +140,6 @@ public sealed class HeadlessUnitTestSession : IDisposable
_cancellationTokenSource.Dispose(); _cancellationTokenSource.Dispose();
} }
/// <summary>
/// Creates instance of <see cref="HeadlessUnitTestSession"/>.
/// </summary>
/// <typeparam name="TEntryPointType">
/// Parameter from which <see cref="AppBuilder"/> should be created.
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </typeparam>
public static HeadlessUnitTestSession StartNew<
[DynamicallyAccessedMembers(DynamicallyAccessed)] TEntryPointType>()
{
return StartNew(typeof(TEntryPointType));
}
/// <summary> /// <summary>
/// Creates instance of <see cref="HeadlessUnitTestSession"/>. /// Creates instance of <see cref="HeadlessUnitTestSession"/>.
/// </summary> /// </summary>
@ -122,7 +148,8 @@ public sealed class HeadlessUnitTestSession : IDisposable
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </param> /// </param>
public static HeadlessUnitTestSession StartNew( public static HeadlessUnitTestSession StartNew(
[DynamicallyAccessedMembers(DynamicallyAccessed)] Type entryPointType) [DynamicallyAccessedMembers(DynamicallyAccessed)]
Type entryPointType)
{ {
var tcs = new TaskCompletionSource<HeadlessUnitTestSession>(); var tcs = new TaskCompletionSource<HeadlessUnitTestSession>();
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
@ -141,15 +168,8 @@ public sealed class HeadlessUnitTestSession : IDisposable
appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions());
} }
appBuilder.SetupWithoutStarting();
if (!Dispatcher.UIThread.SupportsRunLoops)
{
throw new InvalidOperationException("Avalonia Headless platform has failed to initialize.");
}
// ReSharper disable once AccessToModifiedClosure // ReSharper disable once AccessToModifiedClosure
tcs.SetResult(new HeadlessUnitTestSession(entryPointType, cancellationTokenSource, queue, task!)); tcs.SetResult(new HeadlessUnitTestSession(appBuilder, cancellationTokenSource, queue, task!));
} }
catch (Exception e) catch (Exception e)
{ {
@ -166,7 +186,6 @@ public sealed class HeadlessUnitTestSession : IDisposable
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
} }
} }
}); });
@ -178,34 +197,17 @@ public sealed class HeadlessUnitTestSession : IDisposable
/// Creates a session from AvaloniaTestApplicationAttribute attribute or reuses any existing. /// Creates a session from AvaloniaTestApplicationAttribute attribute or reuses any existing.
/// If AvaloniaTestApplicationAttribute doesn't exist, empty application is used. /// If AvaloniaTestApplicationAttribute doesn't exist, empty application is used.
/// </summary> /// </summary>
/// <remarks> [UnconditionalSuppressMessage("Trimming", "IL2072",
/// Note, only single session can be crated per app execution. Justification = "AvaloniaTestApplicationAttribute attribute should preserve type information.")]
/// </remarks>
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "AvaloniaTestApplicationAttribute attribute should preserve type information.")]
public static HeadlessUnitTestSession GetOrStartForAssembly(Assembly? assembly) public static HeadlessUnitTestSession GetOrStartForAssembly(Assembly? assembly)
{ {
lock (s_lock) return s_session.GetOrAdd(assembly ?? typeof(HeadlessUnitTestSession).Assembly, a =>
{ {
var appBuilderEntryPointType = assembly?.GetCustomAttribute<AvaloniaTestApplicationAttribute>() var appBuilderEntryPointType = a.GetCustomAttribute<AvaloniaTestApplicationAttribute>()
?.AppBuilderEntryPointType; ?.AppBuilderEntryPointType;
return appBuilderEntryPointType is not null ?
if (s_session is not null) StartNew(appBuilderEntryPointType) :
{ StartNew(typeof(Application));
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(
"AvaloniaTestApplicationAttribute must be defined only once per single unit tests session.");
}
return s_session;
}
s_session = appBuilderEntryPointType is not null ? StartNew(appBuilderEntryPointType) : StartNew(typeof(Application));
return s_session;
}
} }
} }

Loading…
Cancel
Save