Skip to content

Commit 4690071

Browse files
committed
feat(debugger): display a virtual variable in the local scope with details about the current exception when an exception breakpoint is triggered
1 parent b155bd1 commit 4690071

File tree

4 files changed

+80
-93
lines changed

4 files changed

+80
-93
lines changed

packages/debugger/src/robotcode/debugger/debugger.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Sequence,
2727
Set,
2828
Tuple,
29+
TypedDict,
2930
Union,
3031
cast,
3132
)
@@ -283,6 +284,12 @@ def end_keyword(self, data: running.Keyword, result: result.Keyword) -> None:
283284
breakpoint_id_manager = IdManager()
284285

285286

287+
class ExceptionInformation(TypedDict):
288+
text: Optional[str]
289+
description: str
290+
status: str
291+
292+
286293
class Debugger:
287294
__instance: ClassVar[Optional["Debugger"]] = None
288295
__lock: ClassVar = threading.RLock()
@@ -356,6 +363,7 @@ def __init__(self) -> None:
356363
self._evaluate_cache: List[Any] = []
357364
self._variables_cache: Dict[int, Any] = {}
358365
self._variables_object_cache: List[Any] = []
366+
self._current_exception: Optional[ExceptionInformation] = None
359367

360368
@property
361369
def state(self) -> State:
@@ -726,6 +734,8 @@ def process_end_state(
726734
self.requested_state = RequestedState.Nothing
727735
self.state = State.Paused
728736

737+
self._current_exception = {"text": text, "description": description, "status": status}
738+
729739
self.send_event(
730740
self,
731741
StoppedEvent(
@@ -779,6 +789,7 @@ def wait_for_running(self) -> None:
779789
continue
780790

781791
break
792+
self._current_exception = None
782793

783794
def start_output_group(self, name: str, attributes: Dict[str, Any], type: Optional[str] = None) -> None:
784795
if self.group_output:
@@ -993,7 +1004,7 @@ def end_test(self, name: str, attributes: Dict[str, Any]) -> None:
9931004
if status == "FAIL":
9941005
self.process_end_state(
9951006
status,
996-
{"failed_test"},
1007+
{""},
9971008
"Test failed.",
9981009
f"Test failed{f': {v}' if (v := attributes.get('message')) else ''}",
9991010
)
@@ -1158,7 +1169,7 @@ def end_keyword(self, name: str, attributes: Dict[str, Any]) -> None:
11581169
{"uncaught_failed_keyword"}
11591170
if self.is_not_caughted_by_keyword()
11601171
and self.is_not_caugthed_by_except(self.last_fail_message)
1161-
else {}
1172+
else []
11621173
),
11631174
},
11641175
"Keyword failed.",
@@ -1413,7 +1424,9 @@ def _new_cache_id(self) -> int:
14131424

14141425
debug_repr = DebugRepr()
14151426

1416-
def _create_variable(self, name: str, value: Any) -> Variable:
1427+
def _create_variable(
1428+
self, name: str, value: Any, presentation_hint: Optional[VariablePresentationHint] = None
1429+
) -> Variable:
14171430
if isinstance(value, Mapping):
14181431
v_id = self._new_cache_id()
14191432
self._variables_cache[v_id] = value
@@ -1424,7 +1437,9 @@ def _create_variable(self, name: str, value: Any) -> Variable:
14241437
variables_reference=v_id,
14251438
named_variables=len(value) + 1,
14261439
indexed_variables=0,
1427-
presentation_hint=VariablePresentationHint(kind="data"),
1440+
presentation_hint=(
1441+
presentation_hint if presentation_hint is not None else VariablePresentationHint(kind="data")
1442+
),
14281443
)
14291444

14301445
if isinstance(value, Sequence) and not isinstance(value, str):
@@ -1502,6 +1517,14 @@ def get_variables(
15021517
)
15031518
elif entry.local_id == variables_reference:
15041519
vars = entry.get_first_or_self().variables()
1520+
1521+
if self._current_exception is not None:
1522+
result["${EXCEPTION}"] = self._create_variable(
1523+
"${EXCEPTION}",
1524+
self._current_exception,
1525+
VariablePresentationHint(kind="virtual"),
1526+
)
1527+
15051528
if vars is not None:
15061529
p = entry.parent() if entry.parent else None
15071530

