Browse Source

Update headless xUnit support to v3 (#20481)

* Update headless xUnit support to v3

* Update API suppressions
pull/20182/head
Julien Lebosquain 3 weeks ago
committed by GitHub
parent
commit
ff0a61486d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 76
      api/Avalonia.Headless.XUnit.nupkg.xml
  2. 9
      build/XUnit.props
  3. 3
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  4. 80
      src/Headless/Avalonia.Headless.XUnit/AvaloniaDelayEnumeratedTheoryTestCase.cs
  5. 32
      src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs
  6. 34
      src/Headless/Avalonia.Headless.XUnit/AvaloniaFactDiscoverer.cs
  7. 126
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs
  8. 63
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs
  9. 132
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs
  10. 31
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunnerContext.cs
  11. 36
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs
  12. 27
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs
  13. 16
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkDiscoverer.cs
  14. 19
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkExecutor.cs
  15. 50
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs
  16. 27
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunnerContext.cs
  17. 30
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs
  18. 92
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryDiscoverer.cs
  19. 31
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs
  20. 10
      src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
  21. 1
      tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj
  22. 1
      tests/Avalonia.Headless.XUnit.PerTest.UnitTests/Avalonia.Headless.XUnit.PerTest.UnitTests.csproj
  23. 5
      tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj

76
api/Avalonia.Headless.XUnit.nupkg.xml

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.XUnit.AvaloniaFactAttribute.#ctor</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer.#ctor(Xunit.Abstractions.IMessageSink)</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.XUnit.AvaloniaFactAttribute.#ctor</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer.#ctor(Xunit.Abstractions.IMessageSink)</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkAttribute</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkAttribute</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
</Suppressions>

9
build/XUnit.props

@ -1,14 +1,13 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Condition="'$(IsXUnit2)'!='true'">
<ItemGroup>
<PackageReference Include="xunit.v3.mtp-v2" Version="3.2.1" />
</ItemGroup>
<ItemGroup Condition="'$(IsXUnit2)'=='true'">
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="YTest.MTP.XUnit2" Version="1.0.2" />
</ItemGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\avalonia.snk</AssemblyOriginatorKeyFile>
<SignAssembly>False</SignAssembly>
<NoWarn>$(NoWarn);CS8002</NoWarn> <!-- ignore signing warnings for unit tests -->
</PropertyGroup>
</Project>

3
src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj

@ -5,8 +5,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Use lower minor version, as it is supposed to be compatible -->
<PackageReference Include="xunit.core" Version="2.4.0" />
<PackageReference Include="xunit.v3.extensibility.core" Version="3.2.1" />
</ItemGroup>
<ItemGroup>

80
src/Headless/Avalonia.Headless.XUnit/AvaloniaDelayEnumeratedTheoryTestCase.cs

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaDelayEnumeratedTheoryTestCase
: XunitDelayEnumeratedTheoryTestCase, ISelfExecutingXunitTestCase
{
public AvaloniaDelayEnumeratedTheoryTestCase(
IXunitTestMethod testMethod,
string testCaseDisplayName,
string uniqueID,
bool @explicit,
bool skipTestWithoutData,
Type[]? skipExceptions = null,
string? skipReason = null,
Type? skipType = null,
string? skipUnless = null,
string? skipWhen = null,
Dictionary<string, HashSet<string>>? traits = null,
string? sourceFilePath = null,
int? sourceLineNumber = null,
int? timeout = null)
: base(
testMethod,
testCaseDisplayName,
uniqueID,
@explicit,
skipTestWithoutData,
skipExceptions,
skipReason,
skipType,
skipUnless,
skipWhen,
traits,
sourceFilePath,
sourceLineNumber,
timeout)
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
public AvaloniaDelayEnumeratedTheoryTestCase()
{
}
public async ValueTask<RunSummary> Run(
ExplicitOption explicitOption,
IMessageBus messageBus,
object?[] constructorArguments,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
{
var tests = await aggregator.RunAsync(CreateTests, []);
// 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 = Task.Run(async () => await AvaloniaTestCaseRunner.Instance.Run(
this,
tests,
messageBus,
aggregator,
cancellationTokenSource,
TestCaseDisplayName,
SkipReason,
explicitOption,
constructorArguments))
.GetAwaiter()
.GetResult();
return runSummary;
}
}

32
src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs

