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:
@@ -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
194
sx/sx/testing.sx
Normal 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"))))))))
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user