[Pather / Tool] more unification work and fixes
This commit is contained in:
parent
8712398990
commit
f9611933ac
19 changed files with 1022 additions and 137 deletions
|
|
@ -108,7 +108,7 @@ def assert_route_bend_budget(pather: Pather, portspec: str, max_bends: int) -> N
|
|||
"""
|
||||
Assert a simple render-step bend budget for route signatures.
|
||||
"""
|
||||
bend_count = sum(1 for step in pather._paths[portspec] if step.opcode == 'L' and step.start_port.rotation != step.end_port.rotation)
|
||||
bend_count = sum(1 for step in pather._paths[portspec] if step.kind == 'bend')
|
||||
assert bend_count <= max_bends
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from contextlib import suppress
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.testing import assert_allclose
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ def rendered_offer_tree(
|
|||
) -> Library:
|
||||
start = Port((0, 0), rotation=0, ptype=source_ptype or offer.in_ptype or "unk")
|
||||
end, data = commit_offer(offer, parameter)
|
||||
return tool.render((RenderStep(offer.opcode, tool, start, end, data),))
|
||||
return tool.render((RenderStep(offer.kind, tool, start, end, data),))
|
||||
|
||||
|
||||
def _make_transition_straight(length: float, width: float = 2, ptype: str = "wire") -> Pattern:
|
||||
|
|
@ -375,7 +376,7 @@ def wildcard_transition_tool() -> tuple[AutoTool, Library]:
|
|||
tool = (
|
||||
AutoTool(bbox_library=lib)
|
||||
.add_straight(lambda length: make_straight(length, ptype="core"), "core", "A", length_range=(0, 1e8))
|
||||
.add_sbend(make_core_sbend, "core", "A", "B", jog_range=(-1e8, 1e8))
|
||||
.add_sbend(make_core_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
||||
.add_transition(lib.abstract("wild_core"), "WILD", "CORE")
|
||||
)
|
||||
return tool, lib
|
||||
|
|
@ -684,6 +685,24 @@ def test_autotool_add_bend_inferred_names_allow_rotational_reuse_without_mirror(
|
|||
assert_rendered_offer_endpoint_matches_plan(tool, ccw_offer, 2, "wire")
|
||||
|
||||
|
||||
def test_autotool_add_bend_reverse_reuse_swaps_cross_ptypes() -> None:
|
||||
lib = Library()
|
||||
bend = make_bend(2, ptype="core", clockwise=True)
|
||||
bend.ports["B"].ptype = "external"
|
||||
lib["bend"] = bend
|
||||
|
||||
tool = AutoTool(bbox_library=lib).add_bend(lib.abstract("bend"), mirror=False)
|
||||
|
||||
cw_offer = tool.primitive_offers("bend", in_ptype="core", ccw=False)[0]
|
||||
ccw_offer = tool.primitive_offers("bend", in_ptype="external", ccw=True)[0]
|
||||
assert (cw_offer.in_ptype, cw_offer.out_ptype) == ("core", "external")
|
||||
assert (ccw_offer.in_ptype, ccw_offer.out_ptype) == ("external", "core")
|
||||
assert cw_offer.commit(2).port_name == "A"
|
||||
assert ccw_offer.commit(2).port_name == "B"
|
||||
assert_rendered_offer_endpoint_matches_plan(tool, cw_offer, 2, "core")
|
||||
assert_rendered_offer_endpoint_matches_plan(tool, ccw_offer, 2, "external")
|
||||
|
||||
|
||||
def test_autotool_add_bend_rejects_clockwise_mismatch() -> None:
|
||||
lib = Library()
|
||||
lib["bend"] = make_bend(2, ptype="wire", clockwise=True)
|
||||
|
|
@ -966,6 +985,28 @@ def test_autotool_validates_cost_before_registering_any_offers() -> None:
|
|||
AutoTool().add_sbend(unused_sbend, jog_range=(-1, 1), cost=-1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('length_range', [(-1, 2), (2, 1), (numpy.nan, 2), (numpy.inf, numpy.inf)])
|
||||
def test_autotool_rejects_invalid_straight_range_before_sampling(
|
||||
length_range: tuple[float, float],
|
||||
) -> None:
|
||||
def unused_straight(_length: float) -> Pattern:
|
||||
raise AssertionError('invalid range should be rejected before metadata inference')
|
||||
|
||||
with pytest.raises(BuildError, match='Straight length_range'):
|
||||
AutoTool().add_straight(unused_straight, length_range=length_range)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('jog_range', [(-1, 2), (2, 1), (numpy.nan, 2), (numpy.inf, numpy.inf)])
|
||||
def test_autotool_rejects_invalid_sbend_range_before_sampling(
|
||||
jog_range: tuple[float, float],
|
||||
) -> None:
|
||||
def unused_sbend(_jog: float) -> Pattern:
|
||||
raise AssertionError('invalid range should be rejected before metadata inference')
|
||||
|
||||
with pytest.raises(BuildError, match='S-bend jog_range'):
|
||||
AutoTool().add_sbend(unused_sbend, jog_range=jog_range)
|
||||
|
||||
|
||||
def test_autotool_s_offer_singleton_jog_range_includes_both_signs() -> None:
|
||||
tool = make_sbend_tool((4, 4))
|
||||
offers = tool.primitive_offers('s', in_ptype="core")
|
||||
|
|
@ -982,9 +1023,8 @@ def test_autotool_s_offer_singleton_jog_range_includes_both_signs() -> None:
|
|||
|
||||
|
||||
def test_autotool_s_offer_rejects_negative_minimum_jog_range() -> None:
|
||||
tool = make_sbend_tool((-4, 4))
|
||||
|
||||
assert tool.primitive_offers('s', in_ptype="core") == ()
|
||||
with pytest.raises(BuildError, match='finite, nonnegative minimum'):
|
||||
make_sbend_tool((-4, 4))
|
||||
|
||||
|
||||
def test_autotool_uturn_offer_endpoint_matches_rendered_offer() -> None:
|
||||
|
|
@ -1107,7 +1147,12 @@ def test_pather_autotool_uses_l_offer_domains(multi_bend_tool: tuple[AutoTool, L
|
|||
def test_autotool_generated_primitives_snapshot_route_options() -> None:
|
||||
markers: list[str | None] = []
|
||||
|
||||
def make_marked_straight(length: float, marker: str | None = None) -> Pattern:
|
||||
def make_marked_straight(
|
||||
length: float,
|
||||
marker: str | None = None,
|
||||
nested: dict[str, list[int]] | None = None,
|
||||
) -> Pattern:
|
||||
_ = nested
|
||||
markers.append(marker)
|
||||
return make_straight(length, ptype="wire")
|
||||
|
||||
|
|
@ -1115,15 +1160,16 @@ def test_autotool_generated_primitives_snapshot_route_options() -> None:
|
|||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
||||
|
||||
first_options = {'marker': 'first'}
|
||||
first_options = {'marker': 'first', 'nested': {'values': [1]}}
|
||||
p.straight("A", 5, tool_options=first_options)
|
||||
first_options['marker'] = 'mutated'
|
||||
first_options['nested']['values'].append(2)
|
||||
p.straight("A", 6, tool_options={'marker': 'second'})
|
||||
|
||||
first_data, second_data = (step.data for step in p._paths['A'])
|
||||
assert isinstance(first_data, AutoTool.GeneratedData)
|
||||
assert isinstance(second_data, AutoTool.GeneratedData)
|
||||
assert dict(first_data.tool_options) == {'marker': 'first'}
|
||||
assert dict(first_data.tool_options) == {'marker': 'first', 'nested': {'values': [1]}}
|
||||
assert dict(second_data.tool_options) == {'marker': 'second'}
|
||||
|
||||
p.render()
|
||||
|
|
@ -1131,6 +1177,17 @@ def test_autotool_generated_primitives_snapshot_route_options() -> None:
|
|||
assert markers == ['first', 'second']
|
||||
|
||||
|
||||
def test_autotool_route_options_must_be_deepcopyable() -> None:
|
||||
class NotCopyable:
|
||||
def __deepcopy__(self, memo: dict[int, Any]) -> None:
|
||||
_ = memo
|
||||
raise TypeError('no copy')
|
||||
|
||||
tool = AutoTool().add_straight(make_straight, 'wire', 'A')
|
||||
with pytest.raises(BuildError, match='must be deep-copyable'):
|
||||
tool.primitive_offers('straight', marker=NotCopyable())
|
||||
|
||||
|
||||
def test_autotool_route_options_do_not_attach_to_reusable_bends(
|
||||
multi_bend_tool: tuple[AutoTool, Library],
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import pytest
|
|||
import numpy
|
||||
from numpy import pi
|
||||
|
||||
from masque import MinimumStatus, Pather, PortPather, Library, Port, RouteError, RouteFailurePolicy
|
||||
from masque import (
|
||||
MinimumStatus, Pather, PortPather, Library, Port, RouteError, RouteFailurePolicy,
|
||||
)
|
||||
from masque.builder.planner import RoutePortContext, RoutingPlanner
|
||||
from masque.builder.planner.planner import NoLegalRouteError
|
||||
from masque.builder.tools import BendOffer, PathTool, RenderStep, StraightOffer, Tool, UOffer
|
||||
|
|
@ -510,6 +512,45 @@ def test_pather_positional_bound_requires_port_rotation() -> None:
|
|||
p.trace_to('A', None, x=-5)
|
||||
|
||||
|
||||
def test_pather_positional_bound_rejects_non_manhattan_rotation() -> None:
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=pi / 4, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='nearly Manhattan'):
|
||||
p.trace_to('A', None, p=-5)
|
||||
|
||||
|
||||
def test_pather_positional_bound_accepts_nearly_manhattan_rotation() -> None:
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=1e-10, ptype='wire')
|
||||
|
||||
p.trace_to('A', None, x=-5)
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-5, -5e-10), atol=1e-8)
|
||||
|
||||
|
||||
def test_pather_bundle_position_bound_rejects_non_manhattan_rotation() -> None:
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=pi / 4, ptype='wire')
|
||||
p.pattern.ports['B'] = Port((1, 1), rotation=pi / 4, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='nearly Manhattan'):
|
||||
p.trace(['A', 'B'], None, pmin=-5)
|
||||
|
||||
|
||||
def test_pather_bundle_extension_bound_allows_non_manhattan_rotation() -> None:
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=pi / 4, ptype='wire')
|
||||
p.pattern.ports['B'] = Port((1, 1), rotation=pi / 4, ptype='wire')
|
||||
|
||||
p.trace(['A', 'B'], None, emin=5)
|
||||
assert len(p._paths['A']) == 1
|
||||
assert len(p._paths['B']) == 1
|
||||
|
||||
|
||||
def test_pather_jog_omitted_length_uses_minimum_length_route() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
|
|
|
|||
|
|
@ -327,6 +327,22 @@ def test_selection_management() -> None:
|
|||
assert 'B' not in p.pattern.ports
|
||||
assert pp.ports == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize('action', ['plug', 'plugged', 'rename', 'mark', 'fork'])
|
||||
def test_empty_selection_exact_one_operations_raise_build_error(action: str) -> None:
|
||||
p = Pather(Library())
|
||||
pp = p.at([])
|
||||
operations = {
|
||||
'plug': lambda: pp.plug('unused', 'A'),
|
||||
'plugged': lambda: pp.plugged('A'),
|
||||
'rename': lambda: pp.rename('new'),
|
||||
'mark': lambda: pp.mark('new'),
|
||||
'fork': lambda: pp.fork('new'),
|
||||
}
|
||||
|
||||
with pytest.raises(BuildError, match='expected exactly one'):
|
||||
operations[action]()
|
||||
|
||||
def test_mark_fork() -> None:
|
||||
lib = Library()
|
||||
p = Pather(lib)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ import numpy
|
|||
import pytest
|
||||
from numpy import pi
|
||||
|
||||
from masque import Library, Path, Port, Pather
|
||||
from masque import Library, Path, Port, Pather, ToolContractError
|
||||
from masque.builder.planner import RoutingPlanner
|
||||
from masque.builder.planner.planner import Solver, SolverRequest
|
||||
from masque.builder.tools import (
|
||||
BendOffer,
|
||||
PathTool,
|
||||
|
|
@ -46,6 +47,81 @@ class PlanningOnlyTool(Tool):
|
|||
return tree
|
||||
|
||||
|
||||
def test_tool_contract_error_is_fatal_even_when_an_alternate_offer_exists() -> None:
|
||||
class BrokenTool(PlanningOnlyTool):
|
||||
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||
if kind != 'straight':
|
||||
return ()
|
||||
return (
|
||||
StraightOffer(
|
||||
in_ptype='wire',
|
||||
out_ptype='wire',
|
||||
endpoint_planner=lambda length: Port((length, 0), pi, ptype='wrong'),
|
||||
commit_planner=lambda length: length,
|
||||
),
|
||||
StraightOffer.generated('wire', lambda length: length),
|
||||
)
|
||||
|
||||
pather = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=BrokenTool(),
|
||||
render='deferred',
|
||||
)
|
||||
with pytest.raises(ToolContractError, match='declared offer out_ptype'):
|
||||
pather.straight('A', 5)
|
||||
|
||||
|
||||
def test_callback_build_error_remains_recoverable_candidate_rejection() -> None:
|
||||
class RecoverableTool(PlanningOnlyTool):
|
||||
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||
if kind != 'straight':
|
||||
return ()
|
||||
|
||||
def rejected(length: float) -> Port:
|
||||
raise BuildError(f'unsupported length {length}')
|
||||
|
||||
return (
|
||||
StraightOffer(
|
||||
in_ptype='wire',
|
||||
out_ptype='wire',
|
||||
endpoint_planner=rejected,
|
||||
commit_planner=lambda length: length,
|
||||
),
|
||||
StraightOffer.generated('wire', lambda length: length),
|
||||
)
|
||||
|
||||
pather = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=RecoverableTool(),
|
||||
render='deferred',
|
||||
)
|
||||
pather.straight('A', 5)
|
||||
assert numpy.allclose(pather.ports['A'].offset, (-5, 0))
|
||||
|
||||
|
||||
def test_solver_offer_cache_accepts_unhashable_request_tool_options() -> None:
|
||||
class CountingTool(PlanningOnlyTool):
|
||||
calls = 0
|
||||
|
||||
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||
self.calls += 1
|
||||
assert kwargs == {'nested': []}
|
||||
return ()
|
||||
|
||||
tool = CountingTool()
|
||||
solver = Solver(SolverRequest(
|
||||
family='straight',
|
||||
tool=tool,
|
||||
in_ptype='wire',
|
||||
tool_options={'nested': []},
|
||||
))
|
||||
solver.primitive_offers('straight', 'wire')
|
||||
solver.primitive_offers('straight', 'wire')
|
||||
assert tool.calls == 1
|
||||
|
||||
|
||||
def test_tool_requires_primitive_offers_override() -> None:
|
||||
class RenderOnlyTool(Tool):
|
||||
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
||||
|
|
@ -100,7 +176,19 @@ def test_offer_canonicalize_parameter_rejects_non_finite_parameter(value: float)
|
|||
|
||||
def test_offer_canonicalize_parameter_rejects_reversed_domain() -> None:
|
||||
with pytest.raises(BuildError, match='lower bound must not exceed upper bound'):
|
||||
canonicalize_offer_parameter(3, (10, 0))
|
||||
StraightOffer(in_ptype='wire', out_ptype='wire', length_domain=(10, 0))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('domain', [(numpy.nan, 1), (1, numpy.nan), (numpy.inf, numpy.inf)])
|
||||
def test_offer_rejects_invalid_domain_at_construction(domain: tuple[float, float]) -> None:
|
||||
with pytest.raises(BuildError, match='domain'):
|
||||
StraightOffer(in_ptype='wire', out_ptype='wire', length_domain=domain)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('domain', [(-1, 2), (-numpy.inf, 2)])
|
||||
def test_length_offer_requires_finite_nonnegative_minimum(domain: tuple[float, float]) -> None:
|
||||
with pytest.raises(BuildError, match='finite, nonnegative minimum'):
|
||||
StraightOffer(in_ptype='wire', out_ptype='wire', length_domain=domain)
|
||||
|
||||
|
||||
def test_ptype_match_distinguishes_exact_wildcard_and_mismatch() -> None:
|
||||
|
|
@ -653,7 +741,7 @@ def test_solver_rejects_rotation_impossible_candidates_before_parameter_solving(
|
|||
p.straight('A', 7)
|
||||
|
||||
assert p._paths['A'][0].data == {'kind': 'valid', 'length': 7}
|
||||
assert invalid_parameters == [0.0, 0.0]
|
||||
assert invalid_parameters == [0.0]
|
||||
|
||||
|
||||
def test_pather_commits_only_selected_offer() -> None:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import numpy
|
|||
from numpy import pi
|
||||
from numpy.testing import assert_allclose
|
||||
|
||||
from ..builder import Pather, RouteError
|
||||
from ..builder import Pather, RouteError, ToolContractError
|
||||
from ..builder.tools import PathTool, RenderStep, StraightOffer, Tool
|
||||
from ..error import BuildError
|
||||
from ..library import Library
|
||||
|
|
@ -307,7 +307,7 @@ def test_pathtool_bend_offer_render_geometry_matches_ports() -> None:
|
|||
offer = tool.primitive_offers("bend", in_ptype="wire", ccw=True)[0]
|
||||
start = Port((0, 0), rotation=pi, ptype="wire")
|
||||
end = offer.endpoint_at(1)
|
||||
tree = tool.render((RenderStep(offer.opcode, tool, start, end, offer.commit(1)),))
|
||||
tree = tool.render((RenderStep(offer.kind, tool, start, end, offer.commit(1)),))
|
||||
pat = tree.top_pattern()
|
||||
path_shape = cast("Path", pat.shapes[(1, 0)][0])
|
||||
|
||||
|
|
@ -321,13 +321,13 @@ def test_pathtool_s_offer_render_geometry_matches_ports() -> None:
|
|||
offer = tool.primitive_offers("s", in_ptype="wire")[0]
|
||||
start = Port((0, 0), rotation=pi, ptype="wire")
|
||||
end = offer.endpoint_at(4)
|
||||
tree = tool.render((RenderStep(offer.opcode, tool, start, end, offer.commit(4)),))
|
||||
tree = tool.render((RenderStep(offer.kind, tool, start, end, offer.commit(4)),))
|
||||
pat = tree.top_pattern()
|
||||
path_shape = cast("Path", pat.shapes[(1, 0)][0])
|
||||
|
||||
assert_allclose(path_shape.vertices, [[0, 0], [1, 0], [1, 4], [2, 4]], atol=1e-10)
|
||||
assert_allclose(pat.ports["B"].offset, [2, 4], atol=1e-10)
|
||||
assert_allclose(pat.ports["B"].rotation, 0, atol=1e-10)
|
||||
assert_allclose(pat.ports["B"].rotation, pi, atol=1e-10)
|
||||
|
||||
def test_deferred_render_uturn_fallback() -> None:
|
||||
lib = Library()
|
||||
|
|
@ -362,7 +362,7 @@ def test_pather_render_auto_renames_single_use_tool_children() -> None:
|
|||
tree = Library()
|
||||
top = Pattern(ports={
|
||||
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
||||
port_names[1]: Port((length, 0), 0, ptype='wire'),
|
||||
port_names[1]: Port((length, 0), pi, ptype='wire'),
|
||||
})
|
||||
child = Pattern(annotations={'batch': [len(batch)]})
|
||||
top.ref('_seg')
|
||||
|
|
@ -402,7 +402,7 @@ def test_custom_tool_render_preserves_segment_subtrees() -> None:
|
|||
tree = Library()
|
||||
top = Pattern(ports={
|
||||
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
||||
port_names[1]: Port((length, 0), 0, ptype='wire'),
|
||||
port_names[1]: Port((length, 0), pi, ptype='wire'),
|
||||
})
|
||||
child = Pattern(annotations={'length': [length]})
|
||||
top.ref('_seg')
|
||||
|
|
@ -479,7 +479,7 @@ def test_pather_render_allows_missing_non_single_use_tool_refs() -> None:
|
|||
tree = Library()
|
||||
top = Pattern(ports={
|
||||
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
||||
port_names[1]: Port((length, 0), 0, ptype='wire'),
|
||||
port_names[1]: Port((length, 0), pi, ptype='wire'),
|
||||
})
|
||||
top.ref('shared')
|
||||
tree['_top'] = top
|
||||
|
|
@ -530,6 +530,52 @@ def test_pather_render_rejects_output_port_that_misses_planned_endpoint(append:
|
|||
assert not p.pattern.refs
|
||||
assert not lib
|
||||
|
||||
|
||||
def test_pather_render_rejects_opposite_output_rotation() -> None:
|
||||
class WrongRotationTool(Tool):
|
||||
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||
if kind != 'straight':
|
||||
return ()
|
||||
return (StraightOffer.generated('wire', lambda length: length),)
|
||||
|
||||
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
||||
length = batch[0].data
|
||||
tree = Library()
|
||||
tree['_top'] = Pattern(ports={
|
||||
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
||||
port_names[1]: Port((length, 0), 0, ptype='wire'),
|
||||
})
|
||||
return tree
|
||||
|
||||
p = Pather(Library(), tools=WrongRotationTool(), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.straight('A', 10)
|
||||
|
||||
with pytest.raises(ToolContractError, match='does not match planned endpoint'):
|
||||
p.render()
|
||||
|
||||
|
||||
def test_pather_render_allows_unspecified_output_rotation() -> None:
|
||||
class UnspecifiedRotationTool(Tool):
|
||||
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs): # noqa: ANN001,ANN202,ARG002
|
||||
if kind != 'straight':
|
||||
return ()
|
||||
return (StraightOffer.generated('wire', lambda length: length),)
|
||||
|
||||
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
||||
length = batch[0].data
|
||||
tree = Library()
|
||||
tree['_top'] = Pattern(ports={
|
||||
port_names[0]: Port((0, 0), 0, ptype='wire'),
|
||||
port_names[1]: Port((length, 0), None, ptype='wire'),
|
||||
})
|
||||
return tree
|
||||
|
||||
p = Pather(Library(), tools=UnspecifiedRotationTool(), render='deferred')
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.straight('A', 10)
|
||||
p.render()
|
||||
|
||||
@pytest.mark.parametrize('append', [True, False])
|
||||
def test_pather_render_rejects_output_port_with_wrong_ptype(append: bool) -> None:
|
||||
class WrongPtypeTool(Tool):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue