Add Testing as top-level docs section with per-module specs

New /testing/ section with 6 pages: overview (all specs), evaluator,
parser, router, renderer, and runners. Each page runs tests server-side
(Python) and offers a browser "Run tests" button (JS). Modular browser
runner (sxRunModularTests) loads framework + per-spec sources from DOM.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 12:37:30 +00:00
parent aab1f3e966
commit 3119b8e310
7 changed files with 674 additions and 232 deletions

View File

@@ -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";
};

View File

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

View File

@@ -14,6 +14,7 @@
(dict :label "Essays" :href "/essays/")
(dict :label "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)

View File

@@ -105,8 +105,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/")
@@ -199,10 +206,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)))))

View File

@@ -1,246 +1,288 @@
;; Testing spec page — SX tests SX.
;; Testing section — SX tests SX across every host.
;; Modular test specs: eval, parser, router, render.
;; Each page shows spec source, runs server-side, offers browser-side run button.
(defcomp ~spec-testing-content (&key spec-source server-results)
;; ---------------------------------------------------------------------------
;; Overview page
;; ---------------------------------------------------------------------------
(defcomp ~testing-overview-content (&key server-results framework-source eval-source parser-source router-source render-source)
(~doc-page :title "Testing"
(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"))))))

View File

@@ -343,9 +343,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
@@ -515,3 +512,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)))

View File

@@ -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.