"""Isomorphic tests — same SX expressions evaluated on Python and JS. Phase 7e: verifies that the bootstrapped evaluator produces identical output across both hosts. Runs each test case through: 1. Python sx_ref.py (bootstrapped from spec) 2. Node.js sx-browser.js (bootstrapped from spec) Compares rendered HTML output to catch any host-specific divergence. """ import json import subprocess import os import pytest from shared.sx.parser import parse from shared.sx.ref import sx_ref # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _HERE = os.path.dirname(os.path.abspath(__file__)) _SX_BROWSER = os.path.abspath(os.path.join(_HERE, "..", "..", "static", "scripts", "sx-browser.js")) def _node_harness(sx_path: str) -> str: return f""" const Sx = require({json.dumps(sx_path)}); let input = ""; process.stdin.on("data", d => input += d); process.stdin.on("end", () => {{ const cases = JSON.parse(input); const results = []; for (const c of cases) {{ try {{ const env = {{}}; if (c.defs) {{ for (const d of c.defs) {{ Sx.eval(Sx.parse(d)[0], env); }} }} const parsed = Sx.parse(c.expr)[0]; let result; if (c.mode === "render") {{ result = Sx.renderToHtml(parsed, env); }} else {{ const val = Sx.eval(parsed, env); result = typeof val === "object" && val !== null && val._nil ? "nil" : String(val); }} results.push({{ ok: true, result: result }}); }} catch (e) {{ results.push({{ ok: false, error: e.message || String(e) }}); }} }} process.stdout.write(JSON.stringify(results)); }}); """ def _run_js(cases: list[dict]) -> list[dict]: """Run test cases through Node.js sx-browser.js.""" harness = _node_harness(_SX_BROWSER) result = subprocess.run( ["node", "-e", harness], input=json.dumps(cases), capture_output=True, text=True, timeout=10, ) if result.returncode != 0: pytest.fail(f"Node.js failed: {result.stderr}") return json.loads(result.stdout) def _normalize_val(val: str) -> str: """Normalize value representations across hosts.""" # Python True/False → lowercase if val == "True": return "true" if val == "False": return "false" # Python float 5.0 → 5 (match JS integer division) try: f = float(val) if f == int(f): return str(int(f)) except (ValueError, OverflowError): pass # Python list repr [1, 2, 3] → 1,2,3 (match JS Array.toString) if val.startswith("[") and val.endswith("]"): inner = val[1:-1] return inner.replace(", ", ",") return val def _run_py(cases: list[dict]) -> list[dict]: """Run test cases through Python sx_ref.py.""" results = [] for c in cases: try: env = {} if c.get("defs"): for d in c["defs"]: sx_ref.evaluate(parse(d), env) parsed = parse(c["expr"]) if c.get("mode") == "render": result = sx_ref.render(parsed, env) else: val = sx_ref.evaluate(parsed, env) from shared.sx.types import NIL as SX_NIL result = "nil" if val is SX_NIL or val is None else _normalize_val(str(val)) results.append({"ok": True, "result": result}) except Exception as e: results.append({"ok": False, "error": str(e)}) return results # --------------------------------------------------------------------------- # Test cases # --------------------------------------------------------------------------- EVAL_CASES = [ # Arithmetic {"expr": "(+ 1 2)", "expected": "3"}, {"expr": "(* 3 4)", "expected": "12"}, {"expr": "(- 10 3)", "expected": "7"}, {"expr": "(/ 10 2)", "expected": "5"}, # Comparison {"expr": "(> 3 2)", "expected": "true"}, {"expr": "(< 1 2)", "expected": "true"}, {"expr": "(= 5 5)", "expected": "true"}, {"expr": '(= "a" "a")', "expected": "true"}, # String ops {"expr": '(str "hello" " " "world")', "expected": "hello world"}, {"expr": '(upper "hello")', "expected": "HELLO"}, {"expr": '(lower "WORLD")', "expected": "world"}, {"expr": '(join "-" (list "a" "b" "c"))', "expected": "a-b-c"}, {"expr": '(len "hello")', "expected": "5"}, {"expr": '(trim " hi ")', "expected": "hi"}, # Collections {"expr": "(len (list 1 2 3))", "expected": "3"}, {"expr": "(first (list 10 20 30))", "expected": "10"}, {"expr": "(last (list 10 20 30))", "expected": "30"}, {"expr": "(nth (list 10 20 30) 1)", "expected": "20"}, {"expr": "(contains? (list 1 2 3) 2)", "expected": "true"}, {"expr": "(empty? (list))", "expected": "true"}, # Logic {"expr": "(if true 1 2)", "expected": "1"}, {"expr": "(if false 1 2)", "expected": "2"}, {"expr": "(and true true)", "expected": "true"}, {"expr": "(or false true)", "expected": "true"}, {"expr": "(not false)", "expected": "true"}, # Let / lambda {"expr": "(let ((x 10) (y 20)) (+ x y))", "expected": "30"}, {"expr": "((fn (x) (* x x)) 5)", "expected": "25"}, # Higher-order {"expr": "(map (fn (x) (* x x)) (list 1 2 3))", "expected": "1,4,9"}, {"expr": "(filter (fn (x) (> x 2)) (list 1 2 3 4))", "expected": "3,4"}, {"expr": "(reduce (fn (a x) (+ a x)) 0 (list 1 2 3))", "expected": "6"}, {"expr": "(some (fn (x) (> x 3)) (list 1 2 5))", "expected": "true"}, {"expr": "(every? (fn (x) (> x 0)) (list 1 2 3))", "expected": "true"}, # Dict {"expr": '(get {:a 1 :b 2} :a)', "expected": "1"}, {"expr": '(len {:x 10 :y 20})', "expected": "2"}, # Keywords {"expr": ":hello", "expected": "hello"}, # Cond {"expr": '(cond (= 1 2) "a" (= 1 1) "b" :else "c")', "expected": "b"}, # Case {"expr": '(let ((x 2)) (case x 1 "one" 2 "two" "other"))', "expected": "two"}, ] RENDER_CASES = [ # Basic elements {"expr": '(div "hello")', "mode": "render", "expected": "
paragraph
"}, # Attributes {"expr": '(div :class "box" "content")', "mode": "render", "expected": '
'},
# Boolean attributes
{"expr": '(input :disabled true)', "mode": "render", "expected": ""},
{"expr": '(input :disabled false)', "mode": "render", "expected": ""},
# Conditional rendering
{"expr": '(div (if true (span "yes") (span "no")))', "mode": "render",
"expected": "a
b