#!/usr/bin/env bash # run-el-test.sh — build and RUN one engine test (tests/*.el), printing its assertions. # # WHY THIS EXISTS (2026-08-06): the engine's tests/*.el files were never runnable from the # tree. `elc` is a COMPILER — it emits C to stdout and exits; it does not execute anything. # So "the tests" could only ever be read, not run, and a signature change could silently # break them (exactly what happened when bridge_save gained its `wire` argument). This # script closes that: emit the test to C, link it against the engine modules, execute it. # # HOW IT WORKS # 1. elc .el -> C on stdout (the test file's `main` + prototypes) # 2. elb (once, cached) -> per-module C for the whole engine into a scratch dir # 3. cc test.c + all modules EXCEPT soul.c (soul.c owns the real `main`) + the runtime # 4. run it # # The test C references only the engine functions it actually calls, so there are no # duplicate-symbol collisions with the module objects. # # RUNTIME: the REPO-PINNED vendor/el-runtime (NOT ~/el-sdk/el_runtime.c — that June build # is missing builtins August code calls: engram_wm_count, engram_wm_top_json, # http_delete_json, http_serve_async; linking against it fails with "symbol(s) not found"). # # USAGE # tests/run-el-test.sh tests/test_bridge_serialization.el # one test # tests/run-el-test.sh --all # every tests/test_*.el # REBUILD=1 tests/run-el-test.sh ... # force module regeneration # # Tests that need a live API key / running soul (see each file's header) will report their # own skips or failures — this runner does not fake them. set -uo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" || exit 2 ELC="${ELC:-$HOME/el-sdk/elc}" ELB="${ELB:-$HOME/Development/el-sdk/bin/elb}" RUNTIME_DIR="${RUNTIME_DIR:-$REPO_ROOT/vendor/el-runtime/v1.0.0-20260501}" SCRATCH="${SCRATCH:-/tmp/el-test-$(basename "$REPO_ROOT")}" MODDIR="$SCRATCH/modules" OPENSSL_INC="${OPENSSL_INC:-/opt/homebrew/opt/openssl@3/include}" OPENSSL_LIB="${OPENSSL_LIB:-/opt/homebrew/opt/openssl@3/lib}" for req in "$ELC" "$ELB" "$RUNTIME_DIR/el_runtime.c"; do [ -e "$req" ] || { echo "run-el-test: missing required input: $req" >&2; exit 2; } done mkdir -p "$MODDIR" || exit 2 # ── Step 1: engine modules (cached — regeneration is the slow part) ──────────── if [ "${REBUILD:-0}" = "1" ] || [ ! -f "$MODDIR/chat.c" ] || [ "chat.el" -nt "$MODDIR/chat.c" ]; then echo "run-el-test: generating engine modules into $MODDIR (this takes ~1-2 min)..." # elb's own final link step fails by design here (it wants to produce a binary named # `neuron` and we only need the per-module .c files it emits first). Ignore its rc. "$ELB" --elc="$ELC" --runtime="$RUNTIME_DIR" --out="$MODDIR/" >"$SCRATCH/elb.log" 2>&1 if [ ! -f "$MODDIR/chat.c" ]; then echo "run-el-test: FATAL — elb produced no chat.c; see $SCRATCH/elb.log" >&2 tail -5 "$SCRATCH/elb.log" >&2 exit 2 fi # elb rewrites *.elh in the source tree as a side effect (cosmetic banner churn plus a # stray soul..elh). Say so; the caller decides whether to `git restore` them. echo "run-el-test: NOTE — elb regenerated *.elh in the source tree (cosmetic churn is expected; a stray soul..elh may appear)." fi # soul.c is needed for its engine functions (layered_cycle et al.) but it also owns the # daemon's real `main`, which would collide with the test's own. Compile it ONCE to an # object with `main` renamed away, and link that instead of the .c. SOUL_OBJ="$SCRATCH/soul-nomain.o" if [ "${REBUILD:-0}" = "1" ] || [ ! -f "$SOUL_OBJ" ] || [ "$MODDIR/soul.c" -nt "$SOUL_OBJ" ]; then cc -std=c11 -O1 -DHAVE_CURL -Dmain=el_soul_daemon_main_unused \ -I "$RUNTIME_DIR" -I "$MODDIR" -I "$OPENSSL_INC" \ -include dist/elp-c-decls.h -Wno-error=implicit-function-declaration \ -c "$MODDIR/soul.c" -o "$SOUL_OBJ" 2>"$SCRATCH/soul-nomain.err" \ || { echo "run-el-test: FATAL — could not compile soul.c without main" >&2 grep -E 'error:' "$SCRATCH/soul-nomain.err" | head -5 >&2; exit 2; } fi # Every module except soul.c (linked as the renamed object above) and the stray soul.elh.c. MODS=("$SOUL_OBJ") for f in "$MODDIR"/*.c; do case "$(basename "$f")" in soul.c|soul.elh.c) continue ;; esac MODS+=("$f") done [ "${#MODS[@]}" -gt 1 ] || { echo "run-el-test: no module objects found" >&2; exit 2; } run_one() { local test_el="$1" local name; name="$(basename "$test_el" .el)" local cfile="$SCRATCH/$name.c" local bin="$SCRATCH/$name" printf '\n══ %s ══\n' "$name" if ! "$ELC" "$test_el" >"$cfile" 2>"$SCRATCH/$name.elc.err"; then echo "COMPILE FAILED (elc):"; tail -10 "$SCRATCH/$name.elc.err"; return 1 fi [ -s "$cfile" ] || { echo "COMPILE FAILED (elc produced empty C)"; return 1; } if ! cc -std=c11 -O1 -DHAVE_CURL -rdynamic \ -I "$RUNTIME_DIR" -I "$MODDIR" -I "$OPENSSL_INC" -L "$OPENSSL_LIB" \ -include dist/elp-c-decls.h -Wno-error=implicit-function-declaration \ -o "$bin" "$cfile" "${MODS[@]}" "$RUNTIME_DIR/el_runtime.c" \ -lssl -lcrypto -lcurl -lpthread -lm 2>"$SCRATCH/$name.link.err"; then echo "LINK FAILED:"; grep -E '"_|error:' "$SCRATCH/$name.link.err" | head -10; return 1 fi # THE RUNNER OWNS THE VERDICT — the test files cannot be trusted to report it. # # Every tests/*.el assert helper does `let pass_count = pass_count + 1` INSIDE an if # BLOCK. El's scope rule (the same one chat.el documents at every while-body mutation: # "mutations inside if *blocks* don't escape scope") means those counters never # increment, so all 9 counted test files print "N passed, M failed" as "0 passed, 0 # failed" — forever, whatever actually happened. A summary that can never report a # failure is worth exactly as much as an assertion that can never fail. Logged as a # bug for the real in-file fix; until then the verdict is computed HERE, from the # assert helpers' own per-line output, which IS reliable. local out="$SCRATCH/$name.out" "$bin" 2>&1 | tee "$out"; local rc=${PIPESTATUS[0]} # NOTE: `grep -c` prints 0 AND exits 1 when there are no matches, so a `|| echo 0` # fallback appends a SECOND zero and every later integer test breaks on "0\n0". # (Caught by running this script — which is the whole argument for running things.) local n_pass n_fail n_pass=$(grep -c '^ PASS: ' "$out" 2>/dev/null); n_pass=${n_pass:-0} n_fail=$(grep -c '^ FAIL: ' "$out" 2>/dev/null); n_fail=${n_fail:-0} echo "── $name: $n_pass passed, $n_fail failed (counted by the runner, not by the file's dead counters)" if [ "$n_fail" -gt 0 ]; then echo " failing assertions:"; grep '^ FAIL: ' "$out" | sed 's/^/ /' return 1 fi if [ "$n_pass" -eq 0 ]; then echo " WARNING: no assertions ran — treating as FAILURE (a test that asserts nothing is not a passing test)" return 1 fi [ $rc -eq 0 ] || { echo " (test binary exited rc=$rc)"; return 1; } return 0 } rc_all=0 if [ "${1:-}" = "--all" ]; then for t in tests/test_*.el; do run_one "$t" || rc_all=1; done else [ $# -ge 1 ] || { echo "usage: tests/run-el-test.sh | --all" >&2; exit 2; } for t in "$@"; do run_one "$t" || rc_all=1; done fi exit $rc_all