Files
2026-05-05 01:38:51 -05:00

321 lines
12 KiB
EmacsLisp

// runtime/test.el El test framework: assertions, registration, and runner.
//
// Provides a minimal but complete test harness for El programs. No external
// dependencies. Written entirely in El using existing runtime primitives.
//
// Quick-start
//
// 1. Write a test function with the standard test signature:
//
// fn test_str_eq_works(_: String) -> String {
// assert_true(str_eq("a", "a"), "same strings are equal")
// assert_false(str_eq("a", "b"), "different strings are not equal")
// return ""
// }
//
// 2. Register it and run:
//
// fn main() -> Void {
// test_case("str_eq works", "test_str_eq_works")
// test_run_all()
// }
//
// Test functions must have the signature (String) -> String. The argument is
// a dummy passed by the threading mechanism (see runtime/thread.el) and should
// be ignored. The return value is likewise ignored results flow through the
// state-based assertion primitives.
//
// State keys
//
// _test_cases JSON array of {"name":"...","fn":"..."}
// _test_pass_count Int as string total passing assertions
// _test_fail_count Int as string total failing assertions
// _test_failures JSON array of failure message strings
// _test_current Name of the test case currently executing
//
// Dependencies
//
// runtime/string.el str_eq, str_concat, str_contains, int_to_str, ...
// runtime/state.el state_set, state_get
// runtime/json.el json_array_len, json_array_get_string, json_get,
// json_escape_string
// runtime/thread.el spawn, join (for dynamic dispatch via dlsym)
// Internal: JSON array helpers
//
// _test_json_append append a quoted, escaped string element to a JSON array.
//
// Given an existing JSON array string (e.g. '["a","b"]') and a plain string
// value, returns a new array with the value appended (e.g. '["a","b","c"]').
//
// The array must be non-empty always init with "[]" before calling.
fn _test_json_append(arr: String, val: String) -> String {
let escaped: String = json_escape_string(val)
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
if str_eq(inner, "") {
return "[\"" + escaped + "\"]"
}
return "[" + inner + ",\"" + escaped + "\"]"
}
// _test_json_obj_append append a raw JSON object string to a JSON array.
//
// Used to build the _test_cases list where each element is already a
// JSON object (not a plain string).
fn _test_json_obj_append(arr: String, obj: String) -> String {
let inner: String = str_slice(arr, 1, str_len(arr) - 1)
if str_eq(inner, "") {
return "[" + obj + "]"
}
return "[" + inner + "," + obj + "]"
}
// Test registration
// test_case register a named test case.
//
// name: Human-readable test case name (shown in output).
// fn_name: Name of a top-level El function with signature
// (String) -> String. The function should call assertion
// primitives from this module. The String arg it receives is ""
// and its return value is ignored.
//
// Test cases are stored in the _test_cases state key and executed in
// registration order by test_run_all().
fn test_case(name: String, fn_name: String) {
let arr: String = state_get("_test_cases")
if str_eq(arr, "") { arr = "[]" }
let escaped_name: String = json_escape_string(name)
let escaped_fn: String = json_escape_string(fn_name)
let obj: String = "{\"name\":\"" + escaped_name + "\",\"fn\":\"" + escaped_fn + "\"}"
let arr = _test_json_obj_append(arr, obj)
state_set("_test_cases", arr)
}
// Test state helpers
// _test_init reset all test counters and failure lists.
//
// Called at the start of test_run_all(). Safe to call multiple times.
fn _test_init() {
state_set("_test_pass_count", "0")
state_set("_test_fail_count", "0")
state_set("_test_failures", "[]")
state_set("_test_current", "")
}
// _test_inc_pass increment the global pass counter by 1.
fn _test_inc_pass() {
let n: Int = str_to_int(state_get("_test_pass_count"))
state_set("_test_pass_count", int_to_str(n + 1))
}
// _test_inc_fail increment the global fail counter by 1.
fn _test_inc_fail() {
let n: Int = str_to_int(state_get("_test_fail_count"))
state_set("_test_fail_count", int_to_str(n + 1))
}
// test_pass record a passing assertion for the current test case.
//
// Increments the pass counter. Called internally by assertions.
fn test_pass(name: String) {
_test_inc_pass()
}
// test_fail record a failing assertion for the current test case.
//
// name: assertion label or description (usually the msg parameter)
// msg: detailed failure message including expected/got values
//
// Increments the fail counter and appends the message to _test_failures.
// Also prints the failure immediately for visibility.
fn test_fail(name: String, msg: String) {
_test_inc_fail()
let failures: String = state_get("_test_failures")
if str_eq(failures, "") { failures = "[]" }
let entry: String = " " + msg
let failures = _test_json_append(failures, entry)
state_set("_test_failures", failures)
println(" FAIL: " + msg)
}
// Assertions
// assert_true assert that condition is true.
//
// condition: the boolean value to test
// msg: description shown on failure
fn assert_true(condition: Bool, msg: String) {
if condition {
test_pass(msg)
return
}
let test_name: String = state_get("_test_current")
test_fail(msg, "[" + test_name + "] " + msg + ": expected true, got false")
}
// assert_false assert that condition is false.
//
// condition: the boolean value to test
// msg: description shown on failure
fn assert_false(condition: Bool, msg: String) {
if !condition {
test_pass(msg)
return
}
let test_name: String = state_get("_test_current")
test_fail(msg, "[" + test_name + "] " + msg + ": expected false, got true")
}
// assert_eq assert that two strings are equal.
//
// a, b: strings to compare
// msg: description shown on failure (quoted values appended automatically)
fn assert_eq(a: String, b: String, msg: String) {
if str_eq(a, b) {
test_pass(msg)
return
}
let test_name: String = state_get("_test_current")
test_fail(msg, "[" + test_name + "] " + msg + ": expected \"" + b + "\", got \"" + a + "\"")
}
// assert_int_eq assert that two integers are equal.
//
// a, b: integers to compare
// msg: description shown on failure
fn assert_int_eq(a: Int, b: Int, msg: String) {
if a == b {
test_pass(msg)
return
}
let test_name: String = state_get("_test_current")
test_fail(msg, "[" + test_name + "] " + msg + ": expected " + int_to_str(b) + ", got " + int_to_str(a))
}
// assert_neq assert that two strings are NOT equal.
//
// a, b: strings to compare
// msg: description shown on failure
fn assert_neq(a: String, b: String, msg: String) {
if !str_eq(a, b) {
test_pass(msg)
return
}
let test_name: String = state_get("_test_current")
test_fail(msg, "[" + test_name + "] " + msg + ": expected values to differ, but both are \"" + a + "\"")
}
// assert_contains assert that string s contains substring sub.
//
// s: haystack string
// sub: needle substring
// msg: description shown on failure
fn assert_contains(s: String, sub: String, msg: String) {
if str_contains(s, sub) {
test_pass(msg)
return
}
let test_name: String = state_get("_test_current")
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not contain \"" + sub + "\"")
}
// assert_starts_with assert that string s starts with prefix.
//
// s: string to inspect
// prefix: expected prefix
// msg: description shown on failure
fn assert_starts_with(s: String, prefix: String, msg: String) {
if str_starts_with(s, prefix) {
test_pass(msg)
return
}
let test_name: String = state_get("_test_current")
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not start with \"" + prefix + "\"")
}
// assert_ends_with assert that string s ends with suffix.
//
// s: string to inspect
// suffix: expected suffix
// msg: description shown on failure
fn assert_ends_with(s: String, suffix: String, msg: String) {
if str_ends_with(s, suffix) {
test_pass(msg)
return
}
let test_name: String = state_get("_test_current")
test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not end with \"" + suffix + "\"")
}
// fail unconditional test failure.
//
// msg: failure message shown in output
//
// Use when a code path that must not be reached is reached, or when an
// expected exception did not occur.
fn fail(msg: String) {
let test_name: String = state_get("_test_current")
test_fail(msg, "[" + test_name + "] " + msg)
}
// Runner
// _test_run_one execute a single registered test case by name.
//
// name: the human-readable test case name (set as _test_current)
// fn_name: the El function to invoke via the thread mechanism
//
// Sets _test_current so that assertions inside the test function know which
// test they belong to. Spawns and immediately joins the test function in a
// child thread (same dlsym mechanism as parallel_map) so dynamic dispatch
// works without needing closures.
fn _test_run_one(name: String, fn_name: String) {
state_set("_test_current", name)
let before_fail: Int = str_to_int(state_get("_test_fail_count"))
let tid: Int = __thread_create(fn_name, "")
__thread_join(tid)
let after_fail: Int = str_to_int(state_get("_test_fail_count"))
if after_fail == before_fail {
println("[test] " + name + " ... PASS")
} else {
println("[test] " + name + " ... FAIL")
}
}
// test_run_all execute all registered test cases and print a summary.
//
// Iterates through every test case registered via test_case(), runs each one,
// prints per-test PASS/FAIL status, then prints a summary line.
//
// Returns the total number of failing assertions. Exit with this value to
// signal CI failure:
//
// fn main() -> Int {
// test_case("str_eq", "test_str_eq")
// return test_run_all()
// }
fn test_run_all() -> Int {
_test_init()
let cases: String = state_get("_test_cases")
if str_eq(cases, "") { cases = "[]" }
let n: Int = json_array_len(cases)
let i: Int = 0
while i < n {
let entry: String = json_array_get(cases, i)
let name: String = json_get(entry, "name")
let fn_name: String = json_get(entry, "fn")
_test_run_one(name, fn_name)
i = i + 1
}
let pass_count: Int = str_to_int(state_get("_test_pass_count"))
let fail_count: Int = str_to_int(state_get("_test_fail_count"))
println("[test] Summary: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")
return fail_count
}