Files
rose-ash/shared/sx/tests/test_evaluator.py
giles ab75e505a8
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Add macros, declarative handlers (defhandler), and convert all fragment routes to sx
Phase 1 — Macros: defmacro + quasiquote syntax (`, ,, ,@) in parser,
evaluator, HTML renderer, and JS mirror. Macro type, expansion, and
round-trip serialization.

Phase 2 — Expanded primitives: app-url, url-for, asset-url, config,
format-date, parse-int (pure); service, request-arg, request-path,
nav-tree, get-children (I/O); jinja-global, relations-from (pure).
Updated _io_service to accept (service "registry-name" "method" :kwargs)
with auto kebab→snake conversion. DTO-to-dict now expands datetime fields
into year/month/day convenience keys. Tuple returns converted to lists.

Phase 3 — Declarative handlers: HandlerDef type, defhandler special form,
handler registry (service → name → HandlerDef), async evaluator+renderer
(async_eval.py) that awaits I/O primitives inline within control flow.
Handler loading from .sx files, execute_handler, blueprint factory.

Phase 4 — Convert all fragment routes: 13 Python fragment handlers across
8 services replaced with declarative .sx handler files. All routes.py
simplified to uniform sx dispatch pattern. Two Jinja HTML handlers
(events/container-cards, events/account-page) kept as Python.

New files: shared/sx/async_eval.py, shared/sx/handlers.py,
shared/sx/tests/test_handlers.py, plus 13 handler .sx files under
{service}/sx/handlers/. MarketService.product_by_slug() added.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:22:18 +00:00

406 lines
12 KiB
Python

"""Tests for the s-expression evaluator."""
import pytest
from shared.sx import parse, evaluate, EvalError, Symbol, Keyword, NIL
from shared.sx.types import Lambda, Component, Macro, HandlerDef
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def ev(text, env=None):
"""Parse and evaluate a single expression."""
return evaluate(parse(text), env)
# ---------------------------------------------------------------------------
# Literals and lookups
# ---------------------------------------------------------------------------
class TestLiterals:
def test_int(self):
assert ev("42") == 42
def test_string(self):
assert ev('"hello"') == "hello"
def test_true(self):
assert ev("true") is True
def test_nil(self):
assert ev("nil") is NIL
def test_symbol_lookup(self):
assert ev("x", {"x": 10}) == 10
def test_undefined_symbol(self):
with pytest.raises(EvalError, match="Undefined symbol"):
ev("xyz")
def test_keyword_evaluates_to_name(self):
assert ev(":foo") == "foo"
# ---------------------------------------------------------------------------
# Arithmetic
# ---------------------------------------------------------------------------
class TestArithmetic:
def test_add(self):
assert ev("(+ 1 2 3)") == 6
def test_sub(self):
assert ev("(- 10 3)") == 7
def test_negate(self):
assert ev("(- 5)") == -5
def test_mul(self):
assert ev("(* 2 3 4)") == 24
def test_div(self):
assert ev("(/ 10 4)") == 2.5
def test_mod(self):
assert ev("(mod 7 3)") == 1
def test_clamp(self):
assert ev("(clamp 15 0 10)") == 10
assert ev("(clamp -5 0 10)") == 0
assert ev("(clamp 5 0 10)") == 5
# ---------------------------------------------------------------------------
# Comparison and predicates
# ---------------------------------------------------------------------------
class TestComparison:
def test_eq(self):
assert ev("(= 1 1)") is True
assert ev("(= 1 2)") is False
def test_lt_gt(self):
assert ev("(< 1 2)") is True
assert ev("(> 2 1)") is True
def test_predicates(self):
assert ev("(odd? 3)") is True
assert ev("(even? 4)") is True
assert ev("(zero? 0)") is True
assert ev("(nil? nil)") is True
assert ev('(string? "hi")') is True
assert ev("(number? 42)") is True
assert ev("(list? (list 1))") is True
assert ev("(dict? {:a 1})") is True
# ---------------------------------------------------------------------------
# Special forms
# ---------------------------------------------------------------------------
class TestSpecialForms:
def test_if_true(self):
assert ev("(if true 1 2)") == 1
def test_if_false(self):
assert ev("(if false 1 2)") == 2
def test_if_no_else(self):
assert ev("(if false 1)") is NIL
def test_when_true(self):
assert ev("(when true 42)") == 42
def test_when_false(self):
assert ev("(when false 42)") is NIL
def test_and_short_circuit(self):
assert ev("(and true true 3)") == 3
assert ev("(and true false 3)") is False
def test_or_short_circuit(self):
assert ev("(or false false 3)") == 3
assert ev("(or false 2 3)") == 2
def test_let_scheme_style(self):
assert ev("(let ((x 10) (y 20)) (+ x y))") == 30
def test_let_clojure_style(self):
assert ev("(let (x 10 y 20) (+ x y))") == 30
def test_let_sequential(self):
assert ev("(let ((x 1) (y (+ x 1))) y)") == 2
def test_begin(self):
assert ev("(begin 1 2 3)") == 3
def test_quote(self):
result = ev("(quote (a b c))")
assert result == [Symbol("a"), Symbol("b"), Symbol("c")]
def test_cond_clojure(self):
assert ev("(cond false 1 true 2 :else 3)") == 2
def test_cond_else(self):
assert ev("(cond false 1 false 2 :else 99)") == 99
def test_case(self):
assert ev('(case 2 1 "one" 2 "two" :else "other")') == "two"
def test_thread_first(self):
assert ev("(-> 5 (+ 3) (* 2))") == 16
def test_define(self):
env = {}
ev("(define x 42)", env)
assert env["x"] == 42
# ---------------------------------------------------------------------------
# Lambda
# ---------------------------------------------------------------------------
class TestLambda:
def test_create_and_call(self):
assert ev("((fn (x) (* x x)) 5)") == 25
def test_closure(self):
result = ev("(let ((a 10)) ((fn (x) (+ x a)) 5))")
assert result == 15
def test_higher_order(self):
result = ev("(let ((double (fn (x) (* x 2)))) (double 7))")
assert result == 14
# ---------------------------------------------------------------------------
# Collections
# ---------------------------------------------------------------------------
class TestCollections:
def test_list_constructor(self):
assert ev("(list 1 2 3)") == [1, 2, 3]
def test_dict_constructor(self):
assert ev("(dict :a 1 :b 2)") == {"a": 1, "b": 2}
def test_get_dict(self):
assert ev('(get {:a 1 :b 2} "a")') == 1
def test_get_list(self):
assert ev("(get (list 10 20 30) 1)") == 20
def test_first_last_rest(self):
assert ev("(first (list 1 2 3))") == 1
assert ev("(last (list 1 2 3))") == 3
assert ev("(rest (list 1 2 3))") == [2, 3]
def test_len(self):
assert ev("(len (list 1 2 3))") == 3
def test_concat(self):
assert ev("(concat (list 1 2) (list 3 4))") == [1, 2, 3, 4]
def test_cons(self):
assert ev("(cons 0 (list 1 2))") == [0, 1, 2]
def test_keys_vals(self):
assert ev("(keys {:a 1 :b 2})") == ["a", "b"]
assert ev("(vals {:a 1 :b 2})") == [1, 2]
def test_merge(self):
assert ev("(merge {:a 1} {:b 2} {:a 3})") == {"a": 3, "b": 2}
def test_assoc(self):
assert ev('(assoc {:a 1} :b 2)') == {"a": 1, "b": 2}
def test_dissoc(self):
assert ev('(dissoc {:a 1 :b 2} :a)') == {"b": 2}
def test_empty(self):
assert ev("(empty? (list))") is True
assert ev("(empty? (list 1))") is False
assert ev("(empty? nil)") is True
def test_contains(self):
assert ev('(contains? {:a 1} "a")') is True
assert ev("(contains? (list 1 2 3) 2)") is True
# ---------------------------------------------------------------------------
# Higher-order forms
# ---------------------------------------------------------------------------
class TestHigherOrder:
def test_map(self):
assert ev("(map (fn (x) (* x x)) (list 1 2 3 4))") == [1, 4, 9, 16]
def test_map_indexed(self):
result = ev("(map-indexed (fn (i x) (+ i x)) (list 10 20 30))")
assert result == [10, 21, 32]
def test_filter(self):
assert ev("(filter (fn (x) (> x 2)) (list 1 2 3 4))") == [3, 4]
def test_reduce(self):
assert ev("(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3))") == 6
def test_some(self):
assert ev("(some (fn (x) (if (> x 3) x nil)) (list 1 2 4 5))") == 4
def test_every(self):
assert ev("(every? (fn (x) (> x 0)) (list 1 2 3))") is True
assert ev("(every? (fn (x) (> x 2)) (list 1 2 3))") is False
# ---------------------------------------------------------------------------
# Strings
# ---------------------------------------------------------------------------
class TestStrings:
def test_str(self):
assert ev('(str "hello" " " "world")') == "hello world"
def test_str_numbers(self):
assert ev('(str "val=" 42)') == "val=42"
def test_upper_lower(self):
assert ev('(upper "hello")') == "HELLO"
assert ev('(lower "HELLO")') == "hello"
def test_join(self):
assert ev('(join ", " (list "a" "b" "c"))') == "a, b, c"
def test_split(self):
assert ev('(split "a,b,c" ",")') == ["a", "b", "c"]
# ---------------------------------------------------------------------------
# defcomp
# ---------------------------------------------------------------------------
class TestDefcomp:
def test_basic_component(self):
env = {}
ev("(defcomp ~card (&key title) title)", env)
assert isinstance(env["~card"], Component)
assert env["~card"].name == "card"
def test_component_call(self):
env = {}
ev("(defcomp ~greeting (&key name) (str \"Hello, \" name \"!\"))", env)
result = ev('(~greeting :name "Alice")', env)
assert result == "Hello, Alice!"
def test_component_with_children(self):
env = {}
ev("(defcomp ~wrapper (&key class &rest children) (list class children))", env)
result = ev('(~wrapper :class "box" 1 2 3)', env)
assert result == ["box", [1, 2, 3]]
def test_component_missing_kwarg_is_nil(self):
env = {}
ev("(defcomp ~opt (&key x y) (list x y))", env)
result = ev("(~opt :x 1)", env)
assert result == [1, NIL]
# ---------------------------------------------------------------------------
# Dict literal evaluation
# ---------------------------------------------------------------------------
class TestDictLiteral:
def test_dict_values_evaluated(self):
assert ev("{:a (+ 1 2) :b (* 3 4)}") == {"a": 3, "b": 12}
# ---------------------------------------------------------------------------
# set!
# ---------------------------------------------------------------------------
class TestSetBang:
def test_set_bang(self):
env = {"x": 1}
ev("(set! x 42)", env)
assert env["x"] == 42
# ---------------------------------------------------------------------------
# Macros
# ---------------------------------------------------------------------------
class TestMacro:
def test_defmacro_creates_macro(self):
env = {}
ev("(defmacro double (x) `(+ ,x ,x))", env)
assert isinstance(env["double"], Macro)
assert env["double"].name == "double"
def test_simple_expansion(self):
env = {}
ev("(defmacro double (x) `(+ ,x ,x))", env)
assert ev("(double 5)", env) == 10
def test_quasiquote_with_splice(self):
env = {}
ev("(defmacro add-all (&rest nums) `(+ ,@nums))", env)
assert ev("(add-all 1 2 3)", env) == 6
def test_rest_param(self):
env = {}
ev("(defmacro my-list (&rest items) `(list ,@items))", env)
assert ev("(my-list 1 2 3)", env) == [1, 2, 3]
def test_macro_with_let(self):
env = {}
ev("(defmacro bind-and-add (name val) `(let ((,name ,val)) (+ ,name 1)))", env)
assert ev("(bind-and-add x 10)", env) == 11
def test_quasiquote_standalone(self):
"""Quasiquote without defmacro works for template expansion."""
env = {"x": 42}
result = ev("`(a ,x b)", env)
assert result == [Symbol("a"), 42, Symbol("b")]
def test_quasiquote_splice(self):
env = {"rest": [1, 2, 3]}
result = ev("`(a ,@rest b)", env)
assert result == [Symbol("a"), 1, 2, 3, Symbol("b")]
def test_macro_wrong_arity(self):
"""Macro with too few args gets NIL for missing params."""
env = {}
ev("(defmacro needs-two (a b) `(+ ,a ,b))", env)
# Calling with 1 arg — b becomes NIL
with pytest.raises(Exception):
ev("(needs-two 5)", env)
def test_macro_in_html_render(self):
"""Macros expand correctly in HTML render context."""
from shared.sx.html import render as html_render
env = {}
ev('(defmacro bold (text) `(strong ,text))', env)
expr = parse('(bold "hello")')
result = html_render(expr, env)
assert result == "<strong>hello</strong>"
# ---------------------------------------------------------------------------
# defhandler
# ---------------------------------------------------------------------------
class TestDefhandler:
def test_defhandler_creates_handler(self):
env = {}
ev("(defhandler link-card (&key slug keys) slug)", env)
assert isinstance(env["handler:link-card"], HandlerDef)
assert env["handler:link-card"].name == "link-card"
assert env["handler:link-card"].params == ["slug", "keys"]
def test_defhandler_body_preserved(self):
env = {}
ev("(defhandler test-handler (&key id) (str id))", env)
handler = env["handler:test-handler"]
assert handler.body is not None