robotcode-runner 2.7.0__py3-none-any.whl → 2.8.0.dev8__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.
- robotcode/runner/__version__.py +1 -1
- robotcode/runner/cli/_markdown.py +13 -0
- robotcode/runner/cli/_robot_version.py +28 -0
- robotcode/runner/cli/_search.py +37 -1
- robotcode/runner/cli/discover/_models.py +11 -1
- robotcode/runner/cli/discover/_render.py +108 -2
- robotcode/runner/cli/discover/discover.py +267 -40
- robotcode/runner/cli/libdoc.py +7 -5
- robotcode/runner/cli/rebot.py +32 -5
- robotcode/runner/cli/results/_html.py +228 -64
- robotcode/runner/cli/results/_models.py +4 -0
- robotcode/runner/cli/results/_render.py +9 -1
- robotcode/runner/cli/results/results.py +241 -48
- robotcode/runner/cli/robot.py +145 -27
- robotcode/runner/cli/testdoc.py +7 -4
- {robotcode_runner-2.7.0.dist-info → robotcode_runner-2.8.0.dev8.dist-info}/METADATA +8 -7
- robotcode_runner-2.8.0.dev8.dist-info/RECORD +25 -0
- {robotcode_runner-2.7.0.dist-info → robotcode_runner-2.8.0.dev8.dist-info}/WHEEL +1 -1
- robotcode_runner-2.7.0.dist-info/RECORD +0 -24
- {robotcode_runner-2.7.0.dist-info → robotcode_runner-2.8.0.dev8.dist-info}/entry_points.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import os
|
|
2
3
|
import platform
|
|
3
4
|
import re
|
|
@@ -10,6 +11,7 @@ from typing import (
|
|
|
10
11
|
Dict,
|
|
11
12
|
Iterable,
|
|
12
13
|
List,
|
|
14
|
+
Mapping,
|
|
13
15
|
MutableMapping,
|
|
14
16
|
Optional,
|
|
15
17
|
Tuple,
|
|
@@ -39,6 +41,7 @@ from robotcode.core.uri import Uri
|
|
|
39
41
|
from robotcode.core.utils.cli import show_hidden_arguments
|
|
40
42
|
from robotcode.core.utils.dataclasses import from_json
|
|
41
43
|
from robotcode.core.utils.path import normalized_path
|
|
44
|
+
from robotcode.modifiers.metadata_modifiers import metadata_entries
|
|
42
45
|
from robotcode.plugin import (
|
|
43
46
|
Application,
|
|
44
47
|
OutputFormat,
|
|
@@ -49,10 +52,16 @@ from robotcode.plugin.click_helper.types import add_options
|
|
|
49
52
|
from robotcode.robot.utils import RF_VERSION
|
|
50
53
|
|
|
51
54
|
from .._markdown import make_md_highlighter
|
|
52
|
-
from ..
|
|
55
|
+
from .._robot_version import RobotVersionCommand, RobotVersionOption
|
|
56
|
+
from .._search import (
|
|
57
|
+
SearchMatcher,
|
|
58
|
+
get_test_metadata,
|
|
59
|
+
make_search_matcher,
|
|
60
|
+
named_metadata,
|
|
61
|
+
)
|
|
53
62
|
from ..robot import ROBOT_OPTIONS, ROBOT_VERSION_OPTIONS, RobotFrameworkEx, handle_robot_options
|
|
54
63
|
from . import _render
|
|
55
|
-
from ._models import Info, ResultItem, Statistics, TagsResult, TestItem
|
|
64
|
+
from ._models import Info, MetadataResult, ResultItem, Statistics, TagsResult, TestItem
|
|
56
65
|
|
|
57
66
|
# Robot Framework 6.1 introduced `--parseinclude`. Before that, the `--suite`
|
|
58
67
|
# option implicitly restricted which files were parsed, so an explicit include
|
|
@@ -69,8 +78,9 @@ DISCOVER_SEARCH_OPTIONS = [
|
|
|
69
78
|
help=(
|
|
70
79
|
"Only include items where TEXT case-insensitively matches the "
|
|
71
80
|
"name, full name, source path, documentation, template name, "
|
|
72
|
-
"timeout, any tag (normalisation-aware),
|
|
73
|
-
"Documentation / Metadata, or anything
|
|
81
|
+
"timeout, any tag (normalisation-aware), test metadata names and "
|
|
82
|
+
"values, the parent suite's Documentation / Metadata, or anything "
|
|
83
|
+
"inside the test body "
|
|
74
84
|
"— keyword names, keyword arguments, assigned variables, "
|
|
75
85
|
"FOR/WHILE/IF conditions, VAR/RETURN values, EXCEPT patterns, "
|
|
76
86
|
"GROUP names. Mutually exclusive with `--search-regex`."
|
|
@@ -264,6 +274,8 @@ class Collector(SuiteVisitor):
|
|
|
264
274
|
self.test_and_tasks: List[TestItem] = []
|
|
265
275
|
self.tags: Dict[str, List[TestItem]] = defaultdict(list)
|
|
266
276
|
self.normalized_tags: Dict[str, List[TestItem]] = defaultdict(list)
|
|
277
|
+
# Suite items carry no metadata; `discover metadata` lists it separately.
|
|
278
|
+
self.suite_metadata: List[Tuple[TestItem, Dict[str, str]]] = []
|
|
267
279
|
self.statistics = Statistics()
|
|
268
280
|
self._collected: List[MutableMapping[str, Any]] = [NormalizedDict(ignore="_")]
|
|
269
281
|
|
|
@@ -289,7 +301,7 @@ class Collector(SuiteVisitor):
|
|
|
289
301
|
name=suite.name,
|
|
290
302
|
longname=suite.longname,
|
|
291
303
|
uri=str(Uri.from_path(absolute_path)) if absolute_path else None,
|
|
292
|
-
source=str(suite.source),
|
|
304
|
+
source=str(suite.source) if suite.source else None,
|
|
293
305
|
rel_source=get_rel_source(suite.source),
|
|
294
306
|
range=(
|
|
295
307
|
Range(
|
|
@@ -307,6 +319,9 @@ class Collector(SuiteVisitor):
|
|
|
307
319
|
raise ValueError(f"Error while parsing suite {suite.source}: {e}") from e
|
|
308
320
|
|
|
309
321
|
self.suites.append(item)
|
|
322
|
+
suite_metadata = named_metadata(suite.metadata)
|
|
323
|
+
if suite_metadata:
|
|
324
|
+
self.suite_metadata.append((item, suite_metadata))
|
|
310
325
|
|
|
311
326
|
if self._current.children is None:
|
|
312
327
|
self._current.children = []
|
|
@@ -330,9 +345,13 @@ class Collector(SuiteVisitor):
|
|
|
330
345
|
self._collected.pop()
|
|
331
346
|
|
|
332
347
|
def visit_test(self, test: TestCase) -> None:
|
|
348
|
+
# A custom parser or a pre-run modifier may create a test without a line number. Robot Framework's lines start
|
|
349
|
+
# at 1, so such a test gets 0 in its ID, as the debugger's events do, and no line and no range.
|
|
350
|
+
lineno = test.lineno or None
|
|
351
|
+
|
|
333
352
|
if test.name in self._collected[-1]:
|
|
334
353
|
LOGGER.warn(
|
|
335
|
-
f"Warning in file '{test.source}' on line {
|
|
354
|
+
f"Warning in file '{test.source}'{f' on line {lineno}' if lineno else ''}: "
|
|
336
355
|
f"Multiple {'task' if test.parent.rpa else 'test'}s with name '{test.name}' in suite "
|
|
337
356
|
f"'{test.parent.longname}'."
|
|
338
357
|
)
|
|
@@ -344,18 +363,23 @@ class Collector(SuiteVisitor):
|
|
|
344
363
|
absolute_path = normalized_path(Path(test.source)) if test.source is not None else None
|
|
345
364
|
item = TestItem(
|
|
346
365
|
type="task" if self._current.rpa else "test",
|
|
347
|
-
id=f"{absolute_path or ''};{test.longname};{
|
|
366
|
+
id=f"{absolute_path or ''};{test.longname};{lineno or 0}",
|
|
348
367
|
name=test.name,
|
|
349
368
|
longname=test.longname,
|
|
350
|
-
lineno=
|
|
369
|
+
lineno=lineno,
|
|
351
370
|
uri=str(Uri.from_path(absolute_path)) if absolute_path else None,
|
|
352
371
|
source=str(test.source),
|
|
353
372
|
rel_source=get_rel_source(test.source),
|
|
354
|
-
range=
|
|
355
|
-
|
|
356
|
-
|
|
373
|
+
range=(
|
|
374
|
+
Range(
|
|
375
|
+
start=Position(line=lineno - 1, character=0),
|
|
376
|
+
end=Position(line=lineno - 1, character=0),
|
|
377
|
+
)
|
|
378
|
+
if lineno
|
|
379
|
+
else None
|
|
357
380
|
),
|
|
358
381
|
tags=sorted(set(normalize(str(t), ignore="_") for t in test.tags)) if test.tags else None,
|
|
382
|
+
metadata=get_test_metadata(test) or None,
|
|
359
383
|
rpa=self._current.rpa,
|
|
360
384
|
)
|
|
361
385
|
except ValueError as e:
|
|
@@ -477,6 +501,8 @@ def handle_options(
|
|
|
477
501
|
exclude_by_longname: Tuple[str, ...],
|
|
478
502
|
robot_options_and_args: Tuple[str, ...],
|
|
479
503
|
search_matcher: Optional[SearchMatcher] = None,
|
|
504
|
+
by_test_metadata: Tuple[str, ...] = (),
|
|
505
|
+
exclude_by_test_metadata: Tuple[str, ...] = (),
|
|
480
506
|
) -> Tuple[TestSuite, Collector, Optional[Dict[str, List[Diagnostic]]]]:
|
|
481
507
|
root_folder, profile, cmd_options = handle_robot_options(app, robot_options_and_args)
|
|
482
508
|
|
|
@@ -500,6 +526,8 @@ def handle_options(
|
|
|
500
526
|
by_longname,
|
|
501
527
|
exclude_by_longname,
|
|
502
528
|
search_matcher=search_matcher,
|
|
529
|
+
by_test_metadata=by_test_metadata,
|
|
530
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
503
531
|
).parse_arguments((*cmd_options, "--runemptysuite", *robot_options_and_args))
|
|
504
532
|
|
|
505
533
|
settings = RobotSettings(options)
|
|
@@ -572,6 +600,29 @@ def handle_options(
|
|
|
572
600
|
raise UnknownError("Unexpected error happened.")
|
|
573
601
|
|
|
574
602
|
|
|
603
|
+
def _show_options(*, default: bool) -> List[Any]:
|
|
604
|
+
"""The flags of `all`, `tests` and `tasks` that add the tags and metadata lines in TEXT mode."""
|
|
605
|
+
return [
|
|
606
|
+
click.option(
|
|
607
|
+
"--show-tags / --no-show-tags",
|
|
608
|
+
"show_tags",
|
|
609
|
+
default=default,
|
|
610
|
+
show_default=True,
|
|
611
|
+
help="Show the tags that are present.",
|
|
612
|
+
),
|
|
613
|
+
click.option(
|
|
614
|
+
"--show-metadata / --no-show-metadata",
|
|
615
|
+
"show_metadata",
|
|
616
|
+
default=default,
|
|
617
|
+
show_default=True,
|
|
618
|
+
# test metadata exists since Robot Framework 7.5
|
|
619
|
+
cls=RobotVersionOption,
|
|
620
|
+
since=(7, 5),
|
|
621
|
+
help="Show the metadata of tests and tasks.",
|
|
622
|
+
),
|
|
623
|
+
]
|
|
624
|
+
|
|
625
|
+
|
|
575
626
|
def _filters_applied(search_substring: Optional[str], search_regex: Optional[str]) -> Optional[Dict[str, str]]:
|
|
576
627
|
if search_substring:
|
|
577
628
|
return {"search": search_substring}
|
|
@@ -585,13 +636,7 @@ def _filters_applied(search_substring: Optional[str], search_regex: Optional[str
|
|
|
585
636
|
add_help_option=True,
|
|
586
637
|
epilog="Use `-- --help` to see `robot` help.",
|
|
587
638
|
)
|
|
588
|
-
@
|
|
589
|
-
"--tags / --no-tags",
|
|
590
|
-
"show_tags",
|
|
591
|
-
default=True,
|
|
592
|
-
show_default=True,
|
|
593
|
-
help="Show the tags that are present.",
|
|
594
|
-
)
|
|
639
|
+
@add_options(*_show_options(default=True))
|
|
595
640
|
@add_options(*ROBOT_OPTIONS)
|
|
596
641
|
@add_options(*DISCOVER_SEARCH_OPTIONS)
|
|
597
642
|
@click.option(
|
|
@@ -606,10 +651,13 @@ def all(
|
|
|
606
651
|
app: Application,
|
|
607
652
|
full_paths: bool,
|
|
608
653
|
show_tags: bool,
|
|
654
|
+
show_metadata: bool,
|
|
609
655
|
search_substring: Optional[str],
|
|
610
656
|
search_regex: Optional[str],
|
|
611
657
|
by_longname: Tuple[str, ...],
|
|
612
658
|
exclude_by_longname: Tuple[str, ...],
|
|
659
|
+
by_test_metadata: Tuple[str, ...],
|
|
660
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
613
661
|
robot_options_and_args: Tuple[str, ...],
|
|
614
662
|
) -> None:
|
|
615
663
|
"""\
|
|
@@ -629,7 +677,13 @@ def all(
|
|
|
629
677
|
|
|
630
678
|
matcher = make_search_matcher(search_substring, search_regex)
|
|
631
679
|
_suite, collector, diagnostics = handle_options(
|
|
632
|
-
app,
|
|
680
|
+
app,
|
|
681
|
+
by_longname,
|
|
682
|
+
exclude_by_longname,
|
|
683
|
+
robot_options_and_args,
|
|
684
|
+
search_matcher=matcher,
|
|
685
|
+
by_test_metadata=by_test_metadata,
|
|
686
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
633
687
|
)
|
|
634
688
|
|
|
635
689
|
if collector.all.children:
|
|
@@ -639,6 +693,7 @@ def all(
|
|
|
639
693
|
collector.all.children[0],
|
|
640
694
|
collector.statistics,
|
|
641
695
|
show_tags=show_tags,
|
|
696
|
+
show_metadata=show_metadata,
|
|
642
697
|
full_paths=full_paths,
|
|
643
698
|
highlight=make_md_highlighter(search_substring, search_regex),
|
|
644
699
|
search_substring=search_substring,
|
|
@@ -666,15 +721,24 @@ def _test_or_tasks(
|
|
|
666
721
|
app: Application,
|
|
667
722
|
full_paths: bool,
|
|
668
723
|
show_tags: bool,
|
|
724
|
+
show_metadata: bool,
|
|
669
725
|
search_substring: Optional[str],
|
|
670
726
|
search_regex: Optional[str],
|
|
671
727
|
by_longname: Tuple[str, ...],
|
|
672
728
|
exclude_by_longname: Tuple[str, ...],
|
|
729
|
+
by_test_metadata: Tuple[str, ...],
|
|
730
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
673
731
|
robot_options_and_args: Tuple[str, ...],
|
|
674
732
|
) -> None:
|
|
675
733
|
matcher = make_search_matcher(search_substring, search_regex)
|
|
676
734
|
_suite, collector, diagnostics = handle_options(
|
|
677
|
-
app,
|
|
735
|
+
app,
|
|
736
|
+
by_longname,
|
|
737
|
+
exclude_by_longname,
|
|
738
|
+
robot_options_and_args,
|
|
739
|
+
search_matcher=matcher,
|
|
740
|
+
by_test_metadata=by_test_metadata,
|
|
741
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
678
742
|
)
|
|
679
743
|
|
|
680
744
|
if collector.all.children:
|
|
@@ -685,6 +749,7 @@ def _test_or_tasks(
|
|
|
685
749
|
collector.statistics,
|
|
686
750
|
selected_type=selected_type,
|
|
687
751
|
show_tags=show_tags,
|
|
752
|
+
show_metadata=show_metadata,
|
|
688
753
|
full_paths=full_paths,
|
|
689
754
|
highlight=make_md_highlighter(search_substring, search_regex),
|
|
690
755
|
search_substring=search_substring,
|
|
@@ -713,13 +778,7 @@ def _test_or_tasks(
|
|
|
713
778
|
add_help_option=True,
|
|
714
779
|
epilog="Use `-- --help` to see `robot` help.",
|
|
715
780
|
)
|
|
716
|
-
@
|
|
717
|
-
"--tags / --no-tags",
|
|
718
|
-
"show_tags",
|
|
719
|
-
default=False,
|
|
720
|
-
show_default=True,
|
|
721
|
-
help="Show the tags that are present.",
|
|
722
|
-
)
|
|
781
|
+
@add_options(*_show_options(default=False))
|
|
723
782
|
@click.option(
|
|
724
783
|
"--full-paths / --no-full-paths",
|
|
725
784
|
"full_paths",
|
|
@@ -734,10 +793,13 @@ def tests(
|
|
|
734
793
|
app: Application,
|
|
735
794
|
full_paths: bool,
|
|
736
795
|
show_tags: bool,
|
|
796
|
+
show_metadata: bool,
|
|
737
797
|
search_substring: Optional[str],
|
|
738
798
|
search_regex: Optional[str],
|
|
739
799
|
by_longname: Tuple[str, ...],
|
|
740
800
|
exclude_by_longname: Tuple[str, ...],
|
|
801
|
+
by_test_metadata: Tuple[str, ...],
|
|
802
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
741
803
|
robot_options_and_args: Tuple[str, ...],
|
|
742
804
|
) -> None:
|
|
743
805
|
"""\
|
|
@@ -760,10 +822,13 @@ def tests(
|
|
|
760
822
|
app,
|
|
761
823
|
full_paths,
|
|
762
824
|
show_tags,
|
|
825
|
+
show_metadata,
|
|
763
826
|
search_substring,
|
|
764
827
|
search_regex,
|
|
765
828
|
by_longname,
|
|
766
829
|
exclude_by_longname,
|
|
830
|
+
by_test_metadata,
|
|
831
|
+
exclude_by_test_metadata,
|
|
767
832
|
robot_options_and_args,
|
|
768
833
|
)
|
|
769
834
|
|
|
@@ -773,13 +838,7 @@ def tests(
|
|
|
773
838
|
add_help_option=True,
|
|
774
839
|
epilog="Use `-- --help` to see `robot` help.",
|
|
775
840
|
)
|
|
776
|
-
@
|
|
777
|
-
"--tags / --no-tags",
|
|
778
|
-
"show_tags",
|
|
779
|
-
default=False,
|
|
780
|
-
show_default=True,
|
|
781
|
-
help="Show the tags that are present.",
|
|
782
|
-
)
|
|
841
|
+
@add_options(*_show_options(default=False))
|
|
783
842
|
@click.option(
|
|
784
843
|
"--full-paths / --no-full-paths",
|
|
785
844
|
"full_paths",
|
|
@@ -794,10 +853,13 @@ def tasks(
|
|
|
794
853
|
app: Application,
|
|
795
854
|
full_paths: bool,
|
|
796
855
|
show_tags: bool,
|
|
856
|
+
show_metadata: bool,
|
|
797
857
|
search_substring: Optional[str],
|
|
798
858
|
search_regex: Optional[str],
|
|
799
859
|
by_longname: Tuple[str, ...],
|
|
800
860
|
exclude_by_longname: Tuple[str, ...],
|
|
861
|
+
by_test_metadata: Tuple[str, ...],
|
|
862
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
801
863
|
robot_options_and_args: Tuple[str, ...],
|
|
802
864
|
) -> None:
|
|
803
865
|
"""\
|
|
@@ -819,10 +881,13 @@ def tasks(
|
|
|
819
881
|
app,
|
|
820
882
|
full_paths,
|
|
821
883
|
show_tags,
|
|
884
|
+
show_metadata,
|
|
822
885
|
search_substring,
|
|
823
886
|
search_regex,
|
|
824
887
|
by_longname,
|
|
825
888
|
exclude_by_longname,
|
|
889
|
+
by_test_metadata,
|
|
890
|
+
exclude_by_test_metadata,
|
|
826
891
|
robot_options_and_args,
|
|
827
892
|
)
|
|
828
893
|
|
|
@@ -849,6 +914,8 @@ def suites(
|
|
|
849
914
|
search_regex: Optional[str],
|
|
850
915
|
by_longname: Tuple[str, ...],
|
|
851
916
|
exclude_by_longname: Tuple[str, ...],
|
|
917
|
+
by_test_metadata: Tuple[str, ...],
|
|
918
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
852
919
|
robot_options_and_args: Tuple[str, ...],
|
|
853
920
|
) -> None:
|
|
854
921
|
"""\
|
|
@@ -868,7 +935,13 @@ def suites(
|
|
|
868
935
|
|
|
869
936
|
matcher = make_search_matcher(search_substring, search_regex)
|
|
870
937
|
_suite, collector, diagnostics = handle_options(
|
|
871
|
-
app,
|
|
938
|
+
app,
|
|
939
|
+
by_longname,
|
|
940
|
+
exclude_by_longname,
|
|
941
|
+
robot_options_and_args,
|
|
942
|
+
search_matcher=matcher,
|
|
943
|
+
by_test_metadata=by_test_metadata,
|
|
944
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
872
945
|
)
|
|
873
946
|
|
|
874
947
|
if collector.all.children:
|
|
@@ -890,7 +963,8 @@ def suites(
|
|
|
890
963
|
else:
|
|
891
964
|
app.print_data(
|
|
892
965
|
ResultItem(
|
|
893
|
-
|
|
966
|
+
# a flat list: the suites without their nested suites and tests
|
|
967
|
+
[dataclasses.replace(suite, children=None) for suite in collector.suites],
|
|
894
968
|
diagnostics,
|
|
895
969
|
filters_applied=_filters_applied(search_substring, search_regex),
|
|
896
970
|
supports_parse_include=SUPPORTS_PARSE_INCLUDE,
|
|
@@ -945,6 +1019,8 @@ def tags(
|
|
|
945
1019
|
search_regex: Optional[str],
|
|
946
1020
|
by_longname: Tuple[str, ...],
|
|
947
1021
|
exclude_by_longname: Tuple[str, ...],
|
|
1022
|
+
by_test_metadata: Tuple[str, ...],
|
|
1023
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
948
1024
|
robot_options_and_args: Tuple[str, ...],
|
|
949
1025
|
) -> None:
|
|
950
1026
|
"""\
|
|
@@ -965,7 +1041,13 @@ def tags(
|
|
|
965
1041
|
|
|
966
1042
|
matcher = make_search_matcher(search_substring, search_regex)
|
|
967
1043
|
_suite, collector, diagnostics = handle_options(
|
|
968
|
-
app,
|
|
1044
|
+
app,
|
|
1045
|
+
by_longname,
|
|
1046
|
+
exclude_by_longname,
|
|
1047
|
+
robot_options_and_args,
|
|
1048
|
+
search_matcher=matcher,
|
|
1049
|
+
by_test_metadata=by_test_metadata,
|
|
1050
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
969
1051
|
)
|
|
970
1052
|
|
|
971
1053
|
if collector.all.children:
|
|
@@ -995,6 +1077,149 @@ def tags(
|
|
|
995
1077
|
)
|
|
996
1078
|
|
|
997
1079
|
|
|
1080
|
+
def _metadata_index(
|
|
1081
|
+
items: Iterable[Tuple[TestItem, Optional[Mapping[str, str]]]],
|
|
1082
|
+
) -> Dict[str, Dict[str, List[TestItem]]]:
|
|
1083
|
+
"""name → value → items, built from the same entries `--by-test-metadata`
|
|
1084
|
+
matches. Names and values that compare equal are one entry, shown in the
|
|
1085
|
+
first spelling found; both are sorted by their normalised form."""
|
|
1086
|
+
names: Dict[str, Tuple[str, Dict[str, Tuple[str, List[TestItem]]]]] = {}
|
|
1087
|
+
for item, metadata in items:
|
|
1088
|
+
for name, line in metadata_entries(metadata or {}):
|
|
1089
|
+
_name, values = names.setdefault(normalize(name, ignore="_"), (name, {}))
|
|
1090
|
+
_value, value_items = values.setdefault(normalize(line, ignore="_"), (line, []))
|
|
1091
|
+
if not value_items or value_items[-1] is not item:
|
|
1092
|
+
value_items.append(item)
|
|
1093
|
+
return {
|
|
1094
|
+
name: {value: value_items for _, (value, value_items) in sorted(values.items())}
|
|
1095
|
+
for _, (name, values) in sorted(names.items())
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
@discover.command(
|
|
1100
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
1101
|
+
add_help_option=True,
|
|
1102
|
+
epilog="Use `-- --help` to see `robot` help.",
|
|
1103
|
+
# test metadata exists since Robot Framework 7.5
|
|
1104
|
+
cls=RobotVersionCommand,
|
|
1105
|
+
since=(7, 5),
|
|
1106
|
+
)
|
|
1107
|
+
@click.option(
|
|
1108
|
+
"--values / --no-values",
|
|
1109
|
+
"show_values",
|
|
1110
|
+
default=False,
|
|
1111
|
+
show_default=True,
|
|
1112
|
+
help="Show the values of each metadata name.",
|
|
1113
|
+
)
|
|
1114
|
+
@click.option(
|
|
1115
|
+
"--tests / --no-tests",
|
|
1116
|
+
"show_tests",
|
|
1117
|
+
default=False,
|
|
1118
|
+
show_default=True,
|
|
1119
|
+
help="Show the tests that have the metadata.",
|
|
1120
|
+
)
|
|
1121
|
+
@click.option(
|
|
1122
|
+
"--tasks / --no-tasks",
|
|
1123
|
+
"show_tasks",
|
|
1124
|
+
default=False,
|
|
1125
|
+
show_default=True,
|
|
1126
|
+
help="Show the tasks that have the metadata.",
|
|
1127
|
+
)
|
|
1128
|
+
@click.option(
|
|
1129
|
+
"--suites / --no-suites",
|
|
1130
|
+
"show_suites",
|
|
1131
|
+
default=False,
|
|
1132
|
+
show_default=True,
|
|
1133
|
+
help="Show the suites that have the metadata.",
|
|
1134
|
+
)
|
|
1135
|
+
@click.option(
|
|
1136
|
+
"--full-paths / --no-full-paths",
|
|
1137
|
+
"full_paths",
|
|
1138
|
+
default=False,
|
|
1139
|
+
show_default=True,
|
|
1140
|
+
help="Show full paths instead of relative.",
|
|
1141
|
+
)
|
|
1142
|
+
@add_options(*ROBOT_OPTIONS)
|
|
1143
|
+
@add_options(*DISCOVER_SEARCH_OPTIONS)
|
|
1144
|
+
@pass_application
|
|
1145
|
+
def metadata(
|
|
1146
|
+
app: Application,
|
|
1147
|
+
show_values: bool,
|
|
1148
|
+
show_tests: bool,
|
|
1149
|
+
show_tasks: bool,
|
|
1150
|
+
show_suites: bool,
|
|
1151
|
+
full_paths: bool,
|
|
1152
|
+
search_substring: Optional[str],
|
|
1153
|
+
search_regex: Optional[str],
|
|
1154
|
+
by_longname: Tuple[str, ...],
|
|
1155
|
+
exclude_by_longname: Tuple[str, ...],
|
|
1156
|
+
by_test_metadata: Tuple[str, ...],
|
|
1157
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
1158
|
+
robot_options_and_args: Tuple[str, ...],
|
|
1159
|
+
) -> None:
|
|
1160
|
+
"""\
|
|
1161
|
+
Discover the metadata of tests, tasks and suites with the selected
|
|
1162
|
+
configuration, profiles, options and arguments.
|
|
1163
|
+
|
|
1164
|
+
Test and task metadata is what `--by-test-metadata` selects by; suite
|
|
1165
|
+
metadata, including `--metadata` given to Robot, is listed in its own
|
|
1166
|
+
section.
|
|
1167
|
+
|
|
1168
|
+
\b
|
|
1169
|
+
Examples:
|
|
1170
|
+
```
|
|
1171
|
+
robotcode discover metadata
|
|
1172
|
+
robotcode discover metadata --values --tests
|
|
1173
|
+
robotcode --format json discover metadata
|
|
1174
|
+
```
|
|
1175
|
+
"""
|
|
1176
|
+
|
|
1177
|
+
matcher = make_search_matcher(search_substring, search_regex)
|
|
1178
|
+
_suite, collector, diagnostics = handle_options(
|
|
1179
|
+
app,
|
|
1180
|
+
by_longname,
|
|
1181
|
+
exclude_by_longname,
|
|
1182
|
+
robot_options_and_args,
|
|
1183
|
+
search_matcher=matcher,
|
|
1184
|
+
by_test_metadata=by_test_metadata,
|
|
1185
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
if collector.all.children:
|
|
1189
|
+
test_metadata = _metadata_index((item, item.metadata) for item in collector.test_and_tasks)
|
|
1190
|
+
suite_metadata = _metadata_index(collector.suite_metadata)
|
|
1191
|
+
|
|
1192
|
+
if app.config.output_format is None or app.config.output_format == OutputFormat.TEXT:
|
|
1193
|
+
app.echo_as_markdown(
|
|
1194
|
+
_render.render_metadata(
|
|
1195
|
+
test_metadata,
|
|
1196
|
+
suite_metadata,
|
|
1197
|
+
collector.statistics,
|
|
1198
|
+
show_values=show_values,
|
|
1199
|
+
show_tests=show_tests,
|
|
1200
|
+
show_tasks=show_tasks,
|
|
1201
|
+
show_suites=show_suites,
|
|
1202
|
+
full_paths=full_paths,
|
|
1203
|
+
highlight=make_md_highlighter(search_substring, search_regex),
|
|
1204
|
+
search_substring=search_substring,
|
|
1205
|
+
search_regex=search_regex,
|
|
1206
|
+
diagnostics=diagnostics,
|
|
1207
|
+
show_diagnostics=app.show_diagnostics,
|
|
1208
|
+
root_folder=app.root_folder,
|
|
1209
|
+
)
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
else:
|
|
1213
|
+
app.print_data(
|
|
1214
|
+
MetadataResult(
|
|
1215
|
+
test_metadata,
|
|
1216
|
+
suite_metadata,
|
|
1217
|
+
filters_applied=_filters_applied(search_substring, search_regex),
|
|
1218
|
+
),
|
|
1219
|
+
remove_defaults=True,
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
|
|
998
1223
|
@discover.command(add_help_option=True)
|
|
999
1224
|
@pass_application
|
|
1000
1225
|
def info(app: Application) -> None:
|
|
@@ -1086,8 +1311,10 @@ def files(
|
|
|
1086
1311
|
# Match the other discover subcommands: chdir into root_folder so
|
|
1087
1312
|
# `get_rel_source` produces paths relative to the project root rather
|
|
1088
1313
|
# than to whatever the caller's process cwd happens to be.
|
|
1089
|
-
with app.chdir(root_folder):
|
|
1090
|
-
|
|
1314
|
+
with app.chdir(root_folder) as orig_folder:
|
|
1315
|
+
# Without duplicates, in the given order: the files come out in the order of the paths. Paths from the
|
|
1316
|
+
# command line count from the folder the command was started in, the configured ones from the root.
|
|
1317
|
+
search_paths = dict.fromkeys(
|
|
1091
1318
|
(
|
|
1092
1319
|
(
|
|
1093
1320
|
[*(app.config.default_paths if app.config.default_paths else ())]
|
|
@@ -1097,7 +1324,7 @@ def files(
|
|
|
1097
1324
|
else [profile.paths]
|
|
1098
1325
|
)
|
|
1099
1326
|
if not paths
|
|
1100
|
-
else [str(p) for p in paths]
|
|
1327
|
+
else [str(p if orig_folder is None else Path(orig_folder, p)) for p in paths]
|
|
1101
1328
|
)
|
|
1102
1329
|
)
|
|
1103
1330
|
if not search_paths:
|
robotcode/runner/cli/libdoc.py
CHANGED
|
@@ -6,6 +6,7 @@ from robot.libdoc import USAGE, LibDoc
|
|
|
6
6
|
from robot.version import get_full_version
|
|
7
7
|
|
|
8
8
|
from robotcode.plugin import Application, pass_application
|
|
9
|
+
from robotcode.plugin.click_helper.exit_codes import robot_exit_codes
|
|
9
10
|
from robotcode.robot.config.loader import load_robot_config_from_path
|
|
10
11
|
from robotcode.robot.config.model import LibDocProfile
|
|
11
12
|
from robotcode.robot.config.utils import get_config_files
|
|
@@ -35,6 +36,7 @@ class LibDocEx(LibDoc):
|
|
|
35
36
|
return super().main(arguments, **options)
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
@robot_exit_codes
|
|
38
40
|
@click.command(
|
|
39
41
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
40
42
|
add_help_option=True,
|
|
@@ -80,11 +82,11 @@ def libdoc(app: Application, robot_options_and_args: Tuple[str, ...]) -> None:
|
|
|
80
82
|
except (TypeError, ValueError) as e:
|
|
81
83
|
raise click.ClickException(str(e)) from e
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
# Of the settings outside of `[libdoc]`, only `python-path` means the same for `libdoc`; `name` and `quiet`
|
|
86
|
+
# there are `robot` options. `[libdoc]` comes last, so it wins and its `extend-` settings add to it.
|
|
87
|
+
libdoc_options = LibDocProfile(python_path=profile.python_path)
|
|
88
|
+
if profile.libdoc is not None:
|
|
89
|
+
libdoc_options.add_options(profile.libdoc)
|
|
88
90
|
|
|
89
91
|
options = libdoc_options.build_command_line()
|
|
90
92
|
|
robotcode/runner/cli/rebot.py
CHANGED
|
@@ -7,6 +7,7 @@ from robot.rebot import USAGE, Rebot
|
|
|
7
7
|
from robot.version import get_full_version
|
|
8
8
|
|
|
9
9
|
from robotcode.plugin import Application, pass_application
|
|
10
|
+
from robotcode.plugin.click_helper.exit_codes import robot_exit_codes
|
|
10
11
|
from robotcode.robot.config.loader import load_robot_config_from_path
|
|
11
12
|
from robotcode.robot.config.model import RebotProfile
|
|
12
13
|
from robotcode.robot.config.utils import get_config_files
|
|
@@ -14,6 +15,25 @@ from robotcode.robot.utils import RF_VERSION
|
|
|
14
15
|
|
|
15
16
|
from ..__version__ import __version__
|
|
16
17
|
|
|
18
|
+
# The settings that Robot Framework does not pass from `robot` to `rebot` after a run (`not_copied` in
|
|
19
|
+
# `RobotSettings.get_rebot_settings`).
|
|
20
|
+
NOT_PASSED_TO_REBOT = (
|
|
21
|
+
"includes",
|
|
22
|
+
"excludes",
|
|
23
|
+
"tests",
|
|
24
|
+
"suites",
|
|
25
|
+
"parse_include",
|
|
26
|
+
"name",
|
|
27
|
+
"doc",
|
|
28
|
+
"metadata",
|
|
29
|
+
"set_tag",
|
|
30
|
+
"output",
|
|
31
|
+
"log_level",
|
|
32
|
+
"timestamp_outputs",
|
|
33
|
+
"console",
|
|
34
|
+
"quiet",
|
|
35
|
+
)
|
|
36
|
+
|
|
17
37
|
|
|
18
38
|
class RebotEx(Rebot):
|
|
19
39
|
def __init__(self, dry: bool) -> None:
|
|
@@ -27,7 +47,7 @@ class RebotEx(Rebot):
|
|
|
27
47
|
line_end = "\n"
|
|
28
48
|
raise Information(
|
|
29
49
|
"Dry run, not executing any commands. "
|
|
30
|
-
f"Would execute
|
|
50
|
+
f"Would execute rebot with the following options and arguments:\n"
|
|
31
51
|
f"{line_end.join((*(f'{k} = {v!r}' for k, v in options.items()), *arguments))}"
|
|
32
52
|
)
|
|
33
53
|
|
|
@@ -37,6 +57,7 @@ class RebotEx(Rebot):
|
|
|
37
57
|
return super().main(datasources, **options)
|
|
38
58
|
|
|
39
59
|
|
|
60
|
+
@robot_exit_codes
|
|
40
61
|
@click.command(
|
|
41
62
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
42
63
|
add_help_option=True,
|
|
@@ -82,11 +103,17 @@ def rebot(app: Application, robot_options_and_args: Tuple[str, ...]) -> None:
|
|
|
82
103
|
except (TypeError, ValueError) as e:
|
|
83
104
|
raise click.ClickException(str(e)) from e
|
|
84
105
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
# Of the settings outside of `[rebot]`, `rebot` gets the ones that Robot Framework passes to it after a
|
|
107
|
+
# run; `[rebot]` comes last, so it wins and its `extend-` settings add to them.
|
|
108
|
+
rebot_options = RebotProfile()
|
|
89
109
|
rebot_options.add_options(profile)
|
|
110
|
+
for name in NOT_PASSED_TO_REBOT:
|
|
111
|
+
setattr(rebot_options, name, None)
|
|
112
|
+
# as Robot Framework does, `run-empty-suite` becomes `process-empty-suite`
|
|
113
|
+
if profile.run_empty_suite is not None:
|
|
114
|
+
rebot_options.process_empty_suite = profile.run_empty_suite
|
|
115
|
+
if profile.rebot is not None:
|
|
116
|
+
rebot_options.add_options(profile.rebot)
|
|
90
117
|
|
|
91
118
|
try:
|
|
92
119
|
options = rebot_options.build_command_line()
|