Merge branch 'worktree-iso-phase-4' into macros
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m49s

This commit is contained in:
2026-03-07 12:37:34 +00:00
15 changed files with 2180 additions and 269 deletions

View File

@@ -1,5 +1,5 @@
// sx-test-runner.js — Run test.sx in the browser using sx-browser.js. // sx-test-runner.js — Run SX test specs in the browser using sx-browser.js.
// Loaded on the /specs/testing page. Uses the Sx global. // Supports both legacy (monolithic test.sx) and modular (per-spec) modes.
(function() { (function() {
var NIL = Sx.NIL; var NIL = Sx.NIL;
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
@@ -21,13 +21,9 @@
return false; return false;
} }
window.sxRunTests = function(srcId, outId, btnId) { // --- Platform functions shared across all specs ---
var src = document.getElementById(srcId).textContent; function makeEnv() {
var out = document.getElementById(outId);
var btn = document.getElementById(btnId);
var stack = [], passed = 0, failed = 0, num = 0, lines = []; var stack = [], passed = 0, failed = 0, num = 0, lines = [];
var env = { var env = {
"try-call": function(thunk) { "try-call": function(thunk) {
try { try {
@@ -49,6 +45,7 @@
"push-suite": function(name) { stack.push(name); }, "push-suite": function(name) { stack.push(name); },
"pop-suite": function() { stack.pop(); }, "pop-suite": function() { stack.pop(); },
// Primitives that sx-browser.js may not expose in env
"equal?": function(a, b) { return deepEqual(a, b); }, "equal?": function(a, b) { return deepEqual(a, b); },
"eq?": function(a, b) { return a === b; }, "eq?": function(a, b) { return a === b; },
"boolean?": function(x) { return typeof x === "boolean"; }, "boolean?": function(x) { return typeof x === "boolean"; },
@@ -68,28 +65,159 @@
}, },
"has-key?": function(d, k) { return d && typeof d === "object" && k in d; }, "has-key?": function(d, k) { return d && typeof d === "object" && k in d; },
"append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); }, "append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); },
"for-each-indexed": function(f, coll) {
for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, i, coll[i]], env);
},
"for-each": function(f, coll) {
for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, coll[i]], env);
},
"dict-set!": function(d, k, v) { if (d) d[k] = v; },
"dict-has?": function(d, k) { return d && typeof d === "object" && k in d; },
"dict-get": function(d, k) { return d ? d[k] : undefined; },
"starts-with?": function(s, prefix) { return String(s).indexOf(prefix) === 0; },
"ends-with?": function(s, suffix) { var str = String(s); return str.indexOf(suffix) === str.length - suffix.length; },
"slice": function(s, start, end) { return end !== undefined ? s.slice(start, end) : s.slice(start); },
"inc": function(n) { return n + 1; },
"append!": function(arr, item) { if (Array.isArray(arr)) arr.push(item); },
"dict": function() { return {}; },
// --- Parser platform functions ---
"sx-parse": function(source) { return Sx.parseAll(source); },
"sx-serialize": function(val) {
if (val === NIL || val === null || val === undefined) return "nil";
if (typeof val === "boolean") return val ? "true" : "false";
if (typeof val === "number") return String(val);
if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
if (val && (val._sym || val._sx_symbol)) return val.name;
if (val && (val._kw || val._sx_keyword)) return ":" + val.name;
if (Array.isArray(val)) return "(" + val.map(function(x) { return env["sx-serialize"](x); }).join(" ") + ")";
if (val && typeof val === "object") {
var parts = [];
Object.keys(val).forEach(function(k) { parts.push(":" + k); parts.push(env["sx-serialize"](val[k])); });
return "{" + parts.join(" ") + "}";
}
return String(val);
},
"make-symbol": function(name) { return Sx.sym ? Sx.sym(name) : { _sx_symbol: true, name: name, toString: function() { return name; } }; },
"make-keyword": function(name) { return Sx.kw ? Sx.kw(name) : { _sx_keyword: true, name: name, toString: function() { return name; } }; },
"symbol-name": function(s) { return s && s.name ? s.name : String(s); },
"keyword-name": function(k) { return k && k.name ? k.name : String(k); },
// --- Render platform function ---
"render-html": function(sxSource) {
if (!Sx.renderToHtml) throw new Error("render-to-html not available");
var exprs = Sx.parseAll(sxSource);
var result = "";
for (var i = 0; i < exprs.length; i++) result += Sx.renderToHtml(exprs[i], env);
return result;
},
}; };
return { env: env, getResults: function() { return { passed: passed, failed: failed, num: num, lines: lines }; } };
}
function evalSource(src, env) {
var exprs = Sx.parseAll(src);
for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);
}
function loadRouterFromBootstrap(env) {
if (Sx.splitPathSegments) {
env["split-path-segments"] = Sx.splitPathSegments;
env["parse-route-pattern"] = Sx.parseRoutePattern;
env["match-route-segments"] = Sx.matchRouteSegments;
env["match-route"] = Sx.matchRoute;
env["find-matching-route"] = Sx.findMatchingRoute;
env["make-route-segment"] = Sx.makeRouteSegment;
}
}
// --- Legacy runner (monolithic test.sx) ---
window.sxRunTests = function(srcId, outId, btnId) {
var src = document.getElementById(srcId).textContent;
var out = document.getElementById(outId);
var btn = document.getElementById(btnId);
var ctx = makeEnv();
try { try {
var t0 = performance.now(); var t0 = performance.now();
var exprs = Sx.parseAll(src); evalSource(src, ctx.env);
for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);
var elapsed = Math.round(performance.now() - t0); var elapsed = Math.round(performance.now() - t0);
lines.push(""); var r = ctx.getResults();
lines.push("1.." + num); r.lines.push("");
lines.push("# tests " + (passed + failed)); r.lines.push("1.." + r.num);
lines.push("# pass " + passed); r.lines.push("# tests " + (r.passed + r.failed));
if (failed > 0) lines.push("# fail " + failed); r.lines.push("# pass " + r.passed);
lines.push("# time " + elapsed + "ms"); if (r.failed > 0) r.lines.push("# fail " + r.failed);
r.lines.push("# time " + elapsed + "ms");
} catch(e) { } catch(e) {
lines.push(""); var r = ctx.getResults();
lines.push("FATAL: " + (e.message || String(e))); r.lines.push("");
r.lines.push("FATAL: " + (e.message || String(e)));
} }
out.textContent = lines.join("\n"); out.textContent = r.lines.join("\n");
out.style.display = "block"; out.style.display = "block";
btn.textContent = passed + "/" + (passed + failed) + " passed" + (failed === 0 ? "" : " (" + failed + " failed)"); btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)");
btn.className = failed > 0 btn.className = r.failed > 0
? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default"
: "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default";
};
// --- Modular runner (per-spec or all) ---
var SPECS = {
"eval": { needs: [] },
"parser": { needs: ["sx-parse"] },
"router": { needs: [] },
"render": { needs: ["render-html"] },
};
window.sxRunModularTests = function(specName, outId, btnId) {
var out = document.getElementById(outId);
var btn = document.getElementById(btnId);
var ctx = makeEnv();
var specs = specName === "all" ? Object.keys(SPECS) : [specName];
try {
var t0 = performance.now();
// Load framework
var fwEl = document.getElementById("test-framework-source");
if (fwEl) {
evalSource(fwEl.textContent, ctx.env);
}
for (var si = 0; si < specs.length; si++) {
var sn = specs[si];
if (!SPECS[sn]) continue;
// Load router from bootstrap if needed
if (sn === "router") loadRouterFromBootstrap(ctx.env);
// Find spec source — either per-spec textarea or embedded in overview
var specEl = document.getElementById("test-spec-" + sn);
if (specEl) {
evalSource(specEl.textContent, ctx.env);
}
}
var elapsed = Math.round(performance.now() - t0);
var r = ctx.getResults();
r.lines.push("");
r.lines.push("1.." + r.num);
r.lines.push("# tests " + (r.passed + r.failed));
r.lines.push("# pass " + r.passed);
if (r.failed > 0) r.lines.push("# fail " + r.failed);
r.lines.push("# time " + elapsed + "ms");
} catch(e) {
var r = ctx.getResults();
r.lines.push("");
r.lines.push("FATAL: " + (e.message || String(e)));
}
out.textContent = r.lines.join("\n");
out.style.display = "block";
btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)");
btn.className = r.failed > 0
? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default" ? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default"
: "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default"; : "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default";
}; };

494
shared/sx/ref/test-eval.sx Normal file
View File

@@ -0,0 +1,494 @@
;; ==========================================================================
;; test-eval.sx — Tests for the core evaluator and primitives
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: eval.sx, primitives.sx
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Literals and types
;; --------------------------------------------------------------------------
(defsuite "literals"
(deftest "numbers are numbers"
(assert-type "number" 42)
(assert-type "number" 3.14)
(assert-type "number" -1))
(deftest "strings are strings"
(assert-type "string" "hello")
(assert-type "string" ""))
(deftest "booleans are booleans"
(assert-type "boolean" true)
(assert-type "boolean" false))
(deftest "nil is nil"
(assert-type "nil" nil)
(assert-nil nil))
(deftest "lists are lists"
(assert-type "list" (list 1 2 3))
(assert-type "list" (list)))
(deftest "dicts are dicts"
(assert-type "dict" {:a 1 :b 2})))
;; --------------------------------------------------------------------------
;; Arithmetic
;; --------------------------------------------------------------------------
(defsuite "arithmetic"
(deftest "addition"
(assert-equal 3 (+ 1 2))
(assert-equal 0 (+ 0 0))
(assert-equal -1 (+ 1 -2))
(assert-equal 10 (+ 1 2 3 4)))
(deftest "subtraction"
(assert-equal 1 (- 3 2))
(assert-equal -1 (- 2 3)))
(deftest "multiplication"
(assert-equal 6 (* 2 3))
(assert-equal 0 (* 0 100))
(assert-equal 24 (* 1 2 3 4)))
(deftest "division"
(assert-equal 2 (/ 6 3))
(assert-equal 2.5 (/ 5 2)))
(deftest "modulo"
(assert-equal 1 (mod 7 3))
(assert-equal 0 (mod 6 3))))
;; --------------------------------------------------------------------------
;; Comparison
;; --------------------------------------------------------------------------
(defsuite "comparison"
(deftest "equality"
(assert-true (= 1 1))
(assert-false (= 1 2))
(assert-true (= "a" "a"))
(assert-false (= "a" "b")))
(deftest "deep equality"
(assert-true (equal? (list 1 2 3) (list 1 2 3)))
(assert-false (equal? (list 1 2) (list 1 3)))
(assert-true (equal? {:a 1} {:a 1}))
(assert-false (equal? {:a 1} {:a 2})))
(deftest "ordering"
(assert-true (< 1 2))
(assert-false (< 2 1))
(assert-true (> 2 1))
(assert-true (<= 1 1))
(assert-true (<= 1 2))
(assert-true (>= 2 2))
(assert-true (>= 3 2))))
;; --------------------------------------------------------------------------
;; String operations
;; --------------------------------------------------------------------------
(defsuite "strings"
(deftest "str concatenation"
(assert-equal "abc" (str "a" "b" "c"))
(assert-equal "hello world" (str "hello" " " "world"))
(assert-equal "42" (str 42))
(assert-equal "" (str)))
(deftest "string-length"
(assert-equal 5 (string-length "hello"))
(assert-equal 0 (string-length "")))
(deftest "substring"
(assert-equal "ell" (substring "hello" 1 4))
(assert-equal "hello" (substring "hello" 0 5)))
(deftest "string-contains?"
(assert-true (string-contains? "hello world" "world"))
(assert-false (string-contains? "hello" "xyz")))
(deftest "upcase and downcase"
(assert-equal "HELLO" (upcase "hello"))
(assert-equal "hello" (downcase "HELLO")))
(deftest "trim"
(assert-equal "hello" (trim " hello "))
(assert-equal "hello" (trim "hello")))
(deftest "split and join"
(assert-equal (list "a" "b" "c") (split "a,b,c" ","))
(assert-equal "a-b-c" (join "-" (list "a" "b" "c")))))
;; --------------------------------------------------------------------------
;; List operations
;; --------------------------------------------------------------------------
(defsuite "lists"
(deftest "constructors"
(assert-equal (list 1 2 3) (list 1 2 3))
(assert-equal (list) (list))
(assert-length 3 (list 1 2 3)))
(deftest "first and rest"
(assert-equal 1 (first (list 1 2 3)))
(assert-equal (list 2 3) (rest (list 1 2 3)))
(assert-nil (first (list)))
(assert-equal (list) (rest (list))))
(deftest "nth"
(assert-equal 1 (nth (list 1 2 3) 0))
(assert-equal 2 (nth (list 1 2 3) 1))
(assert-equal 3 (nth (list 1 2 3) 2)))
(deftest "last"
(assert-equal 3 (last (list 1 2 3)))
(assert-nil (last (list))))
(deftest "cons and append"
(assert-equal (list 0 1 2) (cons 0 (list 1 2)))
(assert-equal (list 1 2 3 4) (append (list 1 2) (list 3 4))))
(deftest "reverse"
(assert-equal (list 3 2 1) (reverse (list 1 2 3)))
(assert-equal (list) (reverse (list))))
(deftest "empty?"
(assert-true (empty? (list)))
(assert-false (empty? (list 1))))
(deftest "len"
(assert-equal 0 (len (list)))
(assert-equal 3 (len (list 1 2 3))))
(deftest "contains?"
(assert-true (contains? (list 1 2 3) 2))
(assert-false (contains? (list 1 2 3) 4)))
(deftest "flatten"
(assert-equal (list 1 2 3 4) (flatten (list (list 1 2) (list 3 4))))))
;; --------------------------------------------------------------------------
;; Dict operations
;; --------------------------------------------------------------------------
(defsuite "dicts"
(deftest "dict literal"
(assert-type "dict" {:a 1 :b 2})
(assert-equal 1 (get {:a 1} "a"))
(assert-equal 2 (get {:a 1 :b 2} "b")))
(deftest "assoc"
(assert-equal {:a 1 :b 2} (assoc {:a 1} "b" 2))
(assert-equal {:a 99} (assoc {:a 1} "a" 99)))
(deftest "dissoc"
(assert-equal {:b 2} (dissoc {:a 1 :b 2} "a")))
(deftest "keys and vals"
(let ((d {:a 1 :b 2}))
(assert-length 2 (keys d))
(assert-length 2 (vals d))
(assert-contains "a" (keys d))
(assert-contains "b" (keys d))))
(deftest "has-key?"
(assert-true (has-key? {:a 1} "a"))
(assert-false (has-key? {:a 1} "b")))
(deftest "merge"
(assert-equal {:a 1 :b 2 :c 3}
(merge {:a 1 :b 2} {:c 3}))
(assert-equal {:a 99 :b 2}
(merge {:a 1 :b 2} {:a 99}))))
;; --------------------------------------------------------------------------
;; Predicates
;; --------------------------------------------------------------------------
(defsuite "predicates"
(deftest "nil?"
(assert-true (nil? nil))
(assert-false (nil? 0))
(assert-false (nil? false))
(assert-false (nil? "")))
(deftest "number?"
(assert-true (number? 42))
(assert-true (number? 3.14))
(assert-false (number? "42")))
(deftest "string?"
(assert-true (string? "hello"))
(assert-false (string? 42)))
(deftest "list?"
(assert-true (list? (list 1 2)))
(assert-false (list? "not a list")))
(deftest "dict?"
(assert-true (dict? {:a 1}))
(assert-false (dict? (list 1))))
(deftest "boolean?"
(assert-true (boolean? true))
(assert-true (boolean? false))
(assert-false (boolean? nil))
(assert-false (boolean? 0)))
(deftest "not"
(assert-true (not false))
(assert-true (not nil))
(assert-false (not true))
(assert-false (not 1))
(assert-false (not "x"))))
;; --------------------------------------------------------------------------
;; Special forms
;; --------------------------------------------------------------------------
(defsuite "special-forms"
(deftest "if"
(assert-equal "yes" (if true "yes" "no"))
(assert-equal "no" (if false "yes" "no"))
(assert-equal "no" (if nil "yes" "no"))
(assert-nil (if false "yes")))
(deftest "when"
(assert-equal "yes" (when true "yes"))
(assert-nil (when false "yes")))
(deftest "cond"
(assert-equal "a" (cond true "a" :else "b"))
(assert-equal "b" (cond false "a" :else "b"))
(assert-equal "c" (cond
false "a"
false "b"
:else "c")))
(deftest "and"
(assert-true (and true true))
(assert-false (and true false))
(assert-false (and false true))
(assert-equal 3 (and 1 2 3)))
(deftest "or"
(assert-equal 1 (or 1 2))
(assert-equal 2 (or false 2))
(assert-equal "fallback" (or nil false "fallback"))
(assert-false (or false false)))
(deftest "let"
(assert-equal 3 (let ((x 1) (y 2)) (+ x y)))
(assert-equal "hello world"
(let ((a "hello") (b " world")) (str a b))))
(deftest "let clojure-style"
(assert-equal 3 (let (x 1 y 2) (+ x y))))
(deftest "do / begin"
(assert-equal 3 (do 1 2 3))
(assert-equal "last" (begin "first" "middle" "last")))
(deftest "define"
(define x 42)
(assert-equal 42 x))
(deftest "set!"
(define x 1)
(set! x 2)
(assert-equal 2 x)))
;; --------------------------------------------------------------------------
;; Lambda and closures
;; --------------------------------------------------------------------------
(defsuite "lambdas"
(deftest "basic lambda"
(let ((add (fn (a b) (+ a b))))
(assert-equal 3 (add 1 2))))
(deftest "closure captures env"
(let ((x 10))
(let ((add-x (fn (y) (+ x y))))
(assert-equal 15 (add-x 5)))))
(deftest "lambda as argument"
(assert-equal (list 2 4 6)
(map (fn (x) (* x 2)) (list 1 2 3))))
(deftest "recursive lambda via define"
(define factorial
(fn (n) (if (<= n 1) 1 (* n (factorial (- n 1))))))
(assert-equal 120 (factorial 5)))
(deftest "higher-order returns lambda"
(let ((make-adder (fn (n) (fn (x) (+ n x)))))
(let ((add5 (make-adder 5)))
(assert-equal 8 (add5 3))))))
;; --------------------------------------------------------------------------
;; Higher-order forms
;; --------------------------------------------------------------------------
(defsuite "higher-order"
(deftest "map"
(assert-equal (list 2 4 6)
(map (fn (x) (* x 2)) (list 1 2 3)))
(assert-equal (list) (map (fn (x) x) (list))))
(deftest "filter"
(assert-equal (list 2 4)
(filter (fn (x) (= (mod x 2) 0)) (list 1 2 3 4)))
(assert-equal (list)
(filter (fn (x) false) (list 1 2 3))))
(deftest "reduce"
(assert-equal 10 (reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3 4)))
(assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list))))
(deftest "some"
(assert-true (some (fn (x) (> x 3)) (list 1 2 3 4 5)))
(assert-false (some (fn (x) (> x 10)) (list 1 2 3))))
(deftest "every?"
(assert-true (every? (fn (x) (> x 0)) (list 1 2 3)))
(assert-false (every? (fn (x) (> x 2)) (list 1 2 3))))
(deftest "map-indexed"
(assert-equal (list "0:a" "1:b" "2:c")
(map-indexed (fn (i x) (str i ":" x)) (list "a" "b" "c")))))
;; --------------------------------------------------------------------------
;; Components
;; --------------------------------------------------------------------------
(defsuite "components"
(deftest "defcomp creates component"
(defcomp ~test-comp (&key title)
(div title))
(assert-true (not (nil? ~test-comp))))
(deftest "component renders with keyword args"
(defcomp ~greeting (&key name)
(span (str "Hello, " name "!")))
(assert-true (not (nil? ~greeting))))
(deftest "component with children"
(defcomp ~box (&key &rest children)
(div :class "box" children))
(assert-true (not (nil? ~box))))
(deftest "component with default via or"
(defcomp ~label (&key text)
(span (or text "default")))
(assert-true (not (nil? ~label)))))
;; --------------------------------------------------------------------------
;; Macros
;; --------------------------------------------------------------------------
(defsuite "macros"
(deftest "defmacro creates macro"
(defmacro unless (cond &rest body)
`(if (not ,cond) (do ,@body)))
(assert-equal "yes" (unless false "yes"))
(assert-nil (unless true "no")))
(deftest "quasiquote and unquote"
(let ((x 42))
(assert-equal (list 1 42 3) `(1 ,x 3))))
(deftest "splice-unquote"
(let ((xs (list 2 3 4)))
(assert-equal (list 1 2 3 4 5) `(1 ,@xs 5)))))
;; --------------------------------------------------------------------------
;; Threading macro
;; --------------------------------------------------------------------------
(defsuite "threading"
(deftest "thread-first"
(assert-equal 8 (-> 5 (+ 1) (+ 2)))
(assert-equal "HELLO" (-> "hello" upcase))
(assert-equal "HELLO WORLD"
(-> "hello"
(str " world")
upcase))))
;; --------------------------------------------------------------------------
;; Truthiness
;; --------------------------------------------------------------------------
(defsuite "truthiness"
(deftest "truthy values"
(assert-true (if 1 true false))
(assert-true (if "x" true false))
(assert-true (if (list 1) true false))
(assert-true (if true true false)))
(deftest "falsy values"
(assert-false (if false true false))
(assert-false (if nil true false)))
;; NOTE: empty list, zero, and empty string truthiness is
;; platform-dependent. Python treats all three as falsy.
;; JavaScript treats [] as truthy but 0 and "" as falsy.
;; These tests are omitted — each bootstrapper should emit
;; platform-specific truthiness tests instead.
)
;; --------------------------------------------------------------------------
;; Edge cases and regression tests
;; --------------------------------------------------------------------------
(defsuite "edge-cases"
(deftest "nested let scoping"
(let ((x 1))
(let ((x 2))
(assert-equal 2 x))
;; outer x should be unchanged by inner let
;; (this tests that let creates a new scope)
))
(deftest "recursive map"
(assert-equal (list (list 2 4) (list 6 8))
(map (fn (sub) (map (fn (x) (* x 2)) sub))
(list (list 1 2) (list 3 4)))))
(deftest "keyword as value"
(assert-equal "class" :class)
(assert-equal "id" :id))
(deftest "dict with evaluated values"
(let ((x 42))
(assert-equal 42 (get {:val x} "val"))))
(deftest "nil propagation"
(assert-nil (get {:a 1} "missing"))
(assert-equal "default" (or (get {:a 1} "missing") "default")))
(deftest "empty operations"
(assert-equal (list) (map (fn (x) x) (list)))
(assert-equal (list) (filter (fn (x) true) (list)))
(assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list)))
(assert-equal 0 (len (list)))
(assert-equal "" (str))))

View File

@@ -0,0 +1,86 @@
;; ==========================================================================
;; test-framework.sx — Reusable test macros and assertion helpers
;;
;; Loaded first by all test runners. Provides deftest, defsuite, and
;; assertion helpers. Requires 5 platform functions from the host:
;;
;; try-call (thunk) -> {:ok true} | {:ok false :error "msg"}
;; report-pass (name) -> platform-specific pass output
;; report-fail (name error) -> platform-specific fail output
;; push-suite (name) -> push suite name onto context stack
;; pop-suite () -> pop suite name from context stack
;;
;; Any host that provides these 5 functions can run any test spec.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; 1. Test framework macros
;; --------------------------------------------------------------------------
(defmacro deftest (name &rest body)
`(let ((result (try-call (fn () ,@body))))
(if (get result "ok")
(report-pass ,name)
(report-fail ,name (get result "error")))))
(defmacro defsuite (name &rest items)
`(do (push-suite ,name)
,@items
(pop-suite)))
;; --------------------------------------------------------------------------
;; 2. Assertion helpers — defined in SX, available in test bodies
;; --------------------------------------------------------------------------
(define assert-equal
(fn (expected actual)
(assert (equal? expected actual)
(str "Expected " (str expected) " but got " (str actual)))))
(define assert-not-equal
(fn (a b)
(assert (not (equal? a b))
(str "Expected values to differ but both are " (str a)))))
(define assert-true
(fn (val)
(assert val (str "Expected truthy but got " (str val)))))
(define assert-false
(fn (val)
(assert (not val) (str "Expected falsy but got " (str val)))))
(define assert-nil
(fn (val)
(assert (nil? val) (str "Expected nil but got " (str val)))))
(define assert-type
(fn (expected-type val)
(let ((actual-type
(if (nil? val) "nil"
(if (boolean? val) "boolean"
(if (number? val) "number"
(if (string? val) "string"
(if (list? val) "list"
(if (dict? val) "dict"
"unknown"))))))))
(assert (= expected-type actual-type)
(str "Expected type " expected-type " but got " actual-type)))))
(define assert-length
(fn (expected-len col)
(assert (= (len col) expected-len)
(str "Expected length " expected-len " but got " (len col)))))
(define assert-contains
(fn (item col)
(assert (some (fn (x) (equal? x item)) col)
(str "Expected collection to contain " (str item)))))
(define assert-throws
(fn (thunk)
(let ((result (try-call thunk)))
(assert (not (get result "ok"))
"Expected an error to be thrown but none was"))))

View File

@@ -0,0 +1,222 @@
;; ==========================================================================
;; test-parser.sx — Tests for the SX parser and serializer
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: parser.sx
;;
;; Platform functions required (beyond test framework):
;; sx-parse (source) -> list of AST expressions
;; sx-serialize (expr) -> SX source string
;; make-symbol (name) -> Symbol value
;; make-keyword (name) -> Keyword value
;; symbol-name (sym) -> string
;; keyword-name (kw) -> string
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Literal parsing
;; --------------------------------------------------------------------------
(defsuite "parser-literals"
(deftest "parse integers"
(assert-equal (list 42) (sx-parse "42"))
(assert-equal (list 0) (sx-parse "0"))
(assert-equal (list -7) (sx-parse "-7")))
(deftest "parse floats"
(assert-equal (list 3.14) (sx-parse "3.14"))
(assert-equal (list -0.5) (sx-parse "-0.5")))
(deftest "parse strings"
(assert-equal (list "hello") (sx-parse "\"hello\""))
(assert-equal (list "") (sx-parse "\"\"")))
(deftest "parse escape: newline"
(assert-equal (list "a\nb") (sx-parse "\"a\\nb\"")))
(deftest "parse escape: tab"
(assert-equal (list "a\tb") (sx-parse "\"a\\tb\"")))
(deftest "parse escape: quote"
(assert-equal (list "a\"b") (sx-parse "\"a\\\"b\"")))
(deftest "parse booleans"
(assert-equal (list true) (sx-parse "true"))
(assert-equal (list false) (sx-parse "false")))
(deftest "parse nil"
(assert-equal (list nil) (sx-parse "nil")))
(deftest "parse keywords"
(let ((result (sx-parse ":hello")))
(assert-length 1 result)
(assert-equal "hello" (keyword-name (first result)))))
(deftest "parse symbols"
(let ((result (sx-parse "foo")))
(assert-length 1 result)
(assert-equal "foo" (symbol-name (first result))))))
;; --------------------------------------------------------------------------
;; Composite parsing
;; --------------------------------------------------------------------------
(defsuite "parser-lists"
(deftest "parse empty list"
(let ((result (sx-parse "()")))
(assert-length 1 result)
(assert-equal (list) (first result))))
(deftest "parse list of numbers"
(let ((result (sx-parse "(1 2 3)")))
(assert-length 1 result)
(assert-equal (list 1 2 3) (first result))))
(deftest "parse nested lists"
(let ((result (sx-parse "(1 (2 3) 4)")))
(assert-length 1 result)
(assert-equal (list 1 (list 2 3) 4) (first result))))
(deftest "parse square brackets as list"
(let ((result (sx-parse "[1 2 3]")))
(assert-length 1 result)
(assert-equal (list 1 2 3) (first result))))
(deftest "parse mixed types"
(let ((result (sx-parse "(42 \"hello\" true nil)")))
(assert-length 1 result)
(let ((lst (first result)))
(assert-equal 42 (nth lst 0))
(assert-equal "hello" (nth lst 1))
(assert-equal true (nth lst 2))
(assert-nil (nth lst 3))))))
;; --------------------------------------------------------------------------
;; Dict parsing
;; --------------------------------------------------------------------------
(defsuite "parser-dicts"
(deftest "parse empty dict"
(let ((result (sx-parse "{}")))
(assert-length 1 result)
(assert-type "dict" (first result))))
(deftest "parse dict with keyword keys"
(let ((result (sx-parse "{:a 1 :b 2}")))
(assert-length 1 result)
(let ((d (first result)))
(assert-type "dict" d)
(assert-equal 1 (get d "a"))
(assert-equal 2 (get d "b")))))
(deftest "parse dict with string values"
(let ((result (sx-parse "{:name \"alice\"}")))
(assert-length 1 result)
(assert-equal "alice" (get (first result) "name")))))
;; --------------------------------------------------------------------------
;; Comments and whitespace
;; --------------------------------------------------------------------------
(defsuite "parser-whitespace"
(deftest "skip line comments"
(assert-equal (list 42) (sx-parse ";; comment\n42"))
(assert-equal (list 1 2) (sx-parse "1 ;; middle\n2")))
(deftest "skip whitespace"
(assert-equal (list 42) (sx-parse " 42 "))
(assert-equal (list 1 2) (sx-parse " 1 \n\t 2 ")))
(deftest "parse multiple top-level expressions"
(assert-length 3 (sx-parse "1 2 3"))
(assert-equal (list 1 2 3) (sx-parse "1 2 3")))
(deftest "empty input"
(assert-equal (list) (sx-parse "")))
(deftest "only comments"
(assert-equal (list) (sx-parse ";; just a comment\n;; another"))))
;; --------------------------------------------------------------------------
;; Quote sugar
;; --------------------------------------------------------------------------
(defsuite "parser-quote-sugar"
(deftest "quasiquote"
(let ((result (sx-parse "`foo")))
(assert-length 1 result)
(let ((expr (first result)))
(assert-type "list" expr)
(assert-equal "quasiquote" (symbol-name (first expr))))))
(deftest "unquote"
(let ((result (sx-parse ",foo")))
(assert-length 1 result)
(let ((expr (first result)))
(assert-type "list" expr)
(assert-equal "unquote" (symbol-name (first expr))))))
(deftest "splice-unquote"
(let ((result (sx-parse ",@foo")))
(assert-length 1 result)
(let ((expr (first result)))
(assert-type "list" expr)
(assert-equal "splice-unquote" (symbol-name (first expr)))))))
;; --------------------------------------------------------------------------
;; Serializer
;; --------------------------------------------------------------------------
(defsuite "serializer"
(deftest "serialize number"
(assert-equal "42" (sx-serialize 42)))
(deftest "serialize string"
(assert-equal "\"hello\"" (sx-serialize "hello")))
(deftest "serialize boolean"
(assert-equal "true" (sx-serialize true))
(assert-equal "false" (sx-serialize false)))
(deftest "serialize nil"
(assert-equal "nil" (sx-serialize nil)))
(deftest "serialize keyword"
(assert-equal ":foo" (sx-serialize (make-keyword "foo"))))
(deftest "serialize symbol"
(assert-equal "bar" (sx-serialize (make-symbol "bar"))))
(deftest "serialize list"
(assert-equal "(1 2 3)" (sx-serialize (list 1 2 3))))
(deftest "serialize empty list"
(assert-equal "()" (sx-serialize (list))))
(deftest "serialize nested"
(assert-equal "(1 (2 3) 4)" (sx-serialize (list 1 (list 2 3) 4)))))
;; --------------------------------------------------------------------------
;; Round-trip: parse then serialize
;; --------------------------------------------------------------------------
(defsuite "parser-roundtrip"
(deftest "roundtrip number"
(assert-equal "42" (sx-serialize (first (sx-parse "42")))))
(deftest "roundtrip string"
(assert-equal "\"hello\"" (sx-serialize (first (sx-parse "\"hello\"")))))
(deftest "roundtrip list"
(assert-equal "(1 2 3)" (sx-serialize (first (sx-parse "(1 2 3)")))))
(deftest "roundtrip nested"
(assert-equal "(a (b c))"
(sx-serialize (first (sx-parse "(a (b c))"))))))

