Browse Source

Merge branch 'master' into fixes/DevTools/WithoutApplicationLifetime

pull/10510/head
Max Katz 3 years ago
committed by GitHub
parent
commit
13c98937b3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .editorconfig
  2. 4
      .nuke/build.schema.json
  3. 47
      Avalonia.sln
  4. 5
      dirs.proj
  5. 13
      nukebuild/Build.cs
  6. 39
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  7. 8
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  8. 1
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  9. 14
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  10. 1
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  11. 4
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  12. 17
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  13. 8
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  14. 1
      src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
  15. 2
      src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs
  16. 21
      src/Avalonia.Controls/AppBuilder.cs
  17. 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  18. 4
      src/Avalonia.Controls/Control.cs
  19. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  20. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  21. 53
      src/Avalonia.FreeDesktop/DBusPlatformSettings.cs
  22. 2
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  23. 9
      src/Avalonia.Remote.Protocol/MetsysBson.cs
  24. 19
      src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
  25. 25
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs
  26. 116
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs
  27. 24
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs
  28. 8
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  29. 35
      src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs
  30. 126
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs
  31. 47
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs
  32. 98
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs
  33. 8
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs
  34. 21
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs
  35. 61
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs
  36. 37
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs
  37. 31
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs
  38. 9
      src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
  39. 17
      src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  40. 27
      src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs
  41. 99
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  42. 109
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  43. 214
      src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
  44. 21
      src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
  45. 4
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  46. 7
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  47. 5
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  48. 1
      tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
  49. 285
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  50. 7
      tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs
  51. 17
      tests/Avalonia.Benchmarks/NullCursorFactory.cs
  52. 107
      tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
  53. 149
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  54. 27
      tests/Avalonia.Benchmarks/NullThreadingPlatform.cs
  55. 5
      tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs
  56. 5
      tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs
  57. 5
      tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs
  58. 5
      tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs
  59. 5
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
  60. 3
      tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs
  61. 7
      tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
  62. 7
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  63. 13
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  64. 7
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  65. 11
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  66. 5
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
  67. 7
      tests/Avalonia.Controls.UnitTests/TimePickerTests.cs
  68. 7
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  69. 7
      tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs
  70. 31
      tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj
  71. 62
      tests/Avalonia.Headless.UnitTests/InputTests.cs
  72. 7
      tests/Avalonia.Headless.UnitTests/RenderingTests.cs
  73. 5
      tests/Avalonia.Headless.UnitTests/TestApplication.cs
  74. 35
      tests/Avalonia.Headless.UnitTests/ThreadingTests.cs
  75. 7
      tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs
  76. 7
      tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj
  77. 2
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  78. 3
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
  79. 1
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  80. 1
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  81. 1
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  82. 62
      tests/Avalonia.UnitTests/ImmediateDispatcher.cs
  83. 63
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  84. 34
      tests/Avalonia.UnitTests/MockGlyphRun.cs
  85. 81
      tests/Avalonia.UnitTests/MockGlyphTypeface.cs
  86. 174
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  87. 179
      tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
  88. 38
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs
  89. 34
      tests/Avalonia.UnitTests/TestServices.cs
  90. 15
      tests/Avalonia.UnitTests/TextTestHelper.cs

4
.editorconfig

@ -177,7 +177,9 @@ dotnet_diagnostic.CA1828.severity = warning
dotnet_diagnostic.CA1829.severity = warning
#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning
#CACA2211:Non-constant fields should not be visible
#CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
dotnet_diagnostic.CA1854.severity = warning
#CA2211:Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = error
# Wrapping preferences

4
.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}'",

47
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}"
@ -265,7 +263,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
@ -598,14 +602,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
@ -634,14 +638,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
@ -711,16 +727,21 @@ 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}
{F8928267-688E-4A51-989C-612A72446D33} = {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}

5
dirs.proj

@ -9,10 +9,11 @@
<ProjectReference Remove="**/*.shproj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
<!-- Exclude iOS, Android and Web samples from build -->
<!-- Exclude iOS, Android and Browser samples from build -->
<ProjectReference Remove="samples/*.iOS/*.csproj" />
<ProjectReference Remove="samples/*.Android/*.csproj" />
<ProjectReference Remove="samples/*.Web/*.csproj" />
<ProjectReference Remove="samples/*.Browser/*.csproj" />
<ProjectReference Remove="samples/*.Blazor/*.csproj" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows')) OR '$(MSBuildRuntimeType)' != 'Full'">
<ProjectReference Remove="src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj" />

13
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<Build>(x => x.Package)
? Execute<Build>(x => x.RunToolsTests)
: Execute<Build>(x => x.RunTests);
}

39
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -729,6 +729,8 @@ namespace Avalonia.Controls
RowDetailsTemplateProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsTemplateChanged(e));
RowDetailsVisibilityModeProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsVisibilityModeChanged(e));
AutoGenerateColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnAutoGenerateColumnsChanged(e));
FocusableProperty.OverrideDefaultValue<DataGrid>(true);
}
/// <summary>
@ -2478,7 +2480,7 @@ namespace Avalonia.Controls
if (_hScrollBar != null)
{
//_hScrollBar.IsTabStop = false;
_hScrollBar.IsTabStop = false;
_hScrollBar.Maximum = 0.0;
_hScrollBar.Orientation = Orientation.Horizontal;
_hScrollBar.IsVisible = false;
@ -2494,7 +2496,7 @@ namespace Avalonia.Controls
if (_vScrollBar != null)
{
//_vScrollBar.IsTabStop = false;
_vScrollBar.IsTabStop = false;
_vScrollBar.Maximum = 0.0;
_vScrollBar.Orientation = Orientation.Vertical;
_vScrollBar.IsVisible = false;
@ -3734,7 +3736,7 @@ namespace Avalonia.Controls
if (sender is Control editingElement)
{
editingElement.LostFocus -= EditingElement_LostFocus;
if (EditingRow != null && EditingColumnIndex != -1)
if (EditingRow != null && _editingColumnIndex != -1)
{
FocusEditingCell(true);
}
@ -4039,18 +4041,22 @@ namespace Avalonia.Controls
return true;
}
Debug.Assert(EditingRow != null);
var editingRow = EditingRow;
if (editingRow is null)
{
return true;
}
Debug.Assert(_editingColumnIndex >= 0);
Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
// Cache these to see if they change later
int currentSlot = CurrentSlot;
int currentColumnIndex = CurrentColumnIndex;
// We're ready to start ending, so raise the event
DataGridCell editingCell = EditingRow.Cells[_editingColumnIndex];
DataGridCell editingCell = editingRow.Cells[_editingColumnIndex];
var editingElement = editingCell.Content as Control;
if (editingElement == null)
{
@ -4058,7 +4064,7 @@ namespace Avalonia.Controls
}
if (raiseEvents)
{
DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, EditingRow, editingElement, editAction);
DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, editingRow, editingElement, editAction);
OnCellEditEnding(e);
if (e.Cancel)
{
@ -4112,7 +4118,7 @@ namespace Avalonia.Controls
}
else
{
if (EditingRow != null)
if (editingRow != null)
{
if (editingCell.IsValid)
{
@ -4120,10 +4126,10 @@ namespace Avalonia.Controls
editingCell.UpdatePseudoClasses();
}
if (EditingRow.IsValid)
if (editingRow.IsValid)
{
EditingRow.IsValid = false;
EditingRow.UpdatePseudoClasses();
editingRow.IsValid = false;
editingRow.UpdatePseudoClasses();
}
}
@ -4169,22 +4175,22 @@ namespace Avalonia.Controls
PopulateCellContent(
isCellEdited: !exitEditingMode,
dataGridColumn: CurrentColumn,
dataGridRow: EditingRow,
dataGridRow: editingRow,
dataGridCell: editingCell);
EditingRow.InvalidateDesiredHeight();
editingRow.InvalidateDesiredHeight();
var column = editingCell.OwningColumn;
if (column.Width.IsSizeToCells || column.Width.IsAuto)
{// Invalidate desired width and force recalculation
column.SetWidthDesiredValue(0);
EditingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
editingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
}
}
// We're done, so raise the CellEditEnded event
if (raiseEvents)
{
OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, EditingRow, editAction));
OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, editingRow, editAction));
}
// There's a chance that somebody reopened this cell for edit within the CellEditEnded handler,
@ -4427,8 +4433,7 @@ namespace Avalonia.Controls
dataGridCell.Focus();
success = dataGridCell.ContainsFocusedElement();
}
//TODO Check
//success = dataGridCell.ContainsFocusedElement() ? true : dataGridCell.Focus();
_focusEditingControl = !success;
}
return success;

8
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -33,6 +33,8 @@ namespace Avalonia.Controls
{
PointerPressedEvent.AddClassHandler<DataGridCell>(
(x,e) => x.DataGridCell_PointerPressed(e), handledEventsToo: true);
FocusableProperty.OverrideDefaultValue<DataGridCell>(true);
IsTabStopProperty.OverrideDefaultValue<DataGridCell>(false);
}
public DataGridCell()
{ }
@ -169,8 +171,7 @@ namespace Avalonia.Controls
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
@ -190,8 +191,7 @@ namespace Avalonia.Controls
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}

1
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -72,6 +72,7 @@ namespace Avalonia.Controls
{
AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x, e) => x.OnAreSeparatorsVisibleChanged(e));
PressedMixin.Attach<DataGridColumnHeader>();
IsTabStopProperty.OverrideDefaultValue<DataGridColumnHeader>(false);
}
/// <summary>

14
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@ -12,6 +12,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Reflection;
using Avalonia.Layout;
namespace Avalonia.Controls
{
@ -489,7 +490,7 @@ namespace Avalonia.Controls
{
DataGridFillerColumn fillerColumn = ColumnsInternal.FillerColumn;
double totalColumnsWidth = ColumnsInternal.VisibleEdgedColumnsWidth;
if (finalWidth > totalColumnsWidth)
if (finalWidth - totalColumnsWidth > LayoutHelper.LayoutEpsilon)
{
fillerColumn.FillerWidth = finalWidth - totalColumnsWidth;
}
@ -971,6 +972,12 @@ namespace Avalonia.Controls
{
cx += _negHorizontalOffset;
_horizontalOffset -= _negHorizontalOffset;
if (_horizontalOffset < LayoutHelper.LayoutEpsilon)
{
// Snap to zero to avoid trying to partially scroll in first scrolled off column below
_horizontalOffset = 0;
}
_negHorizontalOffset = 0;
}
else
@ -979,6 +986,11 @@ namespace Avalonia.Controls
_negHorizontalOffset -= displayWidth - cx;
cx = displayWidth;
}
// Make sure the HorizontalAdjustment is not greater than the new HorizontalOffset
// since it would cause an assertion failure in DataGridCellsPresenter.ShouldDisplayCell
// called by DataGridCellsPresenter.MeasureOverride.
HorizontalAdjustment = Math.Min(HorizontalAdjustment, _horizontalOffset);
}
// second try to scroll entire columns
if (cx < displayWidth && _horizontalOffset > 0)

