Files
rose-ash/shared/sx/tests/test_deps.py
giles 4c97b03dda Wire deps.sx into both bootstrappers, rebootstrap Python + JS
deps.sx is now a spec module that both bootstrap_py.py and bootstrap_js.py
can include via --spec-modules deps. Platform functions (component-deps,
component-set-deps!, component-css-classes, env-components, regex-find-all,
scan-css-classes) implemented natively in both Python and JS.

- Fix deps.sx: env-get-or → env-get, extract nested define to top-level
- bootstrap_py.py: SPEC_MODULES, PLATFORM_DEPS_PY, mangle entries, CLI arg
- bootstrap_js.py: SPEC_MODULES, PLATFORM_DEPS_JS, mangle entries, CLI arg
- Regenerate sx_ref.py and sx-ref.js with deps module
- deps.py: thin dispatcher (SX_USE_REF=1 → bootstrapped, else fallback)
- scan_components_from_sx now returns ~prefixed names (consistent with spec)

Verified: 541 Python tests pass, JS deps tested with Node.js, both code
paths (fallback + bootstrapped) produce identical results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:55:32 +00:00

177 lines
5.8 KiB
Python

"""Tests for the component dependency analyzer."""
import pytest
from shared.sx.parser import parse_all
from shared.sx.types import Component, Macro, Symbol
from shared.sx.deps import (
_scan_ast,
transitive_deps,
compute_all_deps,
scan_components_from_sx,
components_needed,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def make_env(*sx_sources: str) -> dict:
"""Parse and evaluate component definitions into an env dict."""
from shared.sx.evaluator import _eval, _trampoline
env: dict = {}
for source in sx_sources:
exprs = parse_all(source)
for expr in exprs:
_trampoline(_eval(expr, env))
return env
# ---------------------------------------------------------------------------
# _scan_ast
# ---------------------------------------------------------------------------
class TestScanAst:
def test_simple_component_ref(self):
env = make_env('(defcomp ~card (&key title) (div (~badge :label title)))')
comp = env["~card"]
refs = _scan_ast(comp.body)
assert refs == {"~badge"}
def test_no_refs(self):
env = make_env('(defcomp ~plain (&key text) (div :class "p-4" text))')
comp = env["~plain"]
refs = _scan_ast(comp.body)
assert refs == set()
def test_multiple_refs(self):
env = make_env(
'(defcomp ~page (&key title) (div (~header :title title) (~footer)))'
)
comp = env["~page"]
refs = _scan_ast(comp.body)
assert refs == {"~header", "~footer"}
def test_nested_in_control_flow(self):
env = make_env(
'(defcomp ~card (&key big) '
' (if big (~big-card) (~small-card)))'
)
comp = env["~card"]
refs = _scan_ast(comp.body)
assert refs == {"~big-card", "~small-card"}
def test_refs_in_dict(self):
env = make_env(
'(defcomp ~wrap (&key) (div {:slot (~inner)}))'
)
comp = env["~wrap"]
refs = _scan_ast(comp.body)
assert refs == {"~inner"}
# ---------------------------------------------------------------------------
# transitive_deps
# ---------------------------------------------------------------------------
class TestTransitiveDeps:
def test_direct_dep(self):
env = make_env(
'(defcomp ~card (&key) (div (~badge)))',
'(defcomp ~badge (&key) (span ""))',
)
deps = transitive_deps("~card", env)
assert deps == {"~badge"}
def test_transitive(self):
env = make_env(
'(defcomp ~page (&key) (div (~layout)))',
'(defcomp ~layout (&key) (div (~header) (~footer)))',
'(defcomp ~header (&key) (nav "header"))',
'(defcomp ~footer (&key) (footer "footer"))',
)
deps = transitive_deps("~page", env)
assert deps == {"~layout", "~header", "~footer"}
def test_circular(self):
"""Circular deps should not cause infinite recursion."""
env = make_env(
'(defcomp ~a (&key) (div (~b)))',
'(defcomp ~b (&key) (div (~a)))',
)
deps = transitive_deps("~a", env)
assert deps == {"~b"}
def test_no_deps(self):
env = make_env('(defcomp ~leaf (&key) (span "hi"))')
deps = transitive_deps("~leaf", env)
assert deps == set()
def test_missing_component(self):
"""Referencing a component not in env should not crash."""
env = make_env('(defcomp ~card (&key) (div (~unknown)))')
deps = transitive_deps("~card", env)
assert "~unknown" in deps
def test_without_tilde_prefix(self):
env = make_env(
'(defcomp ~card (&key) (div (~badge)))',
'(defcomp ~badge (&key) (span ""))',
)
deps = transitive_deps("card", env)
assert deps == {"~badge"}
# ---------------------------------------------------------------------------
# compute_all_deps
# ---------------------------------------------------------------------------
class TestComputeAllDeps:
def test_sets_deps_on_components(self):
env = make_env(
'(defcomp ~page (&key) (div (~card)))',
'(defcomp ~card (&key) (div (~badge)))',
'(defcomp ~badge (&key) (span ""))',
)
compute_all_deps(env)
assert env["~page"].deps == {"~card", "~badge"}
assert env["~card"].deps == {"~badge"}
assert env["~badge"].deps == set()
# ---------------------------------------------------------------------------
# scan_components_from_sx
# ---------------------------------------------------------------------------
class TestScanComponentsFromSx:
def test_basic(self):
source = '(~card :title "hi" (~badge :label "new"))'
refs = scan_components_from_sx(source)
assert refs == {"~card", "~badge"}
def test_no_components(self):
source = '(div :class "p-4" (p "hello"))'
refs = scan_components_from_sx(source)
assert refs == set()
# ---------------------------------------------------------------------------
# components_needed
# ---------------------------------------------------------------------------
class TestComponentsNeeded:
def test_page_with_deps(self):
env = make_env(
'(defcomp ~page-layout (&key) (div (~nav) (~footer)))',
'(defcomp ~nav (&key) (nav "nav"))',
'(defcomp ~footer (&key) (footer "footer"))',
'(defcomp ~unused (&key) (div "not needed"))',
)
compute_all_deps(env)
page_sx = '(~page-layout)'
needed = components_needed(page_sx, env)
assert "~page-layout" in needed
assert "~nav" in needed
assert "~footer" in needed
assert "~unused" not in needed