Add server-side test runner to /specs/testing page
Python evaluator runs test.sx at page load, results shown alongside the browser runner. Both hosts prove the same 81 tests from the same spec file — server on render, client on click. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,3 +59,8 @@
|
|||||||
:params ()
|
:params ()
|
||||||
:returns "dict"
|
:returns "dict"
|
||||||
:service "sx")
|
:service "sx")
|
||||||
|
|
||||||
|
(define-page-helper "run-spec-tests"
|
||||||
|
:params ()
|
||||||
|
:returns "dict"
|
||||||
|
:service "sx")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
;; Testing spec page — SX tests SX.
|
;; Testing spec page — SX tests SX.
|
||||||
|
|
||||||
(defcomp ~spec-testing-content (&key spec-source)
|
(defcomp ~spec-testing-content (&key spec-source server-results)
|
||||||
(~doc-page :title "Testing"
|
(~doc-page :title "Testing"
|
||||||
(div :class "space-y-8"
|
(div :class "space-y-8"
|
||||||
|
|
||||||
@@ -22,15 +22,27 @@
|
|||||||
(code :class "text-violet-700 text-sm" "if")
|
(code :class "text-violet-700 text-sm" "if")
|
||||||
" works. No code generation, no intermediate files — the evaluator runs the spec."))
|
" works. No code generation, no intermediate files — the evaluator runs the spec."))
|
||||||
|
|
||||||
;; Live test runner
|
;; Server-side results (ran when this page was rendered)
|
||||||
(div :class "space-y-3"
|
(div :class "space-y-3"
|
||||||
(h2 :class "text-2xl font-semibold text-stone-800" "Run in browser")
|
(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"
|
(p :class "text-stone-600"
|
||||||
"This page loaded "
|
"This page loaded "
|
||||||
(code :class "text-violet-700 text-sm" "sx-browser.js")
|
(code :class "text-violet-700 text-sm" "sx-browser.js")
|
||||||
" to render itself. The same evaluator can run "
|
" to render itself. The same evaluator can run "
|
||||||
(code :class "text-violet-700 text-sm" "test.sx")
|
(code :class "text-violet-700 text-sm" "test.sx")
|
||||||
" right here — SX testing SX, in your browser:")
|
" right here:")
|
||||||
(div :class "flex items-center gap-4"
|
(div :class "flex items-center gap-4"
|
||||||
(button :id "test-btn"
|
(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"
|
:class "px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 cursor-pointer"
|
||||||
|
|||||||
@@ -342,7 +342,8 @@
|
|||||||
:source (read-spec-file (get item "filename"))))
|
:source (read-spec-file (get item "filename"))))
|
||||||
extension-spec-items))
|
extension-spec-items))
|
||||||
"testing" (~spec-testing-content
|
"testing" (~spec-testing-content
|
||||||
:spec-source (read-spec-file "test.sx"))
|
: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
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ def _register_sx_helpers() -> None:
|
|||||||
"bundle-analyzer-data": _bundle_analyzer_data,
|
"bundle-analyzer-data": _bundle_analyzer_data,
|
||||||
"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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -491,6 +492,76 @@ def _event_detail_data(slug: str) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _run_spec_tests() -> dict:
|
||||||
|
"""Run test.sx against the Python SX evaluator and return results."""
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from shared.sx.parser import parse_all
|
||||||
|
from shared.sx.evaluator import _eval, _trampoline
|
||||||
|
|
||||||
|
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"
|
||||||
|
test_path = os.path.join(ref_dir, "test.sx")
|
||||||
|
with open(test_path, encoding="utf-8") as f:
|
||||||
|
src = f.read()
|
||||||
|
|
||||||
|
suite_stack: list[str] = []
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
test_num = 0
|
||||||
|
lines: list[str] = []
|
||||||
|
|
||||||
|
def try_call(thunk):
|
||||||
|
try:
|
||||||
|
_trampoline(_eval([thunk], {}))
|
||||||
|
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()
|
||||||
|
|
||||||
|
env = {
|
||||||
|
"try-call": try_call,
|
||||||
|
"report-pass": report_pass,
|
||||||
|
"report-fail": report_fail,
|
||||||
|
"push-suite": push_suite,
|
||||||
|
"pop-suite": pop_suite,
|
||||||
|
}
|
||||||
|
|
||||||
|
t0 = time.monotonic()
|
||||||
|
exprs = parse_all(src)
|
||||||
|
for expr in exprs:
|
||||||
|
_trampoline(_eval(expr, env))
|
||||||
|
elapsed = round((time.monotonic() - t0) * 1000)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"passed": passed,
|
||||||
|
"failed": failed,
|
||||||
|
"total": passed + failed,
|
||||||
|
"elapsed-ms": elapsed,
|
||||||
|
"output": "\n".join(lines),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user