1
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -128,6 +128,7 @@ namespace Avalonia.Controls
DetailsTemplateProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnDetailsTemplateChanged(e));
AreDetailsVisibleProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnAreDetailsVisibleChanged(e));
PointerPressedEvent.AddClassHandler<DataGridRow>((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true);
IsTabStopProperty.OverrideDefaultValue<DataGridRow>(false);
}
/// <summary>

4
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -106,6 +106,7 @@ namespace Avalonia.Controls
{
SublevelIndentProperty.Changed.AddClassHandler<DataGridRowGroupHeader>((x,e) => x.OnSublevelIndentChanged(e));
PressedMixin.Attach<DataGridRowGroupHeader>();
IsTabStopProperty.OverrideDefaultValue<DataGridRowGroupHeader>(false);
}
/// <summary>
@ -301,8 +302,7 @@ namespace Avalonia.Controls
}
else
{
//if (!e.Handled && OwningGrid.IsTabStop)
if (!e.Handled)
if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}

17
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@ -1589,6 +1589,23 @@ namespace Avalonia.Controls
CorrectSlotsAfterDeletion(slot, isRow);
OnRemovedElement(slot, item);
// Synchronize CurrentCellCoordinates, CurrentColumn, CurrentColumnIndex, CurrentItem
// and CurrentSlot with the currently edited cell, since OnRemovingElement called
// SetCurrentCellCore(-1, -1) to temporarily reset the current cell.
if (_temporarilyResetCurrentCell &&
_editingColumnIndex != -1 &&
_previousCurrentItem != null &&
EditingRow != null &&
EditingRow.Slot != -1)
{
ProcessSelectionAndCurrency(
columnIndex: _editingColumnIndex,
item: _previousCurrentItem,
backupSlot: this.EditingRow.Slot,
action: DataGridSelectionAction.None,
scrollIntoView: false);
}
}
private void RemoveNonDisplayedRows(int newFirstDisplayedSlot, int newLastDisplayedSlot)

8
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -82,7 +82,6 @@
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="15" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="CellBorder"
@ -157,7 +156,6 @@
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="Padding" Value="12,0,0,0" />
<Setter Property="FontSize" Value="12" />
@ -268,7 +266,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRowHeader}" TargetType="DataGridRowHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="AreSeparatorsVisible" Value="False" />
<Setter Property="Template">
@ -310,7 +307,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRow}" TargetType="DataGridRow">
<Setter Property="Focusable" Value="False" />
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
<Setter Property="Template">
<ControlTemplate>
@ -408,7 +404,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRowGroupHeader}" TargetType="DataGridRowGroupHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
<Setter Property="FontSize" Value="15" />
@ -433,7 +428,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
Focusable="False"
IsTabStop="False"
Foreground="{TemplateBinding Foreground}" />
<StackPanel Grid.Column="3"
@ -503,6 +498,7 @@
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="FocusAdorner" Value="{x:Null}" />
<Setter Property="DropLocationIndicatorTemplate">
<Template>
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"

1
src/Avalonia.Controls.DataGrid/Themes/Simple.xaml

@ -126,7 +126,6 @@
<ControlTheme x:Key="{x:Type DataGridRowHeader}"
TargetType="DataGridRowHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="PART_Root"

2
src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs

@ -54,7 +54,7 @@ namespace Avalonia.Controls.Utils
/// <returns>True if the currently focused element is within the visual tree of the parent</returns>
internal static bool ContainsFocusedElement(this Visual element)
{
return (element == null) ? false : element.ContainsChild(FocusManager.Instance.Current as Visual);
return element is InputElement { IsKeyboardFocusWithin: true };
}
}
}

21
src/Avalonia.Controls/AppBuilder.cs

@ -288,17 +288,26 @@ namespace Avalonia
}
s_setupWasAlreadyCalled = true;
SetupUnsafe();
}
/// <summary>
/// Setup method that doesn't check for input initalizers being set.
/// Nor
/// </summary>
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();
}
}

1
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -18,7 +18,6 @@
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)"/>
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless.XUnit, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport.Remote, PublicKey=$(AvaloniaPublicKey)" />

4
src/Avalonia.Controls/Control.cs

@ -403,7 +403,9 @@ namespace Avalonia.Controls
{
if (_focusAdorner == null)
{
var template = GetValue(FocusAdornerProperty) ?? adornerLayer.DefaultFocusAdorner;
var template = IsSet(FocusAdornerProperty)
? GetValue(FocusAdornerProperty)
: adornerLayer.DefaultFocusAdorner;
if (template != null)
{

4
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -237,9 +237,9 @@ namespace Avalonia.Diagnostics.Views
else
{
//TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1
if (_frozenPopupStates.ContainsKey(popup))
if (_frozenPopupStates.TryGetValue(popup, out var value))
{
_frozenPopupStates[popup].Dispose();
value.Dispose();
_frozenPopupStates.Remove(popup);
}
}

2
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Tmds.DBus.Protocol" Version="0.15.0" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.6" PrivateAssets="All" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.7" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

53
src/Avalonia.FreeDesktop/DBusPlatformSettings.cs

@ -1,8 +1,8 @@
using System;
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Platform;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop
@ -22,39 +22,47 @@ namespace Avalonia.FreeDesktop
_settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
_ = _settings.WatchSettingChangedAsync(SettingsChangedHandler);
_ = TryGetInitialValueAsync();
_ = TryGetInitialValuesAsync();
}
public override PlatformColorValues GetColorValues() => _lastColorValues ?? base.GetColorValues();
private async Task TryGetInitialValueAsync()
private async Task TryGetInitialValuesAsync()
{
_themeVariant = await TryGetThemeVariantAsync();
_accentColor = await TryGetAccentColorAsync();
_lastColorValues = BuildPlatformColorValues();
if (_lastColorValues is not null)
OnColorValuesChanged(_lastColorValues);
}
private async Task<PlatformThemeVariant?> TryGetThemeVariantAsync()
{
try
{
var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme");
_themeVariant = ReadAsColorScheme(value);
return ToColorScheme(((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value);
}
catch (Exception ex)
catch (DBusException)
{
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.freedesktop.appearance.color-scheme value", ex);
return null;
}
}
private async Task<Color?> TryGetAccentColorAsync()
{
try
{
var value = await _settings!.ReadAsync("org.kde.kdeglobals.General", "AccentColor");
_accentColor = ReadAsAccentColor(value);
return ToAccentColor(((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value);
}
catch (Exception ex)
catch (DBusException)
{
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.kde.kdeglobals.General.AccentColor value", ex);
return null;
}
_lastColorValues = BuildPlatformColorValues();
if (_lastColorValues is not null)
OnColorValuesChanged(_lastColorValues);
}
private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple)
private async void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple)
{
if (exception is not null)
return;
@ -62,12 +70,8 @@ namespace Avalonia.FreeDesktop
switch (valueTuple)
{
case ("org.freedesktop.appearance", "color-scheme", { } colorScheme):
_themeVariant = ReadAsColorScheme(colorScheme);
_lastColorValues = BuildPlatformColorValues();
OnColorValuesChanged(_lastColorValues!);
break;
case ("org.kde.kdeglobals.General", "AccentColor", { } accentColor):
_accentColor = ReadAsAccentColor(accentColor);
_themeVariant = ToColorScheme((colorScheme.Value as DBusUInt32Item)!.Value);
_accentColor = await TryGetAccentColorAsync();
_lastColorValues = BuildPlatformColorValues();
OnColorValuesChanged(_lastColorValues!);
break;
@ -85,21 +89,20 @@ namespace Avalonia.FreeDesktop
return null;
}
private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value)
private static PlatformThemeVariant ToColorScheme(uint value)
{
/*
<member>0: No preference</member>
<member>1: Prefer dark appearance</member>
<member>2: Prefer light appearance</member>
*/
var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1;
var isDark = value == 1;
return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light;
}
private static Color ReadAsAccentColor(DBusVariantItem value)
private static Color ToAccentColor(string value)
{
var colorStr = ((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value;
var rgb = colorStr.Split(',');
var rgb = value.Split(',');
return new Color(255, byte.Parse(rgb[0]), byte.Parse(rgb[1]), byte.Parse(rgb[2]));
}
}

2
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -21,7 +21,7 @@ namespace Avalonia.FreeDesktop
var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
try
{
await dbusFileChooser.GetVersionAsync();
await dbusFileChooser.GetVersionPropertyAsync();
}
catch
{

9
src/Avalonia.Remote.Protocol/MetsysBson.cs

@ -715,7 +715,8 @@ namespace Metsys.Bson
public MagicProperty FindProperty(string name)
{
return _properties.ContainsKey(name) ? _properties[name] : null;
_properties.TryGetValue(name, out var property);
return property;
}
public static TypeHelper GetHelperForType(Type type)
@ -1196,7 +1197,9 @@ namespace Metsys.Bson
}
object container = null;
var property = typeHelper.FindProperty(name);
var propertyType = property != null ? property.Type : _typeMap.ContainsKey(storageType) ? _typeMap[storageType] : typeof(object);
var propertyType = property?.Type
?? (_typeMap.TryGetValue(storageType, out var type1) ? type1 : null)
?? typeof(object);
if (property != null && property.Setter == null)
{
container = property.Getter(instance);
@ -1588,7 +1591,7 @@ namespace Metsys.Bson.Configuration
{
return property;
}
return map.ContainsKey(property) ? map[property] : property;
return map.TryGetValue(property, out var value) ? value : property;
}
public void AddIgnore<T>(string name)

19
src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<IsTestProject>false</IsTestProject>
</PropertyGroup>
<ItemGroup>
<!-- Use lower minor version, as it is supposed to be compatible -->
<PackageReference Include="NUnit" Version="3.13.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
</Project>

25
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;
/// <summary>
/// Identifies a nunit test that starts on Avalonia Dispatcher
/// such that awaited expressions resume on the test's "main thread".
/// </summary>
[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);
}
}

116
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<Action> _beforeTest;
private readonly List<Action> _afterTest;
// There are multiple problems with NUnit integration at the moment when we wrote this integration.
// NUnit doesn't have extensibility API for running on custom dispatcher/sync-context.
// See https://github.com/nunit/nunit/issues/2917 https://github.com/nunit/nunit/issues/2774
// To workaround that we had to replace inner TestMethodCommand with our own implementation while keeping original hierarchy of commands.
// Which will respect proper async/await awaiting code that works with our session and can be block-awaited to fit in NUnit.
// Also, we need to push BeforeTest/AfterTest callbacks to the very same session call.
// I hope there will be a better solution without reflection, but for now that's it.
private static FieldInfo s_innerCommand = typeof(DelegatingTestCommand)
.GetField("innerCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static FieldInfo s_beforeTest = typeof(BeforeAndAfterTestCommand)
.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<Action> beforeTest,
List<Action> 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<Action>(), new List<Action>());
}
private static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command, List<Action> before, List<Action> after)
{
if (command is BeforeAndAfterTestCommand beforeAndAfterTestCommand)
{
if (s_beforeTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> beforeTest)
{
Action<TestExecutionContext> beforeAction = c => before.Add(() => beforeTest(c));
s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction);
}
if (s_afterTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> afterTest)
{
Action<TestExecutionContext> 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<TestResult> 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;
}
}

24
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;
/// <summary>
/// Identifies a nunit theory that starts on Avalonia Dispatcher
/// such that awaited expressions resume on the test's "main thread".
/// </summary>
[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);
}
}