@@ -1880,7 +1903,7 @@ def set_exception_breakpoints(
18801903
if option.filter_id in [
18811904
"failed_keyword",
18821905
"uncaught_failed_keyword",
1883-
"failed_test",
1906+
"",
18841907
"failed_suite",
18851908
]:
18861909
entry = ExceptionBreakpointsEntry(
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from .dap_types import Capabilities, ExceptionBreakpointsFilter
2+
3+
DFEAULT_CAPABILITIES = Capabilities(
4+
supports_configuration_done_request=True,
5+
supports_conditional_breakpoints=True,
6+
supports_hit_conditional_breakpoints=True,
7+
support_terminate_debuggee=True,
8+
supports_evaluate_for_hovers=True,
9+
supports_terminate_request=True,
10+
supports_log_points=True,
11+
supports_set_expression=True,
12+
supports_set_variable=True,
13+
supports_value_formatting_options=True,
14+
exception_breakpoint_filters=[
15+
ExceptionBreakpointsFilter(
16+
filter="failed_keyword",
17+
label="Failed Keywords",
18+
description="Breaks on failed keywords",
19+
default=False,
20+
supports_condition=True,
21+
),
22+
ExceptionBreakpointsFilter(
23+
filter="uncaught_failed_keyword",
24+
label="Uncaught Failed Keywords",
25+
description="Breaks on uncaught failed keywords",
26+
default=True,
27+
supports_condition=True,
28+
),
29+
ExceptionBreakpointsFilter(
30+
filter="failed_test",
31+
label="Failed Test",
32+
description="Breaks on failed tests",
33+
default=False,
34+
supports_condition=True,
35+
),
36+
ExceptionBreakpointsFilter(
37+
filter="failed_suite",
38+
label="Failed Suite",
39+
description="Breaks on failed suite",
40+
default=False,
41+
supports_condition=True,
42+
),
43+
],
44+
supports_exception_options=True,
45+
supports_exception_filter_options=True,
46+
supports_completions_request=True,
47+
supports_a_n_s_i_styling=True,
48+
)

packages/debugger/src/robotcode/debugger/launcher/server.py

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
DisconnectArguments,
2424
DisconnectRequest,
2525
Event,
26-
ExceptionBreakpointsFilter,
2726
InitializeRequest,
2827
InitializeRequestArguments,
2928
LaunchRequestArguments,
@@ -39,6 +38,7 @@
3938
TerminatedEvent,
4039
TerminateRequest,
4140
)
41+
from ..default_capabilities import DFEAULT_CAPABILITIES
4242
from ..protocol import DebugAdapterProtocol
4343
from .client import DAPClient, DAPClientError
4444

@@ -91,49 +91,7 @@ async def _initialize(self, arguments: InitializeRequestArguments, *args: Any, *
9191

9292
self._initialized = True
9393

94-
return Capabilities(
95-
supports_configuration_done_request=True,
96-
supports_conditional_breakpoints=True,
97-
supports_hit_conditional_breakpoints=True,
98-
support_terminate_debuggee=True,
99-
# support_suspend_debuggee=True,
100-
supports_evaluate_for_hovers=True,
101-
supports_terminate_request=True,
102-
supports_log_points=True,
103-
supports_set_expression=True,
104-
supports_set_variable=True,
105-
supports_value_formatting_options=True,
106-
exception_breakpoint_filters=[
107-
ExceptionBreakpointsFilter(
108-
filter="failed_keyword",
109-
label="Failed Keywords",
110-
description="Breaks on failed keywords",
111-
default=False,
112-
),
113-
ExceptionBreakpointsFilter(
114-
filter="uncaught_failed_keyword",
115-
label="Uncaught Failed Keywords",
116-
description="Breaks on uncaught failed keywords",
117-
default=True,
118-
),
119-
ExceptionBreakpointsFilter(
120-
filter="failed_test",
121-
label="Failed Test",
122-
description="Breaks on failed tests",
123-
default=False,
124-
),
125-
ExceptionBreakpointsFilter(
126-
filter="failed_suite",
127-
label="Failed Suite",
128-
description="Breaks on failed suite",
129-
default=False,
130-
),
131-
],
132-
supports_exception_options=True,
133-
supports_exception_filter_options=True,
134-
supports_completions_request=True,
135-
supports_a_n_s_i_styling=True,
136-
)
94+
return DFEAULT_CAPABILITIES
13795

13896
@rpc_method(name="launch", param_type=LaunchRequestArguments)
13997
async def _launch(

packages/debugger/src/robotcode/debugger/server.py

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
EvaluateArguments,
2424
EvaluateResponseBody,
2525
Event,
26-
ExceptionBreakpointsFilter,
2726
ExitedEvent,
2827
ExitedEventBody,
2928
InitializedEvent,
@@ -50,6 +49,7 @@
5049
VariablesResponseBody,
5150
)
5251
from .debugger import Debugger, PathMapping
52+
from .default_capabilities import DFEAULT_CAPABILITIES
5353
from .protocol import DebugAdapterProtocol
5454

5555
TCP_DEFAULT_PORT = 6612
@@ -144,49 +144,7 @@ async def _initialize(self, arguments: InitializeRequestArguments, *args: Any, *
144144
if self.loop is not None:
145145
self.loop.call_soon(self.initialized)
146146

147-
return Capabilities(
148-
supports_configuration_done_request=True,
149-
supports_conditional_breakpoints=True,
150-
supports_hit_conditional_breakpoints=True,
151-
support_terminate_debuggee=True,
152-
# support_suspend_debuggee=True,
153-
supports_evaluate_for_hovers=True,
154-
supports_terminate_request=True,
155-
supports_log_points=True,
156-
supports_set_expression=True,
157-
supports_set_variable=True,
158-
supports_value_formatting_options=True,
159-
exception_breakpoint_filters=[
160-
ExceptionBreakpointsFilter(
161-
filter="failed_keyword",
162-
label="Failed Keywords",
163-
description="Breaks on failed keywords",
164-
default=False,
165-
),
166-
ExceptionBreakpointsFilter(
167-
filter="uncaught_failed_keyword",
168-
label="Uncaught Failed Keywords",
169-
description="Breaks on uncaught failed keywords",
170-
default=True,
171-
),
172-
ExceptionBreakpointsFilter(
173-
filter="failed_test",
174-
label="Failed Test",
175-
description="Breaks on failed tests",
176-
default=False,
177-
),
178-
ExceptionBreakpointsFilter(
179-
filter="failed_suite",
180-
label="Failed Suite",
181-
description="Breaks on failed suite",
182-
default=False,
183-
),
184-
],
185-
supports_exception_options=True,
186-
supports_exception_filter_options=True,
187-
supports_completions_request=True,
188-
supports_a_n_s_i_styling=True,
189-
)
147+
return DFEAULT_CAPABILITIES
190148

191149
@rpc_method(name="attach", param_type=AttachRequestArguments)
192150
async def _attach(

0 commit comments

Comments
 (0)