[RouteError] include callsite traceback in route errors

This commit is contained in:
Jan Petykiewicz 2026-08-27 11:22:37 -07:00
commit 1b84c87d9b
4 changed files with 68 additions and 3 deletions

View file

@ -6,7 +6,7 @@ from enum import Enum, auto
from pprint import pformat from pprint import pformat
from types import MappingProxyType from types import MappingProxyType
from ..error import BuildError from ..error import BuildError, format_stacktrace
RouteOperation = Literal['trace', 'trace_to', 'jog', 'uturn'] RouteOperation = Literal['trace', 'trace_to', 'jog', 'uturn']
@ -68,7 +68,7 @@ class RouteFailureDetails:
class RouteError(BuildError): class RouteError(BuildError):
"""A route-selection failure with structured request diagnostics.""" """A route-selection failure with structured request and call-site diagnostics."""
details: RouteFailureDetails details: RouteFailureDetails
policy: RouteFailurePolicy policy: RouteFailurePolicy
@ -103,4 +103,7 @@ 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()
if stacktrace:
lines.extend(('', 'Stack trace:', stacktrace))
super().__init__('\n'.join(lines)) super().__init__('\n'.join(lines))

View file

@ -1,8 +1,9 @@
from collections.abc import Iterator from collections.abc import Iterator
from types import FunctionType
import pytest import pytest
from ..builder import Pather from ..builder import Pather, PathTool
from ..error import BuildError, LibraryError from ..error import BuildError, LibraryError
from ..library import ( from ..library import (
INameView, 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): class _MetadataSource(ILibraryView):
def __init__(self, mapping: dict[str, Pattern], child_graph: dict[str, set[str]]) -> None: def __init__(self, mapping: dict[str, Pattern], child_graph: dict[str, set[str]]) -> None:
self.mapping = mapping self.mapping = mapping
@ -263,6 +275,24 @@ def test_build_library_validate_is_retryable_after_failure() -> None:
assert report.dependency_graph["parent"] == frozenset({"child"}) 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: def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
builder = LibraryBuilder() builder = LibraryBuilder()
builder["child"] = Pattern() builder["child"] = Pattern()

View file

@ -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: def test_builder_init() -> None:
lib = Library() lib = Library()
b = Pather(lib, name="mypat") b = Pather(lib, name="mypat")

View file

@ -1,5 +1,6 @@
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Literal, Never from typing import Any, Literal, Never
from types import FunctionType
import inspect import inspect
import pytest import pytest
@ -34,6 +35,10 @@ class PlanningOnlyTool(Tool):
return tree return tree
def _external_failing_ccw(pather: Pather) -> None:
pather.ccw('A', 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
@ -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) 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: def test_route_error_reports_failed_minimum_diagnosis(monkeypatch: pytest.MonkeyPatch) -> None:
planner = RoutingPlanner() planner = RoutingPlanner()
context = RoutePortContext( context = RoutePortContext(