robotcode-runner 2.6.2__py3-none-any.whl → 2.8.0.dev548__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 +12 -2
- robotcode/runner/cli/discover/_render.py +108 -2
- robotcode/runner/cli/discover/discover.py +267 -36
- robotcode/runner/cli/libdoc.py +7 -5
- robotcode/runner/cli/rebot.py +32 -5
- robotcode/runner/cli/results/_models.py +4 -0
- robotcode/runner/cli/results/_render.py +9 -1
- robotcode/runner/cli/results/results.py +195 -43
- robotcode/runner/cli/robot.py +58 -8
- robotcode/runner/cli/testdoc.py +7 -4
- {robotcode_runner-2.6.2.dist-info → robotcode_runner-2.8.0.dev548.dist-info}/METADATA +7 -6
- robotcode_runner-2.8.0.dev548.dist-info/RECORD +25 -0
- {robotcode_runner-2.6.2.dist-info → robotcode_runner-2.8.0.dev548.dist-info}/WHEEL +1 -1
- robotcode_runner-2.6.2.dist-info/RECORD +0 -24
- {robotcode_runner-2.6.2.dist-info → robotcode_runner-2.8.0.dev548.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,22 @@ 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
|
|
65
|
+
|
|
66
|
+
# Robot Framework 6.1 introduced `--parseinclude`. Before that, the `--suite`
|
|
67
|
+
# option implicitly restricted which files were parsed, so an explicit include
|
|
68
|
+
# was neither available nor needed. Editors use this to decide whether to pass
|
|
69
|
+
# `-I` when running a subset of tests.
|
|
70
|
+
SUPPORTS_PARSE_INCLUDE = RF_VERSION >= (6, 1)
|
|
56
71
|
|
|
57
72
|
DISCOVER_SEARCH_OPTIONS = [
|
|
58
73
|
click.option(
|
|
@@ -63,8 +78,9 @@ DISCOVER_SEARCH_OPTIONS = [
|
|
|
63
78
|
help=(
|
|
64
79
|
"Only include items where TEXT case-insensitively matches the "
|
|
65
80
|
"name, full name, source path, documentation, template name, "
|
|
66
|
-
"timeout, any tag (normalisation-aware),
|
|
67
|
-
"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 "
|
|
68
84
|
"— keyword names, keyword arguments, assigned variables, "
|
|
69
85
|
"FOR/WHILE/IF conditions, VAR/RETURN values, EXCEPT patterns, "
|
|
70
86
|
"GROUP names. Mutually exclusive with `--search-regex`."
|
|
@@ -252,13 +268,14 @@ class Collector(SuiteVisitor):
|
|
|
252
268
|
uri=str(Uri.from_path(absolute_path)),
|
|
253
269
|
source=str(absolute_path),
|
|
254
270
|
rel_source=get_rel_source(absolute_path),
|
|
255
|
-
needs_parse_include=RF_VERSION >= (6, 1),
|
|
256
271
|
)
|
|
257
272
|
self._current = self.all
|
|
258
273
|
self.suites: List[TestItem] = []
|
|
259
274
|
self.test_and_tasks: List[TestItem] = []
|
|
260
275
|
self.tags: Dict[str, List[TestItem]] = defaultdict(list)
|
|
261
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]]] = []
|
|
262
279
|
self.statistics = Statistics()
|
|
263
280
|
self._collected: List[MutableMapping[str, Any]] = [NormalizedDict(ignore="_")]
|
|
264
281
|
|
|
@@ -284,7 +301,7 @@ class Collector(SuiteVisitor):
|
|
|
284
301
|
name=suite.name,
|
|
285
302
|
longname=suite.longname,
|
|
286
303
|
uri=str(Uri.from_path(absolute_path)) if absolute_path else None,
|
|
287
|
-
source=str(suite.source),
|
|
304
|
+
source=str(suite.source) if suite.source else None,
|
|
288
305
|
rel_source=get_rel_source(suite.source),
|
|
289
306
|
range=(
|
|
290
307
|
Range(
|
|
@@ -302,6 +319,9 @@ class Collector(SuiteVisitor):
|
|
|
302
319
|
raise ValueError(f"Error while parsing suite {suite.source}: {e}") from e
|
|
303
320
|
|
|
304
321
|
self.suites.append(item)
|
|
322
|
+
suite_metadata = named_metadata(suite.metadata)
|
|
323
|
+
if suite_metadata:
|
|
324
|
+
self.suite_metadata.append((item, suite_metadata))
|
|
305
325
|
|
|
306
326
|
if self._current.children is None:
|
|
307
327
|
self._current.children = []
|
|
@@ -351,6 +371,7 @@ class Collector(SuiteVisitor):
|
|
|
351
371
|
end=Position(line=test.lineno - 1, character=0),
|
|
352
372
|
),
|
|
353
373
|
tags=sorted(set(normalize(str(t), ignore="_") for t in test.tags)) if test.tags else None,
|
|
374
|
+
metadata=get_test_metadata(test) or None,
|
|
354
375
|
rpa=self._current.rpa,
|
|
355
376
|
)
|
|
356
377
|
except ValueError as e:
|
|
@@ -472,6 +493,8 @@ def handle_options(
|
|
|
472
493
|
exclude_by_longname: Tuple[str, ...],
|
|
473
494
|
robot_options_and_args: Tuple[str, ...],
|
|
474
495
|
search_matcher: Optional[SearchMatcher] = None,
|
|
496
|
+
by_test_metadata: Tuple[str, ...] = (),
|
|
497
|
+
exclude_by_test_metadata: Tuple[str, ...] = (),
|
|
475
498
|
) -> Tuple[TestSuite, Collector, Optional[Dict[str, List[Diagnostic]]]]:
|
|
476
499
|
root_folder, profile, cmd_options = handle_robot_options(app, robot_options_and_args)
|
|
477
500
|
|
|
@@ -495,6 +518,8 @@ def handle_options(
|
|
|
495
518
|
by_longname,
|
|
496
519
|
exclude_by_longname,
|
|
497
520
|
search_matcher=search_matcher,
|
|
521
|
+
by_test_metadata=by_test_metadata,
|
|
522
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
498
523
|
).parse_arguments((*cmd_options, "--runemptysuite", *robot_options_and_args))
|
|
499
524
|
|
|
500
525
|
settings = RobotSettings(options)
|
|
@@ -567,6 +592,29 @@ def handle_options(
|
|
|
567
592
|
raise UnknownError("Unexpected error happened.")
|
|
568
593
|
|
|
569
594
|
|
|
595
|
+
def _show_options(*, default: bool) -> List[Any]:
|
|
596
|
+
"""The flags of `all`, `tests` and `tasks` that add the tags and metadata lines in TEXT mode."""
|
|
597
|
+
return [
|
|
598
|
+
click.option(
|
|
599
|
+
"--show-tags / --no-show-tags",
|
|
600
|
+
"show_tags",
|
|
601
|
+
default=default,
|
|
602
|
+
show_default=True,
|
|
603
|
+
help="Show the tags that are present.",
|
|
604
|
+
),
|
|
605
|
+
click.option(
|
|
606
|
+
"--show-metadata / --no-show-metadata",
|
|
607
|
+
"show_metadata",
|
|
608
|
+
default=default,
|
|
609
|
+
show_default=True,
|
|
610
|
+
# test metadata exists since Robot Framework 7.5
|
|
611
|
+
cls=RobotVersionOption,
|
|
612
|
+
since=(7, 5),
|
|
613
|
+
help="Show the metadata of tests and tasks.",
|
|
614
|
+
),
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
|
|
570
618
|
def _filters_applied(search_substring: Optional[str], search_regex: Optional[str]) -> Optional[Dict[str, str]]:
|
|
571
619
|
if search_substring:
|
|
572
620
|
return {"search": search_substring}
|
|
@@ -580,13 +628,7 @@ def _filters_applied(search_substring: Optional[str], search_regex: Optional[str
|
|
|
580
628
|
add_help_option=True,
|
|
581
629
|
epilog="Use `-- --help` to see `robot` help.",
|
|
582
630
|
)
|
|
583
|
-
@
|
|
584
|
-
"--tags / --no-tags",
|
|
585
|
-
"show_tags",
|
|
586
|
-
default=True,
|
|
587
|
-
show_default=True,
|
|
588
|
-
help="Show the tags that are present.",
|
|
589
|
-
)
|
|
631
|
+
@add_options(*_show_options(default=True))
|
|
590
632
|
@add_options(*ROBOT_OPTIONS)
|
|
591
633
|
@add_options(*DISCOVER_SEARCH_OPTIONS)
|
|
592
634
|
@click.option(
|
|
@@ -601,10 +643,13 @@ def all(
|
|
|
601
643
|
app: Application,
|
|
602
644
|
full_paths: bool,
|
|
603
645
|
show_tags: bool,
|
|
646
|
+
show_metadata: bool,
|
|
604
647
|
search_substring: Optional[str],
|
|
605
648
|
search_regex: Optional[str],
|
|
606
649
|
by_longname: Tuple[str, ...],
|
|
607
650
|
exclude_by_longname: Tuple[str, ...],
|
|
651
|
+
by_test_metadata: Tuple[str, ...],
|
|
652
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
608
653
|
robot_options_and_args: Tuple[str, ...],
|
|
609
654
|
) -> None:
|
|
610
655
|
"""\
|
|
@@ -624,7 +669,13 @@ def all(
|
|
|
624
669
|
|
|
625
670
|
matcher = make_search_matcher(search_substring, search_regex)
|
|
626
671
|
_suite, collector, diagnostics = handle_options(
|
|
627
|
-
app,
|
|
672
|
+
app,
|
|
673
|
+
by_longname,
|
|
674
|
+
exclude_by_longname,
|
|
675
|
+
robot_options_and_args,
|
|
676
|
+
search_matcher=matcher,
|
|
677
|
+
by_test_metadata=by_test_metadata,
|
|
678
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
628
679
|
)
|
|
629
680
|
|
|
630
681
|
if collector.all.children:
|
|
@@ -634,6 +685,7 @@ def all(
|
|
|
634
685
|
collector.all.children[0],
|
|
635
686
|
collector.statistics,
|
|
636
687
|
show_tags=show_tags,
|
|
688
|
+
show_metadata=show_metadata,
|
|
637
689
|
full_paths=full_paths,
|
|
638
690
|
highlight=make_md_highlighter(search_substring, search_regex),
|
|
639
691
|
search_substring=search_substring,
|
|
@@ -647,7 +699,10 @@ def all(
|
|
|
647
699
|
else:
|
|
648
700
|
app.print_data(
|
|
649
701
|
ResultItem(
|
|
650
|
-
[collector.all],
|
|
702
|
+
[collector.all],
|
|
703
|
+
diagnostics,
|
|
704
|
+
filters_applied=_filters_applied(search_substring, search_regex),
|
|
705
|
+
supports_parse_include=SUPPORTS_PARSE_INCLUDE,
|
|
651
706
|
),
|
|
652
707
|
remove_defaults=True,
|
|
653
708
|
)
|
|
@@ -658,15 +713,24 @@ def _test_or_tasks(
|
|
|
658
713
|
app: Application,
|
|
659
714
|
full_paths: bool,
|
|
660
715
|
show_tags: bool,
|
|
716
|
+
show_metadata: bool,
|
|
661
717
|
search_substring: Optional[str],
|
|
662
718
|
search_regex: Optional[str],
|
|
663
719
|
by_longname: Tuple[str, ...],
|
|
664
720
|
exclude_by_longname: Tuple[str, ...],
|
|
721
|
+
by_test_metadata: Tuple[str, ...],
|
|
722
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
665
723
|
robot_options_and_args: Tuple[str, ...],
|
|
666
724
|
) -> None:
|
|
667
725
|
matcher = make_search_matcher(search_substring, search_regex)
|
|
668
726
|
_suite, collector, diagnostics = handle_options(
|
|
669
|
-
app,
|
|
727
|
+
app,
|
|
728
|
+
by_longname,
|
|
729
|
+
exclude_by_longname,
|
|
730
|
+
robot_options_and_args,
|
|
731
|
+
search_matcher=matcher,
|
|
732
|
+
by_test_metadata=by_test_metadata,
|
|
733
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
670
734
|
)
|
|
671
735
|
|
|
672
736
|
if collector.all.children:
|
|
@@ -677,6 +741,7 @@ def _test_or_tasks(
|
|
|
677
741
|
collector.statistics,
|
|
678
742
|
selected_type=selected_type,
|
|
679
743
|
show_tags=show_tags,
|
|
744
|
+
show_metadata=show_metadata,
|
|
680
745
|
full_paths=full_paths,
|
|
681
746
|
highlight=make_md_highlighter(search_substring, search_regex),
|
|
682
747
|
search_substring=search_substring,
|
|
@@ -694,6 +759,7 @@ def _test_or_tasks(
|
|
|
694
759
|
filtered,
|
|
695
760
|
diagnostics,
|
|
696
761
|
filters_applied=_filters_applied(search_substring, search_regex),
|
|
762
|
+
supports_parse_include=SUPPORTS_PARSE_INCLUDE,
|
|
697
763
|
),
|
|
698
764
|
remove_defaults=True,
|
|
699
765
|
)
|
|
@@ -704,13 +770,7 @@ def _test_or_tasks(
|
|
|
704
770
|
add_help_option=True,
|
|
705
771
|
epilog="Use `-- --help` to see `robot` help.",
|
|
706
772
|
)
|
|
707
|
-
@
|
|
708
|
-
"--tags / --no-tags",
|
|
709
|
-
"show_tags",
|
|
710
|
-
default=False,
|
|
711
|
-
show_default=True,
|
|
712
|
-
help="Show the tags that are present.",
|
|
713
|
-
)
|
|
773
|
+
@add_options(*_show_options(default=False))
|
|
714
774
|
@click.option(
|
|
715
775
|
"--full-paths / --no-full-paths",
|
|
716
776
|
"full_paths",
|
|
@@ -725,10 +785,13 @@ def tests(
|
|
|
725
785
|
app: Application,
|
|
726
786
|
full_paths: bool,
|
|
727
787
|
show_tags: bool,
|
|
788
|
+
show_metadata: bool,
|
|
728
789
|
search_substring: Optional[str],
|
|
729
790
|
search_regex: Optional[str],
|
|
730
791
|
by_longname: Tuple[str, ...],
|
|
731
792
|
exclude_by_longname: Tuple[str, ...],
|
|
793
|
+
by_test_metadata: Tuple[str, ...],
|
|
794
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
732
795
|
robot_options_and_args: Tuple[str, ...],
|
|
733
796
|
) -> None:
|
|
734
797
|
"""\
|
|
@@ -751,10 +814,13 @@ def tests(
|
|
|
751
814
|
app,
|
|
752
815
|
full_paths,
|
|
753
816
|
show_tags,
|
|
817
|
+
show_metadata,
|
|
754
818
|
search_substring,
|
|
755
819
|
search_regex,
|
|
756
820
|
by_longname,
|
|
757
821
|
exclude_by_longname,
|
|
822
|
+
by_test_metadata,
|
|
823
|
+
exclude_by_test_metadata,
|
|
758
824
|
robot_options_and_args,
|
|
759
825
|
)
|
|
760
826
|
|
|
@@ -764,13 +830,7 @@ def tests(
|
|
|
764
830
|
add_help_option=True,
|
|
765
831
|
epilog="Use `-- --help` to see `robot` help.",
|
|
766
832
|
)
|
|
767
|
-
@
|
|
768
|
-
"--tags / --no-tags",
|
|
769
|
-
"show_tags",
|
|
770
|
-
default=False,
|
|
771
|
-
show_default=True,
|
|
772
|
-
help="Show the tags that are present.",
|
|
773
|
-
)
|
|
833
|
+
@add_options(*_show_options(default=False))
|
|
774
834
|
@click.option(
|
|
775
835
|
"--full-paths / --no-full-paths",
|
|
776
836
|
"full_paths",
|
|
@@ -785,10 +845,13 @@ def tasks(
|
|
|
785
845
|
app: Application,
|
|
786
846
|
full_paths: bool,
|
|
787
847
|
show_tags: bool,
|
|
848
|
+
show_metadata: bool,
|
|
788
849
|
search_substring: Optional[str],
|
|
789
850
|
search_regex: Optional[str],
|
|
790
851
|
by_longname: Tuple[str, ...],
|
|
791
852
|
exclude_by_longname: Tuple[str, ...],
|
|
853
|
+
by_test_metadata: Tuple[str, ...],
|
|
854
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
792
855
|
robot_options_and_args: Tuple[str, ...],
|
|
793
856
|
) -> None:
|
|
794
857
|
"""\
|
|
@@ -810,10 +873,13 @@ def tasks(
|
|
|
810
873
|
app,
|
|
811
874
|
full_paths,
|
|
812
875
|
show_tags,
|
|
876
|
+
show_metadata,
|
|
813
877
|
search_substring,
|
|
814
878
|
search_regex,
|
|
815
879
|
by_longname,
|
|
816
880
|
exclude_by_longname,
|
|
881
|
+
by_test_metadata,
|
|
882
|
+
exclude_by_test_metadata,
|
|
817
883
|
robot_options_and_args,
|
|
818
884
|
)
|
|
819
885
|
|
|
@@ -840,6 +906,8 @@ def suites(
|
|
|
840
906
|
search_regex: Optional[str],
|
|
841
907
|
by_longname: Tuple[str, ...],
|
|
842
908
|
exclude_by_longname: Tuple[str, ...],
|
|
909
|
+
by_test_metadata: Tuple[str, ...],
|
|
910
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
843
911
|
robot_options_and_args: Tuple[str, ...],
|
|
844
912
|
) -> None:
|
|
845
913
|
"""\
|
|
@@ -859,7 +927,13 @@ def suites(
|
|
|
859
927
|
|
|
860
928
|
matcher = make_search_matcher(search_substring, search_regex)
|
|
861
929
|
_suite, collector, diagnostics = handle_options(
|
|
862
|
-
app,
|
|
930
|
+
app,
|
|
931
|
+
by_longname,
|
|
932
|
+
exclude_by_longname,
|
|
933
|
+
robot_options_and_args,
|
|
934
|
+
search_matcher=matcher,
|
|
935
|
+
by_test_metadata=by_test_metadata,
|
|
936
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
863
937
|
)
|
|
864
938
|
|
|
865
939
|
if collector.all.children:
|
|
@@ -881,7 +955,11 @@ def suites(
|
|
|
881
955
|
else:
|
|
882
956
|
app.print_data(
|
|
883
957
|
ResultItem(
|
|
884
|
-
|
|
958
|
+
# a flat list: the suites without their nested suites and tests
|
|
959
|
+
[dataclasses.replace(suite, children=None) for suite in collector.suites],
|
|
960
|
+
diagnostics,
|
|
961
|
+
filters_applied=_filters_applied(search_substring, search_regex),
|
|
962
|
+
supports_parse_include=SUPPORTS_PARSE_INCLUDE,
|
|
885
963
|
),
|
|
886
964
|
remove_defaults=True,
|
|
887
965
|
)
|
|
@@ -933,6 +1011,8 @@ def tags(
|
|
|
933
1011
|
search_regex: Optional[str],
|
|
934
1012
|
by_longname: Tuple[str, ...],
|
|
935
1013
|
exclude_by_longname: Tuple[str, ...],
|
|
1014
|
+
by_test_metadata: Tuple[str, ...],
|
|
1015
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
936
1016
|
robot_options_and_args: Tuple[str, ...],
|
|
937
1017
|
) -> None:
|
|
938
1018
|
"""\
|
|
@@ -953,7 +1033,13 @@ def tags(
|
|
|
953
1033
|
|
|
954
1034
|
matcher = make_search_matcher(search_substring, search_regex)
|
|
955
1035
|
_suite, collector, diagnostics = handle_options(
|
|
956
|
-
app,
|
|
1036
|
+
app,
|
|
1037
|
+
by_longname,
|
|
1038
|
+
exclude_by_longname,
|
|
1039
|
+
robot_options_and_args,
|
|
1040
|
+
search_matcher=matcher,
|
|
1041
|
+
by_test_metadata=by_test_metadata,
|
|
1042
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
957
1043
|
)
|
|
958
1044
|
|
|
959
1045
|
if collector.all.children:
|
|
@@ -983,6 +1069,149 @@ def tags(
|
|
|
983
1069
|
)
|
|
984
1070
|
|
|
985
1071
|
|
|
1072
|
+
def _metadata_index(
|
|
1073
|
+
items: Iterable[Tuple[TestItem, Optional[Mapping[str, str]]]],
|
|
1074
|
+
) -> Dict[str, Dict[str, List[TestItem]]]:
|
|
1075
|
+
"""name → value → items, built from the same entries `--by-test-metadata`
|
|
1076
|
+
matches. Names and values that compare equal are one entry, shown in the
|
|
1077
|
+
first spelling found; both are sorted by their normalised form."""
|
|
1078
|
+
names: Dict[str, Tuple[str, Dict[str, Tuple[str, List[TestItem]]]]] = {}
|
|
1079
|
+
for item, metadata in items:
|
|
1080
|
+
for name, line in metadata_entries(metadata or {}):
|
|
1081
|
+
_name, values = names.setdefault(normalize(name, ignore="_"), (name, {}))
|
|
1082
|
+
_value, value_items = values.setdefault(normalize(line, ignore="_"), (line, []))
|
|
1083
|
+
if not value_items or value_items[-1] is not item:
|
|
1084
|
+
value_items.append(item)
|
|
1085
|
+
return {
|
|
1086
|
+
name: {value: value_items for _, (value, value_items) in sorted(values.items())}
|
|
1087
|
+
for _, (name, values) in sorted(names.items())
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
@discover.command(
|
|
1092
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
1093
|
+
add_help_option=True,
|
|
1094
|
+
epilog="Use `-- --help` to see `robot` help.",
|
|
1095
|
+
# test metadata exists since Robot Framework 7.5
|
|
1096
|
+
cls=RobotVersionCommand,
|
|
1097
|
+
since=(7, 5),
|
|
1098
|
+
)
|
|
1099
|
+
@click.option(
|
|
1100
|
+
"--values / --no-values",
|
|
1101
|
+
"show_values",
|
|
1102
|
+
default=False,
|
|
1103
|
+
show_default=True,
|
|
1104
|
+
help="Show the values of each metadata name.",
|
|
1105
|
+
)
|
|
1106
|
+
@click.option(
|
|
1107
|
+
"--tests / --no-tests",
|
|
1108
|
+
"show_tests",
|
|
1109
|
+
default=False,
|
|
1110
|
+
show_default=True,
|
|
1111
|
+
help="Show the tests that have the metadata.",
|
|
1112
|
+
)
|
|
1113
|
+
@click.option(
|
|
1114
|
+
"--tasks / --no-tasks",
|
|
1115
|
+
"show_tasks",
|
|
1116
|
+
default=False,
|
|
1117
|
+
show_default=True,
|
|
1118
|
+
help="Show the tasks that have the metadata.",
|
|
1119
|
+
)
|
|
1120
|
+
@click.option(
|
|
1121
|
+
"--suites / --no-suites",
|
|
1122
|
+
"show_suites",
|
|
1123
|
+
default=False,
|
|
1124
|
+
show_default=True,
|
|
1125
|
+
help="Show the suites that have the metadata.",
|
|
1126
|
+
)
|
|
1127
|
+
@click.option(
|
|
1128
|
+
"--full-paths / --no-full-paths",
|
|
1129
|
+
"full_paths",
|
|
1130
|
+
default=False,
|
|
1131
|
+
show_default=True,
|
|
1132
|
+
help="Show full paths instead of relative.",
|
|
1133
|
+
)
|
|
1134
|
+
@add_options(*ROBOT_OPTIONS)
|
|
1135
|
+
@add_options(*DISCOVER_SEARCH_OPTIONS)
|
|
1136
|
+
@pass_application
|
|
1137
|
+
def metadata(
|
|
1138
|
+
app: Application,
|
|
1139
|
+
show_values: bool,
|
|
1140
|
+
show_tests: bool,
|
|
1141
|
+
show_tasks: bool,
|
|
1142
|
+
show_suites: bool,
|
|
1143
|
+
full_paths: bool,
|
|
1144
|
+
search_substring: Optional[str],
|
|
1145
|
+
search_regex: Optional[str],
|
|
1146
|
+
by_longname: Tuple[str, ...],
|
|
1147
|
+
exclude_by_longname: Tuple[str, ...],
|
|
1148
|
+
by_test_metadata: Tuple[str, ...],
|
|
1149
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
1150
|
+
robot_options_and_args: Tuple[str, ...],
|
|
1151
|
+
) -> None:
|
|
1152
|
+
"""\
|
|
1153
|
+
Discover the metadata of tests, tasks and suites with the selected
|
|
1154
|
+
configuration, profiles, options and arguments.
|
|
1155
|
+
|
|
1156
|
+
Test and task metadata is what `--by-test-metadata` selects by; suite
|
|
1157
|
+
metadata, including `--metadata` given to Robot, is listed in its own
|
|
1158
|
+
section.
|
|
1159
|
+
|
|
1160
|
+
\b
|
|
1161
|
+
Examples:
|
|
1162
|
+
```
|
|
1163
|
+
robotcode discover metadata
|
|
1164
|
+
robotcode discover metadata --values --tests
|
|
1165
|
+
robotcode --format json discover metadata
|
|
1166
|
+
```
|
|
1167
|
+
"""
|
|
1168
|
+
|
|
1169
|
+
matcher = make_search_matcher(search_substring, search_regex)
|
|
1170
|
+
_suite, collector, diagnostics = handle_options(
|
|
1171
|
+
app,
|
|
1172
|
+
by_longname,
|
|
1173
|
+
exclude_by_longname,
|
|
1174
|
+
robot_options_and_args,
|
|
1175
|
+
search_matcher=matcher,
|
|
1176
|
+
by_test_metadata=by_test_metadata,
|
|
1177
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
if collector.all.children:
|
|
1181
|
+
test_metadata = _metadata_index((item, item.metadata) for item in collector.test_and_tasks)
|
|
1182
|
+
suite_metadata = _metadata_index(collector.suite_metadata)
|
|
1183
|
+
|
|
1184
|
+
if app.config.output_format is None or app.config.output_format == OutputFormat.TEXT:
|
|
1185
|
+
app.echo_as_markdown(
|
|
1186
|
+
_render.render_metadata(
|
|
1187
|
+
test_metadata,
|
|
1188
|
+
suite_metadata,
|
|
1189
|
+
collector.statistics,
|
|
1190
|
+
show_values=show_values,
|
|
1191
|
+
show_tests=show_tests,
|
|
1192
|
+
show_tasks=show_tasks,
|
|
1193
|
+
show_suites=show_suites,
|
|
1194
|
+
full_paths=full_paths,
|
|
1195
|
+
highlight=make_md_highlighter(search_substring, search_regex),
|
|
1196
|
+
search_substring=search_substring,
|
|
1197
|
+
search_regex=search_regex,
|
|
1198
|
+
diagnostics=diagnostics,
|
|
1199
|
+
show_diagnostics=app.show_diagnostics,
|
|
1200
|
+
root_folder=app.root_folder,
|
|
1201
|
+
)
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
else:
|
|
1205
|
+
app.print_data(
|
|
1206
|
+
MetadataResult(
|
|
1207
|
+
test_metadata,
|
|
1208
|
+
suite_metadata,
|
|
1209
|
+
filters_applied=_filters_applied(search_substring, search_regex),
|
|
1210
|
+
),
|
|
1211
|
+
remove_defaults=True,
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
|
|
986
1215
|
@discover.command(add_help_option=True)
|
|
987
1216
|
@pass_application
|
|
988
1217
|
def info(app: Application) -> None:
|
|
@@ -1074,8 +1303,10 @@ def files(
|
|
|
1074
1303
|
# Match the other discover subcommands: chdir into root_folder so
|
|
1075
1304
|
# `get_rel_source` produces paths relative to the project root rather
|
|
1076
1305
|
# than to whatever the caller's process cwd happens to be.
|
|
1077
|
-
with app.chdir(root_folder):
|
|
1078
|
-
|
|
1306
|
+
with app.chdir(root_folder) as orig_folder:
|
|
1307
|
+
# Without duplicates, in the given order: the files come out in the order of the paths. Paths from the
|
|
1308
|
+
# command line count from the folder the command was started in, the configured ones from the root.
|
|
1309
|
+
search_paths = dict.fromkeys(
|
|
1079
1310
|
(
|
|
1080
1311
|
(
|
|
1081
1312
|
[*(app.config.default_paths if app.config.default_paths else ())]
|
|
@@ -1085,7 +1316,7 @@ def files(
|
|
|
1085
1316
|
else [profile.paths]
|
|
1086
1317
|
)
|
|
1087
1318
|
if not paths
|
|
1088
|
-
else [str(p) for p in paths]
|
|
1319
|
+
else [str(p if orig_folder is None else Path(orig_folder, p)) for p in paths]
|
|
1089
1320
|
)
|
|
1090
1321
|
)
|
|
1091
1322
|
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()
|
|
@@ -76,6 +76,7 @@ class TestResultItem(CamelSnakeMixin):
|
|
|
76
76
|
message: str
|
|
77
77
|
full_message: Optional[str] = None
|
|
78
78
|
tags: Optional[List[str]] = None
|
|
79
|
+
metadata: Optional[Dict[str, str]] = None
|
|
79
80
|
elapsed_seconds: Optional[float] = None
|
|
80
81
|
start_time: Optional[str] = None
|
|
81
82
|
source: Optional[str] = None
|
|
@@ -152,6 +153,9 @@ class LogTest(CamelSnakeMixin):
|
|
|
152
153
|
# suite. Lets the TEXT renderer group tests under suite headers and
|
|
153
154
|
# JSON consumers cross-reference `LogResult.suites`.
|
|
154
155
|
suite: Optional[str] = None
|
|
156
|
+
# The test's `[Metadata]` (Robot Framework 7.5+), same shape as
|
|
157
|
+
# `LogSuite.metadata`.
|
|
158
|
+
metadata: Optional[Dict[str, str]] = None
|
|
155
159
|
|
|
156
160
|
|
|
157
161
|
@dataclass
|