steppy-dev 0.3.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.
- steppy/CHANGELOG.md +862 -0
- steppy/__init__.py +9 -0
- steppy/__main__.py +13 -0
- steppy/_cli_common.py +183 -0
- steppy/_docker/Dockerfile.installed +53 -0
- steppy/_parallel/__init__.py +7 -0
- steppy/_parallel/compose.py +349 -0
- steppy/_parallel/event_bus.py +418 -0
- steppy/_parallel/failures.py +267 -0
- steppy/_parallel/fingerprint.py +141 -0
- steppy/_parallel/image.py +380 -0
- steppy/_parallel/lastfailed.py +125 -0
- steppy/_parallel/lifecycle.py +90 -0
- steppy/_parallel/preauth.py +109 -0
- steppy/_parallel/progress.py +101 -0
- steppy/_parallel/queue.py +385 -0
- steppy/_parallel/renderer.py +489 -0
- steppy/_parallel/retry.py +316 -0
- steppy/_player_bundle/THIRD_PARTY_LICENSES.txt +158 -0
- steppy/_player_bundle/__init__.py +100 -0
- steppy/_player_bundle/steppy-cursor-legacy.js +2 -0
- steppy/_player_bundle/steppy-cursor-legacy.js.map +1 -0
- steppy/_player_bundle/steppy-player.js +5419 -0
- steppy/_player_bundle/steppy-player.js.map +1 -0
- steppy/_player_bundle/steppy-video.js +472 -0
- steppy/_player_bundle/steppy-video.js.map +1 -0
- steppy/_templating.py +117 -0
- steppy/assertions.py +251 -0
- steppy/auth.py +493 -0
- steppy/build.py +2798 -0
- steppy/build_models.py +669 -0
- steppy/buildid.py +134 -0
- steppy/bundle/__init__.py +47 -0
- steppy/bundle/archive.py +239 -0
- steppy/bundle/classify.py +329 -0
- steppy/bundle/create.py +490 -0
- steppy/bundle/hashing.py +28 -0
- steppy/bundle/manifest.py +334 -0
- steppy/bundle/results.py +546 -0
- steppy/bundle_cli.py +455 -0
- steppy/captures.py +207 -0
- steppy/cli.py +2489 -0
- steppy/cloud/__init__.py +58 -0
- steppy/cloud/client.py +267 -0
- steppy/cloud/upload.py +497 -0
- steppy/compat.py +109 -0
- steppy/config.py +231 -0
- steppy/config_schema.py +87 -0
- steppy/contracts/__init__.py +54 -0
- steppy/contracts/bundle.py +215 -0
- steppy/contracts/report.py +346 -0
- steppy/contracts/tours.py +505 -0
- steppy/coordinator.py +1352 -0
- steppy/coverage/__init__.py +9 -0
- steppy/coverage/gaps.py +190 -0
- steppy/coverage/manifest.py +193 -0
- steppy/coverage/merge.py +363 -0
- steppy/coverage/normalize.py +174 -0
- steppy/coverage/report.py +178 -0
- steppy/coverage/sourcemap.py +219 -0
- steppy/coverage/store.py +473 -0
- steppy/coverage/triage.py +151 -0
- steppy/coverage_cli.py +265 -0
- steppy/durations.py +48 -0
- steppy/exclusive.py +178 -0
- steppy/flaky_cli.py +169 -0
- steppy/generator.py +633 -0
- steppy/guide.py +1467 -0
- steppy/history.py +352 -0
- steppy/init_cli.py +330 -0
- steppy/locales.py +282 -0
- steppy/migrate/__init__.py +35 -0
- steppy/migrate/apply.py +194 -0
- steppy/migrate/plan.py +199 -0
- steppy/migrate/rules.py +465 -0
- steppy/migrate/verify.py +163 -0
- steppy/migrate_cli.py +152 -0
- steppy/models.py +2539 -0
- steppy/naming.py +37 -0
- steppy/parser.py +475 -0
- steppy/pdf.py +193 -0
- steppy/presets.py +284 -0
- steppy/record_cli.py +796 -0
- steppy/report/__init__.py +37 -0
- steppy/report/assets.py +197 -0
- steppy/report/build.py +261 -0
- steppy/report/coverage_source.py +277 -0
- steppy/report/model.py +423 -0
- steppy/report/render.py +303 -0
- steppy/report/source.py +238 -0
- steppy/report_cli.py +173 -0
- steppy/runmeta.py +114 -0
- steppy/selection.py +183 -0
- steppy/sitemap.py +205 -0
- steppy/skills/steppy-app-tours/SKILL.md +448 -0
- steppy/skills/steppy-ci-report/SKILL.md +567 -0
- steppy/skills/steppy-scenario-author/SKILL.md +2176 -0
- steppy/skills/steppy-template/SKILL.md +676 -0
- steppy/templates/_steppy_events.py +291 -0
- steppy/templates/_steppy_tours.py +404 -0
- steppy/templates/angular/component-interactive.ts.j2 +109 -0
- steppy/templates/angular/component-video.ts.j2 +112 -0
- steppy/templates/angular/component.ts.j2 +229 -0
- steppy/templates/angular/content.ts.j2 +31 -0
- steppy/templates/angular/gitignore.j2 +3 -0
- steppy/templates/angular/group.component.ts.j2 +39 -0
- steppy/templates/angular/onboarding.component.ts.j2 +43 -0
- steppy/templates/angular/routes.ts.j2 +34 -0
- steppy/templates/ci/github-actions.yml +284 -0
- steppy/templates/ci/gitlab-ci.yml +178 -0
- steppy/templates/conftest.py.j2 +2405 -0
- steppy/templates/guide.book.html.j2 +145 -0
- steppy/templates/guide.fragment.html.j2 +257 -0
- steppy/templates/guide.html.j2 +301 -0
- steppy/templates/guide.md.j2 +18 -0
- steppy/templates/init/config.yaml +124 -0
- steppy/templates/init/credentials.yaml.example +30 -0
- steppy/templates/init/scenarios/example--first-flow.md +53 -0
- steppy/templates/report/_head.html.j2 +14 -0
- steppy/templates/report/_mark.html.j2 +3 -0
- steppy/templates/report/coverage.html.j2 +124 -0
- steppy/templates/report/index.html.j2 +203 -0
- steppy/templates/report/report.css +401 -0
- steppy/templates/report/scenario.html.j2 +137 -0
- steppy/templates/static/_headers.j2 +30 -0
- steppy/templates/static/_mode_links.html.j2 +22 -0
- steppy/templates/static/_seo_head.html.j2 +37 -0
- steppy/templates/static/cursor.js.j2 +10 -0
- steppy/templates/static/group.html.j2 +78 -0
- steppy/templates/static/guides.css +678 -0
- steppy/templates/static/index.html.j2 +79 -0
- steppy/templates/static/interactive.html.j2 +100 -0
- steppy/templates/static/onboarding.html.j2 +64 -0
- steppy/templates/static/page.html.j2 +145 -0
- steppy/templates/static/robots.txt.j2 +7 -0
- steppy/templates/static/video.html.j2 +135 -0
- steppy/templates/test_scenario.py.j2 +767 -0
- steppy/testids.py +166 -0
- steppy/totp.py +26 -0
- steppy/tour_spec.py +163 -0
- steppy/tours/__init__.py +22 -0
- steppy/tours/actions.py +402 -0
- steppy/tours/compiler.py +144 -0
- steppy/tours/diagnostics.py +137 -0
- steppy/tours/emit.py +239 -0
- steppy/tours/ids.py +40 -0
- steppy/tours/inference.py +399 -0
- steppy/tours/model.py +120 -0
- steppy/tours/paths.py +126 -0
- steppy/tours/pipeline.py +277 -0
- steppy/tours/publish.py +124 -0
- steppy/tours/replay.py +234 -0
- steppy/tours/routes.py +175 -0
- steppy/tours/settings.py +164 -0
- steppy/tours/text.py +237 -0
- steppy/tours_cli.py +219 -0
- steppy/tts/__init__.py +280 -0
- steppy/tts/cache.py +185 -0
- steppy/tts/presets.py +125 -0
- steppy/tts/providers/__init__.py +6 -0
- steppy/tts/providers/http.py +184 -0
- steppy/tts/providers/local.py +207 -0
- steppy/tts/providers/null.py +28 -0
- steppy/tts/registry.py +201 -0
- steppy/tts/track.py +158 -0
- steppy/tts/types.py +94 -0
- steppy/update_cli.py +460 -0
- steppy/video.py +730 -0
- steppy/video_cache.py +239 -0
- steppy/video_cli.py +271 -0
- steppy/worker.py +301 -0
- steppy_dev-0.3.0.dist-info/METADATA +1335 -0
- steppy_dev-0.3.0.dist-info/RECORD +177 -0
- steppy_dev-0.3.0.dist-info/WHEEL +4 -0
- steppy_dev-0.3.0.dist-info/entry_points.txt +2 -0
- steppy_dev-0.3.0.dist-info/licenses/LICENSE +202 -0
- steppy_dev-0.3.0.dist-info/licenses/NOTICE +13 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Pytest plugin: emit ``STEPPY-EVT::*`` events for the parallel coordinator.
|
|
2
|
+
|
|
3
|
+
Loaded when EITHER of two independent gates is set — the generated
|
|
4
|
+
``conftest.py`` opts in behind the same condition, so a plain single-worker run
|
|
5
|
+
loads nothing and its pytest output stays byte-identical to the historic
|
|
6
|
+
behaviour:
|
|
7
|
+
|
|
8
|
+
STEPPY_EMIT_EVENTS=1 stdout lines, parsed live by the coordinator's
|
|
9
|
+
EventBus (``steppy run --workers N``)
|
|
10
|
+
STEPPY_EVENTS_DIR=<dir> one JSONL line per event, appended to
|
|
11
|
+
``<dir>/<scenario>__<locale>-<layout>.jsonl``
|
|
12
|
+
(``steppy run --bundle``)
|
|
13
|
+
|
|
14
|
+
The two sinks are orthogonal: the coordinator wants a live stream it can render,
|
|
15
|
+
a results bundle wants a durable per-test record. A parallel run under
|
|
16
|
+
``--bundle`` sets both.
|
|
17
|
+
|
|
18
|
+
Each stdout event is one line prefixed ``STEPPY-EVT::`` for machine parsing.
|
|
19
|
+
Every emission uses ``flush=True`` so the coordinator sees events in real time
|
|
20
|
+
even when stdout is the container's log driver (no TTY, block-buffered
|
|
21
|
+
otherwise).
|
|
22
|
+
|
|
23
|
+
Event grammar (one per line):
|
|
24
|
+
|
|
25
|
+
STEPPY-EVT::session-start::<test_count>
|
|
26
|
+
STEPPY-EVT::test-start::<nodeid>::<unix_ts>
|
|
27
|
+
STEPPY-EVT::test-end::<nodeid>::<outcome>::<duration_ms>
|
|
28
|
+
STEPPY-EVT::test-fail-tail::<nodeid>::<b64(last_15_lines_of_longrepr)>
|
|
29
|
+
STEPPY-EVT::session-end::<unix_ts>
|
|
30
|
+
|
|
31
|
+
``outcome`` is one of ``passed|failed|skipped|error``. ``error`` covers
|
|
32
|
+
setup/teardown failures (pytest's own ``E`` status).
|
|
33
|
+
|
|
34
|
+
The JSONL sink encodes the SAME vocabulary — same ``kind`` names, same
|
|
35
|
+
``outcome`` values — so the two are two encodings of one grammar rather than two
|
|
36
|
+
grammars. Four kinds never appear in it (``scenario-start``, ``scenario-end``,
|
|
37
|
+
``worker-done``, ``warning``): a single pytest process cannot know them; they
|
|
38
|
+
belong to the worker and the coordinator.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import base64
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
import sys
|
|
47
|
+
import time
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_EVT_PREFIX = "STEPPY-EVT::"
|
|
51
|
+
_FAIL_TAIL_LINES = 15
|
|
52
|
+
|
|
53
|
+
#: Schema version of one JSONL line. Additive fields are always allowed and
|
|
54
|
+
#: readers default what is absent — the same forward-compat rule the screenshot
|
|
55
|
+
#: sidecars follow. Bump only on a breaking change.
|
|
56
|
+
_JSONL_SCHEMA_VERSION = 1
|
|
57
|
+
|
|
58
|
+
# Tracks which nodeids have already had a terminal event emitted so we
|
|
59
|
+
# don't double-count when teardown also reports. Reset per pytest session
|
|
60
|
+
# (module-level state is fine because the worker spawns one pytest
|
|
61
|
+
# subprocess per scenario).
|
|
62
|
+
_emitted_end: set[str] = set()
|
|
63
|
+
|
|
64
|
+
# Set by pytest_configure — the pytest CaptureManager plugin. We use it to
|
|
65
|
+
# suspend fd-level capture around every emit so the ``STEPPY-EVT::*`` lines
|
|
66
|
+
# actually reach the container's stdout (and thus the coordinator's log
|
|
67
|
+
# tailer). Without this, pytest's default ``--capture=fd`` swallows our
|
|
68
|
+
# output during the ``call`` phase of each test, so ``test-end`` events
|
|
69
|
+
# never reach the EventBus and the live "tests done · failed" counters
|
|
70
|
+
# stay pinned at 0 — exactly the bug the parallel UI showed.
|
|
71
|
+
#
|
|
72
|
+
# The JSONL sink needs none of this: it writes to a file, not to stdout.
|
|
73
|
+
_capman = None
|
|
74
|
+
|
|
75
|
+
# nodeid -> (scenario, locale, layout), filled by ``pytest_runtest_setup``.
|
|
76
|
+
_ctx: dict = {}
|
|
77
|
+
|
|
78
|
+
# JSONL paths this process has already opened, so the first write truncates and
|
|
79
|
+
# the rest append. A re-run of one scenario (the coordinator's retry pass)
|
|
80
|
+
# therefore REPLACES its file rather than stacking a second session onto the
|
|
81
|
+
# first — mirroring how ``output/failures/`` is reset per run, and how the
|
|
82
|
+
# coverage merge deletes a scenario's rows before re-inserting them.
|
|
83
|
+
_opened: set = set()
|
|
84
|
+
|
|
85
|
+
_events_dir = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def pytest_configure(config) -> None:
|
|
89
|
+
"""Capture pytest's CaptureManager for use in ``_emit``, and resolve the
|
|
90
|
+
JSONL sink directory once."""
|
|
91
|
+
global _capman, _events_dir
|
|
92
|
+
_capman = config.pluginmanager.getplugin("capturemanager")
|
|
93
|
+
raw = (os.environ.get("STEPPY_EVENTS_DIR") or "").strip()
|
|
94
|
+
_events_dir = raw or None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _emit(*parts: str) -> None:
|
|
98
|
+
"""Write one ``STEPPY-EVT::*`` line, bypassing pytest's stdout capture
|
|
99
|
+
so the coordinator's log tailer actually sees it.
|
|
100
|
+
|
|
101
|
+
Falls back to a plain ``print`` if the capture manager isn't available
|
|
102
|
+
(very early hooks, or pytest configured without capture)."""
|
|
103
|
+
if os.environ.get("STEPPY_EMIT_EVENTS") != "1":
|
|
104
|
+
return
|
|
105
|
+
line = _EVT_PREFIX + "::".join(parts)
|
|
106
|
+
if _capman is not None:
|
|
107
|
+
try:
|
|
108
|
+
_capman.suspend_global_capture(in_=False)
|
|
109
|
+
try:
|
|
110
|
+
sys.stdout.write(line + "\n")
|
|
111
|
+
sys.stdout.flush()
|
|
112
|
+
finally:
|
|
113
|
+
_capman.resume_global_capture()
|
|
114
|
+
return
|
|
115
|
+
except Exception:
|
|
116
|
+
# Fall through to plain print — better degraded than silent.
|
|
117
|
+
pass
|
|
118
|
+
print(line, flush=True)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _context(nodeid: str):
|
|
122
|
+
"""``(scenario, locale, layout)`` for a nodeid — ``(None, None, None)`` when
|
|
123
|
+
the setup hook never saw it (e.g. a collection error)."""
|
|
124
|
+
return _ctx.get(nodeid, (None, None, None))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _jsonl_path(scenario, locale, layout) -> str:
|
|
128
|
+
"""``<events_dir>/<scenario>__<locale>-<layout>.jsonl``.
|
|
129
|
+
|
|
130
|
+
``default`` substitutes for an unset dimension — deliberately the SAME
|
|
131
|
+
convention as the two other writers that face the same problem: the pytest
|
|
132
|
+
param id (``_locale_layout_id`` in conftest.py) and the coverage record
|
|
133
|
+
suffix. Three writers, one convention.
|
|
134
|
+
"""
|
|
135
|
+
stem = "{}__{}-{}".format(scenario or "unknown", locale or "default", layout or "default")
|
|
136
|
+
return os.path.join(_events_dir, stem + ".jsonl")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _record(kind: str, nodeid=None, **fields) -> None:
|
|
140
|
+
"""Append one JSONL line. Never raises — a broken sink must not fail a run.
|
|
141
|
+
|
|
142
|
+
Silence is deliberate rather than lazy: this runs inside every test of a
|
|
143
|
+
long suite, and a per-line warning on a full disk would bury the pytest
|
|
144
|
+
output it is meant to describe. ``steppy bundle create`` is where the
|
|
145
|
+
absence becomes visible, and it says so loudly.
|
|
146
|
+
"""
|
|
147
|
+
if not _events_dir:
|
|
148
|
+
return
|
|
149
|
+
try:
|
|
150
|
+
scenario, locale, layout = _context(nodeid) if nodeid else (None, None, None)
|
|
151
|
+
payload = {
|
|
152
|
+
"v": _JSONL_SCHEMA_VERSION,
|
|
153
|
+
"kind": kind,
|
|
154
|
+
"ts": round(time.time(), 3),
|
|
155
|
+
"scenario": scenario,
|
|
156
|
+
"locale": locale,
|
|
157
|
+
"layout": layout,
|
|
158
|
+
}
|
|
159
|
+
if nodeid is not None:
|
|
160
|
+
payload["nodeid"] = nodeid
|
|
161
|
+
worker = os.environ.get("STEPPY_WORKER_ID")
|
|
162
|
+
if worker:
|
|
163
|
+
payload["worker"] = worker
|
|
164
|
+
payload.update(fields)
|
|
165
|
+
|
|
166
|
+
path = _jsonl_path(scenario, locale, layout)
|
|
167
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
168
|
+
mode = "a" if path in _opened else "w"
|
|
169
|
+
_opened.add(path)
|
|
170
|
+
with open(path, mode, encoding="utf-8", newline="\n") as fh:
|
|
171
|
+
fh.write(json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n")
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _b64(text: str) -> str:
|
|
177
|
+
return base64.b64encode(text.encode("utf-8", errors="replace")).decode("ascii")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _tail(text: str, n: int = _FAIL_TAIL_LINES) -> str:
|
|
181
|
+
if not text:
|
|
182
|
+
return ""
|
|
183
|
+
return "\n".join(text.splitlines()[-n:])
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _emit_end(nodeid: str, outcome: str, report) -> None:
|
|
187
|
+
duration_ms = int((getattr(report, "duration", 0.0) or 0.0) * 1000)
|
|
188
|
+
_emit("test-end", nodeid, outcome, str(duration_ms))
|
|
189
|
+
_record("test-end", nodeid, outcome=outcome, duration_ms=duration_ms)
|
|
190
|
+
if outcome in ("failed", "error") and getattr(report, "longrepr", None) is not None:
|
|
191
|
+
tail_b64 = _b64(_tail(str(report.longrepr)))
|
|
192
|
+
_emit("test-fail-tail", nodeid, tail_b64)
|
|
193
|
+
_record("test-fail-tail", nodeid, tail_b64=tail_b64)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def pytest_collection_finish(session) -> None:
|
|
197
|
+
# Not mirrored into the JSONL sink: its files are keyed by
|
|
198
|
+
# ``(scenario, locale, layout)`` and a session spans several of them, so a
|
|
199
|
+
# session marker has no single home. The bundle needs per-test rows, and
|
|
200
|
+
# ``session-start``'s count is a live-progress concern.
|
|
201
|
+
try:
|
|
202
|
+
count = len(session.items)
|
|
203
|
+
except Exception:
|
|
204
|
+
count = 0
|
|
205
|
+
_emit("session-start", str(count))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def pytest_runtest_protocol(item, nextitem):
|
|
209
|
+
"""Record ``(scenario, locale, layout)`` for this test, from the item itself.
|
|
210
|
+
Always returns ``None`` so pytest runs its own protocol as usual.
|
|
211
|
+
|
|
212
|
+
This exists so the JSONL sink never has to parse a nodeid, which would be
|
|
213
|
+
wrong rather than merely awkward: a locale like ``fr-BE`` contains the same
|
|
214
|
+
separator the id joins on. ``callspec.params`` carries the ``(locale,
|
|
215
|
+
layout)`` tuple verbatim — once per role page the test requests, all equal,
|
|
216
|
+
because ``pytest_generate_tests`` binds them in lockstep — and
|
|
217
|
+
``module.SCENARIO`` is the slug the generator baked in. Both exact.
|
|
218
|
+
|
|
219
|
+
Hook choice is load-bearing, twice over. It must be ``runtest_protocol``
|
|
220
|
+
rather than ``runtest_setup``, because ``pytest_runtest_logstart`` — which
|
|
221
|
+
writes the ``test-start`` line — fires BEFORE setup, and would otherwise
|
|
222
|
+
file that line under an unknown scenario. And it must not be a name the
|
|
223
|
+
generated conftest also defines, since the conftest pulls these hook
|
|
224
|
+
functions into its own namespace BY NAME and would silently shadow itself
|
|
225
|
+
(it owns ``pytest_generate_tests``, ``pytest_collection_modifyitems`` and
|
|
226
|
+
``pytest_runtest_makereport``). Guard: ``tests/test_bundle_events_sink.py``.
|
|
227
|
+
"""
|
|
228
|
+
if not _events_dir:
|
|
229
|
+
return None
|
|
230
|
+
scenario = locale = layout = None
|
|
231
|
+
try:
|
|
232
|
+
scenario = getattr(item.module, "SCENARIO", None)
|
|
233
|
+
except Exception:
|
|
234
|
+
pass
|
|
235
|
+
try:
|
|
236
|
+
callspec = getattr(item, "callspec", None)
|
|
237
|
+
if callspec is not None:
|
|
238
|
+
for value in callspec.params.values():
|
|
239
|
+
if isinstance(value, tuple) and len(value) == 2:
|
|
240
|
+
locale, layout = value
|
|
241
|
+
break
|
|
242
|
+
except Exception:
|
|
243
|
+
pass
|
|
244
|
+
_ctx[item.nodeid] = (scenario, locale, layout)
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def pytest_runtest_logstart(nodeid, location) -> None:
|
|
249
|
+
_emit("test-start", nodeid, f"{time.time():.3f}")
|
|
250
|
+
_record("test-start", nodeid)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def pytest_runtest_logreport(report) -> None:
|
|
254
|
+
nodeid = report.nodeid
|
|
255
|
+
|
|
256
|
+
# If we already emitted a terminal event for this nodeid, only surface
|
|
257
|
+
# an additional fail-tail when teardown itself blew up — useful for
|
|
258
|
+
# debugging fixture cleanup errors. We don't upgrade the outcome.
|
|
259
|
+
if nodeid in _emitted_end:
|
|
260
|
+
if report.when == "teardown" and report.failed and report.longrepr is not None:
|
|
261
|
+
tail_b64 = _b64(_tail(str(report.longrepr)))
|
|
262
|
+
_emit("test-fail-tail", nodeid, tail_b64)
|
|
263
|
+
_record("test-fail-tail", nodeid, tail_b64=tail_b64, phase="teardown")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
if report.when == "setup":
|
|
267
|
+
if report.failed:
|
|
268
|
+
_emit_end(nodeid, "error", report)
|
|
269
|
+
_emitted_end.add(nodeid)
|
|
270
|
+
elif report.skipped:
|
|
271
|
+
_emit_end(nodeid, "skipped", report)
|
|
272
|
+
_emitted_end.add(nodeid)
|
|
273
|
+
# passed setup → wait for the call phase
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
if report.when == "call":
|
|
277
|
+
# outcome ∈ {passed, failed, skipped}
|
|
278
|
+
_emit_end(nodeid, report.outcome, report)
|
|
279
|
+
_emitted_end.add(nodeid)
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
# teardown phase, terminal not yet emitted (rare: setup passed, no call
|
|
283
|
+
# report was produced). Surface teardown failure as error.
|
|
284
|
+
if report.when == "teardown" and report.failed:
|
|
285
|
+
_emit_end(nodeid, "error", report)
|
|
286
|
+
_emitted_end.add(nodeid)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def pytest_sessionfinish(session, exitstatus) -> None:
|
|
290
|
+
# Not mirrored into the JSONL sink — see ``pytest_collection_finish``.
|
|
291
|
+
_emit("session-end", f"{time.time():.3f}")
|