|
|
@@ -18,7 +18,6 @@ namespace Avalonia.Headless;
|
|
|
/// All UI tests are supposed to be executed from one of the <see cref="Dispatch"/> methods to keep execution flow on the UI thread.
|
|
|
/// Disposing unit test session stops internal dispatcher loop.
|
|
|
/// </summary>
|
|
|
-[Unstable("This API is experimental and might be unstable. Use on your risk. API might or might not be changed in a minor update.")]
|
|
|
public sealed class HeadlessUnitTestSession : IDisposable
|
|
|
{
|
|
|
private static readonly Dictionary<Assembly, HeadlessUnitTestSession> s_session = new();
|
|
|
@@ -27,19 +26,25 @@ public sealed class HeadlessUnitTestSession : IDisposable
|
|
|
private readonly CancellationTokenSource _cancellationTokenSource;
|
|
|
private readonly BlockingCollection<(Action, ExecutionContext?)> _queue;
|
|
|
private readonly Task _dispatchTask;
|
|
|
+ private readonly bool _isolated;
|
|
|
+ // Only set and used with PerAssembly isolation
|
|
|
+ private SynchronizationContext? _sharedContext;
|
|
|
|
|
|
internal const DynamicallyAccessedMemberTypes DynamicallyAccessed =
|
|
|
DynamicallyAccessedMemberTypes.PublicMethods |
|
|
|
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
|
|
DynamicallyAccessedMemberTypes.PublicParameterlessConstructor;
|
|
|
|
|
|
- private HeadlessUnitTestSession(AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource,
|
|
|
- BlockingCollection<(Action, ExecutionContext?)> queue, Task dispatchTask)
|
|
|
+ private HeadlessUnitTestSession(
|
|
|
+ AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource,
|
|
|
+ BlockingCollection<(Action, ExecutionContext?)> queue, Task dispatchTask,
|
|
|
+ bool isolated)
|
|
|
{
|
|
|
_appBuilder = appBuilder;
|
|
|
_cancellationTokenSource = cancellationTokenSource;
|
|
|
_queue = queue;
|
|
|
_dispatchTask = dispatchTask;
|
|
|
+ _isolated = isolated;
|
|
|
}
|
|
|
|
|
|
/// <inheritdoc cref="DispatchCore{TResult}"/>
|
|
|
@@ -93,7 +98,9 @@ public sealed class HeadlessUnitTestSession : IDisposable
|
|
|
|
|
|
try
|
|
|
{
|
|
|
- using var application = EnsureApplication();
|
|
|
+ using var application = _isolated
|
|
|
+ ? EnsureIsolatedApplication()
|
|
|
+ : EnsureSharedApplication();
|
|
|
var task = action();
|
|
|
if (task.Status != TaskStatus.RanToCompletion)
|
|
|
{
|
|
|
@@ -123,7 +130,27 @@ public sealed class HeadlessUnitTestSession : IDisposable
|
|
|
return tcs.Task;
|
|
|
}
|
|
|
|
|
|
- private IDisposable EnsureApplication()
|
|
|
+ private IDisposable EnsureSharedApplication()
|
|
|
+ {
|
|
|
+ var oldContext = SynchronizationContext.Current;
|
|
|
+ if (Application.Current is null)
|
|
|
+ {
|
|
|
+ _appBuilder.SetupUnsafe();
|
|
|
+ _sharedContext = SynchronizationContext.Current;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ SynchronizationContext.SetSynchronizationContext(_sharedContext);
|
|
|
+ }
|
|
|
+
|
|
|
+ return Disposable.Create(() =>
|
|
|
+ {
|
|
|
+ Dispatcher.UIThread.RunJobs();
|
|
|
+ SynchronizationContext.SetSynchronizationContext(oldContext);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private IDisposable EnsureIsolatedApplication()
|
|
|
{
|
|
|
var scope = AvaloniaLocator.EnterScope();
|
|
|
var oldContext = SynchronizationContext.Current;
|
|
|
@@ -167,6 +194,24 @@ public sealed class HeadlessUnitTestSession : IDisposable
|
|
|
public static HeadlessUnitTestSession StartNew(
|
|
|
[DynamicallyAccessedMembers(DynamicallyAccessed)]
|
|
|
Type entryPointType)
|
|
|
+ {
|
|
|
+ // Cannot be optional parameter for ABI stability
|
|
|
+ // ReSharper disable once IntroduceOptionalParameters.Global
|
|
|
+ return StartNew(entryPointType, AvaloniaTestIsolationLevel.PerTest);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Creates instance of <see cref="HeadlessUnitTestSession"/>.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="entryPointType">
|
|
|
+ /// Parameter from which <see cref="AppBuilder"/> should be created.
|
|
|
+ /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
|
|
|
+ /// </param>
|
|
|
+ /// <param name="isolationLevel">Defines the isolation level for headless unit tests</param>
|
|
|
+ public static HeadlessUnitTestSession StartNew(
|
|
|
+ [DynamicallyAccessedMembers(DynamicallyAccessed)]
|
|
|
+ Type entryPointType,
|
|
|
+ AvaloniaTestIsolationLevel isolationLevel)
|
|
|
{
|
|
|
var tcs = new TaskCompletionSource<HeadlessUnitTestSession>();
|
|
|
var cancellationTokenSource = new CancellationTokenSource();
|
|
|
@@ -178,6 +223,7 @@ public sealed class HeadlessUnitTestSession : IDisposable
|
|
|
try
|
|
|
{
|
|
|
var appBuilder = AppBuilder.Configure(entryPointType);
|
|
|
+ var runIsolated = isolationLevel == AvaloniaTestIsolationLevel.PerTest;
|
|
|
|
|
|
// If windowing subsystem wasn't initialized by user, force headless with default parameters.
|
|
|
if (appBuilder.WindowingSubsystemName != "Headless")
|
|
|
@@ -186,7 +232,7 @@ public sealed class HeadlessUnitTestSession : IDisposable
|
|
|
}
|
|
|
|
|
|
// ReSharper disable once AccessToModifiedClosure
|
|
|
- tcs.SetResult(new HeadlessUnitTestSession(appBuilder, cancellationTokenSource, queue, task!));
|
|
|
+ tcs.SetResult(new HeadlessUnitTestSession(appBuilder, cancellationTokenSource, queue, task!, runIsolated));
|
|
|
}
|
|
|
catch (Exception e)
|
|
|
{
|
|
|
@@ -234,9 +280,12 @@ public sealed class HeadlessUnitTestSession : IDisposable
|
|
|
var appBuilderEntryPointType = assembly.GetCustomAttribute<AvaloniaTestApplicationAttribute>()
|
|
|
?.AppBuilderEntryPointType;
|
|
|
|
|
|
+ var isolationLevel = assembly.GetCustomAttribute<AvaloniaTestIsolationAttribute>()
|
|
|
+ ?.IsolationLevel ?? AvaloniaTestIsolationLevel.PerTest;
|
|
|
+
|
|
|
session = appBuilderEntryPointType is not null ?
|
|
|
- StartNew(appBuilderEntryPointType) :
|
|
|
- StartNew(typeof(Application));
|
|
|
+ StartNew(appBuilderEntryPointType, isolationLevel) :
|
|
|
+ StartNew(typeof(Application), isolationLevel);
|
|
|
|
|
|
s_session.Add(assembly, session);
|
|
|
}
|