@ -1,9 +1,7 @@
using System;
using System.ComponentModel;
using System.Threading;
using System.Runtime.CompilerServices;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
@ -12,24 +10,8 @@ namespace Avalonia.Headless.XUnit;
/// 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);
}
}
[XunitTestCaseDiscoverer(typeof(AvaloniaFactDiscoverer))]
public sealed class AvaloniaFactAttribute(
[CallerFilePath] string? sourceFilePath = null,
[CallerLineNumber] int sourceLineNumber = -1)
: FactAttribute(sourceFilePath, sourceLineNumber);

34
src/Headless/Avalonia.Headless.XUnit/AvaloniaFactDiscoverer.cs

@ -0,0 +1,34 @@
using System;
using System.ComponentModel;
using Xunit.Internal;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
[EditorBrowsable(EditorBrowsableState.Never)]
public class AvaloniaFactDiscoverer : FactDiscoverer
{
protected override IXunitTestCase CreateTestCase(
ITestFrameworkDiscoveryOptions discoveryOptions,
IXunitTestMethod testMethod,
IFactAttribute factAttribute)
{
var details = TestIntrospectionHelper.GetTestCaseDetails(discoveryOptions, testMethod, factAttribute);
return new AvaloniaTestCase(
details.ResolvedTestMethod,
details.TestCaseDisplayName,
details.UniqueID,
details.Explicit,
details.SkipExceptions,
details.SkipReason,
details.SkipType,
details.SkipUnless,
details.SkipWhen,
testMethod.Traits.ToReadWrite(StringComparer.OrdinalIgnoreCase),
sourceFilePath: details.SourceFilePath,
sourceLineNumber: details.SourceLineNumber,
timeout: details.Timeout);
}
}

126
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs

@ -1,126 +0,0 @@
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);
}
}
}

63
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs

@ -1,20 +1,46 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Avalonia.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestCase : XunitTestCase
internal sealed class AvaloniaTestCase : XunitTestCase, ISelfExecutingXunitTestCase
{
public AvaloniaTestCase(
IMessageSink diagnosticMessageSink,
TestMethodDisplay defaultMethodDisplay,
ITestMethod testMethod,
object?[]? testMethodArguments = null)
: base(diagnosticMessageSink, defaultMethodDisplay, TestMethodDisplayOptions.None, testMethod, testMethodArguments)
IXunitTestMethod testMethod,
string testCaseDisplayName,
string uniqueID,
bool @explicit,
Type[]? skipExceptions = null,
string? skipReason = null,
Type? skipType = null,
string? skipUnless = null,
string? skipWhen = null,
Dictionary<string, HashSet<string>>? traits = null,
object?[]? testMethodArguments = null,
string? sourceFilePath = null,
int? sourceLineNumber = null,
int? timeout = null)
: base(
testMethod,
testCaseDisplayName,
uniqueID,
@explicit,
skipExceptions,
skipReason,
skipType,
skipUnless,
skipWhen,
traits,
testMethodArguments,
sourceFilePath,
sourceLineNumber,
timeout)
{
}
@ -24,23 +50,30 @@ internal class AvaloniaTestCase : XunitTestCase
{
}
public override Task<RunSummary> RunAsync(
IMessageSink diagnosticMessageSink,
public async ValueTask<RunSummary> Run(
ExplicitOption explicitOption,
IMessageBus messageBus,
object[] constructorArguments,
object?[] constructorArguments,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
{
var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly);
var tests = await aggregator.RunAsync(CreateTests, []);
// 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 =
Task.Run(() => AvaloniaTestCaseRunner.RunTest(session, this, DisplayName, SkipReason, constructorArguments,
TestMethodArguments, messageBus, aggregator, cancellationTokenSource))
var runSummary = Task.Run(async () => await AvaloniaTestCaseRunner.Instance.Run(
this,
tests,
messageBus,
aggregator,
cancellationTokenSource,
TestCaseDisplayName,
SkipReason,
explicitOption,
constructorArguments))
.GetAwaiter()
.GetResult();
return Task.FromResult(runSummary);
return runSummary;
}
}

132
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs

@ -1,103 +1,59 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestCaseRunner : XunitTestCaseRunner
internal sealed class AvaloniaTestCaseRunner
: XunitTestCaseRunnerBase<AvaloniaTestCaseRunnerContext, IXunitTestCase, IXunitTest>
{
private readonly HeadlessUnitTestSession _session;
private readonly Action? _onAfterTestInvoked;
public static AvaloniaTestCaseRunner Instance { get; } = new();
public AvaloniaTestCaseRunner(
HeadlessUnitTestSession session, 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)
private AvaloniaTestCaseRunner()
{
_session = session;
_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)
public async ValueTask<RunSummary> Run(
IXunitTestCase testCase,
IReadOnlyCollection<IXunitTest> tests,
IMessageBus messageBus,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
string displayName,
string? skipReason,
ExplicitOption explicitOption,
object?[] constructorArguments)
{
var afterTest = () => Dispatcher.UIThread.RunJobs();
var runner = new AvaloniaTestCaseRunner(session, afterTest, testCase, displayName,
skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource);
return runner.RunAsync();
}
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(_session, _onAfterTestInvoked, test, messageBus, testClass, constructorArguments,
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource);
}
private class AvaloniaTestRunner : XunitTestRunner
{
private readonly HeadlessUnitTestSession _session;
private readonly Action? _onAfterTestInvoked;
public AvaloniaTestRunner(
HeadlessUnitTestSession session, 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)
{
_session = session;
_onAfterTestInvoked = onAfterTestInvoked;
}
protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
{
return _session.Dispatch(
() => new AvaloniaTestInvoker(_onAfterTestInvoked, Test, MessageBus, TestClass,
ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator,
CancellationTokenSource).RunAsync(),
CancellationTokenSource.Token);
}
var session = HeadlessUnitTestSession.GetOrStartForAssembly(testCase.TestClass.Class.Assembly);
await using var ctxt = new AvaloniaTestCaseRunnerContext(
testCase,
tests,
messageBus,
aggregator,
cancellationTokenSource,
displayName,
skipReason,
explicitOption,
constructorArguments,
session);
await ctxt.InitializeAsync();
return await Run(ctxt);
}
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);
}
}
}
protected override ValueTask<RunSummary> RunTest(
AvaloniaTestCaseRunnerContext ctxt,
IXunitTest test)
=> AvaloniaTestRunner.Instance.Run(
test,
ctxt.MessageBus,
ctxt.ConstructorArguments,
ctxt.ExplicitOption,
ctxt.Aggregator.Clone(),
ctxt.CancellationTokenSource,
ctxt.BeforeAfterTestAttributes,
ctxt.Session);
}

31
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunnerContext.cs

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestCaseRunnerContext(
IXunitTestCase testCase,
IReadOnlyCollection<IXunitTest> tests,
IMessageBus messageBus,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
string displayName,
string? skipReason,
ExplicitOption explicitOption,
object?[] constructorArguments,
HeadlessUnitTestSession session)
: XunitTestCaseRunnerContext(
testCase,
tests,
messageBus,
aggregator,
cancellationTokenSource,
displayName,
skipReason,
explicitOption,
constructorArguments)
{
public HeadlessUnitTestSession Session { get; } = session;
}

36
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs

@ -1,35 +1,13 @@
using System.Collections.Generic;
using System.Reflection;
using Xunit.Abstractions;
using Xunit.Sdk;
using System.Reflection;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestFramework : XunitTestFramework
internal sealed class AvaloniaTestFramework : XunitTestFramework
{
public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink)
{
}
protected override ITestFrameworkDiscoverer CreateDiscoverer(Assembly assembly)
=> new AvaloniaTestFrameworkDiscoverer(new XunitTestAssembly(assembly, null, assembly.GetName().Version));
protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
=> new Executor(assemblyName, SourceInformationProvider, DiagnosticMessageSink);
private class Executor : XunitTestFrameworkExecutor
{
public Executor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider,
IMessageSink diagnosticMessageSink) : base(assemblyName, sourceInformationProvider,
diagnosticMessageSink)
{
}
protected override async void RunTestCases(IEnumerable<IXunitTestCase> testCases,
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
{
using (var assemblyRunner = new AvaloniaTestAssemblyRunner(
TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
executionOptions)) await assemblyRunner.RunAsync();
}
}
protected override ITestFrameworkExecutor CreateExecutor(Assembly assembly)
=> new AvaloniaTestFrameworkExecutor(new XunitTestAssembly(assembly, null, assembly.GetName().Version));
}

