Skip to content

Commit 5f92375

Browse files
dbrattliclaude
andauthored
test: adopt Scriptorium (Quill + Nib) for the cross-target suite (#35)
* test: adopt Scriptorium (Quill + Nib) for the cross-target suite Replace the hand-rolled runner in test/Program.fs with Scriptorium.Quill and the shouldEqual/shouldBeTrue helpers with Scriptorium.Nib, laid out the way Fable.Giraffe does it: one shared suite plus a small runner project per target. test/shared/ Helpers.fs, ActorTests.fs, SupervisionTests.fs, BuilderTests.fs test/dotnet/ dotnet run test/python/ Fable -> Python test/js/ Fable -> JS/Node test/beam/ Fable -> Erlang All 28 tests were ported, not rewritten — only the assertions and the runner changed. Quill speaks Async and the library speaks ActorOp, so Helpers.toAsync bridges the two: the identity on Python/JS/.NET (where ActorOp = Async) and a Run of the CPS chain on BEAM (where Async is erased to synchronous callbacks anyway). Upgrade Fable 5.11 -> 5.13: 5.11 emits Fable package sub-namespaces into nested Sinks/src/ directories that rebar3 never compiles, leaving scriptorium_parchment_sinks_universal undefined and crashing the BEAM run after the tests had passed. 5.13 emits it flat. Verified the src BEAM build and all three timeflies examples still build. Add a JS runner, and make test-native a real behavioral run rather than a compile smoke test — the non-BEAM Actor is MailboxProcessor-based, so the suite genuinely exercises it on .NET. The BEAM run is now self-contained in build/tests-beam (Fable pulls the Fable.Actor sources into the same outDir and generates the rebar.config), so it no longer shares apps/ with the library build, where two competing main.erl modules had to coexist. Delete test/test_runner.erl — dead code listing AsyncRx-era modules that no longer exist. 28/28 on .NET, Python, JS and BEAM. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: collapse the per-target test projects into one The four runner projects were ceremony copied from Fable.Giraffe without re-deriving whether its cause applies here. It does not: Giraffe has a src project per target that each test project must reference (plus BEAM-only deps like Fable.Beam.Cowboy and a rebar cowboy dep), whereas Fable.Actor has one library project with #if FABLE_COMPILER_BEAM inside. Fable.Beam arrives transitively through the project reference, so even the BEAM runner needed no package of its own. The four Main.fs files were byte-identical apart from comments, and the four fsproj files differed only by that one redundant Fable.Beam reference. One project compiles to all four targets from a shared obj/, back to back, with no --noCache — 28/28 on .NET, Python, JS and BEAM. The per-target runner comments are consolidated into test/Main.fs, and test/shared/ is gone: it only meant something when there was a non-shared counterpart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: fix the dev=true Fable path in the justfile fable_repo pointed at ../fable/beam-improvements-17 and ../fable/main. The branch name is long dead, and the parent path is wrong too — ../fable does not exist on a case-sensitive filesystem, so every dev=true recipe was broken, not just the BEAM one. Point it at ../Fable/src/Fable.Cli, the same convention Fable.Giraffe uses, and let whatever branch that checkout has out be the one used. That collapses fable/fable_beam back into a single variable: the split only encoded "the BEAM backend lives on its own branch", which stopped being true once every backend landed in one repo. Also route build-timeflies-js through {{fable}} — it hardcoded `dotnet fable`, so dev=true silently skipped it. Verified dev=true now expands to an existing Fable.Cli.fsproj, and the default dev=false path still runs 28/28 on all four targets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8fec1f1 commit 5f92375

17 files changed

Lines changed: 1195 additions & 1122 deletions

.config/dotnet-tools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"isRoot": true,
44
"tools": {
55
"fable": {
6-
"version": "5.11.0",
6+
"version": "5.13.0",
77
"commands": [
88
"fable"
99
],

.github/workflows/build-and-test.yml

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ jobs:
2929
with:
3030
python-version: '3.12'
3131

32+
- name: Setup Node.js
33+
uses: actions/setup-node@v5
34+
with:
35+
node-version: '20'
36+
3237
- name: Install uv
3338
uses: astral-sh/setup-uv@v6
3439

@@ -41,20 +46,21 @@ jobs:
4146
- name: Build library
4247
run: dotnet build src/Fable.Actor
4348

44-
- name: Build tests
45-
run: dotnet build test
46-
49+
# The shared suite (test/shared) runs on .NET, Python, JS and BEAM
4750
- name: Run .NET tests
48-
run: dotnet run --project test
49-
50-
- name: Compile tests to Python
51-
run: dotnet fable test --lang python --outDir build/tests --exclude Fable.Core --noCache
51+
run: just test-native
5252

5353
- name: Run Python tests
54-
run: cd build/tests && uv run --project ../.. python program.py
54+
run: just test-python
55+
56+
- name: Run JS tests
57+
run: just test-js
5558

56-
- name: Build BEAM and run tests
59+
- name: Run BEAM tests
5760
run: just test-beam
5861

62+
- name: Build BEAM library
63+
run: just build
64+
5965
- name: Pack NuGet
6066
run: just pack

CLAUDE.md

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,46 @@ Fable.Actor is a cross-platform actor library written in F# and compiled via [Fa
99
## Build Commands
1010

1111
```sh
12-
dotnet build src/Fable.Actor # Type-check
13-
dotnet fantomas src -r # Format
12+
just check # Type-check library + test projects
13+
just build # F# -> Erlang, then rebar3 compile
14+
just format # dotnet fantomas src test
15+
just test # Shared suite on .NET + Python + JS + BEAM
16+
just test-native # .NET only (fastest feedback loop)
17+
just test-beam # BEAM only
1418
```
1519

20+
## Tests
21+
22+
One suite, one project (`test/Fable.Actor.Tests.fsproj`), compiled to each target:
23+
24+
```
25+
test/Helpers.fs ActorTests.fs SupervisionTests.fs BuilderTests.fs Main.fs
26+
```
27+
28+
Assertions come from [Scriptorium](https://github.com/fable-hub/Scriptorium) — Nib for
29+
`assertThat x (isEqualTo y)`, Quill for the runner (`runTests [ ... ]`).
30+
31+
Do **not** split this into a project per target. Fable.Giraffe does, because it has a src project
32+
per target that each test project must reference; Fable.Actor has one library project with
33+
`#if FABLE_COMPILER_BEAM` inside, and `Fable.Beam` arrives transitively through the project
34+
reference. One project compiles cleanly to all four targets from a shared `obj/`, no `--noCache`
35+
needed. Split only if a target ever needs its own package or rebar dependency.
36+
37+
Notes:
38+
39+
- **Quill speaks `Async`, the library speaks `ActorOp`.** `Helpers.toAsync` bridges them: the
40+
identity on Python/JS/.NET (where `ActorOp = Async`), and on BEAM a `Run` of the CPS chain
41+
(where `Async` is erased to synchronous callbacks anyway). Tests read
42+
`testAsync("name", fun _ -> toAsync (actor { ... }))`.
43+
- **`reporter`** (in `Helpers.fs`) is how a test observes state that crosses a process boundary:
44+
on BEAM a `let mutable` captured by a spawned actor is a copy, so the value has to be published
45+
with `Actor.cast` and read back with `Actor.call`.
46+
- **BEAM prints `Error in process <0.x.0> with exit value:` reports.** That is the VM logging the
47+
children the supervision tests deliberately crash — expected, not a failure. The exit code and
48+
Quill's summary line are what matter.
49+
- The BEAM run is self-contained: Fable compiles the suite *and* the `Fable.Actor` sources it
50+
references into `build/tests-beam`, and generates the `rebar.config` for it.
51+
1652
## Architecture
1753

1854
One F# project: `src/Fable.Actor/`
@@ -48,6 +84,9 @@ Each target provides a native `fable_actor_platform` module implementing `IActor
4884
## Dependencies
4985

5086
- .NET SDK 10+
51-
- Fable 5.11 (local tool, see `.config/dotnet-tools.json`)
52-
- Fable.Core 5.0.0
87+
- Fable 5.13 (local tool, see `.config/dotnet-tools.json`) — 5.11 emits Fable package
88+
sub-namespaces into nested `Sinks/src/` directories that rebar3 never compiles, which breaks
89+
Scriptorium.Parchment on BEAM
90+
- Fable.Core 5.0.0 (library) / 5.2.0 (test projects, required by Scriptorium)
5391
- fable-library 5.11+ (Python target)
92+
- Scriptorium.Quill 0.5.1 + Scriptorium.Nib 0.4.1 (test projects only)

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ just format # Format source with Fantomas
1818

1919
## Test
2020

21+
One behavioral suite in `test/` runs on every target — the same project, compiled by Fable to each
22+
language. Tests are written with [Scriptorium](https://github.com/fable-hub/Scriptorium) — Quill for
23+
the runner, Nib for assertions.
24+
2125
```sh
22-
just test-native # Run .NET tests
26+
just test-native # Run on .NET (MailboxProcessor implementation)
2327
just test-python # Compile to Python via Fable, then run
24-
just test-beam # Compile to Erlang via Fable, then run
25-
just test # Run .NET + Python tests
28+
just test-js # Compile to JavaScript via Fable, then run under Node
29+
just test-beam # Compile to Erlang via Fable, then run on the BEAM VM
30+
just test # All four
2631
```
2732

2833
## Quick Start

justfile

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Fable.Actor development tasks
22

3-
# Development mode: use local Fable repo instead of dotnet tool
3+
# Development mode: compile with a local Fable checkout instead of the pinned dotnet tool.
4+
# Whatever branch that checkout has out is what gets used — every backend lives in one repo.
45
# Usage: just dev=true test-beam
56
dev := "false"
6-
fable_repo := justfile_directory() / "../fable"
7-
fable := if dev == "true" { "dotnet run --project " + fable_repo / "beam-improvements-17/src/Fable.Cli" + " --" } else { "dotnet fable" }
8-
fable_python := if dev == "true" { "dotnet run --project " + fable_repo / "main/src/Fable.Cli" + " --" } else { "dotnet fable" }
7+
fable_repo := justfile_directory() / "../Fable"
8+
fable := if dev == "true" { "dotnet run --project " + fable_repo / "src/Fable.Cli" + " --" } else { "dotnet fable" }
99

1010
src_path := "src/Fable.Actor"
1111
build_path := "build"
@@ -30,10 +30,11 @@ build: clean
3030
# Build F# projects only (type check)
3131
check:
3232
dotnet build src/Fable.Actor
33+
dotnet build {{test_path}}
3334

3435
# Format source files
3536
format:
36-
dotnet fantomas src
37+
dotnet fantomas src {{test_path}}
3738

3839
# Setup tooling
3940
restore:
@@ -65,32 +66,37 @@ shipit *args:
6566

6667
# --- Tests ---
6768

68-
# Run all tests (.NET + Python + BEAM)
69-
test: test-native test-python test-beam
69+
# One suite in test/, compiled to each target from the same project. Assertions come from
70+
# Scriptorium.Nib, the runner from Scriptorium.Quill.
71+
72+
# Run the behavioral suite on every target (.NET + Python + JS + BEAM)
73+
test: test-native test-python test-js test-beam
7074

71-
# Run .NET tests only
75+
# .NET target: a real behavioral run — the non-BEAM Actor is MailboxProcessor-based
7276
test-native:
73-
dotnet build {{test_path}}
74-
@echo "Running .NET tests..."
7577
dotnet run --project {{test_path}}
7678

77-
# Run Python tests: compile F# → Python via Fable, then run
79+
# Python target: compile the suite to Python and run the explicit runner
7880
test-python:
79-
rm -rf {{build_path}}/tests
80-
{{fable_python}} {{test_path}} --lang python --outDir {{build_path}}/tests --exclude Fable.Core --noCache
81-
@echo "Running Python tests..."
82-
cd {{build_path}}/tests && uv run --project ../.. python program.py
83-
84-
# Run BEAM tests: compile F# → Erlang via Fable, then run
85-
test-beam: build
86-
{{fable}} {{test_path}} --exclude Fable.Core --lang beam --outDir apps/test --noCache
87-
cd {{justfile_directory()}} && rebar3 compile
88-
@echo "Running BEAM tests..."
89-
cd {{justfile_directory()}} && erl \
90-
-pa _build/default/lib/*/ebin \
91-
-noshell \
92-
-eval "main:main([])" \
93-
-s init stop
81+
rm -rf {{build_path}}/tests-py
82+
{{fable}} {{test_path}} --exclude Fable.Core --lang python --outDir {{build_path}}/tests-py
83+
uv run python {{build_path}}/tests-py/main.py
84+
85+
# JS target: compile the suite to JS and run it under Node
86+
test-js:
87+
rm -rf {{build_path}}/tests-js
88+
{{fable}} {{test_path}} --exclude Fable.Core --lang javascript --outDir {{build_path}}/tests-js
89+
echo '{"type":"module"}' > {{build_path}}/tests-js/package.json
90+
node {{build_path}}/tests-js/Main.js
91+
92+
# BEAM target: compile the suite to Erlang, build with rebar3, run on the BEAM VM.
93+
# Fable pulls the Fable.Actor sources into the same outDir, so this app is self-contained
94+
# and the generated rebar.config needs no edits. Quill calls halt/1 with the exit code.
95+
test-beam:
96+
rm -rf {{build_path}}/tests-beam
97+
{{fable}} {{test_path}} --exclude Fable.Core --lang beam --outDir {{build_path}}/tests-beam
98+
cd {{build_path}}/tests-beam && rebar3 compile
99+
cd {{build_path}}/tests-beam && erl -noshell -pa _build/default/lib/*/ebin -eval 'main:main([])'
94100

95101
# --- Timeflies example ---
96102

@@ -121,7 +127,7 @@ timeflies_py_out := timeflies_py_path / "output"
121127
# Build timeflies-python: F# → Python via Fable
122128
build-timeflies-python:
123129
rm -rf {{timeflies_py_out}}
124-
{{fable_python}} {{timeflies_py_src}} --lang python --outDir {{timeflies_py_out}} --exclude Fable.Core --noCache
130+
{{fable}} {{timeflies_py_src}} --lang python --outDir {{timeflies_py_out}} --exclude Fable.Core --noCache
125131
touch {{timeflies_py_out}}/src/__init__.py
126132
touch {{timeflies_py_out}}/src/Fable_Actor/__init__.py
127133

@@ -137,7 +143,7 @@ timeflies_js_src := timeflies_js_path / "src"
137143
# Build timeflies-js: F# → JavaScript via Fable
138144
build-timeflies-js:
139145
cd {{timeflies_js_path}} && npm install
140-
cd {{timeflies_js_path}} && dotnet fable src --noCache
146+
cd {{timeflies_js_path}} && {{fable}} src --noCache
141147

142148
# Run timeflies-js demo on http://localhost:3000
143149
run-timeflies-js: build-timeflies-js

rebar.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{erl_opts, [no_debug_info, nowarn_unused_result]}.
22

3-
{project_app_dirs, ["apps/fable_actor", "apps/fable_actor/fable_modules/*", "apps/test"]}.
3+
{project_app_dirs, ["apps/fable_actor", "apps/fable_actor/fable_modules/*"]}.

0 commit comments

Comments
 (0)