Skip to content

Commit c18f40d

Browse files
committed
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
1 parent d8e00c9 commit c18f40d

File tree

4 files changed

+36
-104
lines changed

4 files changed

+36
-104
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Jonas Obrist
114114
Jordan Guymon
115115
Jordan Moldow
116116
Jordan Speicher
117+
Joseph Hunkeler
117118
Joshua Bronson
118119
Jurko Gospodnetić
119120
Justyna Janczyszyn

changelog/3547.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``--junitxml`` emits XML data compatible with JUnit's offical schema releases.

src/_pytest/junitxml.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,14 @@ def record_testreport(self, testreport):
110110
classnames = names[:-1]
111111
if self.xml.prefix:
112112
classnames.insert(0, self.xml.prefix)
113-
attrs = {
114-
"classname": ".".join(classnames),
115-
"name": bin_xml_escape(names[-1]),
116-
"file": testreport.location[0],
117-
}
118-
if testreport.location[1] is not None:
119-
attrs["line"] = testreport.location[1]
113+
attrs = {"classname": ".".join(classnames), "name": bin_xml_escape(names[-1])}
120114
if hasattr(testreport, "url"):
121115
attrs["url"] = testreport.url
122116
self.attrs = attrs
123117
self.attrs.update(existing_attrs) # restore any user-defined attributes
124118

125119
def to_xml(self):
126-
testcase = Junit.testcase(time=self.duration, **self.attrs)
120+
testcase = Junit.testcase(time="%.3f" % self.duration, **self.attrs)
127121
testcase.append(self.make_properties_node())
128122
for node in self.nodes:
129123
testcase.append(node)
@@ -543,7 +537,7 @@ def pytest_sessionfinish(self):
543537
name=self.suite_name,
544538
errors=self.stats["error"],
545539
failures=self.stats["failure"],
546-
skips=self.stats["skipped"],
540+
skipped=self.stats["skipped"],
547541
tests=numtests,
548542
time="%.3f" % suite_time_delta,
549543
).unicode(indent=0)

testing/test_junitxml.py

Lines changed: 31 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def test_xpass():
107107
result, dom = runandparse(testdir)
108108
assert result.ret
109109
node = dom.find_first_by_tag("testsuite")
110-
node.assert_attr(name="pytest", errors=0, failures=1, skips=2, tests=5)
110+
node.assert_attr(name="pytest", errors=0, failures=1, skipped=2, tests=5)
111111

112112
def test_summing_simple_with_errors(self, testdir):
113113
testdir.makepyfile(
@@ -133,7 +133,7 @@ def test_xpass():
133133
result, dom = runandparse(testdir)
134134
assert result.ret
135135
node = dom.find_first_by_tag("testsuite")
136-
node.assert_attr(name="pytest", errors=1, failures=2, skips=1, tests=5)
136+
node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5)
137137

