Component names now reflect filesystem location using / as path separator and : as namespace separator for shared components: ~sx-header → ~layouts/header ~layout-app-body → ~shared:layout/app-body ~blog-admin-dashboard → ~admin/dashboard 209 files, 4,941 replacements across all services. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
745 lines
23 KiB
Python
745 lines
23 KiB
Python
"""Parity tests: hand-written evaluator/renderer vs bootstrapped sx_ref.py.
|
|
|
|
Every test runs the same SX input through both implementations and asserts
|
|
identical results. This is the gate for migrating from hand-written to
|
|
bootstrapped code — 100% parity required before any swap.
|
|
|
|
Run: python -m pytest shared/sx/tests/test_parity.py -v
|
|
"""
|
|
|
|
import pytest
|
|
from shared.sx.parser import parse, parse_all
|
|
from shared.sx.types import Symbol, Keyword, Lambda, Component, Macro, NIL
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Two evaluators, one interface
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def hw_eval(text, env=None):
|
|
"""Evaluate via hand-written evaluator.py."""
|
|
from shared.sx.ref.sx_ref import evaluate as _evaluate
|
|
if env is None:
|
|
env = {}
|
|
return _evaluate(parse(text), env)
|
|
|
|
|
|
def ref_eval(text, env=None):
|
|
"""Evaluate via bootstrapped sx_ref.py."""
|
|
from shared.sx.ref.sx_ref import evaluate as _evaluate, EvalError
|
|
if env is None:
|
|
env = {}
|
|
return _evaluate(parse(text), env)
|
|
|
|
|
|
def hw_render(text, env=None):
|
|
"""Render via hand-written html.py."""
|
|
from shared.sx.html import render
|
|
if env is None:
|
|
env = {}
|
|
return render(parse(text), env)
|
|
|
|
|
|
def ref_render(text, env=None):
|
|
"""Render via bootstrapped sx_ref.py."""
|
|
from shared.sx.ref.sx_ref import render
|
|
if env is None:
|
|
env = {}
|
|
return render(parse(text), env)
|
|
|
|
|
|
def hw_eval_multi(text, env=None):
|
|
"""Evaluate multiple expressions (e.g. defines then call)."""
|
|
from shared.sx.ref.sx_ref import evaluate as _evaluate
|
|
if env is None:
|
|
env = {}
|
|
result = None
|
|
for expr in parse_all(text):
|
|
result = _evaluate(expr, env)
|
|
return result, env
|
|
|
|
|
|
def ref_eval_multi(text, env=None):
|
|
"""Evaluate multiple expressions via sx_ref.py."""
|
|
from shared.sx.ref.sx_ref import evaluate as _evaluate
|
|
if env is None:
|
|
env = {}
|
|
result = None
|
|
for expr in parse_all(text):
|
|
result = _evaluate(expr, env)
|
|
return result, env
|
|
|
|
|
|
def normalize(val):
|
|
"""Normalize values for comparison (handle NIL variants)."""
|
|
if val is None or val is NIL:
|
|
return None
|
|
if isinstance(val, list):
|
|
return [normalize(v) for v in val]
|
|
if isinstance(val, dict):
|
|
return {k: normalize(v) for k, v in val.items()}
|
|
return val
|
|
|
|
|
|
def assert_parity(sx_text, env_hw=None, env_ref=None):
|
|
"""Assert both evaluators produce the same result."""
|
|
hw = normalize(hw_eval(sx_text, env_hw))
|
|
ref = normalize(ref_eval(sx_text, env_ref))
|
|
assert hw == ref, f"MISMATCH on {sx_text!r}:\n hw={hw!r}\n ref={ref!r}"
|
|
|
|
|
|
def assert_render_parity(sx_text, env_hw=None, env_ref=None):
|
|
"""Assert both renderers produce the same HTML."""
|
|
hw = hw_render(sx_text, env_hw)
|
|
ref = ref_render(sx_text, env_ref)
|
|
assert hw == ref, f"RENDER MISMATCH on {sx_text!r}:\n hw={hw!r}\n ref={ref!r}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Literals
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityLiterals:
|
|
def test_int(self):
|
|
assert_parity("42")
|
|
|
|
def test_float(self):
|
|
assert_parity("3.14")
|
|
|
|
def test_string(self):
|
|
assert_parity('"hello"')
|
|
|
|
def test_true(self):
|
|
assert_parity("true")
|
|
|
|
def test_false(self):
|
|
assert_parity("false")
|
|
|
|
def test_nil(self):
|
|
assert_parity("nil")
|
|
|
|
def test_keyword(self):
|
|
assert_parity(":foo")
|
|
|
|
def test_symbol_lookup(self):
|
|
assert_parity("x", {"x": 10}, {"x": 10})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Arithmetic
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityArithmetic:
|
|
def test_add(self):
|
|
assert_parity("(+ 1 2 3)")
|
|
|
|
def test_sub(self):
|
|
assert_parity("(- 10 3)")
|
|
|
|
def test_negate(self):
|
|
assert_parity("(- 5)")
|
|
|
|
def test_mul(self):
|
|
assert_parity("(* 2 3 4)")
|
|
|
|
def test_div(self):
|
|
assert_parity("(/ 10 4)")
|
|
|
|
def test_mod(self):
|
|
assert_parity("(mod 7 3)")
|
|
|
|
def test_clamp(self):
|
|
assert_parity("(clamp 15 0 10)")
|
|
assert_parity("(clamp -5 0 10)")
|
|
|
|
def test_abs(self):
|
|
assert_parity("(abs -5)")
|
|
|
|
def test_floor_ceil(self):
|
|
assert_parity("(floor 3.7)")
|
|
assert_parity("(ceil 3.2)")
|
|
|
|
def test_round(self):
|
|
assert_parity("(round 3.5)")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Comparison and predicates
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityComparison:
|
|
def test_eq(self):
|
|
assert_parity("(= 1 1)")
|
|
assert_parity("(= 1 2)")
|
|
|
|
def test_lt_gt(self):
|
|
assert_parity("(< 1 2)")
|
|
assert_parity("(> 2 1)")
|
|
|
|
def test_lte_gte(self):
|
|
assert_parity("(<= 1 1)")
|
|
assert_parity("(>= 2 1)")
|
|
|
|
def test_predicates(self):
|
|
assert_parity("(odd? 3)")
|
|
assert_parity("(even? 4)")
|
|
assert_parity("(zero? 0)")
|
|
assert_parity("(nil? nil)")
|
|
assert_parity('(string? "hi")')
|
|
assert_parity("(number? 42)")
|
|
assert_parity("(list? (list 1))")
|
|
assert_parity("(dict? {:a 1})")
|
|
|
|
def test_empty(self):
|
|
assert_parity("(empty? (list))")
|
|
assert_parity("(empty? (list 1))")
|
|
assert_parity("(empty? nil)")
|
|
assert_parity('(empty? "")')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Special forms
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParitySpecialForms:
|
|
def test_if_true(self):
|
|
assert_parity("(if true 1 2)")
|
|
|
|
def test_if_false(self):
|
|
assert_parity("(if false 1 2)")
|
|
|
|
def test_if_no_else(self):
|
|
assert_parity("(if false 1)")
|
|
|
|
def test_when_true(self):
|
|
assert_parity("(when true 42)")
|
|
|
|
def test_when_false(self):
|
|
assert_parity("(when false 42)")
|
|
|
|
def test_and(self):
|
|
assert_parity("(and true true 3)")
|
|
assert_parity("(and true false 3)")
|
|
|
|
def test_or(self):
|
|
assert_parity("(or false false 3)")
|
|
assert_parity("(or false 2 3)")
|
|
|
|
def test_let_scheme(self):
|
|
assert_parity("(let ((x 10) (y 20)) (+ x y))")
|
|
|
|
def test_let_clojure(self):
|
|
assert_parity("(let (x 10 y 20) (+ x y))")
|
|
|
|
def test_let_sequential(self):
|
|
assert_parity("(let ((x 1) (y (+ x 1))) y)")
|
|
|
|
def test_begin(self):
|
|
assert_parity("(begin 1 2 3)")
|
|
|
|
def test_cond_clojure(self):
|
|
assert_parity("(cond false 1 true 2 :else 3)")
|
|
|
|
def test_cond_else(self):
|
|
assert_parity("(cond false 1 false 2 :else 99)")
|
|
|
|
def test_case(self):
|
|
assert_parity('(case 2 1 "one" 2 "two" :else "other")')
|
|
|
|
def test_thread_first(self):
|
|
assert_parity("(-> 5 (+ 3) (* 2))")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Lambda
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityLambda:
|
|
def test_create_and_call(self):
|
|
assert_parity("((fn (x) (* x x)) 5)")
|
|
|
|
def test_closure(self):
|
|
assert_parity("(let ((a 10)) ((fn (x) (+ x a)) 5))")
|
|
|
|
def test_higher_order(self):
|
|
assert_parity("(let ((double (fn (x) (* x 2)))) (double 7))")
|
|
|
|
def test_multi_body(self):
|
|
assert_parity("((fn (x) (+ x 1) (* x 2)) 5)")
|
|
|
|
def test_rest_params(self):
|
|
# &rest is only supported in defcomp/defmacro, not bare lambda
|
|
# Both evaluators should error on this
|
|
with pytest.raises(Exception):
|
|
hw_eval("((fn (&rest args) args) 1 2 3)")
|
|
with pytest.raises(Exception):
|
|
ref_eval("((fn (&rest args) args) 1 2 3)")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Collections
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityCollections:
|
|
def test_list(self):
|
|
assert_parity("(list 1 2 3)")
|
|
|
|
def test_dict_literal(self):
|
|
assert_parity("{:a (+ 1 2) :b (* 3 4)}")
|
|
|
|
def test_dict_constructor(self):
|
|
assert_parity("(dict :a 1 :b 2)")
|
|
|
|
def test_get_dict(self):
|
|
assert_parity('(get {:a 1 :b 2} "a")')
|
|
|
|
def test_get_list(self):
|
|
assert_parity("(get (list 10 20 30) 1)")
|
|
|
|
def test_first_last_rest(self):
|
|
assert_parity("(first (list 1 2 3))")
|
|
assert_parity("(last (list 1 2 3))")
|
|
assert_parity("(rest (list 1 2 3))")
|
|
|
|
def test_len(self):
|
|
assert_parity("(len (list 1 2 3))")
|
|
assert_parity('(len "hello")')
|
|
|
|
def test_concat(self):
|
|
assert_parity("(concat (list 1 2) (list 3 4))")
|
|
|
|
def test_cons(self):
|
|
assert_parity("(cons 0 (list 1 2))")
|
|
|
|
def test_keys_vals(self):
|
|
assert_parity("(keys {:a 1 :b 2})")
|
|
assert_parity("(vals {:a 1 :b 2})")
|
|
|
|
def test_merge(self):
|
|
assert_parity("(merge {:a 1} {:b 2} {:a 3})")
|
|
|
|
def test_assoc(self):
|
|
assert_parity("(assoc {:a 1} :b 2)")
|
|
|
|
def test_dissoc(self):
|
|
assert_parity("(dissoc {:a 1 :b 2} :a)")
|
|
|
|
def test_contains(self):
|
|
assert_parity('(contains? {:a 1} "a")')
|
|
assert_parity("(contains? (list 1 2 3) 2)")
|
|
|
|
def test_nth(self):
|
|
assert_parity("(nth (list 10 20 30) 1)")
|
|
|
|
def test_slice(self):
|
|
assert_parity("(slice (list 1 2 3 4 5) 1 3)")
|
|
assert_parity("(slice (list 1 2 3 4 5) 2)")
|
|
|
|
def test_range(self):
|
|
assert_parity("(range 0 5)")
|
|
assert_parity("(range 1 10 2)")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Higher-order forms
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityHigherOrder:
|
|
def test_map(self):
|
|
assert_parity("(map (fn (x) (* x x)) (list 1 2 3 4))")
|
|
|
|
def test_map_indexed(self):
|
|
assert_parity("(map-indexed (fn (i x) (+ i x)) (list 10 20 30))")
|
|
|
|
def test_filter(self):
|
|
assert_parity("(filter (fn (x) (> x 2)) (list 1 2 3 4))")
|
|
|
|
def test_reduce(self):
|
|
assert_parity("(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3))")
|
|
|
|
def test_some(self):
|
|
assert_parity("(some (fn (x) (if (> x 3) x nil)) (list 1 2 4 5))")
|
|
|
|
def test_every(self):
|
|
assert_parity("(every? (fn (x) (> x 0)) (list 1 2 3))")
|
|
assert_parity("(every? (fn (x) (> x 2)) (list 1 2 3))")
|
|
|
|
def test_for_each(self):
|
|
# for-each returns nil
|
|
assert_parity("(for-each (fn (x) x) (list 1 2 3))")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Strings
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityStrings:
|
|
def test_str(self):
|
|
assert_parity('(str "hello" " " "world")')
|
|
|
|
def test_str_with_numbers(self):
|
|
assert_parity('(str "val=" 42)')
|
|
|
|
def test_str_with_nil(self):
|
|
assert_parity('(str "a" nil "b")')
|
|
|
|
def test_upper_lower(self):
|
|
assert_parity('(upper "hello")')
|
|
assert_parity('(lower "HELLO")')
|
|
|
|
def test_join(self):
|
|
assert_parity('(join ", " (list "a" "b" "c"))')
|
|
|
|
def test_split(self):
|
|
assert_parity('(split "a,b,c" ",")')
|
|
|
|
def test_replace(self):
|
|
assert_parity('(replace "hello world" "world" "there")')
|
|
|
|
def test_starts_ends_with(self):
|
|
assert_parity('(starts-with? "hello" "hel")')
|
|
assert_parity('(ends-with? "hello" "llo")')
|
|
|
|
def test_trim(self):
|
|
assert_parity('(trim " hello ")')
|
|
|
|
def test_index_of(self):
|
|
assert_parity('(index-of "hello" "ll")')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Define and defcomp
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityDefine:
|
|
def test_define(self):
|
|
_, hw_env = hw_eval_multi("(define x 42) x")
|
|
_, ref_env = ref_eval_multi("(define x 42) x")
|
|
assert hw_env["x"] == ref_env["x"]
|
|
|
|
def test_define_fn(self):
|
|
hw_r, _ = hw_eval_multi("(define double (fn (x) (* x 2))) (double 5)")
|
|
ref_r, _ = ref_eval_multi("(define double (fn (x) (* x 2))) (double 5)")
|
|
assert hw_r == ref_r
|
|
|
|
def test_defcomp(self):
|
|
hw_r, hw_env = hw_eval_multi(
|
|
'(defcomp ~card (&key title) (str "Card: " title))'
|
|
' (~card :title "Hello")'
|
|
)
|
|
ref_r, ref_env = ref_eval_multi(
|
|
'(defcomp ~card (&key title) (str "Card: " title))'
|
|
' (~card :title "Hello")'
|
|
)
|
|
assert hw_r == ref_r
|
|
assert isinstance(hw_env["~card"], Component)
|
|
assert isinstance(ref_env["~card"], Component)
|
|
|
|
def test_defcomp_with_children(self):
|
|
hw_r, _ = hw_eval_multi(
|
|
"(defcomp ~wrap (&key &rest children) children)"
|
|
" (~wrap 1 2 3)"
|
|
)
|
|
ref_r, _ = ref_eval_multi(
|
|
"(defcomp ~wrap (&key &rest children) children)"
|
|
" (~wrap 1 2 3)"
|
|
)
|
|
assert hw_r == ref_r
|
|
|
|
def test_defcomp_missing_kwarg(self):
|
|
hw_r, _ = hw_eval_multi(
|
|
"(defcomp ~opt (&key x y) (list x y))"
|
|
" (~opt :x 1)"
|
|
)
|
|
ref_r, _ = ref_eval_multi(
|
|
"(defcomp ~opt (&key x y) (list x y))"
|
|
" (~opt :x 1)"
|
|
)
|
|
assert normalize(hw_r) == normalize(ref_r)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: Macros
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityMacros:
|
|
def test_defmacro_expansion(self):
|
|
hw_r, _ = hw_eval_multi(
|
|
"(defmacro double (x) `(+ ,x ,x))"
|
|
" (double 5)"
|
|
)
|
|
ref_r, _ = ref_eval_multi(
|
|
"(defmacro double (x) `(+ ,x ,x))"
|
|
" (double 5)"
|
|
)
|
|
assert hw_r == ref_r
|
|
|
|
def test_macro_with_splice(self):
|
|
hw_r, _ = hw_eval_multi(
|
|
"(defmacro add-all (&rest nums) `(+ ,@nums))"
|
|
" (add-all 1 2 3)"
|
|
)
|
|
ref_r, _ = ref_eval_multi(
|
|
"(defmacro add-all (&rest nums) `(+ ,@nums))"
|
|
" (add-all 1 2 3)"
|
|
)
|
|
assert hw_r == ref_r
|
|
|
|
def test_quasiquote(self):
|
|
hw = hw_eval("`(a ,x b)", {"x": 42})
|
|
ref = ref_eval("`(a ,x b)", {"x": 42})
|
|
# Both should produce [Symbol("a"), 42, Symbol("b")]
|
|
assert len(hw) == len(ref)
|
|
assert hw[1] == ref[1] == 42
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: set! and mutation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParitySetBang:
|
|
def test_set_bang(self):
|
|
hw_env = {"x": 1}
|
|
ref_env = {"x": 1}
|
|
hw_eval("(set! x 42)", hw_env)
|
|
ref_eval("(set! x 42)", ref_env)
|
|
assert hw_env["x"] == ref_env["x"] == 42
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Eval parity: TCO / recursion
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityTCO:
|
|
def test_recursive_sum(self):
|
|
src = """
|
|
(define sum (fn (n acc)
|
|
(if (<= n 0) acc
|
|
(sum (- n 1) (+ acc n)))))
|
|
(sum 1000 0)
|
|
"""
|
|
hw_r, _ = hw_eval_multi(src)
|
|
ref_r, _ = ref_eval_multi(src)
|
|
assert hw_r == ref_r == 500500
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Render parity: Basic HTML
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityRenderBasic:
|
|
def test_string(self):
|
|
assert_render_parity('"Hello"')
|
|
|
|
def test_number(self):
|
|
assert_render_parity("42")
|
|
|
|
def test_simple_tag(self):
|
|
assert_render_parity('(div "hello")')
|
|
|
|
def test_tag_with_class(self):
|
|
assert_render_parity('(div :class "container" "content")')
|
|
|
|
def test_nested_tags(self):
|
|
assert_render_parity('(div (p "para") (span "text"))')
|
|
|
|
def test_void_element(self):
|
|
assert_render_parity('(br)')
|
|
assert_render_parity('(img :src "test.png" :alt "test")')
|
|
|
|
def test_boolean_attr(self):
|
|
assert_render_parity('(input :type "checkbox" :checked true)')
|
|
assert_render_parity('(input :type "text" :disabled false)')
|
|
|
|
def test_fragment(self):
|
|
assert_render_parity('(<> (p "a") (p "b"))')
|
|
|
|
def test_nil_child(self):
|
|
assert_render_parity("(div nil)")
|
|
|
|
def test_escaped_text(self):
|
|
assert_render_parity('(div "<script>alert(1)</script>")')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Render parity: Special forms in render context
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityRenderForms:
|
|
def test_if(self):
|
|
assert_render_parity('(if true (span "yes") (span "no"))')
|
|
assert_render_parity('(if false (span "yes") (span "no"))')
|
|
|
|
def test_when(self):
|
|
assert_render_parity('(when true (p "shown"))')
|
|
assert_render_parity('(when false (p "hidden"))')
|
|
|
|
def test_cond(self):
|
|
assert_render_parity('(cond false (span "a") true (span "b") :else (span "c"))')
|
|
|
|
def test_let(self):
|
|
assert_render_parity('(let ((x "hello")) (p x))')
|
|
|
|
def test_map(self):
|
|
assert_render_parity('(map (fn (x) (li x)) (list "a" "b" "c"))')
|
|
|
|
def test_begin(self):
|
|
assert_render_parity('(begin (p "a") (p "b"))')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Render parity: Components
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityRenderComponents:
|
|
def test_simple_component(self):
|
|
src = '(defcomp ~card (&key title) (div :class "card" (h2 title)))'
|
|
call = '(~card :title "Hello")'
|
|
hw_env = {}
|
|
ref_env = {}
|
|
hw_eval(src, hw_env)
|
|
ref_eval(src, ref_env)
|
|
assert_render_parity(call, hw_env, ref_env)
|
|
|
|
def test_component_with_children(self):
|
|
src = '(defcomp ~box (&key &rest children) (div :class "box" children))'
|
|
call = '(~box (p "a") (p "b"))'
|
|
hw_env = {}
|
|
ref_env = {}
|
|
hw_eval(src, hw_env)
|
|
ref_eval(src, ref_env)
|
|
assert_render_parity(call, hw_env, ref_env)
|
|
|
|
def test_nested_components(self):
|
|
src = """
|
|
(defcomp ~inner (&key text) (span text))
|
|
(defcomp ~outer (&key) (div (~inner :text "hello")))
|
|
"""
|
|
hw_env = {}
|
|
ref_env = {}
|
|
hw_eval_multi(src, hw_env)
|
|
ref_eval_multi(src, ref_env)
|
|
assert_render_parity("(~outer)", hw_env, ref_env)
|
|
|
|
def test_component_with_when(self):
|
|
src = '(defcomp ~opt (&key show label) (when show (span label)))'
|
|
hw_env = {}
|
|
ref_env = {}
|
|
hw_eval(src, hw_env)
|
|
ref_eval(src, ref_env)
|
|
hw_html = hw_render('(~opt :show true :label "yes")', hw_env)
|
|
ref_html = ref_render('(~opt :show true :label "yes")', ref_env)
|
|
assert hw_html == ref_html
|
|
hw_html2 = hw_render('(~opt :show false :label "no")', hw_env)
|
|
ref_html2 = ref_render('(~opt :show false :label "no")', ref_env)
|
|
assert hw_html2 == ref_html2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Render parity: Macros in render
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityRenderMacros:
|
|
def test_macro_in_render(self):
|
|
src = '(defmacro bold (text) `(strong ,text))'
|
|
hw_env = {}
|
|
ref_env = {}
|
|
hw_eval(src, hw_env)
|
|
ref_eval(src, ref_env)
|
|
assert_render_parity('(bold "hello")', hw_env, ref_env)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Render parity: raw! and html: prefix
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityRenderSpecial:
|
|
def test_raw_html(self):
|
|
assert_render_parity('(raw! "<b>bold</b>")')
|
|
|
|
def test_style_attr(self):
|
|
assert_render_parity('(div :style "color:red" "text")')
|
|
|
|
def test_data_attr(self):
|
|
assert_render_parity('(div :data-id "123" "text")')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Deps parity
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityDeps:
|
|
def _make_envs(self, *sources):
|
|
hw_env = {}
|
|
ref_env = {}
|
|
for src in sources:
|
|
hw_eval_multi(src, hw_env)
|
|
ref_eval_multi(src, ref_env)
|
|
return hw_env, ref_env
|
|
|
|
def test_transitive_deps(self):
|
|
from shared.sx.deps import _transitive_deps_fallback
|
|
from shared.sx.ref.sx_ref import transitive_deps as ref_td
|
|
hw_env, ref_env = self._make_envs(
|
|
'(defcomp ~page (&key) (div (~layout)))',
|
|
'(defcomp ~layout (&key) (div (~header) (~footer)))',
|
|
'(defcomp ~header (&key) (nav "header"))',
|
|
'(defcomp ~footer (&key) (footer "footer"))',
|
|
)
|
|
hw_deps = _transitive_deps_fallback("~page", hw_env)
|
|
ref_deps = set(ref_td("~page", ref_env))
|
|
assert hw_deps == ref_deps
|
|
|
|
def test_compute_all_deps(self):
|
|
from shared.sx.deps import _compute_all_deps_fallback
|
|
from shared.sx.ref.sx_ref import compute_all_deps as ref_cad
|
|
hw_env, ref_env = self._make_envs(
|
|
'(defcomp ~a (&key) (div (~b)))',
|
|
'(defcomp ~b (&key) (div (~c)))',
|
|
'(defcomp ~c (&key) (span "leaf"))',
|
|
)
|
|
_compute_all_deps_fallback(hw_env)
|
|
ref_cad(ref_env)
|
|
for key in ("~a", "~b", "~c"):
|
|
hw_d = hw_env[key].deps or set()
|
|
ref_d = ref_env[key].deps
|
|
assert set(hw_d) == set(ref_d), f"Deps mismatch for {key}"
|
|
|
|
def test_scan_components_from_sx(self):
|
|
from shared.sx.deps import _scan_components_from_sx_fallback
|
|
from shared.sx.ref.sx_ref import scan_components_from_source as ref_sc
|
|
source = '(~card :title "hi" (~shared:misc/badge :label "new"))'
|
|
hw = _scan_components_from_sx_fallback(source)
|
|
ref = set(ref_sc(source))
|
|
assert hw == ref
|
|
|
|
def test_io_refs_parity(self):
|
|
from shared.sx.deps import _compute_all_io_refs_fallback
|
|
from shared.sx.ref.sx_ref import compute_all_io_refs as ref_cio
|
|
io_names = {"highlight", "app-url", "config", "fetch-data"}
|
|
hw_env, ref_env = self._make_envs(
|
|
'(defcomp ~page (&key) (div (~plans/environment-images/nav) (fetch-data "x")))',
|
|
'(defcomp ~plans/environment-images/nav (&key) (nav (app-url "/")))',
|
|
'(defcomp ~pure (&key) (div "hello"))',
|
|
)
|
|
_compute_all_io_refs_fallback(hw_env, io_names)
|
|
ref_cio(ref_env, list(io_names))
|
|
for key in ("~page", "~plans/environment-images/nav", "~pure"):
|
|
hw_refs = hw_env[key].io_refs or set()
|
|
ref_refs = ref_env[key].io_refs
|
|
assert set(hw_refs) == set(ref_refs), f"IO refs mismatch for {key}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Error parity
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParityErrors:
|
|
def test_undefined_symbol(self):
|
|
from shared.sx.types import EvalError as HwError
|
|
from shared.sx.ref.sx_ref import EvalError as RefError
|
|
with pytest.raises(HwError):
|
|
hw_eval("undefined_var")
|
|
with pytest.raises(RefError):
|
|
ref_eval("undefined_var")
|