robotcode-runner 2.7.0__py3-none-any.whl → 2.8.0.dev631__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robotcode/runner/__version__.py +1 -1
- robotcode/runner/cli/_markdown.py +13 -0
- robotcode/runner/cli/_robot_version.py +28 -0
- robotcode/runner/cli/_search.py +37 -1
- robotcode/runner/cli/discover/_models.py +11 -1
- robotcode/runner/cli/discover/_render.py +108 -2
- robotcode/runner/cli/discover/discover.py +267 -40
- robotcode/runner/cli/libdoc.py +7 -5
- robotcode/runner/cli/rebot.py +32 -5
- robotcode/runner/cli/results/_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 +56 -8
- robotcode/runner/cli/testdoc.py +7 -4
- {robotcode_runner-2.7.0.dist-info → robotcode_runner-2.8.0.dev631.dist-info}/METADATA +7 -6
- robotcode_runner-2.8.0.dev631.dist-info/RECORD +25 -0
- {robotcode_runner-2.7.0.dist-info → robotcode_runner-2.8.0.dev631.dist-info}/WHEEL +1 -1
- robotcode_runner-2.7.0.dist-info/RECORD +0 -24
- {robotcode_runner-2.7.0.dist-info → robotcode_runner-2.8.0.dev631.dist-info}/entry_points.txt +0 -0
robotcode/runner/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.
|
|
1
|
+
__version__ = "2.8.0-dev.631"
|
|
@@ -294,6 +294,19 @@ def highlight_md(text: str, highlight: Optional[Callable[[str], str]]) -> str:
|
|
|
294
294
|
return highlight(text) if highlight else text
|
|
295
295
|
|
|
296
296
|
|
|
297
|
+
def metadata_md(metadata: Dict[str, str], highlight: Optional[Callable[[str], str]] = None) -> str:
|
|
298
|
+
"""Test metadata as `name: value, name: value`.
|
|
299
|
+
|
|
300
|
+
All pairs share one line, so the lines of a multi-line value are joined
|
|
301
|
+
with spaces.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def hi(text: str) -> str:
|
|
305
|
+
return highlight_md(md_escape(text), highlight)
|
|
306
|
+
|
|
307
|
+
return ", ".join(f"{hi(name)}: {hi(' '.join(value.splitlines()))}" for name, value in metadata.items())
|
|
308
|
+
|
|
309
|
+
|
|
297
310
|
def make_md_highlighter(search_substring: Optional[str], search_regex: Optional[str]) -> Optional[Callable[[str], str]]:
|
|
298
311
|
"""Build a `text -> highlighted text` callback that wraps each
|
|
299
312
|
regex/substring match in an inline-code span. Returns `None` when
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Click options and commands that RobotCode offers only from a certain
|
|
2
|
+
Robot Framework version on.
|
|
3
|
+
|
|
4
|
+
The version is declared once, with `since`: before it, `--help` hides the
|
|
5
|
+
option or command; `scripts/create_cmdline_doc.py` marks it with the version
|
|
6
|
+
in the generated CLI reference. `--help` itself carries no note, since it only
|
|
7
|
+
lists the option where the installed version has it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Tuple
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from robotcode.robot.utils import RF_VERSION
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RobotVersionOption(click.Option):
|
|
18
|
+
def __init__(self, *args: Any, since: Tuple[int, int], **kwargs: Any) -> None:
|
|
19
|
+
kwargs["hidden"] = RF_VERSION < since
|
|
20
|
+
super().__init__(*args, **kwargs)
|
|
21
|
+
self.since = since
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RobotVersionCommand(click.Command):
|
|
25
|
+
def __init__(self, *args: Any, since: Tuple[int, int], **kwargs: Any) -> None:
|
|
26
|
+
kwargs["hidden"] = RF_VERSION < since
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
self.since = since
|
robotcode/runner/cli/_search.py
CHANGED
|
@@ -12,7 +12,7 @@ The `--search` / `--search-regex` semantics live here so both `results` and
|
|
|
12
12
|
|
|
13
13
|
import re
|
|
14
14
|
from dataclasses import dataclass
|
|
15
|
-
from typing import TYPE_CHECKING, Callable, Iterable, Optional
|
|
15
|
+
from typing import TYPE_CHECKING, Callable, Dict, Iterable, Mapping, Optional
|
|
16
16
|
|
|
17
17
|
import click
|
|
18
18
|
from robot.api import SuiteVisitor
|
|
@@ -21,6 +21,7 @@ from robot.model import TestCase as ModelTestCase
|
|
|
21
21
|
from robot.model.control import For, IfBranch, Return, TryBranch, While
|
|
22
22
|
from robot.model.message import Message
|
|
23
23
|
from robot.result import Keyword as ResultKeyword
|
|
24
|
+
from robot.result import TestCase as ResultTestCase
|
|
24
25
|
from robot.result.model import StatusMixin
|
|
25
26
|
from robot.running import TestCase as RunningTestCase
|
|
26
27
|
from robot.running import TestSuite
|
|
@@ -51,6 +52,34 @@ else:
|
|
|
51
52
|
return for_.variables # type: ignore[attr-defined,no-any-return,unused-ignore]
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
# RF 7.5 added `[Metadata]` to tests and tasks; `TestCase.metadata` does not
|
|
56
|
+
# exist before.
|
|
57
|
+
SUPPORTS_TEST_METADATA = RF_VERSION >= (7, 5)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def named_metadata(metadata: Mapping[str, str]) -> Dict[str, str]:
|
|
61
|
+
"""The entries of a suite's or test's metadata that have a name.
|
|
62
|
+
|
|
63
|
+
A `Metadata` setting with nothing after it gives an entry with an empty
|
|
64
|
+
name and an empty value in Robot's model. That is no metadata: Robot
|
|
65
|
+
itself leaves it out of `output.xml` (but not out of `output.json`).
|
|
66
|
+
"""
|
|
67
|
+
return {name: value for name, value in metadata.items() if name}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Shared by `discover` and `results` so both emit the same name → value
|
|
71
|
+
# mapping (author's spelling, RF's order).
|
|
72
|
+
if SUPPORTS_TEST_METADATA:
|
|
73
|
+
|
|
74
|
+
def get_test_metadata(test: ModelTestCase) -> Dict[str, str]:
|
|
75
|
+
return named_metadata(test.metadata) # type: ignore[attr-defined,unused-ignore]
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
|
|
79
|
+
def get_test_metadata(test: ModelTestCase) -> Dict[str, str]:
|
|
80
|
+
return {}
|
|
81
|
+
|
|
82
|
+
|
|
54
83
|
@dataclass(frozen=True)
|
|
55
84
|
class SearchMatcher:
|
|
56
85
|
general: Callable[[Optional[str]], bool]
|
|
@@ -296,7 +325,9 @@ class SearchModifier(SuiteVisitor):
|
|
|
296
325
|
A test matches if any of the following matches the predicate:
|
|
297
326
|
- name, full name, source path
|
|
298
327
|
- documentation, template name (running model only), timeout setting
|
|
328
|
+
- the test's own message, e.g. its failure message (result-tree only)
|
|
299
329
|
- any of its tags (with Robot's tag normalisation)
|
|
330
|
+
- any of its `[Metadata]` names or values (Robot Framework 7.5+)
|
|
300
331
|
- any keyword name, keyword argument, assigned variable, FOR/WHILE
|
|
301
332
|
condition or VAR/RETURN/EXCEPT/GROUP element inside the test body,
|
|
302
333
|
setup or teardown
|
|
@@ -328,10 +359,15 @@ class SearchModifier(SuiteVisitor):
|
|
|
328
359
|
# don't preserve it, so guard with isinstance.
|
|
329
360
|
if isinstance(test, RunningTestCase) and self.matcher.general(test.template):
|
|
330
361
|
return True
|
|
362
|
+
if isinstance(test, ResultTestCase) and self.matcher.general(test.message):
|
|
363
|
+
return True
|
|
331
364
|
if test.timeout is not None and self.matcher.general(str(test.timeout)):
|
|
332
365
|
return True
|
|
333
366
|
if any(self.matcher.tag(str(t)) for t in test.tags):
|
|
334
367
|
return True
|
|
368
|
+
for name, value in get_test_metadata(test).items():
|
|
369
|
+
if self.matcher.general(name) or self.matcher.general(value):
|
|
370
|
+
return True
|
|
335
371
|
if test.has_setup and self.matcher.matches_body([test.setup]):
|
|
336
372
|
return True
|
|
337
373
|
if test.has_teardown and self.matcher.matches_body([test.teardown]):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Data models emitted by `robotcode discover`.
|
|
2
2
|
|
|
3
3
|
All models inherit from `CamelSnakeMixin` so the JSON output uses
|
|
4
|
-
camelCase keys (`
|
|
4
|
+
camelCase keys (`relSource`, `supportsParseInclude`, `robotVersionString`, …)
|
|
5
5
|
consistent with the `results` family. Editor integrations and CI
|
|
6
6
|
recipes rely on that shape.
|
|
7
7
|
"""
|
|
@@ -27,6 +27,7 @@ class TestItem(CamelSnakeMixin):
|
|
|
27
27
|
description: Optional[str] = None
|
|
28
28
|
range: Optional[Range] = None
|
|
29
29
|
tags: Optional[List[str]] = None
|
|
30
|
+
metadata: Optional[Dict[str, str]] = None
|
|
30
31
|
error: Optional[str] = None
|
|
31
32
|
rpa: Optional[bool] = None
|
|
32
33
|
|
|
@@ -54,6 +55,15 @@ class TagsResult(CamelSnakeMixin):
|
|
|
54
55
|
filters_applied: Optional[Dict[str, str]] = None
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
@dataclass
|
|
59
|
+
class MetadataResult(CamelSnakeMixin):
|
|
60
|
+
"""`discover metadata`: name → value → items, for tests/tasks and for suites."""
|
|
61
|
+
|
|
62
|
+
metadata: Dict[str, Dict[str, List[TestItem]]]
|
|
63
|
+
suite_metadata: Dict[str, Dict[str, List[TestItem]]]
|
|
64
|
+
filters_applied: Optional[Dict[str, str]] = None
|
|
65
|
+
|
|
66
|
+
|
|
57
67
|
@dataclass
|
|
58
68
|
class Info(CamelSnakeMixin):
|
|
59
69
|
robot_version_string: str
|
|
@@ -25,6 +25,7 @@ from .._markdown import (
|
|
|
25
25
|
filters_footer_md,
|
|
26
26
|
highlight_md,
|
|
27
27
|
md_escape,
|
|
28
|
+
metadata_md,
|
|
28
29
|
path_paren,
|
|
29
30
|
)
|
|
30
31
|
from ._models import Info, Statistics, TestItem
|
|
@@ -301,6 +302,7 @@ def render_tests_or_tasks(
|
|
|
301
302
|
*,
|
|
302
303
|
selected_type: str,
|
|
303
304
|
show_tags: bool,
|
|
305
|
+
show_metadata: bool = False,
|
|
304
306
|
full_paths: bool,
|
|
305
307
|
highlight: Optional[Callable[[str], str]] = None,
|
|
306
308
|
search_substring: Optional[str] = None,
|
|
@@ -325,6 +327,8 @@ def render_tests_or_tasks(
|
|
|
325
327
|
if show_tags and item.tags:
|
|
326
328
|
tags = ", ".join(f"`{_hi(str(tag), highlight)}`" for tag in sorted(item.tags))
|
|
327
329
|
lines.append(f" - _Tags:_ {tags}")
|
|
330
|
+
if show_metadata and item.metadata:
|
|
331
|
+
lines.append(f" - _Metadata:_ {metadata_md(item.metadata, highlight)}")
|
|
328
332
|
body_md = "\n".join(lines)
|
|
329
333
|
|
|
330
334
|
return _block_md(
|
|
@@ -391,6 +395,88 @@ def render_tags(
|
|
|
391
395
|
)
|
|
392
396
|
|
|
393
397
|
|
|
398
|
+
# ---------------------------------------------------------------------------
|
|
399
|
+
# `discover metadata`
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _metadata_section_md(
|
|
404
|
+
index: Dict[str, Dict[str, List[TestItem]]],
|
|
405
|
+
*,
|
|
406
|
+
show_values: bool,
|
|
407
|
+
show_item: Callable[[TestItem], bool],
|
|
408
|
+
full_paths: bool,
|
|
409
|
+
highlight: Optional[Callable[[str], str]],
|
|
410
|
+
) -> List[str]:
|
|
411
|
+
def item_md(item: TestItem) -> str:
|
|
412
|
+
paren = _item_paren(item, full_paths=full_paths, with_line=item.type != "suite")
|
|
413
|
+
return f"**{_hi(item.longname, highlight)}**{paren}"
|
|
414
|
+
|
|
415
|
+
lines: List[str] = []
|
|
416
|
+
for name, values in index.items():
|
|
417
|
+
lines.append(f"- **{_hi(name, highlight)}**")
|
|
418
|
+
if show_values:
|
|
419
|
+
for value, items in values.items():
|
|
420
|
+
lines.append(f" - {_hi(value, highlight) if value else '_(no value)_'}")
|
|
421
|
+
lines.extend(f" - {item_md(item)}" for item in items if show_item(item))
|
|
422
|
+
else:
|
|
423
|
+
# Each item once under its name, even when it has several values.
|
|
424
|
+
listed: List[TestItem] = []
|
|
425
|
+
for items in values.values():
|
|
426
|
+
listed.extend(item for item in items if show_item(item) and all(item is not i for i in listed))
|
|
427
|
+
lines.extend(f" - {item_md(item)}" for item in listed)
|
|
428
|
+
return lines
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def render_metadata(
|
|
432
|
+
test_metadata: Dict[str, Dict[str, List[TestItem]]],
|
|
433
|
+
suite_metadata: Dict[str, Dict[str, List[TestItem]]],
|
|
434
|
+
statistics: Statistics,
|
|
435
|
+
*,
|
|
436
|
+
show_values: bool,
|
|
437
|
+
show_tests: bool,
|
|
438
|
+
show_tasks: bool,
|
|
439
|
+
show_suites: bool,
|
|
440
|
+
full_paths: bool,
|
|
441
|
+
highlight: Optional[Callable[[str], str]] = None,
|
|
442
|
+
search_substring: Optional[str] = None,
|
|
443
|
+
search_regex: Optional[str] = None,
|
|
444
|
+
diagnostics: Optional[Dict[str, List[Diagnostic]]] = None,
|
|
445
|
+
show_diagnostics: bool = False,
|
|
446
|
+
root_folder: Optional[Path] = None,
|
|
447
|
+
) -> str:
|
|
448
|
+
def show_test_or_task(item: TestItem) -> bool:
|
|
449
|
+
if show_tests == show_tasks:
|
|
450
|
+
return show_tests
|
|
451
|
+
return item.type == ("test" if show_tests else "task")
|
|
452
|
+
|
|
453
|
+
parts: List[str] = []
|
|
454
|
+
for heading, index, show_item in (
|
|
455
|
+
("Tests and Tasks", test_metadata, show_test_or_task),
|
|
456
|
+
("Suites", suite_metadata, lambda _item: show_suites),
|
|
457
|
+
):
|
|
458
|
+
if not index:
|
|
459
|
+
continue
|
|
460
|
+
if parts:
|
|
461
|
+
parts.append("")
|
|
462
|
+
parts += [f"## {heading}", ""]
|
|
463
|
+
parts += _metadata_section_md(
|
|
464
|
+
index, show_values=show_values, show_item=show_item, full_paths=full_paths, highlight=highlight
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
return _block_md(
|
|
468
|
+
heading="Metadata",
|
|
469
|
+
body_md="\n".join(parts) if parts else "_(no metadata matched)_",
|
|
470
|
+
statistics=statistics,
|
|
471
|
+
diagnostics=diagnostics,
|
|
472
|
+
show_diagnostics=show_diagnostics,
|
|
473
|
+
full_paths=full_paths,
|
|
474
|
+
root_folder=root_folder,
|
|
475
|
+
search_substring=search_substring,
|
|
476
|
+
search_regex=search_regex,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
394
480
|
# ---------------------------------------------------------------------------
|
|
395
481
|
# `discover all` — workspace → suites → tests/tasks tree as nested list
|
|
396
482
|
# ---------------------------------------------------------------------------
|
|
@@ -401,6 +487,7 @@ def render_all(
|
|
|
401
487
|
statistics: Statistics,
|
|
402
488
|
*,
|
|
403
489
|
show_tags: bool,
|
|
490
|
+
show_metadata: bool = False,
|
|
404
491
|
full_paths: bool,
|
|
405
492
|
highlight: Optional[Callable[[str], str]] = None,
|
|
406
493
|
search_substring: Optional[str] = None,
|
|
@@ -412,7 +499,15 @@ def render_all(
|
|
|
412
499
|
"""The full discovery tree: workspace at depth 0, suites at
|
|
413
500
|
increasing depth, tests/tasks as leaves with their line number."""
|
|
414
501
|
tree_lines: List[str] = []
|
|
415
|
-
_emit_tree(
|
|
502
|
+
_emit_tree(
|
|
503
|
+
root,
|
|
504
|
+
tree_lines,
|
|
505
|
+
depth=0,
|
|
506
|
+
show_tags=show_tags,
|
|
507
|
+
show_metadata=show_metadata,
|
|
508
|
+
full_paths=full_paths,
|
|
509
|
+
highlight=highlight,
|
|
510
|
+
)
|
|
416
511
|
return _block_md(
|
|
417
512
|
heading="All",
|
|
418
513
|
body_md="\n".join(tree_lines),
|
|
@@ -432,6 +527,7 @@ def _emit_tree(
|
|
|
432
527
|
*,
|
|
433
528
|
depth: int,
|
|
434
529
|
show_tags: bool,
|
|
530
|
+
show_metadata: bool,
|
|
435
531
|
full_paths: bool,
|
|
436
532
|
highlight: Optional[Callable[[str], str]] = None,
|
|
437
533
|
) -> None:
|
|
@@ -443,5 +539,15 @@ def _emit_tree(
|
|
|
443
539
|
if is_leaf and show_tags and item.tags:
|
|
444
540
|
tags = ", ".join(f"`{_hi(str(tag), highlight)}`" for tag in sorted(item.tags))
|
|
445
541
|
out.append(f"{indent} - _Tags:_ {tags}")
|
|
542
|
+
if is_leaf and show_metadata and item.metadata:
|
|
543
|
+
out.append(f"{indent} - _Metadata:_ {metadata_md(item.metadata, highlight)}")
|
|
446
544
|
for child in item.children or []:
|
|
447
|
-
_emit_tree(
|
|
545
|
+
_emit_tree(
|
|
546
|
+
child,
|
|
547
|
+
out,
|
|
548
|
+
depth=depth + 1,
|
|
549
|
+
show_tags=show_tags,
|
|
550
|
+
show_metadata=show_metadata,
|
|
551
|
+
full_paths=full_paths,
|
|
552
|
+
highlight=highlight,
|
|
553
|
+
)
|