// runtime/eltest.el — El test framework runner (Phase 1). // // This is the RUNNER. It is written in El and consumes a registry that the // compiler generates into the same translation unit when invoked as // `elc --test`. Nothing here discovers tests; discovery already happened at // compile time, which is what makes `--list` and filtering possible later. // // ── Architecture ───────────────────────────────────────────────────────────── // // The compiler lowers each `test "name" { ... }` block into a static C // function and emits a static table of (name, fn) pairs plus a small set of // index-based accessors. El has no function pointers, so the runner never // sees one — it works entirely in indices: // // __el_reg_count() -> Int number of registered tests // __el_reg_name(i) -> String test name at index i // __el_reg_invoke(i) -> Int run test i, return its failure count // __el_reg_last_ns() -> Int wall-clock ns of the last invoke // __el_reg_msg() -> String first failure message of the last invoke // __el_reg_asserts() -> Int assertions executed in the last invoke // __el_opt_json() -> Int 1 if --json was passed // // Timing is taken in the generated C, immediately around the call, so no El // call overhead lands inside the measurement. // // ── Output ─────────────────────────────────────────────────────────────────── // // Structured events are the source of truth. The human renderer is written // FROM the same fields the NDJSON renderer emits — never the reverse. Parsing // human output back into structure is the one clear architectural mistake in // Go's test tooling and we do not repeat it. // // Every result carries a duration. Always. A framework that cannot report how // long its tests took cannot surface a performance regression, and a // regression nobody can see is one nobody fixes. // ── Small helpers (no imports — this file must stay self-contained) ────────── // _elt_json_escape — minimal JSON string escaping for the NDJSON renderer. fn _elt_json_escape(s: String) -> String { let out: String = "" let n: Int = str_len(s) let i: Int = 0 while i < n { let ch: String = str_slice(s, i, i + 1) if str_eq(ch, "\"") { let out = out + "\\\"" } else { if str_eq(ch, "\\") { let out = out + "\\\\" } else { if str_eq(ch, "\n") { let out = out + "\\n" } else { if str_eq(ch, "\t") { let out = out + "\\t" } else { if str_eq(ch, "\r") { let out = out + "\\r" } else { let out = out + ch } } } } } let i = i + 1 } return out } // _elt_pad3 — left-pad an integer to three digits (for the ms.fraction form). fn _elt_pad3(v: Int) -> String { if v < 10 { return "00" + int_to_str(v) } if v < 100 { return "0" + int_to_str(v) } return int_to_str(v) } // _elt_ms — render a nanosecond duration as "M.mmm" milliseconds. // // Deliberately avoids the modulo operator: the remainder is derived by // subtraction so this stays portable across El backends. fn _elt_ms(ns: Int) -> String { let total_us: Int = ns / 1000 let ms_whole: Int = total_us / 1000 let us_rem: Int = total_us - (ms_whole * 1000) return int_to_str(ms_whole) + "." + _elt_pad3(us_rem) } // _elt_secs — render a nanosecond duration as fractional seconds, for the // NDJSON `elapsed` field. JUnit XML and test2json both use seconds-as-decimal. fn _elt_secs(ns: Int) -> String { let total_ms: Int = ns / 1000000 let s_whole: Int = total_ms / 1000 let ms_rem: Int = total_ms - (s_whole * 1000) return int_to_str(s_whole) + "." + _elt_pad3(ms_rem) } // ── Event emission ─────────────────────────────────────────────────────────── // // One function per event shape. Both renderers read the same fields; the // human renderer is a projection of the event, not a separate code path. fn _elt_emit_run(json_mode: Bool, name: String) { if json_mode { println("{\"action\":\"run\",\"test\":\"" + _elt_json_escape(name) + "\"}") } } fn _elt_emit_result(json_mode: Bool, name: String, fails: Int, ns: Int, asserts: Int, msg: String) { if json_mode { let action: String = "pass" if fails > 0 { let action = "fail" } let line: String = "{\"action\":\"" + action + "\"" let line = line + ",\"test\":\"" + _elt_json_escape(name) + "\"" let line = line + ",\"elapsed\":" + _elt_secs(ns) let line = line + ",\"assertions\":" + int_to_str(asserts) if fails > 0 { let line = line + ",\"failures\":" + int_to_str(fails) let line = line + ",\"message\":\"" + _elt_json_escape(msg) + "\"" } let line = line + "}" println(line) return } // Human renderer — duration is never optional. if fails > 0 { println("FAIL " + name + " (" + _elt_ms(ns) + "ms)") println(" " + msg) return } println("ok " + name + " (" + _elt_ms(ns) + "ms)") return } fn _elt_emit_summary(json_mode: Bool, total: Int, failed: Int, ns: Int, asserts: Int) { let passed: Int = total - failed if json_mode { let line: String = "{\"action\":\"summary\"" let line = line + ",\"tests\":" + int_to_str(total) let line = line + ",\"passed\":" + int_to_str(passed) let line = line + ",\"failed\":" + int_to_str(failed) let line = line + ",\"assertions\":" + int_to_str(asserts) let line = line + ",\"elapsed\":" + _elt_secs(ns) let line = line + "}" println(line) return } println("") println(int_to_str(total) + " tests, " + int_to_str(passed) + " passed, " + int_to_str(failed) + " failed, " + int_to_str(asserts) + " assertions in " + _elt_ms(ns) + "ms") return } // ── The runner ─────────────────────────────────────────────────────────────── // el_test_main — drive the compile-time registry. // // Called from the generated main(). Returns the number of FAILING TESTS, which // becomes the process exit code. Note that this counts tests, not assertions: // a test is the unit of result. The old harness counted assertions globally and // therefore could not say which test failed, how long any of them took, or // whether a test had run at all. fn el_test_main() -> Int { let json_mode: Bool = false if __el_opt_json() == 1 { let json_mode = true } let n: Int = __el_reg_count() let i: Int = 0 let failed: Int = 0 let total_ns: Int = 0 let total_asserts: Int = 0 while i < n { let name: String = __el_reg_name(i) _elt_emit_run(json_mode, name) let fails: Int = __el_reg_invoke(i) let ns: Int = __el_reg_last_ns() let asserts: Int = __el_reg_asserts() let msg: String = __el_reg_msg() let total_ns = total_ns + ns let total_asserts = total_asserts + asserts if fails > 0 { let failed = failed + 1 } _elt_emit_result(json_mode, name, fails, ns, asserts, msg) let i = i + 1 } _elt_emit_summary(json_mode, n, failed, total_ns, total_asserts) return failed }