27
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs

@ -1,8 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Xunit.Abstractions;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
@ -12,27 +9,9 @@ namespace Avalonia.Headless.XUnit;
/// <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>
/// Discoverer implementation for the Avalonia testing framework.
/// </summary>
public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer
{
/// <summary>
/// Creates instance of <see cref="AvaloniaTestFrameworkTypeDiscoverer"/>.
/// </summary>
public AvaloniaTestFrameworkTypeDiscoverer(IMessageSink _)
{
}
/// <inheritdoc/>
public Type GetTestFrameworkType(IAttributeInfo attribute)
{
return typeof(AvaloniaTestFramework);
}
public Type FrameworkType
=> typeof(AvaloniaTestFramework);
}

16
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkDiscoverer.cs

@ -0,0 +1,16 @@
using Xunit;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestFrameworkDiscoverer : XunitTestFrameworkDiscoverer
{
public AvaloniaTestFrameworkDiscoverer(
IXunitTestAssembly testAssembly,
IXunitTestCollectionFactory? collectionFactory = null)
: base(testAssembly, collectionFactory)
{
DiscovererTypeCache[typeof(FactAttribute)] = typeof(AvaloniaFactDiscoverer);
DiscovererTypeCache[typeof(TheoryAttribute)] = typeof(AvaloniaTheoryDiscoverer);
}
}

19
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkExecutor.cs

@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestFrameworkExecutor(IXunitTestAssembly testAssembly)
: XunitTestFrameworkExecutor(testAssembly)
{
private readonly HeadlessUnitTestSession _session = HeadlessUnitTestSession.GetOrStartForAssembly(testAssembly.Assembly);
protected override ITestFrameworkDiscoverer CreateDiscoverer()
=> new AvaloniaTestFrameworkDiscoverer(TestAssembly);
public override async ValueTask DisposeAsync()
{
await _session.DisposeAsync();
await base.DisposeAsync();
}
}

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

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestRunner : XunitTestRunnerBase<AvaloniaTestRunnerContext, IXunitTest>
{
public static AvaloniaTestRunner Instance { get; } = new();
private AvaloniaTestRunner()
{
}
public async ValueTask<RunSummary> Run(
IXunitTest test,
IMessageBus messageBus,
object?[] constructorArguments,
ExplicitOption explicitOption,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
IReadOnlyCollection<IBeforeAfterTestAttribute> beforeAfterAttributes,
HeadlessUnitTestSession session)
{
await using var ctxt = new AvaloniaTestRunnerContext(
test,
messageBus,
explicitOption,
aggregator,
cancellationTokenSource,
beforeAfterAttributes,
constructorArguments,
session
);
await ctxt.InitializeAsync();
return await session.Dispatch(
async () =>
{
var dispatcher = Dispatcher.UIThread;
var summary = await Run(ctxt);
dispatcher.RunJobs();
return summary;
},
ctxt.CancellationTokenSource.Token);
}
}

27
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunnerContext.cs

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestRunnerContext(
IXunitTest test,
IMessageBus messageBus,
ExplicitOption explicitOption,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
IReadOnlyCollection<IBeforeAfterTestAttribute> beforeAfterTestAttributes,
object?[] constructorArguments,
HeadlessUnitTestSession session)
: XunitTestRunnerContext(
test,
messageBus,
explicitOption,
aggregator,
cancellationTokenSource,
beforeAfterTestAttributes,
constructorArguments)
{
public HeadlessUnitTestSession Session { get; } = session;
}

30
src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs

@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
@ -12,26 +9,5 @@ namespace Avalonia.Headless.XUnit;
/// 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);
}
}
[XunitTestCaseDiscoverer(typeof(AvaloniaTheoryDiscoverer))]
public sealed class AvaloniaTheoryAttribute : TheoryAttribute;

