[RouteError] include callsite traceback in route errors
This commit is contained in:
parent
ce7463e57c
commit
1b84c87d9b
4 changed files with 68 additions and 3 deletions
|
|
@ -6,7 +6,7 @@ from enum import Enum, auto
|
|||
from pprint import pformat
|
||||
from types import MappingProxyType
|
||||
|
||||
from ..error import BuildError
|
||||
from ..error import BuildError, format_stacktrace
|
||||
|
||||
|
||||
RouteOperation = Literal['trace', 'trace_to', 'jog', 'uturn']
|
||||
|
|
@ -68,7 +68,7 @@ class RouteFailureDetails:
|
|||
|
||||
|
||||
class RouteError(BuildError):
|
||||
"""A route-selection failure with structured request diagnostics."""
|
||||
"""A route-selection failure with structured request and call-site diagnostics."""
|
||||
|
||||
details: RouteFailureDetails
|
||||
policy: RouteFailurePolicy
|
||||
|
|
@ -103,4 +103,7 @@ 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))
|
||||
super().__init__('\n'.join(lines))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from collections.abc import Iterator
|
||||
from types import FunctionType
|
||||
|
||||
import pytest
|
||||
|
||||
from ..builder import Pather
|
||||
from ..builder import Pather, PathTool
|
||||
from ..error import BuildError, LibraryError
|
||||
from ..library import (
|
||||
INameView,
|
||||
|
|
@ -28,6 +29,17 @@ def _owned_by(report: BuildReport, owner: str) -> set[str]:
|
|||
}
|
||||
|
||||
|
||||
def _external_failing_route_recipe(lib: ILibrary) -> Pattern:
|
||||
pather = Pather(
|
||||
lib,
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=PathTool(layer='M1', width=2, ptype='wire'),
|
||||
render='deferred',
|
||||
)
|
||||
pather.ccw('A', 10, out_ptype='optical')
|
||||
return pather.pattern
|
||||
|
||||
|
||||
class _MetadataSource(ILibraryView):
|
||||
def __init__(self, mapping: dict[str, Pattern], child_graph: dict[str, set[str]]) -> None:
|
||||
self.mapping = mapping
|
||||
|
|
@ -263,6 +275,24 @@ def test_build_library_validate_is_retryable_after_failure() -> None:
|
|||
assert report.dependency_graph["parent"] == frozenset({"child"})
|
||||
|
||||
|
||||
def test_build_library_error_preserves_route_call_site() -> None:
|
||||
builder = LibraryBuilder()
|
||||
external_recipe = FunctionType(
|
||||
_external_failing_route_recipe.__code__.replace(co_filename='/project/user_recipe.py'),
|
||||
globals(),
|
||||
)
|
||||
builder.cells.top = cell(external_recipe)(builder.library)
|
||||
|
||||
with pytest.raises(BuildError) as exc_info:
|
||||
builder.validate()
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
|
||||
builder = LibraryBuilder()
|
||||
builder["child"] = Pattern()
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ def test_route_failure_details_enforces_minimum_status_invariants() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_plain_build_error_does_not_include_stacktrace() -> None:
|
||||
assert str(BuildError('plain builder failure')) == 'plain builder failure'
|
||||
|
||||
|
||||
def test_builder_init() -> None:
|
||||
lib = Library()
|
||||
b = Pather(lib, name="mypat")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from collections.abc import Sequence
|
||||
from typing import Any, Literal, Never
|
||||
from types import FunctionType
|
||||
import inspect
|
||||
|
||||
import pytest
|
||||
|
|
@ -34,6 +35,10 @@ class PlanningOnlyTool(Tool):
|
|||
return tree
|
||||
|
||||
|
||||
def _external_failing_ccw(pather: Pather) -> None:
|
||||
pather.ccw('A', 10, out_ptype='optical')
|
||||
|
||||
|
||||
class FirstPortOnlyTraceTool(PlanningOnlyTool):
|
||||
def __init__(self) -> None:
|
||||
self.render_calls = 0
|
||||
|
|
@ -270,6 +275,29 @@ def test_route_error_reports_no_route_at_any_length() -> None:
|
|||
assert 'no legal route exists at any length' in str(exc_info.value)
|
||||
|
||||
|
||||
def test_route_error_reports_external_pather_call_site() -> 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_ccw.__code__.replace(co_filename='/project/user_layout.py'),
|
||||
globals(),
|
||||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
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
|
||||
|
||||
|
||||
def test_route_error_reports_failed_minimum_diagnosis(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
planner = RoutingPlanner()
|
||||
context = RoutePortContext(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue