Merge pull request #1091 from RonnyPfannschmidt/fix-1074

fix issue 1074 and clean up junitxml
This commit is contained in:
Ronny Pfannschmidt 2015-12-07 22:13:16 +01:00
commit ffa572531a
3 changed files with 456 additions and 295 deletions

View File

@ -1,6 +1,10 @@
2.8.5.dev0 2.8.5.dev0
---------- ----------
- fix #1074: precompute junitxml chunks instead of storing the whole tree in objects
Thanks Bruno Oliveira for the report and Ronny Pfannschmidt for the PR
2.8.4 2.8.4
----- -----

View File

@ -1,9 +1,13 @@
""" report test results in JUnit-XML format, for use with Hudson and build integration servers. """
report test results in JUnit-XML format,
for use with Jenkins and build integration servers.
Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
Based on initial code from Ross Lawley. Based on initial code from Ross Lawley.
""" """
# Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/
# src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
import py import py
import os import os
import re import re
@ -19,10 +23,10 @@ else:
unicode = str unicode = str
long = int long = int
class Junit(py.xml.Namespace): class Junit(py.xml.Namespace):
pass pass
# We need to get the subset of the invalid unicode ranges according to # We need to get the subset of the invalid unicode ranges according to
# XML 1.0 which are valid in this python build. Hence we calculate # XML 1.0 which are valid in this python build. Hence we calculate
# this dynamically instead of hardcoding it. The spec range of valid # this dynamically instead of hardcoding it. The spec range of valid
@ -30,21 +34,19 @@ class Junit(py.xml.Namespace):
# | [#x10000-#x10FFFF] # | [#x10000-#x10FFFF]
_legal_chars = (0x09, 0x0A, 0x0d) _legal_chars = (0x09, 0x0A, 0x0d)
_legal_ranges = ( _legal_ranges = (
(0x20, 0x7E), (0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF),
(0x80, 0xD7FF),
(0xE000, 0xFFFD),
(0x10000, 0x10FFFF),
) )
_legal_xml_re = [unicode("%s-%s") % (unichr(low), unichr(high)) _legal_xml_re = [
for (low, high) in _legal_ranges unicode("%s-%s") % (unichr(low), unichr(high))
if low < sys.maxunicode] for (low, high) in _legal_ranges if low < sys.maxunicode
]
_legal_xml_re = [unichr(x) for x in _legal_chars] + _legal_xml_re _legal_xml_re = [unichr(x) for x in _legal_chars] + _legal_xml_re
illegal_xml_re = re.compile(unicode('[^%s]') % illegal_xml_re = re.compile(unicode('[^%s]') % unicode('').join(_legal_xml_re))
unicode('').join(_legal_xml_re))
del _legal_chars del _legal_chars
del _legal_ranges del _legal_ranges
del _legal_xml_re del _legal_xml_re
def bin_xml_escape(arg): def bin_xml_escape(arg):
def repl(matchobj): def repl(matchobj):
i = ord(matchobj.group()) i = ord(matchobj.group())
@ -52,83 +54,72 @@ def bin_xml_escape(arg):
return unicode('#x%02X') % i return unicode('#x%02X') % i
else: else:
return unicode('#x%04X') % i return unicode('#x%04X') % i
return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg)))
@pytest.fixture
def record_xml_property(request): class _NodeReporter(object):
"""Fixture that adds extra xml properties to the tag for the calling test. def __init__(self, nodeid, xml):
The fixture is callable with (name, value), with value being automatically
xml-encoded. self.id = nodeid
self.xml = xml
self.add_stats = self.xml.add_stats
self.duration = 0
self.properties = {}
self.property_insert_order = []
self.nodes = []
self.testcase = None
self.attrs = {}
def append(self, node):
self.xml.add_stats(type(node).__name__)
self.nodes.append(node)
def add_property(self, name, value):
name = str(name)
if name not in self.property_insert_order:
self.property_insert_order.append(name)
self.properties[name] = bin_xml_escape(value)
def make_properties_node(self):
"""Return a Junit node containing custom properties, if any.
""" """
def inner(name, value): if self.properties:
if hasattr(request.config, "_xml"): return Junit.properties([
request.config._xml.add_custom_property(name, value) Junit.property(name=name, value=self.properties[name])
msg = 'record_xml_property is an experimental feature' for name in self.property_insert_order
request.config.warn(code='C3', message=msg, ])
fslocation=request.node.location[:2]) return ''
return inner
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting")
group.addoption('--junitxml', '--junit-xml', action="store",
dest="xmlpath", metavar="path", default=None,
help="create junit-xml style report file at given path.")
group.addoption('--junitprefix', '--junit-prefix', action="store",
metavar="str", default=None,
help="prepend prefix to classnames in junit-xml output")
def pytest_configure(config): def record_testreport(self, testreport):
xmlpath = config.option.xmlpath assert not self.testcase
# prevent opening xmllog on slave nodes (xdist) names = mangle_testnames(testreport.nodeid.split("::"))
if xmlpath and not hasattr(config, 'slaveinput'):
config._xml = LogXML(xmlpath, config.option.junitprefix)
config.pluginmanager.register(config._xml)
def pytest_unconfigure(config):
xml = getattr(config, '_xml', None)
if xml:
del config._xml
config.pluginmanager.unregister(xml)
def mangle_testnames(names):
names = [x.replace(".py", "") for x in names if x != '()']
names[0] = names[0].replace("/", '.')
return names
class LogXML(object):
def __init__(self, logfile, prefix):
logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(os.path.abspath(logfile))
self.prefix = prefix
self.tests = []
self.tests_by_nodeid = {} # nodeid -> Junit.testcase
self.durations = {} # nodeid -> total duration (setup+call+teardown)
self.passed = self.skipped = 0
self.failed = self.errors = 0
self.custom_properties = {}
def add_custom_property(self, name, value):
self.custom_properties[str(name)] = bin_xml_escape(str(value))
def _opentestcase(self, report):
names = mangle_testnames(report.nodeid.split("::"))
classnames = names[:-1] classnames = names[:-1]
if self.prefix: if self.xml.prefix:
classnames.insert(0, self.prefix) classnames.insert(0, self.xml.prefix)
attrs = { attrs = {
"classname": ".".join(classnames), "classname": ".".join(classnames),
"name": bin_xml_escape(names[-1]), "name": bin_xml_escape(names[-1]),
"file": report.location[0], "file": testreport.location[0],
"time": self.durations.get(report.nodeid, 0),
} }
if report.location[1] is not None: if testreport.location[1] is not None:
attrs["line"] = report.location[1] attrs["line"] = testreport.location[1]
testcase = Junit.testcase(**attrs) self.attrs = attrs
custom_properties = self.pop_custom_properties()
if custom_properties: def to_xml(self):
testcase.append(custom_properties) testcase = Junit.testcase(time=self.duration, **self.attrs)
self.tests.append(testcase) testcase.append(self.make_properties_node())
self.tests_by_nodeid[report.nodeid] = testcase for node in self.nodes:
testcase.append(node)
return testcase
def _add_simple(self, kind, message, data=None):
data = bin_xml_escape(data)
node = kind(data, message=message)
self.append(node)
def _write_captured_output(self, report): def _write_captured_output(self, report):
for capname in ('out', 'err'): for capname in ('out', 'err'):
@ -140,34 +131,16 @@ class LogXML(object):
tag = getattr(Junit, 'system-' + capname) tag = getattr(Junit, 'system-' + capname)
self.append(tag(bin_xml_escape(allcontent))) self.append(tag(bin_xml_escape(allcontent)))
def append(self, obj):
self.tests[-1].append(obj)
def pop_custom_properties(self):
"""Return a Junit node containing custom properties set for
the current test, if any, and reset the current custom properties.
"""
if self.custom_properties:
result = Junit.properties(
[
Junit.property(name=name, value=value)
for name, value in self.custom_properties.items()
]
)
self.custom_properties.clear()
return result
return None
def append_pass(self, report): def append_pass(self, report):
self.passed += 1 self.add_stats('passed')
self._write_captured_output(report) self._write_captured_output(report)
def append_failure(self, report): def append_failure(self, report):
# msg = str(report.longrepr.reprtraceback.extraline) # msg = str(report.longrepr.reprtraceback.extraline)
if hasattr(report, "wasxfail"): if hasattr(report, "wasxfail"):
self.append( self._add_simple(
Junit.skipped(message="xfail-marked test passes unexpectedly")) Junit.skipped,
self.skipped += 1 "xfail-marked test passes unexpectedly")
else: else:
if hasattr(report.longrepr, "reprcrash"): if hasattr(report.longrepr, "reprcrash"):
message = report.longrepr.reprcrash.message message = report.longrepr.reprcrash.message
@ -179,30 +152,26 @@ class LogXML(object):
fail = Junit.failure(message=message) fail = Junit.failure(message=message)
fail.append(bin_xml_escape(report.longrepr)) fail.append(bin_xml_escape(report.longrepr))
self.append(fail) self.append(fail)
self.failed += 1
self._write_captured_output(report) self._write_captured_output(report)
def append_collect_error(self, report): def append_collect_error(self, report):
# msg = str(report.longrepr.reprtraceback.extraline) # msg = str(report.longrepr.reprtraceback.extraline)
self.append(Junit.error(bin_xml_escape(report.longrepr), self.append(Junit.error(bin_xml_escape(report.longrepr),
message="collection failure")) message="collection failure"))
self.errors += 1
def append_collect_skipped(self, report): def append_collect_skipped(self, report):
#msg = str(report.longrepr.reprtraceback.extraline) self._add_simple(
self.append(Junit.skipped(bin_xml_escape(report.longrepr), Junit.skipped, "collection skipped", report.longrepr)
message="collection skipped"))
self.skipped += 1
def append_error(self, report): def append_error(self, report):
self.append(Junit.error(bin_xml_escape(report.longrepr), self._add_simple(
message="test setup failure")) Junit.error, "test setup failure", report.longrepr)
self.errors += 1
def append_skipped(self, report): def append_skipped(self, report):
if hasattr(report, "wasxfail"): if hasattr(report, "wasxfail"):
self.append(Junit.skipped(bin_xml_escape(report.wasxfail), self._add_simple(
message="expected test failure")) Junit.skipped, "expected test failure", report.wasxfail
)
else: else:
filename, lineno, skipreason = report.longrepr filename, lineno, skipreason = report.longrepr
if skipreason.startswith("Skipped: "): if skipreason.startswith("Skipped: "):
@ -210,11 +179,113 @@ class LogXML(object):
self.append( self.append(
Junit.skipped("%s:%s: %s" % (filename, lineno, skipreason), Junit.skipped("%s:%s: %s" % (filename, lineno, skipreason),
type="pytest.skip", type="pytest.skip",
message=skipreason message=skipreason))
))
self.skipped += 1
self._write_captured_output(report) self._write_captured_output(report)
def finalize(self):
data = self.to_xml().unicode(indent=0)
self.__dict__.clear()
self.to_xml = lambda: py.xml.raw(data)
@pytest.fixture
def record_xml_property(request):
"""Fixture that adds extra xml properties to the tag for the calling test.
The fixture is callable with (name, value), with value being automatically
xml-encoded.
"""
request.node.warn(
code='C3',
message='record_xml_property is an experimental feature',
)
xml = getattr(request.config, "_xml", None)
if xml is not None:
node_reporter = xml.node_reporter(request.node.nodeid)
return node_reporter.add_property
else:
def add_property_noop(name, value):
pass
return add_property_noop
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting")
group.addoption(
'--junitxml', '--junit-xml',
action="store",
dest="xmlpath",
metavar="path",
default=None,
help="create junit-xml style report file at given path.")
group.addoption(
'--junitprefix', '--junit-prefix',
action="store",
metavar="str",
default=None,
help="prepend prefix to classnames in junit-xml output")
def pytest_configure(config):
xmlpath = config.option.xmlpath
# prevent opening xmllog on slave nodes (xdist)
if xmlpath and not hasattr(config, 'slaveinput'):
config._xml = LogXML(xmlpath, config.option.junitprefix)
config.pluginmanager.register(config._xml)
def pytest_unconfigure(config):
xml = getattr(config, '_xml', None)
if xml:
del config._xml
config.pluginmanager.unregister(xml)
def mangle_testnames(names):
names = [x.replace(".py", "") for x in names if x != '()']
names[0] = names[0].replace("/", '.')
return names
class LogXML(object):
def __init__(self, logfile, prefix):
logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(os.path.abspath(logfile))
self.prefix = prefix
self.stats = dict.fromkeys([
'error',
'passed',
'failure',
'skipped',
], 0)
self.node_reporters = {} # nodeid -> _NodeReporter
self.node_reporters_ordered = []
def node_reporter(self, report):
nodeid = getattr(report, 'nodeid', report)
# local hack to handle xdist report order
slavenode = getattr(report, 'node', None)
key = nodeid, slavenode
if key in self.node_reporters:
#TODO: breasks for --dist=each
return self.node_reporters[key]
reporter = _NodeReporter(nodeid, self)
self.node_reporters[key] = reporter
self.node_reporters_ordered.append(reporter)
return reporter
def add_stats(self, key):
if key in self.stats:
self.stats[key] += 1
def _opentestcase(self, report):
reporter = self.node_reporter(report)
reporter.record_testreport(report)
return reporter
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
"""handle a setup/call/teardown report, generating the appropriate """handle a setup/call/teardown report, generating the appropriate
xml tags as necessary. xml tags as necessary.
@ -240,47 +311,40 @@ class LogXML(object):
""" """
if report.passed: if report.passed:
if report.when == "call": # ignore setup/teardown if report.when == "call": # ignore setup/teardown
self._opentestcase(report) reporter = self._opentestcase(report)
self.append_pass(report) reporter.append_pass(report)
elif report.failed: elif report.failed:
self._opentestcase(report) reporter = self._opentestcase(report)
if report.when != "call": if report.when == "call":
self.append_error(report) reporter.append_failure(report)
else: else:
self.append_failure(report) reporter.append_error(report)
elif report.skipped: elif report.skipped:
self._opentestcase(report) reporter = self._opentestcase(report)
self.append_skipped(report) reporter.append_skipped(report)
self.update_testcase_duration(report) self.update_testcase_duration(report)
if report.when == "teardown":
self.node_reporter(report).finalize()
def update_testcase_duration(self, report): def update_testcase_duration(self, report):
"""accumulates total duration for nodeid from given report and updates """accumulates total duration for nodeid from given report and updates
the Junit.testcase with the new total if already created. the Junit.testcase with the new total if already created.
""" """
total = self.durations.get(report.nodeid, 0.0) reporter = self.node_reporter(report)
total += getattr(report, 'duration', 0.0) reporter.duration += getattr(report, 'duration', 0.0)
self.durations[report.nodeid] = total
testcase = self.tests_by_nodeid.get(report.nodeid)
if testcase is not None:
testcase.attr.time = total
def pytest_collectreport(self, report): def pytest_collectreport(self, report):
if not report.passed: if not report.passed:
self._opentestcase(report) reporter = self._opentestcase(report)
if report.failed: if report.failed:
self.append_collect_error(report) reporter.append_collect_error(report)
else: else:
self.append_collect_skipped(report) reporter.append_collect_skipped(report)
def pytest_internalerror(self, excrepr): def pytest_internalerror(self, excrepr):
self.errors += 1 reporter = self.node_reporter('internal')
data = bin_xml_escape(excrepr) reporter.attrs.update(classname="pytest", name='internal')
self.tests.append( reporter._add_simple(Junit.error, 'internal error', excrepr)
Junit.testcase(
Junit.error(data, message="internal error"),
classname="pytest",
name="internal"))
def pytest_sessionstart(self): def pytest_sessionstart(self):
self.suite_start_time = time.time() self.suite_start_time = time.time()
@ -292,19 +356,20 @@ class LogXML(object):
logfile = open(self.logfile, 'w', encoding='utf-8') logfile = open(self.logfile, 'w', encoding='utf-8')
suite_stop_time = time.time() suite_stop_time = time.time()
suite_time_delta = suite_stop_time - self.suite_start_time suite_time_delta = suite_stop_time - self.suite_start_time
numtests = self.passed + self.failed
numtests = self.stats['passed'] + self.stats['failure']
logfile.write('<?xml version="1.0" encoding="utf-8"?>') logfile.write('<?xml version="1.0" encoding="utf-8"?>')
logfile.write(Junit.testsuite( logfile.write(Junit.testsuite(
self.tests, [x.to_xml() for x in self.node_reporters_ordered],
name="pytest", name="pytest",
errors=self.errors, errors=self.stats['error'],
failures=self.failed, failures=self.stats['failure'],
skips=self.skipped, skips=self.stats['skipped'],
tests=numtests, tests=numtests,
time="%.3f" % suite_time_delta, time="%.3f" % suite_time_delta, ).unicode(indent=0))
).unicode(indent=0))
logfile.close() logfile.close()
def pytest_terminal_summary(self, terminalreporter): def pytest_terminal_summary(self, terminalreporter):
terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile)) terminalreporter.write_sep("-",
"generated xml file: %s" % (self.logfile))

