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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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