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>
177 lines
6.1 KiB
Python
177 lines
6.1 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.ref.sx_ref import eval_expr as _eval, trampoline as _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 (~shared:misc/badge :label title)))')
|
|
comp = env["~card"]
|
|
refs = _scan_ast(comp.body)
|
|
assert refs == {"~shared:misc/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 (~shared:misc/badge)))',
|
|
'(defcomp ~shared:misc/badge (&key) (span "★"))',
|
|
)
|
|
deps = transitive_deps("~card", env)
|
|
assert deps == {"~shared:misc/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 (~shared:misc/badge)))',
|
|
'(defcomp ~shared:misc/badge (&key) (span "★"))',
|
|
)
|
|
deps = transitive_deps("card", env)
|
|
assert deps == {"~shared:misc/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 (~shared:misc/badge)))',
|
|
'(defcomp ~shared:misc/badge (&key) (span "★"))',
|
|
)
|
|
compute_all_deps(env)
|
|
assert env["~page"].deps == {"~card", "~shared:misc/badge"}
|
|
assert env["~card"].deps == {"~shared:misc/badge"}
|
|
assert env["~shared:misc/badge"].deps == set()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# scan_components_from_sx
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestScanComponentsFromSx:
|
|
def test_basic(self):
|
|
source = '(~card :title "hi" (~shared:misc/badge :label "new"))'
|
|
refs = scan_components_from_sx(source)
|
|
assert refs == {"~card", "~shared:misc/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 (~plans/environment-images/nav) (~footer)))',
|
|
'(defcomp ~plans/environment-images/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 "~plans/environment-images/nav" in needed
|
|
assert "~footer" in needed
|
|
assert "~unused" not in needed
|