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
|
|
@ -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