robotcode-debugger 2.7.0__tar.gz → 2.8.0__tar.gz
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.
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/.gitignore +4 -1
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/PKG-INFO +5 -4
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/pyproject.toml +3 -2
- robotcode_debugger-2.8.0/src/robotcode/debugger/__version__.py +1 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/cli.py +10 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/dap_types.py +18 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/debugger.py +167 -22
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/default_capabilities.py +4 -3
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/server.py +40 -5
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/listeners.py +156 -17
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/run.py +53 -18
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/server.py +16 -0
- robotcode_debugger-2.7.0/src/robotcode/debugger/__version__.py +0 -1
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/README.md +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/__init__.py +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/hooks.py +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/id_manager.py +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/__init__.py +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/cli.py +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/client.py +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/run.py +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/mixins.py +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/protocol.py +0 -0
- {robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/py.typed +0 -0
|
@@ -279,6 +279,9 @@ dist
|
|
|
279
279
|
# vuepress build output
|
|
280
280
|
.vuepress/dist
|
|
281
281
|
|
|
282
|
+
# Astro (documentation site)
|
|
283
|
+
docs/.astro/
|
|
284
|
+
|
|
282
285
|
# Serverless directories
|
|
283
286
|
.serverless/
|
|
284
287
|
|
|
@@ -339,7 +342,7 @@ bundled/libs
|
|
|
339
342
|
# .agents
|
|
340
343
|
.agents/
|
|
341
344
|
skills-lock.json
|
|
342
|
-
|
|
345
|
+
#.claude
|
|
343
346
|
|
|
344
347
|
# sarif files
|
|
345
348
|
/**/*.sarif.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: robotcode-debugger
|
|
3
|
-
Version: 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.
|
|
29
|
-
Requires-Dist: robotcode-runner==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'
|
|
@@ -18,6 +18,7 @@ classifiers = [
|
|
|
18
18
|
"Programming Language :: Python :: 3.12",
|
|
19
19
|
"Programming Language :: Python :: 3.13",
|
|
20
20
|
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Programming Language :: Python :: 3.15",
|
|
21
22
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
22
23
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
23
24
|
"Operating System :: OS Independent",
|
|
@@ -29,8 +30,8 @@ classifiers = [
|
|
|
29
30
|
dynamic = ["version"]
|
|
30
31
|
dependencies = [
|
|
31
32
|
"robotframework>=5.0.0",
|
|
32
|
-
"robotcode-jsonrpc2==2.
|
|
33
|
-
"robotcode-runner==2.
|
|
33
|
+
"robotcode-jsonrpc2==2.8.0",
|
|
34
|
+
"robotcode-runner==2.8.0",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
[project.optional-dependencies]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.8.0"
|
|
@@ -145,8 +145,14 @@ def debug(
|
|
|
145
145
|
robot_options_and_args: Tuple[str, ...],
|
|
146
146
|
) -> None:
|
|
147
147
|
"""Starts a Robot Framework debug session and waits for incomming connections."""
|
|
148
|
+
from robotcode.runner.cli.robot import load_robot_profile
|
|
149
|
+
|
|
148
150
|
from .run import run_debugger
|
|
149
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
|
+
|
|
150
156
|
mode, port, bind, pipe_name = resolve_server_options(
|
|
151
157
|
ctx,
|
|
152
158
|
app,
|
|
@@ -204,5 +210,9 @@ def debug(
|
|
|
204
210
|
raise
|
|
205
211
|
except KeyboardInterrupt:
|
|
206
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
|
|
207
217
|
except Exception as e:
|
|
208
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.
|
|
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
|
|
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]
|
|
753
|
-
except
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
{robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/default_capabilities.py
RENAMED
|
@@ -28,21 +28,22 @@ DFEAULT_CAPABILITIES = Capabilities(
|
|
|
28
28
|
),
|
|
29
29
|
ExceptionBreakpointsFilter(
|
|
30
30
|
filter="failed_test",
|
|
31
|
-
label="Failed
|
|
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
|
|
39
|
-
description="Breaks on failed
|
|
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
|
)
|
{robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/server.py
RENAMED
|
@@ -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[
|
|
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
|
-
|
|
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
|
|
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.
|
|
145
|
+
self._forget_failed_keywords()
|
|
115
146
|
|
|
116
147
|
def start_test(self, name: str, attributes: Dict[str, Any]) -> None:
|
|
117
|
-
self.
|
|
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'
|
|
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=
|
|
160
|
+
attributes=without_empty_lineno(attributes),
|
|
130
161
|
source=source_from_attributes(attributes) or None,
|
|
131
|
-
lineno=attributes.get("lineno"
|
|
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'
|
|
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=
|
|
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"
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
),
|
|
@@ -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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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 +0,0 @@
|
|
|
1
|
-
__version__ = "2.7.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/__init__.py
RENAMED
|
File without changes
|
{robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/cli.py
RENAMED
|
File without changes
|
{robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/client.py
RENAMED
|
File without changes
|
{robotcode_debugger-2.7.0 → robotcode_debugger-2.8.0}/src/robotcode/debugger/launcher/run.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|