Browse Source

Fix HeadlessUnitTestSession creation race condition (#12979)

* Fix HeadlessUnitTestSession creation race condition

* Bind Compositor/MediaContext to a fixed UI thread

* Fix dead lock in AvaloniaTestCase

* Rename Compositor.UIThreadDispatcher to Dispatcher
release/11.0.5-rc1
Julien Lebosquain 2 years ago
committed by Steven Kirk
parent
commit
183e37d0fc
  1. 8
      src/Avalonia.Base/Media/MediaContext.Clock.cs
  2. 6
      src/Avalonia.Base/Media/MediaContext.Compositor.cs
  3. 13
      src/Avalonia.Base/Media/MediaContext.cs
  4. 2
      src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs
  5. 23
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  6. 1
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  7. 11
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs
  8. 29
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs
  9. 36
      src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
  10. 1
      tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs
  11. 2
      tests/Avalonia.RenderTests/TestBase.cs
  12. 2
      tests/Avalonia.UnitTests/CompositorTestServices.cs
  13. 2
      tests/Avalonia.UnitTests/RendererMocks.cs

8
src/Avalonia.Base/Media/MediaContext.Clock.cs

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Media; namespace Avalonia.Media;
@ -33,12 +31,12 @@ internal partial class MediaContext
public IDisposable Subscribe(IObserver<TimeSpan> observer) public IDisposable Subscribe(IObserver<TimeSpan> observer)
{ {
_parent.ScheduleRender(false); _parent.ScheduleRender(false);
Dispatcher.UIThread.VerifyAccess(); _parent._dispatcher.VerifyAccess();
_observers.Add(observer); _observers.Add(observer);
_newObservers.Add(observer); _newObservers.Add(observer);
return Disposable.Create(() => return Disposable.Create(() =>
{ {
Dispatcher.UIThread.VerifyAccess(); _parent._dispatcher.VerifyAccess();
_observers.Remove(observer); _observers.Remove(observer);
}); });
} }
@ -79,4 +77,4 @@ internal partial class MediaContext
} }
public void RequestAnimationFrame(Action<TimeSpan> action) => _clock.RequestAnimationFrame(action); public void RequestAnimationFrame(Action<TimeSpan> action) => _clock.RequestAnimationFrame(action);
} }

6
src/Avalonia.Base/Media/MediaContext.Compositor.cs

@ -1,6 +1,4 @@
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Transport; using Avalonia.Rendering.Composition.Transport;
@ -22,7 +20,7 @@ partial class MediaContext
_requestedCommits.Remove(compositor); _requestedCommits.Remove(compositor);
_pendingCompositionBatches[compositor] = commit; _pendingCompositionBatches[compositor] = commit;
commit.Processed.ContinueWith(_ => commit.Processed.ContinueWith(_ =>
Dispatcher.UIThread.Post(() => CompositionBatchFinished(compositor, commit), DispatcherPriority.Send)); _dispatcher.Post(() => CompositionBatchFinished(compositor, commit), DispatcherPriority.Send));
return commit; return commit;
} }
@ -147,4 +145,4 @@ partial class MediaContext
// TODO: maybe skip the full render here? // TODO: maybe skip the full render here?
ScheduleRender(true); ScheduleRender(true);
} }
} }

13
src/Avalonia.Base/Media/MediaContext.cs

