robotcode-runner 2.7.0__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,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 .._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
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), the parent suite's "
73
- "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 "
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 = []
@@ -356,6 +371,7 @@ class Collector(SuiteVisitor):
356
371
  end=Position(line=test.lineno - 1, character=0),
357
372
  ),
358
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,
359
375
  rpa=self._current.rpa,
360
376
  )
361
377
  except ValueError as e:
@@ -477,6 +493,8 @@ def handle_options(
477
493
  exclude_by_longname: Tuple[str, ...],
478
494
  robot_options_and_args: Tuple[str, ...],
479
495
  search_matcher: Optional[SearchMatcher] = None,
496
+ by_test_metadata: Tuple[str, ...] = (),
497
+ exclude_by_test_metadata: Tuple[str, ...] = (),
480
498
  ) -> Tuple[TestSuite, Collector, Optional[Dict[str, List[Diagnostic]]]]:
481
499
  root_folder, profile, cmd_options = handle_robot_options(app, robot_options_and_args)
482
500
 
@@ -500,6 +518,8 @@ def handle_options(
500
518
  by_longname,
501
519
  exclude_by_longname,
502
520
  search_matcher=search_matcher,
521
+ by_test_metadata=by_test_metadata,
522
+ exclude_by_test_metadata=exclude_by_test_metadata,
503
523
  ).parse_arguments((*cmd_options, "--runemptysuite", *robot_options_and_args))
504
524
 
505
525
  settings = RobotSettings(options)
@@ -572,6 +592,29 @@ def handle_options(
572
592
  raise UnknownError("Unexpected error happened.")
573
593
 
574
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
+
575
618
  def _filters_applied(search_substring: Optional[str], search_regex: Optional[str]) -> Optional[Dict[str, str]]:
576
619
  if search_substring:
577
620
  return {"search": search_substring}
@@ -585,13 +628,7 @@ def _filters_applied(search_substring: Optional[str], search_regex: Optional[str
585
628
  add_help_option=True,
586
629
  epilog="Use `-- --help` to see `robot` help.",
587
630
  )
588
- @click.option(
589
- "--tags / --no-tags",
590
- "show_tags",
591
- default=True,
592
- show_default=True,
593
- help="Show the tags that are present.",
594
- )
631
+ @add_options(*_show_options(default=True))
595
632
  @add_options(*ROBOT_OPTIONS)
596
633
  @add_options(*DISCOVER_SEARCH_OPTIONS)
597
634
  @click.option(
@@ -606,10 +643,13 @@ def all(
606
643
  app: Application,
607
644
  full_paths: bool,
608
645
  show_tags: bool,
646
+ show_metadata: bool,
609
647
  search_substring: Optional[str],
610
648
  search_regex: Optional[str],
611
649
  by_longname: Tuple[str, ...],
612
650
  exclude_by_longname: Tuple[str, ...],
651
+ by_test_metadata: Tuple[str, ...],
652
+ exclude_by_test_metadata: Tuple[str, ...],
613
653
  robot_options_and_args: Tuple[str, ...],
614
654
  ) -> None:
615
655
  """\
@@ -629,7 +669,13 @@ def all(
629
669
 
630
670
  matcher = make_search_matcher(search_substring, search_regex)
631
671
  _suite, collector, diagnostics = handle_options(
632
- 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,
633
679
  )
634
680
 
635
681
  if collector.all.children:
@@ -639,6 +685,7 @@ def all(
639
685
  collector.all.children[0],
640
686
  collector.statistics,
641
687
  show_tags=show_tags,
688
+ show_metadata=show_metadata,
642
689
  full_paths=full_paths,
643
690
  highlight=make_md_highlighter(search_substring, search_regex),
644
691
  search_substring=search_substring,
@@ -666,15 +713,24 @@ def _test_or_tasks(
666
713
  app: Application,
667
714
  full_paths: bool,
668
715
  show_tags: bool,
716
+ show_metadata: bool,
669
717
  search_substring: Optional[str],
670
718
  search_regex: Optional[str],
671
719
  by_longname: Tuple[str, ...],
672
720
  exclude_by_longname: Tuple[str, ...],
721
+ by_test_metadata: Tuple[str, ...],
722
+ exclude_by_test_metadata: Tuple[str, ...],
673
723
  robot_options_and_args: Tuple[str, ...],
674
724
  ) -> None:
675
725
  matcher = make_search_matcher(search_substring, search_regex)
676
726
  _suite, collector, diagnostics = handle_options(
677
- 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,
678
734
  )
679
735
 
680
736
  if collector.all.children:
@@ -685,6 +741,7 @@ def _test_or_tasks(
685
741
  collector.statistics,
686
742
  selected_type=selected_type,
687
743
  show_tags=show_tags,
744
+ show_metadata=show_metadata,
688
745
  full_paths=full_paths,
689
746
  highlight=make_md_highlighter(search_substring, search_regex),
690
747
  search_substring=search_substring,
@@ -713,13 +770,7 @@ def _test_or_tasks(
713
770
  add_help_option=True,
714
771
  epilog="Use `-- --help` to see `robot` help.",
715
772
  )
716
- @click.option(
717
- "--tags / --no-tags",
718
- "show_tags",
719
- default=False,
720
- show_default=True,
721
- help="Show the tags that are present.",
722
- )
773
+ @add_options(*_show_options(default=False))
723
774
  @click.option(
724
775
  "--full-paths / --no-full-paths",
725
776
  "full_paths",
@@ -734,10 +785,13 @@ def tests(
734
785
  app: Application,
735
786
  full_paths: bool,
736
787
  show_tags: bool,
788
+ show_metadata: bool,
737
789
  search_substring: Optional[str],
738
790
  search_regex: Optional[str],
739
791
  by_longname: Tuple[str, ...],
740
792
  exclude_by_longname: Tuple[str, ...],
793
+ by_test_metadata: Tuple[str, ...],
794
+ exclude_by_test_metadata: Tuple[str, ...],
741
795
  robot_options_and_args: Tuple[str, ...],
742
796
  ) -> None:
743
797
  """\
@@ -760,10 +814,13 @@ def tests(
760
814
  app,
761
815
  full_paths,
762
816
  show_tags,
817
+ show_metadata,
763
818
  search_substring,
764
819
  search_regex,
765
820
  by_longname,
766
821
  exclude_by_longname,
822
+ by_test_metadata,
823
+ exclude_by_test_metadata,
767
824
  robot_options_and_args,
768
825
  )
769
826
 
@@ -773,13 +830,7 @@ def tests(
773
830
  add_help_option=True,
774
831
  epilog="Use `-- --help` to see `robot` help.",
775
832
  )
776
- @click.option(
777
- "--tags / --no-tags",
778
- "show_tags",
779
- default=False,
780
- show_default=True,
781
- help="Show the tags that are present.",
782
- )
833
+ @add_options(*_show_options(default=False))
783
834
  @click.option(
784
835
  "--full-paths / --no-full-paths",
785
836
  "full_paths",
@@ -794,10 +845,13 @@ def tasks(
794
845
  app: Application,
795
846
  full_paths: bool,
796
847
  show_tags: bool,
848
+ show_metadata: bool,
797
849
  search_substring: Optional[str],
798
850
  search_regex: Optional[str],
799
851
  by_longname: Tuple[str, ...],
800
852
  exclude_by_longname: Tuple[str, ...],
853
+ by_test_metadata: Tuple[str, ...],
854
+ exclude_by_test_metadata: Tuple[str, ...],
801
855
  robot_options_and_args: Tuple[str, ...],
802
856
  ) -> None:
803
857
  """\
@@ -819,10 +873,13 @@ def tasks(
819
873
  app,
820
874
  full_paths,
821
875
  show_tags,
876
+ show_metadata,
822
877
  search_substring,
823
878
  search_regex,
824
879
  by_longname,
825
880
  exclude_by_longname,
881
+ by_test_metadata,
882
+ exclude_by_test_metadata,
826
883
  robot_options_and_args,
827
884
  )
828
885
 
@@ -849,6 +906,8 @@ def suites(
849
906
  search_regex: Optional[str],
850
907
  by_longname: Tuple[str, ...],
851
908
  exclude_by_longname: Tuple[str, ...],
909
+ by_test_metadata: Tuple[str, ...],
910
+ exclude_by_test_metadata: Tuple[str, ...],
852
911
  robot_options_and_args: Tuple[str, ...],
853
912
  ) -> None:
854
913
  """\
@@ -868,7 +927,13 @@ def suites(
868
927
 
869
928
  matcher = make_search_matcher(search_substring, search_regex)
870
929
  _suite, collector, diagnostics = handle_options(
871
- 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,
872
937
  )
873
938
 
874
939
  if collector.all.children:
@@ -890,7 +955,8 @@ def suites(
890
955
  else:
891
956
  app.print_data(
892
957
  ResultItem(
893
- collector.suites,
958
+ # a flat list: the suites without their nested suites and tests
959
+ [dataclasses.replace(suite, children=None) for suite in collector.suites],
894
960
  diagnostics,
895
961
  filters_applied=_filters_applied(search_substring, search_regex),
896
962
  supports_parse_include=SUPPORTS_PARSE_INCLUDE,
@@ -945,6 +1011,8 @@ def tags(
945
1011
  search_regex: Optional[str],
946
1012
  by_longname: Tuple[str, ...],
947
1013
  exclude_by_longname: Tuple[str, ...],
1014
+ by_test_metadata: Tuple[str, ...],
1015
+ exclude_by_test_metadata: Tuple[str, ...],
948
1016
  robot_options_and_args: Tuple[str, ...],
949
1017
  ) -> None:
950
1018
  """\
@@ -965,7 +1033,13 @@ def tags(
965
1033
 
966
1034
  matcher = make_search_matcher(search_substring, search_regex)
967
1035
  _suite, collector, diagnostics = handle_options(
968
- 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,
969
1043
  )
970
1044
 
971
1045
  if collector.all.children:
@@ -995,6 +1069,149 @@ def tags(
995
1069
  )
996
1070
 
997
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
+
998
1215
  @discover.command(add_help_option=True)
999
1216
  @pass_application
1000
1217
  def info(app: Application) -> None:
@@ -1086,8 +1303,10 @@ def files(
1086
1303
  # Match the other discover subcommands: chdir into root_folder so
1087
1304
  # `get_rel_source` produces paths relative to the project root rather
1088
1305
  # than to whatever the caller's process cwd happens to be.
1089
- with app.chdir(root_folder):
1090
- 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(
1091
1310
  (
1092
1311
  (
1093
1312
  [*(app.config.default_paths if app.config.default_paths else ())]
@@ -1097,7 +1316,7 @@ def files(
1097
1316
  else [profile.paths]
1098
1317
  )
1099
1318
  if not paths
1100
- 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]
1101
1320
  )
1102
1321
  )
1103
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
@@ -29,6 +29,7 @@ from .._markdown import (
29
29
  make_md_highlighter,
30
30
  md_escape,
31
31
  md_table,
32
+ metadata_md,
32
33
  path_paren,
33
34
  timing_suffix,
34
35
  )
@@ -51,7 +52,8 @@ from ._models import (
51
52
  # `show`); `LogTest` is the `log`-specific record carrying a body tree.
52
53
  # Both expose the fields the per-test renderers need (`full_name`,
53
54
  # `status`, `message`, `source`, `rel_source`, `lineno`, `elapsed_seconds`,
54
- # `start_time`), so we treat them interchangeably at the renderer level.
55
+ # `start_time`, `metadata`), so we treat them interchangeably at the renderer
56
+ # level.
55
57
  _TestLike = Union[TestResultItem, LogTest]
56
58
 
57
59
  _LEVEL_ORDER = {"TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3, "ERROR": 4, "FAIL": 5}
@@ -172,6 +174,7 @@ def render_show(
172
174
  data: ShowResult,
173
175
  *,
174
176
  show_tags: bool,
177
+ show_metadata: bool = False,
175
178
  full_paths: bool = False,
176
179
  show_timing: bool = False,
177
180
  sort_field: Optional[str] = None,
@@ -197,6 +200,8 @@ def render_show(
197
200
  if show_tags and t.tags:
198
201
  tags = ", ".join(highlight_md(md_escape(tag), highlight) for tag in t.tags)
199
202
  out.append(f" - _Tags:_ {tags}")
203
+ if show_metadata and t.metadata:
204
+ out.append(f" - _Metadata:_ {metadata_md(t.metadata, highlight)}")
200
205
 
201
206
  if data.truncated:
202
207
  out.append("")
@@ -490,6 +495,9 @@ def _test_header_md(
490
495
  head += _source_paren(t, full_paths=full_paths)
491
496
  head += f" {bold_status(t.status)}"
492
497
  head += timing_suffix(t.elapsed_seconds, t.start_time, show_timing=show_timing)
498
+ if t.metadata:
499
+ # a paragraph, not a list: a list would merge with the list of the test body below
500
+ head += f"\n\n_Metadata:_ {metadata_md(t.metadata, highlight)}"
493
501
  return head
494
502
 
495
503