robotcode-debugger 2.6.2__py3-none-any.whl → 2.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- __version__ = "2.6.2"
1
+ __version__ = "2.8.0"
robotcode/debugger/cli.py CHANGED
@@ -9,6 +9,7 @@ from robotcode.plugin.click_helper.options import (
9
9
  server_options,
10
10
  )
11
11
  from robotcode.plugin.click_helper.types import AddressesPort, add_options
12
+ from robotcode.plugin.click_helper.wrappable import wrappable
12
13
 
13
14
  from .__version__ import __version__
14
15
 
@@ -16,6 +17,7 @@ DEBUGGER_DEFAULT_PORT = 6612
16
17
  DEBUGPY_DEFAULT_PORT = 5678
17
18
 
18
19
 
20
+ @wrappable
19
21
  @click.command(
20
22
  context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
21
23
  add_help_option=True,
@@ -143,8 +145,14 @@ def debug(
143
145
  robot_options_and_args: Tuple[str, ...],
144
146
  ) -> None:
145
147
  """Starts a Robot Framework debug session and waits for incomming connections."""
148
+ from robotcode.runner.cli.robot import load_robot_profile
149
+
146
150
  from .run import run_debugger
147
151
 
152
+ # Before the debug server starts, so that it runs under the `wrapper` too; an error
153
+ # of the configuration shows when `robot` runs in the debug session.
154
+ load_robot_profile(app, robot_options_and_args)
155
+
148
156
  mode, port, bind, pipe_name = resolve_server_options(
149
157
  ctx,
150
158
  app,
@@ -202,5 +210,9 @@ def debug(
202
210
  raise
203
211
  except KeyboardInterrupt:
204
212
  app.keyboard_interrupt()
213
+ except click.ClickException:
214
+ # Errors robotcode reports itself, such as a broken configuration; the root
215
+ # group gives them Robot Framework's exit code.
216
+ raise
205
217
  except Exception as e:
206
218
  raise UnknownError(str(e)) from e
@@ -1119,6 +1119,24 @@ class SetExceptionBreakpointsResponse(Response):
1119
1119
  body: Optional[SetExceptionBreakpointsResponseBody] = None
1120
1120
 
1121
1121
 
1122
+ @dataclass
1123
+ class ExceptionInfoArguments(Model):
1124
+ thread_id: int
1125
+
1126
+
1127
+ @dataclass
1128
+ class ExceptionDetails(Model):
1129
+ stack_trace: Optional[str] = None
1130
+
1131
+
1132
+ @dataclass
1133
+ class ExceptionInfoResponseBody(Model):
1134
+ exception_id: str
1135
+ break_mode: ExceptionBreakMode
1136
+ description: Optional[str] = None
1137
+ details: Optional[ExceptionDetails] = None
1138
+
1139
+
1122
1140
  @dataclass
1123
1141
  class _CompletionsRequest:
1124
1142
  arguments: CompletionsArguments
@@ -1,4 +1,5 @@
1
1
  import itertools
2
+ import operator
2
3
  import os
3
4
  import pathlib
4
5
  import re
@@ -35,6 +36,7 @@ from typing import (
35
36
  from robot.api.parsing import get_model
36
37
  from robot.errors import VariableError
37
38
  from robot.output import LOGGER
39
+ from robot.result import Keyword as ResultKeyword
38
40
  from robot.running import EXECUTION_CONTEXTS, Keyword, TestCase, TestSuite
39
41
  from robot.running.model import Try
40
42
  from robot.utils import Matcher as RobotMatcher
@@ -52,7 +54,10 @@ from .dap_types import (
52
54
  ContinuedEventBody,
53
55
  EvaluateArgumentContext,
54
56
  Event,
57
+ ExceptionBreakMode,
58
+ ExceptionDetails,
55
59
  ExceptionFilterOptions,
60
+ ExceptionInfoResponseBody,
56
61
  ExceptionOptions,
57
62
  OutputCategory,
58
63
  OutputEvent,
@@ -291,6 +296,30 @@ class HitCountEntry(NamedTuple):
291
296
  type: str
292
297
 
293
298
 
299
+ # A breakpoint's hit count is read as in debugpy: `N` or `== N` stops at the N-th hit, `>`, `>=`,
300
+ # `<` and `<=` compare the hits with N, and `% N` stops at every N-th hit.
301
+ _HIT_CONDITION_PATTERN = re.compile(r"^\s*(==|>=|<=|>|<|%)?\s*(\d+)\s*$")
302
+ _HIT_CONDITION_OPERATORS: Dict[str, Callable[[int, int], bool]] = {
303
+ "==": operator.eq,
304
+ ">": operator.gt,
305
+ ">=": operator.ge,
306
+ "<": operator.lt,
307
+ "<=": operator.le,
308
+ "%": lambda hits, value: hits % value == 0,
309
+ }
310
+
311
+
312
+ def hit_condition_met(hit_condition: str, hits: int) -> bool:
313
+ """Whether a breakpoint with `hit_condition` stops at its `hits`-th hit.
314
+
315
+ Raises `ValueError` for a hit condition that has none of the forms above.
316
+ """
317
+ match = _HIT_CONDITION_PATTERN.match(hit_condition)
318
+ if match is None or (match.group(1) == "%" and int(match.group(2)) == 0):
319
+ raise ValueError(f"'{hit_condition}' is not a valid hit count; use N, == N, > N, >= N, < N, <= N or % N.")
320
+ return _HIT_CONDITION_OPERATORS[match.group(1) or "=="](hits, int(match.group(2)))
321
+
322
+
294
323
  class PathMapping(NamedTuple):
295
324
  local_root: Optional[str]
296
325
  remote_root: Optional[str]
@@ -299,6 +328,21 @@ class PathMapping(NamedTuple):
299
328
  class DebugLoggerBase:
300
329
  def __init__(self) -> None:
301
330
  self.steps: List[Any] = []
331
+ self.last_fail_message: Optional[str] = None
332
+ self.last_fail_traceback: Optional[str] = None
333
+
334
+ def log_message(self, message: Any) -> None:
335
+ # Robot Framework logs a failure's Python traceback at DEBUG right after its message; unlike a listener, a
336
+ # registered logger gets it whatever the log level
337
+ if message.level == "FAIL":
338
+ self.last_fail_message = message.message
339
+ self.last_fail_traceback = None
340
+ elif (
341
+ message.level == "DEBUG"
342
+ and message.message.startswith("Traceback (most recent call last):")
343
+ and message.message.splitlines()[1:2] != [" None"]
344
+ ):
345
+ self.last_fail_traceback = message.message
302
346
 
303
347
 
304
348
  if RF_VERSION < (7, 0):
@@ -437,6 +481,7 @@ class Debugger:
437
481
  self._variables_cache: Dict[int, Any] = {}
438
482
  self._variables_object_cache: List[Any] = []
439
483
  self._current_exception: Optional[ExceptionInformation] = None
484
+ self._current_exception_info: Optional[ExceptionInfoResponseBody] = None
440
485
 
441
486
  @property
442
487
  def state(self) -> State:
@@ -658,7 +703,17 @@ class Debugger:
658
703
  if self.state == State.Stopped:
659
704
  return
660
705
 
661
- if self.requested_state == RequestedState.Pause:
706
+ if self.terminated:
707
+ # after a terminate request the run goes on to its end, so pending pause and step requests are dropped
708
+ if self.requested_state in (
709
+ RequestedState.Pause,
710
+ RequestedState.Next,
711
+ RequestedState.StepIn,
712
+ RequestedState.StepOut,
713
+ ):
714
+ self.requested_state = RequestedState.Nothing
715
+
716
+ elif self.requested_state == RequestedState.Pause:
662
717
  self.requested_state = RequestedState.Nothing
663
718
  self.state = State.Paused
664
719
 
@@ -723,6 +778,9 @@ class Debugger:
723
778
  source_path = self.map_path_to_client(str(Path(source).absolute()))
724
779
  if source_path in self.breakpoints:
725
780
  breakpoints = [v for v in self.breakpoints[source_path].breakpoints if v.line == line_no]
781
+ if self.terminated:
782
+ # after a terminate request only breakpoints that write a log message, and never pause, are left
783
+ breakpoints = [v for v in breakpoints if v.log_message]
726
784
  if len(breakpoints) > 0:
727
785
  for point in breakpoints:
728
786
  if point.condition is not None:
@@ -737,23 +795,44 @@ class Debugger:
737
795
  )
738
796
  except (SystemExit, KeyboardInterrupt):
739
797
  raise
740
- except BaseException:
798
+ except BaseException as e:
741
799
  hit = False
800
+ error = str(e) or e.__class__.__name__
801
+ self.send_event(
802
+ self,
803
+ OutputEvent(
804
+ body=OutputEventBody(
805
+ output=f"Breakpoint condition error: {error}{os.linesep}",
806
+ category=OutputCategory.STDERR,
807
+ source=Source(path=str(source_path)),
808
+ line=line_no,
809
+ )
810
+ ),
811
+ )
742
812
 
743
813
  if not hit:
744
814
  return
745
- if point.hit_condition is not None:
815
+ if point.hit_condition:
746
816
  hit = False
747
817
  entry = HitCountEntry(source_path, line_no, type)
748
818
  if entry not in self.hit_counts:
749
819
  self.hit_counts[entry] = 0
750
820
  self.hit_counts[entry] += 1
751
821
  try:
752
- hit = self.hit_counts[entry] != int(point.hit_condition)
753
- except (SystemExit, KeyboardInterrupt):
754
- raise
755
- except BaseException:
822
+ hit = hit_condition_met(point.hit_condition, self.hit_counts[entry])
823
+ except ValueError as e:
756
824
  hit = False
825
+ self.send_event(
826
+ self,
827
+ OutputEvent(
828
+ body=OutputEventBody(
829
+ output=f"Breakpoint hit count error: {e}{os.linesep}",
830
+ category=OutputCategory.STDERR,
831
+ source=Source(path=str(source_path)),
832
+ line=line_no,
833
+ )
834
+ ),
835
+ )
757
836
  if not hit:
758
837
  return
759
838
  if point.log_message:
@@ -798,27 +877,39 @@ class Debugger:
798
877
  filter_id: Set[str],
799
878
  description: str,
800
879
  text: Optional[str],
880
+ name: str,
881
+ message: Optional[str],
801
882
  ) -> None:
802
883
  if self.state == State.CallKeyword:
803
884
  return
804
885
  if self.state == State.Stopped:
805
886
  return
806
887
 
807
- if (
808
- not self.terminated
809
- and status == "FAIL"
810
- and any(
811
- v
812
- for v in self.exception_breakpoints
813
- if v.filter_options and any(o for o in v.filter_options if o.filter_id in filter_id)
814
- )
815
- ):
888
+ matched_filter_ids = {
889
+ o.filter_id
890
+ for v in self.exception_breakpoints
891
+ if v.filter_options
892
+ for o in v.filter_options
893
+ if o.filter_id in filter_id
894
+ }
895
+
896
+ if not self.terminated and status == "FAIL" and matched_filter_ids:
816
897
  reason = StoppedReason.EXCEPTION
817
898
 
818
899
  self.requested_state = RequestedState.Nothing
819
900
  self.state = State.Paused
820
901
 
821
902
  self._current_exception = {"text": text, "description": description, "status": status}
903
+ self._current_exception_info = ExceptionInfoResponseBody(
904
+ exception_id=name,
905
+ break_mode=(
906
+ ExceptionBreakMode.UNHANDLED
907
+ if "uncaught_failed_keyword" in matched_filter_ids
908
+ else ExceptionBreakMode.ALWAYS
909
+ ),
910
+ description=message,
911
+ details=self._create_exception_details(message),
912
+ )
822
913
 
823
914
  self.send_event(
824
915
  self,
@@ -840,8 +931,16 @@ class Debugger:
840
931
  lambda: (
841
932
  self.state in [State.Running, State.Stopped, State.CallKeyword]
842
933
  or self.requested_state != RequestedState.Nothing
934
+ or self.terminated
843
935
  )
844
936
  )
937
+ # a pause that began while a terminate request arrived ends at once, the run goes on to its end
938
+ if (
939
+ self.terminated
940
+ and self.state == State.Paused
941
+ and self.requested_state == RequestedState.Nothing
942
+ ):
943
+ self.requested_state = RequestedState.Running
845
944
 
846
945
  if self.state == State.CallKeyword:
847
946
  self._evaluated_keyword_result = None
@@ -876,6 +975,7 @@ class Debugger:
876
975
 
877
976
  break
878
977
  self._current_exception = None
978
+ self._current_exception_info = None
879
979
 
880
980
  def start_output_group(self, name: str, attributes: AttributeDict, type: Optional[str] = None) -> None:
881
981
  if self.group_output:
@@ -887,7 +987,7 @@ class Debugger:
887
987
  OutputEvent(
888
988
  body=OutputEventBody(
889
989
  output=f"\u001b[38;5;14m{(type + ' ') if type else ''}\u001b[0m{name}\n",
890
- category="OutputCategory.CONSOLE",
990
+ category=OutputCategory.CONSOLE,
891
991
  group=OutputGroup.START,
892
992
  source=Source(path=str(self.map_path_to_client(source))) if source else None,
893
993
  line=line_no if source is not None else None,
@@ -1018,7 +1118,7 @@ class Debugger:
1018
1118
  entry = self.add_stackframe_entry(name, type, source, line_no, longname=longname)
1019
1119
 
1020
1120
  if self.debug:
1021
- if self.stop_on_entry:
1121
+ if self.stop_on_entry and not self.terminated:
1022
1122
  self.stop_on_entry = False
1023
1123
 
1024
1124
  self.requested_state = RequestedState.Nothing
@@ -1057,6 +1157,8 @@ class Debugger:
1057
1157
  {"failed_suite"},
1058
1158
  "Suite failed.",
1059
1159
  f"Suite failed{f': {v}' if (v := attributes.get('message')) else ''}",
1160
+ name,
1161
+ attributes.get("message"),
1060
1162
  )
1061
1163
  self.wait_for_running()
1062
1164
 
@@ -1103,9 +1205,11 @@ class Debugger:
1103
1205
  if status == "FAIL":
1104
1206
  self.process_end_state(
1105
1207
  status,
1106
- {""},
1208
+ {"failed_test"},
1107
1209
  "Test failed.",
1108
1210
  f"Test failed{f': {v}' if (v := attributes.get('message')) else ''}",
1211
+ name,
1212
+ attributes.get("message"),
1109
1213
  )
1110
1214
 
1111
1215
  self.wait_for_running()
@@ -1300,6 +1404,8 @@ class Debugger:
1300
1404
  },
1301
1405
  "Keyword failed.",
1302
1406
  f"Keyword failed: {self.last_fail_message}" if self.last_fail_message else "Keyword failed.",
1407
+ name,
1408
+ self.last_fail_message,
1303
1409
  )
1304
1410
 
1305
1411
  self.wait_for_running()
@@ -1757,7 +1863,14 @@ class Debugger:
1757
1863
  if RF_VERSION >= (7, 0):
1758
1864
 
1759
1865
  def _run_keyword(self, kw: Keyword, context: Any) -> Any:
1760
- return kw.run(context.steps[-1][1], context)
1866
+ # An evaluated keyword becomes a child of the running step. At a stop
1867
+ # where no step runs, such as the end of a test or suite, it goes to the
1868
+ # test, and without a test to a detached result.
1869
+ if context.steps:
1870
+ parent = context.steps[-1][1]
1871
+ else:
1872
+ parent = context.test if context.test is not None else ResultKeyword()
1873
+ return kw.run(parent, context)
1761
1874
 
1762
1875
  else:
1763
1876
 
@@ -2062,7 +2175,7 @@ class Debugger:
2062
2175
 
2063
2176
  self._after_evaluate_keyword_event.set()
2064
2177
 
2065
- return result
2178
+ return result
2066
2179
 
2067
2180
  def _create_set_variable_result(self, value: Any) -> SetVariableResult:
2068
2181
  if isinstance(value, Mapping):
@@ -2135,7 +2248,7 @@ class Debugger:
2135
2248
  if option.filter_id in [
2136
2249
  "failed_keyword",
2137
2250
  "uncaught_failed_keyword",
2138
- "",
2251
+ "failed_test",
2139
2252
  "failed_suite",
2140
2253
  ]:
2141
2254
  entry = ExceptionBreakpointsEntry(
@@ -2151,6 +2264,38 @@ class Debugger:
2151
2264
 
2152
2265
  return result or None
2153
2266
 
2267
+ def _create_exception_details(self, message: Optional[str]) -> Optional[ExceptionDetails]:
2268
+ rows: List[Tuple[str, Optional[str]]] = []
2269
+ # outermost first, as Python lists calls, so that a traceback below continues the list into the library
2270
+ for entry in reversed(self.stack_frames):
2271
+ if entry.type == "SUITE":
2272
+ continue
2273
+ position = entry.stack_frames[0] if entry.stack_frames else None if entry.top_hidden else entry
2274
+ if position is None:
2275
+ continue
2276
+ source = self.source_from_entry(position)
2277
+ location = None
2278
+ if source is not None and source.path:
2279
+ location = source.path if position.line is None else f"{source.path}:{position.line}"
2280
+ rows.append((entry.longname or entry.kwname or entry.name or entry.type, location))
2281
+
2282
+ width = max((len(name) for name, _ in rows), default=0)
2283
+ stack_trace = "\n".join(f"{name.ljust(width)} {location}" if location else name for name, location in rows)
2284
+
2285
+ logger = self.debug_logger
2286
+ if message and logger is not None and logger.last_fail_message == message and logger.last_fail_traceback:
2287
+ stack_trace = "\n\n".join(part for part in (stack_trace, logger.last_fail_traceback) if part)
2288
+
2289
+ return ExceptionDetails(stack_trace=stack_trace) if stack_trace else None
2290
+
2291
+ def get_exception_info(self, thread_id: int) -> ExceptionInfoResponseBody:
2292
+ self.check_thread_id(thread_id)
2293
+
2294
+ if self._current_exception_info is None:
2295
+ raise RuntimeError("The run is not stopped at a failure.")
2296
+
2297
+ return self._current_exception_info
2298
+
2154
2299
  if RF_VERSION >= (7, 0):
2155
2300
 
2156
2301
  def _get_keywords_from_lib(self, lib: Any) -> Any:
@@ -28,21 +28,22 @@ DFEAULT_CAPABILITIES = Capabilities(
28
28
  ),
29
29
  ExceptionBreakpointsFilter(
30
30
  filter="failed_test",
31
- label="Failed Test",
31
+ label="Failed Tests",
32
32
  description="Breaks on failed tests",
33
33
  default=False,
34
34
  supports_condition=True,
35
35
  ),
36
36
  ExceptionBreakpointsFilter(
37
37
  filter="failed_suite",
38
- label="Failed Suite",
39
- description="Breaks on failed suite",
38
+ label="Failed Suites",
39
+ description="Breaks on failed suites",
40
40
  default=False,
41
41
  supports_condition=True,
42
42
  ),
43
43
  ],
44
44
  supports_exception_options=True,
45
45
  supports_exception_filter_options=True,
46
+ supports_exception_info_request=True,
46
47
  supports_completions_request=True,
47
48
  supports_a_n_s_i_styling=True,
48
49
  )
@@ -3,9 +3,10 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import itertools
5
5
  import os
6
+ import shlex
6
7
  import sys
7
8
  from pathlib import Path
8
- from typing import Any, Dict, List, Literal, Optional
9
+ from typing import Any, Dict, List, Literal, Mapping, Optional
9
10
 
10
11
  from robotcode.core.types import ServerMode, TcpParams
11
12
  from robotcode.core.utils.logging import LoggingDescriptor
@@ -43,6 +44,56 @@ from ..protocol import DebugAdapterProtocol
43
44
  from .client import DAPClient, DAPClientError
44
45
 
45
46
 
47
+ def _build_robotcode_run_args(
48
+ python: str,
49
+ debugger_script: List[str],
50
+ profiles: Optional[List[str]],
51
+ paths: Optional[List[str]],
52
+ wrapper: Optional[List[str]],
53
+ robot_code_args: Optional[List[str]],
54
+ ) -> List[str]:
55
+ """Build the ``robotcode … debug`` command line.
56
+
57
+ A ``wrapper`` from the launch request (VS Code ``robotcode.debug.launchWrapper``)
58
+ is forwarded to robotcode as ``--wrapper``; robotcode applies it itself (and
59
+ ``--wrapper`` overrides the profile's ``wrapper``). Without one nothing is added
60
+ and robotcode resolves its profile ``wrapper`` alone.
61
+ """
62
+ return [
63
+ python,
64
+ *debugger_script,
65
+ *itertools.chain.from_iterable(["-p", p] for p in profiles or []),
66
+ *itertools.chain.from_iterable(["-dp", p] for p in paths or []),
67
+ *(["--wrapper", shlex.join(wrapper)] if wrapper else []),
68
+ *(robot_code_args or []),
69
+ "debug",
70
+ ]
71
+
72
+
73
+ Console = Literal["robotcodeTerminal", "internalConsole", "integratedTerminal", "externalTerminal"]
74
+
75
+
76
+ def _console_for_client(console: Optional[Console], client_id: Optional[str]) -> Optional[Console]:
77
+ """The console a run uses: RobotCode's own run terminal is part of its VS Code extension, so any other client
78
+ gets the integrated terminal instead."""
79
+ if console == "robotcodeTerminal" and client_id != "vscode":
80
+ return "integratedTerminal"
81
+ return console
82
+
83
+
84
+ def _merged_environment(
85
+ base: Mapping[str, str], env: Mapping[str, Optional[str]], windows: bool
86
+ ) -> Dict[str, Optional[str]]:
87
+ """The environment of a run: ``base`` with the variables of ``env``. On Windows, where a name means the same
88
+ variable in any case, a variable of ``env`` replaces the one of ``base`` whose name differs only in case, and of
89
+ two names in ``env`` that differ only in case, the last one stays."""
90
+ if windows:
91
+ last = {name.upper(): name for name in env}
92
+ env = {name: value for name, value in env.items() if last[name.upper()] == name}
93
+ base = {name: value for name, value in base.items() if name.upper() not in last}
94
+ return {**base, **env}
95
+
96
+
46
97
  class OutputProtocol(asyncio.SubprocessProtocol):
47
98
  def __init__(self, parent: LauncherDebugAdapterProtocol) -> None:
48
99
  super().__init__()
@@ -109,7 +160,7 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
109
160
  paths: Optional[List[str]] = None,
110
161
  args: Optional[List[str]] = None,
111
162
  env: Optional[Dict[str, Optional[Any]]] = None,
112
- console: Optional[Literal["internalConsole", "integratedTerminal", "externalTerminal"]] = "integratedTerminal",
163
+ console: Optional[Console] = "integratedTerminal",
113
164
  name: Optional[str] = None,
114
165
  no_debug: Optional[bool] = None,
115
166
  robotPythonPath: Optional[List[str]] = None, # noqa: N803
@@ -133,6 +184,7 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
133
184
  include: Optional[List[str]] = None,
134
185
  exclude: Optional[List[str]] = None,
135
186
  robotCodeArgs: Optional[List[str]] = None, # noqa: N803
187
+ wrapper: Optional[List[str]] = None,
136
188
  arguments: Optional[LaunchRequestArguments] = None,
137
189
  *_args: Any,
138
190
  **_kwargs: Any,
@@ -145,14 +197,9 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
145
197
 
146
198
  debugger_script = ["-m", "robotcode.cli"] if self.debugger_script is None else [str(Path(self.debugger_script))]
147
199
 
148
- robotcode_run_args = [
149
- python or sys.executable,
150
- *debugger_script,
151
- *itertools.chain.from_iterable(["-p", p] for p in profiles or []),
152
- *itertools.chain.from_iterable(["-dp", p] for p in paths or []),
153
- *(robotCodeArgs or []),
154
- "debug",
155
- ]
200
+ robotcode_run_args = _build_robotcode_run_args(
201
+ python or sys.executable, debugger_script, profiles, paths, wrapper, robotCodeArgs
202
+ )
156
203
 
157
204
  if no_debug:
158
205
  robotcode_run_args += ["--no-debug"]
@@ -240,7 +287,19 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
240
287
 
241
288
  env = {k: ("" if v is None else str(v)) for k, v in env.items()} if env else {}
242
289
 
243
- if console in ["integratedTerminal", "externalTerminal"]:
290
+ console = _console_for_client(
291
+ console, self._initialize_arguments.client_id if self._initialize_arguments is not None else None
292
+ )
293
+
294
+ if console == "robotcodeTerminal":
295
+ # RobotCode's VS Code extension runs it as the process of a terminal of its own, without a shell
296
+ await self.send_event_async(
297
+ Event(
298
+ event="runInRobotCodeTerminal",
299
+ body={"cwd": cwd, "args": [*robotcode_run_args, *run_args], "env": env, "title": name},
300
+ )
301
+ )
302
+ elif console in ["integratedTerminal", "externalTerminal"]:
244
303
  await self.send_request_async(
245
304
  RunInTerminalRequest(
246
305
  arguments=RunInTerminalRequestArguments(
@@ -261,8 +320,7 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
261
320
  )
262
321
  # time.sleep(1) # Give some time for the terminal to start
263
322
  elif console is None or console == "internalConsole":
264
- run_env: Dict[str, Optional[str]] = dict(os.environ)
265
- run_env.update(env)
323
+ run_env = _merged_environment(os.environ, env, sys.platform == "win32")
266
324
 
267
325
  await asyncio.get_event_loop().subprocess_exec(
268
326
  lambda: OutputProtocol(self), *robotcode_run_args, *run_args, cwd=cwd, env=run_env
@@ -1,7 +1,7 @@
1
1
  import re
2
2
  from dataclasses import dataclass
3
3
  from pathlib import Path
4
- from typing import Any, Dict, Iterator, List, Optional, Union, cast
4
+ from typing import Any, Dict, Iterator, List, Optional, Set, Union, cast
5
5
 
6
6
  from robot import result, running
7
7
  from robot.model import Message
@@ -13,6 +13,14 @@ from .debugger import Debugger, LogMessage
13
13
  from .mixins import SyncedEventBody
14
14
 
15
15
 
16
+ @dataclass
17
+ class RobotFailure(Model):
18
+ # indices into failed_keywords: the failed keyword whose message and caught mark are the failure's, and the failed
19
+ # keywords from the innermost call out to the test
20
+ keyword: int
21
+ calls: List[int]
22
+
23
+
16
24
  @dataclass
17
25
  class RobotExecutionEventBody(Model, SyncedEventBody):
18
26
  type: str
@@ -21,6 +29,8 @@ class RobotExecutionEventBody(Model, SyncedEventBody):
21
29
  parent_id: Optional[str] = None
22
30
  attributes: Optional[Dict[str, Any]] = None
23
31
  failed_keywords: Optional[List[Dict[str, Any]]] = None
32
+ # in the order their keywords ended
33
+ failures: Optional[List[RobotFailure]] = None
24
34
  source: Optional[str] = None
25
35
  lineno: Optional[int] = None
26
36
  synced: bool = True
@@ -56,11 +66,31 @@ def source_from_attributes(attributes: Dict[str, Any]) -> str:
56
66
  return s or ""
57
67
 
58
68
 
69
+ def without_empty_lineno(attributes: Dict[str, Any]) -> Dict[str, Any]:
70
+ """A copy of listener attributes with an empty line number as None. Up to Robot Framework 6.1, listeners get ""
71
+ for a test or keyword without one, as a custom parser or a pre-run modifier creates them."""
72
+ result = dict(attributes)
73
+ if result.get("lineno") == "":
74
+ result["lineno"] = None
75
+ return result
76
+
77
+
59
78
  class ListenerV2:
60
79
  ROBOT_LISTENER_API_VERSION = "2"
61
80
 
62
81
  def __init__(self) -> None:
63
82
  self.failed_keywords: Optional[List[Dict[str, Any]]] = None
83
+ # the number of failed keywords when each running keyword or control structure started; per depth, the failed
84
+ # keywords of the last TRY branch there, and those of them that its EXCEPT branch caught
85
+ self.failed_keyword_marks: List[int] = []
86
+ # by the id of a failed keyword, the nearest failed keyword it ran in
87
+ self.failed_keyword_parents: Dict[int, Dict[str, Any]] = {}
88
+ # per running keyword or control structure, whether it logged a failure itself, as Robot Framework does for the
89
+ # keyword whose error it is; and the ids of the failed keywords that did
90
+ self.failure_logged: List[bool] = []
91
+ self.failed_keywords_with_own_failure: Set[int] = set()
92
+ self.try_failures: Dict[int, List[Dict[str, Any]]] = {}
93
+ self.caught_by_except: Dict[int, List[Dict[str, Any]]] = {}
64
94
  self.last_fail_message: Optional[str] = None
65
95
  self.suite_id_stack: List[str] = []
66
96
 
@@ -105,16 +135,17 @@ class ListenerV2:
105
135
  id=id,
106
136
  parent_id=self.suite_id_stack[-1] if self.suite_id_stack else None,
107
137
  failed_keywords=self.failed_keywords,
138
+ failures=self._failures(),
108
139
  source=source_from_attributes(attributes) or None,
109
140
  lineno=attributes.get("lineno", None),
110
141
  ),
111
142
  ),
112
143
  )
113
144
  self.suite_id_stack.pop()
114
- self.failed_keywords = None
145
+ self._forget_failed_keywords()
115
146
 
116
147
  def start_test(self, name: str, attributes: Dict[str, Any]) -> None:
117
- self.failed_keywords = None
148
+ self._forget_failed_keywords()
118
149
 
119
150
  Debugger.instance.send_event(
120
151
  self,
@@ -124,11 +155,11 @@ class ListenerV2:
124
155
  type="test",
125
156
  name=name,
126
157
  id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')};"
127
- f"{attributes.get('lineno', 0)}",
158
+ f"{attributes.get('lineno') or 0}",
128
159
  parent_id=self.suite_id_stack[-1] if self.suite_id_stack else None,
129
- attributes=dict(attributes),
160
+ attributes=without_empty_lineno(attributes),
130
161
  source=source_from_attributes(attributes) or None,
131
- lineno=attributes.get("lineno", None),
162
+ lineno=attributes.get("lineno") or None,
132
163
  ),
133
164
  ),
