diff --git a/api/Avalonia.Headless.XUnit.nupkg.xml b/api/Avalonia.Headless.XUnit.nupkg.xml new file mode 100644 index 0000000000..c87cf909fe --- /dev/null +++ b/api/Avalonia.Headless.XUnit.nupkg.xml @@ -0,0 +1,76 @@ + + + + + CP0001 + T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer + baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + + + CP0001 + T:Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer + baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + + + CP0001 + T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer + baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + + + CP0001 + T:Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer + baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + + + CP0002 + M:Avalonia.Headless.XUnit.AvaloniaFactAttribute.#ctor + baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + + + CP0002 + M:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer.#ctor(Xunit.Abstractions.IMessageSink) + baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + + + CP0002 + M:Avalonia.Headless.XUnit.AvaloniaFactAttribute.#ctor + baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + + + CP0002 + M:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer.#ctor(Xunit.Abstractions.IMessageSink) + baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + + + CP0007 + T:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer + baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + + + CP0007 + T:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer + baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + + + CP0008 + T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkAttribute + baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll + + + CP0008 + T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkAttribute + baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll + + diff --git a/build/XUnit.props b/build/XUnit.props index c2ccb1fa2c..5c63ed69db 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -1,14 +1,13 @@  - + + - - - - + $(MSBuildThisFileDirectory)\avalonia.snk False $(NoWarn);CS8002 + diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj index 12f890c744..4ed3d6674e 100644 --- a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -5,8 +5,7 @@ - - + diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaDelayEnumeratedTheoryTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaDelayEnumeratedTheoryTestCase.cs new file mode 100644 index 0000000000..f2f9bb3b38 --- /dev/null +++ b/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>? 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 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; + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs index f501fc7a56..08b0984821 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs +++ b/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". /// [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); diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaFactDiscoverer.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaFactDiscoverer.cs new file mode 100644 index 0000000000..5c8e971e18 --- /dev/null +++ b/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); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs deleted file mode 100644 index 4b1cf84914..0000000000 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs +++ /dev/null @@ -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 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 RunTestCollectionAsync( - IMessageBus messageBus, - ITestCollection testCollection, - IEnumerable 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 testCases, - IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, - ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection, - testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource) - { - _session = session; - } - - protected override Task RunTestClassAsync( - ITestClass testClass, - IReflectionTypeInfo @class, - IEnumerable 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 testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, - ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource, IDictionary collectionFixtureMappings) : - base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, - cancellationTokenSource, collectionFixtureMappings) - { - _session = session; - } - - protected override Task RunTestMethodAsync( - ITestMethod testMethod, - IReflectionMethodInfo method, - IEnumerable 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 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 RunTestCaseAsync(IXunitTestCase testCase) - { - return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason, - _constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator, - _cancellationTokenSource); - } - } -} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs index c620397dd5..a0a73c2c06 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs +++ b/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>? 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 RunAsync( - IMessageSink diagnosticMessageSink, + public async ValueTask 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; } } diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs index 79b564f11d..7a52cf97c0 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs +++ b/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 { - 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 RunTest(HeadlessUnitTestSession session, - IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, - object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) + public async ValueTask Run( + IXunitTestCase testCase, + IReadOnlyCollection 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 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 beforeAfterAttributes, ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments, - testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource) - { - _session = session; - _onAfterTestInvoked = onAfterTestInvoked; - } - - protected override Task 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 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 RunTest( + AvaloniaTestCaseRunnerContext ctxt, + IXunitTest test) + => AvaloniaTestRunner.Instance.Run( + test, + ctxt.MessageBus, + ctxt.ConstructorArguments, + ctxt.ExplicitOption, + ctxt.Aggregator.Clone(), + ctxt.CancellationTokenSource, + ctxt.BeforeAfterTestAttributes, + ctxt.Session); } diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunnerContext.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunnerContext.cs new file mode 100644 index 0000000000..2c8decf9da --- /dev/null +++ b/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 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; +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs index aa9b3e7e18..61db27755b 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs +++ b/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 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)); } diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs index bdd8f3b0ea..7e33adfbfb 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs +++ b/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; /// /// It is an alternative to using [AvaloniaFact] or [AvaloniaTheory] attributes on every test method. /// -[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute { -} - -/// -/// Discoverer implementation for the Avalonia testing framework. -/// -public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer -{ - /// - /// Creates instance of . - /// - public AvaloniaTestFrameworkTypeDiscoverer(IMessageSink _) - { - } - - /// - public Type GetTestFrameworkType(IAttributeInfo attribute) - { - return typeof(AvaloniaTestFramework); - } + public Type FrameworkType + => typeof(AvaloniaTestFramework); } diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkDiscoverer.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkDiscoverer.cs new file mode 100644 index 0000000000..e29498452b --- /dev/null +++ b/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); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkExecutor.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkExecutor.cs new file mode 100644 index 0000000000..348a718b4b --- /dev/null +++ b/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(); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs new file mode 100644 index 0000000000..ad3c33028b --- /dev/null +++ b/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 +{ + public static AvaloniaTestRunner Instance { get; } = new(); + + private AvaloniaTestRunner() + { + } + + public async ValueTask Run( + IXunitTest test, + IMessageBus messageBus, + object?[] constructorArguments, + ExplicitOption explicitOption, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource, + IReadOnlyCollection 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); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunnerContext.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunnerContext.cs new file mode 100644 index 0000000000..137e8c3477 --- /dev/null +++ b/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 beforeAfterTestAttributes, + object?[] constructorArguments, + HeadlessUnitTestSession session) + : XunitTestRunnerContext( + test, + messageBus, + explicitOption, + aggregator, + cancellationTokenSource, + beforeAfterTestAttributes, + constructorArguments) +{ + public HeadlessUnitTestSession Session { get; } = session; +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs index 53c997f08f..86bdf0bfae 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs +++ b/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". /// [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 CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) - { - yield return new AvaloniaTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow); - } - - protected override IEnumerable 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; diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryDiscoverer.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryDiscoverer.cs new file mode 100644 index 0000000000..a56891beed --- /dev/null +++ b/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> 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>([testCase]); + } + + protected override ValueTask> 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>([testCase]); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs deleted file mode 100644 index ea7e7abee4..0000000000 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs +++ /dev/null @@ -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 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); - } -} diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs index 65f648b98c..26c076c1e1 100644 --- a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs +++ b/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 methods to keep execution flow on the UI thread. /// Disposing unit test session stops internal dispatcher loop. /// -public sealed class HeadlessUnitTestSession : IDisposable +public sealed class HeadlessUnitTestSession : IDisposable, IAsyncDisposable { private static readonly Dictionary 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(); + } + /// /// Creates instance of . /// diff --git a/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj b/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj index e638032870..270e078ed3 100644 --- a/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj +++ b/tests/Avalonia.Headless.XUnit.PerAssembly.UnitTests/Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj @@ -4,7 +4,6 @@ Exe true $(DefineConstants);XUNIT - true 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 index e638032870..270e078ed3 100644 --- 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 @@ -4,7 +4,6 @@ Exe true $(DefineConstants);XUNIT - true diff --git a/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj b/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj index a11ecc1fb4..797265af46 100644 --- a/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj +++ b/tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj @@ -13,10 +13,7 @@ - - - - +