Files
ledgrab/server/tests/test_composite_nesting.py
T
alexei.dolgolyov 888f8fd16e refactor(types): PEP-604 union sweep + UP007/UP045 enforcement
ruff --select UP007,UP045 --fix converted ~1760 sites across the
backend: `Optional[T]` → `T | None`, `Union[X, Y]` → `X | Y`. The
remaining module-level alias targets that ruff conservatively skips
(BindableFloatInput, ColorList, DeviceConfig) were converted by hand
earlier in the pass. black -formatted the result so the wider unions
fit cleanly under the 100-char line budget.

pyproject.toml now sets [tool.ruff.lint] extend-select = ["UP007",
"UP045"] so future legacy imports fire CI on every push. The
pre-commit ruff hook was bumped from v0.8.0 -> v0.15.12 to recognise
UP045 (split off from UP007 in v0.13).
2026-05-23 01:21:44 +03:00

169 lines
7.1 KiB
Python

"""Tests for composite nesting: cycle detection, depth limits, transitive dependencies."""
import pytest
from ledgrab.storage.color_strip_store import ColorStripStore, MAX_COMPOSITE_DEPTH
from ledgrab.storage.database import Database
# ── Fixtures ──────────────────────────────────────────────────────────
@pytest.fixture
def tmp_db(tmp_path):
db = Database(tmp_path / "test.db")
yield db
db.close()
@pytest.fixture
def store(tmp_db):
return ColorStripStore(tmp_db)
def _create_static(store: ColorStripStore, name: str) -> str:
"""Create a single-color strip source, return its ID."""
source = store.create_source(name=name, source_type="single_color", colors=[[255, 0, 0]])
return source.id
def _create_composite(store: ColorStripStore, name: str, layer_ids: list[str]) -> str:
"""Create a composite source referencing the given layer IDs, return its ID."""
layers = [
{"source_id": sid, "blend_mode": "normal", "opacity": 1.0, "enabled": True}
for sid in layer_ids
]
source = store.create_source(name=name, source_type="composite", layers=layers)
return source.id
# ── Transitive Dependencies ──────────────────────────────────────────
class TestTransitiveDependencies:
def test_leaf_has_no_deps(self, store):
sid = _create_static(store, "leaf")
assert store.get_transitive_dependencies(sid) == set()
def test_composite_deps_are_its_layers(self, store):
a = _create_static(store, "A")
b = _create_static(store, "B")
comp = _create_composite(store, "comp", [a, b])
deps = store.get_transitive_dependencies(comp)
assert deps == {a, b}
def test_nested_composite_transitive_deps(self, store):
a = _create_static(store, "A")
b = _create_static(store, "B")
inner = _create_composite(store, "inner", [a])
outer = _create_composite(store, "outer", [inner, b])
deps = store.get_transitive_dependencies(outer)
assert deps == {inner, a, b}
def test_nonexistent_id_returns_empty(self, store):
assert store.get_transitive_dependencies("css_nonexist") == set()
# ── Nesting Depth ────────────────────────────────────────────────────
class TestNestingDepth:
def test_leaf_depth_is_zero(self, store):
sid = _create_static(store, "leaf")
assert store.get_nesting_depth(sid) == 0
def test_flat_composite_depth_is_one(self, store):
a = _create_static(store, "A")
comp = _create_composite(store, "comp", [a])
assert store.get_nesting_depth(comp) == 1
def test_two_level_nesting_depth(self, store):
a = _create_static(store, "A")
inner = _create_composite(store, "inner", [a])
outer = _create_composite(store, "outer", [inner])
assert store.get_nesting_depth(outer) == 2
def test_depth_uses_deepest_branch(self, store):
a = _create_static(store, "A")
b = _create_static(store, "B")
inner = _create_composite(store, "inner", [a])
outer = _create_composite(store, "outer", [inner, b])
# inner has depth 1, b has depth 0 → outer picks max (1) + 1 = 2
assert store.get_nesting_depth(outer) == 2
# ── Validate Nesting ─────────────────────────────────────────────────
class TestValidateNesting:
def test_self_reference_rejected(self, store):
a = _create_static(store, "A")
comp = _create_composite(store, "comp", [a])
with pytest.raises(ValueError, match="self-reference"):
store.validate_nesting(comp, [comp])
def test_direct_cycle_rejected(self, store):
"""A → B → A should be rejected."""
a = _create_static(store, "A")
comp_b = _create_composite(store, "B", [a])
comp_a = _create_composite(store, "A-comp", [comp_b])
# Try to add A-comp as a layer of B → would create B → A-comp → B
with pytest.raises(ValueError, match="circular reference"):
store.validate_nesting(comp_b, [comp_a])
def test_transitive_cycle_rejected(self, store):
"""A → B → C, then trying C → A should be rejected."""
leaf = _create_static(store, "leaf")
c = _create_composite(store, "C", [leaf])
b = _create_composite(store, "B", [c])
a = _create_composite(store, "A", [b])
# Try to add A as a layer of C → C → A → B → C
with pytest.raises(ValueError, match="circular reference"):
store.validate_nesting(c, [a])
def test_valid_nesting_accepted(self, store):
"""A → B where B is composite with only leaves should be fine."""
leaf = _create_static(store, "leaf")
inner = _create_composite(store, "inner", [leaf])
outer_id = _create_composite(store, "outer", [])
# Should not raise
store.validate_nesting(outer_id, [inner])
def test_depth_limit_exceeded(self, store):
"""Build a chain deeper than MAX_COMPOSITE_DEPTH and verify rejection."""
# Build chain: leaf → comp1 → comp2 → ... → compN
prev = _create_static(store, "leaf")
for i in range(MAX_COMPOSITE_DEPTH):
prev = _create_composite(store, f"comp_{i}", [prev])
# prev is now at depth MAX_COMPOSITE_DEPTH
# Trying to wrap it in one more composite should fail
wrapper = _create_composite(store, "wrapper", [])
with pytest.raises(ValueError, match="Nesting depth"):
store.validate_nesting(wrapper, [prev])
def test_depth_at_limit_accepted(self, store):
"""A chain exactly at MAX_COMPOSITE_DEPTH should be accepted."""
prev = _create_static(store, "leaf")
for i in range(MAX_COMPOSITE_DEPTH - 1):
prev = _create_composite(store, f"comp_{i}", [prev])
# prev is at depth MAX_COMPOSITE_DEPTH - 1
wrapper = _create_composite(store, "wrapper", [])
# 1 + (MAX_COMPOSITE_DEPTH - 1) = MAX_COMPOSITE_DEPTH → should pass
store.validate_nesting(wrapper, [prev])
def test_multiple_children_validates_all(self, store):
"""If one child creates a cycle, validation fails even if others are fine."""
leaf = _create_static(store, "leaf")
comp = _create_composite(store, "comp", [leaf])
with pytest.raises(ValueError, match="self-reference"):
store.validate_nesting(comp, [leaf, comp])
def test_empty_children_accepted(self, store):
"""Empty children list should always pass."""
comp = _create_composite(store, "comp", [])
store.validate_nesting(comp, [])
def test_none_children_skipped(self, store):
"""Empty-string child IDs should be skipped."""
comp = _create_composite(store, "comp", [])
store.validate_nesting(comp, ["", ""])