1210 lines
40 KiB
Python
1210 lines
40 KiB
Python
import pytest
|
|
from collections.abc import Mapping, MutableMapping
|
|
from typing import cast, TYPE_CHECKING
|
|
from numpy.testing import assert_allclose
|
|
from ..library import (
|
|
IBorrowing, INameView, IMaterializable, LayerMappedView, Library, LibraryView,
|
|
LazyLibrary, OverlayLibrary, PortLoadView,
|
|
)
|
|
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, preflight_source_aware
|
|
|
|
if TYPE_CHECKING:
|
|
from ..shapes import Polygon
|
|
|
|
|
|
def test_library_basic() -> None:
|
|
lib = Library()
|
|
pat = Pattern()
|
|
lib["cell1"] = pat
|
|
|
|
assert isinstance(lib, INameView)
|
|
assert "cell1" in lib
|
|
assert lib["cell1"] is pat
|
|
assert len(lib) == 1
|
|
|
|
with pytest.raises(LibraryError):
|
|
lib["cell1"] = Pattern() # Overwriting not allowed
|
|
|
|
|
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary, OverlayLibrary])
|
|
def test_writable_libraries_are_restricted_mappings(
|
|
library_cls: type[Library] | type[LazyLibrary] | type[OverlayLibrary],
|
|
) -> None:
|
|
lib = library_cls()
|
|
|
|
assert isinstance(lib, Mapping)
|
|
assert not isinstance(lib, MutableMapping)
|
|
for method in ("update", "setdefault", "pop", "popitem", "clear"):
|
|
assert not hasattr(lib, method)
|
|
|
|
lib["top"] = Pattern()
|
|
del lib["top"]
|
|
assert not lib
|
|
|
|
|
|
@pytest.mark.parametrize('cached', [False, True])
|
|
def test_overlay_reuses_names_and_keeps_new_sources_independent(cached: bool) -> None:
|
|
source = Library({'a': Pattern(), 'parent': Pattern().ref('a')})
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(source)
|
|
if cached:
|
|
overlay['parent']
|
|
overlay.rename('a', 'b', move_references=True)
|
|
overlay.rename('b', 'a', move_references=True)
|
|
assert overlay.child_graph()['parent'] == {'a'}
|
|
assert set(overlay.materialize('parent', persist=False).refs) == {'a'}
|
|
overlay.rename('a', 'b', move_references=True)
|
|
overlay.add_source(Library({'a': Pattern(), 'new_parent': Pattern().ref('a')}))
|
|
assert overlay.child_graph()['parent'] == {'b'}
|
|
assert overlay.child_graph()['new_parent'] == {'a'}
|
|
assert set(overlay['new_parent'].refs) == {'a'}
|
|
assert set(source['parent'].refs) == {'a'}
|
|
|
|
|
|
def test_overlay_reuses_dangling_targets_and_preserves_failed_rename() -> None:
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(Library({'parent': Pattern().ref('missing')}))
|
|
overlay.move_references('missing', 'other')
|
|
overlay.move_references('other', 'missing')
|
|
assert set(overlay['parent'].refs) == {'missing'}
|
|
overlay['used'] = Pattern()
|
|
before = list(overlay)
|
|
with pytest.raises(LibraryError, match='already exists'):
|
|
overlay.rename('parent', 'used', move_references=True)
|
|
assert list(overlay) == before
|
|
assert set(overlay['parent'].refs) == {'missing'}
|
|
del overlay['used']
|
|
overlay.rename('parent', 'used')
|
|
assert list(overlay) == ['used']
|
|
|
|
|
|
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_empty_ref_buckets_do_not_create_hierarchy_edges() -> None:
|
|
parent = Pattern()
|
|
parent.refs["ghost"]
|
|
parent.refs["parent"]
|
|
lib = Library({"parent": parent, "ghost": Pattern()})
|
|
|
|
assert not parent.has_refs()
|
|
assert parent.referenced_patterns() == set()
|
|
assert set(lib.tops()) == {"parent", "ghost"}
|
|
assert lib.child_graph() == {"parent": set(), "ghost": set()}
|
|
assert lib.parent_graph() == {"parent": set(), "ghost": set()}
|
|
|
|
lib.dfs(parent, hierarchy=("parent",))
|
|
flat = lib.flatten("parent")["parent"]
|
|
assert not flat.refs
|
|
|
|
|
|
def test_library_dangling() -> None:
|
|
lib = Library()
|
|
lib["parent"] = Pattern()
|
|
lib["parent"].ref("missing")
|
|
|
|
assert lib.dangling_refs() == {"missing"}
|
|
|
|
|
|
def test_library_reachability_ignores_unnamed_refs() -> None:
|
|
pattern = Pattern()
|
|
pattern.ref(None)
|
|
lib = Library({"top": pattern})
|
|
|
|
assert pattern.referenced_patterns() == {None}
|
|
assert lib.referenced_patterns() == set()
|
|
assert lib.dangling_refs() == set()
|
|
|
|
|
|
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"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("method", "args"),
|
|
[
|
|
("child_graph", ()),
|
|
("parent_graph", ()),
|
|
("child_order", ()),
|
|
("find_refs_local", ("top",)),
|
|
("find_refs_global", ("top",)),
|
|
("prune_empty", ()),
|
|
],
|
|
)
|
|
def test_library_rejects_unknown_dangling_mode(method: str, args: tuple[object, ...]) -> None:
|
|
lib = Library({"top": Pattern()})
|
|
|
|
with pytest.raises(ValueError, match="dangling-reference mode"):
|
|
getattr(lib, method)(*args, dangling="typo")
|
|
|
|
|
|
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_find_refs_global_includes_composed_scale_column() -> None:
|
|
lib = Library({"leaf": Pattern(), "child": Pattern(), "top": Pattern()})
|
|
lib["child"].ref("leaf", scale=2)
|
|
lib["top"].ref("child", scale=3)
|
|
|
|
transforms = lib.find_refs_global("leaf")
|
|
|
|
assert_allclose(transforms[("top", "child", "leaf")], [[0, 0, 0, 0, 6]])
|
|
|
|
|
|
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_preflight_source_aware_skips_source_backed_patterns() -> None:
|
|
source = Library()
|
|
source["copied"] = Pattern()
|
|
source["materialized"] = Pattern()
|
|
mapped_names: list[str] = []
|
|
|
|
def map_layer(layer: int | tuple[int, int] | str) -> int | tuple[int, int] | str:
|
|
mapped_names.append(str(layer))
|
|
return layer
|
|
|
|
view = LayerMappedView(source, map_layer, copy_through=True)
|
|
view["materialized"].rect("named", xmin=0, xmax=1, ymin=0, ymax=1)
|
|
|
|
result = preflight_source_aware(view, allow_named_layers=True)
|
|
|
|
assert isinstance(result, OverlayLibrary)
|
|
assert mapped_names == []
|
|
assert result.source_cell("copied") is not None
|
|
assert result.source_cell("materialized") is None
|
|
assert not source["materialized"].shapes
|
|
assert "named" in result["materialized"].shapes
|
|
|
|
|
|
def test_preflight_source_aware_checks_only_cells_without_provenance() -> None:
|
|
source = Library()
|
|
source["copied"] = Pattern().rect("copied_layer", xmin=0, xmax=1, ymin=0, ymax=1)
|
|
source["materialized"] = Pattern().rect("materialized_layer", xmin=0, xmax=1, ymin=0, ymax=1)
|
|
view = LayerMappedView(source, lambda layer: layer, copy_through=True)
|
|
|
|
preflight_source_aware(view, allow_named_layers=False)
|
|
|
|
view["materialized"]
|
|
with pytest.raises(PatternError, match="materialized_layer"):
|
|
preflight_source_aware(view, allow_named_layers=False)
|
|
|
|
|
|
def test_preflight_source_aware_prunes_only_safe_checked_patterns() -> None:
|
|
source = Library({"child": Pattern(), "parent": Pattern()})
|
|
source["parent"].ref("child")
|
|
view = LayerMappedView(source, lambda layer: layer, copy_through=True)
|
|
view["child"]
|
|
|
|
result = preflight_source_aware(view, prune_empty_patterns=True)
|
|
|
|
assert isinstance(result, OverlayLibrary)
|
|
assert "child" in result
|
|
assert result.source_cell("parent") is not None
|
|
|
|
view["parent"]
|
|
result = preflight_source_aware(view, prune_empty_patterns=True)
|
|
|
|
assert "child" not in result
|
|
assert "parent" not in result
|
|
assert "child" in source
|
|
assert "child" in source["parent"].refs
|
|
|
|
|
|
def test_preflight_source_aware_wraps_only_checked_patterns() -> None:
|
|
source = Library({"copied": Pattern(), "materialized": Pattern()})
|
|
for name in source:
|
|
source[name].rect((1, 0), xmin=0, xmax=1, ymin=0, ymax=1)
|
|
source[name].shapes[(1, 0)][0].repetition = Grid(a_vector=(10, 0), a_count=2)
|
|
view = LayerMappedView(source, lambda layer: layer, copy_through=True)
|
|
view["materialized"]
|
|
|
|
result = preflight_source_aware(view, wrap_repeated_shapes=True)
|
|
|
|
assert isinstance(result, OverlayLibrary)
|
|
assert result.source_cell("copied") is not None
|
|
assert source["copied"].shapes[(1, 0)][0].repetition is not None
|
|
assert result["materialized"].shapes[(1, 0)] == []
|
|
wrapper_names = result["materialized"].referenced_patterns()
|
|
assert len(wrapper_names) == 1
|
|
wrapper_name = next(iter(wrapper_names))
|
|
assert wrapper_name is not None
|
|
assert result[wrapper_name].shapes[(1, 0)][0].repetition is None
|
|
|
|
|
|
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_pattern_polygon_traversal_ignores_empty_dangling_bucket() -> None:
|
|
child = Pattern()
|
|
child.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
|
|
parent = Pattern()
|
|
parent.ref("child")
|
|
parent.refs["missing"]
|
|
lib = Library({"child": child, "parent": parent})
|
|
|
|
polygons = parent.layer_as_polygons((1, 0), flatten=True, library=lib)
|
|
|
|
assert len(polygons) == 1
|
|
|
|
|
|
def test_recursive_geometry_rejects_reference_cycles() -> None:
|
|
lib = Library({"a": Pattern(), "b": Pattern()})
|
|
lib["a"].ref("b")
|
|
lib["b"].ref("a")
|
|
|
|
with pytest.raises(PatternError, match=r"calculating bounds: .* -> .* ->"):
|
|
lib["a"].get_bounds(library=lib)
|
|
with pytest.raises(PatternError, match=r"collecting layer polygons: .* -> .* ->"):
|
|
lib["a"].layer_as_polygons((1, 0), library=lib)
|
|
with pytest.raises(PatternError, match=r"visualizing pattern hierarchy: .* -> .* ->"):
|
|
lib["a"].visualize(library=lib)
|
|
with pytest.raises(PatternError, match="Circular reference"):
|
|
lib["a"].deepcopy().flatten(library=lib)
|
|
|
|
|
|
def test_nonrecursive_geometry_allows_reference_cycles() -> None:
|
|
lib = Library({"loop": Pattern()})
|
|
lib["loop"].ref("loop")
|
|
|
|
assert lib["loop"].get_bounds(library=lib, recurse=False) is None
|
|
assert lib["loop"].layer_as_polygons((1, 0), flatten=False, library=lib) == []
|
|
|
|
|
|
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_lazy_library_reachability_loads_only_reachable_cells() -> None:
|
|
lib = LazyLibrary()
|
|
calls = {"top": 0, "child": 0, "unused": 0}
|
|
|
|
def make(name: str, target: str | None = None) -> Pattern:
|
|
calls[name] += 1
|
|
pattern = Pattern()
|
|
if target is not None:
|
|
pattern.ref(target)
|
|
return pattern
|
|
|
|
lib["top"] = lambda: make("top", "child")
|
|
lib["child"] = lambda: make("child")
|
|
lib["unused"] = lambda: make("unused")
|
|
|
|
assert lib.referenced_patterns("top") == {"child"}
|
|
assert calls == {"top": 1, "child": 1, "unused": 0}
|
|
|
|
|
|
def test_abstract_view_membership_does_not_materialize_lazy_cells() -> None:
|
|
lib = LazyLibrary()
|
|
calls = 0
|
|
|
|
def make_pat() -> Pattern:
|
|
nonlocal calls
|
|
calls += 1
|
|
return Pattern()
|
|
|
|
lib["lazy"] = make_pat
|
|
abstracts = lib.abstract_view()
|
|
|
|
assert "lazy" in abstracts
|
|
assert "missing" not in abstracts
|
|
assert calls == 0
|
|
|
|
|
|
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
|
|
|
|
|
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
|
def test_library_rename_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
|
lib = library_cls()
|
|
lib["top"] = Pattern()
|
|
lib["parent"] = Pattern()
|
|
lib["parent"].ref("top")
|
|
|
|
lib.rename("top", "top", move_references=True)
|
|
|
|
assert set(lib.keys()) == {"top", "parent"}
|
|
assert "top" in lib["parent"].refs
|
|
assert len(lib["parent"].refs["top"]) == 1
|
|
|
|
|
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
|
def test_library_rename_top_self_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
|
lib = library_cls()
|
|
lib["top"] = Pattern()
|
|
|
|
lib.rename_top("top")
|
|
|
|
assert list(lib.keys()) == ["top"]
|
|
|
|
|
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
|
def test_library_rename_missing_raises_library_error(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
|
lib = library_cls()
|
|
lib["top"] = Pattern()
|
|
|
|
with pytest.raises(LibraryError, match="does not exist"):
|
|
lib.rename("missing", "new")
|
|
|
|
|
|
@pytest.mark.parametrize("library_cls", [Library, LazyLibrary])
|
|
def test_library_move_references_same_target_is_noop(library_cls: type[Library] | type[LazyLibrary]) -> None:
|
|
lib = library_cls()
|
|
lib["top"] = Pattern()
|
|
lib["parent"] = Pattern()
|
|
lib["parent"].ref("top")
|
|
|
|
lib.move_references("top", "top")
|
|
|
|
assert "top" in lib["parent"].refs
|
|
assert len(lib["parent"].refs["top"]) == 1
|
|
|
|
|
|
def test_library_dfs_can_replace_existing_patterns() -> None:
|
|
lib = Library()
|
|
child = Pattern()
|
|
lib["child"] = child
|
|
top = Pattern()
|
|
top.ref("child")
|
|
lib["top"] = top
|
|
|
|
replacement_top = Pattern(ports={"T": Port((1, 2), 0)})
|
|
replacement_child = Pattern(ports={"C": Port((3, 4), 0)})
|
|
|
|
def visit_after(pattern: Pattern, hierarchy: tuple[str | None, ...], **kwargs) -> Pattern: # noqa: ARG001
|
|
if hierarchy[-1] == "child":
|
|
return replacement_child
|
|
if hierarchy[-1] == "top":
|
|
return replacement_top
|
|
return pattern
|
|
|
|
lib.dfs(lib["top"], visit_after=visit_after, hierarchy=("top",), transform=True)
|
|
|
|
assert lib["top"] is replacement_top
|
|
assert lib["child"] is replacement_child
|
|
|
|
|
|
def test_lazy_library_dfs_can_replace_existing_patterns() -> None:
|
|
lib = LazyLibrary()
|
|
lib["child"] = lambda: Pattern()
|
|
lib["top"] = lambda: Pattern(refs={"child": []})
|
|
|
|
top = lib["top"]
|
|
top.ref("child")
|
|
|
|
replacement_top = Pattern(ports={"T": Port((1, 2), 0)})
|
|
replacement_child = Pattern(ports={"C": Port((3, 4), 0)})
|
|
|
|
def visit_after(pattern: Pattern, hierarchy: tuple[str | None, ...], **kwargs) -> Pattern: # noqa: ARG001
|
|
if hierarchy[-1] == "child":
|
|
return replacement_child
|
|
if hierarchy[-1] == "top":
|
|
return replacement_top
|
|
return pattern
|
|
|
|
lib.dfs(top, visit_after=visit_after, hierarchy=("top",), transform=True)
|
|
|
|
assert lib["top"] is replacement_top
|
|
assert lib["child"] is replacement_child
|
|
|
|
|
|
def test_library_add_no_duplicates_respects_mutate_other_false() -> None:
|
|
src_pat = Pattern(ports={"A": Port((0, 0), 0)})
|
|
lib = Library({"a": Pattern()})
|
|
|
|
lib.add({"b": src_pat}, mutate_other=False)
|
|
|
|
assert lib["b"] is not src_pat
|
|
lib["b"].ports["A"].offset[0] = 123
|
|
assert tuple(src_pat.ports["A"].offset) == (0.0, 0.0)
|
|
|
|
|
|
def test_library_add_returns_only_renamed_entries() -> None:
|
|
lib = Library({"a": Pattern(), "_shape": Pattern()})
|
|
|
|
assert lib.add({"b": Pattern(), "c": Pattern()}, mutate_other=False) == {}
|
|
|
|
rename_map = lib.add({"_shape": Pattern(), "keep": Pattern()}, mutate_other=False)
|
|
|
|
assert set(rename_map) == {"_shape"}
|
|
assert rename_map["_shape"] != "_shape"
|
|
assert "keep" not in rename_map
|
|
|
|
|
|
def test_library_add_name_failure_is_atomic() -> None:
|
|
destination = Library({"x": Pattern(), "y": Pattern()})
|
|
source_parent = Pattern()
|
|
source_parent.ref("x")
|
|
source = Library({"x": Pattern(), "y": source_parent})
|
|
|
|
with pytest.raises(LibraryError, match="Unresolved duplicate"):
|
|
destination.add(source, rename_theirs=lambda _lib, _name: "z", mutate_other=True)
|
|
|
|
assert set(destination) == {"x", "y"}
|
|
assert set(source) == {"x", "y"}
|
|
assert set(source["y"].refs) == {"x"}
|
|
|
|
|
|
def test_library_add_callback_sees_earlier_name_reservations() -> None:
|
|
destination = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
|
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
|
callback_views: list[set[str]] = []
|
|
|
|
def rename(view: INameView, _name: str) -> str:
|
|
assert isinstance(view, INameView)
|
|
assert not isinstance(view, Mapping)
|
|
assert not hasattr(view, "__getitem__")
|
|
callback_views.append(set(view))
|
|
return view.get_name("_shape")
|
|
|
|
rename_map = destination.add(source, rename_theirs=rename)
|
|
|
|
assert len(set(rename_map.values())) == 2
|
|
assert rename_map["_shape$A"] in callback_views[1]
|
|
assert set(rename_map.values()) <= set(destination)
|
|
|
|
|
|
def test_library_can_add_itself_with_mutate_other() -> None:
|
|
lib = Library({"_helper": Pattern()})
|
|
|
|
rename_map = lib.add(lib, mutate_other=True)
|
|
|
|
assert rename_map["_helper"] in lib
|
|
assert len(lib) == 2
|
|
|
|
|
|
def test_overlay_add_source_callback_sees_earlier_name_reservations() -> None:
|
|
overlay = OverlayLibrary()
|
|
overlay["_shape$A"] = Pattern()
|
|
overlay["_shape$B"] = Pattern()
|
|
source = Library({"_shape$A": Pattern(), "_shape$B": Pattern()})
|
|
|
|
rename_map = overlay.add_source(
|
|
source,
|
|
rename_theirs=lambda view, _name: view.get_name("_shape"),
|
|
)
|
|
|
|
assert len(set(rename_map.values())) == 2
|
|
assert set(rename_map.values()) <= set(overlay)
|
|
|
|
|
|
def test_overlay_add_source_renames_conflicting_single_use_name_by_default() -> None:
|
|
overlay = OverlayLibrary()
|
|
overlay['_myCellName$F'] = Pattern()
|
|
source = Library({'_myCellName$F': Pattern(), 'source_top': Pattern()})
|
|
source['source_top'].ref('_myCellName$F')
|
|
|
|
rename_map = overlay.add_source(source)
|
|
|
|
assert rename_map == {'_myCellName$F': '_myCellName'}
|
|
assert set(overlay['source_top'].refs) == {'_myCellName'}
|
|
|
|
|
|
def test_overlay_add_source_can_explicitly_disable_default_renaming() -> None:
|
|
overlay = OverlayLibrary()
|
|
overlay['_helper$A'] = Pattern()
|
|
|
|
with pytest.raises(LibraryError, match='Conflicting name'):
|
|
overlay.add_source(Library({'_helper$A': Pattern()}), rename_theirs=None)
|
|
|
|
|
|
def test_port_load_view_detaches_already_materialized_overlay_pattern() -> None:
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(Library({"top": Pattern()}))
|
|
raw = overlay["top"]
|
|
processed = PortLoadView(
|
|
overlay,
|
|
ports={"top": {"P": Port((1, 2), 0)}},
|
|
)
|
|
|
|
processed_top = processed["top"]
|
|
|
|
assert processed_top is not raw
|
|
assert not raw.ports
|
|
assert set(processed_top.ports) == {"P"}
|
|
assert not hasattr(processed, "close")
|
|
|
|
|
|
def test_layer_mapped_view_detaches_and_maps_shapes_and_labels() -> None:
|
|
import masque
|
|
|
|
child = Pattern()
|
|
source_pattern = Pattern(ports={'P': Port((1, 2), 0, ptype='wire')})
|
|
source_pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
|
|
source_pattern.polygon('B', vertices=[[2, 0], [3, 0], [2, 1]])
|
|
source_pattern.label('TEXT', string='port', offset=(1, 2))
|
|
source_pattern.ref('child')
|
|
source_pattern.annotations['note'] = ['unchanged']
|
|
source = Library({'child': child, 'top': source_pattern})
|
|
mapped = LayerMappedView(
|
|
source,
|
|
lambda layer: {'A': 'DRAW', 'B': 'DRAW', 'TEXT': 'LABEL'}.get(layer, layer),
|
|
)
|
|
|
|
mapped_top = mapped['top']
|
|
|
|
assert mapped_top is not source_pattern
|
|
assert set(mapped_top.shapes) == {'DRAW'}
|
|
assert len(mapped_top.shapes['DRAW']) == 2
|
|
assert set(mapped_top.labels) == {'LABEL'}
|
|
assert set(mapped_top.ports) == {'P'}
|
|
assert set(mapped_top.refs) == {'child'}
|
|
assert mapped_top.annotations == {'note': ['unchanged']}
|
|
assert set(source_pattern.shapes) == {'A', 'B'}
|
|
assert set(source_pattern.labels) == {'TEXT'}
|
|
assert mapped.child_graph() == source.child_graph()
|
|
assert mapped.source_order() == source.source_order()
|
|
assert masque.LayerMappedView is LayerMappedView
|
|
assert masque.PortLoadView is PortLoadView
|
|
assert not hasattr(masque, 'PortsLibraryView')
|
|
assert not hasattr(masque.library, 'PortsLibraryView')
|
|
|
|
|
|
def test_layer_mapped_view_preflight_rejects_none_layer() -> None:
|
|
pattern = Pattern()
|
|
pattern.polygon((1, 0), vertices=[[0, 0], [1, 0], [0, 1]])
|
|
mapped = LayerMappedView(Library({'top': pattern}), lambda _layer: None) # type: ignore[arg-type]
|
|
|
|
with pytest.raises(PatternError, match=r"returned None for source layer \(1, 0\)"):
|
|
preflight_source_aware(mapped)
|
|
|
|
|
|
def test_layer_mapped_view_materialization_and_copy_through() -> None:
|
|
pattern = Pattern()
|
|
pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
|
|
source = Library({'top': pattern})
|
|
mapped = LayerMappedView(source, lambda _layer: 'B')
|
|
passthrough = LayerMappedView(source, lambda _layer: 'B', copy_through=True)
|
|
|
|
assert mapped.source_cell('top') is None
|
|
assert passthrough.source_cell('top') == (source, 'top')
|
|
transient = passthrough.materialize('top', persist=False)
|
|
assert set(transient.shapes) == {'B'}
|
|
assert passthrough.source_cell('top') == (source, 'top')
|
|
|
|
persistent = passthrough['top']
|
|
assert passthrough['top'] is persistent
|
|
assert passthrough.source_cell('top') is None
|
|
assert set(persistent.shapes) == {'B'}
|
|
|
|
|
|
def test_layer_mapped_view_composes_with_ports_and_overlay() -> None:
|
|
pattern = Pattern()
|
|
pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
|
|
source = Library({'top': pattern})
|
|
ports = PortLoadView(source, ports={'top': {'P': Port((1, 2), 0)}})
|
|
mapped = LayerMappedView(ports, lambda _layer: 'B')
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(mapped)
|
|
|
|
top = overlay['top']
|
|
|
|
assert set(top.shapes) == {'B'}
|
|
assert set(top.ports) == {'P'}
|
|
assert mapped.borrowed_sources() == (ports,)
|
|
assert not pattern.ports
|
|
|
|
|
|
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 isinstance(sub, Library)
|
|
assert "a" in sub
|
|
assert "b" in sub
|
|
assert "c" not in sub
|
|
assert sub["a"] is lib["a"]
|
|
|
|
del sub["b"]
|
|
assert "b" in lib
|
|
|
|
|
|
def test_lazy_library_subtree_preserves_type_and_shares_materialized_patterns() -> None:
|
|
lib = LazyLibrary()
|
|
child = Pattern()
|
|
top = Pattern()
|
|
top.ref("child")
|
|
lib["child"] = lambda: child
|
|
lib["top"] = lambda: top
|
|
lib["unused"] = Pattern()
|
|
|
|
subtree = lib.subtree("top")
|
|
|
|
assert isinstance(subtree, LazyLibrary)
|
|
assert set(subtree) == {"child", "top"}
|
|
assert subtree["top"] is lib["top"]
|
|
del subtree["child"]
|
|
assert "child" in lib
|
|
|
|
|
|
def test_overlay_subtree_preserves_type_sources_and_independent_materialization() -> None:
|
|
source = Library({"child": Pattern(), "top": Pattern(), "unused": Pattern()})
|
|
source["top"].ref("child")
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(source)
|
|
|
|
subtree = overlay.subtree("top")
|
|
|
|
assert isinstance(subtree, OverlayLibrary)
|
|
assert set(subtree) == {"child", "top"}
|
|
assert subtree.borrowed_sources() == overlay.borrowed_sources()
|
|
subtree_top = subtree["top"]
|
|
overlay_top = overlay["top"]
|
|
assert subtree_top is not overlay_top
|
|
|
|
shared_subtree = overlay.subtree("top")
|
|
assert shared_subtree["top"] is overlay_top
|
|
del shared_subtree["child"]
|
|
assert "child" in overlay
|
|
|
|
|
|
def test_overlay_subtree_preserves_reference_remaps() -> None:
|
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
|
source["parent"].ref("leaf")
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(source)
|
|
overlay.rename("leaf", "renamed", move_references=True)
|
|
|
|
subtree = overlay.subtree("parent")
|
|
|
|
assert isinstance(subtree, OverlayLibrary)
|
|
assert set(subtree) == {"parent", "renamed"}
|
|
assert set(subtree["parent"].refs) == {"renamed"}
|
|
|
|
|
|
def test_read_only_subtree_is_a_borrowed_subtree_view() -> None:
|
|
source = Library({"child": Pattern(), "top": Pattern(), "unused": Pattern()})
|
|
source["top"].ref("child")
|
|
view = LibraryView(source)
|
|
|
|
subtree = view.subtree("top")
|
|
|
|
assert subtree.source_order() == ("child", "top")
|
|
assert subtree["top"] is source["top"]
|
|
assert "unused" not in subtree
|
|
with pytest.raises(KeyError):
|
|
_ = subtree["unused"]
|
|
|
|
|
|
def test_library_materialization_and_borrowing_capabilities() -> None:
|
|
lazy = LazyLibrary()
|
|
lazy["top"] = lambda: Pattern()
|
|
|
|
assert isinstance(lazy, IMaterializable)
|
|
assert not isinstance(lazy, IBorrowing)
|
|
assert not hasattr(lazy, "borrowed_sources")
|
|
transient = lazy.materialize("top", persist=False)
|
|
assert "top" not in lazy.cache
|
|
assert transient is not lazy["top"]
|
|
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(lazy)
|
|
ports = PortLoadView(overlay)
|
|
subtree = ports.subtree("top")
|
|
|
|
assert isinstance(overlay, IMaterializable)
|
|
assert isinstance(overlay, IBorrowing)
|
|
assert isinstance(ports, IMaterializable)
|
|
assert isinstance(ports, IBorrowing)
|
|
assert isinstance(subtree, IMaterializable)
|
|
assert isinstance(subtree, IBorrowing)
|
|
assert overlay.borrowed_sources() == (lazy,)
|
|
assert ports.borrowed_sources() == (overlay,)
|
|
assert subtree.borrowed_sources() == (ports,)
|
|
eager = Library({"top": Pattern()})
|
|
plain_view = LibraryView(lazy)
|
|
assert not isinstance(eager, IMaterializable | IBorrowing)
|
|
assert not isinstance(plain_view, IMaterializable | IBorrowing)
|
|
|
|
|
|
def test_detached_materialization_default_is_owned_and_deduplicated() -> None:
|
|
shared_a = Pattern()
|
|
shared_b = Pattern()
|
|
lazy = LazyLibrary()
|
|
lazy['a'] = lambda: shared_a
|
|
lazy['b'] = lambda: shared_b
|
|
|
|
detached = lazy.materialize_many_detached(('b', 'a', 'b'))
|
|
|
|
assert tuple(detached) == ('b', 'a')
|
|
assert detached['a'] is not shared_a
|
|
assert detached['b'] is not shared_b
|
|
detached['a'].polygon('L', vertices=[[0, 0], [1, 0], [0, 1]])
|
|
assert not shared_a.shapes
|
|
assert not lazy.cache
|
|
|
|
|
|
def test_nested_detached_views_copy_plain_source_once(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
source_pattern = Pattern()
|
|
source_pattern.polygon('A', vertices=[[0, 0], [1, 0], [0, 1]])
|
|
source = Library({'top': source_pattern})
|
|
ports = PortLoadView(source, ports={'top': {'P': Port((1, 2), 0)}})
|
|
mapped = LayerMappedView(ports, lambda _layer: 'B')
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(mapped)
|
|
|
|
original_deepcopy = Pattern.deepcopy
|
|
copied: list[Pattern] = []
|
|
|
|
def count_deepcopy(pattern: Pattern) -> Pattern:
|
|
copied.append(pattern)
|
|
return original_deepcopy(pattern)
|
|
|
|
monkeypatch.setattr(Pattern, 'deepcopy', count_deepcopy)
|
|
detached = overlay.materialize_detached('top')
|
|
|
|
assert copied == [source_pattern]
|
|
assert set(detached.shapes) == {'B'}
|
|
assert set(detached.ports) == {'P'}
|
|
assert not source_pattern.ports
|
|
assert not ports._cache
|
|
assert not mapped._cache
|
|
assert overlay.source_cell('top') == (mapped, 'top')
|
|
|
|
|
|
def test_nested_detached_views_copy_cached_pattern_once(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
source_pattern = Pattern()
|
|
source = Library({'top': source_pattern})
|
|
ports = PortLoadView(source, ports={'top': {'P': Port((1, 2), 0)}})
|
|
cached = ports['top']
|
|
mapped = LayerMappedView(ports, lambda layer: layer)
|
|
|
|
original_deepcopy = Pattern.deepcopy
|
|
copied: list[Pattern] = []
|
|
|
|
def count_deepcopy(pattern: Pattern) -> Pattern:
|
|
copied.append(pattern)
|
|
return original_deepcopy(pattern)
|
|
|
|
monkeypatch.setattr(Pattern, 'deepcopy', count_deepcopy)
|
|
detached = mapped.materialize_detached('top')
|
|
|
|
assert copied == [cached]
|
|
assert detached is not cached
|
|
detached.ports.clear()
|
|
assert set(cached.ports) == {'P'}
|
|
|
|
|
|
def test_borrowed_source_cell_tracks_persistent_materialization() -> None:
|
|
source = Library({"top": Pattern()})
|
|
source.library_info = {"name": "not-forwarded"} # type: ignore[attr-defined]
|
|
processed = PortLoadView(source)
|
|
|
|
assert processed.source_cell("top") == (source, "top")
|
|
assert not hasattr(processed, "library_info")
|
|
assert not hasattr(processed, "raw_struct_bytes")
|
|
_ = processed.materialize_many(("top",), persist=False)
|
|
assert processed.source_cell("top") == (source, "top")
|
|
|
|
subtree = processed.subtree("top")
|
|
assert subtree.source_cell("top") == (processed, "top")
|
|
assert not hasattr(subtree, "library_info")
|
|
_ = subtree["top"]
|
|
assert processed.source_cell("top") is None
|
|
assert subtree.source_cell("top") == (processed, "top")
|
|
|
|
|
|
def test_overlay_source_cell_tracks_names_references_and_materialization() -> None:
|
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
|
source["parent"].ref("leaf")
|
|
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(source)
|
|
assert overlay.source_cell("parent") == (source, "parent")
|
|
|
|
overlay.rename("parent", "renamed_parent")
|
|
assert overlay.source_cell("renamed_parent") == (source, "parent")
|
|
|
|
overlay.rename("leaf", "renamed_leaf", move_references=True)
|
|
assert overlay.source_cell("renamed_parent") is None
|
|
|
|
materialized = OverlayLibrary()
|
|
materialized.add_source(source)
|
|
_ = materialized["parent"]
|
|
assert materialized.source_cell("parent") is None
|
|
|
|
|
|
@pytest.mark.parametrize("materialize_parent", [False, True])
|
|
@pytest.mark.parametrize("move_references", [False, True])
|
|
def test_overlay_rename_is_independent_of_materialization(
|
|
materialize_parent: bool,
|
|
move_references: bool,
|
|
) -> None:
|
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
|
source["parent"].ref("leaf")
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(source)
|
|
|
|
if materialize_parent:
|
|
_ = overlay["parent"]
|
|
|
|
overlay.rename("leaf", "renamed", move_references=move_references)
|
|
expected_target = "renamed" if move_references else "leaf"
|
|
|
|
assert overlay.child_graph(dangling="include")["parent"] == {expected_target}
|
|
assert set(overlay["parent"].refs) == {expected_target}
|
|
assert overlay.child_graph(dangling="include")["parent"] == {expected_target}
|
|
|
|
|
|
def test_overlay_rejects_unknown_dangling_mode() -> None:
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(Library({"top": Pattern()}))
|
|
|
|
with pytest.raises(ValueError, match="dangling-reference mode"):
|
|
overlay.child_graph(dangling="typo")
|
|
with pytest.raises(ValueError, match="dangling-reference mode"):
|
|
overlay.find_refs_local("top", dangling="typo")
|
|
|
|
|
|
def test_overlay_rename_preserves_initial_import_target_without_moving_refs() -> None:
|
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
|
source["parent"].ref("leaf")
|
|
overlay = OverlayLibrary()
|
|
overlay["leaf"] = Pattern()
|
|
rename_map = overlay.add_source(source, rename_theirs=lambda lib, name: lib.get_name(name))
|
|
imported_leaf = rename_map["leaf"]
|
|
|
|
overlay.rename(imported_leaf, "renamed", move_references=False)
|
|
|
|
assert set(overlay["parent"].refs) == {imported_leaf}
|
|
|
|
|
|
def test_overlay_chained_rename_moves_unmaterialized_references() -> None:
|
|
source = Library({"leaf": Pattern(), "parent": Pattern()})
|
|
source["parent"].ref("leaf")
|
|
overlay = OverlayLibrary()
|
|
overlay.add_source(source)
|
|
|
|
overlay.rename("leaf", "middle", move_references=True)
|
|
overlay.rename("middle", "final", move_references=True)
|
|
|
|
assert set(overlay["parent"].refs) == {"final"}
|
|
|
|
|
|
def _assert_tree_merge_rejected(tree: Library) -> None:
|
|
destination = Library({"existing": Pattern()})
|
|
|
|
with pytest.raises(LibraryError, match="exactly one topcell"):
|
|
destination << tree
|
|
|
|
assert set(destination) == {"existing"}
|
|
|
|
|
|
def test_library_tree_merge_rejects_empty_tree() -> None:
|
|
_assert_tree_merge_rejected(Library())
|
|
|
|
|
|
def test_library_tree_merge_rejects_cycle_without_top() -> None:
|
|
tree = Library({"a": Pattern(), "b": Pattern()})
|
|
tree["a"].ref("b")
|
|
tree["b"].ref("a")
|
|
_assert_tree_merge_rejected(tree)
|
|
|
|
|
|
def test_library_tree_merge_rejects_multiple_tops() -> None:
|
|
_assert_tree_merge_rejected(Library({"a": Pattern(), "b": Pattern()}))
|
|
|
|
|
|
def test_library_child_order_cycle_raises_library_error() -> None:
|
|
lib = Library()
|
|
lib["a"] = Pattern()
|
|
lib["a"].ref("b")
|
|
lib["b"] = Pattern()
|
|
lib["b"].ref("a")
|
|
|
|
with pytest.raises(LibraryError, match="Cycle found while building child order"):
|
|
lib.child_order()
|
|
|
|
|
|
def test_library_find_refs_global_cycle_raises_library_error() -> None:
|
|
lib = Library()
|
|
lib["a"] = Pattern()
|
|
lib["a"].ref("a")
|
|
|
|
with pytest.raises(LibraryError, match="Cycle found while building child order"):
|
|
lib.find_refs_global("a")
|
|
|
|
|
|
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)
|