View File

@@ -0,0 +1,167 @@
;; ==========================================================================
;; test-render.sx — Tests for the HTML rendering adapter
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: render.sx, adapter-html.sx
;;
;; Platform functions required (beyond test framework):
;; render-html (sx-source) -> HTML string
;; Parses the sx-source string, evaluates via render-to-html in a
;; fresh env, and returns the resulting HTML string.
;; (This is a test-only convenience that wraps parse + render-to-html.)
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Basic element rendering
;; --------------------------------------------------------------------------
(defsuite "render-elements"
(deftest "simple div"
(assert-equal "<div>hello</div>"
(render-html "(div \"hello\")")))
(deftest "nested elements"
(assert-equal "<div><span>hi</span></div>"
(render-html "(div (span \"hi\"))")))
(deftest "multiple children"
(assert-equal "<div><p>a</p><p>b</p></div>"
(render-html "(div (p \"a\") (p \"b\"))")))
(deftest "text content"
(assert-equal "<p>hello world</p>"
(render-html "(p \"hello\" \" world\")")))
(deftest "number content"
(assert-equal "<span>42</span>"
(render-html "(span 42)"))))
;; --------------------------------------------------------------------------
;; Attributes
;; --------------------------------------------------------------------------
(defsuite "render-attrs"
(deftest "string attribute"
(let ((html (render-html "(div :id \"main\" \"content\")")))
(assert-true (string-contains? html "id=\"main\""))
(assert-true (string-contains? html "content"))))
(deftest "class attribute"
(let ((html (render-html "(div :class \"foo bar\" \"x\")")))
(assert-true (string-contains? html "class=\"foo bar\""))))
(deftest "multiple attributes"
(let ((html (render-html "(a :href \"/home\" :class \"link\" \"Home\")")))
(assert-true (string-contains? html "href=\"/home\""))
(assert-true (string-contains? html "class=\"link\""))
(assert-true (string-contains? html "Home")))))
;; --------------------------------------------------------------------------
;; Void elements
;; --------------------------------------------------------------------------
(defsuite "render-void"
(deftest "br is self-closing"
(assert-equal "<br />" (render-html "(br)")))
(deftest "img with attrs"
(let ((html (render-html "(img :src \"pic.jpg\" :alt \"A pic\")")))
(assert-true (string-contains? html "<img"))
(assert-true (string-contains? html "src=\"pic.jpg\""))
(assert-true (string-contains? html "/>"))
;; void elements should not have a closing tag
(assert-false (string-contains? html "</img>"))))
(deftest "input is self-closing"
(let ((html (render-html "(input :type \"text\" :name \"q\")")))
(assert-true (string-contains? html "<input"))
(assert-true (string-contains? html "/>")))))
;; --------------------------------------------------------------------------
;; Boolean attributes
;; --------------------------------------------------------------------------
(defsuite "render-boolean-attrs"
(deftest "true boolean attr emits name only"
(let ((html (render-html "(input :disabled true :type \"text\")")))
(assert-true (string-contains? html "disabled"))
;; Should NOT have disabled="true"
(assert-false (string-contains? html "disabled=\""))))
(deftest "false boolean attr omitted"
(let ((html (render-html "(input :disabled false :type \"text\")")))
(assert-false (string-contains? html "disabled")))))
;; --------------------------------------------------------------------------
;; Fragments
;; --------------------------------------------------------------------------
(defsuite "render-fragments"
(deftest "fragment renders children without wrapper"
(assert-equal "<p>a</p><p>b</p>"
(render-html "(<> (p \"a\") (p \"b\"))")))
(deftest "empty fragment"
(assert-equal ""
(render-html "(<>)"))))
;; --------------------------------------------------------------------------
;; HTML escaping
;; --------------------------------------------------------------------------
(defsuite "render-escaping"
(deftest "text content is escaped"
(let ((html (render-html "(p \"<script>alert(1)</script>\")")))
(assert-false (string-contains? html "<script>"))
(assert-true (string-contains? html "&lt;script&gt;"))))
(deftest "attribute values are escaped"
(let ((html (render-html "(div :title \"a\\\"b\" \"x\")")))
(assert-true (string-contains? html "title=")))))
;; --------------------------------------------------------------------------
;; Control flow in render context
;; --------------------------------------------------------------------------
(defsuite "render-control-flow"
(deftest "if renders correct branch"
(assert-equal "<p>yes</p>"
(render-html "(if true (p \"yes\") (p \"no\"))"))
(assert-equal "<p>no</p>"
(render-html "(if false (p \"yes\") (p \"no\"))")))
(deftest "when renders or skips"
(assert-equal "<p>ok</p>"
(render-html "(when true (p \"ok\"))"))
(assert-equal ""
(render-html "(when false (p \"ok\"))")))
(deftest "map renders list"
(assert-equal "<li>1</li><li>2</li><li>3</li>"
(render-html "(map (fn (x) (li x)) (list 1 2 3))")))
(deftest "let in render context"
(assert-equal "<p>hello</p>"
(render-html "(let ((x \"hello\")) (p x))"))))
;; --------------------------------------------------------------------------
;; Component rendering
;; --------------------------------------------------------------------------
(defsuite "render-components"
(deftest "component with keyword args"
(assert-equal "<h1>Hello</h1>"
(render-html "(do (defcomp ~title (&key text) (h1 text)) (~title :text \"Hello\"))")))
(deftest "component with children"
(let ((html (render-html "(do (defcomp ~box (&key &rest children) (div :class \"box\" children)) (~box (p \"inside\")))")))
(assert-true (string-contains? html "class=\"box\""))
(assert-true (string-contains? html "<p>inside</p>")))))

