From 6b51f00a4a6ba2dfef519aba1ac2841352a88986 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 9 May 2023 20:51:11 -0400 Subject: [PATCH] Minor adjustments to changes in master + fix failing test (need to tick the timer manually) + add some comments --- Avalonia.sln | 6 ------ .../AvaloniaTestMethodCommand.cs | 17 +++++++++++++---- .../AvaloniaHeadlessPlatform.cs | 17 +++++++++-------- .../HeadlessUnitTestSession.cs | 14 +++++++------- .../HeadlessWindowExtensions.cs | 2 ++ .../Avalonia.Headless.NUnit.UnitTests.csproj | 2 ++ tests/Avalonia.Headless.UnitTests/InputTests.cs | 6 ++++++ 7 files changed, 39 insertions(+), 25 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 2175de6050..a6f8422342 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -269,7 +269,6 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.UnitTests", "tests\Avalonia.Headless.XUnit.UnitTests\Avalonia.Headless.XUnit.UnitTests.csproj", "{EBA7613E-C36C-4E0C-AB45-71B143F86219}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.UnitTests", "tests\Avalonia.Headless.NUnit.UnitTests\Avalonia.Headless.NUnit.UnitTests.csproj", "{47025FBC-2130-42EE-98C9-D3989B3B9446}" -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -654,10 +653,6 @@ Global {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU - {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -741,7 +736,6 @@ Global {EBA7613E-C36C-4E0C-AB45-71B143F86219} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {47025FBC-2130-42EE-98C9-D3989B3B9446} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} - {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs index 91428388b6..bd3f41de6a 100644 --- a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs +++ b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs @@ -9,12 +9,20 @@ using NUnit.Framework.Internal.Commands; namespace Avalonia.Headless.NUnit; -internal class AvaloniaTestMethodCommand : DelegatingTestCommand +internal class AvaloniaTestMethodCommand : TestCommand { private readonly HeadlessUnitTestSession _session; + private readonly TestCommand _innerCommand; private readonly List _beforeTest; private readonly List _afterTest; + // There are multiple problems with NUnit integration at the moment when we wrote this integration. + // NUnit doesn't have extensibility API for running on custom dispatcher/sync-context. + // See https://github.com/nunit/nunit/issues/2917 https://github.com/nunit/nunit/issues/2774 + // To workaround that we had to replace inner TestMethodCommand with our own implementation while keeping original hierarchy of commands. + // Which will respect proper async/await awaiting code that works with our session and can be block-awaited to fit in NUnit. + // Also, we need to push BeforeTest/AfterTest callbacks to the very same session call. + // I hope there will be a better solution without reflection, but for now that's it. private static FieldInfo s_innerCommand = typeof(DelegatingTestCommand) .GetField("innerCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; private static FieldInfo s_beforeTest = typeof(BeforeAndAfterTestCommand) @@ -27,9 +35,10 @@ internal class AvaloniaTestMethodCommand : DelegatingTestCommand TestCommand innerCommand, List beforeTest, List afterTest) - : base(innerCommand) + : base(innerCommand.Test) { _session = session; + _innerCommand = innerCommand; _beforeTest = beforeTest; _afterTest = afterTest; } @@ -78,10 +87,10 @@ internal class AvaloniaTestMethodCommand : DelegatingTestCommand { _beforeTest.ForEach(a => a()); - var testMethod = innerCommand.Test.Method; + var testMethod = _innerCommand.Test.Method; var methodInfo = testMethod!.MethodInfo; - var result = methodInfo.Invoke(context.TestObject, innerCommand.Test.Arguments); + 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) { diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index cefb6772c9..8202dab874 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -21,20 +21,21 @@ namespace Avalonia.Headless private Action? _forceTick; protected override IDisposable StartCore(Action tick) { - bool cancelled = false; var st = Stopwatch.StartNew(); _forceTick = () => tick(st.Elapsed); - DispatcherTimer.Run(() => + + var timer = new DispatcherTimer(DispatcherPriority.Render) { - if (cancelled) - return false; - tick(st.Elapsed); - return !cancelled; - }, TimeSpan.FromSeconds(1.0 / _framesPerSecond), DispatcherPriority.Render); + Interval = TimeSpan.FromSeconds(1.0 / _framesPerSecond), + Tag = "HeadlessRenderTimer" + }; + timer.Tick += (s, e) => tick(st.Elapsed); + timer.Start(); + return Disposable.Create(() => { _forceTick = null; - cancelled = true; + timer.Stop(); }); } diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs index eb47201400..8dc9629b4c 100644 --- a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs +++ b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs @@ -7,6 +7,7 @@ using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls.Platform; +using Avalonia.Metadata; using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.Threading; @@ -15,10 +16,10 @@ namespace Avalonia.Headless; /// /// Headless unit test session that needs to be used by the actual testing framework. -/// All UI tests are supposed to be executed from the or -/// to keep execution flow on the UI thread. +/// All UI tests are supposed to be executed from one of the methods to keep execution flow on the UI thread. /// Disposing unit test session stops internal dispatcher loop. /// +[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 ConcurrentDictionary s_session = new(); @@ -82,14 +83,13 @@ public sealed class HeadlessUnitTestSession : IDisposable using var application = EnsureApplication(); var cts = new CancellationTokenSource(); - using var globalCts = token.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true); - using var localCts = cancellationToken.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true); + using var globalCts = token.Register(s => ((CancellationTokenSource)s!).Cancel(), cts); + using var localCts = cancellationToken.Register(s => ((CancellationTokenSource)s!).Cancel(), cts); try { var task = action(); - task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts, - TaskScheduler.FromCurrentSynchronizationContext()); + task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts); if (cts.IsCancellationRequested) { @@ -97,7 +97,7 @@ public sealed class HeadlessUnitTestSession : IDisposable } var frame = new DispatcherFrame(); - using var innerCts = cts.Token.Register(() => frame.Continue = false, true); + using var innerCts = cts.Token.Register(() => frame.Continue = false); Dispatcher.UIThread.PushFrame(frame); var result = task.GetAwaiter().GetResult(); diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index f37ec11c79..61659dee2b 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -89,6 +89,8 @@ public static class HeadlessWindowExtensions private static void RunJobsOnImpl(this TopLevel topLevel, Action action) { + Dispatcher.UIThread.RunJobs(); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); Dispatcher.UIThread.RunJobs(); action(GetImpl(topLevel)); Dispatcher.UIThread.RunJobs(); diff --git a/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj b/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj index 19a82cb21c..1a69fb582a 100644 --- a/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj +++ b/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj @@ -19,6 +19,8 @@ + + diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs index 5e3b6e762f..63612ca7ae 100644 --- a/tests/Avalonia.Headless.UnitTests/InputTests.cs +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -13,6 +13,7 @@ public class InputTests #endif { private Window _window; + private Application _setupApp; #if NUNIT [SetUp] @@ -21,6 +22,7 @@ public class InputTests public InputTests() #endif { + _setupApp = Application.Current; Dispatcher.UIThread.VerifyAccess(); _window = new Window { @@ -32,6 +34,8 @@ public class InputTests [AvaloniaFact] public void Should_Click_Button_On_Window() { + Assert.True(_setupApp == Application.Current); + var buttonClicked = false; var button = new Button { @@ -57,6 +61,8 @@ public class InputTests public void Dispose() #endif { + Assert.True(_setupApp == Application.Current); + Dispatcher.UIThread.VerifyAccess(); _window.Close(); }