Add hooks for modifying the test results table

This commit is contained in:
Dave Hunt 2017-02-24 14:40:33 +00:00
parent a1413ed734
commit bc2e8a3555
No known key found for this signature in database
GPG Key ID: 4000D32ABB02F959
4 changed files with 122 additions and 29 deletions

View File

@ -3,6 +3,7 @@ Release Notes
**1.14.0 (unreleased)**
* Add hooks for modifying the test results table
* Replace environment section with values from
`pytest-metadata <https://pypi.python.org/pypi/pytest-metadata/>`_
* Fix encoding for asset files

View File

@ -128,6 +128,65 @@ conftest.py file:
extra.append(pytest_html.extras.html('<div>Additional HTML</div>'))
report.extra = extra
Modifying the results table
~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can modify the columns by implementing custom hooks for the header and
rows. The following example :code:`conftest.py` adds a description column with
the test function docstring, adds a sortable time column, and removes the links
column:
.. code-block:: python
from datetime import datetime
from py.xml import html
import pytest
@pytest.mark.optionalhook
def pytest_html_results_table_header(cells):
cells.insert(2, html.th('Description'))
cells.insert(0, html.th('Time', class_='sortable time', col='time'))
cells.pop()
@pytest.mark.optionalhook
def pytest_html_results_table_row(report, cells):
cells.insert(2, html.td(report.description))
cells.insert(1, html.td(datetime.utcnow(), class_='col-time'))
cells.pop()
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
report.description = str(item.function.__doc__)
You can also remove results by implementing the
:code:`pytest_html_results_table_row` hook and removing all cells. The
following example removes all passed results from the report:
.. code-block:: python
import pytest
@pytest.mark.optionalhook
def pytest_html_results_table_row(report, cells):
if report.passed:
del cells[:]
The log output and additional HTML can be modified by implementing the
:code:`pytest_html_results_html` hook. The following example replaces all
additional HTML and log output with a notice that the log is empty:
.. code-block:: python
import pytest
@pytest.mark.optionalhook
def pytest_html_results_table_html(report, data):
if report.passed:
del data[:]
data.append(html.div('No log output captured.', class_='empty log'))
Screenshots
-----------

15
pytest_html/hooks.py Normal file
View File

@ -0,0 +1,15 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
def pytest_html_results_table_header(cells):
""" Called after building results table header. """
def pytest_html_results_table_row(report, cells):
""" Called after building results table row. """
def pytest_html_results_table_html(report, data):
""" Called after building results table additional HTML. """

View File

@ -37,6 +37,11 @@ else:
from cgi import escape
def pytest_addhooks(pluginmanager):
from . import hooks
pluginmanager.add_hookspecs(hooks)
def pytest_addoption(parser):
group = parser.getgroup('terminal reporting')
group.addoption('--html', action='store', dest='htmlpath',
@ -54,10 +59,7 @@ def pytest_configure(config):
htmlpath = config.option.htmlpath
# prevent opening htmlpath on slave nodes (xdist)
if htmlpath and not hasattr(config, 'slaveinput'):
config._html = HTMLReport(htmlpath,
config.getoption('self_contained_html'),
config.pluginmanager
.hasplugin('rerunfailures'))
config._html = HTMLReport(htmlpath, config)
config.pluginmanager.register(config._html)
@ -78,7 +80,7 @@ def data_uri(content, mime_type='text/plain', charset='utf-8'):
class HTMLReport(object):
def __init__(self, logfile, self_contained, has_rerun):
def __init__(self, logfile, config):
logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.abspath(logfile)
self.test_logs = []
@ -86,12 +88,14 @@ class HTMLReport(object):
self.errors = self.failed = 0
self.passed = self.skipped = 0
self.xfailed = self.xpassed = 0
has_rerun = config.pluginmanager.hasplugin('rerunfailures')
self.rerun = 0 if has_rerun else None
self.self_contained = self_contained
self.self_contained = config.getoption('self_contained_html')
self.config = config
class TestResult:
def __init__(self, outcome, report, self_contained, logfile):
def __init__(self, outcome, report, logfile, config):
self.test_id = report.nodeid
if report.when != 'call':
self.test_id = '::'.join([report.nodeid, report.when])
@ -99,8 +103,10 @@ class HTMLReport(object):
self.outcome = outcome
self.additional_html = []
self.links_html = []
self.self_contained = self_contained
self.self_contained = config.getoption('self_contained_html')
self.logfile = logfile
self.config = config
self.row_table = self.row_extra = None
test_index = hasattr(report, 'rerun') and report.rerun + 1 or 0
@ -109,14 +115,22 @@ class HTMLReport(object):
self.append_log_html(report, self.additional_html)
self.row_table = html.tr([
cells = [
html.td(self.outcome, class_='col-result'),
html.td(self.test_id, class_='col-name'),
html.td('{0:.2f}'.format(self.time), class_='col-duration'),
html.td(self.links_html, class_='col-links')])
html.td(self.links_html, class_='col-links')]
self.row_extra = html.tr(html.td(self.additional_html,
class_='extra', colspan='5'))
self.config.hook.pytest_html_results_table_row(
report=report, cells=cells)
self.config.hook.pytest_html_results_table_html(
report=report, data=self.additional_html)
if len(cells) > 0:
self.row_table = html.tr(cells)
self.row_extra = html.tr(html.td(self.additional_html,
class_='extra', colspan=len(cells)))
def __lt__(self, other):
order = ('Error', 'Failed', 'Rerun', 'XFailed',
@ -232,13 +246,16 @@ class HTMLReport(object):
additional_html.append(log)
def _appendrow(self, outcome, report):
result = self.TestResult(outcome, report, self.self_contained,
self.logfile)
index = bisect.bisect_right(self.results, result)
self.results.insert(index, result)
self.test_logs.insert(index, html.tbody(result.row_table,
result.row_extra, class_=result.outcome.lower() +
' results-table-row'))
result = self.TestResult(outcome, report, self.logfile, self.config)
if result.row_table is not None:
index = bisect.bisect_right(self.results, result)
self.results.insert(index, result)
tbody = html.tbody(
result.row_table,
class_='{0} results-table-row'.format(result.outcome.lower()))
if result.row_extra is not None:
tbody.append(result.row_extra)
self.test_logs.insert(index, tbody)
def append_passed(self, report):
if report.when == 'call':
@ -362,19 +379,20 @@ class HTMLReport(object):
if i < len(outcomes):
summary.append(', ')
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('Links')]
session.config.hook.pytest_html_results_table_header(cells=cells)
results = [html.h2('Results'), html.table([html.thead(
html.tr([
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('Links')]),
html.tr(cells),
html.tr([
html.th('No results found. Try to check the filters',
colspan='5')],
colspan=len(cells))],
id='not-found-message', hidden='true'),
id='results-table-head'),
self.test_logs], id='results-table')]