View File

@@ -0,0 +1,125 @@
;; ==========================================================================
;; test-router.sx — Tests for client-side route matching
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: router.sx
;;
;; No additional platform functions needed — router.sx is pure.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; split-path-segments
;; --------------------------------------------------------------------------
(defsuite "split-path-segments"
(deftest "root path"
(assert-equal (list) (split-path-segments "/")))
(deftest "single segment"
(assert-equal (list "docs") (split-path-segments "/docs")))
(deftest "multiple segments"
(assert-equal (list "docs" "hello") (split-path-segments "/docs/hello")))
(deftest "trailing slash stripped"
(assert-equal (list "docs") (split-path-segments "/docs/")))
(deftest "deep path"
(assert-equal (list "a" "b" "c" "d") (split-path-segments "/a/b/c/d"))))
;; --------------------------------------------------------------------------
;; parse-route-pattern
;; --------------------------------------------------------------------------
(defsuite "parse-route-pattern"
(deftest "static pattern"
(let ((segs (parse-route-pattern "/docs/intro")))
(assert-length 2 segs)
(assert-equal "literal" (get (first segs) "type"))
(assert-equal "docs" (get (first segs) "value"))
(assert-equal "literal" (get (nth segs 1) "type"))
(assert-equal "intro" (get (nth segs 1) "value"))))
(deftest "pattern with param"
(let ((segs (parse-route-pattern "/docs/<slug>")))
(assert-length 2 segs)
(assert-equal "literal" (get (first segs) "type"))
(assert-equal "docs" (get (first segs) "value"))
(assert-equal "param" (get (nth segs 1) "type"))
(assert-equal "slug" (get (nth segs 1) "value"))))
(deftest "multiple params"
(let ((segs (parse-route-pattern "/users/<uid>/posts/<pid>")))
(assert-length 4 segs)
(assert-equal "param" (get (nth segs 1) "type"))
(assert-equal "uid" (get (nth segs 1) "value"))
(assert-equal "param" (get (nth segs 3) "type"))
(assert-equal "pid" (get (nth segs 3) "value"))))
(deftest "root pattern"
(assert-equal (list) (parse-route-pattern "/"))))
;; --------------------------------------------------------------------------
;; match-route
;; --------------------------------------------------------------------------
(defsuite "match-route"
(deftest "exact match returns empty params"
(let ((result (match-route "/docs/intro" "/docs/intro")))
(assert-true (not (nil? result)))
(assert-length 0 (keys result))))
(deftest "param match extracts value"
(let ((result (match-route "/docs/hello" "/docs/<slug>")))
(assert-true (not (nil? result)))
(assert-equal "hello" (get result "slug"))))
(deftest "no match returns nil"
(assert-nil (match-route "/docs/hello" "/essays/<slug>"))
(assert-nil (match-route "/docs" "/docs/<slug>")))
(deftest "segment count mismatch returns nil"
(assert-nil (match-route "/a/b/c" "/a/<b>"))
(assert-nil (match-route "/a" "/a/b")))
(deftest "root matches root"
(let ((result (match-route "/" "/")))
(assert-true (not (nil? result)))))
(deftest "multiple params extracted"
(let ((result (match-route "/users/42/posts/99" "/users/<uid>/posts/<pid>")))
(assert-true (not (nil? result)))
(assert-equal "42" (get result "uid"))
(assert-equal "99" (get result "pid")))))
;; --------------------------------------------------------------------------
;; find-matching-route
;; --------------------------------------------------------------------------
(defsuite "find-matching-route"
(deftest "finds first matching route"
(let ((routes (list
{:pattern "/docs/" :parsed (parse-route-pattern "/docs/") :name "docs-index"}
{:pattern "/docs/<slug>" :parsed (parse-route-pattern "/docs/<slug>") :name "docs-page"})))
(let ((result (find-matching-route "/docs/hello" routes)))
(assert-true (not (nil? result)))
(assert-equal "docs-page" (get result "name"))
(assert-equal "hello" (get (get result "params") "slug")))))
(deftest "returns nil for no match"
(let ((routes (list
{:pattern "/docs/<slug>" :parsed (parse-route-pattern "/docs/<slug>") :name "docs-page"})))
(assert-nil (find-matching-route "/essays/hello" routes))))
(deftest "matches exact routes before param routes"
(let ((routes (list
{:pattern "/docs/" :parsed (parse-route-pattern "/docs/") :name "docs-index"}
{:pattern "/docs/<slug>" :parsed (parse-route-pattern "/docs/<slug>") :name "docs-page"})))
;; /docs/ should match docs-index, not docs-page
(let ((result (find-matching-route "/docs/" routes)))
(assert-true (not (nil? result)))
(assert-equal "docs-index" (get result "name"))))))

View File

@@ -1,16 +1,21 @@
;; ========================================================================== ;; ==========================================================================
;; test.sx — Self-hosting SX test framework ;; test.sx — Self-hosting SX test suite (backward-compatible entry point)
;; ;;
;; Defines a minimal test framework in SX that tests SX — the language ;; This file includes the test framework and core eval tests inline.
;; proves its own correctness. The framework is self-executing: any host ;; It exists for backward compatibility — runners that load "test.sx"
;; that provides 5 platform functions can evaluate this file directly. ;; get the same 81 tests as before.
;;
;; For modular testing, runners should instead load:
;; 1. test-framework.sx (macros + assertions)
;; 2. One or more test specs: test-eval.sx, test-parser.sx,
;; test-router.sx, test-render.sx, etc.
;; ;;
;; Platform functions required: ;; Platform functions required:
;; try-call (thunk) {:ok true} | {:ok false :error "msg"} ;; try-call (thunk) -> {:ok true} | {:ok false :error "msg"}
;; report-pass (name) platform-specific pass output ;; report-pass (name) -> platform-specific pass output
;; report-fail (name error) platform-specific fail output ;; report-fail (name error) -> platform-specific fail output
;; push-suite (name) push suite name onto context stack ;; push-suite (name) -> push suite name onto context stack
;; pop-suite () pop suite name from context stack ;; pop-suite () -> pop suite name from context stack
;; ;;
;; Usage: ;; Usage:
;; ;; Host injects platform functions into env, then: ;; ;; Host injects platform functions into env, then:

View File

@@ -1,9 +1,13 @@
// Run test.sx directly against sx-browser.js. // Run SX test specs against sx-browser.js.
// //
// sx-browser.js parses and evaluates test.sx — SX tests itself. // sx-browser.js parses and evaluates test specs — SX tests itself.
// This script provides only platform functions (error catching, reporting). // This script provides only platform functions (error catching, reporting).
// //
// Usage: node shared/sx/tests/run.js // Usage:
// node shared/sx/tests/run.js # run all available specs
// node shared/sx/tests/run.js eval # run only test-eval.sx
// node shared/sx/tests/run.js eval parser router # run specific specs
// node shared/sx/tests/run.js --legacy # run monolithic test.sx
Object.defineProperty(globalThis, "document", { value: undefined, writable: true }); Object.defineProperty(globalThis, "document", { value: undefined, writable: true });
var path = require("path"); var path = require("path");
@@ -86,15 +90,149 @@ var env = {
}, },
"has-key?": function(d, k) { return d && typeof d === "object" && k in d; }, "has-key?": function(d, k) { return d && typeof d === "object" && k in d; },
"append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); }, "append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); },
"for-each-indexed": function(f, coll) {
for (var i = 0; i < (coll||[]).length; i++) {
Sx.eval([f, i, coll[i]], env);
}
},
"dict-set!": function(d, k, v) { if (d) d[k] = v; },
"dict-has?": function(d, k) { return d && typeof d === "object" && k in d; },
"dict-get": function(d, k) { return d ? d[k] : undefined; },
"starts-with?": function(s, prefix) { return String(s).indexOf(prefix) === 0; },
"ends-with?": function(s, suffix) { var str = String(s); return str.indexOf(suffix) === str.length - suffix.length; },
"slice": function(s, start, end) { return end !== undefined ? s.slice(start, end) : s.slice(start); },
"inc": function(n) { return n + 1; },
"append!": function(arr, item) { if (Array.isArray(arr)) arr.push(item); },
"dict": function() { return {}; },
"for-each": function(f, coll) {
for (var i = 0; i < (coll||[]).length; i++) {
Sx.eval([f, coll[i]], env);
}
},
// --- Parser platform functions (for test-parser.sx) ---
"sx-parse": function(source) { return Sx.parseAll(source); },
"sx-serialize": function(val) {
// Basic serializer for test roundtrips
if (val === Sx.NIL || val === null || val === undefined) return "nil";
if (typeof val === "boolean") return val ? "true" : "false";
if (typeof val === "number") return String(val);
if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
// Check Symbol/Keyword BEFORE generic object — they are objects too
if (val && (val._sym || val._sx_symbol)) return val.name;
if (val && (val._kw || val._sx_keyword)) return ":" + val.name;
if (Array.isArray(val)) return "(" + val.map(function(x) { return env["sx-serialize"](x); }).join(" ") + ")";
if (val && typeof val === "object") {
var parts = [];
Object.keys(val).forEach(function(k) {
parts.push(":" + k);
parts.push(env["sx-serialize"](val[k]));
});
return "{" + parts.join(" ") + "}";
}
return String(val);
},
"make-symbol": function(name) { return Sx.sym ? Sx.sym(name) : { _sx_symbol: true, name: name, toString: function() { return name; } }; },
"make-keyword": function(name) { return Sx.kw ? Sx.kw(name) : { _sx_keyword: true, name: name, toString: function() { return name; } }; },
"symbol-name": function(s) { return s && s.name ? s.name : String(s); },
"keyword-name": function(k) { return k && k.name ? k.name : String(k); },
// --- Render platform function (for test-render.sx) ---
"render-html": function(sxSource) {
if (!Sx.renderToHtml) throw new Error("render-to-html not available — html adapter not bootstrapped");
var exprs = Sx.parseAll(sxSource);
var result = "";
for (var i = 0; i < exprs.length; i++) {
result += Sx.renderToHtml(exprs[i], env);
}
return result;
},
}; };
// --- Read and evaluate test.sx --- // --- Resolve which test specs to run ---
var src = fs.readFileSync(path.resolve(__dirname, "../ref/test.sx"), "utf8"); var refDir = path.resolve(__dirname, "../ref");
var exprs = Sx.parseAll(src); var args = process.argv.slice(2);
console.log("TAP version 13"); // Available spec modules and their platform requirements
for (var i = 0; i < exprs.length; i++) { var SPECS = {
Sx.eval(exprs[i], env); "eval": { file: "test-eval.sx", needs: [] },
"parser": { file: "test-parser.sx", needs: ["sx-parse"] },
"router": { file: "test-router.sx", needs: [] },
"render": { file: "test-render.sx", needs: ["render-html"] },
};
function evalFile(filename) {
var filepath = path.resolve(refDir, filename);
if (!fs.existsSync(filepath)) {
console.log("# SKIP " + filename + " (file not found)");
return;
}
var src = fs.readFileSync(filepath, "utf8");
var exprs = Sx.parseAll(src);
for (var i = 0; i < exprs.length; i++) {
Sx.eval(exprs[i], env);
}
}
// Legacy mode — run monolithic test.sx
if (args[0] === "--legacy") {
console.log("TAP version 13");
evalFile("test.sx");
} else {
// Determine which specs to run
var specsToRun;
if (args.length > 0) {
specsToRun = args;
} else {
// Auto-discover: run all specs whose platform functions are available
specsToRun = Object.keys(SPECS);
}
console.log("TAP version 13");
// Always load framework first
evalFile("test-framework.sx");
// Load router.sx if testing router (it defines the functions being tested)
for (var si = 0; si < specsToRun.length; si++) {
var specName = specsToRun[si];
var spec = SPECS[specName];
if (!spec) {
console.log("# SKIP unknown spec: " + specName);
continue;
}
// Check platform requirements
var canRun = true;
for (var ni = 0; ni < spec.needs.length; ni++) {
if (!(spec.needs[ni] in env)) {
console.log("# SKIP " + specName + " (missing: " + spec.needs[ni] + ")");
canRun = false;
break;
}
}
if (!canRun) continue;
// Load prerequisite spec modules
if (specName === "router") {
// Use bootstrapped router functions from sx-browser.js.
// The bare evaluator can't run router.sx faithfully because set!
// inside lambda closures doesn't propagate (dict copies, not cells).
if (Sx.splitPathSegments) {
env["split-path-segments"] = Sx.splitPathSegments;
env["parse-route-pattern"] = Sx.parseRoutePattern;
env["match-route-segments"] = Sx.matchRouteSegments;
env["match-route"] = Sx.matchRoute;
env["find-matching-route"] = Sx.findMatchingRoute;
env["make-route-segment"] = Sx.makeRouteSegment;
} else {
evalFile("router.sx");
}
}
console.log("# --- " + specName + " ---");
evalFile(spec.file);
}
} }
// --- Summary --- // --- Summary ---

View File

@@ -1,23 +1,27 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Run test.sx directly against the Python SX evaluator. """Run SX test specs against the Python SX evaluator.
The Python evaluator parses and evaluates test.sx — SX tests itself. The Python evaluator parses and evaluates test specs — SX tests itself.
This script provides only platform functions (error catching, reporting). This script provides only platform functions (error catching, reporting).
Usage: python shared/sx/tests/run.py Usage:
python shared/sx/tests/run.py # run all available specs
python shared/sx/tests/run.py eval # run only test-eval.sx
python shared/sx/tests/run.py eval parser router # run specific specs
python shared/sx/tests/run.py --legacy # run monolithic test.sx
""" """
from __future__ import annotations from __future__ import annotations
import os import os
import sys import sys
import traceback
_HERE = os.path.dirname(os.path.abspath(__file__)) _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT) sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all from shared.sx.parser import parse_all
from shared.sx.evaluator import _eval, _trampoline from shared.sx.evaluator import _eval, _trampoline, _call_lambda
from shared.sx.types import Symbol, Keyword, Lambda, NIL
# --- Test state --- # --- Test state ---
suite_stack: list[str] = [] suite_stack: list[str] = []
@@ -29,7 +33,7 @@ test_num = 0
def try_call(thunk): def try_call(thunk):
"""Call an SX thunk, catching errors.""" """Call an SX thunk, catching errors."""
try: try:
_trampoline(_eval([thunk], {})) _trampoline(_eval([thunk], env))
return {"ok": True} return {"ok": True}
except Exception as e: except Exception as e:
return {"ok": False, "error": str(e)} return {"ok": False, "error": str(e)}
@@ -60,25 +64,253 @@ def pop_suite():
suite_stack.pop() suite_stack.pop()
def main(): # --- Parser platform functions ---
env = {
"try-call": try_call,
"report-pass": report_pass,
"report-fail": report_fail,
"push-suite": push_suite,
"pop-suite": pop_suite,
}
test_sx = os.path.join(_HERE, "..", "ref", "test.sx") def sx_parse(source):
with open(test_sx) as f: """Parse SX source string into list of AST expressions."""
return parse_all(source)
def sx_serialize(val):
"""Serialize an AST value to SX source text."""
if val is None or val is NIL:
return "nil"
if isinstance(val, bool):
return "true" if val else "false"
if isinstance(val, (int, float)):
return str(val)
if isinstance(val, str):
escaped = val.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
if isinstance(val, Symbol):
return val.name
if isinstance(val, Keyword):
return f":{val.name}"
if isinstance(val, list):
inner = " ".join(sx_serialize(x) for x in val)
return f"({inner})"
if isinstance(val, dict):
parts = []
for k, v in val.items():
parts.append(f":{k}")
parts.append(sx_serialize(v))
return "{" + " ".join(parts) + "}"
return str(val)
def make_symbol(name):
return Symbol(name)
def make_keyword(name):
return Keyword(name)
def symbol_name(sym):
if isinstance(sym, Symbol):
return sym.name
return str(sym)
def keyword_name(kw):
if isinstance(kw, Keyword):
return kw.name
return str(kw)
# --- Render platform function ---
def render_html(sx_source):
"""Parse SX source and render to HTML via the bootstrapped evaluator."""
try:
from shared.sx.ref.sx_ref import render_to_html as _render_to_html
except ImportError:
raise RuntimeError("render-to-html not available — sx_ref.py not built")
exprs = parse_all(sx_source)
render_env = dict(env)
result = ""
for expr in exprs:
result += _render_to_html(expr, render_env)
return result
# --- Spec registry ---
SPECS = {
"eval": {"file": "test-eval.sx", "needs": []},
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
"router": {"file": "test-router.sx", "needs": []},
"render": {"file": "test-render.sx", "needs": ["render-html"]},
}
REF_DIR = os.path.join(_HERE, "..", "ref")
def eval_file(filename, env):
"""Load and evaluate an SX file."""
filepath = os.path.join(REF_DIR, filename)
if not os.path.exists(filepath):
print(f"# SKIP {filename} (file not found)")
return
with open(filepath) as f:
src = f.read() src = f.read()
exprs = parse_all(src) exprs = parse_all(src)
print("TAP version 13")
for expr in exprs: for expr in exprs:
_trampoline(_eval(expr, env)) _trampoline(_eval(expr, env))
# --- Build env ---
env = {
"try-call": try_call,
"report-pass": report_pass,
"report-fail": report_fail,
"push-suite": push_suite,
"pop-suite": pop_suite,
# Parser platform functions
"sx-parse": sx_parse,
"sx-serialize": sx_serialize,
"make-symbol": make_symbol,
"make-keyword": make_keyword,
"symbol-name": symbol_name,
"keyword-name": keyword_name,
# Render platform function
"render-html": render_html,
# Extra primitives needed by spec modules (router.sx, deps.sx)
"for-each-indexed": "_deferred", # replaced below
"dict-set!": "_deferred",
"dict-has?": "_deferred",
"dict-get": "_deferred",
"append!": "_deferred",
"inc": lambda n: n + 1,
}
def _call_sx(fn, args, caller_env):
"""Call an SX lambda or native function with args."""
if isinstance(fn, Lambda):
return _trampoline(_call_lambda(fn, list(args), caller_env))
return fn(*args)
def _for_each_indexed(fn, coll):
"""for-each-indexed that respects set! in lambda closures.
The hand-written evaluator copies envs on lambda calls, which breaks
set! mutation of outer scope. We eval directly in the closure dict
to match the bootstrapped semantics (cell-based mutation).
"""
if isinstance(fn, Lambda):
closure = fn.closure
for i, item in enumerate(coll or []):
# Bind params directly in the closure (no copy)
for p, v in zip(fn.params, [i, item]):
closure[p] = v
_trampoline(_eval(fn.body, closure))
else:
for i, item in enumerate(coll or []):
fn(i, item)
return NIL
def _dict_set(d, k, v):
if isinstance(d, dict):
d[k] = v
return NIL
def _dict_has(d, k):
return isinstance(d, dict) and k in d
def _dict_get(d, k):
if isinstance(d, dict):
return d.get(k, NIL)
return NIL
def _append_mut(lst, item):
if isinstance(lst, list):
lst.append(item)
return NIL
env["for-each-indexed"] = _for_each_indexed
env["dict-set!"] = _dict_set
env["dict-has?"] = _dict_has
env["dict-get"] = _dict_get
env["append!"] = _append_mut
def _load_router_from_bootstrap(env):
"""Load router functions from the bootstrapped sx_ref.py.
The hand-written evaluator can't run router.sx faithfully because
set! inside lambda closures doesn't propagate to outer scopes
(the evaluator uses dict copies, not cells). The bootstrapped code
compiles set! to cell-based mutation, so we import from there.
"""
try:
from shared.sx.ref.sx_ref import (
split_path_segments,
parse_route_pattern,
match_route_segments,
match_route,
find_matching_route,
make_route_segment,
)
env["split-path-segments"] = split_path_segments
env["parse-route-pattern"] = parse_route_pattern
env["match-route-segments"] = match_route_segments
env["match-route"] = match_route
env["find-matching-route"] = find_matching_route
env["make-route-segment"] = make_route_segment
except ImportError:
# Fallback: eval router.sx directly (may fail on set! scoping)
eval_file("router.sx", env)
def main():
global passed, failed, test_num
args = sys.argv[1:]
# Legacy mode
if args and args[0] == "--legacy":
print("TAP version 13")
eval_file("test.sx", env)
else:
# Determine which specs to run
specs_to_run = args if args else list(SPECS.keys())
print("TAP version 13")
# Always load framework first
eval_file("test-framework.sx", env)
for spec_name in specs_to_run:
spec = SPECS.get(spec_name)
if not spec:
print(f"# SKIP unknown spec: {spec_name}")
continue
# Check platform requirements
can_run = True
for need in spec["needs"]:
if need not in env:
print(f"# SKIP {spec_name} (missing: {need})")
can_run = False
break
if not can_run:
continue
# Load prerequisite spec modules
if spec_name == "router":
_load_router_from_bootstrap(env)
print(f"# --- {spec_name} ---")
eval_file(spec["file"], env)
# Summary
print() print()
print(f"1..{test_num}") print(f"1..{test_num}")
print(f"# tests {passed + failed}") print(f"# tests {passed + failed}")

View File

@@ -64,3 +64,8 @@
:params () :params ()
:returns "dict" :returns "dict"
:service "sx") :service "sx")
(define-page-helper "run-modular-tests"
:params (spec-name)
:returns "dict"
:service "sx")

View File

@@ -14,6 +14,7 @@
(dict :label "Essays" :href "/essays/") (dict :label "Essays" :href "/essays/")
(dict :label "Specs" :href "/specs/") (dict :label "Specs" :href "/specs/")
(dict :label "Bootstrappers" :href "/bootstrappers/") (dict :label "Bootstrappers" :href "/bootstrappers/")
(dict :label "Testing" :href "/testing/")
(dict :label "Isomorphism" :href "/isomorphism/") (dict :label "Isomorphism" :href "/isomorphism/")
(dict :label "Plans" :href "/plans/")))) (dict :label "Plans" :href "/plans/"))))
(<> (map (lambda (item) (<> (map (lambda (item)

View File

@@ -107,8 +107,15 @@
(dict :label "Continuations" :href "/specs/continuations") (dict :label "Continuations" :href "/specs/continuations")
(dict :label "call/cc" :href "/specs/callcc") (dict :label "call/cc" :href "/specs/callcc")
(dict :label "Deps" :href "/specs/deps") (dict :label "Deps" :href "/specs/deps")
(dict :label "Router" :href "/specs/router") (dict :label "Router" :href "/specs/router")))
(dict :label "Testing" :href "/specs/testing")))
(define testing-nav-items (list
(dict :label "Overview" :href "/testing/")
(dict :label "Evaluator" :href "/testing/eval")
(dict :label "Parser" :href "/testing/parser")
(dict :label "Router" :href "/testing/router")
(dict :label "Renderer" :href "/testing/render")
(dict :label "Runners" :href "/testing/runners")))
(define isomorphism-nav-items (list (define isomorphism-nav-items (list
(dict :label "Roadmap" :href "/isomorphism/") (dict :label "Roadmap" :href "/isomorphism/")
@@ -201,10 +208,7 @@
:prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations — frag, query, service, current-user, highlight, etc.), computes transitive IO refs through the component graph, and caches the result on each component. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side. The spec provides the classification; each host's async partial evaluator acts on it (expand IO-dependent server-side, serialize pure for client). All functions are pure — each host bootstraps them to native code via --spec-modules deps. Platform functions (component-deps, component-set-deps!, component-css-classes, component-io-refs, component-set-io-refs!, env-components, regex-find-all, scan-css-classes) are implemented natively per target.") :prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations — frag, query, service, current-user, highlight, etc.), computes transitive IO refs through the component graph, and caches the result on each component. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side. The spec provides the classification; each host's async partial evaluator acts on it (expand IO-dependent server-side, serialize pure for client). All functions are pure — each host bootstraps them to native code via --spec-modules deps. Platform functions (component-deps, component-set-deps!, component-css-classes, component-io-refs, component-set-io-refs!, env-components, regex-find-all, scan-css-classes) are implemented natively per target.")
(dict :slug "router" :filename "router.sx" :title "Router" (dict :slug "router" :filename "router.sx" :title "Router"
:desc "Client-side route matching — Flask-style pattern parsing, segment matching, route table search." :desc "Client-side route matching — Flask-style pattern parsing, segment matching, route table search."
:prose "The router module provides pure functions for matching URL paths against Flask-style route patterns (e.g. /docs/<slug>). Used by client-side routing (Phase 3) to determine if a page can be rendered locally without a server roundtrip. split-path-segments breaks a path into segments, parse-route-pattern converts patterns into typed segment descriptors, match-route-segments tests a path against a parsed pattern returning extracted params, and find-matching-route searches a route table for the first match. No platform interface needed — uses only pure string and list primitives. Bootstrapped via --spec-modules deps,router.") :prose "The router module provides pure functions for matching URL paths against Flask-style route patterns (e.g. /docs/<slug>). Used by client-side routing (Phase 3) to determine if a page can be rendered locally without a server roundtrip. split-path-segments breaks a path into segments, parse-route-pattern converts patterns into typed segment descriptors, match-route-segments tests a path against a parsed pattern returning extracted params, and find-matching-route searches a route table for the first match. No platform interface needed — uses only pure string and list primitives. Bootstrapped via --spec-modules deps,router.")))
(dict :slug "testing" :filename "test.sx" :title "Testing"
:desc "Self-hosting test framework — SX tests SX. Bootstraps to pytest and Node.js TAP."
:prose "The test spec defines a minimal test framework in SX that bootstraps to every host. Tests are written in SX and verify SX semantics — the language tests itself. The framework uses only primitives already in primitives.sx (assert, equal?, type-of, str, list, len) plus assertion helpers defined in SX (assert-equal, assert-true, assert-false, assert-nil, assert-type, assert-length, assert-contains). Two bootstrap compilers read test.sx and emit native test files: bootstrap_test.py produces a pytest module, bootstrap_test_js.py produces a Node.js TAP script. The same 81 tests run on both platforms, verifying cross-host parity.")))
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat extension-spec-items module-spec-items))))) (define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat extension-spec-items module-spec-items)))))

View File

@@ -1,246 +1,288 @@
;; Testing spec page — SX tests SX. ;; Testing section — SX tests SX across every host.
;; Modular test specs: eval, parser, router, render.
;; Each page shows spec source, runs server-side, offers browser-side run button.
(defcomp ~spec-testing-content (&key spec-source server-results) ;; ---------------------------------------------------------------------------
;; Overview page
;; ---------------------------------------------------------------------------
(defcomp ~testing-overview-content (&key server-results framework-source eval-source parser-source router-source render-source)
(~doc-page :title "Testing" (~doc-page :title "Testing"
(div :class "space-y-8" (div :class "space-y-8"
;; Intro ;; Intro
(div :class "space-y-4" (div :class "space-y-4"
(p :class "text-lg text-stone-600" (p :class "text-lg text-stone-600"
"SX tests itself. " "SX tests itself. Test specs are written in SX and executed by SX evaluators on every host. "
(code :class "text-violet-700 text-sm" "test.sx") "The same assertions run in Python, Node.js, and in the browser — from the same source files.")
" is a self-executing test spec — it defines " (p :class "text-stone-600"
"The framework defines two macros ("
(code :class "text-violet-700 text-sm" "deftest") (code :class "text-violet-700 text-sm" "deftest")
" and " " and "
(code :class "text-violet-700 text-sm" "defsuite") (code :class "text-violet-700 text-sm" "defsuite")
" as macros, writes 81 test cases, and runs them. Any host that provides five platform functions can evaluate the file directly.") ") and nine assertion helpers, all in SX. Each host provides only "
(p :class "text-stone-600" (strong "five platform functions")
"This is not a test " " — everything else is pure SX."))
(em "of") " SX — it is a test " (em "in") " SX. The same s-expressions that define how "
(code :class "text-violet-700 text-sm" "if")
" works are used to verify that "
(code :class "text-violet-700 text-sm" "if")
" works. No code generation, no intermediate files — the evaluator runs the spec."))
;; Server-side results (ran when this page was rendered) ;; Architecture
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Server: Python evaluator")
(p :class "text-stone-600"
"The Python evaluator ran "
(code :class "text-violet-700 text-sm" "test.sx")
" when this page loaded — "
(strong (str (get server-results "passed") "/" (get server-results "total") " passed"))
" in " (str (get server-results "elapsed-ms")) "ms.")
(pre :class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
(get server-results "output")))
;; Client-side test runner
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Browser: JavaScript evaluator")
(p :class "text-stone-600"
"This page loaded "
(code :class "text-violet-700 text-sm" "sx-browser.js")
" to render itself. The same evaluator can run "
(code :class "text-violet-700 text-sm" "test.sx")
" right here:")
(div :class "flex items-center gap-4"
(button :id "test-btn"
:class "px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 cursor-pointer"
:onclick "sxRunTests('test-sx-source','test-output','test-btn')"
"Run 81 tests"))
(pre :id "test-output"
:class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
:style "display:none"
"")
;; Hidden: raw test.sx source for the browser runner
(textarea :id "test-sx-source" :style "display:none" spec-source)
;; Load the test runner script
(script :src (asset-url "/scripts/sx-test-runner.js")))
;; How it works
(div :class "space-y-3" (div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Architecture") (h2 :class "text-2xl font-semibold text-stone-800" "Architecture")
(p :class "text-stone-600"
"The test framework needs five platform functions. Everything else — macros, assertion helpers, test suites — is pure SX:")
(div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl" (div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono text-stone-700" (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono text-stone-700"
"test.sx Self-executing: macros + helpers + 81 tests "test-framework.sx Macros + assertion helpers (loaded first)
| |
|--- browser sx-browser.js evaluates test.sx in this page +-- test-eval.sx 81 tests: evaluator + primitives
| +-- test-parser.sx 39 tests: tokenizer, parser, serializer
|--- run.js Injects 5 platform fns, evaluates test.sx +-- test-router.sx 18 tests: route matching + param extraction
| | +-- test-render.sx 23 tests: HTML rendering + components
| +-> sx-browser.js JS evaluator (bootstrapped from spec)
|
|--- run.py Injects 5 platform fns, evaluates test.sx
|
+-> evaluator.py Python evaluator
Platform functions (5 total — everything else is pure SX): Runners:
run.js Node.js — injects platform fns, runs specs
run.py Python — injects platform fns, runs specs
sx-test-runner.js Browser — runs specs in this page
Platform functions (5 total):
try-call (thunk) -> {:ok true} | {:ok false :error \"msg\"} try-call (thunk) -> {:ok true} | {:ok false :error \"msg\"}
report-pass (name) -> output pass report-pass (name) -> output pass
report-fail (name error) -> output fail report-fail (name error) -> output fail
push-suite (name) -> push suite context push-suite (name) -> push suite context
pop-suite () -> pop suite context"))) pop-suite () -> pop suite context
;; Framework Per-spec platform functions:
(div :class "space-y-3" parser: sx-parse, sx-serialize, make-symbol, make-keyword, ...
(h2 :class "text-2xl font-semibold text-stone-800" "The test framework") router: (none — pure spec, uses bootstrapped functions)
(p :class "text-stone-600" render: render-html (wraps parse + render-to-html)")))
"The framework defines two macros and nine assertion helpers, all in SX. The macros are the key — they make "
(code :class "text-violet-700 text-sm" "defsuite")
" and "
(code :class "text-violet-700 text-sm" "deftest")
" executable forms, not just declarations:")
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Macros")
(~doc-code :code
(highlight "(defmacro deftest (name &rest body)\n `(let ((result (try-call (fn () ,@body))))\n (if (get result \"ok\")\n (report-pass ,name)\n (report-fail ,name (get result \"error\")))))\n\n(defmacro defsuite (name &rest items)\n `(do (push-suite ,name)\n ,@items\n (pop-suite)))" "lisp")))
(p :class "text-stone-600 text-sm"
(code :class "text-violet-700 text-sm" "deftest")
" wraps the body in a thunk, passes it to "
(code :class "text-violet-700 text-sm" "try-call")
" (the one platform function that catches errors), then reports pass or fail. "
(code :class "text-violet-700 text-sm" "defsuite")
" pushes a name onto the context stack, runs its children, and pops.")
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Assertion helpers")
(~doc-code :code
(highlight "(define assert-equal\n (fn (expected actual)\n (assert (equal? expected actual)\n (str \"Expected \" (str expected) \" but got \" (str actual)))))\n\n(define assert-true (fn (val) (assert val ...)))\n(define assert-false (fn (val) (assert (not val) ...)))\n(define assert-nil (fn (val) (assert (nil? val) ...)))\n(define assert-type (fn (expected-type val) ...))\n(define assert-length (fn (expected-len col) ...))\n(define assert-contains (fn (item col) ...))\n(define assert-throws (fn (thunk) ...))" "lisp"))))
;; Example tests ;; Server results
(div :class "space-y-3" (when server-results
(h2 :class "text-2xl font-semibold text-stone-800" "Example: SX testing SX") (div :class "space-y-3"
(p :class "text-stone-600" (h2 :class "text-2xl font-semibold text-stone-800" "Server: Python evaluator")
"The test suites cover every language feature. Here is the arithmetic suite testing the evaluator's arithmetic primitives:") (p :class "text-stone-600"
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" "Ran all test specs when this page loaded — "
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "From test.sx") (strong (str (get server-results "passed") "/" (get server-results "total") " passed"))
(~doc-code :code " in " (str (get server-results "elapsed-ms")) "ms.")
(highlight "(defsuite \"arithmetic\"\n (deftest \"addition\"\n (assert-equal 3 (+ 1 2))\n (assert-equal 0 (+ 0 0))\n (assert-equal -1 (+ 1 -2))\n (assert-equal 10 (+ 1 2 3 4)))\n\n (deftest \"subtraction\"\n (assert-equal 1 (- 3 2))\n (assert-equal -1 (- 2 3)))\n\n (deftest \"multiplication\"\n (assert-equal 6 (* 2 3))\n (assert-equal 0 (* 0 100))\n (assert-equal 24 (* 1 2 3 4)))\n\n (deftest \"division\"\n (assert-equal 2 (/ 6 3))\n (assert-equal 2.5 (/ 5 2)))\n\n (deftest \"modulo\"\n (assert-equal 1 (mod 7 3))\n (assert-equal 0 (mod 6 3))))" "lisp")))) (pre :class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
(get server-results "output"))))
;; Running tests — JS ;; Browser test runner
(div :class "space-y-3" (div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "JavaScript: direct evaluation") (h2 :class "text-2xl font-semibold text-stone-800" "Browser: JavaScript evaluator")
(p :class "text-stone-600" (p :class "text-stone-600"
"Run all test specs in the browser using "
(code :class "text-violet-700 text-sm" "sx-browser.js") (code :class "text-violet-700 text-sm" "sx-browser.js")
" evaluates " ":")
(code :class "text-violet-700 text-sm" "test.sx") (div :class "flex items-center gap-4"
" directly. The runner injects platform functions and calls " (button :id "test-btn-all"
(code :class "text-violet-700 text-sm" "Sx.eval") :class "px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 cursor-pointer"
" on each parsed expression:") :onclick "sxRunModularTests('all','test-output-all','test-btn-all')"
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" "Run all tests"))
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "run.js") (pre :id "test-output-all"
(~doc-code :code :class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
(highlight "var Sx = require('./sx-browser.js');\nvar src = fs.readFileSync('test.sx', 'utf8');\n\nvar env = {\n 'try-call': function(thunk) {\n try {\n Sx.eval([thunk], env); // call the SX lambda\n return { ok: true };\n } catch(e) {\n return { ok: false, error: e.message };\n }\n },\n 'report-pass': function(name) { console.log('ok - ' + name); },\n 'report-fail': function(name, err) { console.log('not ok - ' + name); },\n 'push-suite': function(n) { stack.push(n); },\n 'pop-suite': function() { stack.pop(); },\n};\n\nvar exprs = Sx.parseAll(src);\nfor (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);" "javascript"))) :style "display:none"
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" "")
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Output") ;; Hidden: all spec sources for the browser runner
(~doc-code :code (textarea :id "test-framework-source" :style "display:none" framework-source)
(highlight "$ node shared/sx/tests/run.js\nTAP version 13\nok 1 - literals > numbers are numbers\nok 2 - literals > strings are strings\n...\nok 81 - edge-cases > empty operations\n\n# tests 81\n# pass 81" "bash")))) (textarea :id "test-spec-eval" :style "display:none" eval-source)
(textarea :id "test-spec-parser" :style "display:none" parser-source)
(textarea :id "test-spec-router" :style "display:none" router-source)
(textarea :id "test-spec-render" :style "display:none" render-source)
(script :src (asset-url "/scripts/sx-test-runner.js")))
;; Running tests — Python ;; Test spec index
(div :class "space-y-3" (div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Python: direct evaluation") (h2 :class "text-2xl font-semibold text-stone-800" "Test specs")
(p :class "text-stone-600" (div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
"Same approach — the Python evaluator runs " (a :href "/testing/eval" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(code :class "text-violet-700 text-sm" "test.sx") (h3 :class "font-semibold text-stone-800" "Evaluator")
" directly:") (p :class "text-sm text-stone-500" "81 tests — literals, arithmetic, strings, lists, dicts, special forms, lambdas, components, macros")
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" (p :class "text-xs text-violet-600 mt-1" "test-eval.sx"))
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "run.py") (a :href "/testing/parser" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(~doc-code :code (h3 :class "font-semibold text-stone-800" "Parser")
(highlight "from shared.sx.parser import parse_all\nfrom shared.sx.evaluator import _eval, _trampoline\n\ndef try_call(thunk):\n try:\n _trampoline(_eval([thunk], {}))\n return {'ok': True}\n except Exception as e:\n return {'ok': False, 'error': str(e)}\n\nenv = {\n 'try-call': try_call,\n 'report-pass': report_pass,\n 'report-fail': report_fail,\n 'push-suite': push_suite,\n 'pop-suite': pop_suite,\n}\n\nfor expr in parse_all(src):\n _trampoline(_eval(expr, env))" "python"))) (p :class "text-sm text-stone-500" "39 tests — tokenization, parsing, escape sequences, quote sugar, serialization, round-trips")
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" (p :class "text-xs text-violet-600 mt-1" "test-parser.sx"))
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Output") (a :href "/testing/router" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(~doc-code :code (h3 :class "font-semibold text-stone-800" "Router")
(highlight "$ python shared/sx/tests/run.py\nTAP version 13\nok 1 - literals > numbers are numbers\n...\nok 81 - edge-cases > empty operations\n\n# tests 81\n# pass 81" "bash")))) (p :class "text-sm text-stone-500" "18 tests — path splitting, pattern parsing, segment matching, parameter extraction")
(p :class "text-xs text-violet-600 mt-1" "test-router.sx"))
(a :href "/testing/render" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
(h3 :class "font-semibold text-stone-800" "Renderer")
(p :class "text-sm text-stone-500" "23 tests — elements, attributes, void elements, fragments, escaping, control flow, components")
(p :class "text-xs text-violet-600 mt-1" "test-render.sx"))))
;; What it proves ;; What it proves
(div :class "rounded-lg border border-blue-200 bg-blue-50 p-5 space-y-3" (div :class "rounded-lg border border-blue-200 bg-blue-50 p-5 space-y-3"
(h2 :class "text-lg font-semibold text-blue-900" "What this proves") (h2 :class "text-lg font-semibold text-blue-900" "What this proves")
(ol :class "list-decimal list-inside text-blue-800 space-y-2 text-sm" (ol :class "list-decimal list-inside text-blue-800 space-y-2 text-sm"
(li "The test spec is " (strong "written in SX") " and " (strong "executed by SX") " — no code generation") (li "Test specs are " (strong "written in SX") " and " (strong "executed by SX") " — no code generation")
(li "The same 81 tests run on " (strong "Python, Node.js, and in the browser") " from the same file") (li "The same tests run on " (strong "Python, Node.js, and in the browser") " from the same files")
(li "Each host provides only " (strong "5 platform functions") " — everything else is pure SX") (li "Each host provides only " (strong "5 platform functions") " — everything else is pure SX")
(li "Adding a new host means implementing 5 functions, not rewriting tests") (li "Modular specs test each part independently — evaluator, parser, router, renderer")
(li "Platform divergences (truthiness of 0, [], \"\") are " (strong "documented, not hidden")) (li "Per-spec platform functions extend the 5-function contract for module-specific capabilities")
(li "The spec is " (strong "executable") " — click the button above to prove it"))) (li "Platform divergences are " (strong "exposed by the tests") ", not hidden"))))))
;; Test suites
;; ---------------------------------------------------------------------------
;; Per-spec test page (reusable for eval, parser, router, render)
;; ---------------------------------------------------------------------------
(defcomp ~testing-spec-content (&key spec-name spec-title spec-desc spec-source framework-source server-results)
(~doc-page :title spec-title
(div :class "space-y-8"
;; Description
(div :class "space-y-4"
(p :class "text-lg text-stone-600" spec-desc))
;; Server-side results
(when server-results
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Server: Python evaluator")
(p :class "text-stone-600"
"Ran "
(code :class "text-violet-700 text-sm" (str "test-" spec-name ".sx"))
" when this page loaded — "
(strong (str (get server-results "passed") "/" (get server-results "total") " passed"))
" in " (str (get server-results "elapsed-ms")) "ms.")
(pre :class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
(get server-results "output"))))
;; Browser test runner
(div :class "space-y-3" (div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "All 15 test suites") (h2 :class "text-2xl font-semibold text-stone-800" "Browser: JavaScript evaluator")
(div :class "overflow-x-auto rounded border border-stone-200" (p :class "text-stone-600"
(table :class "w-full text-left text-sm" "Run this spec in the browser:")
(thead (tr :class "border-b border-stone-200 bg-stone-100" (div :class "flex items-center gap-4"
(th :class "px-3 py-2 font-medium text-stone-600" "Suite") (button :id (str "test-btn-" spec-name)
(th :class "px-3 py-2 font-medium text-stone-600" "Tests") :class "px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 cursor-pointer"
(th :class "px-3 py-2 font-medium text-stone-600" "Covers"))) :onclick (str "sxRunModularTests('" spec-name "','test-output-" spec-name "','test-btn-" spec-name "')")
(tbody (str "Run " spec-title)))
(tr :class "border-b border-stone-100" (pre :id (str "test-output-" spec-name)
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "literals") :class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
(td :class "px-3 py-2" "6") :style "display:none"
(td :class "px-3 py-2 text-stone-700" "number, string, boolean, nil, list, dict type checking")) "")
(tr :class "border-b border-stone-100" ;; Hidden: spec source + framework source for the browser runner
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "arithmetic") (textarea :id (str "test-spec-" spec-name) :style "display:none" spec-source)
(td :class "px-3 py-2" "5") (textarea :id "test-framework-source" :style "display:none" framework-source)
(td :class "px-3 py-2 text-stone-700" "+, -, *, /, mod with edge cases")) (script :src (asset-url "/scripts/sx-test-runner.js")))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "comparison")
(td :class "px-3 py-2" "3")
(td :class "px-3 py-2 text-stone-700" "=, equal?, <, >, <=, >="))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "strings")
(td :class "px-3 py-2" "7")
(td :class "px-3 py-2 text-stone-700" "str, string-length, substring, contains?, upcase, trim, split/join"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "lists")
(td :class "px-3 py-2" "10")
(td :class "px-3 py-2 text-stone-700" "first, rest, nth, last, cons, append, reverse, empty?, contains?, flatten"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "dicts")
(td :class "px-3 py-2" "6")
(td :class "px-3 py-2 text-stone-700" "literals, get, assoc, dissoc, keys/vals, has-key?, merge"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "predicates")
(td :class "px-3 py-2" "7")
(td :class "px-3 py-2 text-stone-700" "nil?, number?, string?, list?, dict?, boolean?, not"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "special-forms")
(td :class "px-3 py-2" "10")
(td :class "px-3 py-2 text-stone-700" "if, when, cond, and, or, let, let (Clojure), do/begin, define, set!"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "lambdas")
(td :class "px-3 py-2" "5")
(td :class "px-3 py-2 text-stone-700" "basic, closures, as argument, recursion, higher-order returns"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "higher-order")
(td :class "px-3 py-2" "6")
(td :class "px-3 py-2 text-stone-700" "map, filter, reduce, some, every?, map-indexed"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "components")
(td :class "px-3 py-2" "4")
(td :class "px-3 py-2 text-stone-700" "defcomp, &key params, &rest children, defaults"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "macros")
(td :class "px-3 py-2" "3")
(td :class "px-3 py-2 text-stone-700" "defmacro, quasiquote/unquote, splice-unquote"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "threading")
(td :class "px-3 py-2" "1")
(td :class "px-3 py-2 text-stone-700" "-> thread-first macro"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "truthiness")
(td :class "px-3 py-2" "2")
(td :class "px-3 py-2 text-stone-700" "truthy/falsy values (platform-universal subset)"))
(tr
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "edge-cases")
(td :class "px-3 py-2" "6")
(td :class "px-3 py-2 text-stone-700" "nested scoping, recursive map, keywords, dict eval, nil propagation, empty ops"))))))
;; Full source ;; Full source
(div :class "space-y-3" (div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Full specification source") (h2 :class "text-2xl font-semibold text-stone-800" "Specification source")
(p :class "text-xs text-stone-400 italic" (p :class "text-xs text-stone-400 italic"
"The s-expression source below is the canonical test specification. " (str "test-" spec-name ".sx")
"Any host that implements the five platform functions can evaluate it directly.") " — the canonical test specification for this module.")
(div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl" (div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words"
(code (highlight spec-source "sx")))))))) (code (highlight spec-source "sx"))))))))
;; ---------------------------------------------------------------------------
;; Runners page
;; ---------------------------------------------------------------------------
(defcomp ~testing-runners-content ()
(~doc-page :title "Test Runners"
(div :class "space-y-8"
(div :class "space-y-4"
(p :class "text-lg text-stone-600"
"Three runners execute the same test specs on different hosts. Each injects the five platform functions and any per-spec extensions, then evaluates the SX source directly.")
(p :class "text-stone-600"
"All runners produce "
(a :href "https://testanything.org/" :class "text-violet-700 underline" "TAP")
" output and accept module names as arguments."))
;; Node.js runner
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Node.js: run.js")
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Usage")
(~doc-code :code
(highlight "# Run all specs\nnode shared/sx/tests/run.js\n\n# Run specific specs\nnode shared/sx/tests/run.js eval parser\n\n# Legacy mode (monolithic test.sx)\nnode shared/sx/tests/run.js --legacy" "bash")))
(p :class "text-stone-600 text-sm"
"Uses "
(code :class "text-violet-700 text-sm" "sx-browser.js")
" as the evaluator. Router tests use bootstrapped functions from "
(code :class "text-violet-700 text-sm" "Sx.splitPathSegments")
" etc. Render tests use "
(code :class "text-violet-700 text-sm" "Sx.renderToHtml")
"."))
;; Python runner
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Python: run.py")
(div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3"
(h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Usage")
(~doc-code :code
(highlight "# Run all specs\npython shared/sx/tests/run.py\n\n# Run specific specs\npython shared/sx/tests/run.py eval parser\n\n# Legacy mode (monolithic test.sx)\npython shared/sx/tests/run.py --legacy" "bash")))
(p :class "text-stone-600 text-sm"
"Uses the hand-written Python evaluator ("
(code :class "text-violet-700 text-sm" "evaluator.py")
"). Router tests import bootstrapped functions from "
(code :class "text-violet-700 text-sm" "sx_ref.py")
" because the hand-written evaluator's "
(code :class "text-violet-700 text-sm" "set!")
" doesn't propagate across lambda closure copies."))
;; Browser runner
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Browser: sx-test-runner.js")
(p :class "text-stone-600"
"Runs in the browser on each test page. The spec source is embedded in a hidden textarea; the runner parses and evaluates it using the same "
(code :class "text-violet-700 text-sm" "sx-browser.js")
" that renders the page itself.")
(p :class "text-stone-600"
"This is the strongest proof of cross-host parity — the browser evaluator that users depend on is the same one running the tests."))
;; Platform functions table
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Platform functions")
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Function")
(th :class "px-3 py-2 font-medium text-stone-600" "Scope")
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "try-call")
(td :class "px-3 py-2" "All specs")
(td :class "px-3 py-2 text-stone-700" "Call a thunk, catch errors, return result dict"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "report-pass")
(td :class "px-3 py-2" "All specs")
(td :class "px-3 py-2 text-stone-700" "Report a passing test"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "report-fail")
(td :class "px-3 py-2" "All specs")
(td :class "px-3 py-2 text-stone-700" "Report a failing test with error message"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "push-suite")
(td :class "px-3 py-2" "All specs")
(td :class "px-3 py-2 text-stone-700" "Push suite name onto context stack"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pop-suite")
(td :class "px-3 py-2" "All specs")
(td :class "px-3 py-2 text-stone-700" "Pop suite name from context stack"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx-parse")
(td :class "px-3 py-2" "Parser")
(td :class "px-3 py-2 text-stone-700" "Parse SX source to AST"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx-serialize")
(td :class "px-3 py-2" "Parser")
(td :class "px-3 py-2 text-stone-700" "Serialize AST back to SX source text"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "render-html")
(td :class "px-3 py-2" "Renderer")
(td :class "px-3 py-2 text-stone-700" "Parse SX source and render to HTML string"))))))
;; Adding a new host
(div :class "rounded-lg border border-blue-200 bg-blue-50 p-5 space-y-3"
(h2 :class "text-lg font-semibold text-blue-900" "Adding a new host")
(ol :class "list-decimal list-inside text-blue-800 space-y-2 text-sm"
(li "Implement the 5 platform functions in your target language")
(li "Load " (code :class "text-sm" "test-framework.sx") " — it defines deftest/defsuite macros")
(li "Load the spec file(s) — the evaluator runs the tests automatically")
(li "Add per-spec platform functions if testing parser or renderer")
(li "Compare TAP output across hosts — divergences reveal platform-specific semantics"))))))

View File

@@ -344,9 +344,6 @@
:filename (get item "filename") :href (str "/specs/" (get item "slug")) :filename (get item "filename") :href (str "/specs/" (get item "slug"))
:source (read-spec-file (get item "filename")))) :source (read-spec-file (get item "filename"))))
extension-spec-items)) extension-spec-items))
"testing" (~spec-testing-content
:spec-source (read-spec-file "test.sx")
:server-results (run-spec-tests))
:else (let ((spec (find-spec slug))) :else (let ((spec (find-spec slug)))
(if spec (if spec
(~spec-detail-content (~spec-detail-content
@@ -516,3 +513,74 @@
"glue-decoupling" (~plan-glue-decoupling-content) "glue-decoupling" (~plan-glue-decoupling-content)
"social-sharing" (~plan-social-sharing-content) "social-sharing" (~plan-social-sharing-content)
:else (~plans-index-content))) :else (~plans-index-content)))
;; ---------------------------------------------------------------------------
;; Testing section
;; ---------------------------------------------------------------------------
(defpage testing-index
:path "/testing/"
:auth :public
:layout (:sx-section
:section "Testing"
:sub-label "Testing"
:sub-href "/testing/"
:sub-nav (~section-nav :items testing-nav-items :current "Overview")
:selected "Overview")
:data (run-modular-tests "all")
:content (~testing-overview-content
:server-results server-results
:framework-source (read-spec-file "test-framework.sx")
:eval-source (read-spec-file "test-eval.sx")
:parser-source (read-spec-file "test-parser.sx")
:router-source (read-spec-file "test-router.sx")
:render-source (read-spec-file "test-render.sx")))
(defpage testing-page
:path "/testing/<slug>"
:auth :public
:layout (:sx-section
:section "Testing"
:sub-label "Testing"
:sub-href "/testing/"
:sub-nav (~section-nav :items testing-nav-items
:current (find-current testing-nav-items slug))
:selected (or (find-current testing-nav-items slug) ""))
:data (case slug
"eval" (run-modular-tests "eval")
"parser" (run-modular-tests "parser")
"router" (run-modular-tests "router")
"render" (run-modular-tests "render")
:else (dict))
:content (case slug
"eval" (~testing-spec-content
:spec-name "eval"
:spec-title "Evaluator Tests"
:spec-desc "81 tests covering the core evaluator and all primitives — literals, arithmetic, comparison, strings, lists, dicts, predicates, special forms, lambdas, higher-order functions, components, macros, threading, and edge cases."
:spec-source (read-spec-file "test-eval.sx")
:framework-source (read-spec-file "test-framework.sx")
:server-results server-results)
"parser" (~testing-spec-content
:spec-name "parser"
:spec-title "Parser Tests"
:spec-desc "39 tests covering tokenization and parsing — integers, floats, strings, escape sequences, booleans, nil, keywords, symbols, lists, dicts, whitespace, comments, quote sugar, serialization, and round-trips."
:spec-source (read-spec-file "test-parser.sx")
:framework-source (read-spec-file "test-framework.sx")
:server-results server-results)
"router" (~testing-spec-content
:spec-name "router"
:spec-title "Router Tests"
:spec-desc "18 tests covering client-side route matching — path splitting, pattern parsing, segment matching, parameter extraction, and route table search."
:spec-source (read-spec-file "test-router.sx")
:framework-source (read-spec-file "test-framework.sx")
:server-results server-results)
"render" (~testing-spec-content
:spec-name "render"
:spec-title "Renderer Tests"
:spec-desc "23 tests covering HTML rendering — elements, attributes, void elements, boolean attributes, fragments, escaping, control flow, and component rendering."
:spec-source (read-spec-file "test-render.sx")
:framework-source (read-spec-file "test-framework.sx")
:server-results server-results)
"runners" (~testing-runners-content)
:else (~testing-overview-content
:server-results server-results)))

View File

@@ -25,6 +25,7 @@ def _register_sx_helpers() -> None:
"routing-analyzer-data": _routing_analyzer_data, "routing-analyzer-data": _routing_analyzer_data,
"data-test-data": _data_test_data, "data-test-data": _data_test_data,
"run-spec-tests": _run_spec_tests, "run-spec-tests": _run_spec_tests,
"run-modular-tests": _run_modular_tests,
}) })
@@ -562,6 +563,199 @@ def _run_spec_tests() -> dict:
} }
def _run_modular_tests(spec_name: str) -> dict:
"""Run modular test specs against the Python SX evaluator.
spec_name: "eval", "parser", "router", "render", or "all".
Returns dict with server-results key containing results per spec.
"""
import os
import time
from shared.sx.parser import parse_all
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.types import Symbol, Keyword, Lambda, NIL
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
if not os.path.isdir(ref_dir):
ref_dir = "/app/shared/sx/ref"
suite_stack: list[str] = []
passed = 0
failed = 0
test_num = 0
lines: list[str] = []
def try_call(thunk):
try:
_trampoline(_eval([thunk], env))
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
def report_pass(name):
nonlocal passed, test_num
test_num += 1
passed += 1
lines.append("ok " + str(test_num) + " - " + " > ".join(suite_stack + [name]))
def report_fail(name, error):
nonlocal failed, test_num
test_num += 1
failed += 1
full = " > ".join(suite_stack + [name])
lines.append("not ok " + str(test_num) + " - " + full)
lines.append(" # " + str(error))
def push_suite(name):
suite_stack.append(name)
def pop_suite():
suite_stack.pop()
def sx_parse(source):
return parse_all(source)
def sx_serialize(val):
if val is None or val is NIL:
return "nil"
if isinstance(val, bool):
return "true" if val else "false"
if isinstance(val, (int, float)):
return str(val)
if isinstance(val, str):
escaped = val.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
if isinstance(val, Symbol):
return val.name
if isinstance(val, Keyword):
return f":{val.name}"
if isinstance(val, list):
inner = " ".join(sx_serialize(x) for x in val)
return f"({inner})"
if isinstance(val, dict):
parts = []
for k, v in val.items():
parts.append(f":{k}")
parts.append(sx_serialize(v))
return "{" + " ".join(parts) + "}"
return str(val)
def render_html(sx_source):
try:
from shared.sx.ref.sx_ref import render_to_html as _render_to_html
except ImportError:
return "<!-- render-to-html not available -->"
exprs = parse_all(sx_source)
render_env = dict(env)
result = ""
for expr in exprs:
result += _render_to_html(expr, render_env)
return result
def _call_sx(fn, args, caller_env):
if isinstance(fn, Lambda):
from shared.sx.evaluator import _call_lambda
return _trampoline(_call_lambda(fn, list(args), caller_env))
return fn(*args)
def _for_each_indexed(fn, coll):
if isinstance(fn, Lambda):
closure = fn.closure
for i, item in enumerate(coll or []):
for p, v in zip(fn.params, [i, item]):
closure[p] = v
_trampoline(_eval(fn.body, closure))
else:
for i, item in enumerate(coll or []):
fn(i, item)
return NIL
env = {
"try-call": try_call,
"report-pass": report_pass,
"report-fail": report_fail,
"push-suite": push_suite,
"pop-suite": pop_suite,
"sx-parse": sx_parse,
"sx-serialize": sx_serialize,
"make-symbol": lambda name: Symbol(name),
"make-keyword": lambda name: Keyword(name),
"symbol-name": lambda sym: sym.name if isinstance(sym, Symbol) else str(sym),
"keyword-name": lambda kw: kw.name if isinstance(kw, Keyword) else str(kw),
"render-html": render_html,
"for-each-indexed": _for_each_indexed,
"dict-set!": lambda d, k, v: (d.__setitem__(k, v), NIL)[-1] if isinstance(d, dict) else NIL,
"dict-has?": lambda d, k: isinstance(d, dict) and k in d,
"dict-get": lambda d, k: d.get(k, NIL) if isinstance(d, dict) else NIL,
"append!": lambda lst, item: (lst.append(item), NIL)[-1] if isinstance(lst, list) else NIL,
"inc": lambda n: n + 1,
}
def eval_file(filename):
filepath = os.path.join(ref_dir, filename)
if not os.path.exists(filepath):
return
with open(filepath) as f:
src = f.read()
exprs = parse_all(src)
for expr in exprs:
_trampoline(_eval(expr, env))
SPECS = {
"eval": {"file": "test-eval.sx", "needs": []},
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
"router": {"file": "test-router.sx", "needs": []},
"render": {"file": "test-render.sx", "needs": ["render-html"]},
}
specs_to_run = list(SPECS.keys()) if spec_name == "all" else [spec_name]
t0 = time.monotonic()
# Load framework
eval_file("test-framework.sx")
for sn in specs_to_run:
spec = SPECS.get(sn)
if not spec:
continue
# Load router from bootstrap if needed
if sn == "router":
try:
from shared.sx.ref.sx_ref import (
split_path_segments,
parse_route_pattern,
match_route_segments,
match_route,
find_matching_route,
make_route_segment,
)
env["split-path-segments"] = split_path_segments
env["parse-route-pattern"] = parse_route_pattern
env["match-route-segments"] = match_route_segments
env["match-route"] = match_route
env["find-matching-route"] = find_matching_route
env["make-route-segment"] = make_route_segment
except ImportError:
eval_file("router.sx")
eval_file(spec["file"])
elapsed = round((time.monotonic() - t0) * 1000)
return {
"server-results": {
"passed": passed,
"failed": failed,
"total": passed + failed,
"elapsed-ms": elapsed,
"output": "\n".join(lines),
"spec": spec_name,
}
}
def _data_test_data() -> dict: def _data_test_data() -> dict:
"""Return test data for the client-side data rendering test page. """Return test data for the client-side data rendering test page.