robotcode-debugger 2.6.2__tar.gz → 2.8.0.dev548__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.6.2 → robotcode_debugger-2.8.0.dev548}/.gitignore +4 -1
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/PKG-INFO +5 -4
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/pyproject.toml +3 -2
- robotcode_debugger-2.8.0.dev548/src/robotcode/debugger/__version__.py +1 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/cli.py +6 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/dap_types.py +18 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/debugger.py +167 -22
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/default_capabilities.py +4 -3
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/launcher/server.py +31 -8
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/listeners.py +133 -5
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/run.py +27 -17
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/server.py +11 -0
- robotcode_debugger-2.6.2/src/robotcode/debugger/__version__.py +0 -1
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/README.md +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/__init__.py +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/hooks.py +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/id_manager.py +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/launcher/__init__.py +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/launcher/cli.py +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/launcher/client.py +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/launcher/run.py +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/mixins.py +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/protocol.py +0 -0
- {robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/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-dev.548
|
|
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-dev.548
|
|
30
|
+
Requires-Dist: robotcode-runner==2.8.0-dev.548
|
|
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-dev.548",
|
|
34
|
+
"robotcode-runner==2.8.0-dev.548",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
[project.optional-dependencies]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.8.0-dev.548"
|
|
@@ -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,
|
|
@@ -202,5 +204,9 @@ def debug(
|
|
|
202
204
|
raise
|
|
203
205
|
except KeyboardInterrupt:
|
|
204
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
|
|
205
211
|
except Exception as e:
|
|
206
212
|
raise UnknownError(str(e)) from e
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/dap_types.py
RENAMED
|
@@ -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
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/debugger.py
RENAMED
|
@@ -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:
|
|
@@ -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
|
)
|
|
@@ -3,6 +3,7 @@ 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
9
|
from typing import Any, Dict, List, Literal, Optional
|
|
@@ -43,6 +44,32 @@ 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
|
+
|
|
46
73
|
class OutputProtocol(asyncio.SubprocessProtocol):
|
|
47
74
|
def __init__(self, parent: LauncherDebugAdapterProtocol) -> None:
|
|
48
75
|
super().__init__()
|
|
@@ -133,6 +160,7 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
|
|
|
133
160
|
include: Optional[List[str]] = None,
|
|
134
161
|
exclude: Optional[List[str]] = None,
|
|
135
162
|
robotCodeArgs: Optional[List[str]] = None, # noqa: N803
|
|
163
|
+
wrapper: Optional[List[str]] = None,
|
|
136
164
|
arguments: Optional[LaunchRequestArguments] = None,
|
|
137
165
|
*_args: Any,
|
|
138
166
|
**_kwargs: Any,
|
|
@@ -145,14 +173,9 @@ class LauncherDebugAdapterProtocol(DebugAdapterProtocol):
|
|
|
145
173
|
|
|
146
174
|
debugger_script = ["-m", "robotcode.cli"] if self.debugger_script is None else [str(Path(self.debugger_script))]
|
|
147
175
|
|
|
148
|
-
robotcode_run_args =
|
|
149
|
-
python or sys.executable,
|
|
150
|
-
|
|
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
|
-
]
|
|
176
|
+
robotcode_run_args = _build_robotcode_run_args(
|
|
177
|
+
python or sys.executable, debugger_script, profiles, paths, wrapper, robotCodeArgs
|
|
178
|
+
)
|
|
156
179
|
|
|
157
180
|
if no_debug:
|
|
158
181
|
robotcode_run_args += ["--no-debug"]
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/listeners.py
RENAMED
|
@@ -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
|
|
@@ -61,6 +71,17 @@ class ListenerV2:
|
|
|
61
71
|
|
|
62
72
|
def __init__(self) -> None:
|
|
63
73
|
self.failed_keywords: Optional[List[Dict[str, Any]]] = None
|
|
74
|
+
# the number of failed keywords when each running keyword or control structure started; per depth, the failed
|
|
75
|
+
# keywords of the last TRY branch there, and those of them that its EXCEPT branch caught
|
|
76
|
+
self.failed_keyword_marks: List[int] = []
|
|
77
|
+
# by the id of a failed keyword, the nearest failed keyword it ran in
|
|
78
|
+
self.failed_keyword_parents: Dict[int, Dict[str, Any]] = {}
|
|
79
|
+
# per running keyword or control structure, whether it logged a failure itself, as Robot Framework does for the
|
|
80
|
+
# keyword whose error it is; and the ids of the failed keywords that did
|
|
81
|
+
self.failure_logged: List[bool] = []
|
|
82
|
+
self.failed_keywords_with_own_failure: Set[int] = set()
|
|
83
|
+
self.try_failures: Dict[int, List[Dict[str, Any]]] = {}
|
|
84
|
+
self.caught_by_except: Dict[int, List[Dict[str, Any]]] = {}
|
|
64
85
|
self.last_fail_message: Optional[str] = None
|
|
65
86
|
self.suite_id_stack: List[str] = []
|
|
66
87
|
|
|
@@ -105,16 +126,17 @@ class ListenerV2:
|
|
|
105
126
|
id=id,
|
|
106
127
|
parent_id=self.suite_id_stack[-1] if self.suite_id_stack else None,
|
|
107
128
|
failed_keywords=self.failed_keywords,
|
|
129
|
+
failures=self._failures(),
|
|
108
130
|
source=source_from_attributes(attributes) or None,
|
|
109
131
|
lineno=attributes.get("lineno", None),
|
|
110
132
|
),
|
|
111
133
|
),
|
|
112
134
|
)
|
|
113
135
|
self.suite_id_stack.pop()
|
|
114
|
-
self.
|
|
136
|
+
self._forget_failed_keywords()
|
|
115
137
|
|
|
116
138
|
def start_test(self, name: str, attributes: Dict[str, Any]) -> None:
|
|
117
|
-
self.
|
|
139
|
+
self._forget_failed_keywords()
|
|
118
140
|
|
|
119
141
|
Debugger.instance.send_event(
|
|
120
142
|
self,
|
|
@@ -154,15 +176,85 @@ class ListenerV2:
|
|
|
154
176
|
parent_id=self.suite_id_stack[-1] if self.suite_id_stack else None,
|
|
155
177
|
attributes=dict(attributes),
|
|
156
178
|
failed_keywords=self.failed_keywords,
|
|
179
|
+
failures=self._failures(),
|
|
157
180
|
source=source_from_attributes(attributes) or None,
|
|
158
181
|
lineno=attributes.get("lineno", None),
|
|
159
182
|
),
|
|
160
183
|
),
|
|
161
184
|
)
|
|
162
185
|
|
|
186
|
+
self._forget_failed_keywords()
|
|
187
|
+
|
|
188
|
+
def _forget_failed_keywords(self) -> None:
|
|
163
189
|
self.failed_keywords = None
|
|
190
|
+
self.failed_keyword_parents = {}
|
|
191
|
+
self.failed_keywords_with_own_failure = set()
|
|
192
|
+
|
|
193
|
+
def _failures(self) -> Optional[List[RobotFailure]]:
|
|
194
|
+
keywords = self.failed_keywords
|
|
195
|
+
if not keywords:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
index = {id(keyword): i for i, keyword in enumerate(keywords)}
|
|
199
|
+
parents = self.failed_keyword_parents
|
|
200
|
+
|
|
201
|
+
# the failed keywords inside a keyword are those it is the parent of that are caught, or not caught, like it; a
|
|
202
|
+
# caught one inside a keyword that is not caught was caught within it and stays a failure of its own
|
|
203
|
+
def is_inside(keyword: Dict[str, Any], parent: Optional[Dict[str, Any]]) -> bool:
|
|
204
|
+
return parent is not None and bool(parent.get("caught")) == bool(keyword.get("caught"))
|
|
205
|
+
|
|
206
|
+
inside: Dict[int, List[Dict[str, Any]]] = {}
|
|
207
|
+
for keyword in keywords:
|
|
208
|
+
parent = parents.get(id(keyword))
|
|
209
|
+
if parent is not None and is_inside(keyword, parent):
|
|
210
|
+
inside.setdefault(id(parent), []).append(keyword)
|
|
211
|
+
|
|
212
|
+
# a keyword that logged a failure of its own after failures inside it turned theirs into its own, as Wait Until
|
|
213
|
+
# Keyword Succeeds does after its last attempt; one that passes them on or collects them logs none
|
|
214
|
+
def turns_into_own(keyword: Dict[str, Any]) -> bool:
|
|
215
|
+
return bool(inside.get(id(keyword))) and id(keyword) in self.failed_keywords_with_own_failure
|
|
216
|
+
|
|
217
|
+
def within(keyword: Dict[str, Any]) -> Iterator[Dict[str, Any]]:
|
|
218
|
+
current, parent = keyword, parents.get(id(keyword))
|
|
219
|
+
while parent is not None and is_inside(current, parent):
|
|
220
|
+
yield parent
|
|
221
|
+
current, parent = parent, parents.get(id(parent))
|
|
222
|
+
|
|
223
|
+
def outwards(keyword: Dict[str, Any]) -> List[int]:
|
|
224
|
+
calls = []
|
|
225
|
+
current: Optional[Dict[str, Any]] = keyword
|
|
226
|
+
while current is not None:
|
|
227
|
+
calls.append(index[id(current)])
|
|
228
|
+
current = parents.get(id(current))
|
|
229
|
+
return calls
|
|
230
|
+
|
|
231
|
+
# the calls of the newest failure inside the keyword, up to it
|
|
232
|
+
def last_inside(keyword: Dict[str, Any]) -> List[int]:
|
|
233
|
+
for candidate in keywords:
|
|
234
|
+
if inside.get(id(candidate)):
|
|
235
|
+
continue
|
|
236
|
+
calls = [index[id(candidate)]]
|
|
237
|
+
for outer in within(candidate):
|
|
238
|
+
if outer is keyword:
|
|
239
|
+
return calls
|
|
240
|
+
calls.append(index[id(outer)])
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
failures = []
|
|
244
|
+
for keyword in reversed(keywords):
|
|
245
|
+
if any(turns_into_own(outer) for outer in within(keyword)):
|
|
246
|
+
continue
|
|
247
|
+
if turns_into_own(keyword):
|
|
248
|
+
failures.append(RobotFailure(index[id(keyword)], last_inside(keyword) + outwards(keyword)))
|
|
249
|
+
elif not inside.get(id(keyword)):
|
|
250
|
+
failures.append(RobotFailure(index[id(keyword)], outwards(keyword)))
|
|
251
|
+
|
|
252
|
+
return failures
|
|
164
253
|
|
|
165
254
|
def start_keyword(self, name: str, attributes: Dict[str, Any]) -> None:
|
|
255
|
+
self.failed_keyword_marks.append(len(self.failed_keywords or []))
|
|
256
|
+
self.failure_logged.append(False)
|
|
257
|
+
|
|
166
258
|
if attributes["type"] in ["KEYWORD", "SETUP", "TEARDOWN"]:
|
|
167
259
|
Debugger.instance.start_output_group(
|
|
168
260
|
f"{name}({', '.join(repr(v) for v in attributes.get('args', []))})",
|
|
@@ -191,6 +283,8 @@ class ListenerV2:
|
|
|
191
283
|
|
|
192
284
|
def end_keyword(self, name: str, attributes: Dict[str, Any]) -> None:
|
|
193
285
|
Debugger.instance.end_keyword(name, attributes)
|
|
286
|
+
mark = self.failed_keyword_marks.pop() if self.failed_keyword_marks else 0
|
|
287
|
+
failure_logged = self.failure_logged.pop() if self.failure_logged else False
|
|
194
288
|
|
|
195
289
|
# if Debugger.instance.state != State.CallKeyword:
|
|
196
290
|
# Debugger.instance.send_event(
|
|
@@ -216,7 +310,38 @@ class ListenerV2:
|
|
|
216
310
|
if self.failed_keywords is None:
|
|
217
311
|
self.failed_keywords = []
|
|
218
312
|
|
|
219
|
-
|
|
313
|
+
keyword = {"message": self.last_fail_message, **attributes}
|
|
314
|
+
# it is the nearest failed keyword around those that ended since it started and have none yet
|
|
315
|
+
for inner in self.failed_keywords[: max(0, len(self.failed_keywords) - mark)]:
|
|
316
|
+
self.failed_keyword_parents.setdefault(id(inner), keyword)
|
|
317
|
+
self.failed_keywords.insert(0, keyword)
|
|
318
|
+
if failure_logged:
|
|
319
|
+
self.failed_keywords_with_own_failure.add(id(keyword))
|
|
320
|
+
|
|
321
|
+
self._mark_caught_failures(attributes, mark, len(self.failed_keyword_marks))
|
|
322
|
+
|
|
323
|
+
def _mark_caught_failures(self, attributes: Dict[str, Any], mark: int, depth: int) -> None:
|
|
324
|
+
# Robot Framework's log expands a failed test only along what failed, so a failure inside a keyword or control
|
|
325
|
+
# structure that did not fail was caught, as by Run Keyword And Ignore Error. Listener version 2 gets the
|
|
326
|
+
# branches of a TRY/EXCEPT but not the TRY/EXCEPT, whose status the log follows: an EXCEPT branch that passed
|
|
327
|
+
# caught the failures of its TRY branch, unless the FINALLY branch fails and with it the TRY/EXCEPT
|
|
328
|
+
kind, status = attributes["type"], attributes["status"]
|
|
329
|
+
failed = self.failed_keywords or []
|
|
330
|
+
inside = failed[: max(0, len(failed) - mark)]
|
|
331
|
+
if status != "FAIL":
|
|
332
|
+
for keyword in inside:
|
|
333
|
+
keyword["caught"] = True
|
|
334
|
+
if kind == "TRY":
|
|
335
|
+
self.try_failures[depth] = inside
|
|
336
|
+
self.caught_by_except[depth] = []
|
|
337
|
+
elif kind == "EXCEPT" and status == "PASS":
|
|
338
|
+
caught = [keyword for keyword in self.try_failures.get(depth, []) if not keyword.get("caught")]
|
|
339
|
+
for keyword in caught:
|
|
340
|
+
keyword["caught"] = True
|
|
341
|
+
self.caught_by_except[depth] = caught
|
|
342
|
+
elif kind == "FINALLY" and status == "FAIL":
|
|
343
|
+
for keyword in self.caught_by_except.pop(depth, []):
|
|
344
|
+
keyword.pop("caught", None)
|
|
220
345
|
|
|
221
346
|
RE_FILE_LINE_MATCHER = re.compile(r".+\sin\sfile\s'(?P<file>.*)'\son\sline\s(?P<line>\d+):(?P<message>.*)")
|
|
222
347
|
|
|
@@ -227,6 +352,8 @@ class ListenerV2:
|
|
|
227
352
|
if message["level"] == "FAIL":
|
|
228
353
|
self.last_fail_message = message["message"]
|
|
229
354
|
Debugger.instance.last_fail_message = self.last_fail_message
|
|
355
|
+
if self.failure_logged:
|
|
356
|
+
self.failure_logged[-1] = True
|
|
230
357
|
|
|
231
358
|
source = current_frame.source if current_frame else None
|
|
232
359
|
line = current_frame.line if current_frame else None
|
|
@@ -404,7 +531,8 @@ class ListenerV3:
|
|
|
404
531
|
"lineno": data_item.lineno if data_item is not None else 0,
|
|
405
532
|
"message": result_item.message,
|
|
406
533
|
},
|
|
407
|
-
id=f"{result_item.source
|
|
534
|
+
id=f"{normalized_path(Path(result_item.source)) if result_item.source else ''};"
|
|
535
|
+
f"{result_item.longname or ''}"
|
|
408
536
|
+ (
|
|
409
537
|
f";{data_item.lineno if data_item is not None else 0}"
|
|
410
538
|
if isinstance(result_item, result.TestCase)
|
|
@@ -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
|
-
|
|
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(
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/server.py
RENAMED
|
@@ -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,
|
|
@@ -384,6 +386,15 @@ class DebugAdapterServerProtocol(DebugAdapterProtocol):
|
|
|
384
386
|
)
|
|
385
387
|
return SetExceptionBreakpointsResponseBody(breakpoints=result) if result else None
|
|
386
388
|
|
|
389
|
+
@rpc_method(name="exceptionInfo", param_type=ExceptionInfoArguments)
|
|
390
|
+
async def _exception_info(
|
|
391
|
+
self,
|
|
392
|
+
arguments: ExceptionInfoArguments,
|
|
393
|
+
*args: Any,
|
|
394
|
+
**kwargs: Any,
|
|
395
|
+
) -> ExceptionInfoResponseBody:
|
|
396
|
+
return Debugger.instance.get_exception_info(arguments.thread_id)
|
|
397
|
+
|
|
387
398
|
@rpc_method(name="completions", param_type=CompletionsArguments)
|
|
388
399
|
async def _completions(
|
|
389
400
|
self,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.6.2"
|
|
File without changes
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/__init__.py
RENAMED
|
File without changes
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/hooks.py
RENAMED
|
File without changes
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/id_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/launcher/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/launcher/run.py
RENAMED
|
File without changes
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/mixins.py
RENAMED
|
File without changes
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/protocol.py
RENAMED
|
File without changes
|
{robotcode_debugger-2.6.2 → robotcode_debugger-2.8.0.dev548}/src/robotcode/debugger/py.typed
RENAMED
|
File without changes
|