From 9185772a40f12f22e6390678be146b609d161584 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 28 May 2026 21:38:03 +0200 Subject: [PATCH 1/3] chore: upgrade xunit from v2 to v3 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 `[]` 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) --- tests/Beam/Fable.Tests.Beam.fsproj | 2 +- tests/Beam/Main.fs | 4 +++- tests/Php/Fable.Tests.Php.fsproj | 3 ++- tests/Php/Main.fs | 4 +++- tests/Python/Fable.Tests.Python.fsproj | 2 +- tests/Python/Main.fs | 4 +++- tests/Rust/Fable.Tests.Rust.fsproj | 3 ++- tests/Rust/tests/src/main.fs | 4 ++++ 8 files changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/Beam/Fable.Tests.Beam.fsproj b/tests/Beam/Fable.Tests.Beam.fsproj index 19bed181ad..360d97ffe1 100644 --- a/tests/Beam/Fable.Tests.Beam.fsproj +++ b/tests/Beam/Fable.Tests.Beam.fsproj @@ -10,7 +10,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Beam/Main.fs b/tests/Beam/Main.fs index b00af3eaac..7cfe91c15e 100644 --- a/tests/Beam/Main.fs +++ b/tests/Beam/Main.fs @@ -2,5 +2,7 @@ module Program () #else -module Program = let [] main _ = 0 +module Program +// On .NET, xunit.v3 generates the test runner entry point automatically. +() #endif diff --git a/tests/Php/Fable.Tests.Php.fsproj b/tests/Php/Fable.Tests.Php.fsproj index cf4454a014..4b3a21263d 100644 --- a/tests/Php/Fable.Tests.Php.fsproj +++ b/tests/Php/Fable.Tests.Php.fsproj @@ -1,5 +1,6 @@ + Exe net10.0 Major false @@ -8,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Php/Main.fs b/tests/Php/Main.fs index ae5f6f6959..d54c0fa690 100644 --- a/tests/Php/Main.fs +++ b/tests/Php/Main.fs @@ -2,5 +2,7 @@ module Program () #else -module Program = let [] main _ = 0 +module Program +// On .NET, xunit.v3 generates the test runner entry point automatically. +() #endif \ No newline at end of file diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index 0bf797f75f..88b68260f3 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -10,7 +10,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Python/Main.fs b/tests/Python/Main.fs index ae5f6f6959..d54c0fa690 100644 --- a/tests/Python/Main.fs +++ b/tests/Python/Main.fs @@ -2,5 +2,7 @@ module Program () #else -module Program = let [] main _ = 0 +module Program +// On .NET, xunit.v3 generates the test runner entry point automatically. +() #endif \ No newline at end of file diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 5bc246aac9..ceaf6dd9c0 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -1,5 +1,6 @@ + Exe net10.0 Major false @@ -8,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Rust/tests/src/main.fs b/tests/Rust/tests/src/main.fs index 6a938fa462..f9b2027a20 100644 --- a/tests/Rust/tests/src/main.fs +++ b/tests/Rust/tests/src/main.fs @@ -7,5 +7,9 @@ let _imports() = () // [] +#if FABLE_COMPILER +// On .NET, xunit.v3 generates the test runner entry point; only emit our own +// entry point when transpiling (Rust needs a `fn main`). [] let main _args = 0 +#endif From 5fc8745bad97e62a1579472a81b85fab0515ac40 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 28 May 2026 22:06:17 +0200 Subject: [PATCH 2/3] chore: stop xunit.v3 attaching runner sources to the Fable build 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 `[]` 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) --- tests/Beam/Fable.Tests.Beam.fsproj | 7 +++++++ tests/Php/Fable.Tests.Php.fsproj | 7 +++++++ tests/Python/Fable.Tests.Python.fsproj | 7 +++++++ tests/Rust/Fable.Tests.Rust.fsproj | 8 ++++++++ tests/Rust/tests/src/main.fs | 4 ++-- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/Beam/Fable.Tests.Beam.fsproj b/tests/Beam/Fable.Tests.Beam.fsproj index 360d97ffe1..10dbefd827 100644 --- a/tests/Beam/Fable.Tests.Beam.fsproj +++ b/tests/Beam/Fable.Tests.Beam.fsproj @@ -1,6 +1,13 @@ Exe + + false + false net10.0 Major false diff --git a/tests/Php/Fable.Tests.Php.fsproj b/tests/Php/Fable.Tests.Php.fsproj index 4b3a21263d..da00f469da 100644 --- a/tests/Php/Fable.Tests.Php.fsproj +++ b/tests/Php/Fable.Tests.Php.fsproj @@ -1,6 +1,13 @@ Exe + + false + false net10.0 Major false diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index 88b68260f3..c26eb31af3 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -1,6 +1,13 @@ Exe + + false + false net10.0 Major false diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index ceaf6dd9c0..69b26e98e3 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -1,6 +1,14 @@ Exe + + false + false net10.0 Major false diff --git a/tests/Rust/tests/src/main.fs b/tests/Rust/tests/src/main.fs index f9b2027a20..ddaf02e64b 100644 --- a/tests/Rust/tests/src/main.fs +++ b/tests/Rust/tests/src/main.fs @@ -8,8 +8,8 @@ let _imports() = // [] #if FABLE_COMPILER -// On .NET, xunit.v3 generates the test runner entry point; only emit our own -// entry point when transpiling (Rust needs a `fn main`). +// Rust needs a `fn main`, so emit the entry point when transpiling. On .NET, +// xunit.v3 generates the test runner entry point instead (see the .fsproj). [] let main _args = 0 #endif From b796ecb7cb629a85ccae788edbf389b9b0fa1ace Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 28 May 2026 23:01:22 +0200 Subject: [PATCH 3/3] chore: run xunit serially in Python tests to avoid Windows flake 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) --- tests/Python/Fable.Tests.Python.fsproj | 7 +++++++ tests/Python/xunit.runner.json | 5 +++++ 2 files changed, 12 insertions(+) create mode 100644 tests/Python/xunit.runner.json diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index c26eb31af3..d6b037022f 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -29,6 +29,13 @@ + + + + diff --git a/tests/Python/xunit.runner.json b/tests/Python/xunit.runner.json new file mode 100644 index 0000000000..8641732346 --- /dev/null +++ b/tests/Python/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeTestCollections": false, + "maxParallelThreads": 1 +}