masque/masque/test/test_library.py

227 lines
6.4 KiB
Python
Raw Normal View History

import pytest
2026-02-16 17:58:34 -08:00
from typing import cast, TYPE_CHECKING
from numpy.testing import assert_allclose
2026-02-15 12:36:13 -08:00
from ..library import Library, LazyLibrary
from ..pattern import Pattern
from ..error import LibraryError, PatternError
from ..ports import Port
from ..repetition import Grid
from ..file.utils import preflight
2026-02-16 17:58:34 -08:00
if TYPE_CHECKING:
from ..shapes import Polygon
2026-02-15 12:36:13 -08:00
def test_library_basic() -> None:
lib = Library()
pat = Pattern()
lib["cell1"] = pat
2026-02-15 12:36:13 -08:00
assert "cell1" in lib
assert lib["cell1"] is pat
assert len(lib) == 1
2026-02-15 12:36:13 -08:00
with pytest.raises(LibraryError):
lib["cell1"] = Pattern() # Overwriting not allowed
def test_library_tops() -> None:
lib = Library()
lib["child"] = Pattern()
lib["parent"] = Pattern()
lib["parent"].ref("child")
2026-02-15 12:36:13 -08:00
assert set(lib.tops()) == {"parent"}
assert lib.top() == "parent"
2026-02-15 12:36:13 -08:00
def test_library_dangling() -> None:
lib = Library()
lib["parent"] = Pattern()
lib["parent"].ref("missing")
2026-02-15 12:36:13 -08:00
assert lib.dangling_refs() == {"missing"}
2026-02-15 12:36:13 -08:00
def test_library_dangling_graph_modes() -> None:
lib = Library()
lib["parent"] = Pattern()
lib["parent"].ref("missing")
with pytest.raises(LibraryError, match="Dangling refs found"):
lib.child_graph()
with pytest.raises(LibraryError, match="Dangling refs found"):
lib.parent_graph()
with pytest.raises(LibraryError, match="Dangling refs found"):
lib.child_order()
assert lib.child_graph(dangling="ignore") == {"parent": set()}
assert lib.parent_graph(dangling="ignore") == {"parent": set()}
assert lib.child_order(dangling="ignore") == ["parent"]
assert lib.child_graph(dangling="include") == {"parent": {"missing"}, "missing": set()}
assert lib.parent_graph(dangling="include") == {"parent": set(), "missing": {"parent"}}
assert lib.child_order(dangling="include") == ["missing", "parent"]
def test_find_refs_with_dangling_modes() -> None:
lib = Library()
lib["target"] = Pattern()
mid = Pattern()
mid.ref("target", offset=(2, 0))
lib["mid"] = mid
top = Pattern()
top.ref("mid", offset=(5, 0))
top.ref("missing", offset=(9, 0))
lib["top"] = top
assert lib.find_refs_local("missing", dangling="ignore") == {}
assert lib.find_refs_global("missing", dangling="ignore") == {}
local_missing = lib.find_refs_local("missing", dangling="include")
assert set(local_missing) == {"top"}
assert_allclose(local_missing["top"][0], [[9, 0, 0, 0, 1]])
global_missing = lib.find_refs_global("missing", dangling="include")
assert_allclose(global_missing[("top", "missing")], [[9, 0, 0, 0, 1]])
with pytest.raises(LibraryError, match="missing"):
lib.find_refs_local("missing")
with pytest.raises(LibraryError, match="missing"):
lib.find_refs_global("missing")
global_target = lib.find_refs_global("target")
assert_allclose(global_target[("top", "mid", "target")], [[7, 0, 0, 0, 1]])
def test_preflight_prune_empty_preserves_dangling_policy(caplog: pytest.LogCaptureFixture) -> None:
def make_lib() -> Library:
lib = Library()
lib["empty"] = Pattern()
lib["top"] = Pattern()
lib["top"].ref("missing")
return lib
caplog.set_level("WARNING")
warned = preflight(make_lib(), allow_dangling_refs=None, prune_empty_patterns=True)
assert "empty" not in warned
assert any("Dangling refs found" in record.message for record in caplog.records)
allowed = preflight(make_lib(), allow_dangling_refs=True, prune_empty_patterns=True)
assert "empty" not in allowed
with pytest.raises(LibraryError, match="Dangling refs found"):
preflight(make_lib(), allow_dangling_refs=False, prune_empty_patterns=True)
2026-02-15 12:36:13 -08:00
def test_library_flatten() -> None:
lib = Library()
child = Pattern()
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
lib["child"] = child
2026-02-15 12:36:13 -08:00
parent = Pattern()
parent.ref("child", offset=(10, 10))
lib["parent"] = parent
2026-02-15 12:36:13 -08:00
flat_lib = lib.flatten("parent")
flat_parent = flat_lib["parent"]
2026-02-15 12:36:13 -08:00
assert not flat_parent.has_refs()
assert len(flat_parent.shapes[(1, 0)]) == 1
# Transformations are baked into vertices for Polygon
2026-02-16 17:58:34 -08:00
assert_vertices = cast("Polygon", flat_parent.shapes[(1, 0)][0]).vertices
assert tuple(assert_vertices[0]) == (10.0, 10.0)
2026-02-15 12:36:13 -08:00
def test_library_flatten_preserves_ports_only_child() -> None:
lib = Library()
child = Pattern(ports={"P1": Port((1, 2), 0)})
lib["child"] = child
parent = Pattern()
parent.ref("child", offset=(10, 10))
lib["parent"] = parent
flat_parent = lib.flatten("parent", flatten_ports=True)["parent"]
assert set(flat_parent.ports) == {"P1"}
assert cast("Port", flat_parent.ports["P1"]).rotation == 0
assert tuple(flat_parent.ports["P1"].offset) == (11.0, 12.0)
def test_library_flatten_repeated_ref_with_ports_raises() -> None:
lib = Library()
child = Pattern(ports={"P1": Port((1, 2), 0)})
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
lib["child"] = child
parent = Pattern()
parent.ref("child", repetition=Grid(a_vector=(10, 0), a_count=2))
lib["parent"] = parent
with pytest.raises(PatternError, match='Cannot flatten ports from repeated ref'):
lib.flatten("parent", flatten_ports=True)
2026-02-15 12:36:13 -08:00
def test_lazy_library() -> None:
lib = LazyLibrary()
called = 0
2026-02-15 12:36:13 -08:00
def make_pat() -> Pattern:
nonlocal called
called += 1
return Pattern()
2026-02-15 12:36:13 -08:00
lib["lazy"] = make_pat
assert called == 0
2026-02-15 12:36:13 -08:00
pat = lib["lazy"]
assert called == 1
assert isinstance(pat, Pattern)
2026-02-15 12:36:13 -08:00
# Second access should be cached
pat2 = lib["lazy"]
assert called == 1
assert pat is pat2
2026-02-15 12:36:13 -08:00
def test_library_rename() -> None:
lib = Library()
lib["old"] = Pattern()
lib["parent"] = Pattern()
lib["parent"].ref("old")
2026-02-15 12:36:13 -08:00
lib.rename("old", "new", move_references=True)
2026-02-15 12:36:13 -08:00
assert "old" not in lib
assert "new" in lib
assert "new" in lib["parent"].refs
assert "old" not in lib["parent"].refs
2026-02-15 12:36:13 -08:00
def test_library_subtree() -> None:
lib = Library()
lib["a"] = Pattern()
lib["b"] = Pattern()
lib["c"] = Pattern()
lib["a"].ref("b")
2026-02-15 12:36:13 -08:00
sub = lib.subtree("a")
assert "a" in sub
assert "b" in sub
assert "c" not in sub
2026-02-15 12:36:13 -08:00
def test_library_get_name() -> None:
lib = Library()
lib["cell"] = Pattern()
2026-02-15 12:36:13 -08:00
name1 = lib.get_name("cell")
assert name1 != "cell"
assert name1.startswith("cell")
2026-02-15 12:36:13 -08:00
name2 = lib.get_name("other")
assert name2 == "other"