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,248 @@
|
|
|
1
|
+
"""Typed events of format v1, and the JSON Schema that describes one line of an events file.
|
|
2
|
+
|
|
3
|
+
The listener emits plain dicts, which is faster; these classes are for programs that read
|
|
4
|
+
events. :func:`from_dict` turns a decoded line into an event, :meth:`Event.to_dict` turns it
|
|
5
|
+
back. Unknown fields are ignored and unknown event types become :class:`UnknownEvent`, so
|
|
6
|
+
readers keep working when newer writers add to the format.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from dataclasses import dataclass, fields
|
|
14
|
+
from importlib import resources
|
|
15
|
+
from typing import Any, ClassVar
|
|
16
|
+
|
|
17
|
+
FORMAT_VERSION = 1
|
|
18
|
+
SCHEMA_FILE = "events-v1.schema.json"
|
|
19
|
+
|
|
20
|
+
Stats = dict[str, int]
|
|
21
|
+
"""``{"total": n, "passed": n, "failed": n, "skipped": n}``"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True, kw_only=True)
|
|
25
|
+
class Event:
|
|
26
|
+
"""Fields that every event has. ``type`` is the class attribute ``TYPE``."""
|
|
27
|
+
|
|
28
|
+
TYPE: ClassVar[str] = ""
|
|
29
|
+
|
|
30
|
+
v: int = FORMAT_VERSION
|
|
31
|
+
seq: int
|
|
32
|
+
t: float
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def type(self) -> str:
|
|
36
|
+
return self.TYPE
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, Any]:
|
|
39
|
+
"""The event as written to a file: envelope first, fields that are None left out."""
|
|
40
|
+
data: dict[str, Any] = {"v": self.v, "seq": self.seq, "t": self.t, "type": self.type}
|
|
41
|
+
for f in fields(self):
|
|
42
|
+
if f.name not in _ENVELOPE:
|
|
43
|
+
value = getattr(self, f.name)
|
|
44
|
+
if value is not None:
|
|
45
|
+
data[f.name] = value
|
|
46
|
+
return data
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(slots=True, kw_only=True)
|
|
50
|
+
class RunStarted(Event):
|
|
51
|
+
"""``python``, ``pid`` and ``cwd`` are missing in events converted from a result file."""
|
|
52
|
+
|
|
53
|
+
TYPE: ClassVar[str] = "run.started"
|
|
54
|
+
|
|
55
|
+
run: str
|
|
56
|
+
rf: str
|
|
57
|
+
python: str | None = None
|
|
58
|
+
pid: int | None = None
|
|
59
|
+
cwd: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(slots=True, kw_only=True)
|
|
63
|
+
class RunEnded(Event):
|
|
64
|
+
TYPE: ClassVar[str] = "run.ended"
|
|
65
|
+
|
|
66
|
+
status: str
|
|
67
|
+
stats: Stats
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(slots=True, kw_only=True)
|
|
71
|
+
class SuiteStarted(Event):
|
|
72
|
+
TYPE: ClassVar[str] = "suite.started"
|
|
73
|
+
|
|
74
|
+
id: str
|
|
75
|
+
parent: str | None = None
|
|
76
|
+
name: str
|
|
77
|
+
longname: str
|
|
78
|
+
source: str | None = None
|
|
79
|
+
tests: int
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(slots=True, kw_only=True)
|
|
83
|
+
class SuiteEnded(Event):
|
|
84
|
+
TYPE: ClassVar[str] = "suite.ended"
|
|
85
|
+
|
|
86
|
+
id: str
|
|
87
|
+
status: str
|
|
88
|
+
message: str | None = None
|
|
89
|
+
elapsed: float
|
|
90
|
+
stats: Stats
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(slots=True, kw_only=True)
|
|
94
|
+
class TestStarted(Event):
|
|
95
|
+
TYPE: ClassVar[str] = "test.started"
|
|
96
|
+
__test__: ClassVar[bool] = False # not a pytest test class
|
|
97
|
+
|
|
98
|
+
id: str
|
|
99
|
+
parent: str
|
|
100
|
+
name: str
|
|
101
|
+
longname: str
|
|
102
|
+
source: str | None = None
|
|
103
|
+
lineno: int | None = None
|
|
104
|
+
tags: list[str] | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(slots=True, kw_only=True)
|
|
108
|
+
class TestEnded(Event):
|
|
109
|
+
TYPE: ClassVar[str] = "test.ended"
|
|
110
|
+
__test__: ClassVar[bool] = False
|
|
111
|
+
|
|
112
|
+
id: str
|
|
113
|
+
status: str
|
|
114
|
+
message: str | None = None
|
|
115
|
+
elapsed: float
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(slots=True, kw_only=True)
|
|
119
|
+
class StepStarted(Event):
|
|
120
|
+
"""A keyword, setup, teardown or control structure. Which fields are set depends on ``kind``.
|
|
121
|
+
|
|
122
|
+
``source`` is left out when it equals the parent's source. ``assign`` is a list of variables
|
|
123
|
+
for keywords and FOR, a ``{variable: value}`` dict for ITERATION and a string for EXCEPT.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
TYPE: ClassVar[str] = "step.started"
|
|
127
|
+
|
|
128
|
+
id: str
|
|
129
|
+
parent: str | None = None
|
|
130
|
+
kind: str
|
|
131
|
+
source: str | None = None
|
|
132
|
+
lineno: int | None = None
|
|
133
|
+
name: str | None = None
|
|
134
|
+
owner: str | None = None
|
|
135
|
+
args: list[str] | None = None
|
|
136
|
+
assign: list[str] | dict[str, str] | str | None = None
|
|
137
|
+
flatten: bool | None = None
|
|
138
|
+
flavor: str | None = None
|
|
139
|
+
values: list[str] | None = None
|
|
140
|
+
condition: str | None = None
|
|
141
|
+
limit: str | None = None
|
|
142
|
+
patterns: list[str] | None = None
|
|
143
|
+
scope: str | None = None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(slots=True, kw_only=True)
|
|
147
|
+
class StepEnded(Event):
|
|
148
|
+
TYPE: ClassVar[str] = "step.ended"
|
|
149
|
+
|
|
150
|
+
id: str
|
|
151
|
+
status: str
|
|
152
|
+
message: str | None = None
|
|
153
|
+
elapsed: float
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(slots=True, kw_only=True)
|
|
157
|
+
class MessageLogged(Event):
|
|
158
|
+
TYPE: ClassVar[str] = "message.logged"
|
|
159
|
+
|
|
160
|
+
parent: str | None = None
|
|
161
|
+
level: str
|
|
162
|
+
text: str | None = None
|
|
163
|
+
truncated: bool | None = None
|
|
164
|
+
html: bool | None = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(slots=True, kw_only=True)
|
|
168
|
+
class StatusChanged(Event):
|
|
169
|
+
"""A suite teardown changed the status of a suite or test that already ended."""
|
|
170
|
+
|
|
171
|
+
TYPE: ClassVar[str] = "status.changed"
|
|
172
|
+
|
|
173
|
+
id: str
|
|
174
|
+
item: str
|
|
175
|
+
status: str
|
|
176
|
+
message: str | None = None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(slots=True, kw_only=True)
|
|
180
|
+
class ResultFileCreated(Event):
|
|
181
|
+
TYPE: ClassVar[str] = "resultfile.created"
|
|
182
|
+
|
|
183
|
+
kind: str
|
|
184
|
+
path: str
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass(slots=True, kw_only=True)
|
|
188
|
+
class UnknownEvent(Event):
|
|
189
|
+
"""An event type this version does not know. ``data`` holds the fields besides the envelope."""
|
|
190
|
+
|
|
191
|
+
name: str
|
|
192
|
+
data: dict[str, Any]
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def type(self) -> str:
|
|
196
|
+
return self.name
|
|
197
|
+
|
|
198
|
+
def to_dict(self) -> dict[str, Any]:
|
|
199
|
+
return {"v": self.v, "seq": self.seq, "t": self.t, "type": self.name, **self.data}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
EVENT_CLASSES: dict[str, type[Event]] = {
|
|
203
|
+
cls.TYPE: cls
|
|
204
|
+
for cls in (
|
|
205
|
+
RunStarted,
|
|
206
|
+
SuiteStarted,
|
|
207
|
+
TestStarted,
|
|
208
|
+
StepStarted,
|
|
209
|
+
MessageLogged,
|
|
210
|
+
StepEnded,
|
|
211
|
+
TestEnded,
|
|
212
|
+
SuiteEnded,
|
|
213
|
+
StatusChanged,
|
|
214
|
+
ResultFileCreated,
|
|
215
|
+
RunEnded,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_ENVELOPE = frozenset({"v", "seq", "t", "type"})
|
|
220
|
+
_FIELDS = {type_: frozenset(f.name for f in fields(cls)) - _ENVELOPE for type_, cls in EVENT_CLASSES.items()}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def from_dict(data: Mapping[str, Any]) -> Event:
|
|
224
|
+
"""Builds the typed event for one decoded line.
|
|
225
|
+
|
|
226
|
+
Unknown fields are ignored; an unknown ``type`` gives an :class:`UnknownEvent`.
|
|
227
|
+
Raises ``ValueError`` when an envelope field or a required field is missing.
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
envelope = {"v": data["v"], "seq": data["seq"], "t": data["t"]}
|
|
231
|
+
type_ = data["type"]
|
|
232
|
+
except KeyError as e:
|
|
233
|
+
raise ValueError(f"event without {e.args[0]!r}: {dict(data)}") from None
|
|
234
|
+
cls = EVENT_CLASSES.get(type_)
|
|
235
|
+
if cls is None:
|
|
236
|
+
return UnknownEvent(**envelope, name=type_, data={k: v for k, v in data.items() if k not in _ENVELOPE})
|
|
237
|
+
known = _FIELDS[type_]
|
|
238
|
+
try:
|
|
239
|
+
return cls(**envelope, **{k: v for k, v in data.items() if k in known})
|
|
240
|
+
except TypeError as e:
|
|
241
|
+
raise ValueError(f"invalid {type_} event: {e}") from None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def schema() -> dict[str, Any]:
|
|
245
|
+
"""The JSON Schema (draft 2020-12) for one line of an events file, format v1. Returns a new dict."""
|
|
246
|
+
text = resources.files(__package__).joinpath(SCHEMA_FILE).read_text(encoding="utf-8")
|
|
247
|
+
result: dict[str, Any] = json.loads(text)
|
|
248
|
+
return result
|