Skip to content

Commit a552d42

Browse files
dbrattliclaude
andcommitted
spike: evaluate Scriptorium (Nib + Quill) as a Python test framework
Proof-of-concept showing Scriptorium's Nib assertions + Quill runner compile to Python via Fable.Python and can test real bindings. Adds spike/scriptorium/ (Spike.fsproj, Main.fs, README.md) and a `just spike-scriptorium` target. Not wired into CI or the main suite — this is a spike to inform whether/how to adopt Scriptorium. See spike/scriptorium/README.md for findings and trade-offs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 774f87a commit a552d42

4 files changed

Lines changed: 152 additions & 0 deletions

File tree

‎justfile‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ test: build
4949
pyright: build
5050
uv run pyright
5151

52+
# SPIKE: run the Scriptorium (Nib + Quill) test suite compiled to Python
53+
spike-scriptorium:
54+
{{fable}} spike/scriptorium --lang python -o spike/scriptorium/build --run uv run python spike/scriptorium/build/main.py
55+
5256
# Create NuGet package with version from CHANGELOG.md
5357
pack:
5458
#!/usr/bin/env bash

‎spike/scriptorium/Main.fs‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
module SpikeMain
2+
3+
// SPIKE: prove Scriptorium's Nib assertions + Quill runner compile to Python via
4+
// Fable.Python and can test real Fable.Python bindings.
5+
6+
open Scriptorium.Quill
7+
open Scriptorium.Nib.Assertion
8+
9+
open type Scriptorium.Quill.Test
10+
open type Scriptorium.Quill.Runner
11+
12+
open Fable.Python.Math
13+
14+
let tests =
15+
[
16+
// Plain F# values through the Nib fluent assertion chain.
17+
testList (
18+
"Nib basics",
19+
[
20+
test ("isEqualTo passes", fun _ -> assertThat 42 (isEqualTo 42))
21+
test (
22+
"chained assertions",
23+
fun _ -> assertThat 42 (isEqualTo 42 >> isGreaterThan 40 >> isLessThan 50)
24+
)
25+
test ("strings", fun _ -> assertThat "hello" (isEqualTo "hello"))
26+
test ("booleans", fun _ -> assertThat (1 = 1) isTrue)
27+
]
28+
)
29+
30+
// Exercise actual Fable.Python stdlib bindings (Python's math module) and
31+
// assert the results with Nib. This runs on real CPython at test time.
32+
testList (
33+
"Fable.Python.Math bindings",
34+
[
35+
test ("math.sqrt", fun _ -> assertThat (math.sqrt 9.0) (isEqualTo 3.0))
36+
test ("math.factorial", fun _ -> assertThat (math.factorial 5) (isEqualTo 120))
37+
test ("math.gcd", fun _ -> assertThat (math.gcd (12, 8)) (isEqualTo 4))
38+
test ("math.floor", fun _ -> assertThat (math.floor 2.9) (isEqualTo 2))
39+
]
40+
)
41+
]
42+
43+
// Fable 5.8.0+ emits `sys.exit(int(main(...)))`, so the entry point's int return
44+
// propagates to the process exit code (1 on failure) with no extra plumbing.
45+
[<EntryPoint>]
46+
let main _ = runTests tests

‎spike/scriptorium/README.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.

‎spike/scriptorium/Spike.fsproj‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<!--
4+
SPIKE: Scriptorium (Nib assertions + Quill runner) compiled to Python via Fable.Python.
5+
6+
Scriptorium ships explicit FABLE_COMPILER_PYTHON support, so its sources compile
7+
straight to CPython. We reference the local checkout (../../../Scriptorium) directly so
8+
the spike tracks the exact source, independent of NuGet publish timing.
9+
10+
Compile + run: see spike/scriptorium/README.md
11+
-->
12+
13+
<PropertyGroup>
14+
<TargetFramework>net9.0</TargetFramework>
15+
<OutputType>Exe</OutputType>
16+
<IsPackable>false</IsPackable>
17+
<LangVersion>preview</LangVersion>
18+
</PropertyGroup>
19+
20+
<ItemGroup>
21+
<Compile Include="Main.fs" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<!-- Quill transitively drags in Nib, Ink and Parchment. -->
26+
<ProjectReference Include="../../../Scriptorium/src/Scriptorium.Quill/Scriptorium.Quill.fsproj" />
27+
<!-- Reference the actual Fable.Python bindings so we test them with Scriptorium. -->
28+
<ProjectReference Include="../../src/Fable.Python.fsproj" />
29+
</ItemGroup>
30+
31+
</Project>

0 commit comments

Comments
 (0)