[pather / RouteError] improve error reporting
This commit is contained in:
parent
1b84c87d9b
commit
4d9aaf2fd9
4 changed files with 118 additions and 25 deletions
|
|
@ -5,8 +5,9 @@ from dataclasses import dataclass
|
|||
from enum import Enum, auto
|
||||
from pprint import pformat
|
||||
from types import MappingProxyType
|
||||
import traceback
|
||||
|
||||
from ..error import BuildError, format_stacktrace
|
||||
from ..error import BuildError
|
||||
|
||||
|
||||
RouteOperation = Literal['trace', 'trace_to', 'jog', 'uturn']
|
||||
|
|
@ -68,10 +69,11 @@ class RouteFailureDetails:
|
|||
|
||||
|
||||
class RouteError(BuildError):
|
||||
"""A route-selection failure with structured request and call-site diagnostics."""
|
||||
"""A route-selection failure with structured request and saved call-stack diagnostics."""
|
||||
|
||||
details: RouteFailureDetails
|
||||
policy: RouteFailurePolicy
|
||||
_call_stack: tuple[traceback.FrameSummary, ...]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -103,7 +105,5 @@ class RouteError(BuildError):
|
|||
]
|
||||
if details.minimum_cause is not None:
|
||||
lines.append(f' minimum_failure: {details.minimum_cause}')
|
||||
stacktrace = format_stacktrace().rstrip()
|
||||
if stacktrace:
|
||||
lines.extend(('', 'Stack trace:', stacktrace))
|
||||
self._call_stack = tuple(traceback.extract_stack()[:-1])
|
||||
super().__init__('\n'.join(lines))
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ from .planner.interface import (
|
|||
RoutePortContext,
|
||||
route_failure_policy,
|
||||
)
|
||||
from .error import RouteFailurePolicy, ToolContractError
|
||||
from .error import RouteError, RouteFailurePolicy, ToolContractError
|
||||
from .planner import RoutingPlanner
|
||||
from .planner.bounds import resolved_position_bound
|
||||
from .logging import PatherLogger
|
||||
|
|
@ -699,6 +699,8 @@ class Pather(PortList):
|
|||
tool_options=tool_opts, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if isinstance(err, RouteError):
|
||||
err.__traceback__ = None
|
||||
if not self._dead or route_failure_policy(err) is RouteFailurePolicy.FATAL:
|
||||
raise
|
||||
if length is not None and len(contexts) == 1:
|
||||
|
|
@ -805,6 +807,8 @@ class Pather(PortList):
|
|||
tool_options=tool_opts, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if isinstance(err, RouteError):
|
||||
err.__traceback__ = None
|
||||
if (
|
||||
not self._dead
|
||||
or len(contexts) != 1
|
||||
|
|
@ -1021,6 +1025,8 @@ class Pather(PortList):
|
|||
tool_options=tool_opts, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if isinstance(err, RouteError):
|
||||
err.__traceback__ = None
|
||||
if (
|
||||
not self._dead
|
||||
or len(contexts) != 1
|
||||
|
|
@ -1098,6 +1104,8 @@ class Pather(PortList):
|
|||
tool_options=tool_opts, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if isinstance(err, RouteError):
|
||||
err.__traceback__ = None
|
||||
if (
|
||||
not self._dead
|
||||
or len(contexts) != 1
|
||||
|
|
@ -1170,16 +1178,20 @@ class Pather(PortList):
|
|||
plan_options=plan_opts,
|
||||
tool_options=tool_opts,
|
||||
):
|
||||
result = self.planner.plan_trace_into(
|
||||
self._route_context(portspec_src),
|
||||
portspec_dst,
|
||||
self.pattern[portspec_dst].copy(),
|
||||
out_ptype = out_ptype,
|
||||
plug_destination = plug_destination,
|
||||
thru = thru,
|
||||
plan_options = plan_opts,
|
||||
tool_options = tool_opts,
|
||||
)
|
||||
try:
|
||||
result = self.planner.plan_trace_into(
|
||||
self._route_context(portspec_src),
|
||||
portspec_dst,
|
||||
self.pattern[portspec_dst].copy(),
|
||||
out_ptype = out_ptype,
|
||||
plug_destination = plug_destination,
|
||||
thru = thru,
|
||||
plan_options = plan_opts,
|
||||
tool_options = tool_opts,
|
||||
)
|
||||
except RouteError as err:
|
||||
err.__traceback__ = None
|
||||
raise
|
||||
self._apply_route_result(result)
|
||||
return self
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from collections.abc import Iterator
|
||||
from types import FunctionType
|
||||
import traceback
|
||||
|
||||
import pytest
|
||||
|
||||
from ..builder import Pather, PathTool
|
||||
from ..builder import Pather, PathTool, RouteError
|
||||
from ..error import BuildError, LibraryError
|
||||
from ..library import (
|
||||
INameView,
|
||||
|
|
@ -288,9 +289,19 @@ def test_build_library_error_preserves_route_call_site() -> None:
|
|||
|
||||
message = str(exc_info.value)
|
||||
assert 'Cause: Unable to plan trace_to route' in message
|
||||
assert 'Stack trace:' in message
|
||||
assert 'File "/project/user_recipe.py"' in message
|
||||
assert 'in _external_failing_route_recipe' in message
|
||||
assert 'Stack trace:' not in message
|
||||
|
||||
route_error = exc_info.value.__cause__
|
||||
assert isinstance(route_error, RouteError)
|
||||
assert route_error.__traceback__ is not None
|
||||
route_frames = traceback.extract_tb(route_error.__traceback__)
|
||||
assert sum(frame.filename == '/project/user_recipe.py' for frame in route_frames) == 1
|
||||
assert not any('/builder/planner/' in frame.filename for frame in route_frames)
|
||||
assert route_error.__cause__ is not None
|
||||
assert any('/builder/planner/' in frame.filename for frame in route_error._call_stack)
|
||||
|
||||
native = ''.join(traceback.format_exception(exc_info.value))
|
||||
assert native.count('/project/user_recipe.py') == 1
|
||||
|
||||
|
||||
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from collections.abc import Sequence
|
|||
from typing import Any, Literal, Never
|
||||
from types import FunctionType
|
||||
import inspect
|
||||
import traceback
|
||||
|
||||
import pytest
|
||||
import numpy
|
||||
|
|
@ -39,6 +40,14 @@ def _external_failing_ccw(pather: Pather) -> None:
|
|||
pather.ccw('A', 10, out_ptype='optical')
|
||||
|
||||
|
||||
def _external_failing_trace_to(pather: Pather) -> None:
|
||||
pather.trace_to('A', True, length=10, out_ptype='optical')
|
||||
|
||||
|
||||
def _external_failing_portpather_ccw(pather: Pather) -> None:
|
||||
pather.at('A').ccw(10, out_ptype='optical')
|
||||
|
||||
|
||||
class FirstPortOnlyTraceTool(PlanningOnlyTool):
|
||||
def __init__(self) -> None:
|
||||
self.render_calls = 0
|
||||
|
|
@ -291,11 +300,72 @@ def test_route_error_reports_external_pather_call_site() -> None:
|
|||
external_call(p)
|
||||
|
||||
message = str(exc_info.value)
|
||||
assert 'Stack trace:' in message
|
||||
assert 'File "/project/user_layout.py"' in message
|
||||
assert 'in _external_failing_ccw' in message
|
||||
assert '/masque/builder/pather.py' not in message
|
||||
assert '/masque/builder/planner/' not in message
|
||||
assert 'Stack trace:' not in message
|
||||
|
||||
route_error = exc_info.value
|
||||
native_frames = traceback.extract_tb(route_error.__traceback__)
|
||||
assert native_frames
|
||||
assert [frame.name for frame in native_frames if frame.filename.endswith('/builder/pather.py')] == [
|
||||
'ccw',
|
||||
'bend',
|
||||
]
|
||||
assert not any('/builder/planner/' in frame.filename for frame in native_frames)
|
||||
|
||||
native = ''.join(traceback.format_exception(route_error))
|
||||
assert native.count('/project/user_layout.py') == 1
|
||||
assert '\nStack trace:\n' not in native
|
||||
assert route_error.__cause__ is not None
|
||||
|
||||
assert isinstance(route_error._call_stack, tuple)
|
||||
assert sum(frame.filename == '/project/user_layout.py' for frame in route_error._call_stack) == 1
|
||||
assert any(frame.filename.endswith('/builder/pather.py') for frame in route_error._call_stack)
|
||||
assert any('/builder/planner/' in frame.filename for frame in route_error._call_stack)
|
||||
|
||||
|
||||
def test_route_error_direct_trace_to_starts_native_traceback_at_caller() -> None:
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=PathTool(layer='M1', width=2, ptype='wire'),
|
||||
render='deferred',
|
||||
)
|
||||
external_call = FunctionType(
|
||||
_external_failing_trace_to.__code__.replace(co_filename='/project/direct_route.py'),
|
||||
globals(),
|
||||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
external_call(p)
|
||||
|
||||
frames = traceback.extract_tb(exc_info.value.__traceback__)
|
||||
assert sum(frame.filename == '/project/direct_route.py' for frame in frames) == 1
|
||||
assert not any(frame.filename.endswith('/builder/pather.py') for frame in frames)
|
||||
assert not any('/builder/planner/' in frame.filename for frame in frames)
|
||||
|
||||
|
||||
def test_route_error_portpather_keeps_only_forwarding_frames() -> None:
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=PathTool(layer='M1', width=2, ptype='wire'),
|
||||
render='deferred',
|
||||
)
|
||||
external_call = FunctionType(
|
||||
_external_failing_portpather_ccw.__code__.replace(co_filename='/project/selected_route.py'),
|
||||
globals(),
|
||||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
external_call(p)
|
||||
|
||||
frames = traceback.extract_tb(exc_info.value.__traceback__)
|
||||
assert sum(frame.filename == '/project/selected_route.py' for frame in frames) == 1
|
||||
assert [frame.name for frame in frames if frame.filename.endswith('/builder/pather.py')] == [
|
||||
'ccw',
|
||||
'bend',
|
||||
'trace_to',
|
||||
]
|
||||
assert not any('/builder/planner/' in frame.filename for frame in frames)
|
||||
|
||||
|
||||
def test_route_error_reports_failed_minimum_diagnosis(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue