Skip to content

Commit b57ca0b

Browse files
gnikonorovssbarnea
andauthored
Allow for report duration formatting (#380)
* first attempt at a working solution * final solution, no tests * Update README.rst Add documentation on how to provide a custom display value for duration formatters * temp * finalize the change * undo test report changes * fixup tests * Clarify default duration column data formatting * fix failing tests Co-authored-by: Sorin Sbarnea <[email protected]>
1 parent 67d2135 commit b57ca0b

File tree

7 files changed

+105
-13
lines changed

7 files changed

+105
-13
lines changed

CHANGES.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Release Notes
22
-------------
33

4+
**3.1.0 (unreleased)**
5+
6+
* Allow for report duration formatting (`#376 <https://github.com/pytest-dev/pytest-html/issues/376>`_)
7+
8+
* Thanks to `@brettnolan <https://github.com/brettnolan>`_ for reporting and `@gnikonorov <https://github.com/gnikonorov>`_ for the fix
9+
410
**3.0.0 (2020-10-28)**
511

612
* Respect ``--capture=no``, ``--show-capture=no``, and ``-s`` pytest flags (`#171 <https://github.com/pytest-dev/pytest-html/issues/171>`_)

README.rst

+22
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,26 @@ or by setting the :code:`render_collapsed` in a configuration file (pytest.ini,
284284
285285
**NOTE:** Setting :code:`render_collapsed` will, unlike the query parameter, affect all statuses.
286286

287+
The formatting of the timestamp used in the :code:`Durations` column can be modified by setting :code:`duration_formatter`
288+
on the :code:`report` attribute. All `time.strftime`_ formatting directives are supported. In addition, it is possible
289+
to supply :code:`%f` to get duration milliseconds. If this value is not set, the values in the :code:`Durations` column are
290+
displayed in :code:`%S.%f` format where :code:`%S` is the total number of seconds a test ran for.
291+
292+
Below is an example of a :code:`conftest.py` file setting :code:`duration_formatter`:
293+
294+
.. code-block:: python
295+
296+
import pytest
297+
298+
299+
@pytest.hookimpl(hookwrapper=True)
300+
def pytest_runtest_makereport(item, call):
301+
outcome = yield
302+
report = outcome.get_result()
303+
setattr(report, "duration_formatter", "%H:%M:%S.%f")
304+
305+
**NOTE**: Milliseconds are always displayed with a precision of 2
306+
287307
Screenshots
288308
-----------
289309

@@ -305,4 +325,6 @@ Resources
305325
- `Issue Tracker <http://github.com/pytest-dev/pytest-html/issues>`_
306326
- `Code <http://github.com/pytest-dev/pytest-html/>`_
307327

328+
308329
.. _JSON: http://json.org/
330+
.. _time.strftime: https://docs.python.org/3/library/time.html#time.strftime

pytest_html/plugin.py

+30-2
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def __init__(self, outcome, report, logfile, config):
157157
if getattr(report, "when", "call") != "call":
158158
self.test_id = "::".join([report.nodeid, report.when])
159159
self.time = getattr(report, "duration", 0.0)
160+
self.formatted_time = getattr(report, "formatted_duration", 0.0)
160161
self.outcome = outcome
161162
self.additional_html = []
162163
self.links_html = []
@@ -183,7 +184,7 @@ def __init__(self, outcome, report, logfile, config):
183184
cells = [
184185
html.td(self.outcome, class_="col-result"),
185186
html.td(self.test_id, class_="col-name"),
186-
html.td(f"{self.time:.2f}", class_="col-duration"),
187+
html.td(self.formatted_time, class_="col-duration"),
187188
html.td(self.links_html, class_="col-links"),
188189
]
189190

@@ -537,7 +538,7 @@ def generate_summary_item(self):
537538
cells = [
538539
html.th("Result", class_="sortable result initial-sort", col="result"),
539540
html.th("Test", class_="sortable", col="name"),
540-
html.th("Duration", class_="sortable numeric", col="duration"),
541+
html.th("Duration", class_="sortable", col="duration"),
541542
html.th("Links", class_="sortable links", col="links"),
542543
]
543544
session.config.hook.pytest_html_results_table_header(cells=cells)
@@ -603,6 +604,32 @@ def generate_summary_item(self):
603604
unicode_doc = unicode_doc.encode("utf-8", errors="xmlcharrefreplace")
604605
return unicode_doc.decode("utf-8")
605606

607+
def _format_duration(self, report):
608+
# parse the report duration into its display version and return it to the caller
609+
duration_formatter = getattr(report, "duration_formatter", None)
610+
string_duration = str(report.duration)
611+
if duration_formatter is None:
612+
if "." in string_duration:
613+
split_duration = string_duration.split(".")
614+
split_duration[1] = split_duration[1][0:2]
615+
616+
string_duration = ".".join(split_duration)
617+
618+
return string_duration
619+
else:
620+
# support %f, since time.strftime doesn't support it out of the box
621+
# keep a precision of 2 for legacy reasons
622+
formatted_milliseconds = "00"
623+
if "." in string_duration:
624+
milliseconds = string_duration.split(".")[1]
625+
formatted_milliseconds = milliseconds[0:2]
626+
627+
duration_formatter = duration_formatter.replace(
628+
"%f", formatted_milliseconds
629+
)
630+
duration_as_gmtime = time.gmtime(report.duration)
631+
return time.strftime(duration_formatter, duration_as_gmtime)
632+
606633
def _generate_environment(self, config):
607634
if not hasattr(config, "_metadata") or config._metadata is None:
608635
return []
@@ -688,6 +715,7 @@ def _post_process_reports(self):
688715
test_report.longrepr = full_text
689716
test_report.extra = extras
690717
test_report.duration = duration
718+
test_report.formatted_duration = self._format_duration(test_report)
691719

692720
if wasxfail:
693721
test_report.wasxfail = True

pytest_html/resources/main.js

+1-9
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ function sort_column(elem) {
2828
toggle_sort_states(elem);
2929
const colIndex = toArray(elem.parentNode.childNodes).indexOf(elem);
3030
let key;
31-
if (elem.classList.contains('numeric')) {
32-
key = key_num;
33-
} else if (elem.classList.contains('result')) {
31+
if (elem.classList.contains('result')) {
3432
key = key_result;
3533
} else if (elem.classList.contains('links')) {
3634
key = key_link;
@@ -173,12 +171,6 @@ function key_alpha(col_index) {
173171
};
174172
}
175173

176-
function key_num(col_index) {
177-
return function(elem) {
178-
return parseFloat(elem.childNodes[1].childNodes[col_index].firstChild.data);
179-
};
180-
}
181-
182174
function key_link(col_index) {
183175
return function(elem) {
184176
const dataCell = elem.childNodes[1].childNodes[col_index].firstChild;

testing/js_test_report.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<tr>
2020
<th class="sortable result initial-sort" col="result">Result</th>
2121
<th class="sortable" col="name">Test</th>
22-
<th class="sortable numeric" col="duration">Duration</th>
22+
<th class="sortable" col="duration">Duration</th>
2323
<th class="sortable links" col="links">Links</th></tr>
2424
<tr hidden="true" id="not-found-message">
2525
<th colspan="5">No results found. Try to check the filters</th>

testing/test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
sort_column_test('[col=name]',
4444
'passed results-table-row', 'rerun results-table-row');
4545

46-
//numeric
46+
//duration
4747
sort_column_test('[col=duration]',
4848
'rerun results-table-row', 'passed results-table-row');
4949
sort_column_test('[col=duration]',

testing/test_pytest_html.py

+44
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,50 @@ def test_sleep():
115115
m = p.search(html)
116116
assert float(m.group(1)) >= sleep
117117

118+
@pytest.mark.parametrize(
119+
"duration_formatter,expected_report_content",
120+
[
121+
("%f", r'<td class="col-duration">\d{2}</td>'),
122+
("%S.%f", r'<td class="col-duration">\d{2}\.\d{2}</td>'),
123+
(
124+
"ABC%H %M %S123",
125+
r'<td class="col-duration">ABC\d{2} \d{2} \d{2}123</td>',
126+
),
127+
],
128+
)
129+
def test_can_format_duration_column(
130+
self, testdir, duration_formatter, expected_report_content
131+
):
132+
133+
testdir.makeconftest(
134+
f"""
135+
import pytest
136+
137+
@pytest.hookimpl(hookwrapper=True)
138+
def pytest_runtest_makereport(item, call):
139+
outcome = yield
140+
report = outcome.get_result()
141+
setattr(report, "duration_formatter", "{duration_formatter}")
142+
"""
143+
)
144+
145+
sleep = float(0.2)
146+
testdir.makepyfile(
147+
"""
148+
import time
149+
def test_sleep():
150+
time.sleep({:f})
151+
""".format(
152+
sleep
153+
)
154+
)
155+
result, html = run(testdir)
156+
assert result.ret == 0
157+
assert_results(html, duration=sleep)
158+
159+
compiled_regex = re.compile(expected_report_content)
160+
assert compiled_regex.search(html)
161+
118162
def test_pass(self, testdir):
119163
testdir.makepyfile("def test_pass(): pass")
120164
result, html = run(testdir)

0 commit comments

Comments
 (0)