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:
@@ -343,9 +343,6 @@
|
||||
:filename (get item "filename") :href (str "/specs/" (get item "slug"))
|
||||
:source (read-spec-file (get item "filename"))))
|
||||
extension-spec-items))
|
||||
"testing" (~spec-testing-content
|
||||
:spec-source (read-spec-file "test.sx")
|
||||
:server-results (run-spec-tests))
|
||||
:else (let ((spec (find-spec slug)))
|
||||
(if spec
|
||||
(~spec-detail-content
|
||||
@@ -515,3 +512,74 @@
|
||||
"glue-decoupling" (~plan-glue-decoupling-content)
|
||||
"social-sharing" (~plan-social-sharing-content)
|
||||
:else (~plans-index-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Testing section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage testing-index
|
||||
:path "/testing/"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Testing"
|
||||
:sub-label "Testing"
|
||||
:sub-href "/testing/"
|
||||
:sub-nav (~section-nav :items testing-nav-items :current "Overview")
|
||||
:selected "Overview")
|
||||
:data (run-modular-tests "all")
|
||||
:content (~testing-overview-content
|
||||
:server-results server-results
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:eval-source (read-spec-file "test-eval.sx")
|
||||
:parser-source (read-spec-file "test-parser.sx")
|
||||
:router-source (read-spec-file "test-router.sx")
|
||||
:render-source (read-spec-file "test-render.sx")))
|
||||
|
||||
(defpage testing-page
|
||||
:path "/testing/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Testing"
|
||||
:sub-label "Testing"
|
||||
:sub-href "/testing/"
|
||||
:sub-nav (~section-nav :items testing-nav-items
|
||||
:current (find-current testing-nav-items slug))
|
||||
:selected (or (find-current testing-nav-items slug) ""))
|
||||
:data (case slug
|
||||
"eval" (run-modular-tests "eval")
|
||||
"parser" (run-modular-tests "parser")
|
||||
"router" (run-modular-tests "router")
|
||||
"render" (run-modular-tests "render")
|
||||
:else (dict))
|
||||
:content (case slug
|
||||
"eval" (~testing-spec-content
|
||||
:spec-name "eval"
|
||||
:spec-title "Evaluator Tests"
|
||||
:spec-desc "81 tests covering the core evaluator and all primitives — literals, arithmetic, comparison, strings, lists, dicts, predicates, special forms, lambdas, higher-order functions, components, macros, threading, and edge cases."
|
||||
:spec-source (read-spec-file "test-eval.sx")
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:server-results server-results)
|
||||
"parser" (~testing-spec-content
|
||||
:spec-name "parser"
|
||||
:spec-title "Parser Tests"
|
||||
:spec-desc "39 tests covering tokenization and parsing — integers, floats, strings, escape sequences, booleans, nil, keywords, symbols, lists, dicts, whitespace, comments, quote sugar, serialization, and round-trips."
|
||||
:spec-source (read-spec-file "test-parser.sx")
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:server-results server-results)
|
||||
"router" (~testing-spec-content
|
||||
:spec-name "router"
|
||||
:spec-title "Router Tests"
|
||||
:spec-desc "18 tests covering client-side route matching — path splitting, pattern parsing, segment matching, parameter extraction, and route table search."
|
||||
:spec-source (read-spec-file "test-router.sx")
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:server-results server-results)
|
||||
"render" (~testing-spec-content
|
||||
:spec-name "render"
|
||||
:spec-title "Renderer Tests"
|
||||
:spec-desc "23 tests covering HTML rendering — elements, attributes, void elements, boolean attributes, fragments, escaping, control flow, and component rendering."
|
||||
:spec-source (read-spec-file "test-render.sx")
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:server-results server-results)
|
||||
"runners" (~testing-runners-content)
|
||||
:else (~testing-overview-content
|
||||
:server-results server-results)))
|
||||
|
||||
@@ -25,6 +25,7 @@ def _register_sx_helpers() -> None:
|
||||
"routing-analyzer-data": _routing_analyzer_data,
|
||||
"data-test-data": _data_test_data,
|
||||
"run-spec-tests": _run_spec_tests,
|
||||
"run-modular-tests": _run_modular_tests,
|
||||
})
|
||||
|
||||
|
||||
@@ -562,6 +563,199 @@ def _run_spec_tests() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _run_modular_tests(spec_name: str) -> dict:
|
||||
"""Run modular test specs against the Python SX evaluator.
|
||||
|
||||
spec_name: "eval", "parser", "router", "render", or "all".
|
||||
Returns dict with server-results key containing results per spec.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.types import Symbol, Keyword, Lambda, NIL
|
||||
|
||||
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
|
||||
if not os.path.isdir(ref_dir):
|
||||
ref_dir = "/app/shared/sx/ref"
|
||||
|
||||
suite_stack: list[str] = []
|
||||
passed = 0
|
||||
failed = 0
|
||||
test_num = 0
|
||||
lines: list[str] = []
|
||||
|
||||
def try_call(thunk):
|
||||
try:
|
||||
_trampoline(_eval([thunk], env))
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
def report_pass(name):
|
||||
nonlocal passed, test_num
|
||||
test_num += 1
|
||||
passed += 1
|
||||
lines.append("ok " + str(test_num) + " - " + " > ".join(suite_stack + [name]))
|
||||
|
||||
def report_fail(name, error):
|
||||
nonlocal failed, test_num
|
||||
test_num += 1
|
||||
failed += 1
|
||||
full = " > ".join(suite_stack + [name])
|
||||
lines.append("not ok " + str(test_num) + " - " + full)
|
||||
lines.append(" # " + str(error))
|
||||
|
||||
def push_suite(name):
|
||||
suite_stack.append(name)
|
||||
|
||||
def pop_suite():
|
||||
suite_stack.pop()
|
||||
|
||||
def sx_parse(source):
|
||||
return parse_all(source)
|
||||
|
||||
def sx_serialize(val):
|
||||
if val is None or val is NIL:
|
||||
return "nil"
|
||||
if isinstance(val, bool):
|
||||
return "true" if val else "false"
|
||||
if isinstance(val, (int, float)):
|
||||
return str(val)
|
||||
if isinstance(val, str):
|
||||
escaped = val.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
if isinstance(val, Symbol):
|
||||
return val.name
|
||||
if isinstance(val, Keyword):
|
||||
return f":{val.name}"
|
||||
if isinstance(val, list):
|
||||
inner = " ".join(sx_serialize(x) for x in val)
|
||||
return f"({inner})"
|
||||
if isinstance(val, dict):
|
||||
parts = []
|
||||
for k, v in val.items():
|
||||
parts.append(f":{k}")
|
||||
parts.append(sx_serialize(v))
|
||||
return "{" + " ".join(parts) + "}"
|
||||
return str(val)
|
||||
|
||||
def render_html(sx_source):
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import render_to_html as _render_to_html
|
||||
except ImportError:
|
||||
return "<!-- render-to-html not available -->"
|
||||
exprs = parse_all(sx_source)
|
||||
render_env = dict(env)
|
||||
result = ""
|
||||
for expr in exprs:
|
||||
result += _render_to_html(expr, render_env)
|
||||
return result
|
||||
|
||||
def _call_sx(fn, args, caller_env):
|
||||
if isinstance(fn, Lambda):
|
||||
from shared.sx.evaluator import _call_lambda
|
||||
return _trampoline(_call_lambda(fn, list(args), caller_env))
|
||||
return fn(*args)
|
||||
|
||||
def _for_each_indexed(fn, coll):
|
||||
if isinstance(fn, Lambda):
|
||||
closure = fn.closure
|
||||
for i, item in enumerate(coll or []):
|
||||
for p, v in zip(fn.params, [i, item]):
|
||||
closure[p] = v
|
||||
_trampoline(_eval(fn.body, closure))
|
||||
else:
|
||||
for i, item in enumerate(coll or []):
|
||||
fn(i, item)
|
||||
return NIL
|
||||
|
||||
env = {
|
||||
"try-call": try_call,
|
||||
"report-pass": report_pass,
|
||||
"report-fail": report_fail,
|
||||
"push-suite": push_suite,
|
||||
"pop-suite": pop_suite,
|
||||
"sx-parse": sx_parse,
|
||||
"sx-serialize": sx_serialize,
|
||||
"make-symbol": lambda name: Symbol(name),
|
||||
"make-keyword": lambda name: Keyword(name),
|
||||
"symbol-name": lambda sym: sym.name if isinstance(sym, Symbol) else str(sym),
|
||||
"keyword-name": lambda kw: kw.name if isinstance(kw, Keyword) else str(kw),
|
||||
"render-html": render_html,
|
||||
"for-each-indexed": _for_each_indexed,
|
||||
"dict-set!": lambda d, k, v: (d.__setitem__(k, v), NIL)[-1] if isinstance(d, dict) else NIL,
|
||||
"dict-has?": lambda d, k: isinstance(d, dict) and k in d,
|
||||
"dict-get": lambda d, k: d.get(k, NIL) if isinstance(d, dict) else NIL,
|
||||
"append!": lambda lst, item: (lst.append(item), NIL)[-1] if isinstance(lst, list) else NIL,
|
||||
"inc": lambda n: n + 1,
|
||||
}
|
||||
|
||||
def eval_file(filename):
|
||||
filepath = os.path.join(ref_dir, filename)
|
||||
if not os.path.exists(filepath):
|
||||
return
|
||||
with open(filepath) as f:
|
||||
src = f.read()
|
||||
exprs = parse_all(src)
|
||||
for expr in exprs:
|
||||
_trampoline(_eval(expr, env))
|
||||
|
||||
SPECS = {
|
||||
"eval": {"file": "test-eval.sx", "needs": []},
|
||||
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
||||
"router": {"file": "test-router.sx", "needs": []},
|
||||
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
||||
}
|
||||
|
||||
specs_to_run = list(SPECS.keys()) if spec_name == "all" else [spec_name]
|
||||
|
||||
t0 = time.monotonic()
|
||||
|
||||
# Load framework
|
||||
eval_file("test-framework.sx")
|
||||
|
||||
for sn in specs_to_run:
|
||||
spec = SPECS.get(sn)
|
||||
if not spec:
|
||||
continue
|
||||
|
||||
# Load router from bootstrap if needed
|
||||
if sn == "router":
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import (
|
||||
split_path_segments,
|
||||
parse_route_pattern,
|
||||
match_route_segments,
|
||||
match_route,
|
||||
find_matching_route,
|
||||
make_route_segment,
|
||||
)
|
||||
env["split-path-segments"] = split_path_segments
|
||||
env["parse-route-pattern"] = parse_route_pattern
|
||||
env["match-route-segments"] = match_route_segments
|
||||
env["match-route"] = match_route
|
||||
env["find-matching-route"] = find_matching_route
|
||||
env["make-route-segment"] = make_route_segment
|
||||
except ImportError:
|
||||
eval_file("router.sx")
|
||||
|
||||
eval_file(spec["file"])
|
||||
|
||||
elapsed = round((time.monotonic() - t0) * 1000)
|
||||
|
||||
return {
|
||||
"server-results": {
|
||||
"passed": passed,
|
||||
"failed": failed,
|
||||
"total": passed + failed,
|
||||
"elapsed-ms": elapsed,
|
||||
"output": "\n".join(lines),
|
||||
"spec": spec_name,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _data_test_data() -> dict:
|
||||
"""Return test data for the client-side data rendering test page.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user