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
|
@@ -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
|
|
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
|
|
|
@@ -16,7 +16,7 @@ All subcommands respect the global `-f/--format` option:
|
|
|
16
16
|
import re
|
|
17
17
|
import shutil
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
|
|
20
20
|
|
|
21
21
|
import click
|
|
22
22
|
from robot.api import SuiteVisitor
|
|
@@ -41,11 +41,11 @@ from robot.result import (
|
|
|
41
41
|
TryBranch,
|
|
42
42
|
While,
|
|
43
43
|
)
|
|
44
|
-
from robot.result.executionerrors import ExecutionErrors
|
|
45
44
|
from robot.result.model import BodyItem, StatusMixin
|
|
46
45
|
from robot.utils import normalize
|
|
47
46
|
|
|
48
|
-
from robotcode.modifiers import ByLongName, ExcludedByLongName
|
|
47
|
+
from robotcode.modifiers import ByLongName, ByTestMetadata, ExcludedByLongName, ExcludedByTestMetadata
|
|
48
|
+
from robotcode.modifiers.metadata_modifiers import MetadataPattern
|
|
49
49
|
from robotcode.plugin import Application, OutputFormat, pass_application
|
|
50
50
|
from robotcode.plugin.click_helper.types import add_options
|
|
51
51
|
from robotcode.robot.config.loader import load_robot_config_from_path
|
|
@@ -63,7 +63,15 @@ from robotcode.robot.utils import RF_VERSION
|
|
|
63
63
|
if TYPE_CHECKING:
|
|
64
64
|
from robot.result import Error, Group, Var # type: ignore[attr-defined,unused-ignore]
|
|
65
65
|
|
|
66
|
-
from ..
|
|
66
|
+
from .._robot_version import RobotVersionOption
|
|
67
|
+
from .._search import (
|
|
68
|
+
ByStatus,
|
|
69
|
+
SearchMatcher,
|
|
70
|
+
SearchModifier,
|
|
71
|
+
get_test_metadata,
|
|
72
|
+
make_search_matcher,
|
|
73
|
+
named_metadata,
|
|
74
|
+
)
|
|
67
75
|
from . import _html, _render
|
|
68
76
|
from ._models import (
|
|
69
77
|
ArtifactRef,
|
|
@@ -143,6 +151,27 @@ RESULT_FILTER_OPTIONS = [
|
|
|
143
151
|
metavar="NAME",
|
|
144
152
|
help="Exclude tests/tasks or suites by long name (exact match).",
|
|
145
153
|
),
|
|
154
|
+
click.option(
|
|
155
|
+
"-btm",
|
|
156
|
+
"--by-test-metadata",
|
|
157
|
+
"by_test_metadata",
|
|
158
|
+
multiple=True,
|
|
159
|
+
metavar="PATTERN",
|
|
160
|
+
# test metadata exists since Robot Framework 7.5
|
|
161
|
+
cls=RobotVersionOption,
|
|
162
|
+
since=(7, 5),
|
|
163
|
+
help="Select tests/tasks by their metadata, for example `Issue:4409 AND Author:Hans*`.",
|
|
164
|
+
),
|
|
165
|
+
click.option(
|
|
166
|
+
"-ebtm",
|
|
167
|
+
"--exclude-by-test-metadata",
|
|
168
|
+
"exclude_by_test_metadata",
|
|
169
|
+
multiple=True,
|
|
170
|
+
metavar="PATTERN",
|
|
171
|
+
cls=RobotVersionOption,
|
|
172
|
+
since=(7, 5),
|
|
173
|
+
help="Exclude tests/tasks by their metadata. Same patterns as `--by-test-metadata`.",
|
|
174
|
+
),
|
|
146
175
|
]
|
|
147
176
|
|
|
148
177
|
|
|
@@ -199,7 +228,8 @@ SEARCH_OPTIONS = [
|
|
|
199
228
|
help=(
|
|
200
229
|
"Only include tests with at least one case-insensitive substring "
|
|
201
230
|
"match against TEXT. Searches test name, full name, failure "
|
|
202
|
-
"message, documentation, template name, timeout, tags,
|
|
231
|
+
"message, documentation, template name, timeout, tags, test "
|
|
232
|
+
"metadata names and values, the "
|
|
203
233
|
"parent suite's Documentation / Metadata, every executed "
|
|
204
234
|
"keyword's name / arguments / [Documentation] / [Tags] / "
|
|
205
235
|
"[Timeout] / failure message, and log messages. Mutually "
|
|
@@ -275,7 +305,7 @@ def results() -> None:
|
|
|
275
305
|
"show_failed",
|
|
276
306
|
default=False,
|
|
277
307
|
show_default=True,
|
|
278
|
-
help="Include the list of failed tests (with messages)
|
|
308
|
+
help="Include the list of failed tests (with messages) below the counts table.",
|
|
279
309
|
)
|
|
280
310
|
@FULL_PATHS_OPTION
|
|
281
311
|
@pass_application
|
|
@@ -288,6 +318,8 @@ def summary(
|
|
|
288
318
|
test_globs: Tuple[str, ...],
|
|
289
319
|
by_longname: Tuple[str, ...],
|
|
290
320
|
exclude_by_longname: Tuple[str, ...],
|
|
321
|
+
by_test_metadata: Tuple[str, ...],
|
|
322
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
291
323
|
search_substring: Optional[str],
|
|
292
324
|
search_regex: Optional[str],
|
|
293
325
|
output_file: Optional[Path],
|
|
@@ -297,7 +329,7 @@ def summary(
|
|
|
297
329
|
"""\
|
|
298
330
|
Print headline counts and overall status for a finished run.
|
|
299
331
|
|
|
300
|
-
Pass `--failed` to also list failed tests
|
|
332
|
+
Pass `--failed` to also list failed tests below the counts.
|
|
301
333
|
Filter options narrow what is counted.
|
|
302
334
|
|
|
303
335
|
\b
|
|
@@ -310,10 +342,14 @@ def summary(
|
|
|
310
342
|
robotcode --format json results summary
|
|
311
343
|
```
|
|
312
344
|
"""
|
|
345
|
+
# before the result file is read, so an invalid pattern fails at once
|
|
346
|
+
matcher = make_search_matcher(search_substring, search_regex)
|
|
313
347
|
profile, root_folder = _resolve_profile(app)
|
|
314
348
|
with app.chdir(root_folder):
|
|
315
349
|
path = _resolve_output_file(app, profile, output_file)
|
|
316
350
|
execution = _load_execution_result(path)
|
|
351
|
+
# Before the tree filters, which would turn the copies of filtered-out messages into execution messages.
|
|
352
|
+
execution_messages = _execution_messages(execution)
|
|
317
353
|
|
|
318
354
|
filters_active = bool(
|
|
319
355
|
status_filters
|
|
@@ -323,10 +359,11 @@ def summary(
|
|
|
323
359
|
or test_globs
|
|
324
360
|
or by_longname
|
|
325
361
|
or exclude_by_longname
|
|
362
|
+
or by_test_metadata
|
|
363
|
+
or exclude_by_test_metadata
|
|
326
364
|
or search_substring
|
|
327
365
|
or search_regex
|
|
328
366
|
)
|
|
329
|
-
matcher = make_search_matcher(search_substring, search_regex)
|
|
330
367
|
_apply_tree_filters(
|
|
331
368
|
execution.suite,
|
|
332
369
|
include_tags,
|
|
@@ -337,11 +374,13 @@ def summary(
|
|
|
337
374
|
exclude_by_longname,
|
|
338
375
|
status_filters,
|
|
339
376
|
matcher,
|
|
377
|
+
by_test_metadata=by_test_metadata,
|
|
378
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
340
379
|
)
|
|
341
380
|
counts = _collect_counts(execution.suite)
|
|
342
381
|
failed = _collect_failures(execution.suite) if show_failed else None
|
|
343
|
-
exec_msg_counts = _count_execution_messages(
|
|
344
|
-
msg_counts = _count_all_messages(execution)
|
|
382
|
+
exec_msg_counts = _count_execution_messages(execution_messages)
|
|
383
|
+
msg_counts = _count_all_messages(execution.suite, execution_messages)
|
|
345
384
|
|
|
346
385
|
data = SummaryResult(
|
|
347
386
|
file=_make_file_info(path),
|
|
@@ -363,6 +402,8 @@ def summary(
|
|
|
363
402
|
exclude_by_longname,
|
|
364
403
|
search_substring,
|
|
365
404
|
search_regex,
|
|
405
|
+
by_test_metadata=by_test_metadata,
|
|
406
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
366
407
|
)
|
|
367
408
|
if filters_active
|
|
368
409
|
else None,
|
|
@@ -393,12 +434,22 @@ def summary(
|
|
|
393
434
|
)
|
|
394
435
|
@FULL_PATHS_OPTION
|
|
395
436
|
@click.option(
|
|
396
|
-
"--tags/--no-tags",
|
|
437
|
+
"--show-tags/--no-show-tags",
|
|
397
438
|
"show_tags",
|
|
398
439
|
default=False,
|
|
399
440
|
show_default=True,
|
|
400
441
|
help="Append the tag list after each test.",
|
|
401
442
|
)
|
|
443
|
+
@click.option(
|
|
444
|
+
"--show-metadata/--no-show-metadata",
|
|
445
|
+
"show_metadata",
|
|
446
|
+
default=False,
|
|
447
|
+
show_default=True,
|
|
448
|
+
# test metadata exists since Robot Framework 7.5
|
|
449
|
+
cls=RobotVersionOption,
|
|
450
|
+
since=(7, 5),
|
|
451
|
+
help="Append the metadata after each test.",
|
|
452
|
+
)
|
|
402
453
|
@click.option(
|
|
403
454
|
"--timing/--no-timing",
|
|
404
455
|
"show_timing",
|
|
@@ -437,6 +488,8 @@ def show(
|
|
|
437
488
|
test_globs: Tuple[str, ...],
|
|
438
489
|
by_longname: Tuple[str, ...],
|
|
439
490
|
exclude_by_longname: Tuple[str, ...],
|
|
491
|
+
by_test_metadata: Tuple[str, ...],
|
|
492
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
440
493
|
shortcut_failed: bool,
|
|
441
494
|
shortcut_passed: bool,
|
|
442
495
|
shortcut_skipped: bool,
|
|
@@ -447,6 +500,7 @@ def show(
|
|
|
447
500
|
message_chars: int,
|
|
448
501
|
full_paths: bool,
|
|
449
502
|
show_tags: bool,
|
|
503
|
+
show_metadata: bool,
|
|
450
504
|
show_timing: bool,
|
|
451
505
|
sort_field: Optional[str],
|
|
452
506
|
reverse: bool,
|
|
@@ -462,20 +516,21 @@ def show(
|
|
|
462
516
|
```
|
|
463
517
|
robotcode results show
|
|
464
518
|
robotcode results show --failed
|
|
465
|
-
robotcode results show --failed --skipped --tags
|
|
519
|
+
robotcode results show --failed --skipped --show-tags
|
|
466
520
|
robotcode results show -i smoke -e wipANDnotready
|
|
467
521
|
robotcode results show -s "MyProject.Login.*"
|
|
468
522
|
robotcode results show --top 20
|
|
469
523
|
robotcode results show --search "AssertionError"
|
|
470
524
|
```
|
|
471
525
|
"""
|
|
526
|
+
# before the result file is read, so an invalid pattern fails at once
|
|
527
|
+
matcher = make_search_matcher(search_substring, search_regex)
|
|
472
528
|
profile, root_folder = _resolve_profile(app)
|
|
473
529
|
with app.chdir(root_folder):
|
|
474
530
|
path = _resolve_output_file(app, profile, output_file)
|
|
475
531
|
execution = _load_execution_result(path)
|
|
476
532
|
|
|
477
533
|
status_filters = _apply_status_shortcuts(status_filters, shortcut_failed, shortcut_passed, shortcut_skipped)
|
|
478
|
-
matcher = make_search_matcher(search_substring, search_regex)
|
|
479
534
|
_apply_tree_filters(
|
|
480
535
|
execution.suite,
|
|
481
536
|
include_tags,
|
|
@@ -486,6 +541,8 @@ def show(
|
|
|
486
541
|
exclude_by_longname,
|
|
487
542
|
status_filters,
|
|
488
543
|
matcher,
|
|
544
|
+
by_test_metadata=by_test_metadata,
|
|
545
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
489
546
|
)
|
|
490
547
|
|
|
491
548
|
all_items = [_make_test_item(t, message_chars=message_chars) for t in _iter_all_tests(execution.suite)]
|
|
@@ -506,6 +563,8 @@ def show(
|
|
|
506
563
|
or test_globs
|
|
507
564
|
or by_longname
|
|
508
565
|
or exclude_by_longname
|
|
566
|
+
or by_test_metadata
|
|
567
|
+
or exclude_by_test_metadata
|
|
509
568
|
or search_substring
|
|
510
569
|
or search_regex
|
|
511
570
|
)
|
|
@@ -524,6 +583,8 @@ def show(
|
|
|
524
583
|
exclude_by_longname,
|
|
525
584
|
search_substring,
|
|
526
585
|
search_regex,
|
|
586
|
+
by_test_metadata=by_test_metadata,
|
|
587
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
527
588
|
)
|
|
528
589
|
if filters_active
|
|
529
590
|
else None,
|
|
@@ -537,6 +598,7 @@ def show(
|
|
|
537
598
|
_render.render_show(
|
|
538
599
|
data,
|
|
539
600
|
show_tags=show_tags,
|
|
601
|
+
show_metadata=show_metadata,
|
|
540
602
|
full_paths=full_paths,
|
|
541
603
|
show_timing=show_timing,
|
|
542
604
|
sort_field=sort_field,
|
|
@@ -619,7 +681,10 @@ def show(
|
|
|
619
681
|
"show_execution_messages",
|
|
620
682
|
default=False,
|
|
621
683
|
show_default=True,
|
|
622
|
-
help=(
|
|
684
|
+
help=(
|
|
685
|
+
"Also show the messages of output.xml's `<errors>` section that come from outside the tests, "
|
|
686
|
+
"such as parsing and import errors."
|
|
687
|
+
),
|
|
623
688
|
)
|
|
624
689
|
@click.option(
|
|
625
690
|
"--keyword-info/--no-keyword-info",
|
|
@@ -653,6 +718,8 @@ def log(
|
|
|
653
718
|
test_globs: Tuple[str, ...],
|
|
654
719
|
by_longname: Tuple[str, ...],
|
|
655
720
|
exclude_by_longname: Tuple[str, ...],
|
|
721
|
+
by_test_metadata: Tuple[str, ...],
|
|
722
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
656
723
|
shortcut_failed: bool,
|
|
657
724
|
shortcut_passed: bool,
|
|
658
725
|
shortcut_skipped: bool,
|
|
@@ -690,13 +757,16 @@ def log(
|
|
|
690
757
|
robotcode results log --execution-messages
|
|
691
758
|
```
|
|
692
759
|
"""
|
|
760
|
+
# before the result file is read, so an invalid pattern fails at once
|
|
761
|
+
matcher = make_search_matcher(search_substring, search_regex)
|
|
693
762
|
profile, root_folder = _resolve_profile(app)
|
|
694
763
|
with app.chdir(root_folder):
|
|
695
764
|
path = _resolve_output_file(app, profile, output_file)
|
|
696
765
|
execution = _load_execution_result(path)
|
|
766
|
+
# Before the tree filters, which would turn the copies of filtered-out messages into execution messages.
|
|
767
|
+
execution_messages = _execution_messages(execution) if show_execution_messages else []
|
|
697
768
|
|
|
698
769
|
status_filters = _apply_status_shortcuts(status_filters, shortcut_failed, shortcut_passed, shortcut_skipped)
|
|
699
|
-
matcher = make_search_matcher(search_substring, search_regex)
|
|
700
770
|
_apply_tree_filters(
|
|
701
771
|
execution.suite,
|
|
702
772
|
include_tags,
|
|
@@ -707,6 +777,8 @@ def log(
|
|
|
707
777
|
exclude_by_longname,
|
|
708
778
|
status_filters,
|
|
709
779
|
matcher,
|
|
780
|
+
by_test_metadata=by_test_metadata,
|
|
781
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
710
782
|
)
|
|
711
783
|
|
|
712
784
|
collector = _LogCollector(
|
|
@@ -721,7 +793,7 @@ def log(
|
|
|
721
793
|
|
|
722
794
|
exec_messages: Optional[List[LogEntry]] = None
|
|
723
795
|
if show_execution_messages:
|
|
724
|
-
exec_messages = _collect_execution_messages(
|
|
796
|
+
exec_messages = _collect_execution_messages(execution_messages, raw_html=raw_html) or None
|
|
725
797
|
|
|
726
798
|
extracted_count = 0
|
|
727
799
|
extract_abs: Optional[Path] = None
|
|
@@ -738,6 +810,8 @@ def log(
|
|
|
738
810
|
or test_globs
|
|
739
811
|
or by_longname
|
|
740
812
|
or exclude_by_longname
|
|
813
|
+
or by_test_metadata
|
|
814
|
+
or exclude_by_test_metadata
|
|
741
815
|
or search_substring
|
|
742
816
|
or search_regex
|
|
743
817
|
)
|
|
@@ -761,6 +835,8 @@ def log(
|
|
|
761
835
|
exclude_by_longname,
|
|
762
836
|
search_substring,
|
|
763
837
|
search_regex,
|
|
838
|
+
by_test_metadata=by_test_metadata,
|
|
839
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
764
840
|
)
|
|
765
841
|
if filters_active
|
|
766
842
|
else None,
|
|
@@ -821,6 +897,8 @@ def stats(
|
|
|
821
897
|
test_globs: Tuple[str, ...],
|
|
822
898
|
by_longname: Tuple[str, ...],
|
|
823
899
|
exclude_by_longname: Tuple[str, ...],
|
|
900
|
+
by_test_metadata: Tuple[str, ...],
|
|
901
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
824
902
|
shortcut_failed: bool,
|
|
825
903
|
shortcut_passed: bool,
|
|
826
904
|
shortcut_skipped: bool,
|
|
@@ -848,13 +926,14 @@ def stats(
|
|
|
848
926
|
robotcode --format json results stats --by tag
|
|
849
927
|
```
|
|
850
928
|
"""
|
|
929
|
+
# before the result file is read, so an invalid pattern fails at once
|
|
930
|
+
matcher = make_search_matcher(search_substring, search_regex)
|
|
851
931
|
profile, root_folder = _resolve_profile(app)
|
|
852
932
|
with app.chdir(root_folder):
|
|
853
933
|
path = _resolve_output_file(app, profile, output_file)
|
|
854
934
|
execution = _load_execution_result(path)
|
|
855
935
|
|
|
856
936
|
status_filters = _apply_status_shortcuts(status_filters, shortcut_failed, shortcut_passed, shortcut_skipped)
|
|
857
|
-
matcher = make_search_matcher(search_substring, search_regex)
|
|
858
937
|
_apply_tree_filters(
|
|
859
938
|
execution.suite,
|
|
860
939
|
include_tags,
|
|
@@ -865,6 +944,8 @@ def stats(
|
|
|
865
944
|
exclude_by_longname,
|
|
866
945
|
status_filters,
|
|
867
946
|
matcher,
|
|
947
|
+
by_test_metadata=by_test_metadata,
|
|
948
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
868
949
|
)
|
|
869
950
|
tests = list(_iter_all_tests(execution.suite))
|
|
870
951
|
|
|
@@ -884,6 +965,8 @@ def stats(
|
|
|
884
965
|
or test_globs
|
|
885
966
|
or by_longname
|
|
886
967
|
or exclude_by_longname
|
|
968
|
+
or by_test_metadata
|
|
969
|
+
or exclude_by_test_metadata
|
|
887
970
|
or search_substring
|
|
888
971
|
or search_regex
|
|
889
972
|
)
|
|
@@ -900,6 +983,8 @@ def stats(
|
|
|
900
983
|
exclude_by_longname,
|
|
901
984
|
search_substring,
|
|
902
985
|
search_regex,
|
|
986
|
+
by_test_metadata=by_test_metadata,
|
|
987
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
903
988
|
)
|
|
904
989
|
if filters_active
|
|
905
990
|
else None,
|
|
@@ -1003,6 +1088,8 @@ def diff(
|
|
|
1003
1088
|
test_globs: Tuple[str, ...],
|
|
1004
1089
|
by_longname: Tuple[str, ...],
|
|
1005
1090
|
exclude_by_longname: Tuple[str, ...],
|
|
1091
|
+
by_test_metadata: Tuple[str, ...],
|
|
1092
|
+
exclude_by_test_metadata: Tuple[str, ...],
|
|
1006
1093
|
search_substring: Optional[str],
|
|
1007
1094
|
search_regex: Optional[str],
|
|
1008
1095
|
full_paths: bool,
|
|
@@ -1026,6 +1113,8 @@ def diff(
|
|
|
1026
1113
|
robotcode --format json results diff baseline.xml
|
|
1027
1114
|
```
|
|
1028
1115
|
"""
|
|
1116
|
+
# before the result file is read, so an invalid pattern fails at once
|
|
1117
|
+
matcher = make_search_matcher(search_substring, search_regex)
|
|
1029
1118
|
profile, root_folder = _resolve_profile(app)
|
|
1030
1119
|
with app.chdir(root_folder):
|
|
1031
1120
|
baseline_path = baseline.resolve()
|
|
@@ -1037,7 +1126,6 @@ def diff(
|
|
|
1037
1126
|
baseline_exec = _load_execution_result(baseline_path)
|
|
1038
1127
|
current_exec = _load_execution_result(current_path)
|
|
1039
1128
|
|
|
1040
|
-
matcher = make_search_matcher(search_substring, search_regex)
|
|
1041
1129
|
for suite in (baseline_exec.suite, current_exec.suite):
|
|
1042
1130
|
_apply_tree_filters(
|
|
1043
1131
|
suite,
|
|
@@ -1049,6 +1137,8 @@ def diff(
|
|
|
1049
1137
|
exclude_by_longname,
|
|
1050
1138
|
status_filters,
|
|
1051
1139
|
matcher,
|
|
1140
|
+
by_test_metadata=by_test_metadata,
|
|
1141
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
1052
1142
|
)
|
|
1053
1143
|
|
|
1054
1144
|
baseline_tests = {_get_full_name(t): t for t in _iter_all_tests(baseline_exec.suite)}
|
|
@@ -1089,6 +1179,8 @@ def diff(
|
|
|
1089
1179
|
or test_globs
|
|
1090
1180
|
or by_longname
|
|
1091
1181
|
or exclude_by_longname
|
|
1182
|
+
or by_test_metadata
|
|
1183
|
+
or exclude_by_test_metadata
|
|
1092
1184
|
or search_substring
|
|
1093
1185
|
or search_regex
|
|
1094
1186
|
)
|
|
@@ -1110,6 +1202,8 @@ def diff(
|
|
|
1110
1202
|
exclude_by_longname,
|
|
1111
1203
|
search_substring,
|
|
1112
1204
|
search_regex,
|
|
1205
|
+
by_test_metadata=by_test_metadata,
|
|
1206
|
+
exclude_by_test_metadata=exclude_by_test_metadata,
|
|
1113
1207
|
)
|
|
1114
1208
|
if filters_active
|
|
1115
1209
|
else None,
|
|
@@ -1246,12 +1340,27 @@ def _candidate_paths(*, search_root: Path, out_dir: Path, out_name: str) -> List
|
|
|
1246
1340
|
return cands
|
|
1247
1341
|
|
|
1248
1342
|
|
|
1343
|
+
# Robot Framework < 7.5 fails with one of these messages on a result file
|
|
1344
|
+
# that carries test-level `[Metadata]`: the first for `output.xml`, the
|
|
1345
|
+
# second for `output.json` (RF 7.2-7.4).
|
|
1346
|
+
_TEST_METADATA_READ_ERRORS = (
|
|
1347
|
+
"Incompatible child element 'meta' for 'test'",
|
|
1348
|
+
"'robot.result.TestCase' object does not have attribute 'metadata'",
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def _test_metadata_hint(error: str) -> str:
|
|
1353
|
+
if not any(text in error for text in _TEST_METADATA_READ_ERRORS):
|
|
1354
|
+
return ""
|
|
1355
|
+
return (
|
|
1356
|
+
"\nThe file was written by Robot Framework 7.5 or newer and contains test metadata; "
|
|
1357
|
+
"reading it requires Robot Framework 7.5+."
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
|
|
1249
1361
|
def _load_execution_result(path: Path) -> Result:
|
|
1250
1362
|
if path.suffix.lower() == ".json" and RF_VERSION < (7, 0):
|
|
1251
|
-
raise click.ClickException(
|
|
1252
|
-
f"Reading JSON result files requires Robot Framework 7.0+; "
|
|
1253
|
-
f"got {'.'.join(str(v) for v in RF_VERSION)} (file: {path})."
|
|
1254
|
-
)
|
|
1363
|
+
raise click.ClickException(f"Reading JSON result files requires Robot Framework 7.0+ (file: {path}).")
|
|
1255
1364
|
try:
|
|
1256
1365
|
from robot.api import ExecutionResult
|
|
1257
1366
|
except ImportError as e:
|
|
@@ -1259,7 +1368,7 @@ def _load_execution_result(path: Path) -> Result:
|
|
|
1259
1368
|
try:
|
|
1260
1369
|
return ExecutionResult(str(path))
|
|
1261
1370
|
except DataError as e:
|
|
1262
|
-
raise click.ClickException(f"failed to parse {path}: {e}") from e
|
|
1371
|
+
raise click.ClickException(f"failed to parse {path}: {e}{_test_metadata_hint(str(e))}") from e
|
|
1263
1372
|
except Exception as e:
|
|
1264
1373
|
raise click.ClickException(f"failed to parse {path}: {e}") from e
|
|
1265
1374
|
|
|
@@ -1370,16 +1479,21 @@ def _apply_tree_filters(
|
|
|
1370
1479
|
exclude_by_longname: Tuple[str, ...] = (),
|
|
1371
1480
|
status_filters: Tuple[str, ...] = (),
|
|
1372
1481
|
matcher: Optional[SearchMatcher] = None,
|
|
1482
|
+
by_test_metadata: Tuple[str, ...] = (),
|
|
1483
|
+
exclude_by_test_metadata: Tuple[str, ...] = (),
|
|
1373
1484
|
) -> None:
|
|
1374
1485
|
"""Apply every filter to the result tree in-place.
|
|
1375
1486
|
|
|
1376
1487
|
Delegates the standard Robot filters (`--include`/`--exclude`,
|
|
1377
1488
|
`--suite`, `--test`) to `TestSuite.filter()`, then bundles the
|
|
1378
|
-
project-specific filters (`-bl`/`-ebl`, `--status`,
|
|
1379
|
-
single `ModelModifier` pass so subsequent code can
|
|
1380
|
-
suite naturally — every surviving test has passed every
|
|
1489
|
+
project-specific filters (`-bl`/`-ebl`, `-btm`/`-ebtm`, `--status`,
|
|
1490
|
+
search) into a single `ModelModifier` pass so subsequent code can
|
|
1491
|
+
iterate the suite naturally — every surviving test has passed every
|
|
1492
|
+
filter.
|
|
1381
1493
|
"""
|
|
1382
1494
|
try:
|
|
1495
|
+
test_metadata_patterns = [MetadataPattern(p) for p in by_test_metadata]
|
|
1496
|
+
excluded_test_metadata_patterns = [MetadataPattern(p) for p in exclude_by_test_metadata]
|
|
1383
1497
|
suite.filter(
|
|
1384
1498
|
included_suites=list(suite_globs) or None,
|
|
1385
1499
|
included_tests=list(test_globs) or None,
|
|
@@ -1394,6 +1508,10 @@ def _apply_tree_filters(
|
|
|
1394
1508
|
modifiers.append(ByLongName(*by_longname))
|
|
1395
1509
|
if exclude_by_longname:
|
|
1396
1510
|
modifiers.append(ExcludedByLongName(*exclude_by_longname))
|
|
1511
|
+
if test_metadata_patterns:
|
|
1512
|
+
modifiers.append(ByTestMetadata(*test_metadata_patterns))
|
|
1513
|
+
if excluded_test_metadata_patterns:
|
|
1514
|
+
modifiers.append(ExcludedByTestMetadata(*excluded_test_metadata_patterns))
|
|
1397
1515
|
if matcher is not None:
|
|
1398
1516
|
modifiers.append(SearchModifier(matcher))
|
|
1399
1517
|
if status_filters:
|
|
@@ -1435,6 +1553,8 @@ def _filters_dict(
|
|
|
1435
1553
|
exclude_by_longname: Tuple[str, ...] = (),
|
|
1436
1554
|
search_substring: Optional[str] = None,
|
|
1437
1555
|
search_regex: Optional[str] = None,
|
|
1556
|
+
by_test_metadata: Tuple[str, ...] = (),
|
|
1557
|
+
exclude_by_test_metadata: Tuple[str, ...] = (),
|
|
1438
1558
|
) -> Dict[str, List[str]]:
|
|
1439
1559
|
out: Dict[str, List[str]] = {}
|
|
1440
1560
|
if status_filters:
|
|
@@ -1451,6 +1571,11 @@ def _filters_dict(
|
|
|
1451
1571
|
out["by-longname"] = list(by_longname)
|
|
1452
1572
|
if exclude_by_longname:
|
|
1453
1573
|
out["exclude-by-longname"] = list(exclude_by_longname)
|
|
1574
|
+
# Metadata patterns are echoed as given, unlike the canonicalised tag patterns.
|
|
1575
|
+
if by_test_metadata:
|
|
1576
|
+
out["by-test-metadata"] = list(by_test_metadata)
|
|
1577
|
+
if exclude_by_test_metadata:
|
|
1578
|
+
out["exclude-by-test-metadata"] = list(exclude_by_test_metadata)
|
|
1454
1579
|
if search_substring:
|
|
1455
1580
|
out["search"] = [search_substring]
|
|
1456
1581
|
if search_regex:
|
|
@@ -1549,6 +1674,7 @@ def _make_test_item(test: TestCase, *, message_chars: int) -> TestResultItem:
|
|
|
1549
1674
|
message=truncated_msg,
|
|
1550
1675
|
full_message=full_msg,
|
|
1551
1676
|
tags=[normalize(str(t), ignore="_") for t in test.tags] if test.tags else None,
|
|
1677
|
+
metadata=get_test_metadata(test) or None,
|
|
1552
1678
|
elapsed_seconds=_elapsed_seconds(test),
|
|
1553
1679
|
start_time=_iso(_start_time(test)),
|
|
1554
1680
|
source=src_str,
|
|
@@ -1562,12 +1688,40 @@ def _collect_failures(suite: TestSuite) -> List[TestResultItem]:
|
|
|
1562
1688
|
return [_make_test_item(t, message_chars=0) for t in _iter_all_tests(suite) if t.status == "FAIL"]
|
|
1563
1689
|
|
|
1564
1690
|
|
|
1565
|
-
|
|
1566
|
-
"""
|
|
1567
|
-
|
|
1568
|
-
|
|
1691
|
+
class _LoggedMessages(ResultVisitor):
|
|
1692
|
+
"""Collect the WARN/ERROR messages of a result tree by the key Robot Framework links them with."""
|
|
1693
|
+
|
|
1694
|
+
def __init__(self) -> None:
|
|
1695
|
+
super().__init__()
|
|
1696
|
+
self.keys: Set[Tuple[Any, ...]] = set()
|
|
1697
|
+
|
|
1698
|
+
def visit_message(self, msg: Message) -> None:
|
|
1699
|
+
if (msg.level or "").upper() in ("WARN", "ERROR"):
|
|
1700
|
+
self.keys.add(_message_key(msg))
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def _message_key(msg: Message) -> Tuple[Any, ...]:
|
|
1704
|
+
return (msg.message, msg.level, msg.timestamp)
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def _execution_messages(execution: Result) -> List[Message]:
|
|
1708
|
+
"""The messages of `output.xml`'s <errors> section that come from outside the tests, such as parsing and
|
|
1709
|
+
import errors.
|
|
1710
|
+
|
|
1711
|
+
Robot Framework also copies every WARN and ERROR message logged by a test or keyword into <errors> and links the
|
|
1712
|
+
copy to its original by message, level and timestamp. Those copies are left out here, matched the same way.
|
|
1713
|
+
Call this before the suite tree is filtered."""
|
|
1714
|
+
if execution.errors is None:
|
|
1715
|
+
return []
|
|
1716
|
+
logged = _LoggedMessages()
|
|
1717
|
+
execution.suite.visit(logged)
|
|
1718
|
+
return [m for m in execution.errors.messages if _message_key(m) not in logged.keys]
|
|
1719
|
+
|
|
1720
|
+
|
|
1721
|
+
def _count_execution_messages(messages: List[Message]) -> Dict[str, int]:
|
|
1722
|
+
"""Count execution messages grouped by level. Returns empty dict if none."""
|
|
1569
1723
|
counts: Dict[str, int] = {}
|
|
1570
|
-
for m in
|
|
1724
|
+
for m in messages:
|
|
1571
1725
|
level = (m.level or "INFO").upper()
|
|
1572
1726
|
counts[level] = counts.get(level, 0) + 1
|
|
1573
1727
|
return counts
|
|
@@ -1586,24 +1740,21 @@ class _MessageCounter(ResultVisitor):
|
|
|
1586
1740
|
self.counts[level] = self.counts.get(level, 0) + 1
|
|
1587
1741
|
|
|
1588
1742
|
|
|
1589
|
-
def _count_all_messages(
|
|
1590
|
-
"""Tally WARN/ERROR/FAIL across
|
|
1743
|
+
def _count_all_messages(suite: TestSuite, execution_messages: List[Message]) -> Dict[str, int]:
|
|
1744
|
+
"""Tally WARN/ERROR/FAIL across the test runtime messages and the execution messages, each message once."""
|
|
1591
1745
|
counter = _MessageCounter()
|
|
1592
|
-
|
|
1593
|
-
for m in execution.errors.messages:
|
|
1746
|
+
for m in execution_messages:
|
|
1594
1747
|
counter.visit_message(m)
|
|
1595
1748
|
# Runtime messages — the visitor handles all suite/test/keyword traversal.
|
|
1596
|
-
|
|
1749
|
+
suite.visit(counter)
|
|
1597
1750
|
return counter.counts
|
|
1598
1751
|
|
|
1599
1752
|
|
|
1600
|
-
def _collect_execution_messages(
|
|
1601
|
-
"""Collect
|
|
1602
|
-
|
|
1603
|
-
if errors is None:
|
|
1604
|
-
return []
|
|
1753
|
+
def _collect_execution_messages(messages: List[Message], *, raw_html: bool = False) -> List[LogEntry]:
|
|
1754
|
+
"""Collect execution messages (see `_execution_messages`) as LogEntry items, preserving order and individual
|
|
1755
|
+
timestamps."""
|
|
1605
1756
|
out: List[LogEntry] = []
|
|
1606
|
-
for m in
|
|
1757
|
+
for m in messages:
|
|
1607
1758
|
level = (m.level or "INFO").upper()
|
|
1608
1759
|
text = m.message or ""
|
|
1609
1760
|
is_html = bool(m.html)
|
|
@@ -1672,7 +1823,7 @@ class _LogCollector(ResultVisitor):
|
|
|
1672
1823
|
name=suite.name,
|
|
1673
1824
|
status=suite.status,
|
|
1674
1825
|
doc=suite.doc or None,
|
|
1675
|
-
metadata=
|
|
1826
|
+
metadata=named_metadata(suite.metadata) or None,
|
|
1676
1827
|
source=src_str,
|
|
1677
1828
|
rel_source=_rel_to_cwd(src_str),
|
|
1678
1829
|
elapsed_seconds=_elapsed_seconds(suite),
|
|
@@ -1707,6 +1858,7 @@ class _LogCollector(ResultVisitor):
|
|
|
1707
1858
|
elapsed_seconds=_elapsed_seconds(test),
|
|
1708
1859
|
start_time=_iso(_start_time(test)),
|
|
1709
1860
|
suite=parent_full_name,
|
|
1861
|
+
metadata=get_test_metadata(test) or None,
|
|
1710
1862
|
)
|
|
1711
1863
|
)
|
|
1712
1864
|
|