Question: Can Scriptorium (Maxime Mangel's F#/Fable testing stack — Nib assertions + Quill runner) be used to write and run tests for Fable.Python, compiled to Python?
Answer: Yes. It works out of the box, because Scriptorium already ships explicit
#if FABLE_COMPILER_PYTHON support.
Spike.fsproj— anExeproject referencing the local../../../Scriptoriumcheckout (Quill, which transitively pulls in Nib, Ink, Parchment) and the realFable.Pythonbindings.Main.fs— a small Quill/Nib suite that exercises plain F# assertions and actualFable.Python.Mathbindings, then exits with the runner's status code.
just spike-scriptorium
# or directly:
dotnet fable spike/scriptorium --lang python -o spike/scriptorium/build \
--run uv run python spike/scriptorium/build/main.pyExpected output: Quill's colored reporter, 8 passed (8), exit code 0. Break an assertion
and the suite reports a colored diff with a clickable Main.fs:NN source link and exits 1.
-
Scriptorium compiles cleanly to Python. All of Ink, Parchment, Nib, Quill and Quill's DSL compiled via
--lang pythonwith no errors. The maintainer already guards every platform-specific spot (cwd→os.getcwd(), stopwatch →time.perf_counter(), stdout →sys.stdout.write,isCI, etc.) behindFABLE_COMPILER_PYTHON. The JS-onlyperformance.now()/setTimeoutemits sit in dead branches gated byCompiler.isJavaScriptand are eliminated for Python. -
The runner works on real CPython — colored dots, per-test timing, a summary table, and a proper failure report with a unified diff and OSC-8 source hyperlink.
-
Exit-code gotcha (now fixed upstream in Fable 5.8.0). Quill correctly returns exit code 1 on failure, but Fable's Python backend used to emit
main(sys.argv)and ignore the return value, so the process always exited 0 — CI would never see failures. This spike originally carried aFable.Python.Sys.sys.exit exitCodeshim to work around it. Fable 5.8.0 fixes it: the generated entry point now emitssys.exit(int(main(...)))(theint(...)coercion matters because fable-library-python'sInt32is not anintsubclass), so the shim was removed andMain.fsis now justlet main _ = runTests tests. Verified: passing suite exits 0, a failing assertion exits 1. -
Snapshot / Browser / Hedgehog are out of scope / blocked.
Nib.Browseris Playwright (JS-only). Scriptorium's ownfable-repros/notes thatNib.SnapshotandHedgehog.Derivehit fable-library-python runtime gaps (Hedgehog: 13/21 tests fail on Python). For Fable.Python the useful surface is Nib + Quill (+ Ink/Parchment).
Today test/ uses a home-grown Fable.Python.Testing module ([<Fact>] + equal/throws*)
compiled to Python and run under pytest. Scriptorium is a different execution model: it is
its own runner with an [<EntryPoint>], run as python main.py (not pytest). Migrating the
suite would mean rewriting [<Fact>] functions into test(...)/testList(...) trees and
swapping equal/throws* for Nib's assertThat ... (isEqualTo ...) combinators. This spike
proves that path is viable before committing to it.
- Reference published NuGet packages (
Scriptorium.Quilletc.) via paket instead of the local source checkout, once the versions with Python support are on nuget.org. - Decide runner strategy: keep pytest for existing tests and add Scriptorium alongside, or
migrate wholesale. Losing pytest means losing its discovery/reporting/CI integrations, so the
sys.exitpropagation above is the minimum needed for CI.