92
src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryDiscoverer.cs

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Xunit;
using Xunit.Internal;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
[EditorBrowsable(EditorBrowsableState.Never)]
public class AvaloniaTheoryDiscoverer : TheoryDiscoverer
{
protected override ValueTask<IReadOnlyCollection<IXunitTestCase>> CreateTestCasesForDataRow(
ITestFrameworkDiscoveryOptions discoveryOptions,
IXunitTestMethod testMethod,
ITheoryAttribute theoryAttribute,
ITheoryDataRow dataRow,
object?[] testMethodArguments)
{
var details = TestIntrospectionHelper.GetTestCaseDetailsForTheoryDataRow(
discoveryOptions,
testMethod,
theoryAttribute,
dataRow,
testMethodArguments);
var traits = TestIntrospectionHelper.GetTraits(testMethod, dataRow);
var testCase = new AvaloniaTestCase(
details.ResolvedTestMethod,
details.TestCaseDisplayName,
details.UniqueID,
details.Explicit,
details.SkipExceptions,
details.SkipReason,
details.SkipType,
details.SkipUnless,
details.SkipWhen,
traits,
testMethodArguments,
sourceFilePath: details.SourceFilePath,
sourceLineNumber: details.SourceLineNumber,
timeout: details.Timeout);
return ValueTask.FromResult<IReadOnlyCollection<IXunitTestCase>>([testCase]);
}
protected override ValueTask<IReadOnlyCollection<IXunitTestCase>> CreateTestCasesForTheory(
ITestFrameworkDiscoveryOptions discoveryOptions,
IXunitTestMethod testMethod,
ITheoryAttribute theoryAttribute)
{
var details = TestIntrospectionHelper.GetTestCaseDetails(discoveryOptions, testMethod, theoryAttribute);
var testCase =
details.SkipReason is not null && details.SkipUnless is null && details.SkipWhen is null
? new AvaloniaTestCase(
details.ResolvedTestMethod,
details.TestCaseDisplayName,
details.UniqueID,
details.Explicit,
details.SkipExceptions,
details.SkipReason,
details.SkipType,
details.SkipUnless,
details.SkipWhen,
testMethod.Traits.ToReadWrite(StringComparer.OrdinalIgnoreCase),
sourceFilePath: details.SourceFilePath,
sourceLineNumber: details.SourceLineNumber,
timeout: details.Timeout
)
: (IXunitTestCase)new AvaloniaDelayEnumeratedTheoryTestCase(
details.ResolvedTestMethod,
details.TestCaseDisplayName,
details.UniqueID,
details.Explicit,
theoryAttribute.SkipTestWithoutData,
details.SkipExceptions,
details.SkipReason,
details.SkipType,
details.SkipUnless,
details.SkipWhen,
testMethod.Traits.ToReadWrite(StringComparer.OrdinalIgnoreCase),
sourceFilePath: details.SourceFilePath,
sourceLineNumber: details.SourceLineNumber,
timeout: details.Timeout
);
return ValueTask.FromResult<IReadOnlyCollection<IXunitTestCase>>([testCase]);
}
}

31
src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs

@ -1,31 +0,0 @@
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);
}
}

10
src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs

@ -18,7 +18,7 @@ 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>
public sealed class HeadlessUnitTestSession : IDisposable
public sealed class HeadlessUnitTestSession : IDisposable, IAsyncDisposable
{
private static readonly Dictionary<Assembly, HeadlessUnitTestSession> s_session = new();
@ -184,6 +184,14 @@ public sealed class HeadlessUnitTestSession : IDisposable
_cancellationTokenSource.Dispose();
}
public async ValueTask DisposeAsync()
{
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
_queue.CompleteAdding();
await _dispatchTask.ConfigureAwait(false);
_cancellationTokenSource.Dispose();
}
/// <summary>
/// Creates instance of <see cref="HeadlessUnitTestSession"/>.
/// </summary>

1
tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj

@ -4,7 +4,6 @@
<OutputType>Exe</OutputType>
<IsTestProject>true</IsTestProject>
<DefineConstants>$(DefineConstants);XUNIT</DefineConstants>
<IsXUnit2>true</IsXUnit2>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />

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

@ -4,7 +4,6 @@
<OutputType>Exe</OutputType>
<IsTestProject>true</IsTestProject>
<DefineConstants>$(DefineConstants);XUNIT</DefineConstants>
<IsXUnit2>true</IsXUnit2>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />

5
tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj

@ -13,10 +13,7 @@
<ProjectReference Include="../../src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="xunit.v3.mtp-v2" Version="3.2.1" />
</ItemGroup>
<Import Project="../../build/XUnit.props" />
<Import Project="../../build/SharedVersion.props" />
<Import Project="../../build/ReferenceCoreLibraries.props" />
<Import Project="../../build/BuildTargets.targets" />

Loading…
Cancel
Save