@ -1,8 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
@ -23,6 +20,7 @@ internal partial class MediaContext : ICompositorScheduler
private readonly Action _inputMarkerHandler; private readonly Action _inputMarkerHandler;
private readonly HashSet<Compositor> _requestedCommits = new(); private readonly HashSet<Compositor> _requestedCommits = new();
private readonly Dictionary<Compositor, CompositionBatch> _pendingCompositionBatches = new(); private readonly Dictionary<Compositor, CompositionBatch> _pendingCompositionBatches = new();
private readonly Dispatcher _dispatcher;
private record TopLevelInfo(Compositor Compositor, CompositingRenderer Renderer, ILayoutManager LayoutManager); private record TopLevelInfo(Compositor Compositor, CompositingRenderer Renderer, ILayoutManager LayoutManager);
private List<Action>? _invokeOnRenderCallbacks; private List<Action>? _invokeOnRenderCallbacks;
@ -38,11 +36,12 @@ internal partial class MediaContext : ICompositorScheduler
private Dictionary<object, TopLevelInfo> _topLevels = new(); private Dictionary<object, TopLevelInfo> _topLevels = new();
private MediaContext() private MediaContext(Dispatcher dispatcher)
{ {
_render = Render; _render = Render;
_inputMarkerHandler = InputMarkerHandler; _inputMarkerHandler = InputMarkerHandler;
_clock = new(this); _clock = new(this);
_dispatcher = dispatcher;
_animationsTimer.Tick += (_, _) => _animationsTimer.Tick += (_, _) =>
{ {
_animationsTimer.Stop(); _animationsTimer.Stop();
@ -58,7 +57,7 @@ internal partial class MediaContext : ICompositorScheduler
// and need to do a full reset for unit tests // and need to do a full reset for unit tests
var context = AvaloniaLocator.Current.GetService<MediaContext>(); var context = AvaloniaLocator.Current.GetService<MediaContext>();
if (context == null) if (context == null)
AvaloniaLocator.CurrentMutable.Bind<MediaContext>().ToConstant(context = new()); AvaloniaLocator.CurrentMutable.Bind<MediaContext>().ToConstant(context = new(Dispatcher.UIThread));
return context; return context;
} }
} }
@ -84,7 +83,7 @@ internal partial class MediaContext : ICompositorScheduler
if (_inputMarkerOp == null) if (_inputMarkerOp == null)
{ {
_inputMarkerOp = Dispatcher.UIThread.InvokeAsync(_inputMarkerHandler, DispatcherPriority.Input); _inputMarkerOp = _dispatcher.InvokeAsync(_inputMarkerHandler, DispatcherPriority.Input);
_inputMarkerAddedAt = _time.Elapsed; _inputMarkerAddedAt = _time.Elapsed;
} }
else if (!now && (_time.Elapsed - _inputMarkerAddedAt).TotalSeconds > MaxSecondsWithoutInput) else if (!now && (_time.Elapsed - _inputMarkerAddedAt).TotalSeconds > MaxSecondsWithoutInput)
@ -93,7 +92,7 @@ internal partial class MediaContext : ICompositorScheduler
} }
_nextRenderOp = Dispatcher.UIThread.InvokeAsync(_render, priority); _nextRenderOp = _dispatcher.InvokeAsync(_render, priority);
} }
/// <summary> /// <summary>

2
src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs

@ -56,7 +56,7 @@ public class CompositionDrawingSurface : CompositionSurface, IDisposable
~CompositionDrawingSurface() ~CompositionDrawingSurface()
{ {
Dispatcher.UIThread.Post(Dispose); Compositor.Dispatcher.Post(Dispose);
} }
public new void Dispose() => base.Dispose(); public new void Dispose() => base.Dispose();

