From 9bf379d32c93f2b16b7c8b9ca99f444b1b36cb67 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Wed, 29 Jul 2026 06:17:20 -0500 Subject: [PATCH] chore: migrate tests to xunit v3 Package swap is a 4-line edit in Directory.Packages.props now that central package management is in: xunit 2.9.3 -> xunit.v3 3.2.2, xunit.runner.visualstudio 3.0.0 -> 3.1.5, Microsoft.NET.Test.Sdk 17.12.0 -> 18.8.1. Staying on the VSTest bridge deliberately, so `dotnet test`, the Nuke DotNetTest targets and coverlet.collector all keep working untouched; Microsoft Testing Platform is a separate follow-up. Mechanical changes: - Xunit.Abstractions no longer exists; ITestOutputHelper moved into Xunit, which is already a global Using. 15 stray usings deleted, no other edits. - DocSamples drops its test packages entirely rather than migrating. It has zero [Fact]/[Theory] methods, holds compile-checked doc samples, and is not in jasperfx.slnx or any Nuke target. xunit.v3 would also have forced OutputType=Exe onto a Microsoft.NET.Sdk.Web project for no benefit. - CodegenTests.MethodCallTester borrowed Xunit.Sdk.ErrorMessage as a return type for its codegen probes via a stray `using Xunit.Sdk`. The tests only assert the inferred type identity, so any reference type does; replaced with a local one and dropped the accidental dependency on xunit internals. Three real failures surfaced, none of them mechanical. All are latent problems that xunit v2 happened to hide: 1. JasperFxOptions.HasReferenceToJasperFxTool loaded every referenced assembly and read its attributes with no guard. v3 test projects are executables, so Assembly.GetEntryAssembly() is now the test assembly rather than testhost.exe, and the walk reached a project reference whose own dependency is missing from the output -- FileNotFoundException out of host startup. This is a product bug, not a test bug: any application whose assembly graph contains an unresolvable reference (an optional dependency that was never deployed, something trimmed) crashes the same way today. Detection is best-effort, so unloadable assemblies are now skipped. 2. EventTests.SliceGroupTests self-stubs IProjectionStorage, which extends IAsyncDisposable, and left DisposeAsync throwing NotImplementedException. v2 never called it; v3 disposes test classes that implement IAsyncDisposable, so all 16 tests in the class failed in teardown. The stub is part of the storage contract, not xunit lifecycle -- made it a no-op. 3. CommandExecutorTester.run_an_async_command_that_fails asserted on output captured via Console.SetOut, but CommandExecutor renders failures through Spectre's AnsiConsole, which caches the writer it binds to on first use process-wide. The test was therefore passing on test ordering alone (Console.SetOut cannot redirect an already-pinned Spectre console) and the happy-path tests passed only because they use plain Console.WriteLine. Now binds AnsiConsole explicitly, so it no longer depends on order. Also retires two dead CI environment variables. FRAMEWORK bound to no Nuke parameter, and DISABLE_TEST_PARALLELIZATION was read by nothing at all -- no test project, runner config, or build target. The two suites that genuinely must not run concurrently already declare it in code via [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)], which v3 still honors. CONFIGURATION stays; it does bind to the Nuke parameter. Test counts match the v2 baseline exactly on both frameworks -- no discovery loss: CoreTests 475, CodegenTests 419, CommandLineTests 295, EventTests 648, EventStoreTests 72, CodegenTests.FSharp 1. Stable over three consecutive runs. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/dotnet.yml | 10 ++++++++-- Directory.Packages.props | 6 +++--- .../CodegenTests.FSharp.csproj | 2 +- .../FSharpCompilationGate.cs | 1 - .../CodeGeneration_ordering_determinism.cs | 1 - src/CodegenTests/CodegenTests.csproj | 2 +- src/CodegenTests/FSharpControlFlowTests.cs | 1 - src/CodegenTests/GeneratedTypeTests.cs | 1 - src/CodegenTests/MethodCallTester.cs | 13 +++++++++++-- src/CodegenTests/Samples/Frames.cs | 1 - src/CodegenTests/Samples/HelloWorld.cs | 1 - src/CodegenTests/Samples/InjectedFieldUsage.cs | 1 - src/CodegenTests/Samples/UsingSourceWriter.cs | 1 - src/CodegenTests/Services/BruteForceTests.cs | 1 - .../inline_enumerable_with_mixed_lifetimes.cs | 1 - .../Services/keyed_constructor_injection.cs | 1 - .../Services/keyed_service_location.cs | 1 - src/CommandLineTests/CommandExecutorTester.cs | 12 ++++++++++++ src/CommandLineTests/CommandLineTests.csproj | 2 +- src/CoreTests/Core/data_segregation.cs | 1 - src/CoreTests/CoreTests.csproj | 2 +- src/DocSamples/DocSamples.csproj | 17 +++++++---------- src/EventStoreTests/EventStoreTests.csproj | 2 +- .../TestingSupport/TestLogger.cs | 1 - src/EventTests/EventTests.csproj | 2 +- src/EventTests/Projections/SliceGroupTests.cs | 5 ++++- src/EventTests/TestingSupport/TestLogger.cs | 1 - .../JasperFx.Aspire.Tests.csproj | 2 +- ...JasperFx.Events.SourceGenerator.Tests.csproj | 2 +- .../JasperFx.SourceGenerator.Tests.csproj | 2 +- src/JasperFx/JasperFxOptions.cs | 17 +++++++++++++++-- 31 files changed, 69 insertions(+), 44 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index d19c3a99..feafd8ad 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -21,9 +21,15 @@ env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 pg_db: marten_testing pg_user: postgres + # CONFIGURATION binds to the Nuke [Parameter] of the same name (Nuke reads parameters from the + # environment), so it genuinely selects the Release build. FRAMEWORK and + # DISABLE_TEST_PARALLELIZATION used to sit here too but bound to nothing: there is no Framework + # parameter, and no test project, runner config, or build target ever read the latter. The two + # suites that actually must not run concurrently (CodegenTests mutates process-wide statics and + # compiles assemblies; CommandLineTests redirects Console) declare that in code via + # [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)], which xunit v3 still + # honors, so the serialization is enforced where it can't be silently dropped. CONFIGURATION: Release - FRAMEWORK: net9.0 - DISABLE_TEST_PARALLELIZATION: true NUKE_TELEMETRY_OPTOUT: true jobs: diff --git a/Directory.Packages.props b/Directory.Packages.props index d341748b..8132fade 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -80,11 +80,11 @@ - + - - + + diff --git a/src/CodegenTests.FSharp/CodegenTests.FSharp.csproj b/src/CodegenTests.FSharp/CodegenTests.FSharp.csproj index 757bbe0a..a806e676 100644 --- a/src/CodegenTests.FSharp/CodegenTests.FSharp.csproj +++ b/src/CodegenTests.FSharp/CodegenTests.FSharp.csproj @@ -24,7 +24,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CodegenTests.FSharp/FSharpCompilationGate.cs b/src/CodegenTests.FSharp/FSharpCompilationGate.cs index 35af2488..82ae7c40 100644 --- a/src/CodegenTests.FSharp/FSharpCompilationGate.cs +++ b/src/CodegenTests.FSharp/FSharpCompilationGate.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using Shouldly; -using Xunit.Abstractions; namespace CodegenTests.FSharp; diff --git a/src/CodegenTests/CodeGeneration_ordering_determinism.cs b/src/CodegenTests/CodeGeneration_ordering_determinism.cs index fc7cbacd..75b444d5 100644 --- a/src/CodegenTests/CodeGeneration_ordering_determinism.cs +++ b/src/CodegenTests/CodeGeneration_ordering_determinism.cs @@ -3,7 +3,6 @@ using JasperFx.CodeGeneration.Model; using Shouldly; using Xunit; -using Xunit.Abstractions; namespace CodegenTests; diff --git a/src/CodegenTests/CodegenTests.csproj b/src/CodegenTests/CodegenTests.csproj index 0768ed8b..697b779e 100644 --- a/src/CodegenTests/CodegenTests.csproj +++ b/src/CodegenTests/CodegenTests.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CodegenTests/FSharpControlFlowTests.cs b/src/CodegenTests/FSharpControlFlowTests.cs index 5088aac6..28049f5b 100644 --- a/src/CodegenTests/FSharpControlFlowTests.cs +++ b/src/CodegenTests/FSharpControlFlowTests.cs @@ -2,7 +2,6 @@ using JasperFx.CodeGeneration.Frames; using JasperFx.CodeGeneration.Model; using Shouldly; -using Xunit.Abstractions; namespace CodegenTests; diff --git a/src/CodegenTests/GeneratedTypeTests.cs b/src/CodegenTests/GeneratedTypeTests.cs index 9f21b4db..c3c1ac5c 100644 --- a/src/CodegenTests/GeneratedTypeTests.cs +++ b/src/CodegenTests/GeneratedTypeTests.cs @@ -2,7 +2,6 @@ using JasperFx.CodeGeneration.Model; using JasperFx.Core; using Shouldly; -using Xunit.Abstractions; namespace CodegenTests; diff --git a/src/CodegenTests/MethodCallTester.cs b/src/CodegenTests/MethodCallTester.cs index e9054ec0..9d84063a 100644 --- a/src/CodegenTests/MethodCallTester.cs +++ b/src/CodegenTests/MethodCallTester.cs @@ -3,7 +3,6 @@ using JasperFx.CodeGeneration.Model; using NSubstitute; using Shouldly; -using Xunit.Sdk; namespace CodegenTests; @@ -478,5 +477,15 @@ public Task GetString() public interface IMartenOp { - + +} + +/// +/// A return type for the MethodCallTarget probes below. The tests only assert that +/// MethodCall infers this exact type (and the argument name derived from it), so any reference +/// type does; it previously borrowed Xunit.Sdk.ErrorMessage via a stray `using Xunit.Sdk`, +/// which quietly tied a codegen test to xunit's internals and broke on the v3 upgrade. +/// +public class ErrorMessage +{ } \ No newline at end of file diff --git a/src/CodegenTests/Samples/Frames.cs b/src/CodegenTests/Samples/Frames.cs index dec0f56d..d086ab49 100644 --- a/src/CodegenTests/Samples/Frames.cs +++ b/src/CodegenTests/Samples/Frames.cs @@ -3,7 +3,6 @@ using JasperFx.CodeGeneration.Frames; using JasperFx.CodeGeneration.Model; using Xunit; -using Xunit.Abstractions; namespace CodegenTests.Samples; diff --git a/src/CodegenTests/Samples/HelloWorld.cs b/src/CodegenTests/Samples/HelloWorld.cs index 06cd78f5..3b4ab63d 100644 --- a/src/CodegenTests/Samples/HelloWorld.cs +++ b/src/CodegenTests/Samples/HelloWorld.cs @@ -2,7 +2,6 @@ using System.Linq; using JasperFx.RuntimeCompiler; using Xunit; -using Xunit.Abstractions; namespace CodegenTests.Samples; diff --git a/src/CodegenTests/Samples/InjectedFieldUsage.cs b/src/CodegenTests/Samples/InjectedFieldUsage.cs index 069437a2..a96305f6 100644 --- a/src/CodegenTests/Samples/InjectedFieldUsage.cs +++ b/src/CodegenTests/Samples/InjectedFieldUsage.cs @@ -3,7 +3,6 @@ using JasperFx.CodeGeneration.Frames; using JasperFx.CodeGeneration.Model; using Xunit; -using Xunit.Abstractions; namespace CodegenTests.Samples; diff --git a/src/CodegenTests/Samples/UsingSourceWriter.cs b/src/CodegenTests/Samples/UsingSourceWriter.cs index 78567679..54fdb234 100644 --- a/src/CodegenTests/Samples/UsingSourceWriter.cs +++ b/src/CodegenTests/Samples/UsingSourceWriter.cs @@ -1,7 +1,6 @@ using System; using JasperFx.CodeGeneration; using Xunit; -using Xunit.Abstractions; namespace CodegenTests.Samples; diff --git a/src/CodegenTests/Services/BruteForceTests.cs b/src/CodegenTests/Services/BruteForceTests.cs index 0823fed9..7b393cd5 100644 --- a/src/CodegenTests/Services/BruteForceTests.cs +++ b/src/CodegenTests/Services/BruteForceTests.cs @@ -5,7 +5,6 @@ using JasperFx.RuntimeCompiler; using Microsoft.Extensions.DependencyInjection; using Shouldly; -using Xunit.Abstractions; namespace CodegenTests.Services; diff --git a/src/CodegenTests/Services/inline_enumerable_with_mixed_lifetimes.cs b/src/CodegenTests/Services/inline_enumerable_with_mixed_lifetimes.cs index 5e179293..f590daee 100644 --- a/src/CodegenTests/Services/inline_enumerable_with_mixed_lifetimes.cs +++ b/src/CodegenTests/Services/inline_enumerable_with_mixed_lifetimes.cs @@ -7,7 +7,6 @@ using JasperFx.RuntimeCompiler; using Microsoft.Extensions.DependencyInjection; using Shouldly; -using Xunit.Abstractions; namespace CodegenTests.Services; diff --git a/src/CodegenTests/Services/keyed_constructor_injection.cs b/src/CodegenTests/Services/keyed_constructor_injection.cs index 98fe597e..45ac0729 100644 --- a/src/CodegenTests/Services/keyed_constructor_injection.cs +++ b/src/CodegenTests/Services/keyed_constructor_injection.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection; using Shouldly; using Xunit; -using Xunit.Abstractions; namespace CodegenTests.Services; diff --git a/src/CodegenTests/Services/keyed_service_location.cs b/src/CodegenTests/Services/keyed_service_location.cs index af92e298..25f37cc0 100644 --- a/src/CodegenTests/Services/keyed_service_location.cs +++ b/src/CodegenTests/Services/keyed_service_location.cs @@ -5,7 +5,6 @@ using JasperFx.RuntimeCompiler; using Microsoft.Extensions.DependencyInjection; using Shouldly; -using Xunit.Abstractions; namespace CodegenTests.Services; diff --git a/src/CommandLineTests/CommandExecutorTester.cs b/src/CommandLineTests/CommandExecutorTester.cs index c520115e..4c54f5ac 100644 --- a/src/CommandLineTests/CommandExecutorTester.cs +++ b/src/CommandLineTests/CommandExecutorTester.cs @@ -2,6 +2,7 @@ using JasperFx.CommandLine; using JasperFx.Core; using Shouldly; +using Spectre.Console; namespace CommandLineTests { @@ -23,6 +24,17 @@ public CommandExecutorTester() { Console.SetOut(theOutput); + // CommandExecutor renders failures through Spectre's AnsiConsole, which caches the + // TextWriter it binds to on first use anywhere in the process. Console.SetOut alone + // therefore does NOT redirect the failure path -- whichever test touched Spectre first + // has already pinned the writer. That made run_an_async_command_that_fails pass or fail + // purely on test ordering (it survived xunit v2's order and broke under v3's). Bind + // Spectre explicitly so this class captures failure output regardless of order. + AnsiConsole.Console = AnsiConsole.Create(new AnsiConsoleSettings + { + Out = new AnsiConsoleOutput(theOutput) + }); + executor = CommandExecutor.For(_ => { _.RegisterCommands(GetType().GetTypeInfo().Assembly); diff --git a/src/CommandLineTests/CommandLineTests.csproj b/src/CommandLineTests/CommandLineTests.csproj index 87221f58..177cd71b 100644 --- a/src/CommandLineTests/CommandLineTests.csproj +++ b/src/CommandLineTests/CommandLineTests.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/CoreTests/Core/data_segregation.cs b/src/CoreTests/Core/data_segregation.cs index 9633fbdb..fb8efd77 100644 --- a/src/CoreTests/Core/data_segregation.cs +++ b/src/CoreTests/Core/data_segregation.cs @@ -1,5 +1,4 @@ using JasperFx.Core; -using Xunit.Abstractions; namespace CoreTests.Core; diff --git a/src/CoreTests/CoreTests.csproj b/src/CoreTests/CoreTests.csproj index c3124487..3be00c2a 100644 --- a/src/CoreTests/CoreTests.csproj +++ b/src/CoreTests/CoreTests.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/DocSamples/DocSamples.csproj b/src/DocSamples/DocSamples.csproj index d0539fe1..3daffb5b 100644 --- a/src/DocSamples/DocSamples.csproj +++ b/src/DocSamples/DocSamples.csproj @@ -5,16 +5,13 @@ false - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + diff --git a/src/EventStoreTests/EventStoreTests.csproj b/src/EventStoreTests/EventStoreTests.csproj index 65c7ddfb..7f50890f 100644 --- a/src/EventStoreTests/EventStoreTests.csproj +++ b/src/EventStoreTests/EventStoreTests.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/EventStoreTests/TestingSupport/TestLogger.cs b/src/EventStoreTests/TestingSupport/TestLogger.cs index 145431e5..8d96df15 100644 --- a/src/EventStoreTests/TestingSupport/TestLogger.cs +++ b/src/EventStoreTests/TestingSupport/TestLogger.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using JasperFx.Core.Reflection; using Microsoft.Extensions.Logging; -using Xunit.Abstractions; namespace EventStoreTests.TestingSupport; diff --git a/src/EventTests/EventTests.csproj b/src/EventTests/EventTests.csproj index 803e7cb2..7a806155 100644 --- a/src/EventTests/EventTests.csproj +++ b/src/EventTests/EventTests.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/EventTests/Projections/SliceGroupTests.cs b/src/EventTests/Projections/SliceGroupTests.cs index 77775aa8..27554d71 100644 --- a/src/EventTests/Projections/SliceGroupTests.cs +++ b/src/EventTests/Projections/SliceGroupTests.cs @@ -544,9 +544,12 @@ Task IProjectionStorage.LoadAsync(string id, CancellationTok throw new NotImplementedException(); } + // Part of the IProjectionStorage contract this class self-stubs, NOT xunit lifecycle -- but + // xunit v3 disposes a test class that implements IAsyncDisposable, so a throwing stub here + // fails every test in the class during teardown. Nothing in these tests needs disposal. ValueTask IAsyncDisposable.DisposeAsync() { - throw new NotImplementedException(); + return ValueTask.CompletedTask; } Task> IStorageOperations.FetchProjectionStorageAsync(string tenantId, CancellationToken cancellationToken) diff --git a/src/EventTests/TestingSupport/TestLogger.cs b/src/EventTests/TestingSupport/TestLogger.cs index 38f2bd49..80ed6ed5 100644 --- a/src/EventTests/TestingSupport/TestLogger.cs +++ b/src/EventTests/TestingSupport/TestLogger.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using JasperFx.Core.Reflection; using Microsoft.Extensions.Logging; -using Xunit.Abstractions; namespace EventTests.TestingSupport; diff --git a/src/JasperFx.Aspire.Tests/JasperFx.Aspire.Tests.csproj b/src/JasperFx.Aspire.Tests/JasperFx.Aspire.Tests.csproj index 935b095a..58fd08d2 100644 --- a/src/JasperFx.Aspire.Tests/JasperFx.Aspire.Tests.csproj +++ b/src/JasperFx.Aspire.Tests/JasperFx.Aspire.Tests.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/JasperFx.Events.SourceGenerator.Tests/JasperFx.Events.SourceGenerator.Tests.csproj b/src/JasperFx.Events.SourceGenerator.Tests/JasperFx.Events.SourceGenerator.Tests.csproj index 1deb41c3..4acb5299 100644 --- a/src/JasperFx.Events.SourceGenerator.Tests/JasperFx.Events.SourceGenerator.Tests.csproj +++ b/src/JasperFx.Events.SourceGenerator.Tests/JasperFx.Events.SourceGenerator.Tests.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/JasperFx.SourceGenerator.Tests/JasperFx.SourceGenerator.Tests.csproj b/src/JasperFx.SourceGenerator.Tests/JasperFx.SourceGenerator.Tests.csproj index 0540ca50..e041b46c 100644 --- a/src/JasperFx.SourceGenerator.Tests/JasperFx.SourceGenerator.Tests.csproj +++ b/src/JasperFx.SourceGenerator.Tests/JasperFx.SourceGenerator.Tests.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/JasperFx/JasperFxOptions.cs b/src/JasperFx/JasperFxOptions.cs index 76f6d9f9..877731c3 100644 --- a/src/JasperFx/JasperFxOptions.cs +++ b/src/JasperFx/JasperFxOptions.cs @@ -22,8 +22,21 @@ public static bool HasReferenceToJasperFxTool(Assembly assembly) var names = assembly.GetReferencedAssemblies(); foreach (var name in names) { - var reference = Assembly.Load(name); - if (reference != null && reference.HasAttribute()) return true; + // Best-effort detection: an application's assembly graph routinely contains references + // that cannot be loaded or reflected over at runtime -- optional dependencies that were + // never deployed, assemblies trimmed away, or (as here) a reference whose own + // dependencies are missing from the output so that enumerating its custom attributes + // throws. None of that means the app is a JasperFx tool, and none of it is worth + // failing host startup over, so skip the assembly and keep looking. + try + { + var reference = Assembly.Load(name); + if (reference != null && reference.HasAttribute()) return true; + } + catch (Exception) + { + // Intentionally ignored; see above. + } } return false;