diff --git a/Avalonia.sln b/Avalonia.sln
index 5dfd11b671..8413f571c5 100644
--- a/Avalonia.sln
+++ b/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}
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index b0d8106d32..a63fb1664b 100644
--- a/nukebuild/Build.cs
+++ b/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 => _ => _
diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestIsolationAttribute.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestIsolationAttribute.cs
new file mode 100644
index 0000000000..0a816b98d6
--- /dev/null
+++ b/src/Headless/Avalonia.Headless/HeadlessUnitTestIsolationAttribute.cs
@@ -0,0 +1,47 @@
+using System;
+
+namespace Avalonia.Headless;
+
+///
+/// Defines the isolation level for headless unit tests,
+/// controlling how and its
+/// associated are managed
+/// between test runs.
+///
+public enum AvaloniaTestIsolationLevel
+{
+ ///
+ /// Reuses a single and
+ /// instance across all tests within the assembly.
+ ///
+ ///
+ /// 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.
+ ///
+ PerAssembly,
+
+ ///
+ /// Recreates the and
+ /// for each individual test method.
+ ///
+ ///
+ /// 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.
+ ///
+ PerTest
+}
+
+///
+/// Specifies how headless unit tests should be isolated from each other,
+/// defining when the test runtime should recreate the
+/// and instances.
+///
+[AttributeUsage(AttributeTargets.Assembly)]
+public sealed class AvaloniaTestIsolationAttribute(AvaloniaTestIsolationLevel isolationLevel) : Attribute
+{
+ ///
+ /// Gets the isolation level for headless tests.
+ ///
+ public AvaloniaTestIsolationLevel IsolationLevel { get; } = isolationLevel;
+}
diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
index 796d0d860e..65f648b98c 100644
--- a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
+++ b/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 methods to keep execution flow on the UI thread.
/// Disposing unit test session stops internal dispatcher loop.
///
-[Unstable("This API is experimental and might be unstable. Use on your risk. API might or might not be changed in a minor update.")]
public sealed class HeadlessUnitTestSession : IDisposable
{
private static readonly Dictionary 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;
}
///
@@ -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);
+ }
+
+ ///
+ /// Creates instance of .
+ ///
+ ///
+ /// Parameter from which should be created.
+ /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
+ ///
+ /// Defines the isolation level for headless unit tests
+ public static HeadlessUnitTestSession StartNew(
+ [DynamicallyAccessedMembers(DynamicallyAccessed)]
+ Type entryPointType,
+ AvaloniaTestIsolationLevel isolationLevel)
{
var tcs = new TaskCompletionSource();
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()
?.AppBuilderEntryPointType;
+ var isolationLevel = assembly.GetCustomAttribute()
+ ?.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);
}
diff --git a/tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/AssemblyInfo.cs b/tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/AssemblyInfo.cs
new file mode 100644
index 0000000000..185fabffa8
--- /dev/null
+++ b/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)]
diff --git a/tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj b/tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj
similarity index 100%
rename from tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj
rename to tests/Avalonia.Headless.NUnit.PerAssembly.UnitTests/Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj
diff --git a/tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs b/tests/Avalonia.Headless.NUnit.PerTest.UnitTests/AssemblyInfo.cs
similarity index 73%
rename from tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs
rename to tests/Avalonia.Headless.NUnit.PerTest.UnitTests/AssemblyInfo.cs
index a2ba1f8d38..6679637603 100644
--- a/tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs
+++ b/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)]
diff --git a/tests/Avalonia.Headless.NUnit.PerTest.UnitTests/Avalonia.Headless.NUnit.PerTest.UnitTests.csproj b/tests/Avalonia.Headless.NUnit.PerTest.UnitTests/Avalonia.Headless.NUnit.PerTest.UnitTests.csproj
new file mode 100644
index 0000000000..301b96e0e8
--- /dev/null
+++ b/tests/Avalonia.Headless.NUnit.PerTest.UnitTests/Avalonia.Headless.NUnit.PerTest.UnitTests.csproj
@@ -0,0 +1,31 @@
+
+
+ $(AvsCurrentTargetFramework)
+ true
+ $(DefineConstants);NUNIT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Avalonia.Headless.UnitTests/IsolationTests.cs b/tests/Avalonia.Headless.UnitTests/IsolationTests.cs
new file mode 100644
index 0000000000..114b24ce92
--- /dev/null
+++ b/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 s_previousAppRef;
+ private static WeakReference 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()?.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(currentApp);
+ s_previousDispatcherRef = new WeakReference(currentDispatcher);
+ }
+}
diff --git a/tests/Avalonia.Headless.UnitTests/LeakTests.cs b/tests/Avalonia.Headless.UnitTests/LeakTests.cs
index b8e2f9ca53..29ade72b57 100644
--- a/tests/Avalonia.Headless.UnitTests/LeakTests.cs
+++ b/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);
}
diff --git a/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/AssemblyInfo.cs b/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/AssemblyInfo.cs
new file mode 100644
index 0000000000..460567e85e
--- /dev/null
+++ b/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)]
diff --git a/tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj b/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj
similarity index 100%
rename from tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj
rename to tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj
diff --git a/tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs b/tests/Avalonia.Headless.XUnit.PerTest.UnitTests/AssemblyInfo.cs
similarity index 72%
rename from tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs
rename to tests/Avalonia.Headless.XUnit.PerTest.UnitTests/AssemblyInfo.cs
index b7f132bf77..a80e01cfad 100644
--- a/tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs
+++ b/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)]
diff --git a/tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj b/tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj
new file mode 100644
index 0000000000..322ce6bb3a
--- /dev/null
+++ b/tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj
@@ -0,0 +1,24 @@
+
+
+ $(AvsCurrentTargetFramework)
+ true
+ $(DefineConstants);XUNIT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+