23
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -1,13 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Animation.Easings; using Avalonia.Animation.Easings;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport; using Avalonia.Rendering.Composition.Transport;
using Avalonia.Threading; using Avalonia.Threading;
@ -40,6 +37,8 @@ namespace Avalonia.Rendering.Composition
internal IEasing DefaultEasing { get; } internal IEasing DefaultEasing { get; }
internal Dispatcher Dispatcher { get; }
private DiagnosticTextRenderer? DiagnosticTextRenderer private DiagnosticTextRenderer? DiagnosticTextRenderer
{ {
get get
@ -69,16 +68,18 @@ namespace Avalonia.Rendering.Composition
} }
internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false) internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits = false)
: this(loop, gpu, useUiThreadForSynchronousCommits, MediaContext.Instance, false) : this(loop, gpu, useUiThreadForSynchronousCommits, MediaContext.Instance, false, Dispatcher.UIThread)
{ {
} }
internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu, internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu,
bool useUiThreadForSynchronousCommits, bool useUiThreadForSynchronousCommits,
ICompositorScheduler scheduler, bool reclaimBuffersImmediately) ICompositorScheduler scheduler, bool reclaimBuffersImmediately,
Dispatcher dispatcher)
{ {
Loop = loop; Loop = loop;
UseUiThreadForSynchronousCommits = useUiThreadForSynchronousCommits; UseUiThreadForSynchronousCommits = useUiThreadForSynchronousCommits;
Dispatcher = dispatcher;
_batchMemoryPool = new(reclaimBuffersImmediately); _batchMemoryPool = new(reclaimBuffersImmediately);
_batchObjectPool = new(reclaimBuffersImmediately); _batchObjectPool = new(reclaimBuffersImmediately);
_server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool);
@ -99,14 +100,14 @@ namespace Avalonia.Rendering.Composition
/// <returns>A CompositionBatch object that provides batch lifetime information</returns> /// <returns>A CompositionBatch object that provides batch lifetime information</returns>
public CompositionBatch RequestCompositionBatchCommitAsync() public CompositionBatch RequestCompositionBatchCommitAsync()
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.VerifyAccess();
if (_nextCommit == null) if (_nextCommit == null)
{ {
_nextCommit = new (); _nextCommit = new ();
var pending = _pendingBatch; var pending = _pendingBatch;
if (pending != null) if (pending != null)
pending.Processed.ContinueWith( pending.Processed.ContinueWith(
_ => Dispatcher.UIThread.Post(_triggerCommitRequested, DispatcherPriority.Send)); _ => Dispatcher.Post(_triggerCommitRequested, DispatcherPriority.Send));
else else
_triggerCommitRequested(); _triggerCommitRequested();
} }
@ -130,7 +131,7 @@ namespace Avalonia.Rendering.Composition
CompositionBatch CommitCore() CompositionBatch CommitCore()
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.VerifyAccess();
using var noPump = NonPumpingLockHelper.Use(); using var noPump = NonPumpingLockHelper.Use();
var commit = _nextCommit ??= new(); var commit = _nextCommit ??= new();
@ -197,7 +198,7 @@ namespace Avalonia.Rendering.Composition
internal void RegisterForSerialization(ICompositorSerializable compositionObject) internal void RegisterForSerialization(ICompositorSerializable compositionObject)
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.VerifyAccess();
if(_objectSerializationHashSet.Add(compositionObject)) if(_objectSerializationHashSet.Add(compositionObject))
_objectSerializationQueue.Enqueue(compositionObject); _objectSerializationQueue.Enqueue(compositionObject);
RequestCommitAsync(); RequestCommitAsync();
@ -217,14 +218,14 @@ namespace Avalonia.Rendering.Composition
/// </summary> /// </summary>
public void RequestCompositionUpdate(Action action) public void RequestCompositionUpdate(Action action)
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.VerifyAccess();
_invokeBeforeCommitWrite.Enqueue(action); _invokeBeforeCommitWrite.Enqueue(action);
RequestCommitAsync(); RequestCommitAsync();
} }
internal void PostServerJob(Action job) internal void PostServerJob(Action job)
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.VerifyAccess();
_pendingServerCompositorJobs.Add(job); _pendingServerCompositorJobs.Add(job);
RequestCommitAsync(); RequestCommitAsync();
} }

1
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

@ -108,6 +108,7 @@ public partial class Dispatcher
job = s_uiThread._queue.Peek(); job = s_uiThread._queue.Peek();
if (job == null || job.Priority <= DispatcherPriority.Inactive) if (job == null || job.Priority <= DispatcherPriority.Inactive)
{ {
s_uiThread.ShutdownImpl();
s_uiThread = null; s_uiThread = null;
return; return;
} }

11
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs

@ -1,9 +1,7 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Abstractions; using Xunit.Abstractions;
using Xunit.Sdk; using Xunit.Sdk;
@ -37,10 +35,11 @@ internal class AvaloniaTestCase : XunitTestCase
// We need to block the XUnit thread to ensure its concurrency throttle is effective. // 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. // See https://github.com/AArnott/Xunit.StaFact/pull/55#issuecomment-826187354 for details.
var runSummary = AvaloniaTestCaseRunner var runSummary =
.RunTest(session, this, DisplayName, SkipReason, constructorArguments, Task.Run(() => AvaloniaTestCaseRunner.RunTest(session, this, DisplayName, SkipReason, constructorArguments,
TestMethodArguments, messageBus, aggregator, cancellationTokenSource) TestMethodArguments, messageBus, aggregator, cancellationTokenSource))
.GetAwaiter().GetResult(); .GetAwaiter()
.GetResult();
return Task.FromResult(runSummary); return Task.FromResult(runSummary);
} }

29
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs

@ -11,15 +11,17 @@ namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestCaseRunner : XunitTestCaseRunner internal class AvaloniaTestCaseRunner : XunitTestCaseRunner
{ {
private readonly HeadlessUnitTestSession _session;
private readonly Action? _onAfterTestInvoked; private readonly Action? _onAfterTestInvoked;
public AvaloniaTestCaseRunner( public AvaloniaTestCaseRunner(
Action? onAfterTestInvoked, HeadlessUnitTestSession session, Action? onAfterTestInvoked,
IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments,
object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments, CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments,
testMethodArguments, messageBus, aggregator, cancellationTokenSource) testMethodArguments, messageBus, aggregator, cancellationTokenSource)
{ {
_session = session;
_onAfterTestInvoked = onAfterTestInvoked; _onAfterTestInvoked = onAfterTestInvoked;
} }
@ -29,43 +31,46 @@ internal class AvaloniaTestCaseRunner : XunitTestCaseRunner
CancellationTokenSource cancellationTokenSource) CancellationTokenSource cancellationTokenSource)
{ {
var afterTest = () => Dispatcher.UIThread.RunJobs(); var afterTest = () => Dispatcher.UIThread.RunJobs();
return session.Dispatch(async () =>
{ var runner = new AvaloniaTestCaseRunner(session, afterTest, testCase, displayName,
var runner = new AvaloniaTestCaseRunner(afterTest, testCase, displayName, skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource);
skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource); return runner.RunAsync();
return await runner.RunAsync();
}, cancellationTokenSource.Token);
} }
protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass, protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass,
object[] constructorArguments, object[] constructorArguments,
MethodInfo testMethod, object[] testMethodArguments, string skipReason, MethodInfo testMethod, object[] testMethodArguments, string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
{ {
return new AvaloniaTestRunner(_onAfterTestInvoked, test, messageBus, testClass, constructorArguments, return new AvaloniaTestRunner(_session, _onAfterTestInvoked, test, messageBus, testClass, constructorArguments,
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource); testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource);
} }
private class AvaloniaTestRunner : XunitTestRunner private class AvaloniaTestRunner : XunitTestRunner
{ {
private readonly HeadlessUnitTestSession _session;
private readonly Action? _onAfterTestInvoked; private readonly Action? _onAfterTestInvoked;
public AvaloniaTestRunner( public AvaloniaTestRunner(
Action? onAfterTestInvoked, HeadlessUnitTestSession session, Action? onAfterTestInvoked,
ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod,
object[] testMethodArguments, string skipReason, object[] testMethodArguments, string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, ExceptionAggregator aggregator, IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments, CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments,
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource) testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource)
{ {
_session = session;
_onAfterTestInvoked = onAfterTestInvoked; _onAfterTestInvoked = onAfterTestInvoked;
} }
protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator) protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
{ {
return new AvaloniaTestInvoker(_onAfterTestInvoked, Test, MessageBus, TestClass, ConstructorArguments, return _session.Dispatch(
TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync(); () => new AvaloniaTestInvoker(_onAfterTestInvoked, Test, MessageBus, TestClass,
ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator,
CancellationTokenSource).RunAsync(),
CancellationTokenSource.Token);
} }
} }

36
src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs

