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 + + + + + + + + + + + + + + + + + + +