Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ just setup # Install .NET tools (Fable, Paket, Fantomas, ShipIt)
just restore # Restore NuGet dependencies
just build # Build F# source
just test # Full pipeline: F# → Erlang → compile → run on BEAM
just test-dotnet # Verify F# compiles (no BEAM needed)
just format # Format with Fantomas
just format-check # Check formatting
just dev=true test # Test against local ../fable repo instead of dotnet tool
Expand All @@ -26,29 +25,53 @@ just dev=true test # Test against local ../fable repo instead of dotnet tool
### Test pipeline detail (`just test`)

1. `dotnet build test/` — compile F# to IL
2. `dotnet fable test/ --lang Erlang --outDir build/tests` — transpile to `.erl`
3. Copy `test/test_runner.erl` into `build/tests/src/`
2. `dotnet fable test/ --lang Erlang --outDir build/tests` — transpile to `.erl` (Quill's
`[<EntryPoint>]` in `Main.fs` becomes `main:main/1`)
3. Copy the helper servers (`test_counter_server.erl`, `test_basic_sup.erl`) + `rebar.config`
into `build/tests/src/`
4. `cd build/tests && rebar3 compile` — compile Erlang to BEAM bytecode
5. `erl -noshell ...` — run test_runner which discovers and executes all `test_*` functions
5. `erl -noshell ...` — run `main:main([])`, the Scriptorium (Quill) runner, which executes the
registered suites and halts the VM with its exit code (non-zero on failure)

The test project consumes Scriptorium from NuGet (`Scriptorium.Quill` + `Scriptorium.Nib`) via
explicit `PackageReference`s, pinned to the same versions as `../Fable.Actor`. Fable.Core is also
pinned explicitly — an unpinned, paket-injected Fable.Core left `Compiler.isDotnet` undefined when
Fable transpiled Scriptorium's shipped source and failed the BEAM build.

## Writing Tests

Tests live in `test/Test*.fs`. Each test function is marked `[<Fact>]` and uses `equal` for assertions:
Tests live in `test/Test*.fs` and use the Scriptorium test framework: **Nib** for assertions and
the **Quill** runner to execute them. Each file exposes a `tests` value and registers itself in
`Main.fs`:

```fsharp
open Fable.Beam.Testing

[<Fact>]
let ``test something works`` () =
let result = 2 + 2
result |> equal 4
module Fable.Beam.Tests.Foo

open Scriptorium.Quill
open Scriptorium.Nib.Assertion
open type Scriptorium.Quill.Test

let tests =
testList (
"Foo",
[ test ("something works", fun _ ->
let result = 2 + 2
assertThat result (isEqualTo 4) ) ]
)
```

