diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 875161d336..d2f2ee36d5 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -101,10 +101,6 @@ "type": "boolean", "description": "skip-tests" }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", diff --git a/Avalonia.sln b/Avalonia.sln index 16de32d661..8015a5a007 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -246,8 +246,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}" @@ -264,7 +262,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.UnitTests", "tests\Avalonia.Headless.NUnit.UnitTests\Avalonia.Headless.NUnit.UnitTests.csproj", "{2999D79E-3C20-4A90-B651-CA7E0AC92D35}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.UnitTests", "tests\Avalonia.Headless.XUnit.UnitTests\Avalonia.Headless.XUnit.UnitTests.csproj", "{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -597,14 +601,14 @@ Global {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU - {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 {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 @@ -629,14 +633,26 @@ 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 {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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 + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.Build.0 = Release|Any CPU + {2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Release|Any CPU.Build.0 = Release|Any CPU + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -706,15 +722,20 @@ Global {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {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} {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} {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {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} {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} - {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {2999D79E-3C20-4A90-B651-CA7E0AC92D35} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index e17bad28d7..87e1e86bf9 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -35,8 +35,6 @@ using MicroCom.CodeGenerator; partial class Build : NukeBuild { - [Solution("Avalonia.sln")] readonly Solution Solution; - BuildParameters Parameters { get; set; } protected override void OnBuildInitialized() { @@ -143,10 +141,12 @@ partial class Build : NukeBuild void RunCoreTest(string projectName) { Information($"Running tests from {projectName}"); - var project = Solution.GetProject(projectName).NotNull("project != null"); + var project = RootDirectory.GlobFiles(@$"**\{projectName}.csproj").FirstOrDefault() + ?? throw new InvalidOperationException($"Project {projectName} doesn't exist"); + // Nuke and MSBuild tools have build-in helpers to get target frameworks from the project. // Unfortunately, it gets broken with every second SDK update, so we had to do it manually. - var fileXml = XDocument.Parse(File.ReadAllText(project.Path)); + var fileXml = XDocument.Parse(File.ReadAllText(project)); var targetFrameworks = fileXml.Descendants("TargetFrameworks") .FirstOrDefault()?.Value.Split(';').Select(f => f.Trim()); if (targetFrameworks is null) @@ -212,7 +212,8 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); - RunCoreTest("Avalonia.Headless.UnitTests"); + RunCoreTest("Avalonia.Headless.NUnit.UnitTests"); + RunCoreTest("Avalonia.Headless.XUnit.UnitTests"); }); Target RunRenderTests => _ => _ @@ -311,7 +312,7 @@ partial class Build : NukeBuild public static int Main() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? Execute(x => x.Package) + ? Execute(x => x.RunToolsTests) : Execute(x => x.RunTests); } diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 9af50180dd..77cc9d4dcb 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -288,17 +288,26 @@ namespace Avalonia } s_setupWasAlreadyCalled = true; + SetupUnsafe(); + } + + /// + /// Setup method that doesn't check for input initalizers being set. + /// Nor + /// + internal void SetupUnsafe() + { _optionsInitializers?.Invoke(); - RuntimePlatformServicesInitializer(); - RenderingSubsystemInitializer(); - WindowingSubsystemInitializer(); - AfterPlatformServicesSetupCallback(Self); - Instance = _appFactory(); + RuntimePlatformServicesInitializer?.Invoke(); + RenderingSubsystemInitializer?.Invoke(); + WindowingSubsystemInitializer?.Invoke(); + AfterPlatformServicesSetupCallback?.Invoke(Self); + Instance = _appFactory!(); Instance.ApplicationLifetime = _lifetime; AvaloniaLocator.CurrentMutable.BindToSelf(Instance); Instance.RegisterServices(); Instance.Initialize(); - AfterSetupCallback(Self); + AfterSetupCallback?.Invoke(Self); Instance.OnFrameworkInitializationCompleted(); } } diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 9c4bacbedf..42e965409a 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -17,7 +17,6 @@ - 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..49f1de31f2 --- /dev/null +++ b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj @@ -0,0 +1,19 @@ + + + netstandard2.0;net6.0 + false + + + + + + + + + + + + + + + diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs new file mode 100644 index 0000000000..94b75cf849 --- /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 AvaloniaTestMethodCommand.ProcessCommand(session, command); + } +} diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs new file mode 100644 index 0000000000..bd3f41de6a --- /dev/null +++ b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.Threading; +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; +using NUnit.Framework.Internal.Commands; + +namespace Avalonia.Headless.NUnit; + +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) + .GetField("BeforeTest", BindingFlags.Instance | BindingFlags.NonPublic)!; + private static FieldInfo s_afterTest = typeof(BeforeAndAfterTestCommand) + .GetField("AfterTest", BindingFlags.Instance | BindingFlags.NonPublic)!; + + private AvaloniaTestMethodCommand( + HeadlessUnitTestSession session, + TestCommand innerCommand, + List beforeTest, + List afterTest) + : base(innerCommand.Test) + { + _session = session; + _innerCommand = innerCommand; + _beforeTest = beforeTest; + _afterTest = afterTest; + } + + public static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command) + { + return ProcessCommand(session, command, new List(), new List()); + } + + private static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command, List before, List after) + { + if (command is BeforeAndAfterTestCommand beforeAndAfterTestCommand) + { + if (s_beforeTest.GetValue(beforeAndAfterTestCommand) is Action beforeTest) + { + Action beforeAction = c => before.Add(() => beforeTest(c)); + s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction); + } + if (s_afterTest.GetValue(beforeAndAfterTestCommand) is Action afterTest) + { + Action afterAction = c => after.Add(() => afterTest(c)); + s_afterTest.SetValue(beforeAndAfterTestCommand, afterAction); + } + } + + if (command is DelegatingTestCommand delegatingTestCommand + && s_innerCommand.GetValue(delegatingTestCommand) is TestCommand inner) + { + s_innerCommand.SetValue(delegatingTestCommand, ProcessCommand(session, inner, before, after)); + } + else if (command is TestMethodCommand methodCommand) + { + return new AvaloniaTestMethodCommand(session, methodCommand, before, after); + } + + return command; + } + + public override TestResult Execute(TestExecutionContext context) + { + return _session.Dispatch(() => ExecuteTestMethod(context), default).GetAwaiter().GetResult(); + } + + // Unfortunately, NUnit has issues with custom synchronization contexts, which means we need to add some hacks to make it work. + private async Task ExecuteTestMethod(TestExecutionContext context) + { + _beforeTest.ForEach(a => a()); + + 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(); + + if (context.ExecutionStatus != TestExecutionStatus.AbortRequested) + { + _afterTest.ForEach(a => a()); + } + + return context.CurrentResult; + } +} diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs new file mode 100644 index 0000000000..85ed67dbd2 --- /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 AvaloniaTestMethodCommand.ProcessCommand(session, command); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj index c2c58b4f94..4ab70eb07d 100644 --- a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -1,12 +1,12 @@ - net6.0 - enable - enable + netstandard2.0;net6.0 + false - + + diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs new file mode 100644 index 0000000000..f501fc7a56 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs @@ -0,0 +1,35 @@ +using System; +using System.ComponentModel; +using System.Threading; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +/// +/// Identifies an xunit test that starts on Avalonia Dispatcher +/// such that awaited expressions resume on the test's "main thread". +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +[XunitTestCaseDiscoverer("Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer", "Avalonia.Headless.XUnit")] +public sealed class AvaloniaFactAttribute : FactAttribute +{ + +} + +[EditorBrowsable(EditorBrowsableState.Never)] +public class AvaloniaUIFactDiscoverer : FactDiscoverer +{ + private readonly IMessageSink diagnosticMessageSink; + public AvaloniaUIFactDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + this.diagnosticMessageSink = diagnosticMessageSink; + } + + protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + return new AvaloniaTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs new file mode 100644 index 0000000000..4b1cf84914 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestAssemblyRunner : XunitTestAssemblyRunner +{ + private HeadlessUnitTestSession? _session; + + public AvaloniaTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable testCases, + IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, + executionMessageSink, executionOptions) + { + } + + protected override void SetupSyncContext(int maxParallelThreads) + { + _session = HeadlessUnitTestSession.GetOrStartForAssembly( + Assembly.Load(new AssemblyName(TestAssembly.Assembly.Name))); + base.SetupSyncContext(1); + } + + public override void Dispose() + { + _session?.Dispose(); + base.Dispose(); + } + + protected override Task RunTestCollectionAsync( + IMessageBus messageBus, + ITestCollection testCollection, + IEnumerable testCases, + CancellationTokenSource cancellationTokenSource) + { + return new AvaloniaTestCollectionRunner(_session!, testCollection, testCases, DiagnosticMessageSink, messageBus, + TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); + } + + private class AvaloniaTestCollectionRunner : XunitTestCollectionRunner + { + private readonly HeadlessUnitTestSession _session; + + public AvaloniaTestCollectionRunner(HeadlessUnitTestSession session, + ITestCollection testCollection, IEnumerable testCases, + IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, + ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection, + testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource) + { + _session = session; + } + + protected override Task RunTestClassAsync( + ITestClass testClass, + IReflectionTypeInfo @class, + IEnumerable testCases) + { + return new AvaloniaTestClassRunner(_session, testClass, @class, testCases, DiagnosticMessageSink, MessageBus, + TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, + CollectionFixtureMappings).RunAsync(); + } + } + + private class AvaloniaTestClassRunner : XunitTestClassRunner + { + private readonly HeadlessUnitTestSession _session; + + public AvaloniaTestClassRunner(HeadlessUnitTestSession session, ITestClass testClass, + IReflectionTypeInfo @class, + IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, + ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource, IDictionary collectionFixtureMappings) : + base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, + cancellationTokenSource, collectionFixtureMappings) + { + _session = session; + } + + protected override Task RunTestMethodAsync( + ITestMethod testMethod, + IReflectionMethodInfo method, + IEnumerable testCases, + object[] constructorArguments) + { + return new AvaloniaTestMethodRunner(_session, testMethod, Class, method, testCases, DiagnosticMessageSink, + MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource, + constructorArguments).RunAsync(); + } + } + + private class AvaloniaTestMethodRunner : XunitTestMethodRunner + { + private readonly HeadlessUnitTestSession _session; + private readonly IMessageBus _messageBus; + private readonly ExceptionAggregator _aggregator; + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly object[] _constructorArguments; + + public AvaloniaTestMethodRunner(HeadlessUnitTestSession session, ITestMethod testMethod, + IReflectionTypeInfo @class, + IReflectionMethodInfo method, IEnumerable testCases, IMessageSink diagnosticMessageSink, + IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, + object[] constructorArguments) : base(testMethod, @class, method, testCases, diagnosticMessageSink, + messageBus, aggregator, cancellationTokenSource, constructorArguments) + { + _session = session; + _messageBus = messageBus; + _aggregator = aggregator; + _cancellationTokenSource = cancellationTokenSource; + _constructorArguments = constructorArguments; + } + + protected override Task RunTestCaseAsync(IXunitTestCase testCase) + { + return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason, + _constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator, + _cancellationTokenSource); + } + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs new file mode 100644 index 0000000000..092662745c --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestCase : XunitTestCase +{ + public AvaloniaTestCase( + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + ITestMethod testMethod, + object?[]? testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, TestMethodDisplayOptions.None, testMethod, testMethodArguments) + { + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public AvaloniaTestCase() + { + } + + public override Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly); + + // 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. + var runSummary = AvaloniaTestCaseRunner + .RunTest(session, this, DisplayName, SkipReason, constructorArguments, + TestMethodArguments, messageBus, aggregator, cancellationTokenSource) + .GetAwaiter().GetResult(); + + return Task.FromResult(runSummary); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs new file mode 100644 index 0000000000..97fcfa2521 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestCaseRunner : XunitTestCaseRunner +{ + private readonly Action? _onAfterTestInvoked; + + public AvaloniaTestCaseRunner( + Action? onAfterTestInvoked, + IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, + object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments, + testMethodArguments, messageBus, aggregator, cancellationTokenSource) + { + _onAfterTestInvoked = onAfterTestInvoked; + } + + public static Task RunTest(HeadlessUnitTestSession session, + IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, + object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + var afterTest = () => Dispatcher.UIThread.RunJobs(); + return session.Dispatch(async () => + { + var runner = new AvaloniaTestCaseRunner(afterTest, testCase, displayName, + skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource); + return await runner.RunAsync(); + }, cancellationTokenSource.Token); + } + + protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass, + object[] constructorArguments, + MethodInfo testMethod, object[] testMethodArguments, string skipReason, + IReadOnlyList beforeAfterAttributes, + ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) + { + return new AvaloniaTestRunner(_onAfterTestInvoked, test, messageBus, testClass, constructorArguments, + testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource); + } + + private class AvaloniaTestRunner : XunitTestRunner + { + private readonly Action? _onAfterTestInvoked; + + public AvaloniaTestRunner( + Action? onAfterTestInvoked, + ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, + object[] testMethodArguments, string skipReason, + IReadOnlyList beforeAfterAttributes, ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments, + testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource) + { + _onAfterTestInvoked = onAfterTestInvoked; + } + + protected override Task InvokeTestMethodAsync(ExceptionAggregator aggregator) + { + return new AvaloniaTestInvoker(_onAfterTestInvoked, Test, MessageBus, TestClass, ConstructorArguments, + TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync(); + } + } + + private class AvaloniaTestInvoker : XunitTestInvoker + { + private readonly Action? _onAfterTestInvoked; + + public AvaloniaTestInvoker( + Action? onAfterTestInvoked, + ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, + object[] testMethodArguments, IReadOnlyList beforeAfterAttributes, + ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(test, messageBus, + testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator, + cancellationTokenSource) + { + _onAfterTestInvoked = onAfterTestInvoked; + } + + protected override async Task AfterTestMethodInvokedAsync() + { + await base.AfterTestMethodInvokedAsync(); + + // Only here we can execute random code after the test, where exception will be properly handled by the XUnit. + if (_onAfterTestInvoked is not null) + { + Aggregator.Run(_onAfterTestInvoked); + } + } + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs index 21086fa946..aa9b3e7e18 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs @@ -1,10 +1,11 @@ -using System.Reflection; +using System.Collections.Generic; +using System.Reflection; using Xunit.Abstractions; using Xunit.Sdk; namespace Avalonia.Headless.XUnit; -internal class AvaloniaTestFramework : XunitTestFramework +internal class AvaloniaTestFramework : XunitTestFramework { public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink) { @@ -26,8 +27,7 @@ internal class AvaloniaTestFramework : XunitTestFramework IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) { - executionOptions.SetValue("xunit.execution.DisableParallelization", false); - using (var assemblyRunner = new AvaloniaTestRunner( + using (var assemblyRunner = new AvaloniaTestAssemblyRunner( TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions)) await assemblyRunner.RunAsync(); } diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs index 3eace30805..bdd8f3b0ea 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs @@ -1,4 +1,6 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using Xunit.Abstractions; using Xunit.Sdk; @@ -7,20 +9,13 @@ namespace Avalonia.Headless.XUnit; /// /// Sets up global avalonia test framework using avalonia application builder passed as a parameter. /// +/// +/// It is an alternative to using [AvaloniaFact] or [AvaloniaTheory] attributes on every test method. +/// [TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute { - /// - /// Creates instance of . - /// - /// - /// Parameter from which should be created. - /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. - /// - public AvaloniaTestFrameworkAttribute( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] - Type appBuilderEntryPointType) { } } /// @@ -38,8 +33,6 @@ public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer /// public Type GetTestFrameworkType(IAttributeInfo attribute) { - var builderType = attribute.GetConstructorArguments().First() as Type - ?? throw new InvalidOperationException("AppBuilderEntryPointType parameter must be defined on the AvaloniaTestFrameworkAttribute attribute."); - return typeof(AvaloniaTestFramework<>).MakeGenericType(builderType); + return typeof(AvaloniaTestFramework); } } diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs deleted file mode 100644 index 42604adf46..0000000000 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Avalonia.Threading; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Avalonia.Headless.XUnit; - -internal class AvaloniaTestRunner : XunitTestAssemblyRunner -{ - private CancellationTokenSource? _cancellationTokenSource; - - public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable testCases, - IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, - ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, - executionMessageSink, executionOptions) - { - } - - protected override void SetupSyncContext(int maxParallelThreads) - { - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = new CancellationTokenSource(); - SynchronizationContext.SetSynchronizationContext(InitNewApplicationContext(_cancellationTokenSource.Token).Result); - } - - public override void Dispose() - { - _cancellationTokenSource?.Cancel(); - base.Dispose(); - } - - internal static Task InitNewApplicationContext(CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(); - - new Thread(() => - { - try - { - var appBuilder = AppBuilder.Configure(typeof(TAppBuilderEntry)); - - // If windowing subsystem wasn't initialized by user, force headless with default parameters. - if (appBuilder.WindowingSubsystemName != "Headless") - { - appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); - } - - appBuilder.SetupWithoutStarting(); - - tcs.SetResult(SynchronizationContext.Current!); - } - catch (Exception e) - { - tcs.SetException(e); - } - - Dispatcher.UIThread.MainLoop(cancellationToken); - }) { IsBackground = true }.Start(); - - return tcs.Task; - } -} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs new file mode 100644 index 0000000000..53c997f08f --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +/// +/// Identifies an xunit theory that starts on Avalonia Dispatcher +/// such that awaited expressions resume on the test's "main thread". +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +[XunitTestCaseDiscoverer("Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer", "Avalonia.Headless.XUnit")] +public sealed class AvaloniaTheoryAttribute : TheoryAttribute +{ +} + +[EditorBrowsable(EditorBrowsableState.Never)] +public class AvaloniaTheoryDiscoverer : TheoryDiscoverer +{ + public AvaloniaTheoryDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) + { + } + + protected override IEnumerable CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) + { + yield return new AvaloniaTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow); + } + + protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + { + yield return new AvaloniaTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs new file mode 100644 index 0000000000..ea7e7abee4 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTheoryTestCase : XunitTheoryTestCase +{ + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public AvaloniaTheoryTestCase() + { + } + + public AvaloniaTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) + { + } + + public override Task RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) + { + var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly); + + return AvaloniaTestCaseRunner + .RunTest(session, this, DisplayName, SkipReason, constructorArguments, + TestMethodArguments, messageBus, aggregator, cancellationTokenSource); + } +} diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index b626eaeb68..893cb0074c 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -12,7 +12,16 @@ + + + + + + + + + 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/AvaloniaTestApplicationAttribute.cs b/src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs new file mode 100644 index 0000000000..9159657ec4 --- /dev/null +++ b/src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs @@ -0,0 +1,27 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Avalonia.Headless; + +/// +/// Sets up global avalonia test framework using avalonia application builder passed as a parameter. +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] +public sealed class AvaloniaTestApplicationAttribute : Attribute +{ + public Type AppBuilderEntryPointType { get; } + + /// + /// Creates instance of . + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + public AvaloniaTestApplicationAttribute( + [DynamicallyAccessedMembers(HeadlessUnitTestSession.DynamicallyAccessed)] + Type appBuilderEntryPointType) + { + AppBuilderEntryPointType = appBuilderEntryPointType; + } +} diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 04e1ea99a5..ab157f8062 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -18,7 +18,8 @@ namespace Avalonia.Headless { AvaloniaLocator.CurrentMutable .Bind().ToConstant(new HeadlessPlatformRenderInterface()) - .Bind().ToConstant(new HeadlessFontManagerStub()); + .Bind().ToConstant(new HeadlessFontManagerStub()) + .Bind().ToConstant(new HeadlessTextShaperStub()); } public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; @@ -128,18 +129,30 @@ namespace Avalonia.Headless Point baselineOrigin, Rect bounds) { - return new HeadlessGlyphRunStub(); + return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds); } - private class HeadlessGlyphRunStub : IGlyphRunImpl + internal class HeadlessGlyphRunStub : IGlyphRunImpl { - public Rect Bounds => new Rect(new Size(8, 12)); + public HeadlessGlyphRunStub( + IGlyphTypeface glyphTypeface, + double fontRenderingEmSize, + Point baselineOrigin, + Rect bounds) + { + GlyphTypeface = glyphTypeface; + FontRenderingEmSize = fontRenderingEmSize; + BaselineOrigin = baselineOrigin; + Bounds =bounds; + } - public Point BaselineOrigin => new Point(0, 8); + public Rect Bounds { get; } - public IGlyphTypeface GlyphTypeface => new HeadlessGlyphTypefaceImpl(); + public Point BaselineOrigin { get; } - public double FontRenderingEmSize => 12; + public IGlyphTypeface GlyphTypeface { get; } + + public double FontRenderingEmSize { get; } public void Dispose() { @@ -234,8 +247,11 @@ namespace Avalonia.Headless private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { + private HeadlessStreamingGeometryContextStub _context; + public HeadlessStreamingGeometryStub() : base(default) { + _context = new HeadlessStreamingGeometryContextStub(this); } public IStreamGeometryImpl Clone() @@ -245,13 +261,18 @@ namespace Avalonia.Headless public IStreamGeometryContextImpl Open() { - return new HeadlessStreamingGeometryContextStub(this); + return _context; + } + + public override bool FillContains(Point point) + { + return _context.FillContains(point); } private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl { private readonly HeadlessStreamingGeometryStub _parent; - private double _x1, _y1, _x2, _y2; + private List points = new List(); public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent) { _parent = parent; @@ -259,19 +280,30 @@ namespace Avalonia.Headless private void Track(Point pt) { - if (_x1 > pt.X) - _x1 = pt.X; - if (_x2 < pt.X) - _x2 = pt.X; - if (_y1 > pt.Y) - _y1 = pt.Y; - if (_y2 < pt.Y) - _y2 = pt.Y; + points.Add(pt); } + public Rect CalculateBounds() + { + var left = double.MaxValue; + var right = double.MinValue; + var top = double.MaxValue; + var bottom = double.MinValue; + + foreach (var p in points) + { + left = Math.Min(p.X, left); + right = Math.Max(p.X, right); + top = Math.Min(p.Y, top); + bottom = Math.Max(p.Y, bottom); + } + + return new Rect(new Point(left, top), new Point(right, bottom)); + } + public void Dispose() { - _parent.Bounds = new Rect(_x1, _y1, _x2 - _x1, _y2 - _y1); + _parent.Bounds = CalculateBounds(); } public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) @@ -303,6 +335,35 @@ namespace Avalonia.Headless { } + + public bool FillContains(Point point) + { + // Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html + // to determine if the point is in the geometry (since it will always be convex in this situation) + for (int i = 0; i < points.Count; i++) + { + var a = points[i]; + var b = points[(i + 1) % points.Count]; + var c = points[(i + 2) % points.Count]; + + Vector v0 = c - a; + Vector v1 = b - a; + Vector v2 = point - a; + + var dot00 = v0 * v0; + var dot01 = v0 * v1; + var dot02 = v0 * v2; + var dot11 = v1 * v1; + var dot12 = v1 * v2; + + + var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); + var u = (dot11 * dot02 - dot01 * dot12) * invDenom; + var v = (dot00 * dot12 - dot01 * dot02) * invDenom; + if ((u >= 0) && (v >= 0) && (u + v < 1)) return true; + } + return false; + } } } @@ -368,7 +429,7 @@ namespace Avalonia.Headless } } - private class HeadlessDrawingContextStub : IDrawingContextImpl + internal class HeadlessDrawingContextStub : IDrawingContextImpl { public void Dispose() { diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 769fea7c6e..471ea9a6d0 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; @@ -11,6 +13,7 @@ using Avalonia.Input.Platform; using Avalonia.Media; using Avalonia.Media.Fonts; using Avalonia.Media.TextFormatting; +using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; @@ -82,22 +85,22 @@ namespace Avalonia.Headless { public FontMetrics Metrics => new FontMetrics { - DesignEmHeight = 1, - Ascent = 8, - Descent = 4, + DesignEmHeight = 10, + Ascent = 2, + Descent = 10, + IsFixedPitch = true, LineGap = 0, UnderlinePosition = 2, UnderlineThickness = 1, StrikethroughPosition = 2, - StrikethroughThickness = 1, - IsFixedPitch = true + StrikethroughThickness = 1 }; public int GlyphCount => 1337; - public FontSimulations FontSimulations { get; } + public FontSimulations FontSimulations => FontSimulations.None; - public string FamilyName => "Arial"; + public string FamilyName => "$Default"; public FontWeight Weight => FontWeight.Normal; @@ -111,24 +114,31 @@ namespace Avalonia.Headless public ushort GetGlyph(uint codepoint) { - return 1; + return (ushort)codepoint; } public bool TryGetGlyph(uint codepoint, out ushort glyph) { - glyph = 1; + glyph = 8; return true; } public int GetGlyphAdvance(ushort glyph) { - return 12; + return 8; } public int[] GetGlyphAdvances(ReadOnlySpan glyphs) { - return glyphs.ToArray().Select(x => (int)x).ToArray(); + var advances = new int[glyphs.Length]; + + for (var i = 0; i < advances.Length; i++) + { + advances[i] = 8; + } + + return advances; } public ushort[] GetGlyphs(ReadOnlySpan codepoints) @@ -146,8 +156,8 @@ namespace Avalonia.Headless { metrics = new GlyphMetrics { - Height = 10, - Width = 8 + Width = 10, + Height = 10 }; return true; @@ -161,40 +171,81 @@ namespace Avalonia.Headless var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; + var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var textSpan = text.Span; + var textStartIndex = TextTestHelper.GetStartCharIndex(text); + + for (var i = 0; i < shapedBuffer.Length;) + { + var glyphCluster = i + textStartIndex; + + var codepoint = Codepoint.ReadAt(textSpan, i, out var count); + + var glyphIndex = typeface.GetGlyph(codepoint); - return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + for (var j = 0; j < count; ++j) + { + shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); + } + + i += count; + } + + return shapedBuffer; } } internal class HeadlessFontManagerStub : IFontManagerImpl { + private readonly string _defaultFamilyName; + + public HeadlessFontManagerStub(string defaultFamilyName = "Default") + { + _defaultFamilyName = defaultFamilyName; + } + + public int TryCreateGlyphTypefaceCount { get; private set; } + public string GetDefaultFontFamilyName() { - return "Arial"; + return _defaultFamilyName; } - public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) { - return new string[] { "Arial" }; + return new[] { _defaultFamilyName }; } - public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + FontStretch fontStretch, + CultureInfo? culture, out Typeface fontKey) { - glyphTypeface= new HeadlessGlyphTypefaceImpl(); + fontKey = new Typeface(_defaultFamilyName); - return true; + return false; } - public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - glyphTypeface = new HeadlessGlyphTypefaceImpl(); + glyphTypeface = null; + + TryCreateGlyphTypefaceCount++; + + if (familyName == "Unknown") + { + return false; + } + + glyphTypeface = new HeadlessGlyphTypefaceImpl(); return true; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface) + public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) { - typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); + glyphTypeface = new HeadlessGlyphTypefaceImpl(); + return true; } } @@ -249,4 +300,14 @@ namespace Avalonia.Headless return ScreenHelper.ScreenFromWindow(window, AllScreens); } } + + internal static class TextTestHelper + { + public static int GetStartCharIndex(ReadOnlyMemory text) + { + if (!MemoryMarshal.TryGetString(text, out _, out var start, out _)) + throw new InvalidOperationException("text memory should have been a string"); + return start; + } + } } diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs new file mode 100644 index 0000000000..1610f6796c --- /dev/null +++ b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +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; + +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 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(); + + private readonly AppBuilder _appBuilder; + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly BlockingCollection _queue; + private readonly Task _dispatchTask; + + internal const DynamicallyAccessedMemberTypes DynamicallyAccessed = + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.PublicParameterlessConstructor; + + private HeadlessUnitTestSession(AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource, + BlockingCollection queue, Task dispatchTask) + { + _appBuilder = appBuilder; + _cancellationTokenSource = cancellationTokenSource; + _queue = queue; + _dispatchTask = dispatchTask; + } + + /// + public Task Dispatch(Action action, CancellationToken cancellationToken) + { + return Dispatch(() => + { + action(); + return Task.FromResult(0); + }, cancellationToken); + } + + /// + public Task Dispatch(Func action, CancellationToken cancellationToken) + { + return Dispatch(() => Task.FromResult(action()), cancellationToken); + } + + /// + /// Dispatch method queues an async operation on the dispatcher thread, creates a new application instance, + /// setting app avalonia services, and runs parameter. + /// + /// Action to execute on the dispatcher thread with avalonia services. + /// Cancellation token to cancel execution. + /// + /// If global session was already cancelled and thread killed, it's not possible to dispatch any actions again + /// + public Task Dispatch(Func> action, CancellationToken cancellationToken) + { + if (_cancellationTokenSource.IsCancellationRequested) + { + throw new ObjectDisposedException("Session was already disposed."); + } + + var token = _cancellationTokenSource.Token; + + var tcs = new TaskCompletionSource(); + _queue.Add(() => + { + 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); + + try + { + var task = action(); + task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts, + TaskScheduler.FromCurrentSynchronizationContext()); + + if (cts.IsCancellationRequested) + { + return; + } + + var frame = new DispatcherFrame(); + using var innerCts = cts.Token.Register(() => frame.Continue = false, true); + Dispatcher.UIThread.PushFrame(frame); + + var result = task.GetAwaiter().GetResult(); + tcs.TrySetResult(result); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }); + return tcs.Task; + } + + private IDisposable EnsureApplication() + { + var scope = AvaloniaLocator.EnterScope(); + try + { + Dispatcher.ResetForUnitTests(); + _appBuilder.SetupUnsafe(); + } + catch + { + scope.Dispose(); + throw; + } + + return Disposable.Create(() => + { + scope.Dispose(); + Dispatcher.ResetForUnitTests(); + }); + } + + public void Dispose() + { + _cancellationTokenSource.Cancel(); + _queue.CompleteAdding(); + _dispatchTask.Wait(); + _cancellationTokenSource.Dispose(); + } + + /// + /// Creates instance of . + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + public static HeadlessUnitTestSession StartNew( + [DynamicallyAccessedMembers(DynamicallyAccessed)] + Type entryPointType) + { + var tcs = new TaskCompletionSource(); + var cancellationTokenSource = new CancellationTokenSource(); + var queue = new BlockingCollection(); + + Task? task = null; + task = Task.Run(() => + { + try + { + var appBuilder = AppBuilder.Configure(entryPointType); + + // If windowing subsystem wasn't initialized by user, force headless with default parameters. + if (appBuilder.WindowingSubsystemName != "Headless") + { + appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); + } + + // ReSharper disable once AccessToModifiedClosure + tcs.SetResult(new HeadlessUnitTestSession(appBuilder, cancellationTokenSource, queue, task!)); + } + catch (Exception e) + { + tcs.SetException(e); + return; + } + + while (!cancellationTokenSource.IsCancellationRequested) + { + try + { + var action = queue.Take(cancellationTokenSource.Token); + action(); + } + catch (OperationCanceledException) + { + } + } + }); + + return tcs.Task.GetAwaiter().GetResult(); + } + + /// + /// Creates a session from AvaloniaTestApplicationAttribute attribute or reuses any existing. + /// If AvaloniaTestApplicationAttribute doesn't exist, empty application is used. + /// + [UnconditionalSuppressMessage("Trimming", "IL2072", + Justification = "AvaloniaTestApplicationAttribute attribute should preserve type information.")] + public static HeadlessUnitTestSession GetOrStartForAssembly(Assembly? assembly) + { + return s_session.GetOrAdd(assembly ?? typeof(HeadlessUnitTestSession).Assembly, a => + { + var appBuilderEntryPointType = a.GetCustomAttribute() + ?.AppBuilderEntryPointType; + return appBuilderEntryPointType is not null ? + StartNew(appBuilderEntryPointType) : + StartNew(typeof(Application)); + }); + } +} diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index 7d4b7f5477..61659dee2b 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -44,53 +44,56 @@ public static class HeadlessWindowExtensions /// Simulates keyboard press on the headless window/toplevel. /// public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => - RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers); + RunJobsOnImpl(topLevel, w => w.KeyPress(key, modifiers)); /// /// Simulates keyboard release on the headless window/toplevel. /// public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => - RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers); + RunJobsOnImpl(topLevel, w => w.KeyRelease(key, modifiers)); /// /// Simulates mouse down on the headless window/toplevel. /// public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None) => - RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers); + RunJobsOnImpl(topLevel, w => w.MouseDown(point, button, modifiers)); /// /// Simulates mouse move on the headless window/toplevel. /// public static void MouseMove(this TopLevel topLevel, Point point, RawInputModifiers modifiers = RawInputModifiers.None) => - RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers); + RunJobsOnImpl(topLevel, w => w.MouseMove(point, modifiers)); /// /// Simulates mouse up on the headless window/toplevel. /// public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None) => - RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers); + RunJobsOnImpl(topLevel, w => w.MouseUp(point, button, modifiers)); /// /// Simulates mouse wheel on the headless window/toplevel. /// public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None) => - RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers); + RunJobsOnImpl(topLevel, w => w.MouseWheel(point, delta, modifiers)); /// /// Simulates drag'n'drop target on the headless window/toplevel. /// public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => - RunJobsAndGetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); + RunJobsOnImpl(topLevel, w => w.DragDrop(point, type, data, effects, modifiers)); - private static IHeadlessWindow RunJobsAndGetImpl(this TopLevel topLevel) + private static void RunJobsOnImpl(this TopLevel topLevel, Action action) { Dispatcher.UIThread.RunJobs(); - return GetImpl(topLevel); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + Dispatcher.UIThread.RunJobs(); + action(GetImpl(topLevel)); + Dispatcher.UIThread.RunJobs(); } private static IHeadlessWindow GetImpl(this TopLevel topLevel) diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index 3ccec872d2..adb5431ce6 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Headless; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -27,7 +28,7 @@ namespace Avalonia.Base.UnitTests.Media [Fact] public void Should_Throw_When_Default_FamilyName_Is_Null() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null)))) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new HeadlessFontManagerStub(null!)))) { Assert.Throws(() => FontManager.Current); } @@ -39,7 +40,7 @@ namespace Avalonia.Base.UnitTests.Media var options = new FontManagerOptions { DefaultFamilyName = "MyFont" }; using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(fontManagerImpl: new MockFontManagerImpl()))) + .With(fontManagerImpl: new HeadlessFontManagerStub()))) { AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); @@ -62,7 +63,7 @@ namespace Avalonia.Base.UnitTests.Media }; using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(fontManagerImpl: new MockFontManagerImpl()))) + .With(fontManagerImpl: new HeadlessFontManagerStub()))) { AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 84ce341e98..c273cc6489 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Headless; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; @@ -179,13 +180,13 @@ namespace Avalonia.Base.UnitTests.Media glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]); } - return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel); + return new GlyphRun(new HeadlessGlyphTypefaceImpl(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel); } private static IDisposable Start() { return UnitTestApplication.Start(TestServices.StyledWindow.With( - renderInterface: new MockPlatformRenderInterface())); + renderInterface: new HeadlessPlatformRenderInterface())); } } } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs index 27bb0355e6..7cd02d2907 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Avalonia.Base.UnitTests.VisualTree; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Shapes; diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs deleted file mode 100644 index 37adb03628..0000000000 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ /dev/null @@ -1,285 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.UnitTests; -using Avalonia.Media.Imaging; -using Avalonia.Media.TextFormatting; - -namespace Avalonia.Base.UnitTests.VisualTree -{ - class MockRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext - { - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) - { - throw new NotImplementedException(); - } - - public bool IsLost => false; - - public object TryGetFeature(Type featureType) => null; - - public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) - { - throw new NotImplementedException(); - } - - public IStreamGeometryImpl CreateStreamGeometry() - { - return new MockStreamGeometry(); - } - - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) - { - throw new NotImplementedException(); - } - - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(Stream stream) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmap(string fileName) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(string fileName) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride) - { - throw new NotImplementedException(); - } - - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - IReadOnlyList glyphInfos, Point baselineOrigin, Rect bounds) - { - throw new NotImplementedException(); - } - - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) - { - return this; - } - - public bool SupportsIndividualRoundRects { get; set; } - public AlphaFormat DefaultAlphaFormat { get; } - public PixelFormat DefaultPixelFormat { get; } - public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true; - - public IFontManagerImpl CreateFontManager() - { - return new MockFontManagerImpl(); - } - - public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat fmt, AlphaFormat alphaFormat) - { - throw new NotImplementedException(); - } - - public IGeometryImpl CreateEllipseGeometry(Rect rect) - { - throw new NotImplementedException(); - } - - public IGeometryImpl CreateLineGeometry(Point p1, Point p2) - { - throw new NotImplementedException(); - } - - public IGeometryImpl CreateRectangleGeometry(Rect rect) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) - { - throw new NotImplementedException(); - } - - class MockStreamGeometry : IStreamGeometryImpl - { - private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); - public Rect Bounds - { - get - { - throw new NotImplementedException(); - } - } - - public double ContourLength { get; } - - public IStreamGeometryImpl Clone() - { - return this; - } - - public void Dispose() - { - } - - public bool FillContains(Point point) - { - return _impl.FillContains(point); - } - - public Rect GetRenderBounds(IPen pen) - { - throw new NotImplementedException(); - } - - public IGeometryImpl Intersect(IGeometryImpl geometry) - { - throw new NotImplementedException(); - } - - public IStreamGeometryContextImpl Open() - { - return _impl; - } - - public bool StrokeContains(IPen pen, Point point) - { - throw new NotImplementedException(); - } - - public ITransformedGeometryImpl WithTransform(Matrix transform) - { - throw new NotImplementedException(); - } - - public bool TryGetPointAtDistance(double distance, out Point point) - { - throw new NotImplementedException(); - } - - public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) - { - throw new NotImplementedException(); - } - - public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) - { - throw new NotImplementedException(); - } - - class MockStreamGeometryContext : IStreamGeometryContextImpl - { - private List points = new List(); - public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) - { - throw new NotImplementedException(); - } - - public void BeginFigure(Point startPoint, bool isFilled) - { - points.Add(startPoint); - } - - public void CubicBezierTo(Point point1, Point point2, Point point3) - { - throw new NotImplementedException(); - } - - public void Dispose() - { - } - - public void EndFigure(bool isClosed) - { - } - - public void LineTo(Point point) - { - points.Add(point); - } - - public void QuadraticBezierTo(Point control, Point endPoint) - { - throw new NotImplementedException(); - } - - public void SetFillRule(FillRule fillRule) - { - } - - public bool FillContains(Point point) - { - // Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html - // to determine if the point is in the geometry (since it will always be convex in this situation) - for (int i = 0; i < points.Count; i++) - { - var a = points[i]; - var b = points[(i + 1) % points.Count]; - var c = points[(i + 2) % points.Count]; - - Vector v0 = c - a; - Vector v1 = b - a; - Vector v2 = point - a; - - var dot00 = v0 * v0; - var dot01 = v0 * v1; - var dot02 = v0 * v2; - var dot11 = v1 * v1; - var dot12 = v1 * v2; - - - var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); - var u = (dot11 * dot02 - dot01 * dot12) * invDenom; - var v = (dot00 * dot12 - dot01 * dot02) * invDenom; - if ((u >= 0) && (v >= 0) && (u + v < 1)) return true; - } - return false; - } - } - } - - public void Dispose() - { - - } - } - -} diff --git a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs index 5df1dd1679..4202e60fd6 100644 --- a/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; using Avalonia.Controls; +using Avalonia.Headless; using Avalonia.Threading; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; @@ -15,11 +16,7 @@ namespace Avalonia.Benchmarks.Layout public ControlsBenchmark() { - _app = UnitTestApplication.Start( - TestServices.StyledWindow.With( - renderInterface: new NullRenderingPlatform(), - dispatcherImpl: new NullThreadingPlatform(), - standardCursorFactory: new NullCursorFactory())); + _app = UnitTestApplication.Start(TestServices.StyledWindow); _root = new TestRoot(true, null) { diff --git a/tests/Avalonia.Benchmarks/NullCursorFactory.cs b/tests/Avalonia.Benchmarks/NullCursorFactory.cs deleted file mode 100644 index 9aeb353151..0000000000 --- a/tests/Avalonia.Benchmarks/NullCursorFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Avalonia.Input; -using Avalonia.Platform; - -namespace Avalonia.Benchmarks -{ - internal class NullCursorFactory : ICursorFactory - { - public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new NullCursorImpl(); - ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) => new NullCursorImpl(); - - private class NullCursorImpl : ICursorImpl - { - public void Dispose() { } - } - } -} diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs deleted file mode 100644 index 3513843367..0000000000 --- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Utilities; -using Avalonia.Media.Imaging; - -namespace Avalonia.Benchmarks -{ - internal class NullDrawingContextImpl : IDrawingContextImpl - { - public void Dispose() - { - } - - public Matrix Transform { get; set; } - - public RenderOptions RenderOptions { get; set; } - - public void Clear(Color color) - { - } - - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) - { - } - - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) - { - } - - public void DrawLine(IPen pen, Point p1, Point p2) - { - } - - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) - { - } - - public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) - { - } - - public void DrawEllipse(IBrush brush, IPen pen, Rect rect) - { - } - - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) - { - } - - public IDrawingContextLayerImpl CreateLayer(Size size) - { - return null; - } - - public void PushClip(Rect clip) - { - } - - public void PushClip(RoundedRect clip) - { - } - - public void PopClip() - { - } - - public void PushOpacity(double opacity, Rect bounds) - { - } - - public void PopOpacity() - { - } - - public void PushOpacityMask(IBrush mask, Rect bounds) - { - } - - public void PopOpacityMask() - { - } - - public void PushGeometryClip(IGeometryImpl clip) - { - } - - public void PopGeometryClip() - { - } - - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) - { - } - - public void PopBitmapBlendMode() - { - } - - public void Custom(ICustomDrawOperation custom) - { - } - - public object GetFeature(Type t) => null; - } -} diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs deleted file mode 100644 index 55cd9e8d4b..0000000000 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.UnitTests; -using Avalonia.Media.Imaging; -using Avalonia.Media.TextFormatting; -using Microsoft.Diagnostics.Runtime; - -namespace Avalonia.Benchmarks -{ - internal class NullRenderingPlatform : IPlatformRenderInterface, IPlatformRenderInterfaceContext - { - public IGeometryImpl CreateEllipseGeometry(Rect rect) - { - return new MockStreamGeometryImpl(); - } - - public IGeometryImpl CreateLineGeometry(Point p1, Point p2) - { - return new MockStreamGeometryImpl(); - } - - public IGeometryImpl CreateRectangleGeometry(Rect rect) - { - return new MockStreamGeometryImpl(); - } - - public IStreamGeometryImpl CreateStreamGeometry() - { - return new MockStreamGeometryImpl(); - } - - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) - { - throw new NotImplementedException(); - } - - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) - { - throw new NotImplementedException(); - } - - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) - { - throw new NotImplementedException(); - } - - public bool IsLost => false; - - public object TryGetFeature(Type featureType) => null; - - public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(string fileName) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(Stream stream) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmap(string fileName) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IFontManagerImpl CreateFontManager() - { - return new MockFontManagerImpl(); - } - - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) - { - return new MockStreamGeometryImpl(); - } - - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - IReadOnlyList glyphInfos, Point baselineOrigin, Rect bounds) - { - return new MockGlyphRun(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds); - } - - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) - { - return this; - } - - public bool SupportsIndividualRoundRects => true; - - public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; - - public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; - public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true; - - public void Dispose() - { - - } - } -} diff --git a/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs b/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs deleted file mode 100644 index 1b5b60031c..0000000000 --- a/tests/Avalonia.Benchmarks/NullThreadingPlatform.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Reactive.Disposables; -using System.Threading; -using Avalonia.Platform; -using Avalonia.Threading; - -namespace Avalonia.Benchmarks -{ - internal class NullThreadingPlatform : IDispatcherImpl - { - public void Signal() - { - } - - public void UpdateTimer(long? dueTimeInMs) - { - } - - public bool CurrentThreadIsLoopThread => true; - -#pragma warning disable CS0067 - public event Action Signaled; - public event Action Timer; - public long Now => 0; -#pragma warning restore CS0067 - } -} diff --git a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs index 2905b1e464..ba217017d1 100644 --- a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs +++ b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs @@ -1,4 +1,5 @@ using Avalonia.Controls.Shapes; +using Avalonia.Headless; using Avalonia.Media; using Avalonia.Platform; using BenchmarkDotNet.Attributes; @@ -21,9 +22,9 @@ namespace Avalonia.Benchmarks.Rendering _lineFill = new Line { Fill = new SolidColorBrush() }; _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() }; - _drawingContext = new PlatformDrawingContext(new NullDrawingContextImpl(), true); + _drawingContext = new PlatformDrawingContext(new HeadlessPlatformRenderInterface.HeadlessDrawingContextStub(), true); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new NullRenderingPlatform()); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new HeadlessPlatformRenderInterface()); } [Benchmark] diff --git a/tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs b/tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs index 00080e3b82..9e79b25488 100644 --- a/tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs +++ b/tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs @@ -20,10 +20,7 @@ namespace Avalonia.Benchmarks.Styling public ControlTheme_Change() { - _app = UnitTestApplication.Start( - TestServices.StyledWindow.With( - renderInterface: new NullRenderingPlatform(), - dispatcherImpl: new NullThreadingPlatform())); + _app = UnitTestApplication.Start(TestServices.StyledWindow); // Simulate an application with a lot of styles by creating a tree of nested panels, // each with a bunch of styles applied. diff --git a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs index a32f98e462..ebd8a6474a 100644 --- a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs +++ b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls; +using Avalonia.Headless; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.UnitTests; @@ -20,12 +21,8 @@ namespace Avalonia.Benchmarks.Styling assetLoader: new StandardAssetLoader(), globalClock: new MockGlobalClock(), platform: new AppBuilder().RuntimePlatform, - renderInterface: new MockPlatformRenderInterface(), standardCursorFactory: Mock.Of(), theme: () => CreateTheme(), - dispatcherImpl: new NullThreadingPlatform(), - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl(), windowingPlatform: new MockWindowingPlatform()); return UnitTestApplication.Start(services); diff --git a/tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs b/tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs index 307f15a6c0..03f3f47276 100644 --- a/tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs +++ b/tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs @@ -17,10 +17,7 @@ namespace Avalonia.Benchmarks.Styling public Style_Apply_Detach_Complex() { - _app = UnitTestApplication.Start( - TestServices.StyledWindow.With( - renderInterface: new NullRenderingPlatform(), - dispatcherImpl: new NullThreadingPlatform())); + _app = UnitTestApplication.Start(TestServices.StyledWindow); // Simulate an application with a lot of styles by creating a tree of nested panels, // each with a bunch of styles applied. diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 22ca9d8c6d..bc90abad7e 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -30,10 +30,7 @@ public class HugeTextLayout : IDisposable { _manySmallStrings = Enumerable.Range(0, 1000).Select(_ => RandomString(s_rand.Next(2, 15))).ToArray(); - var testServices = TestServices.StyledWindow.With( - renderInterface: new NullRenderingPlatform(), - dispatcherImpl: new NullThreadingPlatform(), - standardCursorFactory: new NullCursorFactory()); + var testServices = TestServices.StyledWindow; if (s_useSkia) { diff --git a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs index 8eadb3a3f0..9ec3cb9b9a 100644 --- a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs @@ -45,9 +45,6 @@ namespace Avalonia.Benchmarks.Themes private static IDisposable CreateApp() { var services = new TestServices( - renderInterface: new NullRenderingPlatform(), - dispatcherImpl: new NullThreadingPlatform(), - standardCursorFactory: new NullCursorFactory(), theme: () => LoadFluentTheme()); return UnitTestApplication.Start(services); diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 3b2ca51976..6c9aefc5a7 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -2,6 +2,7 @@ using System.Linq; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Headless; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -203,10 +204,10 @@ namespace Avalonia.Controls.UnitTests } private static TestServices Services => TestServices.MockThreadingInterface.With( - fontManagerImpl: new MockFontManagerImpl(), + fontManagerImpl: new HeadlessFontManagerStub(), standardCursorFactory: Mock.Of(), - textShaperImpl: new MockTextShaperImpl(), - renderInterface: new MockPlatformRenderInterface()); + textShaperImpl: new HeadlessTextShaperStub(), + renderInterface: new HeadlessPlatformRenderInterface()); private static IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index dec295169f..3d4852c4ff 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; @@ -1022,12 +1023,12 @@ namespace Avalonia.Controls.UnitTests return UnitTestApplication.Start( TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), - fontManagerImpl: new MockFontManagerImpl(), + fontManagerImpl: new HeadlessFontManagerStub(), keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), - renderInterface: new MockPlatformRenderInterface(), - textShaperImpl: new MockTextShaperImpl())); + renderInterface: new HeadlessPlatformRenderInterface(), + textShaperImpl: new HeadlessTextShaperStub())); } private class ItemsControlWithContainer : ItemsControl, IStyleable diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 1d1065501f..04acd1c36d 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Layout; @@ -891,16 +892,16 @@ namespace Avalonia.Controls.UnitTests keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), - renderInterface: new MockPlatformRenderInterface(), - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl(), + renderInterface: new HeadlessPlatformRenderInterface(), + fontManagerImpl: new HeadlessFontManagerStub(), + textShaperImpl: new HeadlessTextShaperStub(), standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( - renderInterface: new MockPlatformRenderInterface(), + renderInterface: new HeadlessPlatformRenderInterface(), standardCursorFactory: Mock.Of(), - textShaperImpl: new MockTextShaperImpl(), - fontManagerImpl: new MockFontManagerImpl()); + textShaperImpl: new HeadlessTextShaperStub(), + fontManagerImpl: new HeadlessFontManagerStub()); private static IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 4f135d94ee..78ac6bb3e2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Styling; @@ -1347,12 +1348,12 @@ namespace Avalonia.Controls.UnitTests.Primitives return UnitTestApplication.Start( TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), - fontManagerImpl: new MockFontManagerImpl(), + fontManagerImpl: new HeadlessFontManagerStub(), keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), - renderInterface: new MockPlatformRenderInterface(), - textShaperImpl: new MockTextShaperImpl())); + renderInterface: new HeadlessPlatformRenderInterface(), + textShaperImpl: new HeadlessTextShaperStub())); } private class TestSelector : SelectingItemsControl diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 9c858a20e1..ca1986d293 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Layout; @@ -1103,14 +1104,14 @@ namespace Avalonia.Controls.UnitTests keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), standardCursorFactory: Mock.Of(), - textShaperImpl: new MockTextShaperImpl(), - fontManagerImpl: new MockFontManagerImpl()); + textShaperImpl: new HeadlessTextShaperStub(), + fontManagerImpl: new HeadlessFontManagerStub()); private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of(), - renderInterface: new MockPlatformRenderInterface(), - textShaperImpl: new MockTextShaperImpl(), - fontManagerImpl: new MockFontManagerImpl()); + renderInterface: new HeadlessPlatformRenderInterface(), + textShaperImpl: new HeadlessTextShaperStub(), + fontManagerImpl: new HeadlessFontManagerStub()); private IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index bb952ca7e0..9a9c4d352e 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -6,6 +6,7 @@ using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Headless; using Avalonia.Markup.Data; using Avalonia.Platform; using Avalonia.UnitTests; @@ -87,8 +88,8 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of(), - textShaperImpl: new MockTextShaperImpl(), - fontManagerImpl: new MockFontManagerImpl()); + textShaperImpl: new HeadlessTextShaperStub(), + fontManagerImpl: new HeadlessFontManagerStub()); private static IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs index e0bf230d99..1449f3f759 100644 --- a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -2,6 +2,7 @@ using System.Linq; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Headless; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -99,10 +100,10 @@ namespace Avalonia.Controls.UnitTests } private static TestServices Services => TestServices.MockThreadingInterface.With( - fontManagerImpl: new MockFontManagerImpl(), + fontManagerImpl: new HeadlessFontManagerStub(), standardCursorFactory: Mock.Of(), - textShaperImpl: new MockTextShaperImpl(), - renderInterface: new MockPlatformRenderInterface()); + textShaperImpl: new HeadlessTextShaperStub(), + renderInterface: new HeadlessPlatformRenderInterface()); private static IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 12792305e2..7d290b1ec0 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Layout; @@ -1694,12 +1695,12 @@ namespace Avalonia.Controls.UnitTests return UnitTestApplication.Start( TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), - fontManagerImpl: new MockFontManagerImpl(), + fontManagerImpl: new HeadlessFontManagerStub(), keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), - renderInterface: new MockPlatformRenderInterface(), - textShaperImpl: new MockTextShaperImpl())); + renderInterface: new HeadlessPlatformRenderInterface(), + textShaperImpl: new HeadlessTextShaperStub())); } private class Node : NotifyingBase diff --git a/tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs b/tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs new file mode 100644 index 0000000000..a2ba1f8d38 --- /dev/null +++ b/tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs @@ -0,0 +1,7 @@ +global using NUnit.Framework; +global using Avalonia.Headless.NUnit; + +using Avalonia.Headless; +using Avalonia.Headless.UnitTests; + +[assembly: AvaloniaTestApplication(typeof(TestApplication))] diff --git a/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj b/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj new file mode 100644 index 0000000000..1a69fb582a --- /dev/null +++ b/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj @@ -0,0 +1,31 @@ + + + net6.0 + true + $(DefineConstants);NUNIT + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs index 3c0ecbfdb7..e751e14df3 100644 --- a/tests/Avalonia.Headless.UnitTests/InputTests.cs +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -1,16 +1,46 @@ +using System; +using System.Reactive.Disposables; +using System.Threading; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; -using Xunit; namespace Avalonia.Headless.UnitTests; public class InputTests +#if XUNIT + : IDisposable +#endif { - [Fact] + private Window _window; + private Application _setupApp; + +#if NUNIT + [SetUp] + public void SetUp() +#elif XUNIT + public InputTests() +#endif + { + _setupApp = Application.Current; + Dispatcher.UIThread.VerifyAccess(); + _window = new Window + { + Width = 100, + Height = 100 + }; + } + +#if NUNIT + [AvaloniaTest, Timeout(10000)] +#elif XUNIT + [AvaloniaFact(Timeout = 10000)] +#endif public void Should_Click_Button_On_Window() { + Assert.True(_setupApp == Application.Current); + var buttonClicked = false; var button = new Button { @@ -19,18 +49,26 @@ public class InputTests }; button.Click += (_, _) => buttonClicked = true; + + _window.Content = button; + _window.Show(); - var window = new Window - { - Width = 100, - Height = 100, - Content = button - }; - window.Show(); + _window.MouseDown(new Point(50, 50), MouseButton.Left); + _window.MouseUp(new Point(50, 50), MouseButton.Left); - window.MouseDown(new Point(50, 50), MouseButton.Left); - window.MouseUp(new Point(50, 50), MouseButton.Left); - Assert.True(buttonClicked); } + +#if NUNIT + [TearDown] + public void TearDown() +#elif XUNIT + public void Dispose() +#endif + { + Assert.True(_setupApp == Application.Current); + + Dispatcher.UIThread.VerifyAccess(); + _window.Close(); + } } diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs index bc50686235..3f45bf97e4 100644 --- a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -2,13 +2,16 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; -using Xunit; namespace Avalonia.Headless.UnitTests; public class RenderingTests { - [Fact] +#if NUNIT + [AvaloniaTest, Timeout(10000)] +#elif XUNIT + [AvaloniaFact(Timeout = 10000)] +#endif public void Should_Render_Last_Frame_To_Bitmap() { var window = new Window diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs index 7bfa0144f3..ae923bf52e 100644 --- a/tests/Avalonia.Headless.UnitTests/TestApplication.cs +++ b/tests/Avalonia.Headless.UnitTests/TestApplication.cs @@ -1,10 +1,5 @@ using Avalonia.Headless.UnitTests; -using Avalonia.Headless.XUnit; using Avalonia.Themes.Simple; -using Xunit; - -[assembly: AvaloniaTestFramework(typeof(TestApplication))] -[assembly: CollectionBehavior(DisableTestParallelization = true)] namespace Avalonia.Headless.UnitTests; diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs index 419ee5519e..403ff84f2c 100644 --- a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -2,31 +2,52 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; -using Xunit; namespace Avalonia.Headless.UnitTests; public class ThreadingTests { - [Fact] +#if NUNIT + [AvaloniaTest, Timeout(10000)] +#elif XUNIT + [AvaloniaFact(Timeout = 10000)] +#endif public void Should_Be_On_Dispatcher_Thread() { Dispatcher.UIThread.VerifyAccess(); } + +#if NUNIT + [AvaloniaTest(Ignore = "This test should always fail, enable to test if it fails")] +#elif XUNIT + [AvaloniaFact(Skip = "This test should always fail, enable to test if it fails")] +#endif + public void Should_Fail_Test_On_Delayed_Post_When_FlushDispatcher() + { + Dispatcher.UIThread.Post(() => throw new InvalidOperationException(), DispatcherPriority.Default); + } - [Fact] - public async Task DispatcherTimer_Works_On_The_Same_Thread() +#if NUNIT + [AvaloniaTheory, Timeout(10000), TestCase(1), TestCase(10), TestCase(100)] +#elif XUNIT + [AvaloniaTheory(Timeout = 10000), InlineData(1), InlineData(10), InlineData(100)] +#endif + public async Task DispatcherTimer_Works_On_The_Same_Thread(int interval) { + await Task.Delay(100); + var currentThread = Thread.CurrentThread; var tcs = new TaskCompletionSource(); + var hasCompleted = false; DispatcherTimer.RunOnce(() => { - Assert.Equal(currentThread, Thread.CurrentThread); + hasCompleted = currentThread == Thread.CurrentThread; tcs.SetResult(); - }, TimeSpan.FromTicks(1)); + }, TimeSpan.FromTicks(interval)); - await tcs.Task; + await tcs.Task; + Assert.True(hasCompleted); } } diff --git a/tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs b/tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs new file mode 100644 index 0000000000..3e4629907a --- /dev/null +++ b/tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs @@ -0,0 +1,7 @@ +global using Xunit; +global using Avalonia.Headless.XUnit; +using Avalonia.Headless; +using Avalonia.Headless.UnitTests; +using Avalonia.Headless.XUnit; + +[assembly: AvaloniaTestApplication(typeof(TestApplication))] diff --git a/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj b/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj similarity index 83% rename from tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj rename to tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj index 78a3ab186e..25578c3ad8 100644 --- a/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj +++ b/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj @@ -1,9 +1,10 @@ - net6.0 true + $(DefineConstants);XUNIT + @@ -11,6 +12,10 @@ + + + + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 062195b003..06589beddf 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -402,6 +402,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml [Fact] public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater() { + GC.KeepAlive(typeof(ItemsRepeater)); + using (UnitTestApplication.Start(TestServices.StyledWindow)) { var xaml = @" diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index 8ca16bd873..62f1cd2032 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Headless; using Avalonia.Media; using Avalonia.UnitTests; using SkiaSharp; @@ -90,7 +91,7 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Only_Try_To_Create_GlyphTypeface_Once() { - var fontManagerImpl = new MockFontManagerImpl(); + var fontManagerImpl = new HeadlessFontManagerStub(); using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: fontManagerImpl))) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index c84fcaaa9a..543dd0805e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; +using Avalonia.Headless; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 1a39dd5223..1feaefbcc1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; +using Avalonia.Headless; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index aec6647226..0d49f78dd9 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs deleted file mode 100644 index f6f3de3bc6..0000000000 --- a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Threading; - -namespace Avalonia.UnitTests -{ - /// - /// Immediately invokes dispatched jobs on the current thread. - /// - public class ImmediateDispatcher : IDispatcher - { - /// - public bool CheckAccess() - { - return true; - } - - /// - public void Post(Action action, DispatcherPriority priority) - { - action(); - } - - /// - public void Post(SendOrPostCallback action, object arg, DispatcherPriority priority) - { - action(arg); - } - - /// - public Task InvokeAsync(Action action, DispatcherPriority priority) - { - action(); - return Task.CompletedTask; - } - - /// - public Task InvokeAsync(Func function, DispatcherPriority priority) - { - var result = function(); - return Task.FromResult(result); - } - - /// - public Task InvokeAsync(Func function, DispatcherPriority priority) - { - return function(); - } - - /// - public Task InvokeAsync(Func> function, DispatcherPriority priority) - { - return function(); - } - - /// - public void VerifyAccess() - { - } - } -} diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs deleted file mode 100644 index 16423884b3..0000000000 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using Avalonia.Media; -using Avalonia.Platform; - -namespace Avalonia.UnitTests -{ - public class MockFontManagerImpl : IFontManagerImpl - { - private readonly string _defaultFamilyName; - - public MockFontManagerImpl(string defaultFamilyName = "Default") - { - _defaultFamilyName = defaultFamilyName; - } - - public int TryCreateGlyphTypefaceCount { get; private set; } - - public string GetDefaultFontFamilyName() - { - return _defaultFamilyName; - } - - string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) - { - return new[] { _defaultFamilyName }; - } - - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, - CultureInfo culture, out Typeface fontKey) - { - fontKey = new Typeface(_defaultFamilyName); - - return false; - } - - public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, - FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) - { - glyphTypeface = null; - - TryCreateGlyphTypefaceCount++; - - if (familyName == "Unknown") - { - return false; - } - - glyphTypeface = new MockGlyphTypeface(); - - return true; - } - - public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) - { - glyphTypeface = new MockGlyphTypeface(); - - return true; - } - } -} diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs deleted file mode 100644 index 805c0b6a8a..0000000000 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Platform; - -namespace Avalonia.UnitTests -{ - public class MockGlyphRun : IGlyphRunImpl - { - public MockGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, Point baselineOrigin, Rect bounds) - { - GlyphTypeface = glyphTypeface; - FontRenderingEmSize = fontRenderingEmSize; - BaselineOrigin = baselineOrigin; - Bounds =bounds; - } - - public IGlyphTypeface GlyphTypeface { get; } - - public double FontRenderingEmSize { get; } - - public Point BaselineOrigin { get; } - - public Rect Bounds { get; } - - public void Dispose() - { - - } - - public IReadOnlyList GetIntersections(float lowerLimit, float upperLimit) - => Array.Empty(); - } -} diff --git a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs deleted file mode 100644 index 5fcee7f515..0000000000 --- a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using Avalonia.Media; - -namespace Avalonia.UnitTests -{ - public class MockGlyphTypeface : IGlyphTypeface - { - public FontMetrics Metrics => new FontMetrics - { - DesignEmHeight = 10, - Ascent = 2, - Descent = 10, - IsFixedPitch = true - }; - - public int GlyphCount => 1337; - - public FontSimulations FontSimulations => throw new NotImplementedException(); - - public string FamilyName => "$Default"; - - public FontWeight Weight { get; } - - public FontStyle Style { get; } - - public FontStretch Stretch { get; } - - public ushort GetGlyph(uint codepoint) - { - return (ushort)codepoint; - } - - public ushort[] GetGlyphs(ReadOnlySpan codepoints) - { - return new ushort[codepoints.Length]; - } - - public int GetGlyphAdvance(ushort glyph) - { - return 8; - } - - public bool TryGetGlyph(uint codepoint, out ushort glyph) - { - glyph = 8; - - return true; - } - - public int[] GetGlyphAdvances(ReadOnlySpan glyphs) - { - var advances = new int[glyphs.Length]; - - for (var i = 0; i < advances.Length; i++) - { - advances[i] = 8; - } - - return advances; - } - - public void Dispose() { } - - public bool TryGetTable(uint tag, out byte[] table) - { - table = null; - return false; - } - - public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) - { - metrics = new GlyphMetrics - { - Width = 10, - Height = 10 - }; - - return true; - } - } -} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs deleted file mode 100644 index 8647461c0e..0000000000 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Media.Imaging; -using Avalonia.Media.TextFormatting; -using Moq; - -namespace Avalonia.UnitTests -{ - public class MockPlatformRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext - { - public IGeometryImpl CreateEllipseGeometry(Rect rect) - { - return Mock.Of(); - } - - public IGeometryImpl CreateLineGeometry(Point p1, Point p2) - { - return Mock.Of(); - } - - public IGeometryImpl CreateRectangleGeometry(Rect rect) - { - return Mock.Of(x => x.Bounds == rect); - } - - class MockRenderTarget : IRenderTarget - { - public void Dispose() - { - - } - - public IDrawingContextImpl CreateDrawingContext() - { - var m = new Mock(); - m.Setup(c => c.CreateLayer(It.IsAny())) - .Returns(() => - { - var r = new Mock(); - r.Setup(r => r.CreateDrawingContext()) - .Returns(CreateDrawingContext()); - return r.Object; - } - ); - return m.Object; - - } - - public bool IsCorrupted => false; - } - - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) - { - return new MockRenderTarget(); - } - - public bool IsLost => false; - - public object TryGetFeature(Type featureType) => null; - - public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) - { - return Mock.Of(); - } - - public IStreamGeometryImpl CreateStreamGeometry() - { - return new MockStreamGeometryImpl(); - } - - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) - { - return Mock.Of(); - } - - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) - { - return Mock.Of(); - } - - public IWriteableBitmapImpl CreateWriteableBitmap( - PixelSize size, - Vector dpi, - PixelFormat format, - AlphaFormat alphaFormat) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(Stream stream) - { - return Mock.Of(); - } - - public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, - BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmap(string fileName) - { - throw new NotImplementedException(); - } - - public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(string fileName) - { - return Mock.Of(); - } - - public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - return Mock.Of(); - } - - public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - return Mock.Of(); - } - - public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) - { - return Mock.Of(); - } - - public IBitmapImpl LoadBitmap( - PixelFormat format, - AlphaFormat alphaFormat, - IntPtr data, - PixelSize size, - Vector dpi, - int stride) - { - throw new NotImplementedException(); - } - - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin, Rect bounds) - { - return new MockGlyphRun(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds); - } - - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; - - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) - { - return Mock.Of(); - } - - public bool SupportsIndividualRoundRects { get; set; } - - public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; - - public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; - public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true; - - public void Dispose() - { - } - } -} diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs deleted file mode 100644 index 9d039a386e..0000000000 --- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Platform; - -namespace Avalonia.UnitTests -{ - public class MockStreamGeometryImpl : IStreamGeometryImpl, ITransformedGeometryImpl - { - private MockStreamGeometryContext _context; - - public MockStreamGeometryImpl() - { - Transform = Matrix.Identity; - _context = new MockStreamGeometryContext(); - } - - public MockStreamGeometryImpl(Matrix transform) - { - Transform = transform; - _context = new MockStreamGeometryContext(); - } - - private MockStreamGeometryImpl(Matrix transform, MockStreamGeometryContext context) - { - Transform = transform; - _context = context; - } - - public IGeometryImpl SourceGeometry { get; } - - public Rect Bounds => _context.CalculateBounds(); - - public double ContourLength { get; } - - public Matrix Transform { get; } - - public IStreamGeometryImpl Clone() - { - return this; - } - - public void Dispose() - { - } - - public bool FillContains(Point point) - { - return _context.FillContains(point); - } - - public bool StrokeContains(IPen pen, Point point) - { - return false; - } - - public Rect GetRenderBounds(IPen pen) => Bounds; - - public IGeometryImpl Intersect(IGeometryImpl geometry) - { - return new MockStreamGeometryImpl(Transform); - } - - public IStreamGeometryContextImpl Open() - { - return _context; - } - - public ITransformedGeometryImpl WithTransform(Matrix transform) - { - return new MockStreamGeometryImpl(transform, _context); - } - - public bool TryGetPointAtDistance(double distance, out Point point) - { - point = new Point(); - return false; - } - - public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) - { - point = new Point(); - tangent = new Point(); - return false; - } - - public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) - { - segmentGeometry = null; - return false; - } - - class MockStreamGeometryContext : IStreamGeometryContextImpl - { - private List points = new List(); - public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) - { - } - - public void BeginFigure(Point startPoint, bool isFilled) - { - points.Add(startPoint); - } - - public Rect CalculateBounds() - { - var left = double.MaxValue; - var right = double.MinValue; - var top = double.MaxValue; - var bottom = double.MinValue; - - foreach (var p in points) - { - left = Math.Min(p.X, left); - right = Math.Max(p.X, right); - top = Math.Min(p.Y, top); - bottom = Math.Max(p.Y, bottom); - } - - return new Rect(new Point(left, top), new Point(right, bottom)); - } - - public void CubicBezierTo(Point point1, Point point2, Point point3) - { - } - - public void Dispose() - { - } - - public void EndFigure(bool isClosed) - { - } - - public void LineTo(Point point) - { - points.Add(point); - } - - public void QuadraticBezierTo(Point control, Point endPoint) - { - throw new NotImplementedException(); - } - - public void SetFillRule(FillRule fillRule) - { - } - - public bool FillContains(Point point) - { - // Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html - // to determine if the point is in the geometry (since it will always be convex in this situation) - for (int i = 0; i < points.Count; i++) - { - var a = points[i]; - var b = points[(i + 1) % points.Count]; - var c = points[(i + 2) % points.Count]; - - Vector v0 = c - a; - Vector v1 = b - a; - Vector v2 = point - a; - - var dot00 = v0 * v0; - var dot01 = v0 * v1; - var dot02 = v0 * v2; - var dot11 = v1 * v1; - var dot12 = v1 * v2; - - - var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); - var u = (dot11 * dot02 - dot01 * dot12) * invDenom; - var v = (dot00 * dot12 - dot01 * dot02) * invDenom; - if ((u >= 0) && (v >= 0) && (u + v < 1)) return true; - } - return false; - } - } - } -} diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs deleted file mode 100644 index b5f4777192..0000000000 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using Avalonia.Media.TextFormatting; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Platform; - -namespace Avalonia.UnitTests -{ - public class MockTextShaperImpl : ITextShaperImpl - { - public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) - { - var typeface = options.Typeface; - var fontRenderingEmSize = options.FontRenderingEmSize; - var bidiLevel = options.BidiLevel; - var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); - var textSpan = text.Span; - var textStartIndex = TextTestHelper.GetStartCharIndex(text); - - for (var i = 0; i < shapedBuffer.Length;) - { - var glyphCluster = i + textStartIndex; - - var codepoint = Codepoint.ReadAt(textSpan, i, out var count); - - var glyphIndex = typeface.GetGlyph(codepoint); - - for (var j = 0; j < count; ++j) - { - shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); - } - - i += count; - } - - return shapedBuffer; - } - } -} diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 9b95e71d8c..f226a7ef55 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using Avalonia.Controls; using System.Reflection; using Avalonia.Animation; +using Avalonia.Headless; using Avalonia.Threading; namespace Avalonia.UnitTests @@ -22,25 +23,25 @@ namespace Avalonia.UnitTests public static readonly TestServices StyledWindow = new TestServices( assetLoader: new StandardAssetLoader(), platform: new StandardRuntimePlatform(), - renderInterface: new MockPlatformRenderInterface(), - standardCursorFactory: Mock.Of(), + renderInterface: new HeadlessPlatformRenderInterface(), + standardCursorFactory: new HeadlessCursorFactoryStub(), theme: () => CreateSimpleTheme(), - dispatcherImpl: Mock.Of(x => x.CurrentThreadIsLoopThread == true), - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl(), + dispatcherImpl: new NullDispatcherImpl(), + fontManagerImpl: new HeadlessFontManagerStub(), + textShaperImpl: new HeadlessTextShaperStub(), windowingPlatform: new MockWindowingPlatform()); public static readonly TestServices MockPlatformRenderInterface = new TestServices( assetLoader: new StandardAssetLoader(), - renderInterface: new MockPlatformRenderInterface(), - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl()); + renderInterface: new HeadlessPlatformRenderInterface(), + fontManagerImpl: new HeadlessFontManagerStub(), + textShaperImpl: new HeadlessTextShaperStub()); public static readonly TestServices MockPlatformWrapper = new TestServices( platform: Mock.Of()); public static readonly TestServices MockThreadingInterface = new TestServices( - dispatcherImpl: Mock.Of(x => x.CurrentThreadIsLoopThread == true)); + dispatcherImpl: new NullDispatcherImpl()); public static readonly TestServices MockWindowingPlatform = new TestServices( windowingPlatform: new MockWindowingPlatform()); @@ -51,13 +52,13 @@ namespace Avalonia.UnitTests keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), assetLoader: new StandardAssetLoader(), - renderInterface: new MockPlatformRenderInterface(), - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl()); + renderInterface: new HeadlessPlatformRenderInterface(), + fontManagerImpl: new HeadlessFontManagerStub(), + textShaperImpl: new HeadlessTextShaperStub()); public static readonly TestServices TextServices = new TestServices( assetLoader: new StandardAssetLoader(), - renderInterface: new MockPlatformRenderInterface(), + renderInterface: new HeadlessPlatformRenderInterface(), fontManagerImpl: new HarfBuzzFontManagerImpl(), textShaperImpl: new HarfBuzzTextShaperImpl()); @@ -158,12 +159,5 @@ namespace Avalonia.UnitTests { return new SimpleTheme(); } - - private static IPlatformRenderInterface CreateRenderInterfaceMock() - { - return Mock.Of(x => - x.CreateStreamGeometry() == Mock.Of( - y => y.Open() == Mock.Of())); - } } } diff --git a/tests/Avalonia.UnitTests/TextTestHelper.cs b/tests/Avalonia.UnitTests/TextTestHelper.cs deleted file mode 100644 index b572333027..0000000000 --- a/tests/Avalonia.UnitTests/TextTestHelper.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Avalonia.UnitTests -{ - public static class TextTestHelper - { - public static int GetStartCharIndex(ReadOnlyMemory text) - { - if (!MemoryMarshal.TryGetString(text, out _, out var start, out _)) - throw new InvalidOperationException("text memory should have been a string"); - return start; - } - } -}