138138
def test_timing_function(self, testdir):
139139
testdir.makepyfile(
@@ -170,12 +170,7 @@ def test_function(arg):
170170
node = dom.find_first_by_tag("testsuite")
171171
node.assert_attr(errors=1, tests=1)
172172
tnode = node.find_first_by_tag("testcase")
173-
tnode.assert_attr(
174-
file="test_setup_error.py",
175-
line="5",
176-
classname="test_setup_error",
177-
name="test_function",
178-
)
173+
tnode.assert_attr(classname="test_setup_error", name="test_function")
179174
fnode = tnode.find_first_by_tag("error")
180175
fnode.assert_attr(message="test setup failure")
181176
assert "ValueError" in fnode.toxml()
@@ -197,12 +192,7 @@ def test_function(arg):
197192
assert result.ret
198193
node = dom.find_first_by_tag("testsuite")
199194
tnode = node.find_first_by_tag("testcase")
200-
tnode.assert_attr(
201-
file="test_teardown_error.py",
202-
line="6",
203-
classname="test_teardown_error",
204-
name="test_function",
205-
)
195+
tnode.assert_attr(classname="test_teardown_error", name="test_function")
206196
fnode = tnode.find_first_by_tag("error")
207197
fnode.assert_attr(message="test teardown failure")
208198
assert "ValueError" in fnode.toxml()
@@ -243,14 +233,9 @@ def test_skip():
243233
result, dom = runandparse(testdir)
244234
assert result.ret == 0
245235
node = dom.find_first_by_tag("testsuite")
246-
node.assert_attr(skips=1)
236+
node.assert_attr(skipped=1)
247237
tnode = node.find_first_by_tag("testcase")
248-
tnode.assert_attr(
249-
file="test_skip_contains_name_reason.py",
250-
line="1",
251-
classname="test_skip_contains_name_reason",
252-
name="test_skip",
253-
)
238+
tnode.assert_attr(classname="test_skip_contains_name_reason", name="test_skip")
254239
snode = tnode.find_first_by_tag("skipped")
255240
snode.assert_attr(type="pytest.skip", message="hello23")
256241

@@ -266,13 +251,10 @@ def test_skip():
266251
result, dom = runandparse(testdir)
267252
assert result.ret == 0
268253
node = dom.find_first_by_tag("testsuite")
269-
node.assert_attr(skips=1)
254+
node.assert_attr(skipped=1)
270255
tnode = node.find_first_by_tag("testcase")
271256
tnode.assert_attr(
272-
file="test_mark_skip_contains_name_reason.py",
273-
line="1",
274-
classname="test_mark_skip_contains_name_reason",
275-
name="test_skip",
257+
classname="test_mark_skip_contains_name_reason", name="test_skip"
276258
)
277259
snode = tnode.find_first_by_tag("skipped")
278260
snode.assert_attr(type="pytest.skip", message="hello24")
@@ -290,13 +272,10 @@ def test_skip():
290272
result, dom = runandparse(testdir)
291273
assert result.ret == 0
292274
node = dom.find_first_by_tag("testsuite")
293-
node.assert_attr(skips=1)
275+
node.assert_attr(skipped=1)
294276
tnode = node.find_first_by_tag("testcase")
295277
tnode.assert_attr(
296-
file="test_mark_skipif_contains_name_reason.py",
297-
line="2",
298-
classname="test_mark_skipif_contains_name_reason",
299-
name="test_skip",
278+
classname="test_mark_skipif_contains_name_reason", name="test_skip"
300279
)
301280
snode = tnode.find_first_by_tag("skipped")
302281
snode.assert_attr(type="pytest.skip", message="hello25")
@@ -329,10 +308,7 @@ def test_method(self):
329308
node.assert_attr(failures=1)
330309
tnode = node.find_first_by_tag("testcase")
331310
tnode.assert_attr(
332-
file="test_classname_instance.py",
333-
line="1",
334-
classname="test_classname_instance.TestClass",
335-
name="test_method",
311+
classname="test_classname_instance.TestClass", name="test_method"
336312
)
337313

338314
def test_classname_nested_dir(self, testdir):
@@ -343,12 +319,7 @@ def test_classname_nested_dir(self, testdir):
343319
node = dom.find_first_by_tag("testsuite")
344320
node.assert_attr(failures=1)
345321
tnode = node.find_first_by_tag("testcase")
346-
tnode.assert_attr(
347-
file=os.path.join("sub", "test_hello.py"),
348-
line="0",
349-
classname="sub.test_hello",
350-
name="test_func",
351-
)
322+
tnode.assert_attr(classname="sub.test_hello", name="test_func")
352323

353324
def test_internal_error(self, testdir):
354325
testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0")
@@ -384,12 +355,7 @@ def test_fail():
384355
node = dom.find_first_by_tag("testsuite")
385356
node.assert_attr(failures=1, tests=1)
386357
tnode = node.find_first_by_tag("testcase")
387-
tnode.assert_attr(
388-
file="test_failure_function.py",
389-
line="3",
390-
classname="test_failure_function",
391-
name="test_fail",
392-
)
358+
tnode.assert_attr(classname="test_failure_function", name="test_fail")
393359
fnode = tnode.find_first_by_tag("failure")
394360
fnode.assert_attr(message="ValueError: 42")
395361
assert "ValueError" in fnode.toxml()
@@ -446,10 +412,7 @@ def test_func(arg1):
446412

447413
tnode = node.find_nth_by_tag("testcase", index)
448414
tnode.assert_attr(
449-
file="test_failure_escape.py",
450-
line="1",
451-
classname="test_failure_escape",
452-
name="test_func[%s]" % char,
415+
classname="test_failure_escape", name="test_func[%s]" % char
453416
)
454417
sysout = tnode.find_first_by_tag("system-out")
455418
text = sysout.text
@@ -470,18 +433,10 @@ def test_hello(self):
470433
node = dom.find_first_by_tag("testsuite")
471434
node.assert_attr(failures=1, tests=2)
472435
tnode = node.find_first_by_tag("testcase")
473-
tnode.assert_attr(
474-
file="test_junit_prefixing.py",
475-
line="0",
476-
classname="xyz.test_junit_prefixing",
477-
name="test_func",
478-
)
436+
tnode.assert_attr(classname="xyz.test_junit_prefixing", name="test_func")
479437
tnode = node.find_nth_by_tag("testcase", 1)
480438
tnode.assert_attr(
481-
file="test_junit_prefixing.py",
482-
line="3",
483-
classname="xyz.test_junit_prefixing.TestHello",
484-
name="test_hello",
439+
classname="xyz.test_junit_prefixing.TestHello", name="test_hello"
485440
)
486441

487442
def test_xfailure_function(self, testdir):
@@ -495,14 +450,9 @@ def test_xfail():
495450
result, dom = runandparse(testdir)
496451
assert not result.ret
497452
node = dom.find_first_by_tag("testsuite")
498-
node.assert_attr(skips=1, tests=1)
453+
node.assert_attr(skipped=1, tests=1)
499454
tnode = node.find_first_by_tag("testcase")
500-
tnode.assert_attr(
501-
file="test_xfailure_function.py",
502-
line="1",
503-
classname="test_xfailure_function",
504-
name="test_xfail",
505-
)
455+
tnode.assert_attr(classname="test_xfailure_function", name="test_xfail")
506456
fnode = tnode.find_first_by_tag("skipped")
507457
fnode.assert_attr(message="expected test failure")
508458
# assert "ValueError" in fnode.toxml()
@@ -538,14 +488,9 @@ def test_xpass():
538488
result, dom = runandparse(testdir)
539489
# assert result.ret
540490
node = dom.find_first_by_tag("testsuite")
541-
node.assert_attr(skips=0, tests=1)
491+
node.assert_attr(skipped=0, tests=1)
542492
tnode = node.find_first_by_tag("testcase")
543-
tnode.assert_attr(
544-
file="test_xfailure_xpass.py",
545-
line="1",
546-
classname="test_xfailure_xpass",
547-
name="test_xpass",
548-
)
493+
tnode.assert_attr(classname="test_xfailure_xpass", name="test_xpass")
549494

550495
def test_xfailure_xpass_strict(self, testdir):
551496
testdir.makepyfile(
@@ -559,14 +504,9 @@ def test_xpass():
559504
result, dom = runandparse(testdir)
560505
# assert result.ret
561506
node = dom.find_first_by_tag("testsuite")
562-
node.assert_attr(skips=0, tests=1)
507+
node.assert_attr(skipped=0, tests=1)
563508
tnode = node.find_first_by_tag("testcase")
564-
tnode.assert_attr(
565-
file="test_xfailure_xpass_strict.py",
566-
line="1",
567-
classname="test_xfailure_xpass_strict",
568-
name="test_xpass",
569-
)
509+
tnode.assert_attr(classname="test_xfailure_xpass_strict", name="test_xpass")
570510
fnode = tnode.find_first_by_tag("failure")
571511
fnode.assert_attr(message="[XPASS(strict)] This needs to fail!")
572512

@@ -577,8 +517,6 @@ def test_collect_error(self, testdir):
577517
node = dom.find_first_by_tag("testsuite")
578518
node.assert_attr(errors=1, tests=1)
579519
tnode = node.find_first_by_tag("testcase")
580-
tnode.assert_attr(file="test_collect_error.py", name="test_collect_error")
581-
assert tnode["line"] is None
582520
fnode = tnode.find_first_by_tag("error")
583521
fnode.assert_attr(message="collection failure")
584522
assert "SyntaxError" in fnode.toxml()
@@ -761,7 +699,7 @@ def repr_failure(self, excinfo):
761699
result, dom = runandparse(testdir)
762700
assert result.ret
763701
node = dom.find_first_by_tag("testsuite")
764-
node.assert_attr(errors=0, failures=1, skips=0, tests=1)
702+
node.assert_attr(errors=0, failures=1, skipped=0, tests=1)
765703
tnode = node.find_first_by_tag("testcase")
766704
tnode.assert_attr(name="myfile.xyz")
767705
fnode = tnode.find_first_by_tag("failure")
@@ -1123,20 +1061,18 @@ def test_pass():
11231061

11241062
assert "INTERNALERROR" not in result.stdout.str()
11251063

1126-
items = sorted(
1127-
"%(classname)s %(name)s %(file)s" % x for x in dom.find_by_tag("testcase")
1128-
)
1064+
items = sorted("%(classname)s %(name)s" % x for x in dom.find_by_tag("testcase"))
11291065
import pprint
11301066

11311067
pprint.pprint(items)
11321068
assert items == [
1133-
u"conftest a conftest.py",
1134-
u"conftest a conftest.py",
1135-
u"conftest b conftest.py",
1136-
u"test_fancy_items_regression a test_fancy_items_regression.py",
1137-
u"test_fancy_items_regression a test_fancy_items_regression.py",
1138-
u"test_fancy_items_regression b test_fancy_items_regression.py",
1139-
u"test_fancy_items_regression test_pass" u" test_fancy_items_regression.py",
1069+
u"conftest a",
1070+
u"conftest a",
1071+
u"conftest b",
1072+
u"test_fancy_items_regression a",
1073+
u"test_fancy_items_regression a",
1074+
u"test_fancy_items_regression b",
1075+
u"test_fancy_items_regression test_pass",
11401076
]
11411077

11421078

0 commit comments

Comments
 (0)