Skip to content

Allow for report duration formatting #380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Nov 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -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>`_)
Expand Down
22 changes: 22 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------

Expand All @@ -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
32 changes: 30 additions & 2 deletions pytest_html/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def __init__(self, outcome, report, logfile, config):
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 = []
Expand All @@ -183,7 +184,7 @@ def __init__(self, outcome, report, logfile, config):
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"),
]

Expand Down Expand Up @@ -537,7 +538,7 @@ def generate_summary_item(self):
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)
Expand Down Expand Up @@ -603,6 +604,32 @@ def generate_summary_item(self):
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 []
Expand Down Expand Up @@ -688,6 +715,7 @@ def _post_process_reports(self):
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
Expand Down
10 changes: 1 addition & 9 deletions pytest_html/resources/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion testing/js_test_report.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion testing/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]',
Expand Down
44 changes: 44 additions & 0 deletions testing/test_pytest_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,50 @@ def test_sleep():
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)
Expand Down