pytest-html/testing/test_pytest_html.py

839 lines
28 KiB
Python

# 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 base64 import b64encode
from distutils.version import LooseVersion
import json
import os
import sys
import pkg_resources
import random
import re
import hashlib
import pytest
PY3 = sys.version_info[0] == 3
pytest_plugins = ("pytester",)
def run(testdir, path="report.html", *args):
path = testdir.tmpdir.join(path)
result = testdir.runpytest("--html", path, *args)
return result, read_html(path)
def read_html(path):
with open(str(path)) as f:
return f.read()
def assert_results_by_outcome(html, test_outcome, test_outcome_number, label=None):
# Asserts if the test number of this outcome in the summary is correct
regex_summary = r"(\d)+ {0}".format(label or test_outcome)
assert int(re.search(regex_summary, html).group(1)) == test_outcome_number
# Asserts if the generated checkbox of this outcome is correct
regex_checkbox = (
'<input checked="true" class="filter" '
'data-test-result="{0}"'.format(test_outcome)
)
if test_outcome_number == 0:
regex_checkbox += ' disabled="true"'
assert re.search(regex_checkbox, html) is not None
# Asserts if the table rows of this outcome are correct
regex_table = 'tbody class="{0} '.format(test_outcome)
assert len(re.findall(regex_table, html)) == test_outcome_number
def assert_results(
html,
tests=1,
duration=None,
passed=1,
skipped=0,
failed=0,
errors=0,
xfailed=0,
xpassed=0,
rerun=0,
):
# Asserts total amount of tests
total_tests = re.search(r"(\d)+ tests ran", html)
assert int(total_tests.group(1)) == tests
# Asserts tests running duration
if duration is not None:
tests_duration = re.search(r"([\d,.]+) seconds", html)
assert float(tests_duration.group(1)) >= float(duration)
# Asserts by outcome
assert_results_by_outcome(html, "passed", passed)
assert_results_by_outcome(html, "skipped", skipped)
assert_results_by_outcome(html, "failed", failed)
assert_results_by_outcome(html, "error", errors, "errors")
assert_results_by_outcome(html, "xfailed", xfailed, "expected failures")
assert_results_by_outcome(html, "xpassed", xpassed, "unexpected passes")
assert_results_by_outcome(html, "rerun", rerun)
class TestHTML:
def test_durations(self, testdir):
sleep = float(0.2)
testdir.makepyfile(
"""
import time
def test_sleep():
time.sleep({0:f})
""".format(
sleep * 2
)
)
result, html = run(testdir)
assert result.ret == 0
assert_results(html, duration=sleep)
p = re.compile(r'<td class="col-duration">([\d,.]+)</td>')
m = p.search(html)
assert float(m.group(1)) >= sleep
def test_pass(self, testdir):
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
assert result.ret == 0
assert_results(html)
def test_skip(self, testdir):
reason = str(random.random())
testdir.makepyfile(
"""
import pytest
def test_skip():
pytest.skip('{0}')
""".format(
reason
)
)
result, html = run(testdir)
assert result.ret == 0
assert_results(html, tests=0, passed=0, skipped=1)
assert "Skipped: {0}".format(reason) in html
def test_fail(self, testdir):
testdir.makepyfile("def test_fail(): assert False")
result, html = run(testdir)
assert result.ret
assert_results(html, passed=0, failed=1)
assert "AssertionError" in html
def test_rerun(self, testdir):
testdir.makepyfile(
"""
import pytest
@pytest.mark.flaky(reruns=5)
def test_example():
assert False
"""
)
result, html = run(testdir)
assert result.ret
assert_results(html, passed=0, failed=1, rerun=5)
def test_no_rerun(self, testdir):
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir, "report.html", "-p", "no:rerunfailures")
assert result.ret == 0
assert re.search('data-test-result="rerun"', html) is None
def test_conditional_xfails(self, testdir):
testdir.makepyfile(
"""
import pytest
@pytest.mark.xfail(False, reason='reason')
def test_fail(): assert False
@pytest.mark.xfail(False, reason='reason')
def test_pass(): pass
@pytest.mark.xfail(True, reason='reason')
def test_xfail(): assert False
@pytest.mark.xfail(True, reason='reason')
def test_xpass(): pass
"""
)
result, html = run(testdir)
assert result.ret
assert_results(html, tests=4, passed=1, failed=1, xfailed=1, xpassed=1)
def test_setup_error(self, testdir):
testdir.makepyfile(
"""
import pytest
@pytest.fixture
def arg(request):
raise ValueError()
def test_function(arg):
pass
"""
)
result, html = run(testdir)
assert result.ret
assert_results(html, tests=0, passed=0, errors=1)
assert "::setup" in html
assert "ValueError" in html
def test_xfail(self, testdir):
reason = str(random.random())
testdir.makepyfile(
"""
import pytest
def test_xfail():
pytest.xfail('{0}')
""".format(
reason
)
)
result, html = run(testdir)
assert result.ret == 0
assert_results(html, passed=0, xfailed=1)
assert "XFailed: {0}".format(reason) in html
def test_xpass(self, testdir):
testdir.makepyfile(
"""
import pytest
@pytest.mark.xfail()
def test_xpass():
pass
"""
)
result, html = run(testdir)
assert result.ret == 0
assert_results(html, passed=0, xpassed=1)
def test_create_report_path(self, testdir):
testdir.makepyfile("def test_pass(): pass")
path = os.path.join("directory", "report.html")
result, html = run(testdir, path)
assert result.ret == 0
assert_results(html)
@pytest.mark.parametrize("path", ["", "directory"])
def test_report_title(self, testdir, path):
testdir.makepyfile("def test_pass(): pass")
report_name = "report.html"
path = os.path.join(path, report_name)
result, html = run(testdir, path)
assert result.ret == 0
report_title = "<h1>{0}</h1>".format(report_name)
assert report_title in html
def test_report_title_addopts_env_var(self, testdir, monkeypatch):
report_location = "REPORT_LOCATION"
report_name = "MuhReport"
monkeypatch.setenv(report_location, report_name)
testdir.makefile(
".ini",
pytest="""
[pytest]
addopts = --html ${0}
""".format(
report_location
),
)
testdir.makepyfile("def test_pass(): pass")
result = testdir.runpytest()
assert result.ret == 0
report_title = "<h1>{0}</h1>".format(report_name)
assert report_title in read_html(report_name)
def test_resources_inline_css(self, testdir):
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir, "report.html", "--self-contained-html")
assert result.ret == 0
content = pkg_resources.resource_string(
"pytest_html", os.path.join("resources", "style.css")
)
if PY3:
content = content.decode("utf-8")
assert content
assert content in html
def test_resources(self, testdir):
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
assert result.ret == 0
content = pkg_resources.resource_string(
"pytest_html", os.path.join("resources", "main.js")
)
if PY3:
content = content.decode("utf-8")
assert content
assert content in html
regex_css_link = '<link href="assets/style.css" rel="stylesheet"'
assert re.search(regex_css_link, html) is not None
@pytest.mark.parametrize("result", ["pass", "fail"])
def test_stdout(self, testdir, result):
content = "<spam>ham</spam>"
escaped = "&lt;spam&gt;ham&lt;/spam&gt;"
testdir.makepyfile(
"""
def test_stdout():
print('{0}')
assert '{1}' == 'pass'""".format(
content, result
)
)
_, html = run(testdir)
assert content not in html
assert escaped in html
def test_custom_content_in_summary(self, testdir):
content_prefix = str(random.random())
content_summary = str(random.random())
content_suffix = str(random.random())
testdir.makeconftest(
"""
import pytest
from py.xml import html
@pytest.mark.optionalhook
def pytest_html_results_summary(prefix, summary, postfix):
prefix.append(html.p("prefix is {0}"))
summary.extend([html.p("extra summary is {1}")])
postfix.extend([html.p("postfix is {2}")])
""".format(
content_prefix, content_summary, content_suffix
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
assert result.ret == 0
assert len(re.findall(content_prefix, html)) == 1
assert len(re.findall(content_summary, html)) == 1
assert len(re.findall(content_suffix, html)) == 1
def test_extra_html(self, testdir):
content = str(random.random())
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.html('<div>{0}</div>')]
""".format(
content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
assert result.ret == 0
assert content in html
@pytest.mark.parametrize(
"content, encoded",
[("u'\u0081'", "woE="), ("'foo'", "Zm9v"), ("b'\\xe2\\x80\\x93'", "4oCT")],
)
def test_extra_text(self, testdir, content, encoded):
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.text({0})]
""".format(
content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir, "report.html", "--self-contained-html")
assert result.ret == 0
href = "data:text/plain;charset=utf-8;base64,{0}".format(encoded)
link = '<a class="text" href="{0}" target="_blank">Text</a>'.format(href)
assert link in html
def test_extra_json(self, testdir):
content = {str(random.random()): str(random.random())}
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.json({0})]
""".format(
content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir, "report.html", "--self-contained-html")
assert result.ret == 0
content_str = json.dumps(content)
if PY3:
data = b64encode(content_str.encode("utf-8")).decode("ascii")
else:
data = b64encode(content_str)
href = "data:application/json;charset=utf-8;base64,{0}".format(data)
link = '<a class="json" href="{0}" target="_blank">JSON</a>'.format(href)
assert link in html
def test_extra_url(self, testdir):
content = str(random.random())
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.url('{0}')]
""".format(
content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
assert result.ret == 0
link = '<a class="url" href="{0}" target="_blank">URL</a>'.format(content)
assert link in html
@pytest.mark.parametrize(
"mime_type, extension",
[
("image/png", "png"),
("image/png", "image"),
("image/jpeg", "jpg"),
("image/svg+xml", "svg"),
],
)
def test_extra_image(self, testdir, mime_type, extension):
content = str(random.random())
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.{0}('{1}')]
""".format(
extension, content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir, "report.html", "--self-contained-html")
assert result.ret == 0
src = "data:{0};base64,{1}".format(mime_type, content)
assert '<img src="{0}"/>'.format(src) in html
def test_extra_image_windows(self, mocker, testdir):
mock_isfile = mocker.patch("pytest_html.plugin.isfile")
mock_isfile.side_effect = ValueError("stat: path too long for Windows")
self.test_extra_image(testdir, "image/png", "png")
assert mock_isfile.call_count == 1
@pytest.mark.parametrize(
"content", [("u'\u0081'"), ("'foo'"), ("b'\\xe2\\x80\\x93'")]
)
def test_extra_text_separated(self, testdir, content):
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.text({0})]
""".format(
content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
hash_key = "test_extra_text_separated.py::" "test_pass00"
hash_generator = hashlib.md5()
hash_generator.update(hash_key.encode("utf-8"))
assert result.ret == 0
src = "{0}/{1}".format(
"assets", "{0}_{1}.txt".format(hash_key, hash_generator.hexdigest())
)
link = '<a class="text" href="{0}" target="_blank">'.format(src)
assert link in html
assert os.path.exists(src)
@pytest.mark.parametrize(
"file_extension, extra_type",
[("png", "image"), ("png", "png"), ("svg", "svg"), ("jpg", "jpg")],
)
def test_extra_image_separated(self, testdir, file_extension, extra_type):
content = b64encode("foo".encode("utf-8")).decode("ascii")
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.{0}('{1}')]
""".format(
extra_type, content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
hash_key = "test_extra_image_separated.py::test_pass00"
hash_generator = hashlib.md5()
hash_generator.update(hash_key.encode("utf-8"))
assert result.ret == 0
src = "{0}/{1}".format(
"assets",
"{0}_{1}.{2}".format(hash_key, hash_generator.hexdigest(), file_extension),
)
link = '<a class="image" href="{0}" target="_blank">'.format(src)
assert link in html
assert os.path.exists(src)
@pytest.mark.parametrize(
"file_extension, extra_type",
[("png", "image"), ("png", "png"), ("svg", "svg"), ("jpg", "jpg")],
)
def test_extra_image_separated_rerun(self, testdir, file_extension, extra_type):
content = b64encode("foo".encode("utf-8")).decode("ascii")
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.{0}('{1}')]
""".format(
extra_type, content
)
)
testdir.makepyfile(
"""
import pytest
@pytest.mark.flaky(reruns=2)
def test_fail():
assert False"""
)
result, html = run(testdir)
for i in range(1, 4):
hash_key = "test_extra_image_separated_rerun.py::" "test_fail0{0}".format(i)
hash_generator = hashlib.md5()
hash_generator.update(hash_key.encode("utf-8"))
src = "assets/{0}_{1}.{2}".format(
hash_key, hash_generator.hexdigest(), file_extension
)
link = '<a class="image" href="{0}" target="_blank">'.format(src)
assert result.ret
assert link in html
assert os.path.exists(src)
@pytest.mark.parametrize("src_type", ["https://", "file://", "image.png"])
def test_extra_image_non_b64(self, testdir, src_type):
content = src_type
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.image('{0}')]
""".format(
content
)
)
testdir.makepyfile("def test_pass(): pass")
if src_type == "image.png":
testdir.makefile(".png", image="pretty picture")
result, html = run(testdir, "report.html")
assert result.ret == 0
assert '<a href="{0}"><img src="{0}"/>'.format(content) in html
def test_very_long_test_name(self, testdir):
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
from pytest_html import extras
report.extra = [extras.image('image.png')]
"""
)
# This will get truncated
test_name = "test_{}".format("a" * 300)
testdir.makepyfile(
"""
def {0}():
assert False
""".format(
test_name
)
)
result, html = run(testdir)
hash_key = "test_very_long_test_name.py::{}00".format(test_name)
hash_generator = hashlib.md5()
hash_generator.update(hash_key.encode("utf-8"))
src = "assets/{0}_{1}.png".format(hash_key[:218], hash_generator.hexdigest())
link = '<a class="image" href="{0}" target="_blank">'.format(src)
assert result.ret
assert link in html
assert os.path.exists(src)
def test_no_environment(self, testdir):
testdir.makeconftest(
"""
def pytest_configure(config):
config._metadata = None
"""
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
assert result.ret == 0
assert "Environment" not in html
def test_environment(self, testdir):
content = str(random.random())
testdir.makeconftest(
"""
def pytest_configure(config):
config._metadata['content'] = '{0}'
""".format(
content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
assert result.ret == 0
assert "Environment" in html
assert len(re.findall(content, html)) == 1
def test_environment_xdist(self, testdir):
content = str(random.random())
testdir.makeconftest(
"""
def pytest_configure(config):
for i in range(2):
config._metadata['content'] = '{0}'
""".format(
content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir, "report.html", "-n", "1")
assert result.ret == 0
assert "Environment" in html
assert len(re.findall(content, html)) == 1
def test_environment_xdist_reruns(self, testdir):
content = str(random.random())
testdir.makeconftest(
"""
def pytest_configure(config):
for i in range(2):
config._metadata['content'] = '{0}'
""".format(
content
)
)
testdir.makepyfile("def test_fail(): assert False")
result, html = run(testdir, "report.html", "-n", "1", "--reruns", "1")
assert result.ret
assert "Environment" in html
assert len(re.findall(content, html)) == 1
def test_environment_list_value(self, testdir):
content = tuple(str(random.random()) for i in range(10))
content += tuple(random.random() for i in range(10))
expected_content = ", ".join((str(i) for i in content))
expected_html_re = r"<td>content</td>\n\s+<td>{}</td>".format(expected_content)
testdir.makeconftest(
"""
def pytest_configure(config):
for i in range(2):
config._metadata['content'] = {0}
""".format(
content
)
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
assert result.ret == 0
assert "Environment" in html
assert len(re.findall(expected_html_re, html)) == 1
def test_environment_ordered(self, testdir):
testdir.makeconftest(
"""
from collections import OrderedDict
def pytest_configure(config):
config._metadata = OrderedDict([('ZZZ', 1), ('AAA', 2)])
"""
)
testdir.makepyfile("def test_pass(): pass")
result, html = run(testdir)
assert result.ret == 0
assert "Environment" in html
assert len(re.findall("ZZZ.+AAA", html, re.DOTALL)) == 1
@pytest.mark.xfail(
sys.version_info < (3, 2)
and LooseVersion(pytest.__version__) >= LooseVersion("2.8.0"),
reason="Fails on earlier versions of Python and pytest",
run=False,
)
def test_xdist_crashing_slave(self, testdir):
"""https://github.com/pytest-dev/pytest-html/issues/21"""
testdir.makepyfile(
"""
import os
def test_exit():
os._exit(0)
"""
)
result, html = run(testdir, "report.html", "-n", "1")
assert "INTERNALERROR>" not in result.stdout.str()
def test_utf8_surrogate(self, testdir):
testdir.makepyfile(
r"""
import pytest
@pytest.mark.parametrize('val', ['\ud800'])
def test_foo(val):
pass
"""
)
result, html = run(testdir)
assert result.ret == 0
assert_results(html, passed=1)
def test_ansi_color(self, testdir):
try:
import ansi2html # NOQA
ANSI = True
except ImportError:
# ansi2html is not installed
ANSI = False
pass_content = [
'<span class="ansi31">RCOLOR',
'<span class="ansi32">GCOLOR',
'<span class="ansi33">YCOLOR',
]
testdir.makepyfile(
r"""
def test_ansi():
colors = ['\033[31mRCOLOR\033[0m', '\033[32mGCOLOR\033[0m',
'\033[33mYCOLOR\033[0m']
for color in colors:
print(color)
"""
)
result, html = run(testdir, "report.html", "--self-contained-html")
assert result.ret == 0
for content in pass_content:
if ANSI:
assert content in html
else:
assert content not in html
@pytest.mark.parametrize("content", [("'foo'"), ("u'\u0081'")])
def test_utf8_longrepr(self, testdir, content):
testdir.makeconftest(
"""
import pytest
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call':
report.longrepr = 'utf8 longrepr: ' + {0}
""".format(
content
)
)
testdir.makepyfile(
"""
def test_fail():
testtext = 'utf8 longrepr: '
assert False
"""
)
result, html = run(testdir, "report.html", "--self-contained-html")
assert result.ret
assert "utf8 longrepr" in html
def test_collect_error(self, testdir):
testdir.makepyfile(
"""
import xyz
def test_pass(): pass
"""
)
result, html = run(testdir)
assert result.ret
assert_results(html, tests=0, passed=0, errors=1)
regex_error = "(Import|ModuleNotFound)Error: No module named .*xyz"
assert re.search(regex_error, html) is not None
@pytest.mark.parametrize("colors", [(["red"]), (["green", "blue"])])
def test_css(self, testdir, colors):
testdir.makepyfile("def test_pass(): pass")
css = {}
cssargs = []
for color in colors:
style = "* {{color: {}}}".format(color)
path = testdir.makefile(".css", **{color: style})
css[color] = {"style": style, "path": path}
cssargs.extend(["--css", path])
result, html = run(testdir, "report.html", "--self-contained-html", *cssargs)
assert result.ret == 0
for k, v in css.items():
assert str(v["path"]) in html
assert v["style"] in html
def test_css_invalid(self, testdir):
testdir.makepyfile("def test_pass(): pass")
result = testdir.runpytest("--html", "report.html", "--css", "style.css")
assert result.ret
assert "No such file or directory: 'style.css'" in result.stderr.str()
def test_css_invalid_no_html(self, testdir):
testdir.makepyfile("def test_pass(): pass")
result = testdir.runpytest("--css", "style.css")
assert result.ret == 0