Emit JUnit compatible XML

* Remove non-standard testcase elements: 'file' and 'line'
* Replace testcase element 'skips' with 'skipped'
* Time resolution uses the standard format: 0.000
* Tests use corrected XML output with proper attributes
This commit is contained in:
Joseph Hunkeler 2018-12-02 02:45:31 -05:00
parent 4947eb85c0
commit af2ee1e80a
No known key found for this signature in database
GPG Key ID: 5C1D2774D4689768
4 changed files with 36 additions and 104 deletions

View File

@ -118,6 +118,7 @@ Jonas Obrist
Jordan Guymon Jordan Guymon
Jordan Moldow Jordan Moldow
Jordan Speicher Jordan Speicher
Joseph Hunkeler
Joshua Bronson Joshua Bronson
Jurko Gospodnetić Jurko Gospodnetić
Justyna Janczyszyn Justyna Janczyszyn

View File

@ -0,0 +1 @@
``--junitxml`` emits XML data compatible with JUnit's offical schema releases.

View File

@ -107,20 +107,14 @@ class _NodeReporter(object):
classnames = names[:-1] classnames = names[:-1]
if self.xml.prefix: if self.xml.prefix:
classnames.insert(0, self.xml.prefix) classnames.insert(0, self.xml.prefix)
attrs = { attrs = {"classname": ".".join(classnames), "name": bin_xml_escape(names[-1])}
"classname": ".".join(classnames),
"name": bin_xml_escape(names[-1]),
"file": testreport.location[0],
}
if testreport.location[1] is not None:
attrs["line"] = testreport.location[1]
if hasattr(testreport, "url"): if hasattr(testreport, "url"):
attrs["url"] = testreport.url attrs["url"] = testreport.url
self.attrs = attrs self.attrs = attrs
self.attrs.update(existing_attrs) # restore any user-defined attributes self.attrs.update(existing_attrs) # restore any user-defined attributes
def to_xml(self): def to_xml(self):
testcase = Junit.testcase(time=self.duration, **self.attrs) testcase = Junit.testcase(time="%.3f" % self.duration, **self.attrs)
testcase.append(self.make_properties_node()) testcase.append(self.make_properties_node())
for node in self.nodes: for node in self.nodes:
testcase.append(node) testcase.append(node)
@ -545,7 +539,7 @@ class LogXML(object):
name=self.suite_name, name=self.suite_name,
errors=self.stats["error"], errors=self.stats["error"],
failures=self.stats["failure"], failures=self.stats["failure"],
skips=self.stats["skipped"], skipped=self.stats["skipped"],
tests=numtests, tests=numtests,
time="%.3f" % suite_time_delta, time="%.3f" % suite_time_delta,
).unicode(indent=0) ).unicode(indent=0)

View File

