2026-02-15 01:41:31 -08:00
|
|
|
import pytest
|
2026-02-16 17:58:34 -08:00
|
|
|
from typing import cast, TYPE_CHECKING
|
2026-03-30 22:10:26 -07:00
|
|
|
from numpy.testing import assert_allclose
|
2026-02-15 12:36:13 -08:00
|
|
|
from ..library import Library, LazyLibrary
|
2026-02-15 01:41:31 -08:00
|
|
|
from ..pattern import Pattern
|
2026-03-30 21:11:00 -07:00
|
|
|
from ..error import LibraryError, PatternError
|
|
|
|
|
from ..ports import Port
|
|
|
|
|
from ..repetition import Grid
|
2026-03-30 22:10:26 -07:00
|
|
|
from ..file.utils import preflight
|
2026-02-15 01:41:31 -08:00
|
|
|
|
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:
|
2026-02-15 01:41:31 -08:00
|
|
|
lib = Library()
|
|
|
|
|
pat = Pattern()
|
|
|
|
|
lib["cell1"] = pat
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -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:
|
2026-02-15 01:41:31 -08:00
|
|
|
lib = Library()
|
|
|
|
|
lib["child"] = Pattern()
|
|
|
|
|
lib["parent"] = Pattern()
|
|
|
|
|
lib["parent"].ref("child")
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -08:00
|
|
|
assert set(lib.tops()) == {"parent"}
|
|
|
|
|
assert lib.top() == "parent"
|
|
|
|
|
|
2026-02-15 12:36:13 -08:00
|
|
|
|
|
|
|
|
def test_library_dangling() -> None:
|
2026-02-15 01:41:31 -08:00
|
|
|
lib = Library()
|
|
|
|
|
lib["parent"] = Pattern()
|
|
|
|
|
lib["parent"].ref("missing")
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -08:00
|
|
|
assert lib.dangling_refs() == {"missing"}
|
|
|
|
|
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-03-30 22:10:26 -07: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:
|
2026-02-15 01:41:31 -08:00
|
|
|
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
|
|
|
|
2026-02-15 01:41:31 -08:00
|
|
|
parent = Pattern()
|
|
|
|
|
parent.ref("child", offset=(10, 10))
|
|
|
|
|
lib["parent"] = parent
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -08:00
|
|
|
flat_lib = lib.flatten("parent")
|
|
|
|
|
flat_parent = flat_lib["parent"]
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -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
|
2026-02-15 01:41:31 -08:00
|
|
|
assert tuple(assert_vertices[0]) == (10.0, 10.0)
|
|
|
|
|
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-03-30 21:11:00 -07: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)
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 21:17:33 -07:00
|
|
|
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:
|
2026-02-15 01:41:31 -08:00
|
|
|
lib = LazyLibrary()
|
|
|
|
|
called = 0
|
2026-02-15 12:36:13 -08:00
|
|
|
|
|
|
|
|
def make_pat() -> Pattern:
|
2026-02-15 01:41:31 -08:00
|
|
|
nonlocal called
|
|
|
|
|
called += 1
|
|
|
|
|
return Pattern()
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -08:00
|
|
|
lib["lazy"] = make_pat
|
|
|
|
|
assert called == 0
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -08:00
|
|
|
pat = lib["lazy"]
|
|
|
|
|
assert called == 1
|
|
|
|
|
assert isinstance(pat, Pattern)
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -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:
|
2026-02-15 01:41:31 -08:00
|
|
|
lib = Library()
|
|
|
|
|
lib["old"] = Pattern()
|
|
|
|
|
lib["parent"] = Pattern()
|
|
|
|
|
lib["parent"].ref("old")
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -08:00
|
|
|
lib.rename("old", "new", move_references=True)
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -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:
|
2026-02-15 01:41:31 -08:00
|
|
|
lib = Library()
|
|
|
|
|
lib["a"] = Pattern()
|
|
|
|
|
lib["b"] = Pattern()
|
|
|
|
|
lib["c"] = Pattern()
|
|
|
|
|
lib["a"].ref("b")
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -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:
|
2026-02-15 01:41:31 -08:00
|
|
|
lib = Library()
|
|
|
|
|
lib["cell"] = Pattern()
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -08:00
|
|
|
name1 = lib.get_name("cell")
|
|
|
|
|
assert name1 != "cell"
|
|
|
|
|
assert name1.startswith("cell")
|
2026-02-15 12:36:13 -08:00
|
|
|
|
2026-02-15 01:41:31 -08:00
|
|
|
name2 = lib.get_name("other")
|
|
|
|
|
assert name2 == "other"
|