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