346 lines
10 KiB
Python
346 lines
10 KiB
Python
import pytest
|
|
from typing import cast, TYPE_CHECKING
|
|
from numpy.testing import assert_allclose
|
|
from ..library import Library, LazyLibrary
|
|
from ..pattern import Pattern
|
|
from ..error import LibraryError, PatternError
|
|
from ..ports import Port
|
|
from ..repetition import Grid
|
|
from ..shapes import Arc, Ellipse, Path, Text
|
|
from ..file.utils import preflight
|
|
|
|
if TYPE_CHECKING:
|
|
from ..shapes import Polygon
|
|
|
|
|
|
def test_library_basic() -> None:
|
|
lib = Library()
|
|
pat = Pattern()
|
|
lib["cell1"] = pat
|
|
|
|
assert "cell1" in lib
|
|
assert lib["cell1"] is pat
|
|
assert len(lib) == 1
|
|
|
|
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")
|
|
|
|
assert set(lib.tops()) == {"parent"}
|
|
assert lib.top() == "parent"
|
|
|
|
|
|
def test_library_dangling() -> None:
|
|
lib = Library()
|
|
lib["parent"] = Pattern()
|
|
lib["parent"].ref("missing")
|
|
|
|
assert lib.dangling_refs() == {"missing"}
|
|
|
|
|
|
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)
|
|
|
|
|
|
def test_library_flatten() -> None:
|
|
lib = Library()
|
|
child = Pattern()
|
|
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
|
|
lib["child"] = child
|
|
|
|
parent = Pattern()
|
|
parent.ref("child", offset=(10, 10))
|
|
lib["parent"] = parent
|
|
|
|
flat_lib = lib.flatten("parent")
|
|
flat_parent = flat_lib["parent"]
|
|
|
|
assert not flat_parent.has_refs()
|
|
assert len(flat_parent.shapes[(1, 0)]) == 1
|
|
# Transformations are baked into vertices for Polygon
|
|
assert_vertices = cast("Polygon", flat_parent.shapes[(1, 0)][0]).vertices
|
|
assert tuple(assert_vertices[0]) == (10.0, 10.0)
|
|
|
|
|
|
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)
|
|
|
|
|
|
def test_library_flatten_dangling_ok_nested_preserves_dangling_refs() -> None:
|
|
lib = Library()
|
|
child = Pattern()
|
|
child.ref("missing")
|
|
lib["child"] = child
|
|
|
|
parent = Pattern()
|
|
parent.ref("child")
|
|
lib["parent"] = parent
|
|
|
|
flat = lib.flatten("parent", dangling_ok=True)
|
|
|
|
assert set(flat["child"].refs) == {"missing"}
|
|
assert flat["child"].has_refs()
|
|
assert set(flat["parent"].refs) == {"missing"}
|
|
assert flat["parent"].has_refs()
|
|
|
|
|
|
def test_lazy_library() -> None:
|
|
lib = LazyLibrary()
|
|
called = 0
|
|
|
|
def make_pat() -> Pattern:
|
|
nonlocal called
|
|
called += 1
|
|
return Pattern()
|
|
|
|
lib["lazy"] = make_pat
|
|
assert called == 0
|
|
|
|
pat = lib["lazy"]
|
|
assert called == 1
|
|
assert isinstance(pat, Pattern)
|
|
|
|
# Second access should be cached
|
|
pat2 = lib["lazy"]
|
|
assert called == 1
|
|
assert pat is pat2
|
|
|
|
|
|
def test_library_rename() -> None:
|
|
lib = Library()
|
|
lib["old"] = Pattern()
|
|
lib["parent"] = Pattern()
|
|
lib["parent"].ref("old")
|
|
|
|
lib.rename("old", "new", move_references=True)
|
|
|
|
assert "old" not in lib
|
|
assert "new" in lib
|
|
assert "new" in lib["parent"].refs
|
|
assert "old" not in lib["parent"].refs
|
|
|
|
|
|
def test_library_subtree() -> None:
|
|
lib = Library()
|
|
lib["a"] = Pattern()
|
|
lib["b"] = Pattern()
|
|
lib["c"] = Pattern()
|
|
lib["a"].ref("b")
|
|
|
|
sub = lib.subtree("a")
|
|
assert "a" in sub
|
|
assert "b" in sub
|
|
assert "c" not in sub
|
|
|
|
|
|
def test_library_get_name() -> None:
|
|
lib = Library()
|
|
lib["cell"] = Pattern()
|
|
|
|
name1 = lib.get_name("cell")
|
|
assert name1 != "cell"
|
|
assert name1.startswith("cell")
|
|
|
|
name2 = lib.get_name("other")
|
|
assert name2 == "other"
|
|
|
|
|
|
def test_library_dedup_shapes_does_not_merge_custom_capped_paths() -> None:
|
|
lib = Library()
|
|
pat = Pattern()
|
|
pat.shapes[(1, 0)] += [
|
|
Path(vertices=[[0, 0], [10, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(1, 2)),
|
|
Path(vertices=[[20, 0], [30, 0]], width=2, cap=Path.Cap.SquareCustom, cap_extensions=(3, 4)),
|
|
]
|
|
lib["top"] = pat
|
|
|
|
lib.dedup(norm_value=1, threshold=2)
|
|
|
|
assert not lib["top"].refs
|
|
assert len(lib["top"].shapes[(1, 0)]) == 2
|
|
|
|
|
|
def test_library_dedup_text_preserves_scale_and_mirror_flag() -> None:
|
|
lib = Library()
|
|
pat = Pattern()
|
|
pat.shapes[(1, 0)] += [
|
|
Text("A", 10, "dummy.ttf", offset=(0, 0)),
|
|
Text("A", 10, "dummy.ttf", offset=(100, 0)),
|
|
]
|
|
lib["top"] = pat
|
|
|
|
lib.dedup(exclude_types=(), norm_value=5, threshold=2)
|
|
|
|
target_name = next(iter(lib["top"].refs))
|
|
refs = lib["top"].refs[target_name]
|
|
assert [ref.mirrored for ref in refs] == [False, False]
|
|
assert [ref.scale for ref in refs] == [2.0, 2.0]
|
|
assert cast("Text", lib[target_name].shapes[(1, 0)][0]).height == 5
|
|
|
|
flat = lib.flatten("top")["top"]
|
|
assert [cast("Text", shape).height for shape in flat.shapes[(1, 0)]] == [10, 10]
|
|
|
|
|
|
def test_library_dedup_handles_arc_and_ellipse_labels() -> None:
|
|
lib = Library()
|
|
pat = Pattern()
|
|
pat.shapes[(1, 0)] += [
|
|
Arc(radii=(10, 20), angles=(0, 1), width=2, offset=(0, 0)),
|
|
Arc(radii=(10, 20), angles=(0, 1), width=2, offset=(50, 0)),
|
|
]
|
|
pat.shapes[(2, 0)] += [
|
|
Ellipse(radii=(10, 20), offset=(0, 0)),
|
|
Ellipse(radii=(10, 20), offset=(50, 0)),
|
|
]
|
|
lib["top"] = pat
|
|
|
|
lib.dedup(exclude_types=(), norm_value=1, threshold=2)
|
|
|
|
assert len(lib["top"].refs) == 2
|
|
assert lib["top"].shapes[(1, 0)] == []
|
|
assert lib["top"].shapes[(2, 0)] == []
|
|
|
|
flat = lib.flatten("top")["top"]
|
|
assert sum(isinstance(shape, Arc) for shape in flat.shapes[(1, 0)]) == 2
|
|
assert sum(isinstance(shape, Ellipse) for shape in flat.shapes[(2, 0)]) == 2
|
|
|
|
|
|
def test_library_dedup_handles_multiple_duplicate_groups() -> None:
|
|
from ..shapes import Circle
|
|
|
|
lib = Library()
|
|
pat = Pattern()
|
|
pat.shapes[(1, 0)] += [Circle(radius=1, offset=(0, 0)), Circle(radius=1, offset=(10, 0))]
|
|
pat.shapes[(2, 0)] += [Path(vertices=[[0, 0], [5, 0]], width=2), Path(vertices=[[10, 0], [15, 0]], width=2)]
|
|
lib["top"] = pat
|
|
|
|
lib.dedup(exclude_types=(), norm_value=1, threshold=2)
|
|
|
|
assert len(lib["top"].refs) == 2
|
|
assert all(len(refs) == 2 for refs in lib["top"].refs.values())
|
|
assert len(lib["top"].shapes[(1, 0)]) == 0
|
|
assert len(lib["top"].shapes[(2, 0)]) == 0
|
|
|
|
|
|
def test_library_dedup_uses_stable_target_names_per_label() -> None:
|
|
from ..shapes import Circle
|
|
|
|
lib = Library()
|
|
|
|
p1 = Pattern()
|
|
p1.shapes[(1, 0)] += [Circle(radius=1, offset=(0, 0)), Circle(radius=1, offset=(10, 0))]
|
|
lib["p1"] = p1
|
|
|
|
p2 = Pattern()
|
|
p2.shapes[(2, 0)] += [Path(vertices=[[0, 0], [5, 0]], width=2), Path(vertices=[[10, 0], [15, 0]], width=2)]
|
|
lib["p2"] = p2
|
|
|
|
lib.dedup(exclude_types=(), norm_value=1, threshold=2)
|
|
|
|
circle_target = next(iter(lib["p1"].refs))
|
|
path_target = next(iter(lib["p2"].refs))
|
|
|
|
assert circle_target != path_target
|
|
assert all(isinstance(shape, Circle) for shapes in lib[circle_target].shapes.values() for shape in shapes)
|
|
assert all(isinstance(shape, Path) for shapes in lib[path_target].shapes.values() for shape in shapes)
|