restructure: move el compiler content into lang/
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user