robotframework-events 0.1.0__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.
- robotframework_events/__init__.py +30 -0
- robotframework_events/__main__.py +5 -0
- robotframework_events/cli.py +176 -0
- robotframework_events/convert.py +172 -0
- robotframework_events/events-v1.schema.json +175 -0
- robotframework_events/events.py +248 -0
- robotframework_events/listener.py +523 -0
- robotframework_events/py.typed +0 -0
- robotframework_events/reader.py +118 -0
- robotframework_events/state.py +207 -0
- robotframework_events-0.1.0.dist-info/METADATA +112 -0
- robotframework_events-0.1.0.dist-info/RECORD +16 -0
- robotframework_events-0.1.0.dist-info/WHEEL +5 -0
- robotframework_events-0.1.0.dist-info/entry_points.txt +2 -0
- robotframework_events-0.1.0.dist-info/licenses/LICENSE +21 -0
- robotframework_events-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Robot Framework execution events as JSON Lines.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
robot --listener robotframework_events.Listener tests/
|
|
6
|
+
robot --listener robotframework_events.Listener:file=results/events.jsonl tests/
|
|
7
|
+
|
|
8
|
+
Programs that run Robot Framework in the same process (for example robotcode's
|
|
9
|
+
debugger) can receive every event with :func:`add_callback`. Programs that read
|
|
10
|
+
events files use :func:`read_events` and :class:`RunState`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .convert import convert
|
|
14
|
+
from .events import Event, from_dict, schema
|
|
15
|
+
from .listener import Listener, add_callback, remove_callback
|
|
16
|
+
from .reader import read_events
|
|
17
|
+
from .state import RunState
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Event",
|
|
21
|
+
"Listener",
|
|
22
|
+
"RunState",
|
|
23
|
+
"add_callback",
|
|
24
|
+
"convert",
|
|
25
|
+
"from_dict",
|
|
26
|
+
"read_events",
|
|
27
|
+
"remove_callback",
|
|
28
|
+
"schema",
|
|
29
|
+
]
|
|
30
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""The ``rfevents`` command: ``tail`` shows a run in the terminal, ``convert`` builds events from a result file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from typing import TextIO
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .convert import convert
|
|
15
|
+
from .events import (
|
|
16
|
+
Event,
|
|
17
|
+
MessageLogged,
|
|
18
|
+
RunEnded,
|
|
19
|
+
StepStarted,
|
|
20
|
+
SuiteEnded,
|
|
21
|
+
SuiteStarted,
|
|
22
|
+
TestEnded,
|
|
23
|
+
TestStarted,
|
|
24
|
+
)
|
|
25
|
+
from .listener import LEVELS
|
|
26
|
+
from .reader import read_events
|
|
27
|
+
from .state import RunState, step_label
|
|
28
|
+
|
|
29
|
+
COLORS = {"PASS": "32", "FAIL": "31", "SKIP": "33", "NOT RUN": "90", "WARN": "33", "ERROR": "31"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv: list[str] | None = None) -> int:
|
|
33
|
+
parser = argparse.ArgumentParser(prog="rfevents", description="Robot Framework execution events.")
|
|
34
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
35
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
36
|
+
|
|
37
|
+
tail = commands.add_parser("tail", help="show a run from its events file, readable")
|
|
38
|
+
tail.add_argument("file", help="events file (.jsonl or .jsonl.gz)")
|
|
39
|
+
tail.add_argument("-f", "--follow", action="store_true", help="wait for new events until the run ends")
|
|
40
|
+
tail.add_argument(
|
|
41
|
+
"--steps",
|
|
42
|
+
type=int,
|
|
43
|
+
default=1,
|
|
44
|
+
metavar="DEPTH",
|
|
45
|
+
help="show steps this many levels below each test, 0 for none (default 1)",
|
|
46
|
+
)
|
|
47
|
+
tail.add_argument(
|
|
48
|
+
"--level",
|
|
49
|
+
default="WARN",
|
|
50
|
+
type=str.upper,
|
|
51
|
+
choices=list(LEVELS),
|
|
52
|
+
help="show log messages from this level (default WARN)",
|
|
53
|
+
)
|
|
54
|
+
tail.add_argument("--color", choices=["auto", "always", "never"], default="auto")
|
|
55
|
+
|
|
56
|
+
conv = commands.add_parser("convert", help="build events from output.xml or output.json")
|
|
57
|
+
conv.add_argument("output", help="output.xml or output.json")
|
|
58
|
+
conv.add_argument("-o", "--outfile", help="events file to write (default: standard output)")
|
|
59
|
+
|
|
60
|
+
args = parser.parse_args(argv)
|
|
61
|
+
if isinstance(sys.stdout, io.TextIOWrapper):
|
|
62
|
+
sys.stdout.reconfigure(errors="replace")
|
|
63
|
+
try:
|
|
64
|
+
if args.command == "tail":
|
|
65
|
+
color = args.color == "always" or (
|
|
66
|
+
args.color == "auto" and sys.stdout.isatty() and "NO_COLOR" not in os.environ
|
|
67
|
+
)
|
|
68
|
+
Tail(sys.stdout, args.steps, args.level, color).show(read_events(args.file, follow=args.follow))
|
|
69
|
+
else:
|
|
70
|
+
write_converted(args.output, args.outfile)
|
|
71
|
+
except KeyboardInterrupt:
|
|
72
|
+
return 130
|
|
73
|
+
except OutputClosed:
|
|
74
|
+
# keep Python from failing again when it flushes stdout at exit
|
|
75
|
+
sys.stdout = open(os.devnull, "w") # noqa: SIM115
|
|
76
|
+
return 0
|
|
77
|
+
except (OSError, ValueError) as e:
|
|
78
|
+
print(f"rfevents: {e}", file=sys.stderr)
|
|
79
|
+
return 2
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def write_converted(output: str, outfile: str | None) -> None:
|
|
84
|
+
encode = json.JSONEncoder(ensure_ascii=False, separators=(",", ":")).encode
|
|
85
|
+
lines = (encode(event) + "\n" for event in convert(output))
|
|
86
|
+
if outfile is None:
|
|
87
|
+
sys.stdout.writelines(lines)
|
|
88
|
+
return
|
|
89
|
+
with open(outfile, "w", encoding="utf-8") as file:
|
|
90
|
+
file.writelines(lines)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class OutputClosed(Exception):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Tail:
|
|
98
|
+
"""Prints suites, tests, top-level steps, failures and warnings as they happen."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, out: TextIO, steps: int = 1, level: str = "WARN", color: bool = False) -> None:
|
|
101
|
+
self.out = out
|
|
102
|
+
self.steps = steps
|
|
103
|
+
self.level = LEVELS[level]
|
|
104
|
+
self.color = color
|
|
105
|
+
self.state = RunState()
|
|
106
|
+
|
|
107
|
+
def show(self, events: Iterable[Event]) -> None:
|
|
108
|
+
for event in events:
|
|
109
|
+
self.state.apply(event)
|
|
110
|
+
self.print_event(event)
|
|
111
|
+
|
|
112
|
+
def print_event(self, event: Event) -> None:
|
|
113
|
+
if isinstance(event, SuiteStarted):
|
|
114
|
+
self.line(self.indent(event.id), event.longname if event.parent is None else event.name)
|
|
115
|
+
elif isinstance(event, TestStarted):
|
|
116
|
+
self.line(self.indent(event.id), event.name)
|
|
117
|
+
elif isinstance(event, StepStarted):
|
|
118
|
+
depth = self.step_depth(event.id)
|
|
119
|
+
if depth <= self.steps:
|
|
120
|
+
node = self.state.nodes[event.id]
|
|
121
|
+
kind = "" if event.kind == "KEYWORD" else ("ITER" if event.kind == "ITERATION" else event.kind)
|
|
122
|
+
parts = [kind, step_label(node), *(event.args or [])]
|
|
123
|
+
self.line(self.indent(event.id), " ".join(p for p in parts if p))
|
|
124
|
+
elif isinstance(event, TestEnded):
|
|
125
|
+
node = self.state.nodes[event.id]
|
|
126
|
+
self.line(
|
|
127
|
+
self.indent(event.id), self.paint(node.status, node.status), node.name, f"({event.elapsed:.2f} s)"
|
|
128
|
+
)
|
|
129
|
+
if node.message:
|
|
130
|
+
self.line(self.indent(event.id) + " ", node.message)
|
|
131
|
+
elif isinstance(event, SuiteEnded):
|
|
132
|
+
node = self.state.nodes[event.id]
|
|
133
|
+
if node.parent is not None:
|
|
134
|
+
return
|
|
135
|
+
s = event.stats
|
|
136
|
+
self.line(
|
|
137
|
+
"",
|
|
138
|
+
self.paint(node.status, node.status),
|
|
139
|
+
getattr(node.started, "longname", node.name),
|
|
140
|
+
f"{s['total']} tests, {s['passed']} passed, {s['failed']} failed, {s['skipped']} skipped",
|
|
141
|
+
)
|
|
142
|
+
elif isinstance(event, MessageLogged):
|
|
143
|
+
if LEVELS.get(event.level, 2) >= self.level and event.parent in self.state.nodes:
|
|
144
|
+
self.line(
|
|
145
|
+
self.indent(self.shown(event.parent)) + " ",
|
|
146
|
+
self.paint(event.level, f"[{event.level}]"),
|
|
147
|
+
event.text or "",
|
|
148
|
+
)
|
|
149
|
+
elif isinstance(event, RunEnded):
|
|
150
|
+
self.out.flush()
|
|
151
|
+
|
|
152
|
+
def indent(self, node_id: str | None) -> str:
|
|
153
|
+
return " " * (len(self.state.path(node_id or "")) - 1)
|
|
154
|
+
|
|
155
|
+
def shown(self, node_id: str | None) -> str | None:
|
|
156
|
+
"""The node, or its nearest ancestor that is printed."""
|
|
157
|
+
for node in reversed(self.state.path(node_id or "")):
|
|
158
|
+
if node.kind in ("SUITE", "TEST") or self.step_depth(node.id) <= self.steps:
|
|
159
|
+
return node.id
|
|
160
|
+
return node_id
|
|
161
|
+
|
|
162
|
+
def step_depth(self, node_id: str) -> int:
|
|
163
|
+
"""1 for steps directly in a test or suite, 2 for their children, ..."""
|
|
164
|
+
return sum(1 for node in self.state.path(node_id) if node.kind not in ("SUITE", "TEST"))
|
|
165
|
+
|
|
166
|
+
def paint(self, status: str, text: str) -> str:
|
|
167
|
+
code = COLORS.get(status)
|
|
168
|
+
return f"\033[{code}m{text}\033[0m" if self.color and code else text
|
|
169
|
+
|
|
170
|
+
def line(self, indent: str, *parts: str) -> None:
|
|
171
|
+
self.out.write(indent + " ".join(p for p in parts if p) + "\n")
|
|
172
|
+
self.out.flush()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
sys.exit(main())
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Events built from an existing ``output.xml`` or ``output.json``.
|
|
2
|
+
|
|
3
|
+
Gives events for runs that were recorded without the listener, so they can be replayed.
|
|
4
|
+
What the result file does not store is left out: steps have no ``source`` and ``lineno``,
|
|
5
|
+
``run.started`` has no ``pid`` and ``cwd``, and children of ``robot:flatten`` keywords are
|
|
6
|
+
missing, as in ``log.html``. Statuses are final, so there are no ``status.changed`` events.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import uuid
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from robot.api import ExecutionResult
|
|
19
|
+
from robot.result import TestCase, TestSuite
|
|
20
|
+
|
|
21
|
+
from .events import FORMAT_VERSION
|
|
22
|
+
from .listener import ROOT_TYPES, _stats, step_fields
|
|
23
|
+
|
|
24
|
+
GENERATOR = re.compile(r"Robot (\S+) \(\w+ (\S+)")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def convert(path: str | os.PathLike[str], *, run: str | None = None, max_text: int = 4096) -> Iterator[dict[str, Any]]:
|
|
28
|
+
"""Yields the events of a result file as dicts, in the order a live run emits them."""
|
|
29
|
+
result = ExecutionResult(str(path))
|
|
30
|
+
return _Converter(max_text).events(result, str(path), run or str(uuid.uuid4()))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _time(value: datetime | None) -> float | None:
|
|
34
|
+
return round(value.timestamp(), 3) if value else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _Counter:
|
|
38
|
+
__slots__ = ("children", "id")
|
|
39
|
+
|
|
40
|
+
def __init__(self, id: str) -> None:
|
|
41
|
+
self.id = id
|
|
42
|
+
self.children = 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _Converter:
|
|
46
|
+
def __init__(self, max_text: int) -> None:
|
|
47
|
+
self.max_text = max_text
|
|
48
|
+
self.seq = 0
|
|
49
|
+
self.t = 0.0
|
|
50
|
+
|
|
51
|
+
def emit(self, type_: str, t: float | None, **fields: Any) -> dict[str, Any]:
|
|
52
|
+
self.seq += 1
|
|
53
|
+
# NOT RUN items have no times; keep time moving forward
|
|
54
|
+
self.t = max(self.t, t) if t is not None else self.t
|
|
55
|
+
event: dict[str, Any] = {"v": FORMAT_VERSION, "seq": self.seq, "t": self.t, "type": type_}
|
|
56
|
+
event.update((k, v) for k, v in fields.items() if v is not None and v != "")
|
|
57
|
+
return event
|
|
58
|
+
|
|
59
|
+
def events(self, result: Any, path: str, run: str) -> Iterator[dict[str, Any]]:
|
|
60
|
+
suite = result.suite
|
|
61
|
+
match = GENERATOR.match(getattr(result, "generator", "") or "")
|
|
62
|
+
self.t = _time(suite.start_time) or 0.0
|
|
63
|
+
yield self.emit(
|
|
64
|
+
"run.started",
|
|
65
|
+
self.t,
|
|
66
|
+
run=run,
|
|
67
|
+
rf=match.group(1) if match else "unknown",
|
|
68
|
+
python=match.group(2) if match else None,
|
|
69
|
+
)
|
|
70
|
+
yield from self.suite(suite, "s1", None)
|
|
71
|
+
yield self.emit("resultfile.created", None, kind="OUTPUT", path=path)
|
|
72
|
+
yield self.emit("run.ended", None, status=suite.status, stats=_stats(suite))
|
|
73
|
+
|
|
74
|
+
def suite(self, suite: TestSuite, id: str, parent: str | None) -> Iterator[dict[str, Any]]:
|
|
75
|
+
yield self.emit(
|
|
76
|
+
"suite.started",
|
|
77
|
+
_time(suite.start_time),
|
|
78
|
+
id=id,
|
|
79
|
+
parent=parent,
|
|
80
|
+
name=suite.name,
|
|
81
|
+
longname=suite.full_name,
|
|
82
|
+
source=str(suite.source) if suite.source else None,
|
|
83
|
+
tests=suite.test_count,
|
|
84
|
+
)
|
|
85
|
+
counter = _Counter(id)
|
|
86
|
+
if suite.has_setup:
|
|
87
|
+
yield from self.step(suite.setup, counter, id)
|
|
88
|
+
for index, child in enumerate(suite.suites, 1):
|
|
89
|
+
yield from self.suite(child, f"{id}-s{index}", id)
|
|
90
|
+
for index, test in enumerate(suite.tests, 1):
|
|
91
|
+
yield from self.test(test, f"{id}-t{index}", id)
|
|
92
|
+
if suite.has_teardown:
|
|
93
|
+
yield from self.step(suite.teardown, counter, id)
|
|
94
|
+
yield self.emit(
|
|
95
|
+
"suite.ended",
|
|
96
|
+
_time(suite.end_time),
|
|
97
|
+
id=id,
|
|
98
|
+
status=suite.status,
|
|
99
|
+
message=suite.message,
|
|
100
|
+
elapsed=round(suite.elapsed_time.total_seconds(), 3),
|
|
101
|
+
stats=_stats(suite),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def test(self, test: TestCase, id: str, parent: str) -> Iterator[dict[str, Any]]:
|
|
105
|
+
yield self.emit(
|
|
106
|
+
"test.started",
|
|
107
|
+
_time(test.start_time),
|
|
108
|
+
id=id,
|
|
109
|
+
parent=parent,
|
|
110
|
+
name=test.name,
|
|
111
|
+
longname=test.full_name,
|
|
112
|
+
source=str(test.source) if test.source else None,
|
|
113
|
+
lineno=test.lineno,
|
|
114
|
+
tags=list(test.tags) or None,
|
|
115
|
+
)
|
|
116
|
+
counter = _Counter(id)
|
|
117
|
+
if test.has_setup:
|
|
118
|
+
yield from self.step(test.setup, counter, id)
|
|
119
|
+
yield from self.body(test.body, counter, id)
|
|
120
|
+
if test.has_teardown:
|
|
121
|
+
yield from self.step(test.teardown, counter, id)
|
|
122
|
+
yield self.emit(
|
|
123
|
+
"test.ended",
|
|
124
|
+
_time(test.end_time),
|
|
125
|
+
id=id,
|
|
126
|
+
status=test.status,
|
|
127
|
+
message=test.message,
|
|
128
|
+
elapsed=round(test.elapsed_time.total_seconds(), 3),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def body(self, items: Any, counter: _Counter, parent: str) -> Iterator[dict[str, Any]]:
|
|
132
|
+
for item in items:
|
|
133
|
+
if item.type == "MESSAGE":
|
|
134
|
+
text = item.message or ""
|
|
135
|
+
truncated = len(text) > self.max_text
|
|
136
|
+
yield self.emit(
|
|
137
|
+
"message.logged",
|
|
138
|
+
_time(item.timestamp),
|
|
139
|
+
parent=parent,
|
|
140
|
+
level=item.level,
|
|
141
|
+
text=text[: self.max_text] if truncated else text,
|
|
142
|
+
truncated=True if truncated else None,
|
|
143
|
+
html=True if item.html else None,
|
|
144
|
+
)
|
|
145
|
+
elif item.type in ROOT_TYPES:
|
|
146
|
+
# IF/TRY branches are numbered as children of the root's parent
|
|
147
|
+
yield from self.body(item.body, counter, parent)
|
|
148
|
+
else:
|
|
149
|
+
yield from self.step(item, counter, parent)
|
|
150
|
+
|
|
151
|
+
def step(self, item: Any, counter: _Counter, parent: str) -> Iterator[dict[str, Any]]:
|
|
152
|
+
counter.children += 1
|
|
153
|
+
id = f"{counter.id}-k{counter.children}"
|
|
154
|
+
kind = item.type
|
|
155
|
+
yield self.emit(
|
|
156
|
+
"step.started", _time(item.start_time), id=id, parent=parent, kind=kind, **step_fields(kind, item)
|
|
157
|
+
)
|
|
158
|
+
own = _Counter(id)
|
|
159
|
+
if getattr(item, "has_setup", False):
|
|
160
|
+
yield from self.step(item.setup, own, id)
|
|
161
|
+
if hasattr(item, "body"):
|
|
162
|
+
yield from self.body(item.body, own, id)
|
|
163
|
+
if getattr(item, "has_teardown", False):
|
|
164
|
+
yield from self.step(item.teardown, own, id)
|
|
165
|
+
yield self.emit(
|
|
166
|
+
"step.ended",
|
|
167
|
+
_time(item.end_time),
|
|
168
|
+
id=id,
|
|
169
|
+
status=item.status,
|
|
170
|
+
message=item.message,
|
|
171
|
+
elapsed=round(item.elapsed_time.total_seconds(), 4),
|
|
172
|
+
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/timdegroot1996/robotframework-events/blob/main/src/robotframework_events/events-v1.schema.json",
|
|
4
|
+
"title": "Robot Framework execution event, format v1",
|
|
5
|
+
"description": "One line of an events JSONL file. Fields with no value are left out. Readers ignore unknown fields and unknown event types.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["v", "seq", "t", "type"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"v": { "description": "Format version.", "const": 1 },
|
|
10
|
+
"seq": { "description": "1, 2, 3, ... per stream, without gaps.", "type": "integer", "minimum": 1 },
|
|
11
|
+
"t": { "description": "Unix time in seconds.", "type": "number" },
|
|
12
|
+
"type": { "type": "string" }
|
|
13
|
+
},
|
|
14
|
+
"allOf": [
|
|
15
|
+
{ "if": { "properties": { "type": { "const": "run.started" } } }, "then": { "$ref": "#/$defs/run.started" } },
|
|
16
|
+
{ "if": { "properties": { "type": { "const": "run.ended" } } }, "then": { "$ref": "#/$defs/run.ended" } },
|
|
17
|
+
{ "if": { "properties": { "type": { "const": "suite.started" } } }, "then": { "$ref": "#/$defs/suite.started" } },
|
|
18
|
+
{ "if": { "properties": { "type": { "const": "suite.ended" } } }, "then": { "$ref": "#/$defs/suite.ended" } },
|
|
19
|
+
{ "if": { "properties": { "type": { "const": "test.started" } } }, "then": { "$ref": "#/$defs/test.started" } },
|
|
20
|
+
{ "if": { "properties": { "type": { "const": "test.ended" } } }, "then": { "$ref": "#/$defs/test.ended" } },
|
|
21
|
+
{ "if": { "properties": { "type": { "const": "step.started" } } }, "then": { "$ref": "#/$defs/step.started" } },
|
|
22
|
+
{ "if": { "properties": { "type": { "const": "step.ended" } } }, "then": { "$ref": "#/$defs/step.ended" } },
|
|
23
|
+
{ "if": { "properties": { "type": { "const": "message.logged" } } }, "then": { "$ref": "#/$defs/message.logged" } },
|
|
24
|
+
{ "if": { "properties": { "type": { "const": "status.changed" } } }, "then": { "$ref": "#/$defs/status.changed" } },
|
|
25
|
+
{ "if": { "properties": { "type": { "const": "resultfile.created" } } }, "then": { "$ref": "#/$defs/resultfile.created" } }
|
|
26
|
+
],
|
|
27
|
+
"$defs": {
|
|
28
|
+
"id": {
|
|
29
|
+
"description": "Robot Framework's item id, as in output.xml and log.html#<id>.",
|
|
30
|
+
"type": "string",
|
|
31
|
+
"pattern": "^s\\d+(-s\\d+)*(-t\\d+)?(-k\\d+)*$"
|
|
32
|
+
},
|
|
33
|
+
"status": { "enum": ["PASS", "FAIL", "SKIP", "NOT RUN", "NOT SET"] },
|
|
34
|
+
"stats": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"required": ["total", "passed", "failed", "skipped"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"total": { "type": "integer", "minimum": 0 },
|
|
39
|
+
"passed": { "type": "integer", "minimum": 0 },
|
|
40
|
+
"failed": { "type": "integer", "minimum": 0 },
|
|
41
|
+
"skipped": { "type": "integer", "minimum": 0 }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"strings": { "type": "array", "items": { "type": "string" } },
|
|
45
|
+
|
|
46
|
+
"run.started": {
|
|
47
|
+
"description": "First event of a stream. 'run' identifies the run; it is not repeated in later events. 'python', 'pid' and 'cwd' are missing in events converted from a result file.",
|
|
48
|
+
"required": ["run", "rf"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"run": { "type": "string", "format": "uuid" },
|
|
51
|
+
"rf": { "description": "Robot Framework version.", "type": "string" },
|
|
52
|
+
"python": { "description": "Python version.", "type": "string" },
|
|
53
|
+
"pid": { "type": "integer" },
|
|
54
|
+
"cwd": { "type": "string" }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"run.ended": {
|
|
58
|
+
"description": "Last event of a stream.",
|
|
59
|
+
"required": ["status", "stats"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"status": { "$ref": "#/$defs/status" },
|
|
62
|
+
"stats": { "$ref": "#/$defs/stats" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"suite.started": {
|
|
66
|
+
"required": ["id", "name", "longname", "tests"],
|
|
67
|
+
"properties": {
|
|
68
|
+
"id": { "$ref": "#/$defs/id" },
|
|
69
|
+
"parent": { "description": "Left out for the top-level suite.", "$ref": "#/$defs/id" },
|
|
70
|
+
"name": { "type": "string" },
|
|
71
|
+
"longname": { "type": "string" },
|
|
72
|
+
"source": { "type": "string" },
|
|
73
|
+
"tests": { "description": "Number of tests in the suite and its child suites.", "type": "integer", "minimum": 0 }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"suite.ended": {
|
|
77
|
+
"required": ["id", "status", "elapsed", "stats"],
|
|
78
|
+
"properties": {
|
|
79
|
+
"id": { "$ref": "#/$defs/id" },
|
|
80
|
+
"status": { "$ref": "#/$defs/status" },
|
|
81
|
+
"message": { "type": "string" },
|
|
82
|
+
"elapsed": { "description": "Seconds.", "type": "number", "minimum": 0 },
|
|
83
|
+
"stats": { "$ref": "#/$defs/stats" }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"test.started": {
|
|
87
|
+
"required": ["id", "parent", "name", "longname"],
|
|
88
|
+
"properties": {
|
|
89
|
+
"id": { "$ref": "#/$defs/id" },
|
|
90
|
+
"parent": { "$ref": "#/$defs/id" },
|
|
91
|
+
"name": { "type": "string" },
|
|
92
|
+
"longname": { "type": "string" },
|
|
93
|
+
"source": { "type": "string" },
|
|
94
|
+
"lineno": { "type": "integer" },
|
|
95
|
+
"tags": { "$ref": "#/$defs/strings" }
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"test.ended": {
|
|
99
|
+
"required": ["id", "status", "elapsed"],
|
|
100
|
+
"properties": {
|
|
101
|
+
"id": { "$ref": "#/$defs/id" },
|
|
102
|
+
"status": { "$ref": "#/$defs/status" },
|
|
103
|
+
"message": { "type": "string" },
|
|
104
|
+
"elapsed": { "description": "Seconds.", "type": "number", "minimum": 0 }
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"step.started": {
|
|
108
|
+
"description": "A keyword, setup, teardown or control structure. IF/ELSE and TRY/EXCEPT roots are not emitted: their branches are children of the root's parent.",
|
|
109
|
+
"required": ["id", "kind"],
|
|
110
|
+
"properties": {
|
|
111
|
+
"id": { "$ref": "#/$defs/id" },
|
|
112
|
+
"parent": { "description": "Nearest emitted ancestor.", "$ref": "#/$defs/id" },
|
|
113
|
+
"kind": {
|
|
114
|
+
"enum": ["KEYWORD", "SETUP", "TEARDOWN", "FOR", "ITERATION", "IF", "ELSE IF", "ELSE", "TRY", "EXCEPT", "FINALLY", "WHILE", "GROUP", "VAR", "RETURN", "BREAK", "CONTINUE", "ERROR"]
|
|
115
|
+
},
|
|
116
|
+
"source": { "description": "Left out when equal to the parent's source.", "type": "string" },
|
|
117
|
+
"lineno": { "type": "integer" },
|
|
118
|
+
"name": { "description": "Keyword, GROUP or VAR name.", "type": "string" },
|
|
119
|
+
"owner": { "description": "Library or resource file of a keyword.", "type": "string" },
|
|
120
|
+
"args": { "description": "Keyword arguments as written, truncated.", "$ref": "#/$defs/strings" },
|
|
121
|
+
"assign": {
|
|
122
|
+
"description": "Variables of a keyword or FOR (list), values of an ITERATION ({variable: value}), variable of an EXCEPT (string).",
|
|
123
|
+
"anyOf": [
|
|
124
|
+
{ "$ref": "#/$defs/strings" },
|
|
125
|
+
{ "type": "object", "additionalProperties": { "type": "string" } },
|
|
126
|
+
{ "type": "string" }
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"flatten": { "description": "Keyword tagged robot:flatten: its children are emitted, but are not in output.xml.", "const": true },
|
|
130
|
+
"flavor": { "description": "FOR flavor, for example 'IN RANGE'.", "type": "string" },
|
|
131
|
+
"values": { "description": "FOR values, VAR values, RETURN values or ERROR values.", "$ref": "#/$defs/strings" },
|
|
132
|
+
"condition": { "description": "IF, ELSE IF or WHILE condition.", "type": "string" },
|
|
133
|
+
"limit": { "description": "WHILE limit.", "type": "string" },
|
|
134
|
+
"patterns": { "description": "EXCEPT patterns.", "$ref": "#/$defs/strings" },
|
|
135
|
+
"scope": { "description": "VAR scope.", "type": "string" }
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"step.ended": {
|
|
139
|
+
"required": ["id", "status", "elapsed"],
|
|
140
|
+
"properties": {
|
|
141
|
+
"id": { "$ref": "#/$defs/id" },
|
|
142
|
+
"status": { "$ref": "#/$defs/status" },
|
|
143
|
+
"message": { "type": "string" },
|
|
144
|
+
"elapsed": { "description": "Seconds.", "type": "number", "minimum": 0 }
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"message.logged": {
|
|
148
|
+
"required": ["level"],
|
|
149
|
+
"properties": {
|
|
150
|
+
"parent": { "description": "The suite, test or step that logged the message.", "$ref": "#/$defs/id" },
|
|
151
|
+
"level": { "enum": ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FAIL", "SKIP"] },
|
|
152
|
+
"text": { "description": "Left out when empty; cut to max_text characters.", "type": "string" },
|
|
153
|
+
"truncated": { "const": true },
|
|
154
|
+
"html": { "const": true }
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"status.changed": {
|
|
158
|
+
"description": "A failing or skipping suite teardown changed the status of a suite or test that already ended. Readers apply the new status.",
|
|
159
|
+
"required": ["id", "item", "status"],
|
|
160
|
+
"properties": {
|
|
161
|
+
"id": { "$ref": "#/$defs/id" },
|
|
162
|
+
"item": { "enum": ["suite", "test"] },
|
|
163
|
+
"status": { "$ref": "#/$defs/status" },
|
|
164
|
+
"message": { "type": "string" }
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"resultfile.created": {
|
|
168
|
+
"required": ["kind", "path"],
|
|
169
|
+
"properties": {
|
|
170
|
+
"kind": { "enum": ["OUTPUT", "LOG", "REPORT", "XUNIT", "DEBUG"] },
|
|
171
|
+
"path": { "type": "string" }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|