134
165
  )
@@ -150,19 +181,89 @@ class ListenerV2:
150
181
  type="test",
151
182
  name=name,
152
183
  id=f"{source_from_attributes(attributes)};{attributes.get('longname', '')};"
153
- f"{attributes.get('lineno', 0)}",
184
+ f"{attributes.get('lineno') or 0}",
154
185
  parent_id=self.suite_id_stack[-1] if self.suite_id_stack else None,
155
- attributes=dict(attributes),
186
+ attributes=without_empty_lineno(attributes),
156
187
  failed_keywords=self.failed_keywords,
188
+ failures=self._failures(),
157
189
  source=source_from_attributes(attributes) or None,
158
- lineno=attributes.get("lineno", None),
190
+ lineno=attributes.get("lineno") or None,
159
191
  ),
160
192
  ),
161
193
  )
162
194
 
195
+ self._forget_failed_keywords()
196
+
197
+ def _forget_failed_keywords(self) -> None:
163
198
  self.failed_keywords = None
199
+ self.failed_keyword_parents = {}
200
+ self.failed_keywords_with_own_failure = set()
201
+
202
+ def _failures(self) -> Optional[List[RobotFailure]]:
203
+ keywords = self.failed_keywords
204
+ if not keywords:
205
+ return None
206
+
207
+ index = {id(keyword): i for i, keyword in enumerate(keywords)}
208
+ parents = self.failed_keyword_parents
209
+
210
+ # the failed keywords inside a keyword are those it is the parent of that are caught, or not caught, like it; a
211
+ # caught one inside a keyword that is not caught was caught within it and stays a failure of its own
212
+ def is_inside(keyword: Dict[str, Any], parent: Optional[Dict[str, Any]]) -> bool:
213
+ return parent is not None and bool(parent.get("caught")) == bool(keyword.get("caught"))
214
+
215
+ inside: Dict[int, List[Dict[str, Any]]] = {}
216
+ for keyword in keywords:
217
+ parent = parents.get(id(keyword))
218
+ if parent is not None and is_inside(keyword, parent):
219
+ inside.setdefault(id(parent), []).append(keyword)
220
+
221
+ # a keyword that logged a failure of its own after failures inside it turned theirs into its own, as Wait Until
222
+ # Keyword Succeeds does after its last attempt; one that passes them on or collects them logs none
223
+ def turns_into_own(keyword: Dict[str, Any]) -> bool:
224
+ return bool(inside.get(id(keyword))) and id(keyword) in self.failed_keywords_with_own_failure
225
+
226
+ def within(keyword: Dict[str, Any]) -> Iterator[Dict[str, Any]]:
227
+ current, parent = keyword, parents.get(id(keyword))
228
+ while parent is not None and is_inside(current, parent):
229
+ yield parent
230
+ current, parent = parent, parents.get(id(parent))
231
+
232
+ def outwards(keyword: Dict[str, Any]) -> List[int]:
233
+ calls = []
234
+ current: Optional[Dict[str, Any]] = keyword
235
+ while current is not None:
236
+ calls.append(index[id(current)])
237
+ current = parents.get(id(current))
238
+ return calls
239
+
240
+ # the calls of the newest failure inside the keyword, up to it
241
+ def last_inside(keyword: Dict[str, Any]) -> List[int]:
242
+ for candidate in keywords:
243
+ if inside.get(id(candidate)):
244
+ continue
245
+ calls = [index[id(candidate)]]
246
+ for outer in within(candidate):
247
+ if outer is keyword:
248
+ return calls
249
+ calls.append(index[id(outer)])
250
+ return []
251
+
252
+ failures = []
253
+ for keyword in reversed(keywords):
254
+ if any(turns_into_own(outer) for outer in within(keyword)):
255
+ continue
256
+ if turns_into_own(keyword):
257
+ failures.append(RobotFailure(index[id(keyword)], last_inside(keyword) + outwards(keyword)))
258
+ elif not inside.get(id(keyword)):
259
+ failures.append(RobotFailure(index[id(keyword)], outwards(keyword)))
260
+
261
+ return failures
164
262
 
