|
| 1 | +# Spike: Scriptorium on Fable.Python |
| 2 | + |
| 3 | +**Question:** Can [Scriptorium](https://github.com/fable-hub/Scriptorium) (Maxime Mangel's |
| 4 | +F#/Fable testing stack — Nib assertions + Quill runner) be used to write and run tests for |
| 5 | +Fable.Python, compiled to Python? |
| 6 | + |
| 7 | +**Answer: Yes.** It works out of the box, because Scriptorium already ships explicit |
| 8 | +`#if FABLE_COMPILER_PYTHON` support. |
| 9 | + |
| 10 | +## What this spike contains |
| 11 | + |
| 12 | +- `Spike.fsproj` — an `Exe` project referencing the local `../../../Scriptorium` checkout |
| 13 | + (Quill, which transitively pulls in Nib, Ink, Parchment) **and** the real `Fable.Python` |
| 14 | + bindings. |
| 15 | +- `Main.fs` — a small Quill/Nib suite that exercises plain F# assertions **and** actual |
| 16 | + `Fable.Python.Math` bindings, then exits with the runner's status code. |
| 17 | + |
| 18 | +## Run it |
| 19 | + |
| 20 | +```bash |
| 21 | +just spike-scriptorium |
| 22 | +# or directly: |
| 23 | +dotnet fable spike/scriptorium --lang python -o spike/scriptorium/build \ |
| 24 | + --run uv run python spike/scriptorium/build/main.py |
| 25 | +``` |
| 26 | + |
| 27 | +Expected output: Quill's colored reporter, `8 passed (8)`, exit code 0. Break an assertion |
| 28 | +and the suite reports a colored diff with a clickable `Main.fs:NN` source link and exits 1. |
| 29 | + |
| 30 | +## Findings |
| 31 | + |
| 32 | +1. **Scriptorium compiles cleanly to Python.** All of Ink, Parchment, Nib, Quill and Quill's |
| 33 | + DSL compiled via `--lang python` with no errors. The maintainer already guards every |
| 34 | + platform-specific spot (`cwd` → `os.getcwd()`, stopwatch → `time.perf_counter()`, |
| 35 | + stdout → `sys.stdout.write`, `isCI`, etc.) behind `FABLE_COMPILER_PYTHON`. The JS-only |
| 36 | + `performance.now()` / `setTimeout` emits sit in dead branches gated by `Compiler.isJavaScript` |
| 37 | + and are eliminated for Python. |
| 38 | + |
| 39 | +2. **The runner works on real CPython** — colored dots, per-test timing, a summary table, and |
| 40 | + a proper failure report with a unified diff and OSC-8 source hyperlink. |
| 41 | + |
| 42 | +3. **Exit-code gotcha (now fixed upstream in Fable 5.8.0).** Quill correctly *returns* exit code |
| 43 | + 1 on failure, but Fable's Python backend used to emit `main(sys.argv)` and **ignore the return |
| 44 | + value**, so the process always exited 0 — CI would never see failures. This spike originally |
| 45 | + carried a `Fable.Python.Sys.sys.exit exitCode` shim to work around it. Fable 5.8.0 fixes it: |
| 46 | + the generated entry point now emits `sys.exit(int(main(...)))` (the `int(...)` coercion matters |
| 47 | + because fable-library-python's `Int32` is not an `int` subclass), so the shim was removed and |
| 48 | + `Main.fs` is now just `let main _ = runTests tests`. Verified: passing suite exits 0, a failing |
| 49 | + assertion exits 1. |
| 50 | + |
| 51 | +4. **Snapshot / Browser / Hedgehog are out of scope / blocked.** `Nib.Browser` is Playwright |
| 52 | + (JS-only). Scriptorium's own `fable-repros/` notes that `Nib.Snapshot` and `Hedgehog.Derive` |
| 53 | + hit fable-library-python runtime gaps (Hedgehog: 13/21 tests fail on Python). For Fable.Python |
| 54 | + the useful surface is **Nib + Quill** (+ Ink/Parchment). |
| 55 | + |
| 56 | +## How this compares to the current test setup |
| 57 | + |
| 58 | +Today `test/` uses a home-grown `Fable.Python.Testing` module (`[<Fact>]` + `equal`/`throws*`) |
| 59 | +compiled to Python and run under **pytest**. Scriptorium is a different execution model: it is |
| 60 | +its own runner with an `[<EntryPoint>]`, run as `python main.py` (not pytest). Migrating the |
| 61 | +suite would mean rewriting `[<Fact>]` functions into `test(...)`/`testList(...)` trees and |
| 62 | +swapping `equal`/`throws*` for Nib's `assertThat ... (isEqualTo ...)` combinators. This spike |
| 63 | +proves that path is viable before committing to it. |
| 64 | + |
| 65 | +## Notes for productionizing (not done in this spike) |
| 66 | + |
| 67 | +- Reference published NuGet packages (`Scriptorium.Quill` etc.) via paket instead of the local |
| 68 | + source checkout, once the versions with Python support are on nuget.org. |
| 69 | +- Decide runner strategy: keep pytest for existing tests and add Scriptorium alongside, or |
| 70 | + migrate wholesale. Losing pytest means losing its discovery/reporting/CI integrations, so the |
| 71 | + `sys.exit` propagation above is the minimum needed for CI. |
0 commit comments