[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 enum import Enum, auto
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
import traceback
|
||||||
|
|
||||||
from ..error import BuildError, format_stacktrace
|
from ..error import BuildError
|
||||||
|
|
||||||
|
|
||||||
RouteOperation = Literal['trace', 'trace_to', 'jog', 'uturn']
|
RouteOperation = Literal['trace', 'trace_to', 'jog', 'uturn']
|
||||||
|
|
@ -68,10 +69,11 @@ class RouteFailureDetails:
|
||||||
|
|
||||||
|
|
||||||
class RouteError(BuildError):
|
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
|
details: RouteFailureDetails
|
||||||
policy: RouteFailurePolicy
|
policy: RouteFailurePolicy
|
||||||
|
_call_stack: tuple[traceback.FrameSummary, ...]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -103,7 +105,5 @@ class RouteError(BuildError):
|
||||||
]
|
]
|
||||||
if details.minimum_cause is not None:
|
if details.minimum_cause is not None:
|
||||||
lines.append(f' minimum_failure: {details.minimum_cause}')
|
lines.append(f' minimum_failure: {details.minimum_cause}')
|
||||||
stacktrace = format_stacktrace().rstrip()
|
self._call_stack = tuple(traceback.extract_stack()[:-1])
|
||||||
if stacktrace:
|
|
||||||
lines.extend(('', 'Stack trace:', stacktrace))
|
|
||||||
super().__init__('\n'.join(lines))
|
super().__init__('\n'.join(lines))
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ from .planner.interface import (
|
||||||
RoutePortContext,
|
RoutePortContext,
|
||||||
route_failure_policy,
|
route_failure_policy,
|
||||||
)
|
)
|
||||||
from .error import RouteFailurePolicy, ToolContractError
|
from .error import RouteError, RouteFailurePolicy, ToolContractError
|
||||||
from .planner import RoutingPlanner
|
from .planner import RoutingPlanner
|
||||||
from .planner.bounds import resolved_position_bound
|
from .planner.bounds import resolved_position_bound
|
||||||
from .logging import PatherLogger
|
from .logging import PatherLogger
|
||||||
|
|
@ -699,6 +699,8 @@ class Pather(PortList):
|
||||||
tool_options=tool_opts, **bounds,
|
tool_options=tool_opts, **bounds,
|
||||||
)
|
)
|
||||||
except (BuildError, NotImplementedError) as err:
|
except (BuildError, NotImplementedError) as err:
|
||||||
|
if isinstance(err, RouteError):
|
||||||
|
err.__traceback__ = None
|
||||||
if not self._dead or route_failure_policy(err) is RouteFailurePolicy.FATAL:
|
if not self._dead or route_failure_policy(err) is RouteFailurePolicy.FATAL:
|
||||||
raise
|
raise
|
||||||
if length is not None and len(contexts) == 1:
|
if length is not None and len(contexts) == 1:
|
||||||
|
|
@ -805,6 +807,8 @@ class Pather(PortList):
|
||||||
tool_options=tool_opts, **bounds,
|
tool_options=tool_opts, **bounds,
|
||||||
)
|
)
|
||||||
except (BuildError, NotImplementedError) as err:
|
except (BuildError, NotImplementedError) as err:
|
||||||
|
if isinstance(err, RouteError):
|
||||||
|
err.__traceback__ = None
|
||||||
if (
|
if (
|
||||||
not self._dead
|
not self._dead
|
||||||
or len(contexts) != 1
|
or len(contexts) != 1
|
||||||
|
|
@ -1021,6 +1025,8 @@ class Pather(PortList):
|
||||||
tool_options=tool_opts, **bounds,
|
tool_options=tool_opts, **bounds,
|
||||||
)
|
)
|
||||||
except (BuildError, NotImplementedError) as err:
|
except (BuildError, NotImplementedError) as err:
|
||||||
|
if isinstance(err, RouteError):
|
||||||
|
err.__traceback__ = None
|
||||||
if (
|
if (
|
||||||
not self._dead
|
not self._dead
|
||||||
or len(contexts) != 1
|
or len(contexts) != 1
|
||||||
|
|
@ -1098,6 +1104,8 @@ class Pather(PortList):
|
||||||
tool_options=tool_opts, **bounds,
|
tool_options=tool_opts, **bounds,
|
||||||
)
|
)
|
||||||
except (BuildError, NotImplementedError) as err:
|
except (BuildError, NotImplementedError) as err:
|
||||||
|
if isinstance(err, RouteError):
|
||||||
|
err.__traceback__ = None
|
||||||
if (
|
if (
|
||||||
not self._dead
|
not self._dead
|
||||||
or len(contexts) != 1
|
or len(contexts) != 1
|
||||||
|
|
@ -1170,16 +1178,20 @@ class Pather(PortList):
|
||||||
plan_options=plan_opts,
|
plan_options=plan_opts,
|
||||||
tool_options=tool_opts,
|
tool_options=tool_opts,
|
||||||
):
|
):
|
||||||
result = self.planner.plan_trace_into(
|
try:
|
||||||
self._route_context(portspec_src),
|
result = self.planner.plan_trace_into(
|
||||||
portspec_dst,
|
self._route_context(portspec_src),
|
||||||
self.pattern[portspec_dst].copy(),
|
portspec_dst,
|
||||||
out_ptype = out_ptype,
|
self.pattern[portspec_dst].copy(),
|
||||||
plug_destination = plug_destination,
|
out_ptype = out_ptype,
|
||||||
thru = thru,
|
plug_destination = plug_destination,
|
||||||
plan_options = plan_opts,
|
thru = thru,
|
||||||
tool_options = tool_opts,
|
plan_options = plan_opts,
|
||||||
)
|
tool_options = tool_opts,
|
||||||
|
)
|
||||||
|
except RouteError as err:
|
||||||
|
err.__traceback__ = None
|
||||||
|
raise
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
|
import traceback
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ..builder import Pather, PathTool
|
from ..builder import Pather, PathTool, RouteError
|
||||||
from ..error import BuildError, LibraryError
|
from ..error import BuildError, LibraryError
|
||||||
from ..library import (
|
from ..library import (
|
||||||
INameView,
|
INameView,
|
||||||
|
|
@ -288,9 +289,19 @@ def test_build_library_error_preserves_route_call_site() -> None:
|
||||||
|
|
||||||
message = str(exc_info.value)
|
message = str(exc_info.value)
|
||||||
assert 'Cause: Unable to plan trace_to route' in message
|
assert 'Cause: Unable to plan trace_to route' in message
|
||||||
assert 'Stack trace:' in message
|
assert 'Stack trace:' not in message
|
||||||
assert 'File "/project/user_recipe.py"' in message
|
|
||||||
assert 'in _external_failing_route_recipe' 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:
|
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 typing import Any, Literal, Never
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
import inspect
|
import inspect
|
||||||
|
import traceback
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -39,6 +40,14 @@ def _external_failing_ccw(pather: Pather) -> None:
|
||||||
pather.ccw('A', 10, out_ptype='optical')
|
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):
|
class FirstPortOnlyTraceTool(PlanningOnlyTool):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.render_calls = 0
|
self.render_calls = 0
|
||||||
|
|
@ -291,11 +300,72 @@ def test_route_error_reports_external_pather_call_site() -> None:
|
||||||
external_call(p)
|
external_call(p)
|
||||||
|
|
||||||
message = str(exc_info.value)
|
message = str(exc_info.value)
|
||||||
assert 'Stack trace:' in message
|
assert 'Stack trace:' not in message
|
||||||
assert 'File "/project/user_layout.py"' in message
|
|
||||||
assert 'in _external_failing_ccw' in message
|
route_error = exc_info.value
|
||||||
assert '/masque/builder/pather.py' not in message
|
native_frames = traceback.extract_tb(route_error.__traceback__)
|
||||||
assert '/masque/builder/planner/' not in message
|
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:
|
def test_route_error_reports_failed_minimum_diagnosis(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue