Skip to content

Commit c2c2451

Browse files
Merge pull request #1071 from nicoddemus/xml-xdist
Wrong xml report when used with pytest-xdist
2 parents bc501a2 + e4b18ea commit c2c2451

File tree

4 files changed

+91
-16
lines changed

4 files changed

+91
-16
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
2.8.1.dev
22
---------
33

4+
- Fix issue #1064: ""--junitxml" regression when used with the
5+
"pytest-xdist" plugin, with test reports being assigned to the wrong tests.
6+
Thanks Daniel Grunwald for the report and Bruno Oliveira for the PR.
7+
48
- (experimental) adapt more SEMVER style versioning and change meaning of
59
master branch in git repo: "master" branch now keeps the bugfixes, changes
610
aimed for micro releases. "features" branch will only be be released

_pytest/junitxml.py

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def __init__(self, logfile, prefix):
101101
self.logfile = os.path.normpath(os.path.abspath(logfile))
102102
self.prefix = prefix
103103
self.tests = []
104+
self.tests_by_nodeid = {} # nodeid -> Junit.testcase
105+
self.durations = {} # nodeid -> total duration (setup+call+teardown)
104106
self.passed = self.skipped = 0
105107
self.failed = self.errors = 0
106108
self.custom_properties = {}
@@ -117,11 +119,16 @@ def _opentestcase(self, report):
117119
"classname": ".".join(classnames),
118120
"name": bin_xml_escape(names[-1]),
119121
"file": report.location[0],
120-
"time": 0,
122+
"time": self.durations.get(report.nodeid, 0),
121123
}
122124
if report.location[1] is not None:
123125
attrs["line"] = report.location[1]
124-
self.tests.append(Junit.testcase(**attrs))
126+
testcase = Junit.testcase(**attrs)
127+
custom_properties = self.pop_custom_properties()
128+
if custom_properties:
129+
testcase.append(custom_properties)
130+
self.tests.append(testcase)
131+
self.tests_by_nodeid[report.nodeid] = testcase
125132

126133
def _write_captured_output(self, report):
127134
for capname in ('out', 'err'):
@@ -136,17 +143,20 @@ def _write_captured_output(self, report):
136143
def append(self, obj):
137144
self.tests[-1].append(obj)
138145

139-
def append_custom_properties(self):
146+
def pop_custom_properties(self):
147+
"""Return a Junit node containing custom properties set for
148+
the current test, if any, and reset the current custom properties.
149+
"""
140150
if self.custom_properties:
141-
self.tests[-1].append(
142-
Junit.properties(
143-
[
144-
Junit.property(name=name, value=value)
145-
for name, value in self.custom_properties.items()
146-
]
147-
)
151+
result = Junit.properties(
152+
[
153+
Junit.property(name=name, value=value)
154+
for name, value in self.custom_properties.items()
155+
]
148156
)
149-
self.custom_properties.clear()
157+
self.custom_properties.clear()
158+
return result
159+
return None
150160

151161
def append_pass(self, report):
152162
self.passed += 1
@@ -206,20 +216,54 @@ def append_skipped(self, report):
206216
self._write_captured_output(report)
207217

208218
def pytest_runtest_logreport(self, report):
209-
if report.when == "setup":
210-
self._opentestcase(report)
211-
self.tests[-1].attr.time += getattr(report, 'duration', 0)
212-
self.append_custom_properties()
219+
"""handle a setup/call/teardown report, generating the appropriate
220+
xml tags as necessary.
221+
222+
note: due to plugins like xdist, this hook may be called in interlaced
223+
order with reports from other nodes. for example:
224+
225+
usual call order:
226+
-> setup node1
227+
-> call node1
228+
-> teardown node1
229+
-> setup node2
230+
-> call node2
231+
-> teardown node2
232+
233+
possible call order in xdist:
234+
-> setup node1
235+
-> call node1
236+
-> setup node2
237+
-> call node2
238+
-> teardown node2
239+
-> teardown node1
240+
"""
213241
if report.passed:
214-
if report.when == "call": # ignore setup/teardown
242+
if report.when == "call": # ignore setup/teardown
243+
self._opentestcase(report)
215244
self.append_pass(report)
216245
elif report.failed:
246+
self._opentestcase(report)
217247
if report.when != "call":
218248
self.append_error(report)
219249
else:
220250
self.append_failure(report)
221251
elif report.skipped:
252+
self._opentestcase(report)
222253
self.append_skipped(report)
254+
self.update_testcase_duration(report)
255+
256+
def update_testcase_duration(self, report):
257+
"""accumulates total duration for nodeid from given report and updates
258+
the Junit.testcase with the new total if already created.
259+
"""
260+
total = self.durations.get(report.nodeid, 0.0)
261+
total += getattr(report, 'duration', 0.0)
262+
self.durations[report.nodeid] = total
263+
264+
testcase = self.tests_by_nodeid.get(report.nodeid)
265+
if testcase is not None:
266+
testcase.attr.time = total
223267

224268
def pytest_collectreport(self, report):
225269
if not report.passed:

doc/en/usage.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ This will add an extra property ``example_key="1"`` to the generated
186186
by something more powerful and general in future versions. The
187187
functionality per-se will be kept, however.
188188

189+
Currently it does not work when used with the ``pytest-xdist`` plugin.
190+
189191
Also please note that using this feature will break any schema verification.
190192
This might be a problem when used with some CI servers.
191193

testing/test_junitxml.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from _pytest.main import EXIT_NOTESTSCOLLECTED
55
import py, sys, os
66
from _pytest.junitxml import LogXML
7+
import pytest
8+
79

810
def runandparse(testdir, *args):
911
resultpath = testdir.tmpdir.join("junit.xml")
@@ -553,6 +555,7 @@ class Report(BaseReport):
553555
log.append_skipped(report)
554556
log.pytest_sessionfinish()
555557

558+
556559
def test_record_property(testdir):
557560
testdir.makepyfile("""
558561
def test_record(record_xml_property):
@@ -565,3 +568,25 @@ def test_record(record_xml_property):
565568
pnode = psnode.getElementsByTagName('property')[0]
566569
assert_attr(pnode, name="foo", value="<1")
567570
result.stdout.fnmatch_lines('*C3*test_record_property.py*experimental*')
571+
572+
573+
def test_random_report_log_xdist(testdir):
574+
"""xdist calls pytest_runtest_logreport as they are executed by the slaves,
575+
with nodes from several nodes overlapping, so junitxml must cope with that
576+
to produce correct reports. #1064
577+
"""
578+
pytest.importorskip('xdist')
579+
testdir.makepyfile("""
580+
import pytest, time
581+
@pytest.mark.parametrize('i', list(range(30)))
582+
def test_x(i):
583+
assert i != 22
584+
""")
585+
_, dom = runandparse(testdir, '-n2')
586+
suite_node = dom.getElementsByTagName("testsuite")[0]
587+
failed = []
588+
for case_node in suite_node.getElementsByTagName("testcase"):
589+
if case_node.getElementsByTagName('failure'):
590+
failed.append(case_node.getAttributeNode('name').value)
591+
592+
assert failed == ['test_x[22]']

0 commit comments

Comments
 (0)