Browse Source
* 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 contextpull/20023/head
committed by
GitHub
14 changed files with 274 additions and 25 deletions
@ -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; |
|||
} |
|||
@ -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,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> |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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,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…
Reference in new issue