[Pather/Planner] Doc cleanup and more informative error enums

This commit is contained in:
Jan Petykiewicz 2026-07-12 17:25:13 -07:00
commit 49713896c3
13 changed files with 370 additions and 95 deletions

View file

@ -5,8 +5,9 @@ import pytest
import numpy
from numpy import pi
from masque import Pather, Library, Port, RouteError
from masque import MinimumStatus, Pather, 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
from masque.error import BuildError
from masque.library import ILibrary
@ -216,8 +217,8 @@ def test_route_error_reports_preferred_minimum_and_request_details() -> None:
assert details.resolved_jog is None
# The length-2 route exists, but normal cost ranking prefers length 5.
assert details.minimum_length == 5
assert details.minimum_attempted
assert not details.minimum_exhausted
assert details.minimum_status is MinimumStatus.FOUND
assert exc_info.value.policy is RouteFailurePolicy.RECOVERABLE
assert details.minimum_cause is None
assert 'preferred_minimum_length: 5' in str(exc_info.value)
assert tool.commit_calls == 0
@ -260,13 +261,53 @@ def test_route_error_reports_no_route_at_any_length() -> None:
p.ccw('A', 10, out_ptype='optical')
details = exc_info.value.details
assert details.minimum_attempted
assert details.minimum_status is MinimumStatus.NO_ROUTE
assert details.minimum_length is None
assert details.minimum_exhausted
assert details.minimum_cause is not None
assert 'no legal route exists at any length' in str(exc_info.value)
def test_route_error_reports_failed_minimum_diagnosis(monkeypatch: pytest.MonkeyPatch) -> None:
planner = RoutingPlanner()
context = RoutePortContext(
'A',
Port((0, 0), rotation=0, ptype='wire'),
PlanningOnlyTool(),
)
solve_count = 0
def solver_for_request(_request: Any) -> Any:
nonlocal solve_count
solve_count += 1
current_solve = solve_count
class FailingSolver:
def solve(self) -> Never:
if current_solve == 1:
raise NoLegalRouteError('requested length has no route')
raise RuntimeError('minimum diagnosis failed')
return FailingSolver()
monkeypatch.setattr(planner, 'solver_for_request', solver_for_request)
with pytest.raises(RouteError) as exc_info:
planner.plan_leg(
'bend',
context,
'trace_to',
{'ccw': True, 'length': 1},
length=1,
ccw=True,
)
details = exc_info.value.details
assert details.minimum_status is MinimumStatus.FAILED
assert details.minimum_length is None
assert details.minimum_cause == 'minimum diagnosis failed'
assert 'minimum-length calculation failed' in str(exc_info.value)
@pytest.mark.parametrize('length', [-1, numpy.nan, numpy.inf])
def test_invalid_route_length_fails_before_offer_query_or_dead_fallback(length: float) -> None:
tool = RequestCountingTool()
@ -281,7 +322,8 @@ def test_invalid_route_length_fails_before_offer_query_or_dead_fallback(length:
p.ccw('A', length)
details = exc_info.value.details
assert not details.minimum_attempted
assert details.minimum_status is MinimumStatus.NOT_EVALUATED
assert exc_info.value.policy is RouteFailurePolicy.FATAL
assert details.minimum_length is None
assert tool.offer_calls == 0
assert numpy.allclose(p.ports['A'].offset, (0, 0))
@ -302,7 +344,7 @@ def test_negative_position_length_reports_bound_without_offer_query() -> None:
details = exc_info.value.details
assert details.request == {'ccw': True, 'x': 1}
assert details.resolved_length == -1
assert not details.minimum_attempted
assert details.minimum_status is MinimumStatus.NOT_EVALUATED
assert tool.offer_calls == 0
@ -325,7 +367,7 @@ def test_negative_bundle_length_reports_failing_port_and_bound() -> None:
assert details.portspec == 'A'
assert details.request == {'ccw': True, 'emax': 0, 'spacing': 2}
assert details.resolved_length == -2
assert not details.minimum_attempted
assert details.minimum_status is MinimumStatus.NOT_EVALUATED
assert tool.offer_calls == 0
@ -348,7 +390,7 @@ def test_negative_each_length_reports_failing_port_without_offer_query() -> None
assert details.portspec == 'A'
assert details.request == {'ccw': None, 'each': -2}
assert details.resolved_length == -2
assert not details.minimum_attempted
assert details.minimum_status is MinimumStatus.NOT_EVALUATED
assert tool.offer_calls == 0
@ -887,7 +929,7 @@ def test_pather_uturn_failed_two_bend_route_is_atomic() -> None:
with pytest.raises(RouteError, match='U-turn') as exc_info:
p.uturn('A', 1.5, length=0)
assert exc_info.value.details.minimum_attempted
assert exc_info.value.details.minimum_status is MinimumStatus.NO_ROUTE
assert exc_info.value.details.minimum_length is None
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))