The test runner discovers functions prefixed with `test_` in modules prefixed with `test_`.
F# test names like `` ``test something works`` `` compile to `test_something_works` in Erlang.
- `test ("desc", fun _ -> ...)` registers one test; `testList` groups them.
- `assertThat actual (expected)` is the Nib assertion; chain with `>>` (e.g. `isGreaterThan 0 >> isEven`).
- No `#if FABLE_COMPILER` needed — Scriptorium runs directly on the BEAM (Fable.Beam's target platform), so write each test body once.
- Quill halts the VM with a non-zero exit code on failure, so a failing test fails `just test`.

To add a new test file: create `test/TestFoo.fs`, expose `let tests = ...`, then add
`<Compile Include="TestFoo.fs" />` to `test/Fable.Beam.Test.fsproj` (order matters — put before
`Main.fs`) and register `Foo.tests` in `Main.fs`.

To add a new test file: create `test/TestFoo.fs` and add `<Compile Include="TestFoo.fs" />`
to `test/Fable.Beam.Test.fsproj` (order matters — put before `Main.fs`).
> **Migration status:** the suite is migrating from the old `[<Fact>]` + Erlang `test_runner.erl`
> discovery to Scriptorium. Only the files that expose a `tests` value are compiled (see the fsproj);
> re-add each remaining file as it migrates.

## Writing Bindings

Expand All @@ -70,7 +93,7 @@ Key rules:
src/
otp/ — Bindings for OTP stdlib modules (Erlang.fs, GenServer.fs, Ets.fs, ...)
cowboy/ — Bindings for Cowboy HTTP framework (separate NuGet package)
test/ — Test files (Test*.fs) + test_runner.erl
test/ — Test files (Test*.fs) using Scriptorium; helper .erl servers for gen_server/supervisor tests
build/tests/ — Generated: transpiled .erl files, rebar3 project, compiled BEAM
```

Expand Down
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,6 @@ just
# Build and run tests on BEAM
just test

# Verify F# compiles (without BEAM)
just test-dotnet

# Format code
just format

Expand Down
32 changes: 12 additions & 20 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,40 +33,32 @@ clean-all: clean
build:
dotnet build {{src_path}}

# Transpile tests to Erlang and compile with rebar3
# Transpile tests to Erlang and compile with rebar3. The entry point is Quill (Main.fs), which
# Fable emits as main:main/1 -- there is no test_runner.erl anymore.
build-beam:
dotnet build {{test_path}}
{{fable}} {{test_path}} --lang Erlang --outDir {{build_path}}/tests
cp {{test_path}}/test_runner.erl {{test_path}}/test_counter_server.erl {{test_path}}/test_basic_sup.erl {{build_path}}/tests/src/
cp {{test_path}}/test_counter_server.erl {{test_path}}/test_basic_sup.erl {{build_path}}/tests/src/
cp {{test_path}}/rebar.config {{build_path}}/tests/rebar.config
cd {{build_path}}/tests && rebar3 compile

# Run BEAM tests (transpile F# to Erlang, compile, run on BEAM)
# Run BEAM tests via the Scriptorium (Quill) runner. main:main/1 runs the registered suites and
# halts the VM with its exit code, so a failing test fails this recipe. The scriptorium_* ebins are
# needed on the code path -- Quill's DSL lives in scriptorium_quill_dsl etc., which erl will not
# load unless their ebin dirs are on the path (an unloaded module shows up as an undef call).
test: build-beam
@echo ""
cd {{build_path}}/tests && erl -noshell \
-pa _build/default/lib/fable_beam_test/ebin \
-pa _build/default/lib/fable_library_beam/ebin \
-pa _build/default/lib/jsx/ebin \
-eval 'test_runner:main(["_build/default/lib/fable_beam_test/ebin"])' \
-pa _build/default/lib/scriptorium_quill/ebin \
-pa _build/default/lib/scriptorium_nib/ebin \
-pa _build/default/lib/scriptorium_parchment/ebin \
-pa _build/default/lib/scriptorium_ink/ebin \
-eval 'main:main([])' \
-s init stop

# Run only the dotnet build (verify F# compiles)
test-dotnet:
dotnet build {{test_path}}
dotnet run --project {{test_path}}

# Spike: run the Scriptorium test framework (Nib + Quill) on the BEAM.
# No test_runner.erl and no [<Fact>]: Quill's runner is the [<EntryPoint>], which Fable emits as
# main:main/1. It halts the VM with the suite's exit code, so a failing test fails this recipe.
spike:
dotnet build spike/scriptorium
{{fable}} spike/scriptorium --lang beam -o spike/scriptorium/beam-build
cd spike/scriptorium/beam-build && rebar3 compile
@echo ""
cd spike/scriptorium/beam-build && \
ERL_LIBS="$(pwd)/_build/default/lib" erl -noshell -eval 'main:main([])' -s init stop

# Create NuGet packages with versions from changelogs
pack:
#!/usr/bin/env bash
Expand Down
77 changes: 0 additions & 77 deletions spike/scriptorium/Main.fs

This file was deleted.

20 changes: 0 additions & 20 deletions spike/scriptorium/Spike.fsproj

This file was deleted.

45 changes: 27 additions & 18 deletions test/Fable.Beam.Test.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
<LangVersion>preview</LangVersion>
<OutputType>Exe</OutputType>
<NoWarn>NU1510</NoWarn>
Expand All @@ -14,34 +13,44 @@
<ProjectReference Include="../src/Fable.Beam.fsproj" />
<ProjectReference Include="../src/jsx/Fable.Beam.Jsx.fsproj" />
</ItemGroup>
<!-- Scriptorium consumed from NuGet (cached + on nuget.org), exactly like Fable.Actor. Fable.Core
is pinned to the same version so Fable transpiles Scriptorium's shipped source against a
known runtime. An unpinned, paket-injected Fable.Core left Compiler.isDotnet undefined when
Fable transpiled Quill's Prelude and failed the BEAM build. -->
<ItemGroup>
<PackageReference Include="Fable.Core" Version="5.2.0" />
<PackageReference Include="Scriptorium.Quill" Version="0.5.1" />
<PackageReference Include="Scriptorium.Nib" Version="0.4.1" />
</ItemGroup>
<!-- Scriptorium migration: only modules converted to Quill/Nib are compiled. Each remaining
file is re-added here (before Main.fs) as it is migrated off [<Fact>]. -->
<ItemGroup>
<Compile Include="TestErlang.fs" />
<Compile Include="TestGenServer.fs" />
<Compile Include="TestSupervisor.fs" />
<Compile Include="TestTimer.fs" />
<Compile Include="TestEts.fs" />
<Compile Include="TestIo.fs" />
<Compile Include="TestIoLib.fs" />
<Compile Include="TestLists.fs" />
<Compile Include="TestMaps.fs" />
<Compile Include="TestBinary.fs" />
<Compile Include="TestGenServer.fs" />
<Compile Include="TestBase64.fs" />
<Compile Include="TestMath.fs" />
<Compile Include="TestIo.fs" />
<Compile Include="TestIoLib.fs" />
<Compile Include="TestRand.fs" />
<Compile Include="TestProplists.fs" />
<Compile Include="TestString.fs" />
<Compile Include="TestBinary.fs" />
<Compile Include="TestCalendar.fs" />
<Compile Include="TestQueue.fs" />
<Compile Include="TestBase64.fs" />
<Compile Include="TestRand.fs" />
<Compile Include="TestLists.fs" />
<Compile Include="TestString.fs" />
<Compile Include="TestErlang.fs" />
<Compile Include="TestRe.fs" />
<Compile Include="TestCalendar.fs" />
<Compile Include="TestDynamic.fs" />
<Compile Include="TestUriString.fs" />
<Compile Include="TestCallbacks.fs" />
<Compile Include="TestOs.fs" />
<Compile Include="TestPort.fs" />
<Compile Include="TestFile.fs" />
<Compile Include="TestSupervisor.fs" />
<Compile Include="TestLogger.fs" />
<Compile Include="TestFile.fs" />
<Compile Include="TestEts.fs" />
<Compile Include="TestJsx.fs" />
<Compile Include="TestDynamic.fs" />
<Compile Include="TestCallbacks.fs" />
<Compile Include="Main.fs" />
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />
</Project>
</Project>
45 changes: 37 additions & 8 deletions test/Main.fs
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
#if FABLE_COMPILER
module Program
module Fable.Beam.Tests.Main

()
#else
module Program =
[<EntryPoint>]
let main _ = 0
#endif
open Scriptorium.Quill
open type Scriptorium.Quill.Runner

// Quill is the entry point here (not an Erlang test_runner): Fable emits this as main:main/1, and
// Quill runs the registered suites then halts the VM with its exit code -- non-zero on failure.
// This is a BEAM-only subset for now: only the modules converted off [<Fact>] are registered.
// Add more `.tests` below as each remaining file migrates over to Scriptorium.
[<EntryPoint>]
let main _ =
runTests
[ Timer.tests
Maps.tests
GenServer.tests
Base64.tests
Math.tests
Io.tests
IoLib.tests
Rand.tests
Proplists.tests
Binary.tests
Calendar.tests
Queue.tests
Lists.tests
String.tests
Erlang.tests
Re.tests
Dynamic.tests
UriString.tests
Callbacks.tests
Os.tests
Port.tests
Supervisor.tests
Logger.tests
File.tests
Ets.tests
Jsx.tests ]
Loading