Skip to content

Migrate test suite to xUnit v3 on Microsoft.Testing.Platform - #5949

Draft
hoyosjs wants to merge 7 commits into
mainfrom
juhoyosa/xunit-v3-mtp-migration
Draft

Migrate test suite to xUnit v3 on Microsoft.Testing.Platform#5949
hoyosjs wants to merge 7 commits into
mainfrom
juhoyosa/xunit-v3-mtp-migration

Conversation

@hoyosjs

@hoyosjs hoyosjs commented Aug 5, 2026

Copy link
Copy Markdown
Member

Goal

Move the repo's tests off the Arcade "classic" xunit.console runner onto xUnit v3 built as self-contained Microsoft.Testing.Platform (MTP) executables. This is the direction Arcade and the broader .NET org are standardizing on, and it lets us drop custom test infrastructure that only existed to patch gaps in xUnit v2.

What changed

Opt-in and runner

  • global.json: added test.runner: Microsoft.Testing.Platform.
  • src/tests/Directory.Build.props: single opt-in for all test projects (TestRunnerName=XUnitV3, OutputType=Exe), guarded on IsTestProject so it doesn't leak into product projects. Individual projects can disable via EnableXUnitV3=false.

Test projects and helpers

  • Migrated all *.UnitTests / test projects (NETCore.Client, Monitoring[.EventPipe], ExtensionCommands, DebugServices, SymbolStore, FileFormats, DbgShim, dotnet-trace, dotnet-counters, eventpipe, SOS.UnitTests) to the v3 APIs.
  • Microsoft.Diagnostics.TestHelpers: updated output-helper plumbing (ITestOutputHelper shape changes, multiplex/file/indented loggers) for v3.
  • Removed the custom SkippableFact/SkippableTheory infrastructure (discoverers, message buses, test cases) - xUnit v3 supports runtime skip natively via Assert.Skip / SkipTestException, so the bespoke attributes are gone.

Build/CI scripts

  • eng/build.ps1 / eng/build.sh: test filters now use MTP options (--filter-method / --filter-class) instead of the old -method / -class console flags.
  • Added --ignore-exit-code 8 so projects in the traversal that legitimately match zero tests don't fail the run, plus a post-run guard that sums the result XMLs and fails if a filter matched nothing anywhere (catches typo'd filters that the per-project ignore would otherwise hide).

