Files
rose-ash/shared/sx/tests/generate_golden.py
giles bb34b4948b OCaml raw! in HTML renderer + SX_USE_OCAML env promotion + golden tests
- sx_render.ml: add raw! handler to HTML renderer (inject pre-rendered
  content without HTML escaping)
- docker-compose.yml: move SX_USE_OCAML/SX_OCAML_BIN to shared env
  (available to all services, not just sx_docs)
- hosts/ocaml/Dockerfile: OCaml kernel build stage
- shared/sx/tests/: golden test data + generator for OCaml render tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:21:04 +00:00

169 lines
6.3 KiB
Python

"""Generate golden HTML/aser test data from the Python evaluator.
Evaluates curated component calls through the Python ref evaluator and
writes golden_data.json — a list of {name, sx_input, expected_html,
expected_aser} triples.
Usage:
python3 shared/sx/tests/generate_golden.py
"""
import asyncio
import json
import os
import sys
_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
if _project_root not in sys.path:
sys.path.insert(0, _project_root)
# Curated test cases — component calls and plain expressions that
# exercise the rendering pipeline. Each entry is (name, sx_input).
GOLDEN_CASES = [
# --- Basic HTML rendering ---
("div_simple", '(div "hello")'),
("div_class", '(div :class "card" "content")'),
("p_text", '(p "paragraph text")'),
("nested_tags", '(div (p "a") (p "b"))'),
("void_br", "(br)"),
("void_hr", "(hr)"),
("void_img", '(img :src "/photo.jpg" :alt "A photo")'),
("void_input", '(input :type "text" :name "q" :placeholder "Search")'),
("fragment", '(<> (p "a") (p "b"))'),
("boolean_attr", '(input :type "checkbox" :checked true)'),
("nil_attr", '(div :class nil "content")'),
("empty_string_attr", '(div :class "" "visible")'),
# --- Control flow ---
("if_true", '(if true (p "yes") (p "no"))'),
("if_false", '(if false (p "yes") (p "no"))'),
("when_true", '(when true (p "shown"))'),
("when_false", '(when false (p "hidden"))'),
("let_binding", '(let ((x "hi")) (p x))'),
("let_multiple", '(let ((x "a") (y "b")) (div (p x) (p y)))'),
("cond_form", '(cond (= 1 2) (p "no") (= 1 1) (p "yes") :else (p "default"))'),
("case_form", '(case "b" "a" "A" "b" "B" :else "?")'),
("and_short", '(and true false)'),
("or_short", '(or false "found")'),
# --- Higher-order forms ---
("map_li", '(map (fn (x) (li x)) (list "a" "b" "c"))'),
("filter_even", '(filter even? (list 1 2 3 4 5))'),
("reduce_sum", '(reduce + 0 (list 1 2 3 4 5))'),
# --- String operations ---
("str_concat", '(str "hello" " " "world")'),
("str_upcase", '(upcase "hello")'),
# --- Component definitions and calls ---
("defcomp_simple",
'(do (defcomp ~test-badge (&key label) (span :class "badge" label)) (~test-badge :label "New"))'),
("defcomp_children",
'(do (defcomp ~test-wrap (&rest children) (div :class "wrap" children)) (~test-wrap (p "inside")))'),
("defcomp_multi_key",
'(do (defcomp ~test-card (&key title subtitle) (div (h2 title) (when subtitle (p subtitle)))) '
'(~test-card :title "Title" :subtitle "Sub"))'),
("defcomp_no_optional",
'(do (defcomp ~test-card2 (&key title subtitle) (div (h2 title) (when subtitle (p subtitle)))) '
'(~test-card2 :title "Only Title"))'),
# --- Nested components ---
("nested_components",
'(do (defcomp ~inner (&key text) (span :class "inner" text)) '
'(defcomp ~outer (&key title &rest children) (div :class "outer" (h2 title) children)) '
'(~outer :title "Hello" (~inner :text "World")))'),
# --- Macros ---
("macro_unless",
'(do (defmacro unless (cond &rest body) (list \'if (list \'not cond) (cons \'do body))) '
'(unless false (p "shown")))'),
# --- Special rendering patterns ---
("do_block", '(div (do (p "a") (p "b")))'),
("nil_child", '(div nil "after-nil")'),
("number_child", '(div 42)'),
("bool_child", '(div true)'),
# --- Data attributes ---
("data_attr", '(div :data-id "123" :data-name "test" "content")'),
# --- raw! (inject pre-rendered HTML) ---
("raw_simple", '(raw! "<b>bold</b>")'),
("raw_in_div", '(div (raw! "<em>italic</em>"))'),
("raw_component",
'(do (defcomp ~rich (&key html) (raw! html)) '
'(~rich :html "<p>CMS</p>"))'),
# --- Shared template components (if available) ---
("misc_error_inline",
'(do (defcomp ~shared:misc/error-inline (&key (message :as string)) '
'(div :class "text-red-600 text-sm" message)) '
'(~shared:misc/error-inline :message "Something went wrong"))'),
("misc_notification_badge",
'(do (defcomp ~shared:misc/notification-badge (&key (count :as number)) '
'(span :class "bg-red-500 text-white text-xs rounded-full px-1.5 py-0.5" count)) '
'(~shared:misc/notification-badge :count 5))'),
("misc_cache_cleared",
'(do (defcomp ~shared:misc/cache-cleared (&key (time-str :as string)) '
'(span :class "text-green-600 font-bold" "Cache cleared at " time-str)) '
'(~shared:misc/cache-cleared :time-str "12:00"))'),
("misc_error_list_item",
'(do (defcomp ~shared:misc/error-list-item (&key (message :as string)) (li message)) '
'(~shared:misc/error-list-item :message "Bad input"))'),
("misc_fragment_error",
'(do (defcomp ~shared:misc/fragment-error (&key (service :as string)) '
'(p :class "text-sm text-red-600" "Service " (b service) " is unavailable.")) '
'(~shared:misc/fragment-error :service "blog"))'),
]
def _generate_html(sx_input: str) -> str:
"""Evaluate SX and render to HTML using the Python evaluator."""
from shared.sx.ref.sx_ref import evaluate, render
from shared.sx.parser import parse_all
env = {}
exprs = parse_all(sx_input)
# For multi-expression inputs (defcomp then call), use evaluate
# to process all defs, then render the final expression
if len(exprs) > 1:
# Evaluate all expressions — defs install into env
result = None
for expr in exprs:
result = evaluate(expr, env)
# Render the final result
return render(result, env)
else:
# Single expression — render directly
return render(exprs[0], env)
def main():
golden = []
ok = 0
failed = 0
for name, sx_input in GOLDEN_CASES:
try:
html = _generate_html(sx_input)
golden.append({
"name": name,
"sx_input": sx_input,
"expected_html": html,
})
ok += 1
except Exception as e:
print(f" SKIP {name}: {e}")
failed += 1
outpath = os.path.join(os.path.dirname(__file__), "golden_data.json")
with open(outpath, "w") as f:
json.dump(golden, f, indent=2, ensure_ascii=False)
print(f"Generated {ok} golden cases ({failed} skipped) → {outpath}")
if __name__ == "__main__":
main()