@ -3,13 +3,10 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Rendering;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.Headless; namespace Avalonia.Headless;
@ -22,7 +19,7 @@ namespace Avalonia.Headless;
[Unstable("This API is experimental and might be unstable. Use on your risk. API might or might not be changed in a minor update.")] [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 public sealed class HeadlessUnitTestSession : IDisposable
{ {
private static readonly ConcurrentDictionary<Assembly, HeadlessUnitTestSession> s_session = new(); private static readonly Dictionary<Assembly, HeadlessUnitTestSession> s_session = new();
private readonly AppBuilder _appBuilder; private readonly AppBuilder _appBuilder;
private readonly CancellationTokenSource _cancellationTokenSource; private readonly CancellationTokenSource _cancellationTokenSource;
@ -61,7 +58,7 @@ public sealed class HeadlessUnitTestSession : IDisposable
/// <summary> /// <summary>
/// Dispatch method queues an async operation on the dispatcher thread, creates a new application instance, /// 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. /// setting app avalonia services, and runs <paramref name="action"/> parameter.
/// </summary> /// </summary>
/// <param name="action">Action to execute on the dispatcher thread with avalonia services.</param> /// <param name="action">Action to execute on the dispatcher thread with avalonia services.</param>
/// <param name="cancellationToken">Cancellation token to cancel execution.</param> /// <param name="cancellationToken">Cancellation token to cancel execution.</param>
@ -80,20 +77,21 @@ 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);
try try
{ {
using var application = EnsureApplication();
var task = action(); var task = action();
task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts, task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts,
TaskScheduler.FromCurrentSynchronizationContext()); TaskScheduler.FromCurrentSynchronizationContext());
if (cts.IsCancellationRequested) if (cts.IsCancellationRequested)
{ {
tcs.TrySetCanceled(cts.Token);
return; return;
} }
@ -202,13 +200,23 @@ public sealed class HeadlessUnitTestSession : IDisposable
Justification = "AvaloniaTestApplicationAttribute attribute should preserve type information.")] Justification = "AvaloniaTestApplicationAttribute attribute should preserve type information.")]
public static HeadlessUnitTestSession GetOrStartForAssembly(Assembly? assembly) public static HeadlessUnitTestSession GetOrStartForAssembly(Assembly? assembly)
{ {
return s_session.GetOrAdd(assembly ?? typeof(HeadlessUnitTestSession).Assembly, a => assembly ??= typeof(HeadlessUnitTestSession).Assembly;
lock (s_session)
{ {
var appBuilderEntryPointType = a.GetCustomAttribute<AvaloniaTestApplicationAttribute>() if (!s_session.TryGetValue(assembly, out var session))
?.AppBuilderEntryPointType; {
return appBuilderEntryPointType is not null ? var appBuilderEntryPointType = assembly.GetCustomAttribute<AvaloniaTestApplicationAttribute>()
StartNew(appBuilderEntryPointType) : ?.AppBuilderEntryPointType;
StartNew(typeof(Application));
}); session = appBuilderEntryPointType is not null ?
StartNew(appBuilderEntryPointType) :
StartNew(typeof(Application));
s_session.Add(assembly, session);
}
return session;
}
} }
} }

1
tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs

@ -2,6 +2,5 @@
global using Avalonia.Headless.XUnit; global using Avalonia.Headless.XUnit;
using Avalonia.Headless; using Avalonia.Headless;
using Avalonia.Headless.UnitTests; using Avalonia.Headless.UnitTests;
using Avalonia.Headless.XUnit;
[assembly: AvaloniaTestApplication(typeof(TestApplication))] [assembly: AvaloniaTestApplication(typeof(TestApplication))]

2
tests/Avalonia.RenderTests/TestBase.cs

@ -108,7 +108,7 @@ namespace Avalonia.Direct2D1.RenderTests
var timer = new ManualRenderTimer(); var timer = new ManualRenderTimer();
var compositor = new Compositor(new RenderLoop(timer), null, true, var compositor = new Compositor(new RenderLoop(timer), null, true,
new DispatcherCompositorScheduler(), true); new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat)) using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat))
{ {
var root = new TestRenderRoot(dpiVector.X / 96, null!); var root = new TestRenderRoot(dpiVector.X / 96, null!);

2
tests/Avalonia.UnitTests/CompositorTestServices.cs

@ -46,7 +46,7 @@ public class CompositorTestServices : IDisposable
AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(Timer); AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(Timer);
Compositor = new Compositor(new RenderLoop(Timer), null, Compositor = new Compositor(new RenderLoop(Timer), null,
true, new DispatcherCompositorScheduler(), true); true, new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
var impl = new TopLevelImpl(Compositor, size ?? new Size(1000, 1000)); var impl = new TopLevelImpl(Compositor, size ?? new Size(1000, 1000));
TopLevel = new EmbeddableControlRoot(impl) TopLevel = new EmbeddableControlRoot(impl)
{ {

2
tests/Avalonia.UnitTests/RendererMocks.cs

@ -18,7 +18,7 @@ namespace Avalonia.UnitTests
public static Compositor CreateDummyCompositor() => public static Compositor CreateDummyCompositor() =>
new(new RenderLoop(new CompositorTestServices.ManualRenderTimer()), null, false, new(new RenderLoop(new CompositorTestServices.ManualRenderTimer()), null, false,
new CompositionCommitScheduler(), true); new CompositionCommitScheduler(), true, Dispatcher.UIThread);
class CompositionCommitScheduler : ICompositorScheduler class CompositionCommitScheduler : ICompositorScheduler
{ {

Loading…
Cancel
Save