x86 test host + global.json cleanup

  • Test host now resolves its runtime from artifacts/dotnet-test (via TestDotNetRoot) instead of the Arcade toolset .dotnet.
  • New InstallHostRuntimeX86 target in eng/InstallRuntimes.proj stages the x86 shared framework into artifacts/dotnet-test/x86, matching the DOTNET_ROOT_X86 that Arcade sets for the MTP exe (a 32-bit apphost reads DOTNET_ROOT_X86 first and won't fall back).
  • Removed the tools.runtimes.dotnet/x86 node from global.json, which previously existed solely to stage that x86 runtime into .dotnet.

Breaking changes

  • Tests are now MTP executables, not xunit.console invocations. Anyone running tests directly, or tooling that shells the old console runner / -method/-class flags, must switch to the MTP equivalents. build.cmd -test / test.cmd and -methodfilter/-classfilter continue to work.
  • The custom SkippableFact/SkippableTheory attributes are removed; skips use xUnit v3's native mechanism.

Validation (beyond CI)

Output still lands in the trx/xml as before. The x86 host redirect was verified by repro on an x64 dev box (test projects are RID-agnostic, so a local -architecture x86 build emits x64 apphosts and can't exercise DOTNET_ROOT_X86 through the normal traversal): a genuine win-x86 apphost hosts correctly from the staged artifacts/dotnet-test/x86 subdir (52 tests pass), and an empty subdir hard-fails with no fallback - confirming the staging is load-bearing. The real CI x86 build/test legs are the final gate for that path.

hoyosjs and others added 7 commits July 21, 2026 13:20
Migrates 11 test projects + TestHelpers/CommonTestRunner from xUnit v2
(Arcade classic xunit.console) to xUnit v3 self-running MTP executables.
Deletes Skippable machinery; uses native Assert.Skip/SkipUnless.
Updates eng/build.ps1 + eng/build.sh test filters to MTP syntax.
SOS.UnitTests temporarily disabled in dirs.proj.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Migrate the last remaining test project (SOS.UnitTests) off the xUnit v2
Skippable* machinery to xUnit v3 running under Microsoft.Testing.Platform,
and re-enable it in the test traversal.

- SOS.cs / SOSRunner.cs: drop `using Xunit.Abstractions` (ITestOutputHelper is
  now in the Xunit namespace under v3) and rename [SkippableFact]/[SkippableTheory]
  to [Fact]/[Theory]. `Xunit.Extensions` stays because both files still throw
  SkipTestException, which emits the v3 dynamic-skip token.
- xunit.runner.json / SOS.UnitTests.csproj: stop shipping xunit.runner.json as
  Content (it collided with the copy Arcade's XUnitV3 tooling injects, causing
  NETSDK1152 during publish). Instead point Arcade's XUnitCoreSettingsFile /
  XUnitDesktopSettingsFile at the project's own file, which both resolves the
  conflict and ensures the settings are honored under MTP. Merge Arcade's
  shadowCopy:false alongside the existing preEnumerateTheories:false.
- global.json: add the .NET 10 test runner switch so standalone `dotnet test`
  drives Microsoft.Testing.Platform instead of VSTest.
- dirs.proj: re-enable SOS.UnitTests in the traversal.

Verified on Windows x64: full tree builds 0/0; MTP discovery finds 30 tests;
SOSExceptionTests.DivZero passes across all runtime configs (full dump ->
SOS-command -> assert pipeline); interpreter tests dynamically skip (not fail).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
When a -classfilter/-methodfilter is active, clear the results XML
directory before the test run, then sum the assembly total counts across
all result XMLs afterward and fail if zero tests ran. This replaces the
timestamp/newer-file approach, which was not portable across the bash and
pwsh scripts and could miscount stale per-TFM result files.
TestConfiguration.BaseDir defaulted to the current working directory
("."). Under the classic xunit.console runner the working directory was
the test output directory, so "." found Debugger.Tests.Config.txt and the
other assets. The Microsoft.Testing.Platform runner launches the test
executable with a different working directory, so default to
AppContext.BaseDirectory (the app base, where the assets are copied)
instead. Callers can still override it.
Remove the central NoWarn suppression of the xUnit v3 analyzers in
src/tests/Directory.Build.props and address the flagged sites so
TreatWarningsAsErrors passes:

- xUnit1051 (29 sites): pass TestContext.Current.CancellationToken at
  call sites that already take a CancellationToken (Task.Delay/Run/Wait,
  StreamReader.ReadToEndAsync, Pipeline.StopAsync, and the CollectLinux
  TestArgs helper). No product signatures change - the analyzer only
  fires where a CancellationToken overload already exists.
- xUnit1031 (1 site): the blocking streamTask.Wait is inside the skipped
  StartEventPipeSessionWithoutStackwalkTestAsync theory (the async fix is
  tied to the race in #4717); keep the blocking wait
  and suppress it with a scoped pragma, matching the existing pattern in
  src/tests/eventpipe/ThreadPoolEvents.cs.
Set TestDotNetRoot to $(ArtifactsDotnetTestDir) for MTP test projects so
Arcade's XUnitV3 RunTests target launches each test executable with
DOTNET_ROOT pointing at the test .NET install instead of the Arcade
toolset SDK (.dotnet). This removes the dependency on the runtime that
global.json's tools.runtimes staged into .dotnet.
Add an InstallHostRuntimeX86 target that stages the x86 shared framework
into artifacts/dotnet-test/x86 on the Windows x86 test leg, matching the
DOTNET_ROOT_X86 that Arcade's XUnitV3 RunTests target sets. A 32-bit
apphost reads DOTNET_ROOT_X86 before DOTNET_ROOT and does not fall back
when it is set, so the runtime must live in that subdirectory.

With the test host now resolving its runtime from artifacts/dotnet-test,
remove the tools.runtimes.dotnet/x86 entry from global.json that used to
stage the same x86 framework into .dotnet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant