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.
@@ -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 .._search import SearchMatcher, make_search_matcher
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), the parent suite's "
67
- "Documentation / Metadata, or anything inside the test body "
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
- @click.option(
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, by_longname, exclude_by_longname, robot_options_and_args, search_matcher=matcher
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], diagnostics, filters_applied=_filters_applied(search_substring, search_regex)
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, by_longname, exclude_by_longname, robot_options_and_args, search_matcher=matcher
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
- @click.option(
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
- @click.option(
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, by_longname, exclude_by_longname, robot_options_and_args, search_matcher=matcher
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
- collector.suites, diagnostics, filters_applied=_filters_applied(search_substring, search_regex)
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, by_longname, exclude_by_longname, robot_options_and_args, search_matcher=matcher
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
- search_paths = set(
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:
@@ -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
- libdoc_options = profile.libdoc
84
- if libdoc_options is None:
85
- libdoc_options = LibDocProfile()
86
-
87
- libdoc_options.add_options(profile)
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
 
@@ -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 libdoc with the following options and arguments:\n"
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
- rebot_options = profile.rebot
86
- if rebot_options is None:
87
- rebot_options = RebotProfile()
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