Skip to content

chore: upgrade xunit from v2 to v3#4623

Open
dbrattli wants to merge 4 commits into
mainfrom
chore/xunit-v3
Open

chore: upgrade xunit from v2 to v3#4623
dbrattli wants to merge 4 commits into
mainfrom
chore/xunit-v3

Conversation

@dbrattli

@dbrattli dbrattli commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

xUnit v2 is deprecated. This upgrades the .NET-side test runner from xunit v2 (XUnit 2.9.3) to xunit.v3 3.2.2 across the Beam, Php, Python and Rust test projects.

Changes

  • Replaced XUnit 2.9.3 with xunit.v3 3.2.2 in all four test .fsproj files.
  • v3 test projects are self-executing, so each project sets OutputType=Exe (added to Rust and Php; Python and Beam already had it) and the .NET-side [<EntryPoint>] is dropped in favour of the entry point generated by xunit.v3:
    • Rust main.fs is also transpiled to Rust's fn main, so its entry point is guarded with #if FABLE_COMPILER instead of removed.
    • Python/Beam/Php Main.fs collapses to a plain empty module Program — both #if branches had become identical.
  • When Fable cracks the project it runs CoreCompile with FABLE_COMPILER set, which would pull in xunit.v3's generated entry point and runner-reporter sources (.NET-only, they break transpilation). XunitAutoGeneratedEntryPoint and XunitRegisterBuiltInRunnerReporters are disabled for the Fable build only.
  • Added xunit.runner.json (serial collections, single thread) to Python, Beam and Php: their MailboxProcessor/async tests assert from fire-and-forget Async.StartImmediate continuations that can outlive their test collection, and xunit.v3 reports such background exceptions as catastrophic (v2 ignored them). Observed failing on Windows; the same race exists in all three projects.
  • xunit.runner.visualstudio 3.1.5 is already v3-compatible, so dotnet test keeps working via VSTest.
  • No test source changes needed — Xunit.FactAttribute, Assert.Equal and Assert.NotEqual are unchanged in v3.

Verification

dotnet test -c Release:

Project Result
Rust ✅ 2449 passed
Python ✅ 2266 passed
Beam ✅ 2439 passed
Php ⚠️ pre-existing compile errors on .NET, unrelated to xunit (project is not run by the build system)

CI timing impact of serial execution

Compared the CI run for this branch against the three most recent successful main runs (xunit v2, fully parallel) — no measurable degradation:

Test step This PR main baselines
Fable Tests - Beam 2m 34s 2m 14s / 2m 20s / 2m 30s
Fable Tests - Python (ubuntu, 3.12) 1m 59s 1m 49s / 1m 49s / 1m 52s
Fable Tests - Python (windows, 3.12) 2m 25s 2m 26s / 2m 30s / 1m 47s

Deltas are within run-to-run variance (baselines themselves spread by 16–43s). These steps are dominated by Fable transpilation and the target-runtime test runs, which are unaffected; the serialized .NET xunit pass takes only ~2–3s for ~2,300 tests locally.

Local timing on a multi-core machine

The relative cost of serialization is larger on a many-core dev machine, but still ~2s in absolute terms. Measured on a 24-core Linux box (dotnet test -c Release --no-build wall clock, xunit-reported duration in parentheses):

Project Serial (this PR) Parallel (pre-PR behavior) Cost
Beam (2,439 tests) 3.8s (1s) 2.4s (0.5s) +1.4s
Python (2,266 tests) 5.4s (3s) 3.5s (2s) +1.9s

Parallelism scales poorly here because xunit parallelizes per test collection (one per test module) and the runtime is dominated by a few slow async/MailboxProcessor collections with real waits — the run can't go below the slowest collection, while the other collections are sub-millisecond assertions either way.

🤖 Generated with Claude Code

Replace the deprecated xunit v2 (`XUnit` 2.9.3) package with `xunit.v3`
3.2.2 in the Beam, Php, Python and Rust test projects.

v3 test projects are self-executing, so each project now sets
`OutputType=Exe` (added to Rust and Php; Python and Beam already had it)
and the .NET-side `[<EntryPoint>]` is dropped in favour of the runner
entry point generated by xunit.v3:

- Rust `main.fs` is also transpiled to Rust's `fn main`, so its entry
  point is guarded with `#if FABLE_COMPILER` rather than removed.
- Python/Beam/Php had the entry point only in the .NET (`#else`) branch
  of `Main.fs`, which is removed.

`xunit.runner.visualstudio` 3.1.5 is already v3-compatible, so
`dotnet test` continues to work. Test sources need no changes:
`Xunit.FactAttribute`, `Assert.Equal` and `Assert.NotEqual` are
unchanged in v3.

Verified with `dotnet test -c Release`: Rust (2449), Python (2266) and
Beam (2439) pass. The Php project has pre-existing compile errors on
.NET unrelated to xunit and is not run by the build system.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Python Type Checking Results (Pyright)

Metric Value
Total errors 34
Files with errors 4
Excluded files 4
New errors ✅ No
Excluded files with errors (4 files)

These files have known type errors and are excluded from CI. Remove from pyrightconfig.ci.json as errors are fixed.

File Errors Status
temp/tests/Python/test_hash_set.py 18 Excluded
temp/tests/Python/test_applicative.py 12 Excluded
temp/tests/Python/test_nested_and_recursive_pattern.py 2 Excluded
temp/tests/Python/fable_modules/thoth_json_python/encode.py 2 Excluded

dbrattli and others added 3 commits May 28, 2026 22:06
CI revealed that the Fable transpilation of the Rust tests failed with
FS0433 ("EntryPointAttribute must be the last declaration in the last
file"). When Fable cracks a project it defines FABLE_COMPILER and runs
the CoreCompile target, which triggers xunit.v3's _XunitAttachSourceFiles
target. That appends a generated entry point and the built-in runner
reporters after the project's own source files. For Rust this collides
with the `[<EntryPoint>]` in tests/src/main.fs (and those sources also
reference runtime types Fable cannot transpile).

Disable both attached source files for the Fable build only, keyed off
the FABLE_COMPILER MSBuild property that Fable's MSBuildCrackerResolver
passes (`/p:FABLE_COMPILER=True`). On .NET the properties keep their
defaults, so xunit.v3 still generates the test runner entry point.

Verified `dotnet test -c Release` (Rust 2449 passing) and the Fable
transpile (Rust/Python/Beam) all succeed with no xunit sources leaking
into the transpiled output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Several MailboxProcessor/async tests assert from inside a fire-and-forget
`Async.StartImmediate`, whose continuation can run after its test
collection has been torn down. xunit.v3 reports such background-thread
exceptions as catastrophic failures and exits non-zero (xunit v2 silently
ignored them). With parallel test collections this surfaces on Windows as
"Catastrophic failure: Assert.Equal() ... Expected 42, Actual 0".

Add tests/Python/xunit.runner.json disabling test-collection
parallelization so each collection fully drains before the next runs.
This is the only xunit suite that runs on Windows in CI (Rust/Beam/Php
.NET tests run on Linux only; the JS suite uses Expecto).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…feedback

- Add xunit.runner.json to Beam and Php: their tests use the same
  fire-and-forget Async.StartImmediate pattern as Python, so they are
  exposed to the same xunit.v3 catastrophic background-exception failures.
- Collapse the now-identical #if FABLE_COMPILER branches in
  Beam/Php/Python Main.fs and add the missing trailing newlines.
- Compress the .fsproj comments explaining the cracking workaround.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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