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>
This commit is contained in:
parent
a71279a7b9
commit
ea2c6ab246
16 changed files with 15911 additions and 14867 deletions
3
changes/testing-framework.md
Normal file
3
changes/testing-framework.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
Built-in testing framework — a `test "name" { … }` block, discovered and run automatically by a synthetic runner (no `entry` to write). Inside a test, `expect(cond)`, `expect_eq(a, b)` and `expect_near(a, b, tol)` assert; on failure they print `file:line: … failed (got …, want …)` and mark the test failed without aborting, 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 and accumulated-integer game math need. Coverage instrumentation is a follow-up.
|
||||
7
docs/language/testing/_section.md
Normal file
7
docs/language/testing/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: testing
|
||||
title: Testing
|
||||
order: 34
|
||||
---
|
||||
|
||||
A built-in testing framework in the spirit of Go's <code>go test</code>: tests live next to the code, run with one command, and report pass/fail — no harness to wire up. A <a href="kw-test"><code>test "name" { … }</code></a> block is discovered automatically and run by a synthetic entry point that prints <code>ok - name</code> or <code>FAIL - name</code> for each, a <code>== N passed, M failed ==</code> summary, and exits non-zero if anything failed (so CI and the <code>bin/x</code> runner catch it). Inside a test, the <code>expect</code>, <code>expect_eq</code> and <code>expect_near</code> assertions check a condition and, on failure, print <code>file:line: … failed (got …, want …)</code> and mark the test failed — without aborting, so one run reports every failure. <code>expect_near</code> takes a tolerance, which is what fixed-point and accumulated-integer game math need. Everything is deterministic and compiles to a native binary, so a suite runs in the same C-free toolchain as the rest of Ludic. Coverage instrumentation is a planned follow-up. Related: <a href="kw-function"><code>function</code></a>, <a href="fn-print"><code>print</code></a>.
|
||||
36
docs/language/testing/kw-test.md
Normal file
36
docs/language/testing/kw-test.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
id: kw-test
|
||||
name: test
|
||||
category: testing
|
||||
kind: keyword
|
||||
tokens: test
|
||||
sig: test "name" { … }
|
||||
tip: A named test block, run automatically with pass/fail reporting.
|
||||
order: 1
|
||||
---
|
||||
|
||||
A <code>test "name" { … }</code> block declares a named test. Every test in a file is discovered automatically and run by a generated entry point — there is no <code>entry</code> to write and nothing to register. The runner prints <code>ok - name</code> for a test whose assertions all held and <code>FAIL - name</code> for one that had a failure, then a <code>== N passed, M failed ==</code> 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 <code>file:line: … failed</code> 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.
|
||||
|
||||
```ludic
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
43
examples/library/testing.ludic
Normal file
43
examples/library/testing.ludic
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# testing.ludic — the built-in testing framework: `test "name" { … }` blocks with
|
||||
# expect / expect_eq / expect_near assertions, discovered and run automatically by
|
||||
# a synthetic runner that prints per-test pass/fail, a summary, and exits non-zero
|
||||
# on any failure. This spec passes, so it prints one `ok - name` per test and:
|
||||
# == 6 passed, 0 failed ==
|
||||
# It is a runnable spec (no `entry`), driven by the runner the compiler generates.
|
||||
program TestingSpec {
|
||||
function add(a: int, b: int) -> int { return a + b }
|
||||
function mul(a: int, b: int) -> int { return a * b }
|
||||
|
||||
test "expect on a boolean" {
|
||||
expect(add(1, 1) == 2)
|
||||
expect(mul(3, 4) == 12)
|
||||
}
|
||||
|
||||
test "expect_eq compares ints" {
|
||||
expect_eq(add(2, 3), 5)
|
||||
expect_eq(mul(6, 7), 42)
|
||||
}
|
||||
|
||||
test "expect_near tolerates a delta" {
|
||||
expect_near(100, 103, 5) # |100 - 103| = 3 <= 5
|
||||
expect_near(add(10, 10), 19, 1) # 20 within 1 of 19
|
||||
}
|
||||
|
||||
test "fixed-point stays close" {
|
||||
let half = fixed(1) / 2 # 0.5 in Q16.16 = 32768
|
||||
expect_near(half, 32768, 0) # exact here, but tolerance is the point
|
||||
let third = fixed(1) / 3 # ~0.3333
|
||||
expect_near(third, 21845, 2) # 65536/3 = 21845.33 -> 21845
|
||||
}
|
||||
|
||||
test "loops and locals work in a test" {
|
||||
var sum = 0
|
||||
for i in 0 .. 5 { sum = sum + i } # 0+1+2+3+4 = 10
|
||||
expect_eq(sum, 10)
|
||||
}
|
||||
|
||||
test "strings compare" {
|
||||
let s = "ab" + "c"
|
||||
expect(s == "abc")
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,25 @@
|
|||
# runtime builtin plus the parameter labels callers may use as named arguments;
|
||||
# after reordering we rewrite the callee to that bare name and fall back into the
|
||||
# ordinary builtin path (which resolves it to its rt_ function).
|
||||
# Emit the failure tail of an `expect*` assertion: if `cond1` (an i1) is false,
|
||||
# set the per-test fail flag and print the message, then continue. With got/want
|
||||
# codes it prints `<msg> (got G, want W)`; otherwise just the message. Leaves the
|
||||
# block live (falls through), so a test keeps running and reports every failure.
|
||||
function emit_expect_fail(cond1: pointer, msgsym: pointer, got: pointer, want: pointer) -> void {
|
||||
let lok = lbl("exok")
|
||||
let lbad = lbl("exbad")
|
||||
emit(` br i1 {cond1}, label %{lok}, label %{lbad}\n`)
|
||||
emit(`{lbad}:\n`)
|
||||
emit(" store i32 1, ptr @L_test_fail\n")
|
||||
if (got == "") {
|
||||
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr {msgsym})\n`)
|
||||
} else {
|
||||
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_expect, ptr {msgsym}, i32 {got}, i32 {want})\n`)
|
||||
}
|
||||
emit(` br label %{lok}\n`)
|
||||
emit(`{lok}:\n`)
|
||||
}
|
||||
|
||||
function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
||||
# Math.* is computed inline (deterministic fixed-point), not routed through a
|
||||
# bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace.
|
||||
|
|
@ -396,6 +415,39 @@ function emit_call(e: Node) -> Val {
|
|||
}
|
||||
if (name == "fixed") { let a = emit_expr(e.kids[0]); return val(emit_bind(`shl i32 {a.code}, 16`), "fixed") }
|
||||
if (name == "floor") { let a = emit_expr(e.kids[0]); return val(emit_bind(`ashr i32 {a.code}, 16`), "int") }
|
||||
# --- the testing framework's assertions (see emit_test_runner) --------------
|
||||
# expect(cond) / expect_eq(a, b) / expect_near(a, b, tol): on failure they set
|
||||
# the per-test fail flag (@L_test_fail) and print `file:line: <what> failed`,
|
||||
# then fall through so a test keeps running and reports every failure. Meant to
|
||||
# be used inside a `test "name" { ... }` block.
|
||||
if (name == "expect") {
|
||||
g_uses_expect = true
|
||||
let a = emit_expr(e.kids[0])
|
||||
let c = emit_bind(`icmp ne i32 {a.code}, 0`)
|
||||
let msg = emit_str_const(`{g_src_name}:{itoa(e.line)}: expect failed`)
|
||||
emit_expect_fail(c, msg, "", "")
|
||||
return val("0", "void")
|
||||
}
|
||||
if (name == "expect_eq") {
|
||||
g_uses_expect = true
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
||||
let c = emit_bind(`icmp eq i32 {a.code}, {b.code}`)
|
||||
let msg = emit_str_const(`{g_src_name}:{itoa(e.line)}: expect_eq failed`)
|
||||
emit_expect_fail(c, msg, a.code, b.code)
|
||||
return val("0", "void")
|
||||
}
|
||||
if (name == "expect_near") {
|
||||
g_uses_expect = true
|
||||
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let tol = emit_expr(e.kids[2])
|
||||
let d = emit_bind(`sub i32 {a.code}, {b.code}`)
|
||||
let neg = emit_bind(`sub i32 0, {d}`)
|
||||
let isneg = emit_bind(`icmp slt i32 {d}, 0`)
|
||||
let ad = emit_bind(`select i1 {isneg}, i32 {neg}, i32 {d}`)
|
||||
let c = emit_bind(`icmp sle i32 {ad}, {tol.code}`)
|
||||
let msg = emit_str_const(`{g_src_name}:{itoa(e.line)}: expect_near failed`)
|
||||
emit_expect_fail(c, msg, a.code, b.code)
|
||||
return val("0", "void")
|
||||
}
|
||||
# The EV2 reflection ABI (the world table), exposed to Ludic so a Ludic mod can
|
||||
# introspect the world by name — the same functions a foreign mod binds. Emitted
|
||||
# only for a modding program (ECS + events), so a plain game is unchanged.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ var g_uses_osrt: bool = false # Os.* (prelude-backed methods) was emitted -> em
|
|||
var g_uses_unicodert: bool = false # Unicode.* was emitted -> emit the UTF-8 runtime
|
||||
var g_uses_fsrt: bool = false # Fs.*/Path.*/Mime.* was emitted -> emit the filesystem runtime
|
||||
var g_uses_datert: bool = false # Date.*/DateTime.* was emitted -> emit the civil<->epoch conversions
|
||||
var g_uses_expect: bool = false # expect/expect_eq/expect_near was emitted -> emit the test-assert globals
|
||||
var g_tests: []Node # test "name" { ... } blocks collected by the parser
|
||||
var g_src_name: pointer = "?" # base name of the source file, for panic/expect file:line messages
|
||||
var g_uses_longstr: bool = false # string(long) / interpolating a long was emitted -> emit fn_long_str
|
||||
|
||||
# loop targets for break/continue (innermost last)
|
||||
|
|
|
|||
|
|
@ -64,6 +64,73 @@ function emit_main(d: Node) -> void {
|
|||
emit("}\n")
|
||||
}
|
||||
|
||||
# ---- the testing framework -------------------------------------------------
|
||||
# A `test "name" { ... }` block lowers to a void function; `expect*` assertions
|
||||
# inside it flip @L_test_fail. A synthetic runner @main runs every test, prints
|
||||
# `ok - name` / `FAIL - name`, a summary, and exits non-zero if any failed.
|
||||
|
||||
# one test block -> a void function @fn__test_<idx> (mirrors emit_fn's shape).
|
||||
function emit_test_fn(t: Node, idx: int) -> void {
|
||||
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
|
||||
ret_ty = "void"
|
||||
let fbody = buf_new()
|
||||
falloc = buf_new()
|
||||
let saved = code
|
||||
code = fbody
|
||||
emit_block(t.a)
|
||||
if not g_term { emit(" br label %ret\n") }
|
||||
emit("ret:\n ret void\n")
|
||||
code = saved
|
||||
emit("define void @fn__test_"); emit(itoa(idx)); emit("() {\nentry:\n")
|
||||
emit(buf_str(falloc))
|
||||
emit(buf_str(fbody))
|
||||
emit("}\n\n")
|
||||
}
|
||||
|
||||
# emit every test body plus the runner @main that drives them.
|
||||
function emit_test_runner() -> void {
|
||||
emith("@L_test_fail = internal global i32 0\n")
|
||||
emith("@.fmt_test_sum = private unnamed_addr constant [28 x i8] c\"== %d passed, %d failed ==\\0A\\00\"\n")
|
||||
var i = 0
|
||||
while i < len(g_tests) { emit_test_fn(g_tests[i], i); i = i + 1 }
|
||||
if g_uses_expect { emith("@.fmt_expect = private unnamed_addr constant [22 x i8] c\"%s (got %d, want %d)\\0A\\00\"\n") }
|
||||
|
||||
ll_t = 0; ll_lbl = 0
|
||||
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")
|
||||
emit(" store i32 %argc, ptr @L_argc\n")
|
||||
emit(" store ptr %argv, ptr @L_argv\n")
|
||||
emit(" %failed = alloca i32\n store i32 0, ptr %failed\n")
|
||||
if has_ecs() { emit(" call void @rt_init()\n") }
|
||||
i = 0
|
||||
while i < len(g_tests) {
|
||||
let okmsg = emit_str_const(`ok - {g_tests[i].s}`)
|
||||
let failmsg = emit_str_const(`FAIL - {g_tests[i].s}`)
|
||||
emit(" store i32 0, ptr @L_test_fail\n")
|
||||
emit(` call void @fn__test_{itoa(i)}()\n`)
|
||||
let f = emit_bind("load i32, ptr @L_test_fail")
|
||||
let isbad = emit_bind(`icmp ne i32 {f}, 0`)
|
||||
let lpass = lbl("tpass"); let lfail = lbl("tfail"); let ldone = lbl("tdone")
|
||||
emit(` br i1 {isbad}, label %{lfail}, label %{lpass}\n`)
|
||||
emit(`{lpass}:\n`)
|
||||
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr {okmsg})\n`)
|
||||
emit(` br label %{ldone}\n`)
|
||||
emit(`{lfail}:\n`)
|
||||
let cf = emit_bind("load i32, ptr %failed")
|
||||
let cf1 = emit_bind(`add i32 {cf}, 1`)
|
||||
emit(` store i32 {cf1}, ptr %failed\n`)
|
||||
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr {failmsg})\n`)
|
||||
emit(` br label %{ldone}\n`)
|
||||
emit(`{ldone}:\n`)
|
||||
i = i + 1
|
||||
}
|
||||
let totf = emit_bind("load i32, ptr %failed")
|
||||
let passed = emit_bind(`sub i32 {itoa(len(g_tests))}, {totf}`)
|
||||
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_test_sum, i32 {passed}, i32 {totf})\n`)
|
||||
let allok = emit_bind(`icmp eq i32 {totf}, 0`)
|
||||
let rc = emit_bind(`select i1 {allok}, i32 0, i32 1`)
|
||||
emit(` ret i32 {rc}\n}\n`)
|
||||
}
|
||||
|
||||
function emit_program() -> void {
|
||||
head = buf_new()
|
||||
code = buf_new()
|
||||
|
|
@ -71,6 +138,7 @@ function emit_program() -> void {
|
|||
g_uses_intstr = false
|
||||
g_uses_strslice = false
|
||||
g_uses_loopback = false
|
||||
g_uses_expect = false
|
||||
loc_name = new []pointer; loc_reg = new []pointer; loc_ty = new []pointer; loc_mut = new []int
|
||||
brk_lbl = new []pointer; cnt_lbl = new []pointer
|
||||
self_stk = new []pointer
|
||||
|
|
@ -85,7 +153,11 @@ function emit_program() -> void {
|
|||
if has_ecs() { emit_ecs_allocator(); emit_snapshot() }
|
||||
if has_ecs() { emit_net() } # N2/N3: @Sync serializers + @Owned storage (gated internally)
|
||||
if has_ui() { emit_ui_build() }
|
||||
if has_systems() and has_entry() { # N5: game owns its loop via `entry`
|
||||
if len(g_tests) > 0 { # a test file: synth a runner @main
|
||||
if has_systems() { emit_game_defs() }
|
||||
emit_test_runner()
|
||||
}
|
||||
else { if has_systems() and has_entry() { # N5: game owns its loop via `entry`
|
||||
emit_game_defs() # system fns, hooks, tick helpers
|
||||
i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i = i + 1 }
|
||||
|
|
@ -94,7 +166,7 @@ function emit_program() -> void {
|
|||
else {
|
||||
i = 0
|
||||
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i = i + 1 }
|
||||
} }
|
||||
} } }
|
||||
if g_uses_loopback { emit_loopback() } # built-in transport, after all net_send/net_poll uses are seen
|
||||
if g_uses_str { emit_str_prelude() } # @fn_str_eq / @fn_str_concat, after all uses are seen
|
||||
if g_uses_intstr { emit_int_str() } # @fn_int_str, for string(int) in interpolation
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ const S_EMIT: int = 49 # emit E(field: v, ...) — fire event E (calls
|
|||
# s=event name a=E_REC of named args; also usable as an
|
||||
# expression (a cancellable event returns its cancelled flag)
|
||||
const S_CANCEL: int = 50 # cancel — inside a listener, veto a `cancellable` event
|
||||
const N_TEST: int = 51 # test "name" { ... } — a named test block (the testing framework)
|
||||
# s=name a=body block line=source line of the test
|
||||
# statements
|
||||
const S_LET: int = 10
|
||||
const S_ASSIGN: int = 11
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ function p_postfix() -> Node {
|
|||
else { if is_op("[") { pi = pi + 1; let lo = expr()
|
||||
if is_op("..") { pi = pi + 1; let sl = node(E_SLICE); sl.a = e; sl.b = lo; sl.c = expr(); eat_op("]"); e = sl } # s[a..b] substring
|
||||
else { let ix = node(E_INDEX); ix.a = e; ix.b = lo; eat_op("]"); e = ix } }
|
||||
else { if is_op("(") { let c = node(E_CALL); c.a = e; args_call(c); e = c } else { break } } }
|
||||
else { if is_op("(") { let c = node(E_CALL); c.a = e; c.line = toks[pi].line; args_call(c); e = c } else { break } } }
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
|
@ -494,9 +494,25 @@ function parse_one_decl() -> void {
|
|||
if is_id("function") { let f = parse_fn(); if is_export { f.ival = 1 }; push(prog, f); return }
|
||||
if is_id("extern") { push(prog, parse_extern()); return }
|
||||
if is_id("entry") { push(prog, parse_main()); return }
|
||||
if is_id("test") { push(g_tests, parse_test()); return }
|
||||
perr("expected declaration")
|
||||
}
|
||||
|
||||
# test "name" { ... } — a named test block, collected into g_tests. The block
|
||||
# runs under a synthetic runner main (see emit_test_runner); `expect` assertions
|
||||
# inside it record failures. Kept out of `prog` so it never emits as a plain fn.
|
||||
function parse_test() -> Node {
|
||||
let ln = toks[pi].line
|
||||
pi = pi + 1 # past `test`
|
||||
if toks[pi].kind != TK_STR { perr("expected a \"name\" string after test") }
|
||||
let n = node(N_TEST)
|
||||
n.s = toks[pi].text
|
||||
n.line = ln
|
||||
pi = pi + 1 # past the name
|
||||
n.a = block()
|
||||
return n
|
||||
}
|
||||
|
||||
# lex and parse an imported fragment into `prog`, saving/restoring lexer state
|
||||
function do_import(rel: pointer) -> void {
|
||||
let full = path_join(cur_dir, rel)
|
||||
|
|
@ -567,6 +583,7 @@ function parse_program() -> void {
|
|||
g_uses_query = false
|
||||
g_uses_reflect = false
|
||||
g_uses_light = false
|
||||
g_tests = new []Node
|
||||
loaded_paths = new []pointer
|
||||
skipnl()
|
||||
g_game_name = "Ludic"
|
||||
|
|
|
|||
30508
selfhost/ludicc.seed.ll
30508
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -110,6 +110,7 @@ entry {
|
|||
if (src == null) { die("ludicc: cannot open input\n") }
|
||||
|
||||
cur_dir = dir_of(path)
|
||||
g_src_name = base_name(path) # for panic/expect file:line messages
|
||||
lex(src)
|
||||
parse_program()
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ object LudicTokens {
|
|||
object LudicVocabulary {
|
||||
val DECL = setOf(
|
||||
"program", "import", "property", "model", "enum", "ui",
|
||||
"const", "var", "function", "extern", "handler", "entry", "event", "scene"
|
||||
"const", "var", "function", "extern", "handler", "entry", "event", "scene", "test"
|
||||
)
|
||||
val CLAUSE = setOf(
|
||||
"phase", "query", "on", "cancellable", "public", "layer", "start"
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@
|
|||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|function|handler|entry|state)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|function|handler|entry|state|test)\\b" },
|
||||
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|void)\\b" },
|
||||
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
|
||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@
|
|||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
|
||||
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|function|handler|entry|state)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|enum|ui|const|var|let|function|handler|entry|state|test)\\b" },
|
||||
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|void)\\b" },
|
||||
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
|
||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ typedef struct {
|
|||
* declaration keyword, anything stmt() dispatches on is a statement keyword. */
|
||||
static const char* LUDIC_KW_DECL[] = {
|
||||
"program","import","property","model","enum","ui",
|
||||
"const","var","function","extern","handler","entry","event","scene", 0
|
||||
"const","var","function","extern","handler","entry","event","scene","test", 0
|
||||
};
|
||||
static const char* LUDIC_KW_CLAUSE[] = {
|
||||
"phase","query","on","cancellable","public","layer","start", 0
|
||||
|
|
|
|||
|
|
@ -56,6 +56,25 @@ function net_case(path: pointer, exp: pointer) -> void {
|
|||
if (got == exp) { ok(`{path} ({got})`) } else { bad2(path, `got [{got}] want [{exp}]`) }
|
||||
}
|
||||
|
||||
# a test spec (issue #12): `test "name" { expect… }` blocks with no `entry`. The
|
||||
# compiler synthesises a runner @main, so we just build, run, and require a clean
|
||||
# exit (0) plus the expected `== N passed, M failed ==` summary on the last line.
|
||||
function spec_case(path: pointer, exp: pointer) -> void {
|
||||
let nm = flat(path)
|
||||
let ll = `/tmp/x_spec_{nm}.ll`
|
||||
let er = `/tmp/x_spec_{nm}.err`
|
||||
if not shq(`bin/ludicc examples/{path}.ludic > {ll} 2>{er}`) {
|
||||
bad2(path, capture_line(`tail -1 {er}`)); return
|
||||
}
|
||||
if not shq(`{cc()} -O2 {ll} -o /tmp/x_spec_{nm} 2>{er}`) {
|
||||
bad2(path, capture_line(`tail -1 {er}`)); return
|
||||
}
|
||||
let rc = sh(`/tmp/x_spec_{nm} > /tmp/x_spec_{nm}.out 2>&1`)
|
||||
let summary = capture_line(`tail -1 /tmp/x_spec_{nm}.out`)
|
||||
if (rc == 0) and (summary == exp) { ok(`{path} ({summary})`) }
|
||||
else { bad2(path, `rc={string(rc)} last=[{summary}]`) }
|
||||
}
|
||||
|
||||
function cmd_test() -> int {
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
|
@ -109,6 +128,7 @@ function cmd_test() -> int {
|
|||
feat_case("library/reflect", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20", "reflect.ludic (Reflect prop/field enumeration + type + get/set/has/kind — runtime reflection over the world schema)")
|
||||
feat_case("library/render", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "render.ludic (Screen pixel/oval/camera/clip/blend_mode/measure_text + Camera set/follow/shake, verified by pixel readback)")
|
||||
feat_case("library/lighting", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "lighting.ludic (Light ambient/point radial falloff + occluder hard shadows — 2D light accumulation, verified by pixel readback)")
|
||||
spec_case("library/testing", "== 6 passed, 0 failed ==")
|
||||
feat_case("library/logging", "", "0 5 2 1", "logging.ludic (Log levels, set_level/level threshold, structured fields)")
|
||||
# Os known-folders/arch and Fs.list read the BSD utsname/dirent layout, so
|
||||
# their asserted values are macOS-specific; skip off Darwin (see is_darwin).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue