Browse Source

Headless AvaloniaTestIsolationLevel (#20000)

* Remove Unstable from HeadlessUnitTestSession

* Implement headless AvaloniaTestIsolationLevel

* Duplicate headless unit tests with different levels of isolation

* Fix accidental ABI breaking change

* Adjust docs

* Add IsolationTests

* Fix RunCoreLibsTests nukebuild target

* Headless: reuse sync context, instead of always forcing avalonia context
pull/20023/head
Max Katz 3 months ago
committed by GitHub
parent
commit
a4bfae1c29
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 42
      Avalonia.sln
  2. 6
      nukebuild/Build.cs
  3. 47
      src/Headless/Avalonia.Headless/HeadlessUnitTestIsolationAttribute.cs
  4. 65
      src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
  5. 8
      tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/AssemblyInfo.cs
  6. 0
      tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj
  7. 1
      tests/Avalonia.Headless.NUnit.PerTest.UnitTests/AssemblyInfo.cs
  8. 31
      tests/Avalonia.Headless.NUnit.PerTest.UnitTests/Avalonia.Headless.NUnit.PerTest.UnitTests.csproj
  9. 63
      tests/Avalonia.Headless.UnitTests/IsolationTests.cs
  10. 3
      tests/Avalonia.Headless.UnitTests/LeakTests.cs
  11. 8
      tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/AssemblyInfo.cs
  12. 0
      tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj
  13. 1
      tests/Avalonia.Headless.XUnit.PerTest.UnitTests/AssemblyInfo.cs
  14. 24
      tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj

42
Avalonia.sln

@ -270,10 +270,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.NUnit", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.NUnit.UnitTests", "tests\Avalonia.Headless.NUnit.UnitTests\Avalonia.Headless.NUnit.UnitTests.csproj", "{2999D79E-3C20-4A90-B651-CA7E0AC92D35}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit.UnitTests", "tests\Avalonia.Headless.XUnit.UnitTests\Avalonia.Headless.XUnit.UnitTests.csproj", "{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Build.Tasks.UnitTest", "tests\Avalonia.Build.Tasks.UnitTest\Avalonia.Build.Tasks.UnitTest.csproj", "{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}"
@ -302,6 +298,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.MacCatalyst"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.tvOS", "samples\ControlCatalog.tvOS\ControlCatalog.tvOS.csproj", "{14342787-B4EF-4076-8C91-BA6C523DE8DF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerAssembly.UnitTests", "tests\Avalonia.Headless.NUnit.PerAssembly.UnitTests\Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj", "{A175EFAE-476C-4DAA-87D5-742C18CFCC27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerTest.UnitTests", "tests\Avalonia.Headless.NUnit.PerTest.UnitTests\Avalonia.Headless.NUnit.PerTest.UnitTests.csproj", "{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.PerAssembly.UnitTests", "tests\Avalonia.Headless.XUnit.PerAssembly.UnitTests\Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj", "{342D2657-2F84-493C-B74B-9D2CAE5D9DAB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.PerTest.UnitTests", "tests\Avalonia.Headless.XUnit.PerTest.UnitTests\Avalonia.Headless.XUnit.PerTest.UnitTests.csproj", "{26918642-829D-4FA2-B60A-BE8D83F4E063}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -652,14 +656,6 @@ Global
{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
{60B4ED1F-ECFA-453B-8A70-1788261C8355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60B4ED1F-ECFA-453B-8A70-1788261C8355}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -704,6 +700,22 @@ Global
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.Build.0 = Release|Any CPU
{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Release|Any CPU.Build.0 = Release|Any CPU
{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}.Release|Any CPU.Build.0 = Release|Any CPU
{342D2657-2F84-493C-B74B-9D2CAE5D9DAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{342D2657-2F84-493C-B74B-9D2CAE5D9DAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{342D2657-2F84-493C-B74B-9D2CAE5D9DAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{342D2657-2F84-493C-B74B-9D2CAE5D9DAB}.Release|Any CPU.Build.0 = Release|Any CPU
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -779,8 +791,6 @@ Global
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
{ED976634-B118-43F8-8B26-0279C7A7044F} = {FF237916-7150-496B-89ED-6CA3292896E7}
{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}
{B0FD6A48-FBAB-4676-B36A-DE76B0922B12} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{9D6AEF22-221F-4F4B-B335-A4BA510F002C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{5BF0C3B8-E595-4940-AB30-2DA206C2F085} = {9D6AEF22-221F-4F4B-B335-A4BA510F002C}
@ -793,6 +803,10 @@ Global
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{DE3C28DD-B602-4750-831D-345102A54CA0} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{14342787-B4EF-4076-8C91-BA6C523DE8DF} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{A175EFAE-476C-4DAA-87D5-742C18CFCC27} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{26918642-829D-4FA2-B60A-BE8D83F4E063} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

6
nukebuild/Build.cs

@ -297,8 +297,10 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.Headless.NUnit.UnitTests");
RunCoreTest("Avalonia.Headless.XUnit.UnitTests");
RunCoreTest("Avalonia.Headless.NUnit.PerAssembly.UnitTests");
RunCoreTest("Avalonia.Headless.NUnit.PerTest.UnitTests");
RunCoreTest("Avalonia.Headless.XUnit.PerAssembly.UnitTests");
RunCoreTest("Avalonia.Headless.XUnit.PerTest.UnitTests");
});
Target RunRenderTests => _ => _

47
src/Headless/Avalonia.Headless/HeadlessUnitTestIsolationAttribute.cs

@ -0,0 +1,47 @@
using System;
namespace Avalonia.Headless;
/// <summary>
/// Defines the isolation level for headless unit tests,
/// controlling how <see cref="Avalonia.Application"/> and its
/// associated <see cref="Avalonia.Threading.Dispatcher"/> are managed
/// between test runs.
/// </summary>
public enum AvaloniaTestIsolationLevel
{
/// <summary>
/// Reuses a single <see cref="Avalonia.Application"/> and <see cref="Avalonia.Threading.Dispatcher"/>
/// instance across all tests within the assembly.
/// </summary>
/// <remarks>
/// Tests must not rely on any global or persistent state that could leak between runs.
/// Headless framework won't dispose any resources after tests when using this mode.
/// </remarks>
PerAssembly,
/// <summary>
/// Recreates the <see cref="Avalonia.Application"/> and <see cref="Avalonia.Threading.Dispatcher"/>
/// for each individual test method.
/// </summary>
/// <remarks>
/// This mode ensures complete test isolation, and should be used for tests that modify global
/// application state or rely on a clean dispatcher environment.
/// This is the default isolation level if none is specified.
/// </remarks>
PerTest
}
/// <summary>
/// Specifies how headless unit tests should be isolated from each other,
/// defining when the test runtime should recreate the
/// <see cref="Avalonia.Application"/> and <see cref="Avalonia.Threading.Dispatcher"/> instances.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class AvaloniaTestIsolationAttribute(AvaloniaTestIsolationLevel isolationLevel) : Attribute
{
/// <summary>
/// Gets the isolation level for headless tests.
/// </summary>
public AvaloniaTestIsolationLevel IsolationLevel { get; } = isolationLevel;
}

65
src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs

@ -18,7 +18,6 @@ namespace Avalonia.Headless;
/// 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 Dictionary<Assembly, HeadlessUnitTestSession> s_session = new();
@ -27,19 +26,25 @@ public sealed class HeadlessUnitTestSession : IDisposable
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly BlockingCollection<(Action, ExecutionContext?)> _queue;
private readonly Task _dispatchTask;
private readonly bool _isolated;
// Only set and used with PerAssembly isolation
private SynchronizationContext? _sharedContext;
internal const DynamicallyAccessedMemberTypes DynamicallyAccessed =
DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicParameterlessConstructor;
private HeadlessUnitTestSession(AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource,
BlockingCollection<(Action, ExecutionContext?)> queue, Task dispatchTask)
private HeadlessUnitTestSession(
AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource,
BlockingCollection<(Action, ExecutionContext?)> queue, Task dispatchTask,
bool isolated)
{
_appBuilder = appBuilder;
_cancellationTokenSource = cancellationTokenSource;
_queue = queue;
_dispatchTask = dispatchTask;
_isolated = isolated;
}
/// <inheritdoc cref="DispatchCore{TResult}"/>
@ -93,7 +98,9 @@ public sealed class HeadlessUnitTestSession : IDisposable
try
{
using var application = EnsureApplication();
using var application = _isolated
? EnsureIsolatedApplication()
: EnsureSharedApplication();
var task = action();
if (task.Status != TaskStatus.RanToCompletion)
{
@ -123,7 +130,27 @@ public sealed class HeadlessUnitTestSession : IDisposable
return tcs.Task;
}
private IDisposable EnsureApplication()
private IDisposable EnsureSharedApplication()
{
var oldContext = SynchronizationContext.Current;
if (Application.Current is null)
{
_appBuilder.SetupUnsafe();
_sharedContext = SynchronizationContext.Current;
}
else
{
SynchronizationContext.SetSynchronizationContext(_sharedContext);
}
return Disposable.Create(() =>
{
Dispatcher.UIThread.RunJobs();
SynchronizationContext.SetSynchronizationContext(oldContext);
});
}
private IDisposable EnsureIsolatedApplication()
{
var scope = AvaloniaLocator.EnterScope();
var oldContext = SynchronizationContext.Current;
@ -167,6 +194,24 @@ public sealed class HeadlessUnitTestSession : IDisposable
public static HeadlessUnitTestSession StartNew(
[DynamicallyAccessedMembers(DynamicallyAccessed)]
Type entryPointType)
{
// Cannot be optional parameter for ABI stability
// ReSharper disable once IntroduceOptionalParameters.Global
return StartNew(entryPointType, AvaloniaTestIsolationLevel.PerTest);
}
/// <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>
/// <param name="isolationLevel">Defines the isolation level for headless unit tests</param>
public static HeadlessUnitTestSession StartNew(
[DynamicallyAccessedMembers(DynamicallyAccessed)]
Type entryPointType,
AvaloniaTestIsolationLevel isolationLevel)
{
var tcs = new TaskCompletionSource<HeadlessUnitTestSession>();
var cancellationTokenSource = new CancellationTokenSource();
@ -178,6 +223,7 @@ public sealed class HeadlessUnitTestSession : IDisposable
try
{
var appBuilder = AppBuilder.Configure(entryPointType);
var runIsolated = isolationLevel == AvaloniaTestIsolationLevel.PerTest;
// If windowing subsystem wasn't initialized by user, force headless with default parameters.
if (appBuilder.WindowingSubsystemName != "Headless")
@ -186,7 +232,7 @@ public sealed class HeadlessUnitTestSession : IDisposable
}
// ReSharper disable once AccessToModifiedClosure
tcs.SetResult(new HeadlessUnitTestSession(appBuilder, cancellationTokenSource, queue, task!));
tcs.SetResult(new HeadlessUnitTestSession(appBuilder, cancellationTokenSource, queue, task!, runIsolated));
}
catch (Exception e)
{
@ -234,9 +280,12 @@ public sealed class HeadlessUnitTestSession : IDisposable
var appBuilderEntryPointType = assembly.GetCustomAttribute<AvaloniaTestApplicationAttribute>()
?.AppBuilderEntryPointType;
var isolationLevel = assembly.GetCustomAttribute<AvaloniaTestIsolationAttribute>()
?.IsolationLevel ?? AvaloniaTestIsolationLevel.PerTest;
session = appBuilderEntryPointType is not null ?
StartNew(appBuilderEntryPointType) :
StartNew(typeof(Application));
StartNew(appBuilderEntryPointType, isolationLevel) :
StartNew(typeof(Application), isolationLevel);
s_session.Add(assembly, session);
}

8
tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/AssemblyInfo.cs

@ -0,0 +1,8 @@
global using NUnit.Framework;
global using Avalonia.Headless.NUnit;
using Avalonia.Headless;
using Avalonia.Headless.UnitTests;
[assembly: AvaloniaTestApplication(typeof(TestApplication))]
[assembly: AvaloniaTestIsolation(AvaloniaTestIsolationLevel.PerAssembly)]

0
tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj → tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj

1
tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs → tests/Avalonia.Headless.NUnit.PerTest.UnitTests/AssemblyInfo.cs

@ -5,3 +5,4 @@ using Avalonia.Headless;
using Avalonia.Headless.UnitTests;
[assembly: AvaloniaTestApplication(typeof(TestApplication))]
[assembly: AvaloniaTestIsolation(AvaloniaTestIsolationLevel.PerTest)]

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

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(AvsCurrentTargetFramework)</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>

63
tests/Avalonia.Headless.UnitTests/IsolationTests.cs

@ -0,0 +1,63 @@
using System;
using System.Reflection;
using Avalonia.Threading;
namespace Avalonia.Headless.UnitTests;
public class IsolationTests
{
private static WeakReference<Application> s_previousAppRef;
private static WeakReference<Dispatcher> s_previousDispatcherRef;
#if NUNIT
[AvaloniaTheory, Timeout(10000)]
[TestCase(1), TestCase(2), TestCase(3)]
#elif XUNIT
[AvaloniaTheory]
[InlineData(1), InlineData(2), InlineData(3)]
#endif
public void Application_Instance_Should_Match_Isolation_Level(int runIndex)
{
var currentApp = Application.Current;
var currentDispatcher = Dispatcher.UIThread;
if (s_previousAppRef is not null && s_previousDispatcherRef is not null)
{
var isolationLevel =
GetType().Assembly.GetCustomAttribute<AvaloniaTestIsolationAttribute>()?.IsolationLevel ??
AvaloniaTestIsolationLevel.PerTest;
if (isolationLevel == AvaloniaTestIsolationLevel.PerTest)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Assert.False(s_previousAppRef.TryGetTarget(out var previousApp),
"Previous Application instance should have been collected.");
Assert.False(s_previousDispatcherRef.TryGetTarget(out var previousDispatcher),
"Previous Dispatcher instance should have been collected.");
Assert.False(previousApp == currentApp);
Assert.False(previousDispatcher == currentDispatcher);
}
else if (isolationLevel == AvaloniaTestIsolationLevel.PerAssembly)
{
Assert.True(s_previousAppRef.TryGetTarget(out var previousApp),
"Previous Application instance should still be alive.");
Assert.True(s_previousDispatcherRef.TryGetTarget(out var previousDispatcher),
"Previous Dispatcher instance should still be alive.");
Assert.True(previousApp == currentApp);
Assert.True(previousDispatcher == currentDispatcher);
}
else
{
throw new InvalidOperationException($"Unknown isolation level: {isolationLevel}");
}
}
s_previousAppRef = new WeakReference<Application>(currentApp);
s_previousDispatcherRef = new WeakReference<Dispatcher>(currentDispatcher);
}
}

3
tests/Avalonia.Headless.UnitTests/LeakTests.cs

@ -42,7 +42,8 @@ public class LeakTests
GC.WaitForPendingFinalizers();
GC.Collect();
if (s_previousFontManager is not null)
// Either previous font manager is collected (IsAlive == false), or it is the same as current (shared isolation mode).
if (s_previousFontManager is not null && s_previousFontManager.Target != fontManager.Target)
{
Assert.False(s_previousFontManager.IsAlive);
}

8
tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/AssemblyInfo.cs

@ -0,0 +1,8 @@
global using Xunit;
global using Avalonia.Headless.XUnit;
using Avalonia.Headless;
using Avalonia.Headless.UnitTests;
[assembly: AvaloniaTestApplication(typeof(TestApplication))]
[assembly: AvaloniaTestIsolation(AvaloniaTestIsolationLevel.PerAssembly)]
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)]

0
tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj → tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj

1
tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs → tests/Avalonia.Headless.XUnit.PerTest.UnitTests/AssemblyInfo.cs

@ -4,3 +4,4 @@ using Avalonia.Headless;
using Avalonia.Headless.UnitTests;
[assembly: AvaloniaTestApplication(typeof(TestApplication))]
[assembly: AvaloniaTestIsolation(AvaloniaTestIsolationLevel.PerTest)]

24
tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(AvsCurrentTargetFramework)</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" />
<Import Project="..\..\build\XUnit.props" />
<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" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
</Project>
Loading…
Cancel
Save