Add self-hosting SX test spec: 81 tests bootstrap to Python + JS

The test framework is written in SX and tests SX — the language proves
its own correctness. test.sx defines assertion helpers (assert-equal,
assert-true, assert-type, etc.) and 15 test suites covering literals,
arithmetic, comparison, strings, lists, dicts, predicates, special forms,
lambdas, higher-order forms, components, macros, threading, truthiness,
and edge cases.

Two bootstrap compilers emit native tests from the same spec:
- bootstrap_test.py → pytest (81/81 pass)
- bootstrap_test_js.py → Node.js TAP using sx-browser.js (81/81 pass)

Also adds missing primitives to spec and Python evaluator: boolean?,
string-length, substring, string-contains?, upcase, downcase, reverse,
flatten, has-key?. Fixes number? to exclude booleans, append to
concatenate lists.

Includes testing docs page in SX app at /specs/testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 10:41:53 +00:00
parent e09bc3b601
commit 754e7557f5
10 changed files with 1901 additions and 5 deletions

View File

@@ -101,7 +101,8 @@
(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 "Router" :href "/specs/router")
(dict :label "Testing" :href "/specs/testing")))
(define isomorphism-nav-items (list
(dict :label "Roadmap" :href "/isomorphism/")
@@ -186,7 +187,10 @@
: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.")))
: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)))))

194
sx/sx/testing.sx Normal file
View File

@@ -0,0 +1,194 @@
;; Testing spec page — SX tests SX.
(defcomp ~spec-testing-content (&key spec-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. The test spec is written in SX and defines a complete test framework — assertion helpers, test suites, and 81 test cases covering every language feature. Bootstrap compilers read "
(code :class "text-violet-700 text-sm" "test.sx")
" and emit native test runners for each host platform.")
(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. The language proves its own correctness, on every platform it compiles to."))
;; How it works
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Architecture")
(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 The spec: assertion helpers + 15 suites + 81 tests
|
|--- bootstrap_test.py Reads test.sx, emits pytest module
| |
| +-> test_sx_spec.py 81 pytest test cases
| |
| +-> shared/sx/evaluator.py (Python SX evaluator)
|
|--- bootstrap_test_js.py Reads test.sx, emits Node.js TAP script
|
+-> test_sx_spec.js 81 TAP test cases
|
+-> sx-browser.js (JS SX evaluator)")))
;; 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 declarative forms and nine assertion helpers, all in pure SX:")
(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" "Forms")
(~doc-code :code
(highlight "(defsuite \"name\" ...tests)\n(deftest \"name\" ...body)" "lisp")))
(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) ...))" "lisp"))))
;; 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"))))
;; Bootstrapping to Python
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Bootstrap to Python")
(p :class "text-stone-600"
(code :class "text-violet-700 text-sm" "bootstrap_test.py")
" parses "
(code :class "text-violet-700 text-sm" "test.sx")
", extracts the "
(code :class "text-violet-700 text-sm" "define")
" forms (assertion helpers) as a preamble, then emits each "
(code :class "text-violet-700 text-sm" "defsuite")
" as a pytest class and each "
(code :class "text-violet-700 text-sm" "deftest")
" as a test method.")
(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" "Generated Python")
(~doc-code :code
(highlight "# Auto-generated from test.sx\nfrom shared.sx.parser import parse_all\nfrom shared.sx.evaluator import _eval, _trampoline\n\ndef _make_env():\n env = {}\n for expr in parse_all(_PREAMBLE):\n _trampoline(_eval(expr, env))\n return env\n\ndef _run(sx_source, env=None):\n if env is None:\n env = _make_env()\n exprs = parse_all(sx_source)\n for expr in exprs:\n result = _trampoline(_eval(expr, env))\n return result\n\nclass TestSpecArithmetic:\n def test_addition(self):\n _run('(do (assert-equal 3 (+ 1 2)) ...)')\n\n def test_subtraction(self):\n _run('(do (assert-equal 1 (- 3 2)) ...)')" "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" "Run it")
(~doc-code :code
(highlight "$ python bootstrap_test.py --output test_sx_spec.py\nParsed 15 suites, 9 preamble defines from test.sx\nTotal test cases: 81\n\n$ pytest test_sx_spec.py -v\n81 passed in 0.15s" "bash"))))
;; Bootstrapping to JavaScript
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Bootstrap to JavaScript")
(p :class "text-stone-600"
(code :class "text-violet-700 text-sm" "bootstrap_test_js.py")
" emits a standalone Node.js script that loads "
(code :class "text-violet-700 text-sm" "sx-browser.js")
" (bootstrapped from the spec), injects any platform-specific primitives into the test env, then runs all 81 tests with TAP output.")
(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" "Generated JavaScript")
(~doc-code :code
(highlight "// Auto-generated from test.sx\nvar Sx = require('./sx-browser.js');\n\nvar _envPrimitives = {\n 'equal?': function(a, b) { return deepEqual(a, b); },\n 'boolean?': function(x) { return typeof x === 'boolean'; },\n // ... platform primitives injected into env\n};\n\nfunction _makeEnv() {\n var env = {};\n for (var k in _envPrimitives) env[k] = _envPrimitives[k];\n var exprs = Sx.parseAll(PREAMBLE);\n for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);\n return env;\n}\n\nfunction _run(name, sxSource) {\n var env = _makeEnv();\n var exprs = Sx.parseAll(sxSource);\n for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);\n}" "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" "Run it")
(~doc-code :code
(highlight "$ python bootstrap_test_js.py --output test_sx_spec.js\nParsed 15 suites, 9 preamble defines from test.sx\nTotal test cases: 81\n\n$ node test_sx_spec.js\nTAP version 13\n1..81\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"))))
;; 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") " — the same language it tests")
(li "The same 81 tests run on " (strong "both Python and JavaScript"))
(li "Both hosts produce " (strong "identical results") " from identical SX source")
(li "Adding a new host requires only a new bootstrapper — the tests are " (strong "already written"))
(li "Platform divergences (truthiness of 0, [], \"\") are " (strong "documented, not hidden"))
(li "The spec is " (strong "executable") " — it doesn't just describe behavior, it verifies it")))
;; Test suites
(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"))))))
;; Full source
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Full specification source")
(p :class "text-xs text-stone-400 italic"
"The s-expression source below is the canonical test specification. "
"Bootstrap compilers read this file to generate native test runners.")
(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"))))))))

View File

@@ -341,6 +341,8 @@
: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"))
:else (let ((spec (find-spec slug)))
(if spec
(~spec-detail-content