Skip to content

Commit 7fa27af

Browse files
committed
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.
1 parent 76497c2 commit 7fa27af

File tree

4 files changed

+39
-5
lines changed

4 files changed

+39
-5
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Janne Vanhala
3939
Jason R. Coombs
4040
Jurko Gospodnetić
4141
Katarzyna Jachim
42+
Kevin Cox
4243
Maciek Fijalkowski
4344
Maho
4445
Marc Schlaich

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767

6868
- add a new ``--noconftest`` argument which ignores all ``conftest.py`` files.
6969

70+
- add ``file`` and ``line`` attributes to JUnit-XML output.
71+
7072
2.7.2 (compared to 2.7.1)
7173
-----------------------------
7274

_pytest/junitxml.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
""" report test results in JUnit-XML format, for use with Hudson and build integration servers.
22
3+
Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
4+
35
Based on initial code from Ross Lawley.
46
"""
57
import py
@@ -93,11 +95,15 @@ def _opentestcase(self, report):
9395
classnames = names[:-1]
9496
if self.prefix:
9597
classnames.insert(0, self.prefix)
96-
self.tests.append(Junit.testcase(
97-
classname=".".join(classnames),
98-
name=bin_xml_escape(names[-1]),
99-
time=0
100-
))
98+
attrs = {
99+
"classname": ".".join(classnames),
100+
"name": bin_xml_escape(names[-1]),
101+
"file": report.location[0],
102+
"time": 0,
103+
}
104+
if report.location[1] is not None:
105+
attrs["line"] = report.location[1]
106+
self.tests.append(Junit.testcase(**attrs))
101107

102108
def _write_captured_output(self, report):
103109
for capname in ('out', 'err'):

testing/test_junitxml.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def test_function(arg):
7070
assert_attr(node, errors=1, tests=0)
7171
tnode = node.getElementsByTagName("testcase")[0]
7272
assert_attr(tnode,
73+
file="test_setup_error.py",
74+
line="2",
7375
classname="test_setup_error",
7476
name="test_function")
7577
fnode = tnode.getElementsByTagName("error")[0]
@@ -88,6 +90,8 @@ def test_skip():
8890
assert_attr(node, skips=1)
8991
tnode = node.getElementsByTagName("testcase")[0]
9092
assert_attr(tnode,
93+
file="test_skip_contains_name_reason.py",
94+
line="1",
9195
classname="test_skip_contains_name_reason",
9296
name="test_skip")
9397
snode = tnode.getElementsByTagName("skipped")[0]
@@ -108,6 +112,8 @@ def test_method(self):
108112
assert_attr(node, failures=1)
109113
tnode = node.getElementsByTagName("testcase")[0]
110114
assert_attr(tnode,
115+
file="test_classname_instance.py",
116+
line="1",
111117
classname="test_classname_instance.TestClass",
112118
name="test_method")
113119

@@ -120,6 +126,8 @@ def test_classname_nested_dir(self, testdir):
120126
assert_attr(node, failures=1)
121127
tnode = node.getElementsByTagName("testcase")[0]
122128
assert_attr(tnode,
129+
file=os.path.join("sub", "test_hello.py"),
130+
line="0",
123131
classname="sub.test_hello",
124132
name="test_func")
125133

@@ -151,6 +159,8 @@ def test_fail():
151159
assert_attr(node, failures=1, tests=1)
152160
tnode = node.getElementsByTagName("testcase")[0]
153161
assert_attr(tnode,
162+
file="test_failure_function.py",
163+
line="1",
154164
classname="test_failure_function",
155165
name="test_fail")
156166
fnode = tnode.getElementsByTagName("failure")[0]
@@ -193,6 +203,8 @@ def test_func(arg1):
193203

194204
tnode = node.getElementsByTagName("testcase")[index]
195205
assert_attr(tnode,
206+
file="test_failure_escape.py",
207+
line="1",
196208
classname="test_failure_escape",
197209
name="test_func[%s]" % char)
198210
sysout = tnode.getElementsByTagName('system-out')[0]
@@ -214,10 +226,14 @@ def test_hello(self):
214226
assert_attr(node, failures=1, tests=2)
215227
tnode = node.getElementsByTagName("testcase")[0]
216228
assert_attr(tnode,
229+
file="test_junit_prefixing.py",
230+
line="0",
217231
classname="xyz.test_junit_prefixing",
218232
name="test_func")
219233
tnode = node.getElementsByTagName("testcase")[1]
220234
assert_attr(tnode,
235+
file="test_junit_prefixing.py",
236+
line="3",
221237
classname="xyz.test_junit_prefixing."
222238
"TestHello",
223239
name="test_hello")
@@ -234,6 +250,8 @@ def test_xfail():
234250
assert_attr(node, skips=1, tests=0)
235251
tnode = node.getElementsByTagName("testcase")[0]
236252
assert_attr(tnode,
253+
file="test_xfailure_function.py",
254+
line="1",
237255
classname="test_xfailure_function",
238256
name="test_xfail")
239257
fnode = tnode.getElementsByTagName("skipped")[0]
@@ -253,6 +271,8 @@ def test_xpass():
253271
assert_attr(node, skips=1, tests=0)
254272
tnode = node.getElementsByTagName("testcase")[0]
255273
assert_attr(tnode,
274+
file="test_xfailure_xpass.py",
275+
line="1",
256276
classname="test_xfailure_xpass",
257277
name="test_xpass")
258278
fnode = tnode.getElementsByTagName("skipped")[0]
@@ -267,8 +287,10 @@ def test_collect_error(self, testdir):
267287
assert_attr(node, errors=1, tests=0)
268288
tnode = node.getElementsByTagName("testcase")[0]
269289
assert_attr(tnode,
290+
file="test_collect_error.py",
270291
#classname="test_collect_error",
271292
name="test_collect_error")
293+
assert tnode.getAttributeNode("line") is None
272294
fnode = tnode.getElementsByTagName("error")[0]
273295
assert_attr(fnode, message="collection failure")
274296
assert "SyntaxError" in fnode.toxml()
@@ -281,8 +303,10 @@ def test_collect_skipped(self, testdir):
281303
assert_attr(node, skips=1, tests=0)
282304
tnode = node.getElementsByTagName("testcase")[0]
283305
assert_attr(tnode,
306+
file="test_collect_skipped.py",
284307
#classname="test_collect_error",
285308
name="test_collect_skipped")
309+
assert tnode.getAttributeNode("line") is None # py.test doesn't give us a line here.
286310
fnode = tnode.getElementsByTagName("skipped")[0]
287311
assert_attr(fnode, message="collection skipped")
288312

@@ -510,6 +534,7 @@ class Report(BaseReport):
510534
longrepr = ustr
511535
sections = []
512536
nodeid = "something"
537+
location = 'tests/filename.py', 42, 'TestClass.method'
513538
report = Report()
514539

515540
# hopefully this is not too brittle ...

0 commit comments

Comments
 (0)