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 <ssbarnea@redhat.com>
This commit is contained in:
Gleb Nikonorov 2020-11-27 07:10:43 -05:00 committed by GitHub
parent 67d213575c
commit b57ca0bcf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 105 additions and 13 deletions

View File

@ -1,6 +1,12 @@
Release Notes
-------------
**3.1.0 (unreleased)**
* Allow for report duration formatting (`#376 <https://github.com/pytest-dev/pytest-html/issues/376>`_)
* Thanks to `@brettnolan <https://github.com/brettnolan>`_ for reporting and `@gnikonorov <https://github.com/gnikonorov>`_ for the fix
**3.0.0 (2020-10-28)**
* Respect ``--capture=no``, ``--show-capture=no``, and ``-s`` pytest flags (`#171 <https://github.com/pytest-dev/pytest-html/issues/171>`_)

View File

@ -284,6 +284,26 @@ or by setting the :code:`render_collapsed` in a configuration file (pytest.ini,
**NOTE:** Setting :code:`render_collapsed` will, unlike the query parameter, affect all statuses.
The formatting of the timestamp used in the :code:`Durations` column can be modified by setting :code:`duration_formatter`
on the :code:`report` attribute. All `time.strftime`_ formatting directives are supported. In addition, it is possible
to supply :code:`%f` to get duration milliseconds. If this value is not set, the values in the :code:`Durations` column are
displayed in :code:`%S.%f` format where :code:`%S` is the total number of seconds a test ran for.
Below is an example of a :code:`conftest.py` file setting :code:`duration_formatter`:
.. code-block:: python
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
setattr(report, "duration_formatter", "%H:%M:%S.%f")
**NOTE**: Milliseconds are always displayed with a precision of 2
Screenshots
-----------
@ -305,4 +325,6 @@ Resources
- `Issue Tracker <http://github.com/pytest-dev/pytest-html/issues>`_
- `Code <http://github.com/pytest-dev/pytest-html/>`_
.. _JSON: http://json.org/
.. _time.strftime: https://docs.python.org/3/library/time.html#time.strftime

View File

@ -157,6 +157,7 @@ class HTMLReport:
if getattr(report, "when", "call") != "call":
self.test_id = "::".join([report.nodeid, report.when])
self.time = getattr(report, "duration", 0.0)
self.formatted_time = getattr(report, "formatted_duration", 0.0)
self.outcome = outcome
self.additional_html = []
self.links_html = []
@ -183,7 +184,7 @@ class HTMLReport:
cells = [
html.td(self.outcome, class_="col-result"),
html.td(self.test_id, class_="col-name"),
html.td(f"{self.time:.2f}", class_="col-duration"),
html.td(self.formatted_time, class_="col-duration"),
html.td(self.links_html, class_="col-links"),
]
@ -537,7 +538,7 @@ class HTMLReport:
cells = [
html.th("Result", class_="sortable result initial-sort", col="result"),
html.th("Test", class_="sortable", col="name"),
html.th("Duration", class_="sortable numeric", col="duration"),
html.th("Duration", class_="sortable", col="duration"),
html.th("Links", class_="sortable links", col="links"),
]
session.config.hook.pytest_html_results_table_header(cells=cells)
@ -603,6 +604,32 @@ class HTMLReport:
unicode_doc = unicode_doc.encode("utf-8", errors="xmlcharrefreplace")
return unicode_doc.decode("utf-8")
def _format_duration(self, report):
# parse the report duration into its display version and return it to the caller
duration_formatter = getattr(report, "duration_formatter", None)
string_duration = str(report.duration)
if duration_formatter is None:
if "." in string_duration:
split_duration = string_duration.split(".")
split_duration[1] = split_duration[1][0:2]
string_duration = ".".join(split_duration)
return string_duration
else:
# support %f, since time.strftime doesn't support it out of the box
# keep a precision of 2 for legacy reasons
formatted_milliseconds = "00"
if "." in string_duration:
milliseconds = string_duration.split(".")[1]
formatted_milliseconds = milliseconds[0:2]
duration_formatter = duration_formatter.replace(
"%f", formatted_milliseconds
)
duration_as_gmtime = time.gmtime(report.duration)
return time.strftime(duration_formatter, duration_as_gmtime)
def _generate_environment(self, config):
if not hasattr(config, "_metadata") or config._metadata is None:
return []
@ -688,6 +715,7 @@ class HTMLReport:
test_report.longrepr = full_text
test_report.extra = extras
test_report.duration = duration
test_report.formatted_duration = self._format_duration(test_report)
if wasxfail:
test_report.wasxfail = True

View File

@ -28,9 +28,7 @@ function sort_column(elem) {
toggle_sort_states(elem);
const colIndex = toArray(elem.parentNode.childNodes).indexOf(elem);
let key;
if (elem.classList.contains('numeric')) {
key = key_num;
} else if (elem.classList.contains('result')) {
if (elem.classList.contains('result')) {
key = key_result;
} else if (elem.classList.contains('links')) {
key = key_link;
@ -173,12 +171,6 @@ function key_alpha(col_index) {
};
}
function key_num(col_index) {
return function(elem) {
return parseFloat(elem.childNodes[1].childNodes[col_index].firstChild.data);
};
}
function key_link(col_index) {
return function(elem) {
const dataCell = elem.childNodes[1].childNodes[col_index].firstChild;

View File

@ -19,7 +19,7 @@
<tr>
<th class="sortable result initial-sort" col="result">Result</th>
<th class="sortable" col="name">Test</th>
<th class="sortable numeric" col="duration">Duration</th>
<th class="sortable" col="duration">Duration</th>
<th class="sortable links" col="links">Links</th></tr>
<tr hidden="true" id="not-found-message">
<th colspan="5">No results found. Try to check the filters</th>

View File

@ -43,7 +43,7 @@
sort_column_test('[col=name]',
'passed results-table-row', 'rerun results-table-row');
//numeric
//duration
sort_column_test('[col=duration]',
'rerun results-table-row', 'passed results-table-row');
sort_column_test('[col=duration]',

View File

@ -115,6 +115,50 @@ class TestHTML:
m = p.search(html)
assert float(m.group(1)) >= sleep
@pytest.mark.parametrize(
"duration_formatter,expected_report_content",
[
("%f", r'<td class="col-duration">\d{2}</td>'),
("%S.%f", r'<td class="col-duration">\d{2}\.\d{2}</td>'),
(
"ABC%H %M %S123",
r'<td class="col-duration">ABC\d{2} \d{2} \d{2}123</td>',
),
],
)
def test_can_format_duration_column(
self, testdir, duration_formatter, expected_report_content
):
testdir.makeconftest(
f"""
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
setattr(report, "duration_formatter", "{duration_formatter}")
"""
)
sleep = float(0.2)
testdir.makepyfile(
"""
import time
def test_sleep():
time.sleep({:f})
""".format(
sleep
)
)
result, html = run(testdir)
assert result.ret == 0
assert_results(html, duration=sleep)
compiled_regex = re.compile(expected_report_content)
assert compiled_regex.search(html)
def test_pass(self, testdir):
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)