[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 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))
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue