Skip to content

Commit 82806d5

Browse files
committed
Add file and lineno attributes to junit-xml output.
This adds the `file` and `lineno` 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.
1 parent 76497c2 commit 82806d5

File tree

2 files changed

+34
-5
lines changed

2 files changed

+34
-5
lines changed

_pytest/junitxml.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,15 @@ def _opentestcase(self, report):
9393
classnames = names[:-1]
9494
if self.prefix:
9595
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-
))
96+
attrs = {
97+
"classname": ".".join(classnames),
98+
"name": bin_xml_escape(names[-1]),
99+
"file": report.location[0],
100+
"time": 0,
101+
}
102+
if report.location[1] is not None:
103+
attrs["lineno"] = report.location[1]
104+
self.tests.append(Junit.testcase(**attrs))
101105

102106
def _write_captured_output(self, report):
103107
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+
lineno="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+
lineno="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+
lineno="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="sub/test_hello.py",
130+
lineno="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+
lineno="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+
lineno="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+
lineno="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+
lineno="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+
lineno="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+
lineno="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("lineno") 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("lineno") is None # JUnit 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)