pytest-xhtml 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.
- pytest_xhtml/__init__.py +9 -0
- pytest_xhtml/__version.py +21 -0
- pytest_xhtml/basereport.py +377 -0
- pytest_xhtml/extras.py +77 -0
- pytest_xhtml/fixtures.py +47 -0
- pytest_xhtml/hooks.py +27 -0
- pytest_xhtml/plugin.py +140 -0
- pytest_xhtml/report.py +41 -0
- pytest_xhtml/report_data.py +153 -0
- pytest_xhtml/resources/app.js +655 -0
- pytest_xhtml/resources/index.jinja2 +153 -0
- pytest_xhtml/resources/style-old.css +319 -0
- pytest_xhtml/resources/style.css +547 -0
- pytest_xhtml/selfcontained_report.py +39 -0
- pytest_xhtml/util.py +56 -0
- pytest_xhtml-0.1.0.dist-info/METADATA +62 -0
- pytest_xhtml-0.1.0.dist-info/RECORD +20 -0
- pytest_xhtml-0.1.0.dist-info/WHEEL +4 -0
- pytest_xhtml-0.1.0.dist-info/entry_points.txt +3 -0
- pytest_xhtml-0.1.0.dist-info/licenses/LICENSE +201 -0
pytest_xhtml/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.1.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
import datetime
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
import warnings
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from html import escape
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from pytest_xhtml import __version__
|
|
18
|
+
from pytest_xhtml import extras
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseReport:
|
|
22
|
+
def __init__(self, report_path, config, report_data, template, css):
|
|
23
|
+
self._report_path = (
|
|
24
|
+
Path.cwd() / Path(os.path.expandvars(report_path)).expanduser()
|
|
25
|
+
)
|
|
26
|
+
self._report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
self._config = config
|
|
28
|
+
self._template = template
|
|
29
|
+
self._css = css
|
|
30
|
+
self._max_asset_filename_length = int(
|
|
31
|
+
config.getini("max_asset_filename_length")
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self._reports = defaultdict(dict)
|
|
35
|
+
self._report = report_data
|
|
36
|
+
self._report.title = self._report_path.name
|
|
37
|
+
self._suite_start_time = time.time()
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def css(self):
|
|
41
|
+
# implement in subclasses
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
def _asset_filename(self, test_id, extra_index, test_index, file_extension):
|
|
45
|
+
return "{}_{}_{}.{}".format(
|
|
46
|
+
re.sub(r"[^\w.]", "_", test_id),
|
|
47
|
+
str(extra_index),
|
|
48
|
+
str(test_index),
|
|
49
|
+
file_extension,
|
|
50
|
+
)[-self._max_asset_filename_length :]
|
|
51
|
+
|
|
52
|
+
def _generate_report(self, self_contained=False):
|
|
53
|
+
generated = datetime.datetime.now()
|
|
54
|
+
test_data = self._report.data
|
|
55
|
+
test_data = json.dumps(test_data)
|
|
56
|
+
rendered_report = self._template.render(
|
|
57
|
+
title=self._report.title,
|
|
58
|
+
date=generated.strftime("%d-%b-%Y"),
|
|
59
|
+
time=generated.strftime("%H:%M:%S"),
|
|
60
|
+
version=__version__,
|
|
61
|
+
styles=self.css,
|
|
62
|
+
run_count=self._run_count(),
|
|
63
|
+
running_state=self._report.running_state,
|
|
64
|
+
self_contained=self_contained,
|
|
65
|
+
outcomes=self._report.outcomes,
|
|
66
|
+
test_data=test_data,
|
|
67
|
+
table_head=self._report.table_header,
|
|
68
|
+
additional_summary=self._report.additional_summary,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self._write_report(rendered_report)
|
|
72
|
+
|
|
73
|
+
def _generate_environment(self):
|
|
74
|
+
try:
|
|
75
|
+
from pytest_metadata.plugin import metadata_key
|
|
76
|
+
|
|
77
|
+
metadata = self._config.stash[metadata_key]
|
|
78
|
+
except ImportError:
|
|
79
|
+
# old version of pytest-metadata
|
|
80
|
+
metadata = self._config._metadata
|
|
81
|
+
warnings.warn(
|
|
82
|
+
"'pytest-metadata < 3.0.0' is deprecated and support will be dropped in next major version",
|
|
83
|
+
DeprecationWarning,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
for key in metadata.keys():
|
|
87
|
+
value = metadata[key]
|
|
88
|
+
if self._is_redactable_environment_variable(key):
|
|
89
|
+
black_box_ascii_value = 0x2593
|
|
90
|
+
metadata[key] = "".join(chr(black_box_ascii_value) for _ in str(value))
|
|
91
|
+
|
|
92
|
+
return metadata
|
|
93
|
+
|
|
94
|
+
def _is_redactable_environment_variable(self, environment_variable):
|
|
95
|
+
redactable_regexes = self._config.getini("environment_table_redact_list")
|
|
96
|
+
for redactable_regex in redactable_regexes:
|
|
97
|
+
if re.match(redactable_regex, environment_variable):
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def _data_content(self, *args, **kwargs):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def _media_content(self, *args, **kwargs):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def _process_extras(self, report, test_id):
|
|
109
|
+
test_index = hasattr(report, "rerun") and report.rerun + 1 or 0
|
|
110
|
+
report_extras = getattr(report, "extras", [])
|
|
111
|
+
for extra_index, extra in enumerate(report_extras):
|
|
112
|
+
content = extra["content"]
|
|
113
|
+
asset_name = self._asset_filename(
|
|
114
|
+
test_id.encode("utf-8").decode("unicode_escape"),
|
|
115
|
+
extra_index,
|
|
116
|
+
test_index,
|
|
117
|
+
extra["extension"],
|
|
118
|
+
)
|
|
119
|
+
if extra["format_type"] == extras.FORMAT_JSON:
|
|
120
|
+
content = json.dumps(content)
|
|
121
|
+
extra["content"] = self._data_content(
|
|
122
|
+
content, asset_name=asset_name, mime_type=extra["mime_type"]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if extra["format_type"] == extras.FORMAT_TEXT:
|
|
126
|
+
if isinstance(content, bytes):
|
|
127
|
+
content = content.decode("utf-8")
|
|
128
|
+
extra["content"] = self._data_content(
|
|
129
|
+
content, asset_name=asset_name, mime_type=extra["mime_type"]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if extra["format_type"] in [extras.FORMAT_IMAGE, extras.FORMAT_VIDEO]:
|
|
133
|
+
extra["content"] = self._media_content(
|
|
134
|
+
content, asset_name=asset_name, mime_type=extra["mime_type"]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return report_extras
|
|
138
|
+
|
|
139
|
+
def _write_report(self, rendered_report):
|
|
140
|
+
with self._report_path.open("w", encoding="utf-8") as f:
|
|
141
|
+
f.write(rendered_report)
|
|
142
|
+
|
|
143
|
+
def _run_count(self):
|
|
144
|
+
relevant_outcomes = ["passed", "failed", "xpassed", "xfailed"]
|
|
145
|
+
counts = 0
|
|
146
|
+
for outcome in self._report.outcomes.keys():
|
|
147
|
+
if outcome in relevant_outcomes:
|
|
148
|
+
counts += self._report.outcomes[outcome]["value"]
|
|
149
|
+
|
|
150
|
+
plural = counts > 1
|
|
151
|
+
duration = _format_duration(self._report.total_duration)
|
|
152
|
+
|
|
153
|
+
if self._report.running_state == "finished":
|
|
154
|
+
return f"{counts} {'tests' if plural else 'test'} took {duration}."
|
|
155
|
+
|
|
156
|
+
return f"{counts}/{self._report.collected_items} {'tests' if plural else 'test'} done."
|
|
157
|
+
|
|
158
|
+
def _hydrate_data(self, data, cells):
|
|
159
|
+
for index, cell in enumerate(cells):
|
|
160
|
+
# extract column name and data if column is sortable
|
|
161
|
+
if "sortable" in self._report.table_header[index]:
|
|
162
|
+
name_match = re.search(r"col-(\w+)", cell)
|
|
163
|
+
data_match = re.search(r"<td.*?>(.*?)</td>", cell)
|
|
164
|
+
if name_match and data_match:
|
|
165
|
+
data[name_match.group(1)] = data_match.group(1)
|
|
166
|
+
|
|
167
|
+
@pytest.hookimpl(trylast=True)
|
|
168
|
+
def pytest_sessionstart(self, session):
|
|
169
|
+
self._report.set_data("environment", self._generate_environment())
|
|
170
|
+
|
|
171
|
+
session.config.hook.pytest_xhtml_report_title(report=self._report)
|
|
172
|
+
|
|
173
|
+
headers = self._report.table_header
|
|
174
|
+
session.config.hook.pytest_xhtml_results_table_header(cells=headers)
|
|
175
|
+
self._report.table_header = _fix_py(headers)
|
|
176
|
+
|
|
177
|
+
self._report.running_state = "started"
|
|
178
|
+
if self._config.getini("generate_report_on_test"):
|
|
179
|
+
self._generate_report()
|
|
180
|
+
|
|
181
|
+
@pytest.hookimpl(trylast=True)
|
|
182
|
+
def pytest_sessionfinish(self, session):
|
|
183
|
+
session.config.hook.pytest_xhtml_results_summary(
|
|
184
|
+
prefix=self._report.additional_summary["prefix"],
|
|
185
|
+
summary=self._report.additional_summary["summary"],
|
|
186
|
+
postfix=self._report.additional_summary["postfix"],
|
|
187
|
+
session=session,
|
|
188
|
+
)
|
|
189
|
+
self._report.running_state = "finished"
|
|
190
|
+
suite_stop_time = time.time()
|
|
191
|
+
self._report.total_duration = suite_stop_time - self._suite_start_time
|
|
192
|
+
self._generate_report()
|
|
193
|
+
|
|
194
|
+
@pytest.hookimpl(trylast=True)
|
|
195
|
+
def pytest_terminal_summary(self, terminalreporter):
|
|
196
|
+
terminalreporter.write_sep(
|
|
197
|
+
"-",
|
|
198
|
+
f"Generated html report: {self._report_path.as_uri()}",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@pytest.hookimpl(trylast=True)
|
|
202
|
+
def pytest_collectreport(self, report):
|
|
203
|
+
if report.failed:
|
|
204
|
+
self._process_report(report, 0, [])
|
|
205
|
+
|
|
206
|
+
@pytest.hookimpl(trylast=True)
|
|
207
|
+
def pytest_collection_finish(self, session):
|
|
208
|
+
self._report.collected_items = len(session.items)
|
|
209
|
+
|
|
210
|
+
@pytest.hookimpl(trylast=True)
|
|
211
|
+
def pytest_runtest_logreport(self, report):
|
|
212
|
+
if hasattr(report, "duration_formatter"):
|
|
213
|
+
warnings.warn(
|
|
214
|
+
"'duration_formatter' has been removed and no longer has any effect!"
|
|
215
|
+
"Please use the 'pytest_xhtml_duration_format' hook instead.",
|
|
216
|
+
DeprecationWarning,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# "reruns" makes this code a mess.
|
|
220
|
+
# We store each combination of when and outcome
|
|
221
|
+
# exactly once, unless that outcome is a "rerun"
|
|
222
|
+
# then we store all of them.
|
|
223
|
+
key = (report.when, report.outcome)
|
|
224
|
+
if report.outcome == "rerun":
|
|
225
|
+
if key not in self._reports[report.nodeid]:
|
|
226
|
+
self._reports[report.nodeid][key] = list()
|
|
227
|
+
self._reports[report.nodeid][key].append(report)
|
|
228
|
+
else:
|
|
229
|
+
self._reports[report.nodeid][key] = [report]
|
|
230
|
+
|
|
231
|
+
finished = report.when == "teardown" and report.outcome != "rerun"
|
|
232
|
+
if not finished:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
# Calculate total duration for a single test.
|
|
236
|
+
# This is needed to add the "teardown" duration
|
|
237
|
+
# to tests total duration.
|
|
238
|
+
test_duration = 0
|
|
239
|
+
for key, reports in self._reports[report.nodeid].items():
|
|
240
|
+
_, outcome = key
|
|
241
|
+
if outcome != "rerun":
|
|
242
|
+
test_duration += reports[0].duration
|
|
243
|
+
|
|
244
|
+
processed_extras = []
|
|
245
|
+
for key, reports in self._reports[report.nodeid].items():
|
|
246
|
+
when, _ = key
|
|
247
|
+
for each in reports:
|
|
248
|
+
test_id = report.nodeid
|
|
249
|
+
if when != "call":
|
|
250
|
+
test_id += f"::{when}"
|
|
251
|
+
processed_extras += self._process_extras(each, test_id)
|
|
252
|
+
|
|
253
|
+
for key, reports in self._reports[report.nodeid].items():
|
|
254
|
+
when, _ = key
|
|
255
|
+
for each in reports:
|
|
256
|
+
dur = test_duration if when == "call" else each.duration
|
|
257
|
+
self._process_report(each, dur, processed_extras)
|
|
258
|
+
|
|
259
|
+
if self._config.getini("generate_report_on_test"):
|
|
260
|
+
self._generate_report()
|
|
261
|
+
|
|
262
|
+
def _process_report(self, report, duration, processed_extras):
|
|
263
|
+
outcome = _process_outcome(report)
|
|
264
|
+
try:
|
|
265
|
+
# hook returns as list for some reason
|
|
266
|
+
formatted_duration = self._config.hook.pytest_xhtml_duration_format(
|
|
267
|
+
duration=duration
|
|
268
|
+
)[0]
|
|
269
|
+
except IndexError:
|
|
270
|
+
formatted_duration = _format_duration(duration)
|
|
271
|
+
|
|
272
|
+
test_id = report.nodeid
|
|
273
|
+
if report.when != "call":
|
|
274
|
+
test_id += f"::{report.when}"
|
|
275
|
+
|
|
276
|
+
data = {
|
|
277
|
+
"extras": processed_extras,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
links = [
|
|
281
|
+
extra
|
|
282
|
+
for extra in data["extras"]
|
|
283
|
+
if extra["format_type"] in ["json", "text", "url"]
|
|
284
|
+
]
|
|
285
|
+
cells = [
|
|
286
|
+
f'<td class="col-result">{outcome}</td>',
|
|
287
|
+
f'<td class="col-testId">{test_id}</td>',
|
|
288
|
+
f'<td class="col-duration">{formatted_duration}</td>',
|
|
289
|
+
f'<td class="col-links">{_process_links(links)}</td>',
|
|
290
|
+
]
|
|
291
|
+
self._config.hook.pytest_xhtml_results_table_row(report=report, cells=cells)
|
|
292
|
+
if not cells:
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
cells = _fix_py(cells)
|
|
296
|
+
self._hydrate_data(data, cells)
|
|
297
|
+
data["resultsTableRow"] = cells
|
|
298
|
+
|
|
299
|
+
processed_logs = _process_logs(report)
|
|
300
|
+
self._config.hook.pytest_xhtml_results_table_html(
|
|
301
|
+
report=report, data=processed_logs
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
self._report.add_test(data, report, outcome, processed_logs)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _format_duration(duration):
|
|
308
|
+
if duration < 1:
|
|
309
|
+
return f"{round(duration * 1000)} ms"
|
|
310
|
+
|
|
311
|
+
hours = math.floor(duration / 3600)
|
|
312
|
+
remaining_seconds = duration % 3600
|
|
313
|
+
minutes = math.floor(remaining_seconds / 60)
|
|
314
|
+
remaining_seconds = remaining_seconds % 60
|
|
315
|
+
seconds = round(remaining_seconds)
|
|
316
|
+
|
|
317
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _is_error(report):
|
|
321
|
+
return (
|
|
322
|
+
report.when in ["setup", "teardown", "collect"] and report.outcome == "failed"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _process_logs(report):
|
|
327
|
+
log = []
|
|
328
|
+
if report.longreprtext:
|
|
329
|
+
log.append(escape(report.longreprtext) + "\n")
|
|
330
|
+
# Don't add captured output to reruns
|
|
331
|
+
if report.outcome != "rerun":
|
|
332
|
+
for section in report.sections:
|
|
333
|
+
header, content = map(escape, section)
|
|
334
|
+
log.append(f"{' ' + header + ' ':-^80}\n{content}")
|
|
335
|
+
|
|
336
|
+
# weird formatting related to logs
|
|
337
|
+
if "log" in header:
|
|
338
|
+
log.append("")
|
|
339
|
+
if "call" in header:
|
|
340
|
+
log.append("")
|
|
341
|
+
if not log:
|
|
342
|
+
log.append("No log output captured.")
|
|
343
|
+
return log
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _process_outcome(report):
|
|
347
|
+
if _is_error(report):
|
|
348
|
+
return "Error"
|
|
349
|
+
if hasattr(report, "wasxfail"):
|
|
350
|
+
if report.outcome in ["passed", "failed"]:
|
|
351
|
+
return "XPassed"
|
|
352
|
+
if report.outcome == "skipped":
|
|
353
|
+
return "XFailed"
|
|
354
|
+
|
|
355
|
+
return report.outcome.capitalize()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _process_links(links):
|
|
359
|
+
a_tag = '<a target="_blank" href="{content}" class="col-links__extra {format_type}">{name}</a>'
|
|
360
|
+
return "".join([a_tag.format_map(link) for link in links])
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _fix_py(cells):
|
|
364
|
+
# backwards-compat
|
|
365
|
+
new_cells = []
|
|
366
|
+
for html in cells:
|
|
367
|
+
if not isinstance(html, str):
|
|
368
|
+
if html.__module__.startswith("py."):
|
|
369
|
+
warnings.warn(
|
|
370
|
+
"The 'py' module is deprecated and support "
|
|
371
|
+
"will be removed in a future release.",
|
|
372
|
+
DeprecationWarning,
|
|
373
|
+
)
|
|
374
|
+
html = str(html)
|
|
375
|
+
html = html.replace("col=", "data-column-type=")
|
|
376
|
+
new_cells.append(html)
|
|
377
|
+
return new_cells
|
pytest_xhtml/extras.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
FORMAT_HTML = "html"
|
|
7
|
+
FORMAT_IMAGE = "image"
|
|
8
|
+
FORMAT_JSON = "json"
|
|
9
|
+
FORMAT_TEXT = "text"
|
|
10
|
+
FORMAT_URL = "url"
|
|
11
|
+
FORMAT_VIDEO = "video"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def extra(
|
|
15
|
+
content: str,
|
|
16
|
+
format_type: str,
|
|
17
|
+
name: Optional[str] = None,
|
|
18
|
+
mime_type: Optional[str] = None,
|
|
19
|
+
extension: Optional[str] = None,
|
|
20
|
+
) -> dict[str, Optional[str]]:
|
|
21
|
+
return {
|
|
22
|
+
"name": name,
|
|
23
|
+
"format_type": format_type,
|
|
24
|
+
"content": content,
|
|
25
|
+
"mime_type": mime_type,
|
|
26
|
+
"extension": extension,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def html(content: str) -> dict[str, Optional[str]]:
|
|
31
|
+
return extra(content, FORMAT_HTML)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def image(
|
|
35
|
+
content: str,
|
|
36
|
+
name: str = "Image",
|
|
37
|
+
mime_type: str = "image/png",
|
|
38
|
+
extension: str = "png",
|
|
39
|
+
) -> dict[str, Optional[str]]:
|
|
40
|
+
return extra(content, FORMAT_IMAGE, name, mime_type, extension)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def png(content: str, name: str = "Image") -> dict[str, Optional[str]]:
|
|
44
|
+
return image(content, name, mime_type="image/png", extension="png")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def jpg(content: str, name: str = "Image") -> dict[str, Optional[str]]:
|
|
48
|
+
return image(content, name, mime_type="image/jpeg", extension="jpg")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def svg(content: str, name: str = "Image") -> dict[str, Optional[str]]:
|
|
52
|
+
return image(content, name, mime_type="image/svg+xml", extension="svg")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def json(content: str, name: str = "JSON") -> dict[str, Optional[str]]:
|
|
56
|
+
return extra(content, FORMAT_JSON, name, "application/json", "json")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def text(content: str, name: str = "Text") -> dict[str, Optional[str]]:
|
|
60
|
+
return extra(content, FORMAT_TEXT, name, "text/plain", "txt")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def url(content: str, name: str = "URL") -> dict[str, Optional[str]]:
|
|
64
|
+
return extra(content, FORMAT_URL, name)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def video(
|
|
68
|
+
content: str,
|
|
69
|
+
name: str = "Video",
|
|
70
|
+
mime_type: str = "video/mp4",
|
|
71
|
+
extension: str = "mp4",
|
|
72
|
+
) -> dict[str, Optional[str]]:
|
|
73
|
+
return extra(content, FORMAT_VIDEO, name, mime_type, extension)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def mp4(content: str, name: str = "Video") -> dict[str, Optional[str]]:
|
|
77
|
+
return video(content, name)
|
pytest_xhtml/fixtures.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
extras_stash_key = pytest.StashKey[list]()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def extra(pytestconfig):
|
|
13
|
+
"""DEPRECATED: Add details to the HTML reports.
|
|
14
|
+
|
|
15
|
+
.. code-block:: python
|
|
16
|
+
|
|
17
|
+
import pytest_xhtml
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_foo(extra):
|
|
21
|
+
extra.append(pytest_xhtml.extras.url("https://www.example.com/"))
|
|
22
|
+
"""
|
|
23
|
+
warnings.warn(
|
|
24
|
+
"The 'extra' fixture is deprecated and will be removed in a future release"
|
|
25
|
+
", use 'extras' instead.",
|
|
26
|
+
DeprecationWarning,
|
|
27
|
+
)
|
|
28
|
+
pytestconfig.stash[extras_stash_key] = []
|
|
29
|
+
yield pytestconfig.stash[extras_stash_key]
|
|
30
|
+
del pytestconfig.stash[extras_stash_key][:]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def extras(pytestconfig):
|
|
35
|
+
"""Add details to the HTML reports.
|
|
36
|
+
|
|
37
|
+
.. code-block:: python
|
|
38
|
+
|
|
39
|
+
import pytest_xhtml
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_foo(extras):
|
|
43
|
+
extras.append(pytest_xhtml.extras.url("https://www.example.com/"))
|
|
44
|
+
"""
|
|
45
|
+
pytestconfig.stash[extras_stash_key] = []
|
|
46
|
+
yield pytestconfig.stash[extras_stash_key]
|
|
47
|
+
del pytestconfig.stash[extras_stash_key][:]
|
pytest_xhtml/hooks.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def pytest_xhtml_report_title(report):
|
|
7
|
+
"""Called before adding the title to the report"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pytest_xhtml_results_summary(prefix, summary, postfix, session):
|
|
11
|
+
"""Called before adding the summary section to the report"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pytest_xhtml_results_table_header(cells):
|
|
15
|
+
"""Called after building results table header."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def pytest_xhtml_results_table_row(report, cells):
|
|
19
|
+
"""Called after building results table row."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pytest_xhtml_results_table_html(report, data):
|
|
23
|
+
"""Called after building results table additional HTML."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def pytest_xhtml_duration_format(duration):
|
|
27
|
+
"""Called before using the default duration formatting."""
|