123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- # 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/.
- from __future__ import absolute_import
- from base64 import b64encode, b64decode
- import datetime
- import json
- import os
- import pkg_resources
- import sys
- import time
- import bisect
- import hashlib
- import warnings
- try:
- from ansi2html import Ansi2HTMLConverter, style
- ANSI = True
- except ImportError:
- # ansi2html is not installed
- ANSI = False
- from py.xml import html, raw
- from . import extras
- from . import __version__, __pypi_url__
- PY3 = sys.version_info[0] == 3
- # Python 2.X and 3.X compatibility
- if PY3:
- basestring = str
- from html import escape
- else:
- from codecs import open
- 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',
- metavar='path', default=None,
- help='create html report file at given path.')
- group.addoption('--self-contained-html', action='store_true',
- help='create a self-contained html file containing all '
- 'necessary styles, scripts, and images - this means '
- 'that the report may not render or function where CSP '
- 'restrictions are in place (see '
- 'https://developer.mozilla.org/docs/Web/Security/CSP)')
- 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)
- config.pluginmanager.register(config._html)
- def pytest_unconfigure(config):
- html = getattr(config, '_html', None)
- if html:
- del config._html
- config.pluginmanager.unregister(html)
- def data_uri(content, mime_type='text/plain', charset='utf-8'):
- data = b64encode(content.encode(charset)).decode('ascii')
- return 'data:{0};charset={1};base64,{2}'.format(mime_type, charset, data)
- class HTMLReport(object):
- def __init__(self, logfile, config):
- logfile = os.path.expanduser(os.path.expandvars(logfile))
- self.logfile = os.path.abspath(logfile)
- self.test_logs = []
- self.results = []
- 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 = config.getoption('self_contained_html')
- self.config = config
- class TestResult:
- def __init__(self, outcome, report, logfile, config):
- self.test_id = report.nodeid
- if report.when != 'call':
- self.test_id = '::'.join([report.nodeid, report.when])
- self.time = getattr(report, 'duration', 0.0)
- self.outcome = outcome
- self.additional_html = []
- self.links_html = []
- 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
- for extra_index, extra in enumerate(getattr(report, 'extra', [])):
- self.append_extra_html(extra, extra_index, test_index)
- self.append_log_html(report, self.additional_html)
- 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')]
- 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',
- 'XPassed', 'Skipped', 'Passed')
- return order.index(self.outcome) < order.index(other.outcome)
- def create_asset(self, content, extra_index,
- test_index, file_extension, mode='w'):
- hash_key = ''.join([self.test_id, str(extra_index),
- str(test_index)]).encode('utf-8')
- hash_generator = hashlib.md5()
- hash_generator.update(hash_key)
- asset_file_name = '{0}.{1}'.format(hash_generator.hexdigest(),
- file_extension)
- asset_path = os.path.join(os.path.dirname(self.logfile),
- 'assets', asset_file_name)
- if not os.path.exists(os.path.dirname(asset_path)):
- os.makedirs(os.path.dirname(asset_path))
- relative_path = '{0}/{1}'.format('assets', asset_file_name)
- kwargs = {'encoding': 'utf-8'} if 'b' not in mode else {}
- with open(asset_path, mode, **kwargs) as f:
- f.write(content)
- return relative_path
- def append_extra_html(self, extra, extra_index, test_index):
- href = None
- if extra.get('format') == extras.FORMAT_IMAGE:
- content = extra.get('content')
- if content.startswith(('file', 'http')) or \
- os.path.isfile(content):
- if self.self_contained:
- warnings.warn('Self-contained HTML report '
- 'includes link to external '
- 'resource: {}'.format(content))
- html_div = html.a(html.img(src=content), href=content)
- elif self.self_contained:
- src = 'data:{0};base64,{1}'.format(
- extra.get('mime_type'),
- content)
- html_div = html.img(src=src)
- else:
- if PY3:
- content = b64decode(content.encode('utf-8'))
- else:
- content = b64decode(content)
- href = src = self.create_asset(
- content, extra_index, test_index,
- extra.get('extension'), 'wb')
- html_div = html.a(html.img(src=src), href=href)
- self.additional_html.append(html.div(html_div, class_='image'))
- elif extra.get('format') == extras.FORMAT_HTML:
- self.additional_html.append(html.div(
- raw(extra.get('content'))))
- elif extra.get('format') == extras.FORMAT_JSON:
- content = json.dumps(extra.get('content'))
- if self.self_contained:
- href = data_uri(content,
- mime_type=extra.get('mime_type'))
- else:
- href = self.create_asset(content, extra_index,
- test_index,
- extra.get('extension'))
- elif extra.get('format') == extras.FORMAT_TEXT:
- content = extra.get('content')
- if isinstance(content, bytes):
- content = content.decode('utf-8')
- if self.self_contained:
- href = data_uri(content)
- else:
- href = self.create_asset(content, extra_index,
- test_index,
- extra.get('extension'))
- elif extra.get('format') == extras.FORMAT_URL:
- href = extra.get('content')
- if href is not None:
- self.links_html.append(html.a(
- extra.get('name'),
- class_=extra.get('format'),
- href=href,
- target='_blank'))
- self.links_html.append(' ')
- def append_log_html(self, report, additional_html):
- log = html.div(class_='log')
- if report.longrepr:
- for line in report.longreprtext.splitlines():
- separator = line.startswith('_ ' * 10)
- if separator:
- log.append(line[:80])
- else:
- exception = line.startswith("E ")
- if exception:
- log.append(html.span(raw(escape(line)),
- class_='error'))
- else:
- log.append(raw(escape(line)))
- log.append(html.br())
- for section in report.sections:
- header, content = map(escape, section)
- log.append(' {0} '.format(header).center(80, '-'))
- log.append(html.br())
- if ANSI:
- converter = Ansi2HTMLConverter(inline=False, escaped=False)
- content = converter.convert(content, full=False)
- log.append(raw(content))
- if len(log) == 0:
- log = html.div(class_='empty log')
- log.append('No log output captured.')
- additional_html.append(log)
- def _appendrow(self, outcome, report):
- 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':
- if hasattr(report, "wasxfail"):
- self.xpassed += 1
- self._appendrow('XPassed', report)
- else:
- self.passed += 1
- self._appendrow('Passed', report)
- def append_failed(self, report):
- if report.when == "call":
- if hasattr(report, "wasxfail"):
- # pytest < 3.0 marked xpasses as failures
- self.xpassed += 1
- self._appendrow('XPassed', report)
- else:
- self.failed += 1
- self._appendrow('Failed', report)
- else:
- self.errors += 1
- self._appendrow('Error', report)
- def append_skipped(self, report):
- if hasattr(report, "wasxfail"):
- self.xfailed += 1
- self._appendrow('XFailed', report)
- else:
- self.skipped += 1
- self._appendrow('Skipped', report)
- def append_other(self, report):
- # For now, the only "other" the plugin give support is rerun
- self.rerun += 1
- self._appendrow('Rerun', report)
- def _generate_report(self, session):
- suite_stop_time = time.time()
- suite_time_delta = suite_stop_time - self.suite_start_time
- numtests = self.passed + self.failed + self.xpassed + self.xfailed
- generated = datetime.datetime.now()
- self.style_css = pkg_resources.resource_string(
- __name__, os.path.join('resources', 'style.css'))
- if PY3:
- self.style_css = self.style_css.decode('utf-8')
- if ANSI:
- ansi_css = [
- '\n/******************************',
- ' * ANSI2HTML STYLES',
- ' ******************************/\n']
- ansi_css.extend([str(r) for r in style.get_styles()])
- self.style_css += '\n'.join(ansi_css)
- css_href = '{0}/{1}'.format('assets', 'style.css')
- html_css = html.link(href=css_href, rel='stylesheet',
- type='text/css')
- if self.self_contained:
- html_css = html.style(raw(self.style_css))
- head = html.head(
- html.meta(charset='utf-8'),
- html.title('Test Report'),
- html_css)
- class Outcome:
- def __init__(self, outcome, total=0, label=None,
- test_result=None, class_html=None):
- self.outcome = outcome
- self.label = label or outcome
- self.class_html = class_html or outcome
- self.total = total
- self.test_result = test_result or outcome
- self.generate_checkbox()
- self.generate_summary_item()
- def generate_checkbox(self):
- checkbox_kwargs = {'data-test-result':
- self.test_result.lower()}
- if self.total == 0:
- checkbox_kwargs['disabled'] = 'true'
- self.checkbox = html.input(type='checkbox',
- checked='true',
- onChange='filter_table(this)',
- name='filter_checkbox',
- class_='filter',
- hidden='true',
- **checkbox_kwargs)
- def generate_summary_item(self):
- self.summary_item = html.span('{0} {1}'.
- format(self.total, self.label),
- class_=self.class_html)
- outcomes = [Outcome('passed', self.passed),
- Outcome('skipped', self.skipped),
- Outcome('failed', self.failed),
- Outcome('error', self.errors, label='errors'),
- Outcome('xfailed', self.xfailed,
- label='expected failures'),
- Outcome('xpassed', self.xpassed,
- label='unexpected passes')]
- if self.rerun is not None:
- outcomes.append(Outcome('rerun', self.rerun))
- summary = [html.h2('Summary'), html.p(
- '{0} tests ran in {1:.2f} seconds. '.format(
- numtests, suite_time_delta)),
- html.p('(Un)check the boxes to filter the results.',
- class_='filter',
- hidden='true')]
- for i, outcome in enumerate(outcomes, start=1):
- summary.append(outcome.checkbox)
- summary.append(outcome.summary_item)
- 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(cells),
- html.tr([
- html.th('No results found. Try to check the filters',
- colspan=len(cells))],
- id='not-found-message', hidden='true'),
- id='results-table-head'),
- self.test_logs], id='results-table')]
- main_js = pkg_resources.resource_string(
- __name__, os.path.join('resources', 'main.js'))
- if PY3:
- main_js = main_js.decode('utf-8')
- body = html.body(
- html.script(raw(main_js)),
- html.p('Report generated on {0} at {1} by'.format(
- generated.strftime('%d-%b-%Y'),
- generated.strftime('%H:%M:%S')),
- html.a(' pytest-html', href=__pypi_url__),
- ' v{0}'.format(__version__)),
- onLoad='init()')
- body.extend(self._generate_environment(session.config))
- body.extend(summary)
- body.extend(results)
- doc = html.html(head, body)
- unicode_doc = u'<!DOCTYPE html>\n{0}'.format(doc.unicode(indent=2))
- if PY3:
- # Fix encoding issues, e.g. with surrogates
- unicode_doc = unicode_doc.encode('utf-8',
- errors='xmlcharrefreplace')
- unicode_doc = unicode_doc.decode('utf-8')
- return unicode_doc
- def _generate_environment(self, config):
- if not hasattr(config, '_metadata') or config._metadata is None:
- return []
- metadata = config._metadata
- environment = [html.h2('Environment')]
- rows = []
- for key in [k for k in sorted(metadata.keys()) if metadata[k]]:
- value = metadata[key]
- if isinstance(value, basestring) and value.startswith('http'):
- value = html.a(value, href=value, target='_blank')
- rows.append(html.tr(html.td(key), html.td(value)))
- environment.append(html.table(rows, id='environment'))
- return environment
- def _save_report(self, report_content):
- dir_name = os.path.dirname(self.logfile)
- assets_dir = os.path.join(dir_name, 'assets')
- if not os.path.exists(dir_name):
- os.makedirs(dir_name)
- if not self.self_contained and not os.path.exists(assets_dir):
- os.makedirs(assets_dir)
- with open(self.logfile, 'w', encoding='utf-8') as f:
- f.write(report_content)
- if not self.self_contained:
- style_path = os.path.join(assets_dir, 'style.css')
- with open(style_path, 'w', encoding='utf-8') as f:
- f.write(self.style_css)
- def pytest_runtest_logreport(self, report):
- if report.passed:
- self.append_passed(report)
- elif report.failed:
- self.append_failed(report)
- elif report.skipped:
- self.append_skipped(report)
- else:
- self.append_other(report)
- def pytest_sessionstart(self, session):
- self.suite_start_time = time.time()
- def pytest_sessionfinish(self, session):
- report_content = self._generate_report(session)
- self._save_report(report_content)
- def pytest_terminal_summary(self, terminalreporter):
- terminalreporter.write_sep('-', 'generated html file: {0}'.format(
- self.logfile))
|