165
263
  def start_keyword(self, name: str, attributes: Dict[str, Any]) -> None:
264
+ self.failed_keyword_marks.append(len(self.failed_keywords or []))
265
+ self.failure_logged.append(False)
266
+
166
267
  if attributes["type"] in ["KEYWORD", "SETUP", "TEARDOWN"]:
167
268
  Debugger.instance.start_output_group(
168
269
  f"{name}({', '.join(repr(v) for v in attributes.get('args', []))})",
@@ -191,6 +292,8 @@ class ListenerV2:
191
292
 
192
293
  def end_keyword(self, name: str, attributes: Dict[str, Any]) -> None:
193
294
  Debugger.instance.end_keyword(name, attributes)
295
+ mark = self.failed_keyword_marks.pop() if self.failed_keyword_marks else 0
296
+ failure_logged = self.failure_logged.pop() if self.failure_logged else False
194
297
 
195
298
  # if Debugger.instance.state != State.CallKeyword:
196
299
  # Debugger.instance.send_event(
@@ -216,7 +319,38 @@ class ListenerV2:
216
319
  if self.failed_keywords is None:
217
320
  self.failed_keywords = []
218
321
 
219
- self.failed_keywords.insert(0, {"message": self.last_fail_message, **attributes})
322
+ keyword = {"message": self.last_fail_message, **without_empty_lineno(attributes)}
323
+ # it is the nearest failed keyword around those that ended since it started and have none yet
324
+ for inner in self.failed_keywords[: max(0, len(self.failed_keywords) - mark)]:
325
+ self.failed_keyword_parents.setdefault(id(inner), keyword)
326
+ self.failed_keywords.insert(0, keyword)
327
+ if failure_logged:
328
+ self.failed_keywords_with_own_failure.add(id(keyword))
329
+
330
+ self._mark_caught_failures(attributes, mark, len(self.failed_keyword_marks))
331
+
332
+ def _mark_caught_failures(self, attributes: Dict[str, Any], mark: int, depth: int) -> None:
333
+ # Robot Framework's log expands a failed test only along what failed, so a failure inside a keyword or control
334
+ # structure that did not fail was caught, as by Run Keyword And Ignore Error. Listener version 2 gets the
335
+ # branches of a TRY/EXCEPT but not the TRY/EXCEPT, whose status the log follows: an EXCEPT branch that passed
336
+ # caught the failures of its TRY branch, unless the FINALLY branch fails and with it the TRY/EXCEPT
337
+ kind, status = attributes["type"], attributes["status"]
338
+ failed = self.failed_keywords or []
339
+ inside = failed[: max(0, len(failed) - mark)]
340
+ if status != "FAIL":
341
+ for keyword in inside:
342
+ keyword["caught"] = True
343
+ if kind == "TRY":
344
+ self.try_failures[depth] = inside
345
+ self.caught_by_except[depth] = []
346
+ elif kind == "EXCEPT" and status == "PASS":
347
+ caught = [keyword for keyword in self.try_failures.get(depth, []) if not keyword.get("caught")]
348
+ for keyword in caught:
349
+ keyword["caught"] = True
350
+ self.caught_by_except[depth] = caught
351
+ elif kind == "FINALLY" and status == "FAIL":
352
+ for keyword in self.caught_by_except.pop(depth, []):
353
+ keyword.pop("caught", None)
220
354
 
221
355
  RE_FILE_LINE_MATCHER = re.compile(r".+\sin\sfile\s'(?P<file>.*)'\son\sline\s(?P<line>\d+):(?P<message>.*)")
222
356
 
@@ -227,9 +361,11 @@ class ListenerV2:
227
361
  if message["level"] == "FAIL":
228
362
  self.last_fail_message = message["message"]
229
363
  Debugger.instance.last_fail_message = self.last_fail_message
364
+ if self.failure_logged:
365
+ self.failure_logged[-1] = True
230
366
 
231
367
  source = current_frame.source if current_frame else None
232
- line = current_frame.line if current_frame else None
368
+ line = (current_frame.line or None) if current_frame else None
233
369
  column = current_frame.column if current_frame else None
234
370
 
235
371
  item_id = next(
@@ -238,7 +374,7 @@ class ListenerV2:
238
374
  f"{normalized_path(Path(item.source)) if item.source is not None else ''};{item.longname}"
239
375
  if item.type == "SUITE"
240
376
  else f"{normalized_path(Path(item.source)) if item.source is not None else ''};"
241
- f"{item.longname};{item.line}"
377
+ f"{item.longname};{item.line or 0}"
242
378
  )
243
379
  for item in Debugger.instance.full_stack_frames
244
380
  if item.type in ["SUITE", "TEST"]
@@ -278,7 +414,7 @@ class ListenerV2:
278
414
  current_frame = Debugger.instance.full_stack_frames[0] if Debugger.instance.full_stack_frames else None
279
415
 
280
416
  source = current_frame.source if current_frame else None
281
- line = current_frame.line if current_frame else None
417
+ line = (current_frame.line or None) if current_frame else None
282
418
  column = current_frame.column if current_frame else None
283
419
 
284
420
  item_id = next(
@@ -287,7 +423,7 @@ class ListenerV2:
287
423
  f"{normalized_path(Path(item.source)) if item.source is not None else ''};{item.longname}"
288
424
  if item.type == "SUITE"
289
425
  else f"{normalized_path(Path(item.source)) if item.source is not None else ''};"
290
- f"{item.longname};{item.line}"
426
+ f"{item.longname};{item.line or 0}"
291
427
  )
292
428
  for item in Debugger.instance.full_stack_frames
293
429
  if item.type in ["SUITE", "TEST"]
@@ -371,7 +507,9 @@ class ListenerV3:
371
507
  yield from enqueue(s)
372
508
  return
373
509
 
374
- yield (f"{normalized_path(item.source) if item.source is not None else ''};{item.longname};{item.lineno}")
510
+ yield (
511
+ f"{normalized_path(item.source) if item.source is not None else ''};{item.longname};{item.lineno or 0}"
512
+ )
375
513
 
376
514
  if self._event_sended:
377
515
  return
@@ -404,9 +542,10 @@ class ListenerV3:
404
542
  "lineno": data_item.lineno if data_item is not None else 0,
405
543
  "message": result_item.message,
406
544
  },
