Merge branch 'worktree-iso-phase-4' into macros
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m49s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m49s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// sx-test-runner.js — Run test.sx in the browser using sx-browser.js.
|
||||
// Loaded on the /specs/testing page. Uses the Sx global.
|
||||
// sx-test-runner.js — Run SX test specs in the browser using sx-browser.js.
|
||||
// Supports both legacy (monolithic test.sx) and modular (per-spec) modes.
|
||||
(function() {
|
||||
var NIL = Sx.NIL;
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
@@ -21,13 +21,9 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
window.sxRunTests = function(srcId, outId, btnId) {
|
||||
var src = document.getElementById(srcId).textContent;
|
||||
var out = document.getElementById(outId);
|
||||
var btn = document.getElementById(btnId);
|
||||
|
||||
// --- Platform functions shared across all specs ---
|
||||
function makeEnv() {
|
||||
var stack = [], passed = 0, failed = 0, num = 0, lines = [];
|
||||
|
||||
var env = {
|
||||
"try-call": function(thunk) {
|
||||
try {
|
||||
@@ -49,6 +45,7 @@
|
||||
"push-suite": function(name) { stack.push(name); },
|
||||
"pop-suite": function() { stack.pop(); },
|
||||
|
||||
// Primitives that sx-browser.js may not expose in env
|
||||
"equal?": function(a, b) { return deepEqual(a, b); },
|
||||
"eq?": function(a, b) { return a === b; },
|
||||
"boolean?": function(x) { return typeof x === "boolean"; },
|
||||
@@ -68,28 +65,159 @@
|
||||
},
|
||||
"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]); },
|
||||
"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 {
|
||||
var t0 = performance.now();
|
||||
var exprs = Sx.parseAll(src);
|
||||
for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);
|
||||
evalSource(src, ctx.env);
|
||||
var elapsed = Math.round(performance.now() - t0);
|
||||
lines.push("");
|
||||
lines.push("1.." + num);
|
||||
lines.push("# tests " + (passed + failed));
|
||||
lines.push("# pass " + passed);
|
||||
if (failed > 0) lines.push("# fail " + failed);
|
||||
lines.push("# time " + elapsed + "ms");
|
||||
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) {
|
||||
lines.push("");
|
||||
lines.push("FATAL: " + (e.message || String(e)));
|
||||
var r = ctx.getResults();
|
||||
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";
|
||||
btn.textContent = passed + "/" + (passed + failed) + " passed" + (failed === 0 ? "" : " (" + failed + " failed)");
|
||||
btn.className = failed > 0
|
||||
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-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-green-600 text-white font-medium text-sm cursor-default";
|
||||
};
|
||||
|
||||
494
shared/sx/ref/test-eval.sx
Normal file
494
shared/sx/ref/test-eval.sx
Normal 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))))
|
||||
86
shared/sx/ref/test-framework.sx
Normal file
86
shared/sx/ref/test-framework.sx
Normal 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"))))
|
||||
222
shared/sx/ref/test-parser.sx
Normal file
222
shared/sx/ref/test-parser.sx
Normal 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))"))))))
|
||||
167
shared/sx/ref/test-render.sx
Normal file
167
shared/sx/ref/test-render.sx
Normal 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 "<script>"))))
|
||||
|
||||
(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>")))))
|
||||
125
shared/sx/ref/test-router.sx
Normal file
125
shared/sx/ref/test-router.sx
Normal 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"))))))
|
||||
@@ -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
|
||||
;; proves its own correctness. The framework is self-executing: any host
|
||||
;; that provides 5 platform functions can evaluate this file directly.
|
||||
;; This file includes the test framework and core eval tests inline.
|
||||
;; It exists for backward compatibility — runners that load "test.sx"
|
||||
;; 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:
|
||||
;; 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
|
||||
;; 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
|
||||
;;
|
||||
;; Usage:
|
||||
;; ;; Host injects platform functions into env, then:
|
||||
|
||||
@@ -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).
|
||||
//
|
||||
// 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 });
|
||||
var path = require("path");
|
||||
@@ -86,15 +90,149 @@ var env = {
|
||||
},
|
||||
"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]); },
|
||||
"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 ---
|
||||
var src = fs.readFileSync(path.resolve(__dirname, "../ref/test.sx"), "utf8");
|
||||
var exprs = Sx.parseAll(src);
|
||||
// --- Resolve which test specs to run ---
|
||||
var refDir = path.resolve(__dirname, "../ref");
|
||||
var args = process.argv.slice(2);
|
||||
|
||||
console.log("TAP version 13");
|
||||
for (var i = 0; i < exprs.length; i++) {
|
||||
Sx.eval(exprs[i], env);
|
||||
// Available spec modules and their platform requirements
|
||||
var 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"] },
|
||||
};
|
||||
|
||||
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 ---
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
#!/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).
|
||||
|
||||
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
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
||||
sys.path.insert(0, _PROJECT)
|
||||
|
||||
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 ---
|
||||
suite_stack: list[str] = []
|
||||
@@ -29,7 +33,7 @@ test_num = 0
|
||||
def try_call(thunk):
|
||||
"""Call an SX thunk, catching errors."""
|
||||
try:
|
||||
_trampoline(_eval([thunk], {}))
|
||||
_trampoline(_eval([thunk], env))
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
@@ -60,25 +64,253 @@ def pop_suite():
|
||||
suite_stack.pop()
|
||||
|
||||
|
||||
def main():
|
||||
env = {
|
||||
"try-call": try_call,
|
||||
"report-pass": report_pass,
|
||||
"report-fail": report_fail,
|
||||
"push-suite": push_suite,
|
||||
"pop-suite": pop_suite,
|
||||
}
|
||||
# --- Parser platform functions ---
|
||||
|
||||
test_sx = os.path.join(_HERE, "..", "ref", "test.sx")
|
||||
with open(test_sx) as f:
|
||||
def sx_parse(source):
|
||||
"""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()
|
||||
|
||||
exprs = parse_all(src)
|
||||
|
||||
print("TAP version 13")
|
||||
for expr in exprs:
|
||||
_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(f"1..{test_num}")
|
||||
print(f"# tests {passed + failed}")
|
||||
|
||||
@@ -64,3 +64,8 @@
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
(define-page-helper "run-modular-tests"
|
||||
:params (spec-name)
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
(dict :label "Essays" :href "/essays/")
|
||||
(dict :label "Specs" :href "/specs/")
|
||||
(dict :label "Bootstrappers" :href "/bootstrappers/")
|
||||
(dict :label "Testing" :href "/testing/")
|
||||
(dict :label "Isomorphism" :href "/isomorphism/")
|
||||
(dict :label "Plans" :href "/plans/"))))
|
||||
(<> (map (lambda (item)
|
||||
|
||||
@@ -107,8 +107,15 @@
|
||||
(dict :label "Continuations" :href "/specs/continuations")
|
||||
(dict :label "call/cc" :href "/specs/callcc")
|
||||
(dict :label "Deps" :href "/specs/deps")
|
||||
(dict :label "Router" :href "/specs/router")
|
||||
(dict :label "Testing" :href "/specs/testing")))
|
||||
(dict :label "Router" :href "/specs/router")))
|
||||
|
||||
(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
|
||||
(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.")
|
||||
(dict :slug "router" :filename "router.sx" :title "Router"
|
||||
: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.")
|
||||
(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.")))
|
||||
: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.")))
|
||||
|
||||
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat extension-spec-items module-spec-items)))))
|
||||
|
||||
|
||||
446
sx/sx/testing.sx
446
sx/sx/testing.sx
@@ -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"
|
||||
(div :class "space-y-8"
|
||||
|
||||
;; Intro
|
||||
(div :class "space-y-4"
|
||||
(p :class "text-lg text-stone-600"
|
||||
"SX tests itself. "
|
||||
(code :class "text-violet-700 text-sm" "test.sx")
|
||||
" is a self-executing test spec — it defines "
|
||||
"SX tests itself. Test specs are written in SX and executed by SX evaluators on every host. "
|
||||
"The same assertions run in Python, Node.js, and in the browser — from the same source files.")
|
||||
(p :class "text-stone-600"
|
||||
"The framework defines two macros ("
|
||||
(code :class "text-violet-700 text-sm" "deftest")
|
||||
" and "
|
||||
(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.")
|
||||
(p :class "text-stone-600"
|
||||
"This is not a test "
|
||||
(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."))
|
||||
") and nine assertion helpers, all in SX. Each host provides only "
|
||||
(strong "five platform functions")
|
||||
" — everything else is pure SX."))
|
||||
|
||||
;; Server-side results (ran when this page was rendered)
|
||||
(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
|
||||
;; Architecture
|
||||
(div :class "space-y-3"
|
||||
(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"
|
||||
(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
|
||||
|
|
||||
|--- run.js Injects 5 platform fns, evaluates test.sx
|
||||
| |
|
||||
| +-> sx-browser.js JS evaluator (bootstrapped from spec)
|
||||
|
|
||||
|--- run.py Injects 5 platform fns, evaluates test.sx
|
||||
|
|
||||
+-> evaluator.py Python evaluator
|
||||
+-- test-eval.sx 81 tests: evaluator + primitives
|
||||
+-- test-parser.sx 39 tests: tokenizer, parser, serializer
|
||||
+-- test-router.sx 18 tests: route matching + param extraction
|
||||
+-- test-render.sx 23 tests: HTML rendering + components
|
||||
|
||||
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\"}
|
||||
report-pass (name) -> output pass
|
||||
report-fail (name error) -> output fail
|
||||
push-suite (name) -> push suite context
|
||||
pop-suite () -> pop suite context")))
|
||||
pop-suite () -> pop suite context
|
||||
|
||||
;; Framework
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "The test framework")
|
||||
(p :class "text-stone-600"
|
||||
"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"))))
|
||||
Per-spec platform functions:
|
||||
parser: sx-parse, sx-serialize, make-symbol, make-keyword, ...
|
||||
router: (none — pure spec, uses bootstrapped functions)
|
||||
render: render-html (wraps parse + render-to-html)")))
|
||||
|
||||
;; Example tests
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Example: SX testing SX")
|
||||
(p :class "text-stone-600"
|
||||
"The test suites cover every language feature. Here is the arithmetic suite testing the evaluator's arithmetic primitives:")
|
||||
(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" "From test.sx")
|
||||
(~doc-code :code
|
||||
(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"))))
|
||||
;; Server 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 all test specs 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"))))
|
||||
|
||||
;; Running tests — JS
|
||||
;; Browser test runner
|
||||
(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"
|
||||
"Run all test specs in the browser using "
|
||||
(code :class "text-violet-700 text-sm" "sx-browser.js")
|
||||
" evaluates "
|
||||
(code :class "text-violet-700 text-sm" "test.sx")
|
||||
" directly. The runner injects platform functions and calls "
|
||||
(code :class "text-violet-700 text-sm" "Sx.eval")
|
||||
" on each parsed expression:")
|
||||
(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" "run.js")
|
||||
(~doc-code :code
|
||||
(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")))
|
||||
(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")
|
||||
(~doc-code :code
|
||||
(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"))))
|
||||
":")
|
||||
(div :class "flex items-center gap-4"
|
||||
(button :id "test-btn-all"
|
||||
:class "px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 cursor-pointer"
|
||||
:onclick "sxRunModularTests('all','test-output-all','test-btn-all')"
|
||||
"Run all tests"))
|
||||
(pre :id "test-output-all"
|
||||
: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: all spec sources for the browser runner
|
||||
(textarea :id "test-framework-source" :style "display:none" framework-source)
|
||||
(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"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Python: direct evaluation")
|
||||
(p :class "text-stone-600"
|
||||
"Same approach — the Python evaluator runs "
|
||||
(code :class "text-violet-700 text-sm" "test.sx")
|
||||
" directly:")
|
||||
(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" "run.py")
|
||||
(~doc-code :code
|
||||
(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")))
|
||||
(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")
|
||||
(~doc-code :code
|
||||
(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"))))
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Test specs")
|
||||
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
(a :href "/testing/eval" :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" "Evaluator")
|
||||
(p :class "text-sm text-stone-500" "81 tests — literals, arithmetic, strings, lists, dicts, special forms, lambdas, components, macros")
|
||||
(p :class "text-xs text-violet-600 mt-1" "test-eval.sx"))
|
||||
(a :href "/testing/parser" :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" "Parser")
|
||||
(p :class "text-sm text-stone-500" "39 tests — tokenization, parsing, escape sequences, quote sugar, serialization, round-trips")
|
||||
(p :class "text-xs text-violet-600 mt-1" "test-parser.sx"))
|
||||
(a :href "/testing/router" :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" "Router")
|
||||
(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
|
||||
(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")
|
||||
(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 "The same 81 tests run on " (strong "Python, Node.js, and in the browser") " from the same file")
|
||||
(li "Test specs are " (strong "written in SX") " and " (strong "executed by SX") " — no code generation")
|
||||
(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 "Adding a new host means implementing 5 functions, not rewriting tests")
|
||||
(li "Platform divergences (truthiness of 0, [], \"\") are " (strong "documented, not hidden"))
|
||||
(li "The spec is " (strong "executable") " — click the button above to prove it")))
|
||||
(li "Modular specs test each part independently — evaluator, parser, router, renderer")
|
||||
(li "Per-spec platform functions extend the 5-function contract for module-specific capabilities")
|
||||
(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"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "All 15 test suites")
|
||||
(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" "Suite")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Tests")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Covers")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "literals")
|
||||
(td :class "px-3 py-2" "6")
|
||||
(td :class "px-3 py-2 text-stone-700" "number, string, boolean, nil, list, dict type checking"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "arithmetic")
|
||||
(td :class "px-3 py-2" "5")
|
||||
(td :class "px-3 py-2 text-stone-700" "+, -, *, /, mod with edge cases"))
|
||||
(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"))))))
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Browser: JavaScript evaluator")
|
||||
(p :class "text-stone-600"
|
||||
"Run this spec in the browser:")
|
||||
(div :class "flex items-center gap-4"
|
||||
(button :id (str "test-btn-" spec-name)
|
||||
:class "px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 cursor-pointer"
|
||||
:onclick (str "sxRunModularTests('" spec-name "','test-output-" spec-name "','test-btn-" spec-name "')")
|
||||
(str "Run " spec-title)))
|
||||
(pre :id (str "test-output-" spec-name)
|
||||
: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: spec source + framework source for the browser runner
|
||||
(textarea :id (str "test-spec-" spec-name) :style "display:none" spec-source)
|
||||
(textarea :id "test-framework-source" :style "display:none" framework-source)
|
||||
(script :src (asset-url "/scripts/sx-test-runner.js")))
|
||||
|
||||
;; Full source
|
||||
(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"
|
||||
"The s-expression source below is the canonical test specification. "
|
||||
"Any host that implements the five platform functions can evaluate it directly.")
|
||||
(str "test-" spec-name ".sx")
|
||||
" — the canonical test specification for this module.")
|
||||
(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"
|
||||
(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"))))))
|
||||
|
||||
@@ -344,9 +344,6 @@
|
||||
:filename (get item "filename") :href (str "/specs/" (get item "slug"))
|
||||
:source (read-spec-file (get item "filename"))))
|
||||
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)))
|
||||
(if spec
|
||||
(~spec-detail-content
|
||||
@@ -516,3 +513,74 @@
|
||||
"glue-decoupling" (~plan-glue-decoupling-content)
|
||||
"social-sharing" (~plan-social-sharing-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)))
|
||||
|
||||
@@ -25,6 +25,7 @@ def _register_sx_helpers() -> None:
|
||||
"routing-analyzer-data": _routing_analyzer_data,
|
||||
"data-test-data": _data_test_data,
|
||||
"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:
|
||||
"""Return test data for the client-side data rendering test page.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user