8
src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<IsTestProject>false</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.core" Version="2.4.2" />
<!-- Use lower minor version, as it is supposed to be compatible -->
<PackageReference Include="xunit.core" Version="2.4.0" />
</ItemGroup>
<ItemGroup>

35
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;
/// <summary>
/// Identifies an xunit test that starts on Avalonia Dispatcher
/// such that awaited expressions resume on the test's "main thread".
/// </summary>
[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);
}
}

126
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<IXunitTestCase> 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<RunSummary> RunTestCollectionAsync(
IMessageBus messageBus,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> 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<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer,
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection,
testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
{
_session = session;
}
protected override Task<RunSummary> RunTestClassAsync(
ITestClass testClass,
IReflectionTypeInfo @class,
IEnumerable<IXunitTestCase> 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<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus,
ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource, IDictionary<Type, object> collectionFixtureMappings) :
base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator,
cancellationTokenSource, collectionFixtureMappings)
{
_session = session;
}
protected override Task<RunSummary> RunTestMethodAsync(
ITestMethod testMethod,
IReflectionMethodInfo method,
IEnumerable<IXunitTestCase> 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<IXunitTestCase> 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<RunSummary> RunTestCaseAsync(IXunitTestCase testCase)
{
return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason,
_constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator,
_cancellationTokenSource);
}
}
}

47
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<RunSummary> 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);
}
}

98
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<RunSummary> 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<BeforeAfterTestAttribute> 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<BeforeAfterTestAttribute> beforeAfterAttributes, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments,
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource)
{
_onAfterTestInvoked = onAfterTestInvoked;
}
protected override Task<decimal> 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<BeforeAfterTestAttribute> 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);
}
}
}
}

8
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<TAppBuilderEntry> : XunitTestFramework
internal class AvaloniaTestFramework : XunitTestFramework
{
public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink)
{
@ -26,8 +27,7 @@ internal class AvaloniaTestFramework<TAppBuilderEntry> : XunitTestFramework
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
{
executionOptions.SetValue("xunit.execution.DisableParallelization", false);
using (var assemblyRunner = new AvaloniaTestRunner<TAppBuilderEntry>(
using (var assemblyRunner = new AvaloniaTestAssemblyRunner(
TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
executionOptions)) await assemblyRunner.RunAsync();
}

21
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;
/// <summary>
/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
/// </summary>
/// <remarks>
/// It is an alternative to using [AvaloniaFact] or [AvaloniaTheory] attributes on every test method.
/// </remarks>
[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")]
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute
{
/// <summary>
/// Creates instance of <see cref="AvaloniaTestFrameworkAttribute"/>.
/// </summary>
/// <param name="appBuilderEntryPointType">
/// Parameter from which <see cref="AppBuilder"/> should be created.
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </param>
public AvaloniaTestFrameworkAttribute(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type appBuilderEntryPointType) { }
}
/// <summary>
@ -38,8 +33,6 @@ public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer
/// <inheritdoc/>
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);
}
}

61
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs

@ -1,61 +0,0 @@
using Avalonia.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestRunner<TAppBuilderEntry> : XunitTestAssemblyRunner
{
private CancellationTokenSource? _cancellationTokenSource;
public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> 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<SynchronizationContext> InitNewApplicationContext(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<SynchronizationContext>();
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;
}
}

37
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;
/// <summary>
/// Identifies an xunit theory that starts on Avalonia Dispatcher
/// such that awaited expressions resume on the test's "main thread".
/// </summary>
[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<IXunitTestCase> CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow)
{
yield return new AvaloniaTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow);
}
protected override IEnumerable<IXunitTestCase> CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute)
{
yield return new AvaloniaTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod);
}
}

