Skip to content

Commit 26e50f1

Browse files
committed
junitxml: adjust junitxml output file to comply with JUnit xsd
Change XML file structure in the manner that failures in call and errors in teardown in one test will appear under separate testcase elements in the XML report.
1 parent 0f3d7ac commit 26e50f1

File tree

3 files changed

+67
-3
lines changed

3 files changed

+67
-3
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,21 @@ Bug Fixes
8787
3.0.7 (unreleased)
8888
=======================
8989

90-
*
90+
* Change junitxml.py to produce reports that comply with Junitxml schema.
91+
If the same test fails with failure in call and then errors in teardown
92+
we split testcase element into two, one containing the error and the other
93+
the failure. (`#2228`_) Thanks to `@kkoukiou`_ for the PR.
9194

9295
*
9396

9497
*
9598

9699
*
97100

101+
.. _@kkoukiou: https://github.com/KKoukiou
102+
103+
.. _#2228: https://github.com/pytest-dev/pytest/issues/2228
104+
98105

99106
3.0.6 (2017-01-22)
100107
==================

_pytest/junitxml.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ def __init__(self, logfile, prefix):
273273
self.node_reporters = {} # nodeid -> _NodeReporter
274274
self.node_reporters_ordered = []
275275
self.global_properties = []
276+
# List of reports that failed on call but teardown is pending.
277+
self.open_reports = []
278+
self.cnt_double_fail_tests = 0
276279

277280
def finalize(self, report):
278281
nodeid = getattr(report, 'nodeid', report)
@@ -332,14 +335,33 @@ def pytest_runtest_logreport(self, report):
332335
-> teardown node2
333336
-> teardown node1
334337
"""
338+
close_report = None
335339
if report.passed:
336340
if report.when == "call": # ignore setup/teardown
337341
reporter = self._opentestcase(report)
338342
reporter.append_pass(report)
339343
elif report.failed:
344+
if report.when == "teardown":
345+
# The following vars are needed when xdist plugin is used
346+
report_wid = getattr(report, "worker_id", None)
347+
report_ii = getattr(report, "item_index", None)
348+
close_report = next(
349+
(rep for rep in self.open_reports
350+
if (rep.nodeid == report.nodeid and
351+
getattr(rep, "item_index", None) == report_ii and
352+
getattr(rep, "worker_id", None) == report_wid
353+
)
354+
), None)
355+
if close_report:
356+
# We need to open new testcase in case we have failure in
357+
# call and error in teardown in order to follow junit
358+
# schema
359+
self.finalize(close_report)
360+
self.cnt_double_fail_tests += 1
340361
reporter = self._opentestcase(report)
341362
if report.when == "call":
342363
reporter.append_failure(report)
364+
self.open_reports.append(report)
343365
else:
344366
reporter.append_error(report)
345367
elif report.skipped:
@@ -348,6 +370,17 @@ def pytest_runtest_logreport(self, report):
348370
self.update_testcase_duration(report)
349371
if report.when == "teardown":
350372
self.finalize(report)
373+
report_wid = getattr(report, "worker_id", None)
374+
report_ii = getattr(report, "item_index", None)
375+
close_report = next(
376+
(rep for rep in self.open_reports
377+
if (rep.nodeid == report.nodeid and
378+
getattr(rep, "item_index", None) == report_ii and
379+
getattr(rep, "worker_id", None) == report_wid
380+
)
381+
), None)
382+
if close_report:
383+
self.open_reports.remove(close_report)
351384

352385
def update_testcase_duration(self, report):
353386
"""accumulates total duration for nodeid from given report and updates
@@ -380,8 +413,9 @@ def pytest_sessionfinish(self):
380413
suite_stop_time = time.time()
381414
suite_time_delta = suite_stop_time - self.suite_start_time
382415

383-
numtests = self.stats['passed'] + self.stats['failure'] + self.stats['skipped'] + self.stats['error']
384-
416+
numtests = (self.stats['passed'] + self.stats['failure'] +
417+
self.stats['skipped'] + self.stats['error'] -
418+
self.cnt_double_fail_tests)
385419
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
386420

387421
logfile.write(Junit.testsuite(

testing/test_junitxml.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,29 @@ def test_function(arg):
189189
fnode.assert_attr(message="test teardown failure")
190190
assert "ValueError" in fnode.toxml()
191191

192+
def test_call_failure_teardown_error(self, testdir):
193+
testdir.makepyfile("""
194+
import pytest
195+
196+
@pytest.fixture
197+
def arg():
198+
yield
199+
raise Exception("Teardown Exception")
200+
def test_function(arg):
201+
raise Exception("Call Exception")
202+
""")
203+
result, dom = runandparse(testdir)
204+
assert result.ret
205+
node = dom.find_first_by_tag("testsuite")
206+
node.assert_attr(errors=1, failures=1, tests=1)
207+
first, second = dom.find_by_tag("testcase")
208+
if not first or not second or first == second:
209+
assert 0
210+
fnode = first.find_first_by_tag("failure")
211+
fnode.assert_attr(message="Exception: Call Exception")
212+
snode = second.find_first_by_tag("error")
213+
snode.assert_attr(message="test teardown failure")
214+
192215
def test_skip_contains_name_reason(self, testdir):
193216
testdir.makepyfile("""
194217
import pytest

0 commit comments

Comments
 (0)