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:
parent
67d213575c
commit
b57ca0bcf2
|
@ -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>`_)
|
||||
|
|
22
README.rst
22
README.rst
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]',
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue