ludic/docs/language/testing/kw-test.md
Orkuncakilkaya ea2c6ab246
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m13s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 19s
feat(testing): built-in test blocks + expect assertions, auto-run with pass/fail (#12)
A `test "name" { ... }` top-level block is discovered automatically and run by a
synthetic runner @main — no `entry` to write, nothing to register. Inside a test,
expect(cond) / expect_eq(a, b) / expect_near(a, b, tol) assert; on failure they
print `file:line: <what> failed (got G, want W)` and set a per-test fail flag but
keep going, so one run reports every failure. The runner prints `ok   - name` /
`FAIL - name` per test, a `== N passed, M failed ==` summary, and exits non-zero
if any test failed — so `ludic spec_test.ludic` drops straight into bin/x and CI.
expect_near carries the tolerance fixed-point / accumulated-integer game math need.

Frontend: new `test` keyword (parse_test -> N_TEST, collected in g_tests) and the
call node now carries its source line for the file:line messages. Backend:
emit_test_runner synthesises @fn__test_i bodies + the runner @main; the expect*
builtins lower to a branch-print-flag tail (emit_expect_fail). g_src_name (set in
main from the input path) supplies the filename. The compiler's own source has no
`test` blocks, so its self-compiled IR is unchanged and the C-free fixpoint holds.

- `test` wired into the vocabulary (ludic_syntax.h, JetBrains lexer, TextMate
  grammar) and documented (docs/language/testing/)
- examples/library/testing.ludic: a passing spec, guarded by a new spec_case in
  the regression suite (build, run, require exit 0 + the expected summary)

Coverage instrumentation (the biggest lift in #12) is left as a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 14:27:45 +03:00

1.7 KiB

id name category kind tokens sig tip order
kw-test test testing keyword test test "name" { … } A named test block, run automatically with pass/fail reporting. 1

A test "name" { … } block declares a named test. Every test in a file is discovered automatically and run by a generated entry point — there is no entry to write and nothing to register. The runner prints ok - name for a test whose assertions all held and FAIL - name for one that had a failure, then a == N passed, M failed == summary, and the program exits non-zero if any test failed. Writing a test should feel like writing a function: put a few assertions in a block and run it.

Assertions (each records a failure and prints file:line: … failed but keeps going, so one run reports every failure):

  • expect(cond) — the boolean cond must be true.
  • expect_eq(a, b) — a must equal b; on failure prints (got a, want b).
  • expect_near(a, b, tol) — a must be within tol of b (absolute). Use it for fixed-point results and accumulated integer math, where an exact match is too brittle.

Run a spec file directly with the compiler-runner — ludic mymath_test.ludic compiles it to a native binary, runs it, and forwards the pass/fail exit code — so it drops straight into bin/x and CI.

program MathSpec {
  function add(a: int, b: int) -> int { return a + b }

  test "addition adds" {
    expect_eq(add(2, 3), 5)
    expect(add(1, 1) == 2)
  }

  test "fixed math is close enough" {
    let half = fixed(1) / 2          # 0.5 in Q16.16
    expect_near(half, 32768, 2)      # within 2 raw units of 0.5
  }
}