407
- id=f"{result_item.source or ''};{result_item.longname or ''}"
545
+ id=f"{normalized_path(Path(result_item.source)) if result_item.source else ''};"
546
+ f"{result_item.longname or ''}"
408
547
  + (
409
- f";{data_item.lineno if data_item is not None else 0}"
548
+ f";{(data_item.lineno if data_item is not None else None) or 0}"
410
549
  if isinstance(result_item, result.TestCase)
411
550
  else ""
412
551
  ),
robotcode/debugger/run.py CHANGED
@@ -5,7 +5,9 @@ import threading
5
5
  import time
6
6
  from concurrent.futures import CancelledError
7
7
  from typing import (
8
+ IO,
8
9
  TYPE_CHECKING,
10
+ Any,
9
11
  Callable,
10
12
  List,
11
13
  Optional,
@@ -25,7 +27,7 @@ from robotcode.core.utils.debugpy import (
25
27
  )
26
28
  from robotcode.core.utils.logging import LoggingDescriptor
27
29
  from robotcode.core.utils.net import find_free_port
28
- from robotcode.plugin import Application
30
+ from robotcode.plugin import Application, UnknownError
29
31
 
30
32
  from .dap_types import Event
31
33
  from .debugger import Debugger
@@ -145,6 +147,40 @@ def start_debugpy(
145
147
  config_done_callback = connect_debugpy
146
148
 
147
149
 
150
+ def _run_robot(ctx: click.Context, app: Application, args: List[str]) -> int:
151
+ from robotcode.runner.cli.robot import robot
152
+
153
+ args = [
154
+ "--listener",
155
+ "robotcode.debugger.listeners.ListenerV3",
156
+ "--listener",
157
+ "robotcode.debugger.listeners.ListenerV2",
158
+ *args,
159
+ ]
160
+
161
+ app.verbose("Start robot")
162
+ try:
163
+ app.verbose(f"Create robot context with args: {args}")
164
+ robot_ctx = robot.make_context("robot", args, parent=ctx)
165
+ robot.invoke(robot_ctx)
166
+ except SystemExit as e:
167
+ return cast(int, e.code)
168
+ return 0
169
+
170
+
171
+ class _ShownError(click.ClickException):
172
+ """An error that `run_debugger` showed before the debug session ended. It keeps the
173
+ error's exit code and shows nothing, so the command shows the error only once."""
174
+
175
+ def __init__(self, error: click.ClickException) -> None:
176
+ super().__init__(error.message)
177
+ # click declares the exit code on the class; this one is the code of the error shown
178
+ self.exit_code = error.exit_code # type: ignore[misc]
179
+
180
+ def show(self, file: Optional[IO[Any]] = None) -> None:
181
+ pass
182
+
183
+
148
184
  @_logger.call
149
185
  def run_debugger(
150
186
  ctx: click.Context,
@@ -167,6 +203,11 @@ def run_debugger(
167
203
  output_timestamps: bool = False,
168
204
  group_output: bool = False,
169
205
  ) -> int:
206
+ if app.config.dry:
207
+ # A dry run starts neither debugpy nor the debug adapter and waits for no
208
+ # client; the robot command only prints what it would execute.
209
+ return _run_robot(ctx, app, args)
210
+
170
211
  if debug and debugpy and not is_debugpy_installed():
171
212
  app.warning("Debugpy not installed")
172
213
 
@@ -215,14 +256,6 @@ def run_debugger(
215
256
  if debugpy and debugpy_wait_for_client:
216
257
  debugpy_connected.wait(wait_for_client_timeout)
217
258
 
218
- args = [
219
- "--listener",
220
- "robotcode.debugger.listeners.ListenerV3",
221
- "--listener",
222
- "robotcode.debugger.listeners.ListenerV2",
223
- *args,
224
- ]
225
-
226
259
  Debugger.instance.stop_on_entry = stop_on_entry
227
260
  Debugger.instance.output_messages = output_messages
228
261
  Debugger.instance.output_log = output_log
@@ -237,15 +270,17 @@ def run_debugger(
237
270
 
238
271
  exit_code = 0
239
272
  try:
240
- from robotcode.runner.cli.robot import robot
241
-
242
- app.verbose("Start robot")
243
- try:
244
- app.verbose(f"Create robot context with args: {args}")
245
- robot_ctx = robot.make_context("robot", args, parent=ctx)
246
- robot.invoke(robot_ctx)
247
- except SystemExit as e:
248
- exit_code = cast(int, e.code)
273
+ exit_code = _run_robot(ctx, app, args)
274
+ except CancelledError:
275
+ raise
276
+ except Exception as e:
277
+ # An error that stops the run, such as one of the configuration: shown
278
+ # while the session lasts, since the debug launcher forwards the output of
279
+ # the run to VS Code's Debug Console only until then. An error that is not
280
+ # robotcode's own is shown as `debug` shows it.
281
+ error = e if isinstance(e, click.ClickException) else UnknownError(str(e))
282
+ error.show()
283
+ raise _ShownError(error) from e
249
284
  finally:
250
285
  if server.protocol.connected:
251
286
  server.protocol.send_event(
@@ -23,6 +23,8 @@ from .dap_types import (
23
23
  EvaluateArguments,
24
24
  EvaluateResponseBody,
25
25
  Event,
26
+ ExceptionInfoArguments,
27
+ ExceptionInfoResponseBody,
26
28
  ExitedEvent,
27
29
  ExitedEventBody,
28
30
  InitializedEvent,
@@ -208,6 +210,11 @@ class DebugAdapterServerProtocol(DebugAdapterProtocol):
208
210
  *args: Any,
209
211
  **kwargs: Any,
210
212
  ) -> None:
213
+ if self.exited:
214
+ # The run has ended and the process only waits for the client to disconnect. A signal would end it with
215
+ # the exit code of an interrupt and skip its exit handlers, such as those that close a library's browsers.
216
+ return
217
+
211
218
  Debugger.instance.terminate()
212
219
 
213
220
  if not restart and not self._sigint_signaled:
@@ -384,6 +391,15 @@ class DebugAdapterServerProtocol(DebugAdapterProtocol):
384
391
  )
385
392
  return SetExceptionBreakpointsResponseBody(breakpoints=result) if result else None
386
393
 
394
+ @rpc_method(name="exceptionInfo", param_type=ExceptionInfoArguments)
395
+ async def _exception_info(
396
+ self,
397
+ arguments: ExceptionInfoArguments,
398
+ *args: Any,
399
+ **kwargs: Any,
400
+ ) -> ExceptionInfoResponseBody:
401
+ return Debugger.instance.get_exception_info(arguments.thread_id)
402
+
387
403
  @rpc_method(name="completions", param_type=CompletionsArguments)
388
404
  async def _completions(
389
405
  self,
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: robotcode-debugger
3
- Version: 2.6.2
3
+ Version: 2.8.0
4
4
  Summary: RobotCode Debugger for Robot Framework
5
5
  Project-URL: Homepage, https://robotcode.io
6
6
  Project-URL: Donate, https://opencollective.com/robotcode
@@ -20,13 +20,14 @@ Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: 3.15
23
24
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
25
  Classifier: Programming Language :: Python :: Implementation :: PyPy
25
26
  Classifier: Topic :: Utilities
26
27
  Classifier: Typing :: Typed
27
28
  Requires-Python: >=3.10
28
- Requires-Dist: robotcode-jsonrpc2==2.6.2
29
- Requires-Dist: robotcode-runner==2.6.2
29
+ Requires-Dist: robotcode-jsonrpc2==2.8.0
30
+ Requires-Dist: robotcode-runner==2.8.0
30
31
  Requires-Dist: robotframework>=5.0.0
31
32
  Provides-Extra: debugpy
32
33
  Requires-Dist: debugpy; extra == 'debugpy'
@@ -0,0 +1,23 @@
1
+ robotcode/debugger/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ robotcode/debugger/__version__.py,sha256=z6im3C9Qb6qiQIpaJdE4f9WQiCnFGSUQnQXDPw_dvDg,22
3
+ robotcode/debugger/cli.py,sha256=9wkuurbTt2W_NJbjzSS0_tSiclOgCdlWq7FjrlyeyTI,6445
4
+ robotcode/debugger/dap_types.py,sha256=9Q_aR_VOXccvFA--X8cnbPyHuAf1XaFyMw7bpQkqXyw,26736
5
+ robotcode/debugger/debugger.py,sha256=jW54s02qi2jA732ohj9Uqpj9cOU7xJqDMa_bLpxiqTc,87157
6
+ robotcode/debugger/default_capabilities.py,sha256=vGfZuKAh7vsZiQVJeTpDfdrnV0jr4MQ0gtgb8XmPVME,1695
7
+ robotcode/debugger/hooks.py,sha256=chkhSysvfMLdwfP-hMcZ7N1r40917vbJ6nh2rmsPw4g,237
8
+ robotcode/debugger/id_manager.py,sha256=jeftrVlKOpgichf9F15KRiaMCHmwJylD33h1_PW8Wd8,2028
9
+ robotcode/debugger/listeners.py,sha256=COUDb8AXBtQQrZqI7bQl2QlbcG3AoWfIMNCp1VhSPnA,24081
10
+ robotcode/debugger/mixins.py,sha256=h2OpTO-ToWr7cImlclSCEKUJdcWm5Bg8XIRJHDHw9Yw,80
11
+ robotcode/debugger/protocol.py,sha256=dx0Dehpc9rC9pjo5ZTay4AR7F2QaP2iCAiv4pta3ClU,12686
12
+ robotcode/debugger/py.typed,sha256=bWew9mHgMy8LqMu7RuqQXFXLBxh2CRx0dUbSx-3wE48,27
13
+ robotcode/debugger/run.py,sha256=c5FdbVkpsc-EgFfpQg51MmKFVgE86UURoYTbc7J6SzM,9749
14
+ robotcode/debugger/server.py,sha256=kK5RC5TDMSBXM4iAenhaBDb0fMPVxJSioqP0NXD6RHI,15532
15
+ robotcode/debugger/launcher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ robotcode/debugger/launcher/cli.py,sha256=riYOtKP7H0TYwVy5dYl2Vn1-yqqCO1u6O1P26lD3nHg,1798
17
+ robotcode/debugger/launcher/client.py,sha256=8YZiC3171RLpXveJUSf2fAXUwUvQvmPZ3ZJPGrlmy6g,5545
18
+ robotcode/debugger/launcher/run.py,sha256=Q85ue2_V13X6sqNE1Tm4IGcUMTgbdL3YaBTZGDj1xbQ,536
19
+ robotcode/debugger/launcher/server.py,sha256=HcxLdmK_fZGkXZHRdU2zRsbZgWx9IeZ9oEQUa8OUTEU,15121
20
+ robotcode_debugger-2.8.0.dist-info/METADATA,sha256=eTAhRKmaDeqkVsw1uswgniAjz6LKMM-dCNGD6Gi8IrU,2976
21
+ robotcode_debugger-2.8.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
22
+ robotcode_debugger-2.8.0.dist-info/entry_points.txt,sha256=cf2AcgCYK41yJAfzBwOYkmdX1MaGFYFx3X7c0t6b4I8,48
23
+ robotcode_debugger-2.8.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,23 +0,0 @@
1
- robotcode/debugger/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- robotcode/debugger/__version__.py,sha256=53Sii4w6BIWn-1RhaTyqUO46gDe4nDCRQDAcpsWFH24,22
3
- robotcode/debugger/cli.py,sha256=1gEgsWUeuQi5WU9X1oiZ1XobCJmgvyJrxvewlBgaogg,5908
4
- robotcode/debugger/dap_types.py,sha256=-nePls-lwpZhbGxA5117PLsQITCOxsbiQBQ6zGlrW2o,26390
5
- robotcode/debugger/debugger.py,sha256=nb-ZlV3wijicU5jxskHRlFKa6D2CRXRVihzs1c341sM,79952
6
- robotcode/debugger/default_capabilities.py,sha256=Bs4ZjPSr3msMVg-SfZ7atZk1v6osKlO1xZixubc9_S0,1650
7
- robotcode/debugger/hooks.py,sha256=chkhSysvfMLdwfP-hMcZ7N1r40917vbJ6nh2rmsPw4g,237
8
- robotcode/debugger/id_manager.py,sha256=jeftrVlKOpgichf9F15KRiaMCHmwJylD33h1_PW8Wd8,2028
9
- robotcode/debugger/listeners.py,sha256=vU3GwhzBNiWTqTEsvXZY1_CHd8OmwcroPzR__MYA-as,16789
10
- robotcode/debugger/mixins.py,sha256=h2OpTO-ToWr7cImlclSCEKUJdcWm5Bg8XIRJHDHw9Yw,80
11
- robotcode/debugger/protocol.py,sha256=dx0Dehpc9rC9pjo5ZTay4AR7F2QaP2iCAiv4pta3ClU,12686
12
- robotcode/debugger/py.typed,sha256=bWew9mHgMy8LqMu7RuqQXFXLBxh2CRx0dUbSx-3wE48,27
13
- robotcode/debugger/run.py,sha256=Aia3u1augQut7449BUMegrqHuxg3ASAaBGi4vh23dDY,8350
14
- robotcode/debugger/server.py,sha256=PyxL1td4xHETss0SP07o0GgVvtxRjYQucVP2LjidC-o,14880
15
- robotcode/debugger/launcher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- robotcode/debugger/launcher/cli.py,sha256=riYOtKP7H0TYwVy5dYl2Vn1-yqqCO1u6O1P26lD3nHg,1798
17
- robotcode/debugger/launcher/client.py,sha256=8YZiC3171RLpXveJUSf2fAXUwUvQvmPZ3ZJPGrlmy6g,5545
18
- robotcode/debugger/launcher/run.py,sha256=Q85ue2_V13X6sqNE1Tm4IGcUMTgbdL3YaBTZGDj1xbQ,536
19
- robotcode/debugger/launcher/server.py,sha256=HzAouatt1m3od0ut9TJrUSfnOOC2lYxYebARVKas8rw,12583
20
- robotcode_debugger-2.6.2.dist-info/METADATA,sha256=V_TwFuTtKB4zbFNyCYrEKBo7y5ALV690BIIZ9jisTdA,2925
21
- robotcode_debugger-2.6.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
22
- robotcode_debugger-2.6.2.dist-info/entry_points.txt,sha256=cf2AcgCYK41yJAfzBwOYkmdX1MaGFYFx3X7c0t6b4I8,48
23
- robotcode_debugger-2.6.2.dist-info/RECORD,,