robotcode-debugger 2.7.0__py3-none-any.whl → 2.8.0.dev631__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.7.0"
1
+ __version__ = "2.8.0-dev.631"
robotcode/debugger/cli.py CHANGED
@@ -204,5 +204,9 @@ def debug(
204
204
  raise
205
205
  except KeyboardInterrupt:
206
206
  app.keyboard_interrupt()
207
+ except click.ClickException:
208
+ # Errors robotcode reports itself, such as a broken configuration; the root
209
+ # group gives them Robot Framework's exit code.
210
+ raise
207
211
  except Exception as e:
208
212
  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
  )
@@ -6,7 +6,7 @@ import os
6
6
  import shlex
7
7
  import sys
8
8
  from pathlib import Path
9
- from typing import Any, Dict, List, Literal, Optional
9
+ from typing import Any, Dict, List, Literal, Mapping, Optional
10
10
 
11
11
  from robotcode.core.types import ServerMode, TcpParams
12
12
  from robotcode.core.utils.logging import LoggingDescriptor
@@ -70,6 +70,30 @@ def _build_robotcode_run_args(
70
70
  ]
71
71
 
72
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
+
73
97
  class OutputProtocol(asyncio.SubprocessProtocol):
74
98
  def __init__(self, parent: LauncherDebugAdapterProtocol) -> None:
75
99
  super().__init__()
@@ -136,7 +160,7 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
136
160
  paths: Optional[List[str]] = None,
137
161
  args: Optional[List[str]] = None,
138
162
  env: Optional[Dict[str, Optional[Any]]] = None,
139
- console: Optional[Literal["internalConsole", "integratedTerminal", "externalTerminal"]] = "integratedTerminal",
163
+ console: Optional[Console] = "integratedTerminal",
140
164
  name: Optional[str] = None,
141
165
  no_debug: Optional[bool] = None,
142
166
  robotPythonPath: Optional[List[str]] = None, # noqa: N803
@@ -263,7 +287,19 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
263
287
 
264
288
  env = {k: ("" if v is None else str(v)) for k, v in env.items()} if env else {}
265
289
 
266
- 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"]:
267
303
  await self.send_request_async(
268
304
  RunInTerminalRequest(
269
305
  arguments=RunInTerminalRequestArguments(
@@ -284,8 +320,7 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
284
320
  )
285
321
  # time.sleep(1) # Give some time for the terminal to start
286
322
  elif console is None or console == "internalConsole":
287
- run_env: Dict[str, Optional[str]] = dict(os.environ)
288
- run_env.update(env)
323
+ run_env = _merged_environment(os.environ, env, sys.platform == "win32")
289
324
 
290
325
  await asyncio.get_event_loop().subprocess_exec(
291
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
@@ -145,6 +145,27 @@ def start_debugpy(
145
145
  config_done_callback = connect_debugpy
146
146
 
147
147
 
148
+ def _run_robot(ctx: click.Context, app: Application, args: List[str]) -> int:
149
+ from robotcode.runner.cli.robot import robot
150
+
151
+ args = [
152
+ "--listener",
153
+ "robotcode.debugger.listeners.ListenerV3",
154
+ "--listener",
155
+ "robotcode.debugger.listeners.ListenerV2",
156
+ *args,
157
+ ]
158
+
159
+ app.verbose("Start robot")
160
+ try:
161
+ app.verbose(f"Create robot context with args: {args}")
162
+ robot_ctx = robot.make_context("robot", args, parent=ctx)
163
+ robot.invoke(robot_ctx)
164
+ except SystemExit as e:
165
+ return cast(int, e.code)
166
+ return 0
167
+
168
+
148
169
  @_logger.call
149
170
  def run_debugger(
150
171
  ctx: click.Context,
@@ -167,6 +188,11 @@ def run_debugger(
167
188
  output_timestamps: bool = False,
168
189
  group_output: bool = False,
169
190
  ) -> int:
191
+ if app.config.dry:
192
+ # A dry run starts neither debugpy nor the debug adapter and waits for no
193
+ # client; the robot command only prints what it would execute.
194
+ return _run_robot(ctx, app, args)
195
+
170
196
  if debug and debugpy and not is_debugpy_installed():
171
197
  app.warning("Debugpy not installed")
172
198
 
@@ -215,14 +241,6 @@ def run_debugger(
215
241
  if debugpy and debugpy_wait_for_client:
216
242
  debugpy_connected.wait(wait_for_client_timeout)
217
243
 
218
- args = [
219
- "--listener",
220
- "robotcode.debugger.listeners.ListenerV3",
221
- "--listener",
222
- "robotcode.debugger.listeners.ListenerV2",
223
- *args,
224
- ]
225
-
226
244
  Debugger.instance.stop_on_entry = stop_on_entry
227
245
  Debugger.instance.output_messages = output_messages
228
246
  Debugger.instance.output_log = output_log
@@ -237,15 +255,7 @@ def run_debugger(
237
255
 
238
256
  exit_code = 0
239
257
  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)
258
+ exit_code = _run_robot(ctx, app, args)
249
259
  finally:
250
260
  if server.protocol.connected:
251
261
  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.7.0
3
+ Version: 2.8.0-dev.631
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.7.0
29
- Requires-Dist: robotcode-runner==2.7.0
29
+ Requires-Dist: robotcode-jsonrpc2==2.8.0-dev.631
30
+ Requires-Dist: robotcode-runner==2.8.0-dev.631
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=9SGjqX5uvERIavT5Cjgw0TSLgQ-nKCFaQ13-E6K7MDA,30
3
+ robotcode/debugger/cli.py,sha256=Ql8SwCU8bJArvqEqvpmY8X-9SRYs86d-qKsPsyVln8k,6168
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=5DCIdWVajg-Acbm0KuNyDwqsDggz4oRX9WwJRZERW5s,8616
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.dev631.dist-info/METADATA,sha256=g1DBAWOQgcO4HZP8_KliyNySMdXqbgKOxwxufIdjAbM,3000
21
+ robotcode_debugger-2.8.0.dev631.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
22
+ robotcode_debugger-2.8.0.dev631.dist-info/entry_points.txt,sha256=cf2AcgCYK41yJAfzBwOYkmdX1MaGFYFx3X7c0t6b4I8,48
23
+ robotcode_debugger-2.8.0.dev631.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
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=EtKWW0Hnl5oWglRNH0HZigvcDT2FEs58ek8buJdwW1E,22
3
- robotcode/debugger/cli.py,sha256=2o5yWq5YwX5bPabAruRoGpwgfsSNEn0COLSsLNv_ZFs,5981
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=cHX13NXY2Yx5cQ9TFcxB_RNmH2Tu1Gl5vlQYqjlqSq8,13421
20
- robotcode_debugger-2.7.0.dist-info/METADATA,sha256=-WRWypXyz8xODRcPZpdXUXc08LNP8zmX83-LtLL2Xa4,2925
21
- robotcode_debugger-2.7.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
22
- robotcode_debugger-2.7.0.dist-info/entry_points.txt,sha256=cf2AcgCYK41yJAfzBwOYkmdX1MaGFYFx3X7c0t6b4I8,48
23
- robotcode_debugger-2.7.0.dist-info/RECORD,,