diff --git a/Avalonia.sln b/Avalonia.sln index d4ccdfdc69..b4148f9337 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -264,7 +264,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit", "src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj", "{ED976634-B118-43F8-8B26-0279C7A7044F}" +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}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -609,10 +613,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 {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -637,6 +637,18 @@ Global {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Build.0 = Release|Any CPU {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.0 = Release|Any CPU + {ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.Build.0 = Release|Any CPU + {EBA7613E-C36C-4E0C-AB45-71B143F86219}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBA7613E-C36C-4E0C-AB45-71B143F86219}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBA7613E-C36C-4E0C-AB45-71B143F86219}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBA7613E-C36C-4E0C-AB45-71B143F86219}.Release|Any CPU.Build.0 = Release|Any CPU + {47025FBC-2130-42EE-98C9-D3989B3B9446}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47025FBC-2130-42EE-98C9-D3989B3B9446}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47025FBC-2130-42EE-98C9-D3989B3B9446}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47025FBC-2130-42EE-98C9-D3989B3B9446}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -707,7 +719,6 @@ Global {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} - {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098} @@ -715,6 +726,9 @@ Global {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098} {4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098} {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {ED976634-B118-43F8-8B26-0279C7A7044F} = {FF237916-7150-496B-89ED-6CA3292896E7} + {EBA7613E-C36C-4E0C-AB45-71B143F86219} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {47025FBC-2130-42EE-98C9-D3989B3B9446} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj new file mode 100644 index 0000000000..3b8950d5d1 --- /dev/null +++ b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj @@ -0,0 +1,18 @@ + + + netstandard2.0;net6.0 + + + + + + + + + + + + + + + diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs new file mode 100644 index 0000000000..fd04146391 --- /dev/null +++ b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal.Commands; + +namespace Avalonia.Headless.NUnit; + +/// +/// Identifies a nunit test that starts on Avalonia Dispatcher +/// such that awaited expressions resume on the test's "main thread". +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] +public sealed class AvaloniaTestAttribute : TestCaseAttribute, IWrapSetUpTearDown +{ + public TestCommand Wrap(TestCommand command) + { + var session = + HeadlessUnitTestSession.GetOrStartForAssembly(command.Test.Method?.MethodInfo.DeclaringType?.Assembly); + + return new AvaloniaTestCommand(session, command); + } +} diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestCommand.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestCommand.cs new file mode 100644 index 0000000000..e5eabb612a --- /dev/null +++ b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestCommand.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; +using NUnit.Framework.Internal.Commands; + +namespace Avalonia.Headless.NUnit; + +internal class AvaloniaTestCommand : DelegatingTestCommand +{ + private readonly HeadlessUnitTestSession _session; + + public AvaloniaTestCommand(HeadlessUnitTestSession session, TestCommand innerCommand) + : base(innerCommand) + { + _session = session; + } + + public override TestResult Execute(TestExecutionContext context) + { + return _session.Dispatcher.InvokeAsync>(async () => + { + var testMethod = innerCommand.Test.Method; + var methodInfo = testMethod!.MethodInfo; + + 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) + { + await task; + } + else if (result is ValueTask valueTask) + { + await valueTask; + } + + context.CurrentResult.SetResult(ResultState.Success); + + if (context.CurrentResult.AssertionResults.Count > 0) + context.CurrentResult.RecordTestCompletion(); + + return context.CurrentResult; + }).GetTask().Unwrap().Result; + } +} diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs new file mode 100644 index 0000000000..1f3cadd296 --- /dev/null +++ b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs @@ -0,0 +1,24 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal.Commands; + +namespace Avalonia.Headless.NUnit; + +/// +/// Identifies a nunit theory that starts on Avalonia Dispatcher +/// such that awaited expressions resume on the test's "main thread". +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] +public sealed class AvaloniaTheoryAttribute : TheoryAttribute, IWrapSetUpTearDown +{ + public TestCommand Wrap(TestCommand command) + { + var session = HeadlessUnitTestSession.GetOrStartForAssembly(command.Test.Method?.MethodInfo.DeclaringType?.Assembly); + + return new AvaloniaTestCommand(session, command); + } +} diff --git a/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs b/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs new file mode 100644 index 0000000000..5488fc956a --- /dev/null +++ b/src/Headless/Avalonia.Headless.NUnit/ExecutionQueue.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Threading; + +namespace Avalonia.Headless.NUnit; + +internal static class ExecutionQueue +{ + static bool _running; + static Queue> _queue = new(); + static async void TryExecuteNext() + { + if (_running || _queue.Count == 0) return; + try + { + _running = true; + await _queue.Dequeue()(); + } + finally + { + _running = false; + } + TryExecuteNext(); + } + + static void ExecuteOnQueue(this Dispatcher dispatcher, Func cb) + { + dispatcher.Post(() => + { + _queue.Enqueue(cb); + TryExecuteNext(); + }); + } +}