@ -107,7 +107,7 @@ class TestPython(object):
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(name="pytest", errors=0, failures=1, skips=2, tests=5) node.assert_attr(name="pytest", errors=0, failures=1, skipped=2, tests=5)
def test_summing_simple_with_errors(self, testdir): def test_summing_simple_with_errors(self, testdir):
testdir.makepyfile( testdir.makepyfile(
@ -133,7 +133,7 @@ class TestPython(object):
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(name="pytest", errors=1, failures=2, skips=1, tests=5) node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5)
def test_timing_function(self, testdir): def test_timing_function(self, testdir):
testdir.makepyfile( testdir.makepyfile(
@ -201,12 +201,7 @@ class TestPython(object):
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(errors=1, tests=1) node.assert_attr(errors=1, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(classname="test_setup_error", name="test_function")
file="test_setup_error.py",
line="5",
classname="test_setup_error",
name="test_function",
)
fnode = tnode.find_first_by_tag("error") fnode = tnode.find_first_by_tag("error")
fnode.assert_attr(message="test setup failure") fnode.assert_attr(message="test setup failure")
assert "ValueError" in fnode.toxml() assert "ValueError" in fnode.toxml()
@ -228,12 +223,7 @@ class TestPython(object):
assert result.ret assert result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(classname="test_teardown_error", name="test_function")
file="test_teardown_error.py",
line="6",
classname="test_teardown_error",
name="test_function",
)
fnode = tnode.find_first_by_tag("error") fnode = tnode.find_first_by_tag("error")
fnode.assert_attr(message="test teardown failure") fnode.assert_attr(message="test teardown failure")
assert "ValueError" in fnode.toxml() assert "ValueError" in fnode.toxml()
@ -274,14 +264,9 @@ class TestPython(object):
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret == 0 assert result.ret == 0
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(skips=1) node.assert_attr(skipped=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(classname="test_skip_contains_name_reason", name="test_skip")
file="test_skip_contains_name_reason.py",
line="1",
classname="test_skip_contains_name_reason",
name="test_skip",
)
snode = tnode.find_first_by_tag("skipped") snode = tnode.find_first_by_tag("skipped")
snode.assert_attr(type="pytest.skip", message="hello23") snode.assert_attr(type="pytest.skip", message="hello23")
@ -297,13 +282,10 @@ class TestPython(object):
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret == 0 assert result.ret == 0
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(skips=1) node.assert_attr(skipped=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(
file="test_mark_skip_contains_name_reason.py", classname="test_mark_skip_contains_name_reason", name="test_skip"
line="1",
classname="test_mark_skip_contains_name_reason",
name="test_skip",
) )
snode = tnode.find_first_by_tag("skipped") snode = tnode.find_first_by_tag("skipped")
snode.assert_attr(type="pytest.skip", message="hello24") snode.assert_attr(type="pytest.skip", message="hello24")
@ -321,13 +303,10 @@ class TestPython(object):
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret == 0 assert result.ret == 0
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(skips=1) node.assert_attr(skipped=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(
file="test_mark_skipif_contains_name_reason.py", classname="test_mark_skipif_contains_name_reason", name="test_skip"
line="2",
classname="test_mark_skipif_contains_name_reason",
name="test_skip",
) )
snode = tnode.find_first_by_tag("skipped") snode = tnode.find_first_by_tag("skipped")
snode.assert_attr(type="pytest.skip", message="hello25") snode.assert_attr(type="pytest.skip", message="hello25")
@ -360,10 +339,7 @@ class TestPython(object):
node.assert_attr(failures=1) node.assert_attr(failures=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(
file="test_classname_instance.py", classname="test_classname_instance.TestClass", name="test_method"
line="1",
classname="test_classname_instance.TestClass",
name="test_method",
) )
def test_classname_nested_dir(self, testdir): def test_classname_nested_dir(self, testdir):
@ -374,12 +350,7 @@ class TestPython(object):
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(failures=1) node.assert_attr(failures=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(classname="sub.test_hello", name="test_func")
file=os.path.join("sub", "test_hello.py"),
line="0",
classname="sub.test_hello",
name="test_func",
)
def test_internal_error(self, testdir): def test_internal_error(self, testdir):
testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0")
@ -415,12 +386,7 @@ class TestPython(object):
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(failures=1, tests=1) node.assert_attr(failures=1, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(classname="test_failure_function", name="test_fail")
file="test_failure_function.py",
line="3",
classname="test_failure_function",
name="test_fail",
)
fnode = tnode.find_first_by_tag("failure") fnode = tnode.find_first_by_tag("failure")
fnode.assert_attr(message="ValueError: 42") fnode.assert_attr(message="ValueError: 42")
assert "ValueError" in fnode.toxml() assert "ValueError" in fnode.toxml()
@ -477,10 +443,7 @@ class TestPython(object):
tnode = node.find_nth_by_tag("testcase", index) tnode = node.find_nth_by_tag("testcase", index)
tnode.assert_attr( tnode.assert_attr(
file="test_failure_escape.py", classname="test_failure_escape", name="test_func[%s]" % char
line="1",
classname="test_failure_escape",
name="test_func[%s]" % char,
) )
sysout = tnode.find_first_by_tag("system-out") sysout = tnode.find_first_by_tag("system-out")
text = sysout.text text = sysout.text
@ -501,18 +464,10 @@ class TestPython(object):
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(failures=1, tests=2) node.assert_attr(failures=1, tests=2)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(classname="xyz.test_junit_prefixing", name="test_func")
file="test_junit_prefixing.py",
line="0",
classname="xyz.test_junit_prefixing",
name="test_func",
)
tnode = node.find_nth_by_tag("testcase", 1) tnode = node.find_nth_by_tag("testcase", 1)
tnode.assert_attr( tnode.assert_attr(
file="test_junit_prefixing.py", classname="xyz.test_junit_prefixing.TestHello", name="test_hello"
line="3",
classname="xyz.test_junit_prefixing.TestHello",
name="test_hello",
) )
def test_xfailure_function(self, testdir): def test_xfailure_function(self, testdir):
@ -526,14 +481,9 @@ class TestPython(object):
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert not result.ret assert not result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(skips=1, tests=1) node.assert_attr(skipped=1, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(classname="test_xfailure_function", name="test_xfail")
file="test_xfailure_function.py",
line="1",
classname="test_xfailure_function",
name="test_xfail",
)
fnode = tnode.find_first_by_tag("skipped") fnode = tnode.find_first_by_tag("skipped")
fnode.assert_attr(message="expected test failure") fnode.assert_attr(message="expected test failure")
# assert "ValueError" in fnode.toxml() # assert "ValueError" in fnode.toxml()
@ -569,14 +519,9 @@ class TestPython(object):
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
# assert result.ret # assert result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(skips=0, tests=1) node.assert_attr(skipped=0, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(classname="test_xfailure_xpass", name="test_xpass")
file="test_xfailure_xpass.py",
line="1",
classname="test_xfailure_xpass",
name="test_xpass",
)
def test_xfailure_xpass_strict(self, testdir): def test_xfailure_xpass_strict(self, testdir):
testdir.makepyfile( testdir.makepyfile(
@ -590,14 +535,9 @@ class TestPython(object):
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
# assert result.ret # assert result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(skips=0, tests=1) node.assert_attr(skipped=0, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr( tnode.assert_attr(classname="test_xfailure_xpass_strict", name="test_xpass")
file="test_xfailure_xpass_strict.py",
line="1",
classname="test_xfailure_xpass_strict",
name="test_xpass",
)
fnode = tnode.find_first_by_tag("failure") fnode = tnode.find_first_by_tag("failure")
fnode.assert_attr(message="[XPASS(strict)] This needs to fail!") fnode.assert_attr(message="[XPASS(strict)] This needs to fail!")
@ -608,8 +548,6 @@ class TestPython(object):
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(errors=1, tests=1) node.assert_attr(errors=1, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr(file="test_collect_error.py", name="test_collect_error")
assert tnode["line"] is None
fnode = tnode.find_first_by_tag("error") fnode = tnode.find_first_by_tag("error")
fnode.assert_attr(message="collection failure") fnode.assert_attr(message="collection failure")
assert "SyntaxError" in fnode.toxml() assert "SyntaxError" in fnode.toxml()
@ -792,7 +730,7 @@ class TestNonPython(object):
result, dom = runandparse(testdir) result, dom = runandparse(testdir)
assert result.ret assert result.ret
node = dom.find_first_by_tag("testsuite") node = dom.find_first_by_tag("testsuite")
node.assert_attr(errors=0, failures=1, skips=0, tests=1) node.assert_attr(errors=0, failures=1, skipped=0, tests=1)
tnode = node.find_first_by_tag("testcase") tnode = node.find_first_by_tag("testcase")
tnode.assert_attr(name="myfile.xyz") tnode.assert_attr(name="myfile.xyz")
fnode = tnode.find_first_by_tag("failure") fnode = tnode.find_first_by_tag("failure")
@ -1155,20 +1093,18 @@ def test_fancy_items_regression(testdir):
assert "INTERNALERROR" not in result.stdout.str() assert "INTERNALERROR" not in result.stdout.str()
items = sorted( items = sorted("%(classname)s %(name)s" % x for x in dom.find_by_tag("testcase"))
"%(classname)s %(name)s %(file)s" % x for x in dom.find_by_tag("testcase")
)
import pprint import pprint
pprint.pprint(items) pprint.pprint(items)
assert items == [ assert items == [
u"conftest a conftest.py", u"conftest a",
u"conftest a conftest.py", u"conftest a",
u"conftest b conftest.py", u"conftest b",
u"test_fancy_items_regression a test_fancy_items_regression.py", u"test_fancy_items_regression a",
u"test_fancy_items_regression a test_fancy_items_regression.py", u"test_fancy_items_regression a",
u"test_fancy_items_regression b test_fancy_items_regression.py", u"test_fancy_items_regression b",
u"test_fancy_items_regression test_pass" u" test_fancy_items_regression.py", u"test_fancy_items_regression test_pass",
] ]