Migrate to 'black' for consistent styling and formatting (#218)
* Migrate to 'black' for consistent styling and formatting * Add black to tox and travis
This commit is contained in:
parent
c9eadb0b76
commit
8e96c6ed94
|
@ -31,4 +31,4 @@ local.properties
|
|||
##Tests JS
|
||||
node_modules/
|
||||
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
|
|
48
.travis.yml
48
.travis.yml
|
@ -1,50 +1,68 @@
|
|||
language: python
|
||||
jobs:
|
||||
include:
|
||||
- stage:
|
||||
- stage: tests
|
||||
language: node_js
|
||||
node_js: node
|
||||
install: npm install phantomjs-prebuilt@2.1.15 grunt-cli grunt grunt-contrib-qunit
|
||||
script: grunt test
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: required
|
||||
env: TOXENV=flake8
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: required
|
||||
env: TOXENV=black
|
||||
|
||||
-
|
||||
python: 2.7
|
||||
env: TOXENV=py27
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: 2.7
|
||||
env: TOXENV=py27-ansi2html
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: 3.6
|
||||
env: TOXENV=py36
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: 3.6
|
||||
env: TOXENV=py36-ansi2html
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: required
|
||||
env: TOXENV=py37
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: required
|
||||
env: TOXENV=py37-ansi2html
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: pypy
|
||||
env: TOXENV=pypy
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: pypy
|
||||
env: TOXENV=pypy-ansi2html
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: pypy3
|
||||
env: TOXENV=pypy3
|
||||
- stage:
|
||||
|
||||
-
|
||||
python: pypy3
|
||||
env: TOXENV=pypy3-ansi2html
|
||||
|
||||
- stage: deploy
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
|
@ -60,7 +78,9 @@ jobs:
|
|||
on:
|
||||
tags: true
|
||||
repo: pytest-dev/pytest-html
|
||||
cache:
|
||||
pip: true
|
||||
|
||||
cache: pip
|
||||
|
||||
install: pip install tox
|
||||
|
||||
script: tox
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
tox = "*"
|
||||
flake8 = "*"
|
||||
black = "*"
|
||||
|
||||
[packages]
|
||||
pytest-html = {editable = true,path = "."}
|
|
@ -5,6 +5,6 @@ try:
|
|||
__version__ = get_distribution(__name__).version
|
||||
except DistributionNotFound:
|
||||
# package is not installed
|
||||
__version__ = 'unknown'
|
||||
__version__ = "unknown"
|
||||
|
||||
__pypi_url__ = 'https://pypi.python.org/pypi/pytest-html'
|
||||
__pypi_url__ = "https://pypi.python.org/pypi/pytest-html"
|
||||
|
|
|
@ -2,45 +2,50 @@
|
|||
# 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/.
|
||||
|
||||
FORMAT_HTML = 'html'
|
||||
FORMAT_IMAGE = 'image'
|
||||
FORMAT_JSON = 'json'
|
||||
FORMAT_TEXT = 'text'
|
||||
FORMAT_URL = 'url'
|
||||
FORMAT_HTML = "html"
|
||||
FORMAT_IMAGE = "image"
|
||||
FORMAT_JSON = "json"
|
||||
FORMAT_TEXT = "text"
|
||||
FORMAT_URL = "url"
|
||||
|
||||
|
||||
def extra(content, format, name=None, mime_type=None, extension=None):
|
||||
return {'name': name, 'format': format, 'content': content,
|
||||
'mime_type': mime_type, 'extension': extension}
|
||||
return {
|
||||
"name": name,
|
||||
"format": format,
|
||||
"content": content,
|
||||
"mime_type": mime_type,
|
||||
"extension": extension,
|
||||
}
|
||||
|
||||
|
||||
def html(content):
|
||||
return extra(content, FORMAT_HTML)
|
||||
|
||||
|
||||
def image(content, name='Image', mime_type='image/png', extension='png'):
|
||||
def image(content, name="Image", mime_type="image/png", extension="png"):
|
||||
return extra(content, FORMAT_IMAGE, name, mime_type, extension)
|
||||
|
||||
|
||||
def png(content, name='Image'):
|
||||
return image(content, name, mime_type='image/png', extension='png')
|
||||
def png(content, name="Image"):
|
||||
return image(content, name, mime_type="image/png", extension="png")
|
||||
|
||||
|
||||
def jpg(content, name='Image'):
|
||||
return image(content, name, mime_type='image/jpeg', extension='jpg')
|
||||
def jpg(content, name="Image"):
|
||||
return image(content, name, mime_type="image/jpeg", extension="jpg")
|
||||
|
||||
|
||||
def svg(content, name='Image'):
|
||||
return image(content, name, mime_type='image/svg+xml', extension='svg')
|
||||
def svg(content, name="Image"):
|
||||
return image(content, name, mime_type="image/svg+xml", extension="svg")
|
||||
|
||||
|
||||
def json(content, name='JSON'):
|
||||
return extra(content, FORMAT_JSON, name, 'application/json', 'json')
|
||||
def json(content, name="JSON"):
|
||||
return extra(content, FORMAT_JSON, name, "application/json", "json")
|
||||
|
||||
|
||||
def text(content, name='Text'):
|
||||
return extra(content, FORMAT_TEXT, name, 'text/plain', 'txt')
|
||||
def text(content, name="Text"):
|
||||
return extra(content, FORMAT_TEXT, name, "text/plain", "txt")
|
||||
|
||||
|
||||
def url(content, name='URL'):
|
||||
def url(content, name="URL"):
|
||||
return extra(content, FORMAT_URL, name)
|
||||
|
|
|
@ -19,6 +19,7 @@ import warnings
|
|||
|
||||
try:
|
||||
from ansi2html import Ansi2HTMLConverter, style
|
||||
|
||||
ANSI = True
|
||||
except ImportError:
|
||||
# ansi2html is not installed
|
||||
|
@ -42,49 +43,62 @@ else:
|
|||
|
||||
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)')
|
||||
group.addoption('--css', action='append', metavar='path', default=[],
|
||||
help='append given css file content to report style file.')
|
||||
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)",
|
||||
)
|
||||
group.addoption(
|
||||
"--css",
|
||||
action="append",
|
||||
metavar="path",
|
||||
default=[],
|
||||
help="append given css file content to report style file.",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
htmlpath = config.getoption('htmlpath')
|
||||
htmlpath = config.getoption("htmlpath")
|
||||
if htmlpath:
|
||||
for csspath in config.getoption('css'):
|
||||
for csspath in config.getoption("css"):
|
||||
open(csspath)
|
||||
if not hasattr(config, 'slaveinput'):
|
||||
if not hasattr(config, "slaveinput"):
|
||||
# prevent opening htmlpath on slave nodes (xdist)
|
||||
config._html = HTMLReport(htmlpath, config)
|
||||
config.pluginmanager.register(config._html)
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
html = getattr(config, '_html', None)
|
||||
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)
|
||||
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)
|
||||
|
@ -93,171 +107,180 @@ class HTMLReport(object):
|
|||
self.errors = self.failed = 0
|
||||
self.passed = self.skipped = 0
|
||||
self.xfailed = self.xpassed = 0
|
||||
has_rerun = config.pluginmanager.hasplugin('rerunfailures')
|
||||
has_rerun = config.pluginmanager.hasplugin("rerunfailures")
|
||||
self.rerun = 0 if has_rerun else None
|
||||
self.self_contained = config.getoption('self_contained_html')
|
||||
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 getattr(report, 'when', 'call') != 'call':
|
||||
self.test_id = '::'.join([report.nodeid, report.when])
|
||||
self.time = getattr(report, 'duration', 0.0)
|
||||
if getattr(report, "when", "call") != "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.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
|
||||
test_index = hasattr(report, "rerun") and report.rerun + 1 or 0
|
||||
|
||||
for extra_index, extra in enumerate(getattr(report, 'extra', [])):
|
||||
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')]
|
||||
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_row(report=report, cells=cells)
|
||||
|
||||
self.config.hook.pytest_html_results_table_html(
|
||||
report=report, data=self.additional_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)))
|
||||
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')
|
||||
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'):
|
||||
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)])
|
||||
hash_key = "".join([self.test_id, str(extra_index), str(test_index)])
|
||||
hash_generator = hashlib.md5()
|
||||
hash_generator.update(hash_key.encode('utf-8'))
|
||||
hash_generator.update(hash_key.encode("utf-8"))
|
||||
hex_digest = hash_generator.hexdigest()
|
||||
# 255 is the common max filename length on various filesystems,
|
||||
# we subtract hash length, file extension length and 2 more
|
||||
# characters for the underscore and dot
|
||||
max_length = 255 - len(hex_digest) - len(file_extension) - 2
|
||||
asset_file_name = '{0}_{1}.{2}'.format(hash_key[:max_length],
|
||||
hex_digest,
|
||||
file_extension)
|
||||
asset_path = os.path.join(os.path.dirname(self.logfile),
|
||||
'assets', asset_file_name)
|
||||
asset_file_name = "{0}_{1}.{2}".format(
|
||||
hash_key[:max_length], hex_digest, 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)
|
||||
relative_path = "{0}/{1}".format("assets", asset_file_name)
|
||||
|
||||
kwargs = {'encoding': 'utf-8'} if 'b' not in mode else {}
|
||||
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 extra.get("format") == extras.FORMAT_IMAGE:
|
||||
content = extra.get("content")
|
||||
try:
|
||||
is_uri_or_path = (content.startswith(('file', 'http')) or
|
||||
isfile(content))
|
||||
is_uri_or_path = content.startswith(("file", "http")) or isfile(
|
||||
content
|
||||
)
|
||||
except ValueError:
|
||||
# On Windows, os.path.isfile throws this exception when
|
||||
# passed a b64 encoded image.
|
||||
is_uri_or_path = False
|
||||
if is_uri_or_path:
|
||||
if self.self_contained:
|
||||
warnings.warn('Self-contained HTML report '
|
||||
'includes link to external '
|
||||
'resource: {}'.format(content))
|
||||
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)
|
||||
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'))
|
||||
content = b64decode(content.encode("utf-8"))
|
||||
else:
|
||||
content = b64decode(content)
|
||||
href = src = self.create_asset(
|
||||
content, extra_index, test_index,
|
||||
extra.get('extension'), 'wb')
|
||||
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'))
|
||||
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_HTML:
|
||||
self.additional_html.append(html.div(raw(extra.get("content"))))
|
||||
|
||||
elif extra.get('format') == extras.FORMAT_JSON:
|
||||
content = json.dumps(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'))
|
||||
href = data_uri(content, mime_type=extra.get("mime_type"))
|
||||
else:
|
||||
href = self.create_asset(content, extra_index,
|
||||
test_index,
|
||||
extra.get('extension'))
|
||||
href = self.create_asset(
|
||||
content, extra_index, test_index, extra.get("extension")
|
||||
)
|
||||
|
||||
elif extra.get('format') == extras.FORMAT_TEXT:
|
||||
content = extra.get('content')
|
||||
elif extra.get("format") == extras.FORMAT_TEXT:
|
||||
content = extra.get("content")
|
||||
if isinstance(content, bytes):
|
||||
content = content.decode('utf-8')
|
||||
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'))
|
||||
href = self.create_asset(
|
||||
content, extra_index, test_index, extra.get("extension")
|
||||
)
|
||||
|
||||
elif extra.get('format') == extras.FORMAT_URL:
|
||||
href = extra.get('content')
|
||||
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(' ')
|
||||
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')
|
||||
log = html.div(class_="log")
|
||||
if report.longrepr:
|
||||
for line in report.longreprtext.splitlines():
|
||||
separator = line.startswith('_ ' * 10)
|
||||
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'))
|
||||
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(" {0} ".format(header).center(80, "-"))
|
||||
log.append(html.br())
|
||||
if ANSI:
|
||||
converter = Ansi2HTMLConverter(inline=False, escaped=False)
|
||||
|
@ -266,8 +289,8 @@ class HTMLReport(object):
|
|||
log.append(html.br())
|
||||
|
||||
if len(log) == 0:
|
||||
log = html.div(class_='empty log')
|
||||
log.append('No log output captured.')
|
||||
log = html.div(class_="empty log")
|
||||
log.append("No log output captured.")
|
||||
additional_html.append(log)
|
||||
|
||||
def _appendrow(self, outcome, report):
|
||||
|
@ -277,45 +300,46 @@ class HTMLReport(object):
|
|||
self.results.insert(index, result)
|
||||
tbody = html.tbody(
|
||||
result.row_table,
|
||||
class_='{0} results-table-row'.format(result.outcome.lower()))
|
||||
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 report.when == "call":
|
||||
if hasattr(report, "wasxfail"):
|
||||
self.xpassed += 1
|
||||
self._appendrow('XPassed', report)
|
||||
self._appendrow("XPassed", report)
|
||||
else:
|
||||
self.passed += 1
|
||||
self._appendrow('Passed', report)
|
||||
self._appendrow("Passed", report)
|
||||
|
||||
def append_failed(self, report):
|
||||
if getattr(report, 'when', None) == "call":
|
||||
if getattr(report, "when", None) == "call":
|
||||
if hasattr(report, "wasxfail"):
|
||||
# pytest < 3.0 marked xpasses as failures
|
||||
self.xpassed += 1
|
||||
self._appendrow('XPassed', report)
|
||||
self._appendrow("XPassed", report)
|
||||
else:
|
||||
self.failed += 1
|
||||
self._appendrow('Failed', report)
|
||||
self._appendrow("Failed", report)
|
||||
else:
|
||||
self.errors += 1
|
||||
self._appendrow('Error', report)
|
||||
self._appendrow("Error", report)
|
||||
|
||||
def append_skipped(self, report):
|
||||
if hasattr(report, "wasxfail"):
|
||||
self.xfailed += 1
|
||||
self._appendrow('XFailed', report)
|
||||
self._appendrow("XFailed", report)
|
||||
else:
|
||||
self.skipped += 1
|
||||
self._appendrow('Skipped', report)
|
||||
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)
|
||||
self._appendrow("Rerun", report)
|
||||
|
||||
def _generate_report(self, session):
|
||||
suite_stop_time = time.time()
|
||||
|
@ -324,42 +348,42 @@ class HTMLReport(object):
|
|||
generated = datetime.datetime.now()
|
||||
|
||||
self.style_css = pkg_resources.resource_string(
|
||||
__name__, os.path.join('resources', 'style.css'))
|
||||
__name__, os.path.join("resources", "style.css")
|
||||
)
|
||||
if PY3:
|
||||
self.style_css = self.style_css.decode('utf-8')
|
||||
self.style_css = self.style_css.decode("utf-8")
|
||||
|
||||
if ANSI:
|
||||
ansi_css = [
|
||||
'\n/******************************',
|
||||
' * ANSI2HTML STYLES',
|
||||
' ******************************/\n']
|
||||
"\n/******************************",
|
||||
" * ANSI2HTML STYLES",
|
||||
" ******************************/\n",
|
||||
]
|
||||
ansi_css.extend([str(r) for r in style.get_styles()])
|
||||
self.style_css += '\n'.join(ansi_css)
|
||||
self.style_css += "\n".join(ansi_css)
|
||||
|
||||
# <DF> Add user-provided CSS
|
||||
for path in self.config.getoption('css'):
|
||||
self.style_css += '\n/******************************'
|
||||
self.style_css += '\n * CUSTOM CSS'
|
||||
self.style_css += '\n * {}'.format(path)
|
||||
self.style_css += '\n ******************************/\n\n'
|
||||
with open(path, 'r') as f:
|
||||
for path in self.config.getoption("css"):
|
||||
self.style_css += "\n/******************************"
|
||||
self.style_css += "\n * CUSTOM CSS"
|
||||
self.style_css += "\n * {}".format(path)
|
||||
self.style_css += "\n ******************************/\n\n"
|
||||
with open(path, "r") as f:
|
||||
self.style_css += f.read()
|
||||
|
||||
css_href = '{0}/{1}'.format('assets', 'style.css')
|
||||
html_css = html.link(href=css_href, rel='stylesheet',
|
||||
type='text/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)
|
||||
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):
|
||||
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
|
||||
|
@ -370,108 +394,130 @@ class HTMLReport(object):
|
|||
self.generate_summary_item()
|
||||
|
||||
def generate_checkbox(self):
|
||||
checkbox_kwargs = {'data-test-result':
|
||||
self.test_result.lower()}
|
||||
checkbox_kwargs = {"data-test-result": self.test_result.lower()}
|
||||
if self.total == 0:
|
||||
checkbox_kwargs['disabled'] = 'true'
|
||||
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)
|
||||
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)
|
||||
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')]
|
||||
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))
|
||||
outcomes.append(Outcome("rerun", self.rerun))
|
||||
|
||||
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')]
|
||||
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(', ')
|
||||
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')]
|
||||
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')]
|
||||
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'))
|
||||
__name__, os.path.join("resources", "main.js")
|
||||
)
|
||||
if PY3:
|
||||
main_js = main_js.decode('utf-8')
|
||||
main_js = main_js.decode("utf-8")
|
||||
|
||||
body = html.body(
|
||||
html.script(raw(main_js)),
|
||||
html.h1(os.path.basename(self.logfile)),
|
||||
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()')
|
||||
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))
|
||||
|
||||
summary_prefix, summary_postfix = [], []
|
||||
session.config.hook.pytest_html_results_summary(
|
||||
prefix=summary_prefix, summary=summary, postfix=summary_postfix)
|
||||
body.extend([html.h2('Summary')] + summary_prefix
|
||||
+ summary + summary_postfix)
|
||||
prefix=summary_prefix, summary=summary, postfix=summary_postfix
|
||||
)
|
||||
body.extend([html.h2("Summary")] + summary_prefix + summary + summary_postfix)
|
||||
|
||||
body.extend(results)
|
||||
|
||||
doc = html.html(head, body)
|
||||
|
||||
unicode_doc = u'<!DOCTYPE html>\n{0}'.format(doc.unicode(indent=2))
|
||||
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')
|
||||
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:
|
||||
if not hasattr(config, "_metadata") or config._metadata is None:
|
||||
return []
|
||||
|
||||
metadata = config._metadata
|
||||
environment = [html.h2('Environment')]
|
||||
environment = [html.h2("Environment")]
|
||||
rows = []
|
||||
|
||||
keys = [k for k in metadata.keys()]
|
||||
|
@ -480,29 +526,29 @@ class HTMLReport(object):
|
|||
|
||||
for key in keys:
|
||||
value = metadata[key]
|
||||
if isinstance(value, basestring) and value.startswith('http'):
|
||||
value = html.a(value, href=value, target='_blank')
|
||||
if isinstance(value, basestring) and value.startswith("http"):
|
||||
value = html.a(value, href=value, target="_blank")
|
||||
elif isinstance(value, (list, tuple, set)):
|
||||
value = ', '.join((str(i) for i in value))
|
||||
value = ", ".join((str(i) for i in value))
|
||||
rows.append(html.tr(html.td(key), html.td(value)))
|
||||
|
||||
environment.append(html.table(rows, id='environment'))
|
||||
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')
|
||||
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:
|
||||
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:
|
||||
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):
|
||||
|
@ -528,5 +574,5 @@ class HTMLReport(object):
|
|||
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
terminalreporter.write_sep(
|
||||
'-',
|
||||
'generated html file: file://{0}'.format(self.logfile))
|
||||
"-", "generated html file: file://{0}".format(self.logfile)
|
||||
)
|
||||
|
|
64
setup.py
64
setup.py
|
@ -1,34 +1,34 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(name='pytest-html',
|
||||
use_scm_version=True,
|
||||
description='pytest plugin for generating HTML reports',
|
||||
long_description=open('README.rst').read(),
|
||||
author='Dave Hunt',
|
||||
author_email='dhunt@mozilla.com',
|
||||
url='https://github.com/pytest-dev/pytest-html',
|
||||
packages=['pytest_html'],
|
||||
package_data={'pytest_html': ['resources/*']},
|
||||
entry_points={'pytest11': ['html = pytest_html.plugin']},
|
||||
setup_requires=['setuptools_scm'],
|
||||
install_requires=[
|
||||
'pytest>=3.0',
|
||||
'pytest-metadata'],
|
||||
license='Mozilla Public License 2.0 (MPL 2.0)',
|
||||
keywords='py.test pytest html report',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Framework :: Pytest',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||
'Operating System :: POSIX',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Operating System :: MacOS :: MacOS X',
|
||||
'Topic :: Software Development :: Quality Assurance',
|
||||
'Topic :: Software Development :: Testing',
|
||||
'Topic :: Utilities',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
])
|
||||
setup(
|
||||
name="pytest-html",
|
||||
use_scm_version=True,
|
||||
description="pytest plugin for generating HTML reports",
|
||||
long_description=open("README.rst").read(),
|
||||
author="Dave Hunt",
|
||||
author_email="dhunt@mozilla.com",
|
||||
url="https://github.com/pytest-dev/pytest-html",
|
||||
packages=["pytest_html"],
|
||||
package_data={"pytest_html": ["resources/*"]},
|
||||
entry_points={"pytest11": ["html = pytest_html.plugin"]},
|
||||
setup_requires=["setuptools_scm"],
|
||||
install_requires=["pytest>=3.0", "pytest-metadata"],
|
||||
license="Mozilla Public License 2.0 (MPL 2.0)",
|
||||
keywords="py.test pytest html report",
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Framework :: Pytest",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Topic :: Software Development :: Quality Assurance",
|
||||
"Topic :: Software Development :: Testing",
|
||||
"Topic :: Utilities",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
],
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
15
tox.ini
15
tox.ini
|
@ -4,7 +4,7 @@
|
|||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
envlist = py{27,36,37,py,py3}{,-ansi2html}, flake8
|
||||
envlist = py{27,36,37,py,py3}{,-ansi2html}, flake8, black
|
||||
|
||||
[testenv]
|
||||
commands = pytest -v -r a {posargs}
|
||||
|
@ -19,3 +19,16 @@ skip_install = true
|
|||
basepython = python
|
||||
deps = flake8
|
||||
commands = flake8 {posargs:.}
|
||||
|
||||
[testenv:black]
|
||||
skip_install = true
|
||||
basepython = python
|
||||
deps = black
|
||||
commands = black --check {posargs:.}
|
||||
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
exclude = .eggs,.tox
|
||||
|
||||
[pytest]
|
||||
testpaths = testing
|
||||
|
|
Loading…
Reference in New Issue