From 7fa27af4083c291d25a53075bcf0528ea6d2d1d6 Mon Sep 17 00:00:00 2001 From: Kevin Cox Date: Tue, 30 Jun 2015 19:30:20 -0400 Subject: [PATCH] Add `file` and `line` attributes to junit-xml output. This adds the `file` and `line` attributes to the junit-xml output which can be used by tooling to identify where tests come from. This can be used for many things such as IDEs jumping to failures and test runners evenly balancing tests among multiple executors. Update test_junitxml.py Foo. --- AUTHORS | 1 + CHANGELOG | 2 ++ _pytest/junitxml.py | 16 +++++++++++----- testing/test_junitxml.py | 25 +++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/AUTHORS b/AUTHORS index 75f66b0b9..7f866e74f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -39,6 +39,7 @@ Janne Vanhala Jason R. Coombs Jurko Gospodnetić Katarzyna Jachim +Kevin Cox Maciek Fijalkowski Maho Marc Schlaich diff --git a/CHANGELOG b/CHANGELOG index cba430e14..07eecd171 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -67,6 +67,8 @@ - add a new ``--noconftest`` argument which ignores all ``conftest.py`` files. +- add ``file`` and ``line`` attributes to JUnit-XML output. + 2.7.2 (compared to 2.7.1) ----------------------------- diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py index 2a1220625..c12fa084a 100644 --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -1,5 +1,7 @@ """ report test results in JUnit-XML format, for use with Hudson 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. """ import py @@ -93,11 +95,15 @@ class LogXML(object): classnames = names[:-1] if self.prefix: classnames.insert(0, self.prefix) - self.tests.append(Junit.testcase( - classname=".".join(classnames), - name=bin_xml_escape(names[-1]), - time=0 - )) + attrs = { + "classname": ".".join(classnames), + "name": bin_xml_escape(names[-1]), + "file": report.location[0], + "time": 0, + } + if report.location[1] is not None: + attrs["line"] = report.location[1] + self.tests.append(Junit.testcase(**attrs)) def _write_captured_output(self, report): for capname in ('out', 'err'): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index d8b18b177..6258cb473 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -70,6 +70,8 @@ class TestPython: assert_attr(node, errors=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file="test_setup_error.py", + line="2", classname="test_setup_error", name="test_function") fnode = tnode.getElementsByTagName("error")[0] @@ -88,6 +90,8 @@ class TestPython: assert_attr(node, skips=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file="test_skip_contains_name_reason.py", + line="1", classname="test_skip_contains_name_reason", name="test_skip") snode = tnode.getElementsByTagName("skipped")[0] @@ -108,6 +112,8 @@ class TestPython: assert_attr(node, failures=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file="test_classname_instance.py", + line="1", classname="test_classname_instance.TestClass", name="test_method") @@ -120,6 +126,8 @@ class TestPython: assert_attr(node, failures=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file=os.path.join("sub", "test_hello.py"), + line="0", classname="sub.test_hello", name="test_func") @@ -151,6 +159,8 @@ class TestPython: assert_attr(node, failures=1, tests=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file="test_failure_function.py", + line="1", classname="test_failure_function", name="test_fail") fnode = tnode.getElementsByTagName("failure")[0] @@ -193,6 +203,8 @@ class TestPython: tnode = node.getElementsByTagName("testcase")[index] assert_attr(tnode, + file="test_failure_escape.py", + line="1", classname="test_failure_escape", name="test_func[%s]" % char) sysout = tnode.getElementsByTagName('system-out')[0] @@ -214,10 +226,14 @@ class TestPython: assert_attr(node, failures=1, tests=2) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file="test_junit_prefixing.py", + line="0", classname="xyz.test_junit_prefixing", name="test_func") tnode = node.getElementsByTagName("testcase")[1] assert_attr(tnode, + file="test_junit_prefixing.py", + line="3", classname="xyz.test_junit_prefixing." "TestHello", name="test_hello") @@ -234,6 +250,8 @@ class TestPython: assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file="test_xfailure_function.py", + line="1", classname="test_xfailure_function", name="test_xfail") fnode = tnode.getElementsByTagName("skipped")[0] @@ -253,6 +271,8 @@ class TestPython: assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file="test_xfailure_xpass.py", + line="1", classname="test_xfailure_xpass", name="test_xpass") fnode = tnode.getElementsByTagName("skipped")[0] @@ -267,8 +287,10 @@ class TestPython: assert_attr(node, errors=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file="test_collect_error.py", #classname="test_collect_error", name="test_collect_error") + assert tnode.getAttributeNode("line") is None fnode = tnode.getElementsByTagName("error")[0] assert_attr(fnode, message="collection failure") assert "SyntaxError" in fnode.toxml() @@ -281,8 +303,10 @@ class TestPython: assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, + file="test_collect_skipped.py", #classname="test_collect_error", name="test_collect_skipped") + assert tnode.getAttributeNode("line") is None # py.test doesn't give us a line here. fnode = tnode.getElementsByTagName("skipped")[0] assert_attr(fnode, message="collection skipped") @@ -510,6 +534,7 @@ def test_unicode_issue368(testdir): longrepr = ustr sections = [] nodeid = "something" + location = 'tests/filename.py', 42, 'TestClass.method' report = Report() # hopefully this is not too brittle ...