test framework phase 1: compile-time registry + El-side runner

Replace the hardcoded test harness main() with a generated static registry
and index-based accessors, and move all reporting into runtime/eltest.el.

The old harness inlined direct calls into main() and counted assertions in
two globals. That shape cannot report which test failed, how long any test
took, or whether a test ran at all -- a misspelled registration reported
success for a test that never executed.

- assertions record into per-test state instead of global counters
- registry table emitted at compile time; discovery strictly precedes
  execution, which is what later enables --list, filtering and sharding
- per-test wall timing on CLOCK_MONOTONIC, taken in C around the call
- runner in El: structured NDJSON events as source of truth, human output
  rendered from the same fields
This commit is contained in:
Neuron
2026-08-15 21:24:03 -05:00
parent 2240d26c32
commit 24fac765a6
2 changed files with 276 additions and 14 deletions
+84 -14
View File
@@ -1705,9 +1705,13 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
} else {
let c_msg = "EL_STR_PTR(" + cg_expr(msg_node) + ")"
}
// Assertions record into PER-TEST state, not global counters. The test
// is the unit of result; a global pass/fail tally cannot say which test
// failed or whether a test ran at all. Reporting is the runner's job
// nothing is printed here.
emit_line(indent + "if (!(" + c_cond + ")) {")
emit_line(indent + " __el_test_fail(__el_cur_test, " + c_msg + "); __el_fail++;")
emit_line(indent + "} else { __el_pass++; }")
emit_line(indent + " __el_test_fail(" + c_msg + ");")
emit_line(indent + "} else { __el_cur_asserts++; }")
return declared
}
@@ -4110,11 +4114,22 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
// Emit test harness preamble (counters, fail printer) when in test mode.
if test_is_mode {
emit_line("#include <stdio.h>")
emit_line("#include <string.h>")
emit_line("#include <time.h>")
emit_blank()
emit_line("static int __el_pass = 0, __el_fail = 0;")
// Per-test result state. Reset by __el_reg_invoke before each test, so
// every test gets its own record rather than contributing to a global
// tally. The first failure message is retained; later ones only bump
// the count, which keeps the common case allocation-free.
emit_line("static int __el_cur_fails = 0;")
emit_line("static int __el_cur_asserts = 0;")
emit_line("static char __el_cur_msg[512] = \"\";")
emit_line("static const char *__el_cur_test = \"(none)\";")
emit_line("static void __el_test_fail(const char *test, const char *msg) {")
emit_line(" fprintf(stderr, \"FAIL %-40s %s\\n\", test, msg);")
emit_line("static void __el_test_fail(const char *msg) {")
emit_line(" if (__el_cur_fails == 0 && msg) {")
emit_line(" snprintf(__el_cur_msg, sizeof __el_cur_msg, \"%s\", msg);")
emit_line(" }")
emit_line(" __el_cur_fails++; __el_cur_asserts++;")
emit_line("}")
emit_blank()
}
@@ -4312,17 +4327,72 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
el_release(sigs)
let test_arena_mark: Any = el_arena_push()
let tn: Int = native_list_len(test_c_names)
// Generated test registry
// Discovery happens HERE, at compile time. The runner never searches
// for tests; it walks this table. That ordering — discovery strictly
// before execution is what makes --list, filtering, sharding and
// per-test reporting possible later, and it is why the old harness
// (which inlined direct calls into main) could not have any of them.
emit_line("typedef void (*__el_test_fp)(void);")
emit_line("typedef struct { const char *name; __el_test_fp fn; } __el_test_entry;")
emit_line("static const __el_test_entry __el_registry[] = {")
let ri: Int = 0
while ri < tn {
let r_name: String = native_list_get(test_names, ri)
let r_cfn: String = native_list_get(test_c_names, ri)
emit_line(" { \"" + c_escape(r_name) + "\", " + r_cfn + " },")
let ri = ri + 1
}
// Trailing sentinel keeps the array non-empty when a file declares no
// tests (a zero-length array is not valid C).
emit_line(" { 0, 0 }")
emit_line("};")
emit_line("static const int __el_registry_n = " + int_to_str(tn) + ";")
emit_blank()
emit_line("static long long __el_last_ns = 0;")
emit_line("static int __el_opt_json_v = 0;")
emit_blank()
// Index-based accessors
// El has no function pointers, so the runner works purely in indices.
// This is the whole seam between generated C and the El-side runner.
emit_line("el_val_t __el_reg_count(void) { return (el_val_t)(int64_t)__el_registry_n; }")
emit_line("el_val_t __el_reg_name(el_val_t i) {")
emit_line(" int64_t k = (int64_t)i;")
emit_line(" if (k < 0 || k >= __el_registry_n) return EL_STR(\"\");")
emit_line(" return EL_STR(__el_registry[k].name);")
emit_line("}")
// Timing is taken immediately around the call, in C, on the MONOTONIC
// clock never the wall clock, which can step backwards under NTP.
emit_line("el_val_t __el_reg_invoke(el_val_t i) {")
emit_line(" int64_t k = (int64_t)i;")
emit_line(" if (k < 0 || k >= __el_registry_n) return 0;")
emit_line(" __el_cur_fails = 0; __el_cur_asserts = 0; __el_cur_msg[0] = '\\0';")
emit_line(" __el_cur_test = __el_registry[k].name;")
emit_line(" struct timespec _t0, _t1;")
emit_line(" clock_gettime(CLOCK_MONOTONIC, &_t0);")
emit_line(" __el_registry[k].fn();")
emit_line(" clock_gettime(CLOCK_MONOTONIC, &_t1);")
emit_line(" __el_last_ns = (long long)(_t1.tv_sec - _t0.tv_sec) * 1000000000LL")
emit_line(" + (long long)(_t1.tv_nsec - _t0.tv_nsec);")
emit_line(" return (el_val_t)(int64_t)__el_cur_fails;")
emit_line("}")
emit_line("el_val_t __el_reg_last_ns(void) { return (el_val_t)(int64_t)__el_last_ns; }")
emit_line("el_val_t __el_reg_msg(void) { return EL_STR(__el_cur_msg); }")
emit_line("el_val_t __el_reg_asserts(void) { return (el_val_t)(int64_t)__el_cur_asserts; }")
emit_line("el_val_t __el_opt_json(void) { return (el_val_t)(int64_t)__el_opt_json_v; }")
emit_blank()
// main() delegates to the El-side runner. Everything above this line is
// generated glue; all reporting logic lives in runtime/eltest.el.
emit_line("int main(int _argc, char **_argv) {")
emit_line(" el_runtime_init_args(_argc, _argv);")
let ti: Int = 0
let tn: Int = native_list_len(test_c_names)
while ti < tn {
let tc_name: String = native_list_get(test_c_names, ti)
emit_line(" " + tc_name + "();")
let ti = ti + 1
}
emit_line(" printf(\"%d passed, %d failed\\n\", __el_pass, __el_fail);")
emit_line(" return __el_fail;")
emit_line(" for (int _i = 1; _i < _argc; _i++) {")
emit_line(" if (strcmp(_argv[_i], \"--json\") == 0) __el_opt_json_v = 1;")
emit_line(" }")
emit_line(" return (int)(int64_t)el_test_main();")
emit_line("}")
el_arena_pop(test_arena_mark)
el_release(test_names)