31
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<RunSummary> 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);
}
}

9
src/Headless/Avalonia.Headless/Avalonia.Headless.csproj

@ -12,7 +12,16 @@
<Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<ItemGroup>
<Compile Remove="..\..\Shared\ModuleInitializer.cs" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Headless.Vnc, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
</Project>

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

@ -21,20 +21,21 @@ namespace Avalonia.Headless
private Action? _forceTick;
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
bool cancelled = false;
var st = Stopwatch.StartNew();
_forceTick = () => tick(st.Elapsed);
DispatcherTimer.Run(() =>
var timer = new DispatcherTimer(DispatcherPriority.Render)
{
if (cancelled)
return false;
tick(st.Elapsed);
return !cancelled;
}, TimeSpan.FromSeconds(1.0 / _framesPerSecond), DispatcherPriority.Render);
Interval = TimeSpan.FromSeconds(1.0 / _framesPerSecond),
Tag = "HeadlessRenderTimer"
};
timer.Tick += (s, e) => tick(st.Elapsed);
timer.Start();
return Disposable.Create(() =>
{
_forceTick = null;
cancelled = true;
timer.Stop();
});
}

27
src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs

@ -0,0 +1,27 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Headless;
/// <summary>
/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class AvaloniaTestApplicationAttribute : Attribute
{
public Type AppBuilderEntryPointType { get; }
/// <summary>
/// Creates instance of <see cref="AvaloniaTestApplicationAttribute"/>.
/// </summary>
/// <param name="appBuilderEntryPointType">
/// Parameter from which <see cref="AppBuilder"/> should be created.
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </param>
public AvaloniaTestApplicationAttribute(
[DynamicallyAccessedMembers(HeadlessUnitTestSession.DynamicallyAccessed)]
Type appBuilderEntryPointType)
{
AppBuilderEntryPointType = appBuilderEntryPointType;
}
}

99
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -18,7 +18,8 @@ namespace Avalonia.Headless
{
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface())
.Bind<IFontManagerImpl>().ToConstant(new HeadlessFontManagerStub());
.Bind<IFontManagerImpl>().ToConstant(new HeadlessFontManagerStub())
.Bind<ITextShaperImpl>().ToConstant(new HeadlessTextShaperStub());
}
public IEnumerable<string> 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<Point> points = new List<Point>();
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()
{

109
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<ushort> 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<uint> 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<char> text)
{
if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
throw new InvalidOperationException("text memory should have been a string");
return start;
}
}
}

214
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;
/// <summary>
/// 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 <see cref="Dispatch"/> methods to keep execution flow on the UI thread.
/// Disposing unit test session stops internal dispatcher loop.
/// </summary>
[Unstable("This API is experimental and might be unstable. Use on your risk. API might or might not be changed in a minor update.")]
public sealed class HeadlessUnitTestSession : IDisposable
{
private static readonly ConcurrentDictionary<Assembly, HeadlessUnitTestSession> s_session = new();
private readonly AppBuilder _appBuilder;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly BlockingCollection<Action> _queue;
private readonly Task _dispatchTask;
internal const DynamicallyAccessedMemberTypes DynamicallyAccessed =
DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicParameterlessConstructor;
private HeadlessUnitTestSession(AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource,
BlockingCollection<Action> queue, Task dispatchTask)
{
_appBuilder = appBuilder;
_cancellationTokenSource = cancellationTokenSource;
_queue = queue;
_dispatchTask = dispatchTask;
}
/// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
public Task Dispatch(Action action, CancellationToken cancellationToken)
{
return Dispatch(() =>
{
action();
return Task.FromResult(0);
}, cancellationToken);
}
/// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
public Task<TResult> Dispatch<TResult>(Func<TResult> action, CancellationToken cancellationToken)
{
return Dispatch(() => Task.FromResult(action()), cancellationToken);
}
/// <summary>
/// Dispatch method queues an async operation on the dispatcher thread, creates a new application instance,
/// setting app avalonia services, and runs <see cref="action"/> parameter.
/// </summary>
/// <param name="action">Action to execute on the dispatcher thread with avalonia services.</param>
/// <param name="cancellationToken">Cancellation token to cancel execution.</param>
/// <exception cref="ObjectDisposedException">
/// If global session was already cancelled and thread killed, it's not possible to dispatch any actions again
/// </exception>
public Task<TResult> Dispatch<TResult>(Func<Task<TResult>> action, CancellationToken cancellationToken)
{
if (_cancellationTokenSource.IsCancellationRequested)
{
throw new ObjectDisposedException("Session was already disposed.");
}
var token = _cancellationTokenSource.Token;
var tcs = new TaskCompletionSource<TResult>();
_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();
}
/// <summary>
/// Creates instance of <see cref="HeadlessUnitTestSession"/>.
/// </summary>
/// <param name="entryPointType">
/// Parameter from which <see cref="AppBuilder"/> should be created.
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </param>
public static HeadlessUnitTestSession StartNew(
[DynamicallyAccessedMembers(DynamicallyAccessed)]
Type entryPointType)
{
var tcs = new TaskCompletionSource<HeadlessUnitTestSession>();
var cancellationTokenSource = new CancellationTokenSource();
var queue = new BlockingCollection<Action>();
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();
}
/// <summary>
/// Creates a session from AvaloniaTestApplicationAttribute attribute or reuses any existing.
/// If AvaloniaTestApplicationAttribute doesn't exist, empty application is used.
/// </summary>
[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<AvaloniaTestApplicationAttribute>()
?.AppBuilderEntryPointType;
return appBuilderEntryPointType is not null ?
StartNew(appBuilderEntryPointType) :
StartNew(typeof(Application));
});
}
}

21
src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs

@ -44,53 +44,56 @@ public static class HeadlessWindowExtensions
/// Simulates keyboard press on the headless window/toplevel.
/// </summary>
public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers);
RunJobsOnImpl(topLevel, w => w.KeyPress(key, modifiers));
/// <summary>
/// Simulates keyboard release on the headless window/toplevel.
/// </summary>
public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers);
RunJobsOnImpl(topLevel, w => w.KeyRelease(key, modifiers));
/// <summary>
/// Simulates mouse down on the headless window/toplevel.
/// </summary>
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));
/// <summary>
/// Simulates mouse move on the headless window/toplevel.
/// </summary>
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));
/// <summary>
/// Simulates mouse up on the headless window/toplevel.
/// </summary>
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));
/// <summary>
/// Simulates mouse wheel on the headless window/toplevel.
/// </summary>
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));
/// <summary>
/// Simulates drag'n'drop target on the headless window/toplevel.
/// </summary>
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<IHeadlessWindow> 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)

4
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -41,9 +41,9 @@ namespace Avalonia.Win32
internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32()))
if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.TryGetValue(wParam.ToInt32(), out var value))
{
s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam);
value.WndProc(hWnd, msg, wParam, lParam);
}
if (msg == WM_TASKBARCREATED)

7
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<InvalidOperationException>(() => 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<FontManagerOptions>().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<FontManagerOptions>().ToConstant(options);

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

1
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;

285
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@ -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<object> 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<IGeometryImpl> 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<GlyphInfo> 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<Point> points = new List<Point>();
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()
{
}
}
}

7
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)
{

17
tests/Avalonia.Benchmarks/NullCursorFactory.cs

@ -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() { }
}
}
}

107
tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs

@ -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<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{
}
public void DrawBitmap(IRef<IBitmapImpl> 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<IGlyphRunImpl> 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;
}
}

149
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -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<IGeometryImpl> children)
{
throw new NotImplementedException();
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2)
{
throw new NotImplementedException();
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> 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<GlyphInfo> 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()
{
}
}
}

27
tests/Avalonia.Benchmarks/NullThreadingPlatform.cs

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

5
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<IPlatformRenderInterface>().ToConstant(new NullRenderingPlatform());
AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface());
}
[Benchmark]

5
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.

5
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<ICursorFactory>(),
theme: () => CreateTheme(),
dispatcherImpl: new NullThreadingPlatform(),
fontManagerImpl: new MockFontManagerImpl(),
textShaperImpl: new MockTextShaperImpl(),
windowingPlatform: new MockWindowingPlatform());
return UnitTestApplication.Start(services);

5
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.

5
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)
{

3
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);

7
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<ICursorFactory>(),
textShaperImpl: new MockTextShaperImpl(),
renderInterface: new MockPlatformRenderInterface());
textShaperImpl: new HeadlessTextShaperStub(),
renderInterface: new HeadlessPlatformRenderInterface());
private static IControlTemplate CreateTemplate()
{

7
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

13
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<ICursorFactory>());
private static TestServices Services => TestServices.MockThreadingInterface.With(
renderInterface: new MockPlatformRenderInterface(),
renderInterface: new HeadlessPlatformRenderInterface(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
textShaperImpl: new MockTextShaperImpl(),
fontManagerImpl: new MockFontManagerImpl());
textShaperImpl: new HeadlessTextShaperStub(),
fontManagerImpl: new HeadlessFontManagerStub());
private static IControlTemplate CreateTemplate()
{

7
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

11
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<ICursorFactory>(),
textShaperImpl: new MockTextShaperImpl(),
fontManagerImpl: new MockFontManagerImpl());
textShaperImpl: new HeadlessTextShaperStub(),
fontManagerImpl: new HeadlessFontManagerStub());
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<ICursorFactory>(),
renderInterface: new MockPlatformRenderInterface(),
textShaperImpl: new MockTextShaperImpl(),
fontManagerImpl: new MockFontManagerImpl());
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub(),
fontManagerImpl: new HeadlessFontManagerStub());
private IControlTemplate CreateTemplate()
{

5
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<ICursorFactory>(),
textShaperImpl: new MockTextShaperImpl(),
fontManagerImpl: new MockFontManagerImpl());
textShaperImpl: new HeadlessTextShaperStub(),
fontManagerImpl: new HeadlessFontManagerStub());
private static IControlTemplate CreateTemplate()
{

7
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<ICursorFactory>(),
textShaperImpl: new MockTextShaperImpl(),
renderInterface: new MockPlatformRenderInterface());
textShaperImpl: new HeadlessTextShaperStub(),
renderInterface: new HeadlessPlatformRenderInterface());
private static IControlTemplate CreateTemplate()
{

7
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

7
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))]

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

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<DefineConstants>$(DefineConstants);NUNIT</DefineConstants>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Headless.UnitTests\**\*.cs" />
<Compile Remove="..\Avalonia.Headless.UnitTests\bin\**\*.cs" />
<Compile Remove="..\Avalonia.Headless.UnitTests\obj\**\*.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
</Project>

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

