masque/masque/test/test_ports.py

293 lines
8.6 KiB
Python

import pytest
from numpy.testing import assert_equal, assert_allclose
from numpy import pi
from ..ports import Port, PortList
from ..error import PortError
from ..pattern import Pattern
def test_port_init() -> None:
p = Port(offset=(10, 20), rotation=pi / 2, ptype="test")
assert_equal(p.offset, [10, 20])
assert p.rotation == pi / 2
assert p.ptype == "test"
def test_port_transform() -> None:
p = Port(offset=(10, 0), rotation=0)
p.rotate_around((0, 0), pi / 2)
assert_allclose(p.offset, [0, 10], atol=1e-10)
assert p.rotation is not None
assert_allclose(p.rotation, pi / 2, atol=1e-10)
p.mirror(0) # Mirror across x axis (axis 0): in-place relative to offset
assert_allclose(p.offset, [0, 10], atol=1e-10)
# rotation was pi/2 (90 deg), mirror across x (0 deg) -> -pi/2 == 3pi/2
assert p.rotation is not None
assert_allclose(p.rotation, 3 * pi / 2, atol=1e-10)
def test_port_flip_across() -> None:
p = Port(offset=(10, 0), rotation=0)
p.flip_across(axis=1) # Mirror across x=0: flips x-offset
assert_equal(p.offset, [-10, 0])
# rotation was 0, mirrored(1) -> pi
assert p.rotation is not None
assert_allclose(p.rotation, pi, atol=1e-10)
def test_port_measure_travel() -> None:
p1 = Port((0, 0), 0)
p2 = Port((10, 5), pi) # Facing each other
(travel, jog), rotation = p1.measure_travel(p2)
assert travel == 10
assert jog == 5
assert rotation == pi
def test_port_list_measure_travel() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {
"A": Port((0, 0), 0),
"B": Port((10, 5), pi),
}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
(travel, jog), rotation = pl.measure_travel("A", "B")
assert travel == 10
assert jog == 5
assert rotation == pi
def test_port_describe_any_rotation() -> None:
p = Port((0, 0), None)
assert p.describe() == "pos=(0, 0), rot=any"
def test_port_list_rename() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {"A": Port((0, 0), 0)}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
pl.rename_ports({"A": "B"})
assert "A" not in pl.ports
assert "B" in pl.ports
def test_port_list_rename_missing_port_raises() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {"A": Port((0, 0), 0)}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
with pytest.raises(PortError, match="Ports to rename were not found"):
pl.rename_ports({"missing": "B"})
assert set(pl.ports) == {"A"}
def test_port_list_rename_colliding_targets_raises() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {"A": Port((0, 0), 0), "B": Port((1, 0), 0)}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
with pytest.raises(PortError, match="Renamed ports would collide"):
pl.rename_ports({"A": "C", "B": "C"})
assert set(pl.ports) == {"A", "B"}
def test_port_list_add_port_pair_requires_distinct_names() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports: dict[str, Port] = {}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
with pytest.raises(PortError, match="Port names must be distinct"):
pl.add_port_pair(names=("A", "A"))
assert not pl.ports
def test_port_list_plugged() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {"A": Port((10, 10), 0), "B": Port((10, 10), pi)}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
pl.plugged({"A": "B"})
assert not pl.ports # Both should be removed
def test_port_list_plugged_empty_raises() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {"A": Port((10, 10), 0), "B": Port((10, 10), pi)}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
with pytest.raises(PortError, match="Must provide at least one port connection"):
pl.plugged({})
assert set(pl.ports) == {"A", "B"}
def test_port_list_plugged_missing_port_raises() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {"A": Port((10, 10), 0), "B": Port((10, 10), pi)}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
with pytest.raises(PortError, match="Connection source ports were not found"):
pl.plugged({"missing": "B"})
assert set(pl.ports) == {"A", "B"}
def test_port_list_plugged_reused_port_raises_atomically() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {"A": Port((0, 0), None), "B": Port((0, 0), None), "C": Port((0, 0), None)}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
for connections in ({"A": "A"}, {"A": "B", "C": "B"}):
pl = MyPorts()
with pytest.raises(PortError, match="Each port may appear in at most one connection"):
pl.plugged(connections)
assert set(pl.ports) == {"A", "B", "C"}
pl = MyPorts()
with pytest.raises(PortError, match="Connection destination ports were not found"):
pl.plugged({"A": "missing"})
assert set(pl.ports) == {"A", "B", "C"}
def test_port_list_plugged_mismatch() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {
"A": Port((10, 10), 0),
"B": Port((11, 10), pi), # Offset mismatch
}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
with pytest.raises(PortError):
pl.plugged({"A": "B"})
def test_port_list_check_ports_duplicate_map_in_values_raise() -> None:
class MyPorts(PortList):
def __init__(self) -> None:
self._ports = {"A": Port((0, 0), 0), "B": Port((0, 0), 0)}
@property
def ports(self) -> dict[str, Port]:
return self._ports
@ports.setter
def ports(self, val: dict[str, Port]) -> None:
self._ports = val
pl = MyPorts()
with pytest.raises(PortError, match="Duplicate values in `map_in`"):
pl.check_ports({"X", "Y"}, map_in={"A": "X", "B": "X"})
assert set(pl.ports) == {"A", "B"}
def test_pattern_plug_rejects_map_out_on_connected_ports_atomically() -> None:
host = Pattern(ports={"A": Port((0, 0), 0)})
other = Pattern(ports={"X": Port((0, 0), pi), "Y": Port((5, 0), 0)})
with pytest.raises(PortError, match="`map_out` keys conflict with connected ports"):
host.plug(other, {"A": "X"}, map_out={"X": "renamed", "Y": "out"}, append=True)
assert set(host.ports) == {"A"}
def test_find_transform_requires_connection_map() -> None:
host = Pattern(ports={"A": Port((0, 0), 0)})
other = Pattern(ports={"X": Port((0, 0), pi)})
with pytest.raises(PortError, match="at least one port connection"):
host.find_transform(other, {})
with pytest.raises(PortError, match="at least one port connection"):
Pattern.find_port_transform({}, {}, {})