View File

@ -2,7 +2,9 @@
from xml.dom import minidom from xml.dom import minidom
from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_NOTESTSCOLLECTED
import py, sys, os import py
import sys
import os
from _pytest.junitxml import LogXML from _pytest.junitxml import LogXML
import pytest import pytest
@ -11,16 +13,71 @@ def runandparse(testdir, *args):
resultpath = testdir.tmpdir.join("junit.xml") resultpath = testdir.tmpdir.join("junit.xml")
result = testdir.runpytest("--junitxml=%s" % resultpath, *args) result = testdir.runpytest("--junitxml=%s" % resultpath, *args)
xmldoc = minidom.parse(str(resultpath)) xmldoc = minidom.parse(str(resultpath))
return result, xmldoc return result, DomNode(xmldoc)
def assert_attr(node, **kwargs): def assert_attr(node, **kwargs):
__tracebackhide__ = True __tracebackhide__ = True
for name, expected in kwargs.items(): def nodeval(node, name):
anode = node.getAttributeNode(name) anode = node.getAttributeNode(name)
assert anode, "node %r has no attribute %r" %(node, name) if anode is not None:
val = anode.value return anode.value
if val != str(expected):
py.test.fail("%r != %r" %(str(val), str(expected))) expected = dict((name, str(value)) for name, value in kwargs.items())
on_node = dict((name, nodeval(node, name)) for name in expected)
assert on_node == expected
class DomNode(object):
def __init__(self, dom):
self.__node = dom
def __repr__(self):
return self.__node.toxml()
def find_first_by_tag(self, tag):
return self.find_nth_by_tag(tag, 0)
def _by_tag(self, tag):
return self.__node.getElementsByTagName(tag)
def find_nth_by_tag(self, tag, n):
items = self._by_tag(tag)
try:
nth = items[n]
except IndexError:
pass
else:
return type(self)(nth)
def find_by_tag(self, tag):
t = type(self)
return [t(x) for x in self.__node.getElementsByTagName(tag)]
def __getitem__(self, key):
node = self.__node.getAttributeNode(key)
if node is not None:
return node.value
def assert_attr(self, **kwargs):
__tracebackhide__ = True
return assert_attr(self.__node, **kwargs)
def toxml(self):
return self.__node.toxml()
@property
def text(self):
return self.__node.childNodes[0].wholeText
@property
def tag(self):
return self.__node.tagName
@property
def next_siebling(self):
return type(self)(self.__node.nextSibling)
class TestPython: class TestPython:
def test_summing_simple(self, testdir): def test_summing_simple(self, testdir):
@ -41,8 +98,8 @@ class TestPython:
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, name="pytest", errors=0, failures=1, skips=3, tests=2) node.assert_attr(name="pytest", errors=0, failures=1, skips=3, tests=2)
def test_timing_function(self, testdir): def test_timing_function(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
@ -55,9 +112,9 @@ class TestPython:
time.sleep(0.01) time.sleep(0.01)
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
val = tnode.getAttributeNode("time").value val = tnode["time"]
assert round(float(val), 2) >= 0.03 assert round(float(val), 2) >= 0.03
def test_setup_error(self, testdir): def test_setup_error(self, testdir):
@ -69,16 +126,16 @@ class TestPython:
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, errors=1, tests=0) node.assert_attr(errors=1, tests=0)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file="test_setup_error.py", file="test_setup_error.py",
line="2", line="2",
classname="test_setup_error", classname="test_setup_error",
name="test_function") name="test_function")
fnode = tnode.getElementsByTagName("error")[0] fnode = tnode.find_first_by_tag("error")
assert_attr(fnode, message="test setup failure") fnode.assert_attr(message="test setup failure")
assert "ValueError" in fnode.toxml() assert "ValueError" in fnode.toxml()
def test_skip_contains_name_reason(self, testdir): def test_skip_contains_name_reason(self, testdir):
@ -89,19 +146,16 @@ class TestPython:
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret == 0 assert result.ret == 0
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, skips=1) node.assert_attr(skips=1)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file="test_skip_contains_name_reason.py", file="test_skip_contains_name_reason.py",
line="1", line="1",
classname="test_skip_contains_name_reason", classname="test_skip_contains_name_reason",
name="test_skip") name="test_skip")
snode = tnode.getElementsByTagName("skipped")[0] snode = tnode.find_first_by_tag("skipped")
assert_attr(snode, snode.assert_attr(type="pytest.skip", message="hello23", )
type="pytest.skip",
message="hello23",
)
def test_classname_instance(self, testdir): def test_classname_instance(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
@ -111,10 +165,10 @@ class TestPython:
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, failures=1) node.assert_attr(failures=1)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file="test_classname_instance.py", file="test_classname_instance.py",
line="1", line="1",
classname="test_classname_instance.TestClass", classname="test_classname_instance.TestClass",
@ -125,10 +179,10 @@ class TestPython:
p.write("def test_func(): 0/0") p.write("def test_func(): 0/0")
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, failures=1) node.assert_attr(failures=1)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file=os.path.join("sub", "test_hello.py"), file=os.path.join("sub", "test_hello.py"),
line="0", line="0",
classname="sub.test_hello", classname="sub.test_hello",
@ -139,12 +193,12 @@ class TestPython:
testdir.makepyfile("def test_function(): pass") testdir.makepyfile("def test_function(): pass")
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, errors=1, tests=0) node.assert_attr(errors=1, tests=0)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, classname="pytest", name="internal") tnode.assert_attr(classname="pytest", name="internal")
fnode = tnode.getElementsByTagName("error")[0] fnode = tnode.find_first_by_tag("error")
assert_attr(fnode, message="internal error") fnode.assert_attr(message="internal error")
assert "Division" in fnode.toxml() assert "Division" in fnode.toxml()
def test_failure_function(self, testdir): def test_failure_function(self, testdir):
@ -158,22 +212,22 @@ class TestPython:
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, failures=1, tests=1) node.assert_attr(failures=1, tests=1)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file="test_failure_function.py", file="test_failure_function.py",
line="1", line="1",
classname="test_failure_function", classname="test_failure_function",
name="test_fail") name="test_fail")
fnode = tnode.getElementsByTagName("failure")[0] fnode = tnode.find_first_by_tag("failure")
assert_attr(fnode, message="ValueError: 42") fnode.assert_attr(message="ValueError: 42")
assert "ValueError" in fnode.toxml() assert "ValueError" in fnode.toxml()
systemout = fnode.nextSibling systemout = fnode.next_siebling
assert systemout.tagName == "system-out" assert systemout.tag == "system-out"
assert "hello-stdout" in systemout.toxml() assert "hello-stdout" in systemout.toxml()
systemerr = systemout.nextSibling systemerr = systemout.next_siebling
assert systemerr.tagName == "system-err" assert systemerr.tag == "system-err"
assert "hello-stderr" in systemerr.toxml() assert "hello-stderr" in systemerr.toxml()
def test_failure_verbose_message(self, testdir): def test_failure_verbose_message(self, testdir):
@ -184,10 +238,10 @@ class TestPython:
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
fnode = tnode.getElementsByTagName("failure")[0] fnode = tnode.find_first_by_tag("failure")
assert_attr(fnode, message="AssertionError: An error assert 0") fnode.assert_attr(message="AssertionError: An error assert 0")
def test_failure_escape(self, testdir): def test_failure_escape(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
@ -199,22 +253,21 @@ class TestPython:
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, failures=3, tests=3) node.assert_attr(failures=3, tests=3)
for index, char in enumerate("<&'"): for index, char in enumerate("<&'"):
tnode = node.getElementsByTagName("testcase")[index] tnode = node.find_nth_by_tag("testcase", index)
assert_attr(tnode, tnode.assert_attr(
file="test_failure_escape.py", file="test_failure_escape.py",
line="1", line="1",
classname="test_failure_escape", classname="test_failure_escape",
name="test_func[%s]" % char) name="test_func[%s]" % char)
sysout = tnode.getElementsByTagName('system-out')[0] sysout = tnode.find_first_by_tag('system-out')
text = sysout.childNodes[0].wholeText text = sysout.text
assert text == '%s\n' % char assert text == '%s\n' % char
def test_junit_prefixing(self, testdir): def test_junit_prefixing(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
def test_func(): def test_func():
@ -225,16 +278,16 @@ class TestPython:
""") """)
result, dom = runandparse(testdir, "--junitprefix=xyz") result, dom = runandparse(testdir, "--junitprefix=xyz")
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, failures=1, tests=2) node.assert_attr(failures=1, tests=2)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file="test_junit_prefixing.py", file="test_junit_prefixing.py",
line="0", line="0",
classname="xyz.test_junit_prefixing", classname="xyz.test_junit_prefixing",
name="test_func") name="test_func")
tnode = node.getElementsByTagName("testcase")[1] tnode = node.find_nth_by_tag("testcase", 1)
assert_attr(tnode, tnode.assert_attr(
file="test_junit_prefixing.py", file="test_junit_prefixing.py",
line="3", line="3",
classname="xyz.test_junit_prefixing." classname="xyz.test_junit_prefixing."
@ -249,16 +302,16 @@ class TestPython:
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert not result.ret assert not result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, skips=1, tests=0) node.assert_attr(skips=1, tests=0)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file="test_xfailure_function.py", file="test_xfailure_function.py",
line="1", line="1",
classname="test_xfailure_function", classname="test_xfailure_function",
name="test_xfail") name="test_xfail")
fnode = tnode.getElementsByTagName("skipped")[0] fnode = tnode.find_first_by_tag("skipped")
assert_attr(fnode, message="expected test failure") fnode.assert_attr(message="expected test failure")
# assert "ValueError" in fnode.toxml() # assert "ValueError" in fnode.toxml()
def test_xfailure_xpass(self, testdir): def test_xfailure_xpass(self, testdir):
@ -270,48 +323,49 @@ class TestPython:
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
# assert result.ret # assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, skips=1, tests=0) node.assert_attr(skips=1, tests=0)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file="test_xfailure_xpass.py", file="test_xfailure_xpass.py",
line="1", line="1",
classname="test_xfailure_xpass", classname="test_xfailure_xpass",
name="test_xpass") name="test_xpass")
fnode = tnode.getElementsByTagName("skipped")[0] fnode = tnode.find_first_by_tag("skipped")
assert_attr(fnode, message="xfail-marked test passes unexpectedly") fnode.assert_attr(message="xfail-marked test passes unexpectedly")
# assert "ValueError" in fnode.toxml() # assert "ValueError" in fnode.toxml()
def test_collect_error(self, testdir): def test_collect_error(self, testdir):
testdir.makepyfile("syntax error") testdir.makepyfile("syntax error")
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, errors=1, tests=0) node.assert_attr(errors=1, tests=0)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file="test_collect_error.py", file="test_collect_error.py",
#classname="test_collect_error",
name="test_collect_error") name="test_collect_error")
assert tnode.getAttributeNode("line") is None assert tnode["line"] is None
fnode = tnode.getElementsByTagName("error")[0] fnode = tnode.find_first_by_tag("error")
assert_attr(fnode, message="collection failure") fnode.assert_attr(message="collection failure")
assert "SyntaxError" in fnode.toxml() assert "SyntaxError" in fnode.toxml()
def test_collect_skipped(self, testdir): def test_collect_skipped(self, testdir):
testdir.makepyfile("import pytest; pytest.skip('xyz')") testdir.makepyfile("import pytest; pytest.skip('xyz')")
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret == EXIT_NOTESTSCOLLECTED assert result.ret == EXIT_NOTESTSCOLLECTED
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, skips=1, tests=0) node.assert_attr(skips=1, tests=0)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(
file="test_collect_skipped.py", file="test_collect_skipped.py",
#classname="test_collect_error",
name="test_collect_skipped") name="test_collect_skipped")
assert tnode.getAttributeNode("line") is None # py.test doesn't give us a line here.
fnode = tnode.getElementsByTagName("skipped")[0] # py.test doesn't give us a line here.
assert_attr(fnode, message="collection skipped") assert tnode["line"] is None
fnode = tnode.find_first_by_tag("skipped")
fnode.assert_attr(message="collection skipped")
def test_unicode(self, testdir): def test_unicode(self, testdir):
value = 'hx\xc4\x85\xc4\x87\n' value = 'hx\xc4\x85\xc4\x87\n'
@ -323,8 +377,8 @@ class TestPython:
""" % value) """ % value)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret == 1 assert result.ret == 1
tnode = dom.getElementsByTagName("testcase")[0] tnode = dom.find_first_by_tag("testcase")
fnode = tnode.getElementsByTagName("failure")[0] fnode = tnode.find_first_by_tag("failure")
if not sys.platform.startswith("java"): if not sys.platform.startswith("java"):
assert "hx" in fnode.toxml() assert "hx" in fnode.toxml()
@ -347,9 +401,9 @@ class TestPython:
print('hello-stdout') print('hello-stdout')
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
pnode = node.getElementsByTagName("testcase")[0] pnode = node.find_first_by_tag("testcase")
systemout = pnode.getElementsByTagName("system-out")[0] systemout = pnode.find_first_by_tag("system-out")
assert "hello-stdout" in systemout.toxml() assert "hello-stdout" in systemout.toxml()
def test_pass_captures_stderr(self, testdir): def test_pass_captures_stderr(self, testdir):
@ -359,27 +413,32 @@ class TestPython:
sys.stderr.write('hello-stderr') sys.stderr.write('hello-stderr')
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
pnode = node.getElementsByTagName("testcase")[0] pnode = node.find_first_by_tag("testcase")
systemout = pnode.getElementsByTagName("system-err")[0] systemout = pnode.find_first_by_tag("system-err")
assert "hello-stderr" in systemout.toxml() assert "hello-stderr" in systemout.toxml()
def test_mangle_testnames(): def test_mangle_testnames():
from _pytest.junitxml import mangle_testnames from _pytest.junitxml import mangle_testnames
names = ["a/pything.py", "Class", "()", "method"] names = ["a/pything.py", "Class", "()", "method"]
newnames = mangle_testnames(names) newnames = mangle_testnames(names)
assert newnames == ["a.pything", "Class", "method"] assert newnames == ["a.pything", "Class", "method"]
def test_dont_configure_on_slaves(tmpdir): def test_dont_configure_on_slaves(tmpdir):
gotten = [] gotten = []
class FakeConfig: class FakeConfig:
def __init__(self): def __init__(self):
self.pluginmanager = self self.pluginmanager = self
self.option = self self.option = self
junitprefix = None junitprefix = None
# XXX: shouldnt need tmpdir ? # XXX: shouldnt need tmpdir ?
xmlpath = str(tmpdir.join('junix.xml')) xmlpath = str(tmpdir.join('junix.xml'))
register = gotten.append register = gotten.append
fake_config = FakeConfig() fake_config = FakeConfig()
from _pytest import junitxml from _pytest import junitxml
junitxml.pytest_configure(fake_config) junitxml.pytest_configure(fake_config)
@ -408,14 +467,12 @@ class TestNonPython:
testdir.tmpdir.join("myfile.xyz").write("hello") testdir.tmpdir.join("myfile.xyz").write("hello")
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
assert_attr(node, errors=0, failures=1, skips=0, tests=1) node.assert_attr(errors=0, failures=1, skips=0, tests=1)
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
assert_attr(tnode, tnode.assert_attr(name="myfile.xyz")
#classname="test_collect_error", fnode = tnode.find_first_by_tag("failure")
name="myfile.xyz") fnode.assert_attr(message="custom item runtest failed")
fnode = tnode.getElementsByTagName("failure")[0]
assert_attr(fnode, message="custom item runtest failed")
assert "custom item runtest failed" in fnode.toxml() assert "custom item runtest failed" in fnode.toxml()
@ -449,6 +506,7 @@ def test_nullbyte_replace(testdir):
text = xmlf.read() text = xmlf.read()
assert '#x0' in text assert '#x0' in text
def test_invalid_xml_escape(): def test_invalid_xml_escape():
# Test some more invalid xml chars, the full range should be # Test some more invalid xml chars, the full range should be
# tested really but let's just thest the edges of the ranges # tested really but let's just thest the edges of the ranges
@ -463,14 +521,13 @@ def test_invalid_xml_escape():
unichr(65) unichr(65)
except NameError: except NameError:
unichr = chr unichr = chr
invalid = (0x00, 0x1, 0xB, 0xC, 0xE, 0x19, invalid = (0x00, 0x1, 0xB, 0xC, 0xE, 0x19, 27, # issue #126
27, # issue #126
0xD800, 0xDFFF, 0xFFFE, 0x0FFFF) # , 0x110000) 0xD800, 0xDFFF, 0xFFFE, 0x0FFFF) # , 0x110000)
valid = (0x9, 0xA, 0x20,) # 0xD, 0xD7FF, 0xE000, 0xFFFD, 0x10000, 0x10FFFF) valid = (0x9, 0xA, 0x20, )
# 0xD, 0xD7FF, 0xE000, 0xFFFD, 0x10000, 0x10FFFF)
from _pytest.junitxml import bin_xml_escape from _pytest.junitxml import bin_xml_escape
for i in invalid: for i in invalid:
got = bin_xml_escape(unichr(i)).uniobj got = bin_xml_escape(unichr(i)).uniobj
if i <= 0xFF: if i <= 0xFF:
@ -481,6 +538,7 @@ def test_invalid_xml_escape():
for i in valid: for i in valid:
assert chr(i) == bin_xml_escape(unichr(i)).uniobj assert chr(i) == bin_xml_escape(unichr(i)).uniobj
def test_logxml_path_expansion(tmpdir, monkeypatch): def test_logxml_path_expansion(tmpdir, monkeypatch):
home_tilde = py.path.local(os.path.expanduser('~')).join('test.xml') home_tilde = py.path.local(os.path.expanduser('~')).join('test.xml')
@ -494,6 +552,7 @@ def test_logxml_path_expansion(tmpdir, monkeypatch):
xml_var = LogXML('$HOME%stest.xml' % tmpdir.sep, None) xml_var = LogXML('$HOME%stest.xml' % tmpdir.sep, None)
assert xml_var.logfile == home_var assert xml_var.logfile == home_var
def test_logxml_changingdir(testdir): def test_logxml_changingdir(testdir):
testdir.makepyfile(""" testdir.makepyfile("""
def test_func(): def test_func():
@ -505,6 +564,7 @@ def test_logxml_changingdir(testdir):
assert result.ret == 0 assert result.ret == 0
assert testdir.tmpdir.join("a/x.xml").check() assert testdir.tmpdir.join("a/x.xml").check()
def test_logxml_makedir(testdir): def test_logxml_makedir(testdir):
"""--junitxml should automatically create directories for the xml file""" """--junitxml should automatically create directories for the xml file"""
testdir.makepyfile(""" testdir.makepyfile("""
@ -515,6 +575,7 @@ def test_logxml_makedir(testdir):
assert result.ret == 0 assert result.ret == 0
assert testdir.tmpdir.join("path/to/results.xml").check() assert testdir.tmpdir.join("path/to/results.xml").check()
def test_escaped_parametrized_names_xml(testdir): def test_escaped_parametrized_names_xml(testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest
@ -524,49 +585,57 @@ def test_escaped_parametrized_names_xml(testdir):
""") """)
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret == 0 assert result.ret == 0
node = dom.getElementsByTagName("testcase")[0] node = dom.find_first_by_tag("testcase")
assert_attr(node, node.assert_attr(name="test_func[#x00]")
name="test_func[#x00]")
def test_unicode_issue368(testdir): def test_unicode_issue368(testdir):
path = testdir.tmpdir.join("test.xml") path = testdir.tmpdir.join("test.xml")
log = LogXML(str(path), None) log = LogXML(str(path), None)
ustr = py.builtin._totext("ВНИ!", "utf-8") ustr = py.builtin._totext("ВНИ!", "utf-8")
from _pytest.runner import BaseReport from _pytest.runner import BaseReport
class Report(BaseReport): class Report(BaseReport):
longrepr = ustr longrepr = ustr
sections = [] sections = []
nodeid = "something" nodeid = "something"
location = 'tests/filename.py', 42, 'TestClass.method' location = 'tests/filename.py', 42, 'TestClass.method'
report = Report()
test_report = Report()
# hopefully this is not too brittle ... # hopefully this is not too brittle ...
log.pytest_sessionstart() log.pytest_sessionstart()
log._opentestcase(report) node_reporter = log._opentestcase(test_report)
log.append_failure(report) node_reporter.append_failure(test_report)
log.append_collect_error(report) node_reporter.append_collect_error(test_report)
log.append_collect_skipped(report) node_reporter.append_collect_skipped(test_report)
log.append_error(report) node_reporter.append_error(test_report)
report.longrepr = "filename", 1, ustr test_report.longrepr = "filename", 1, ustr
log.append_skipped(report) node_reporter.append_skipped(test_report)
report.longrepr = "filename", 1, "Skipped: 卡嘣嘣" test_report.longrepr = "filename", 1, "Skipped: 卡嘣嘣"
log.append_skipped(report) node_reporter.append_skipped(test_report)
report.wasxfail = ustr test_report.wasxfail = ustr
log.append_skipped(report) node_reporter.append_skipped(test_report)
log.pytest_sessionfinish() log.pytest_sessionfinish()
def test_record_property(testdir): def test_record_property(testdir):
testdir.makepyfile(""" testdir.makepyfile("""
def test_record(record_xml_property): import pytest
@pytest.fixture
def other(record_xml_property):
record_xml_property("bar", 1)
def test_record(record_xml_property, other):
record_xml_property("foo", "<1"); record_xml_property("foo", "<1");
""") """)
result, dom = runandparse(testdir, '-rw') result, dom = runandparse(testdir, '-rw')
node = dom.getElementsByTagName("testsuite")[0] node = dom.find_first_by_tag("testsuite")
tnode = node.getElementsByTagName("testcase")[0] tnode = node.find_first_by_tag("testcase")
psnode = tnode.getElementsByTagName('properties')[0] psnode = tnode.find_first_by_tag('properties')
pnode = psnode.getElementsByTagName('property')[0] pnodes = psnode.find_by_tag('property')
assert_attr(pnode, name="foo", value="<1") pnodes[0].assert_attr(name="bar", value="1")
pnodes[1].assert_attr(name="foo", value="<1")
result.stdout.fnmatch_lines('*C3*test_record_property.py*experimental*') result.stdout.fnmatch_lines('*C3*test_record_property.py*experimental*')
@ -583,10 +652,33 @@ def test_random_report_log_xdist(testdir):
assert i != 22 assert i != 22
""") """)
_, dom = runandparse(testdir, '-n2') _, dom = runandparse(testdir, '-n2')
suite_node = dom.getElementsByTagName("testsuite")[0] suite_node = dom.find_first_by_tag("testsuite")
failed = [] failed = []
for case_node in suite_node.getElementsByTagName("testcase"): for case_node in suite_node.find_by_tag("testcase"):
if case_node.getElementsByTagName('failure'): if case_node.find_first_by_tag('failure'):
failed.append(case_node.getAttributeNode('name').value) failed.append(case_node['name'])
assert failed == ['test_x[22]'] assert failed == ['test_x[22]']
def test_runs_twice(testdir):
f = testdir.makepyfile('''
def test_pass():
pass
''')
result = testdir.runpytest(f, f, '--junitxml', testdir.tmpdir.join("test.xml"))
assert 'INTERNALERROR' not in str(result.stdout)
def test_runs_twice_xdist(testdir):
pytest.importorskip('xdist')
f = testdir.makepyfile('''
def test_pass():
pass
''')
result = testdir.runpytest(f,
'--dist', 'each', '--tx', '2*popen',
'--junitxml', testdir.tmpdir.join("test.xml"))
assert 'INTERNALERROR' not in str(result.stdout)