7
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

5
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;

35
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);
}
}

7
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))]

7
tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj → tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<DefineConstants>$(DefineConstants);XUNIT</DefineConstants>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />
<Import Project="..\..\build\Moq.props" />
@ -11,6 +12,10 @@
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<Compile Include="..\Avalonia.Headless.UnitTests\**\*.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj" />

2
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 = @"

3
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)))
{

1
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;

1
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;

1
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@ -10,6 +10,7 @@
<EmbeddedResource Include="..\Avalonia.UnitTests\Assets\*.ttf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless\Avalonia.Headless.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />

62
tests/Avalonia.UnitTests/ImmediateDispatcher.cs

@ -1,62 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
namespace Avalonia.UnitTests
{
/// <summary>
/// Immediately invokes dispatched jobs on the current thread.
/// </summary>
public class ImmediateDispatcher : IDispatcher
{
/// <inheritdoc/>
public bool CheckAccess()
{
return true;
}
/// <inheritdoc/>
public void Post(Action action, DispatcherPriority priority)
{
action();
}
/// <inheritdoc/>
public void Post(SendOrPostCallback action, object arg, DispatcherPriority priority)
{
action(arg);
}
/// <inheritdoc/>
public Task InvokeAsync(Action action, DispatcherPriority priority)
{
action();
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority)
{
var result = function();
return Task.FromResult(result);
}
/// <inheritdoc/>
public Task InvokeAsync(Func<Task> function, DispatcherPriority priority)
{
return function();
}
/// <inheritdoc/>
public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority)
{
return function();
}
/// <inheritdoc/>
public void VerifyAccess()
{
}
}
}

63
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@ -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;
}
}
}

34
tests/Avalonia.UnitTests/MockGlyphRun.cs

@ -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<float> GetIntersections(float lowerLimit, float upperLimit)
=> Array.Empty<float>();
}
}

81
tests/Avalonia.UnitTests/MockGlyphTypeface.cs

@ -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<uint> 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<ushort> 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;
}
}
}

174
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -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<IGeometryImpl>();
}
public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
{
return Mock.Of<IGeometryImpl>();
}
public IGeometryImpl CreateRectangleGeometry(Rect rect)
{
return Mock.Of<IGeometryImpl>(x => x.Bounds == rect);
}
class MockRenderTarget : IRenderTarget
{
public void Dispose()
{
}
public IDrawingContextImpl CreateDrawingContext()
{
var m = new Mock<IDrawingContextImpl>();
m.Setup(c => c.CreateLayer(It.IsAny<Size>()))
.Returns(() =>
{
var r = new Mock<IDrawingContextLayerImpl>();
r.Setup(r => r.CreateDrawingContext())
.Returns(CreateDrawingContext());
return r.Object;
}
);
return m.Object;
}
public bool IsCorrupted => false;
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
return new MockRenderTarget();
}
public bool IsLost => false;
public object TryGetFeature(Type featureType) => null;
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
{
return Mock.Of<IRenderTargetBitmapImpl>();
}
public IStreamGeometryImpl CreateStreamGeometry()
{
return new MockStreamGeometryImpl();
}
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children)
{
return Mock.Of<IGeometryImpl>();
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2)
{
return Mock.Of<IGeometryImpl>();
}
public IWriteableBitmapImpl CreateWriteableBitmap(
PixelSize size,
Vector dpi,
PixelFormat format,
AlphaFormat alphaFormat)
{
throw new NotImplementedException();
}
public IBitmapImpl LoadBitmap(Stream stream)
{
return Mock.Of<IBitmapImpl>();
}
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<IBitmapImpl>();
}
public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
return Mock.Of<IBitmapImpl>();
}
public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
return Mock.Of<IBitmapImpl>();
}
public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
{
return Mock.Of<IBitmapImpl>();
}
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<GlyphInfo> 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<IGeometryImpl>();
}
public bool SupportsIndividualRoundRects { get; set; }
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;
public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888;
public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true;
public void Dispose()
{
}
}
}

179
tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs

@ -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<Point> points = new List<Point>();
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;
}
}
}
}

38
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -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<char> 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;
}
}
}

34
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<ICursorFactory>(),
renderInterface: new HeadlessPlatformRenderInterface(),
standardCursorFactory: new HeadlessCursorFactoryStub(),
theme: () => CreateSimpleTheme(),
dispatcherImpl: Mock.Of<IDispatcherImpl>(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<IRuntimePlatform>());
public static readonly TestServices MockThreadingInterface = new TestServices(
dispatcherImpl: Mock.Of<IDispatcherImpl>(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<IPlatformRenderInterface>(x =>
x.CreateStreamGeometry() == Mock.Of<IStreamGeometryImpl>(
y => y.Open() == Mock.Of<IStreamGeometryContextImpl>()));
}
}
}

15
tests/Avalonia.UnitTests/TextTestHelper.cs

@ -1,15 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Avalonia.UnitTests
{
public static class TextTestHelper
{
public static int GetStartCharIndex(ReadOnlyMemory<char> text)
{
if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
throw new InvalidOperationException("text memory should have been a string");
return start;
}
}
}
Loading…
Cancel
Save