"""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