Browse Source

Minor adjustments to changes in master + fix failing test (need to tick the timer manually) + add some comments

pull/11146/head
Max Katz 3 years ago
parent
commit
6b51f00a4a
  1. 6
      Avalonia.sln
  2. 17
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs
  3. 17
      src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  4. 14
      src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
  5. 2
      src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
  6. 2
      tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj
  7. 6
      tests/Avalonia.Headless.UnitTests/InputTests.cs

6
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}

17
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<Action> _beforeTest;
private readonly List<Action> _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<Action> beforeTest,
List<Action> 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)
{

17
src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -21,20 +21,21 @@ namespace Avalonia.Headless
private Action? _forceTick;
protected override IDisposable StartCore(Action<TimeSpan> 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();
});
}

14
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;
/// <summary>
/// Headless unit test session that needs to be used by the actual testing framework.
/// All UI tests are supposed to be executed from the <see cref="Dispatcher"/> or <see cref="SynchronizationContext"/>
/// to keep execution flow on the UI thread.
/// 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 ConcurrentDictionary<Assembly, HeadlessUnitTestSession> 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();

2
src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs

@ -89,6 +89,8 @@ public static class HeadlessWindowExtensions
private static void RunJobsOnImpl(this TopLevel topLevel, Action<IHeadlessWindow> action)
{
Dispatcher.UIThread.RunJobs();
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Dispatcher.UIThread.RunJobs();
action(GetImpl(topLevel));
Dispatcher.UIThread.RunJobs();

2
tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj

@ -19,6 +19,8 @@
<ItemGroup>
<Compile Include="..\Avalonia.Headless.UnitTests\**\*.cs" />
<Compile Remove="..\Avalonia.Headless.UnitTests\bin\**\*.cs" />
<Compile Remove="..\Avalonia.Headless.UnitTests\obj\**\*.cs" />
</ItemGroup>
<ItemGroup>

6
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();
}

Loading…
Cancel
Save