Merge pull request #1071 from nicoddemus/xml-xdist
Wrong xml report when used with pytest-xdist
This commit is contained in:
		
						commit
						c2c2451788
					
				|  | @ -1,6 +1,10 @@ | ||||||
| 2.8.1.dev | 2.8.1.dev | ||||||
| --------- | --------- | ||||||
| 
 | 
 | ||||||
|  | - Fix issue #1064: ""--junitxml" regression when used with the | ||||||
|  |   "pytest-xdist" plugin, with test reports being assigned to the wrong tests. | ||||||
|  |   Thanks Daniel Grunwald for the report and Bruno Oliveira for the PR. | ||||||
|  | 
 | ||||||
| - (experimental) adapt more SEMVER style versioning and change meaning of  | - (experimental) adapt more SEMVER style versioning and change meaning of  | ||||||
|   master branch in git repo: "master" branch now keeps the bugfixes, changes  |   master branch in git repo: "master" branch now keeps the bugfixes, changes  | ||||||
|   aimed for micro releases.  "features" branch will only be be released  |   aimed for micro releases.  "features" branch will only be be released  | ||||||
|  |  | ||||||
|  | @ -101,6 +101,8 @@ class LogXML(object): | ||||||
|         self.logfile = os.path.normpath(os.path.abspath(logfile)) |         self.logfile = os.path.normpath(os.path.abspath(logfile)) | ||||||
|         self.prefix = prefix |         self.prefix = prefix | ||||||
|         self.tests = [] |         self.tests = [] | ||||||
|  |         self.tests_by_nodeid = {}  # nodeid -> Junit.testcase | ||||||
|  |         self.durations = {}  # nodeid -> total duration (setup+call+teardown) | ||||||
|         self.passed = self.skipped = 0 |         self.passed = self.skipped = 0 | ||||||
|         self.failed = self.errors = 0 |         self.failed = self.errors = 0 | ||||||
|         self.custom_properties = {} |         self.custom_properties = {} | ||||||
|  | @ -117,11 +119,16 @@ class LogXML(object): | ||||||
|             "classname": ".".join(classnames), |             "classname": ".".join(classnames), | ||||||
|             "name": bin_xml_escape(names[-1]), |             "name": bin_xml_escape(names[-1]), | ||||||
|             "file": report.location[0], |             "file": report.location[0], | ||||||
|             "time": 0, |             "time": self.durations.get(report.nodeid, 0), | ||||||
|         } |         } | ||||||
|         if report.location[1] is not None: |         if report.location[1] is not None: | ||||||
|             attrs["line"] = report.location[1] |             attrs["line"] = report.location[1] | ||||||
|         self.tests.append(Junit.testcase(**attrs)) |         testcase = Junit.testcase(**attrs) | ||||||
|  |         custom_properties = self.pop_custom_properties() | ||||||
|  |         if custom_properties: | ||||||
|  |             testcase.append(custom_properties) | ||||||
|  |         self.tests.append(testcase) | ||||||
|  |         self.tests_by_nodeid[report.nodeid] = testcase | ||||||
| 
 | 
 | ||||||
|     def _write_captured_output(self, report): |     def _write_captured_output(self, report): | ||||||
|         for capname in ('out', 'err'): |         for capname in ('out', 'err'): | ||||||
|  | @ -136,17 +143,20 @@ class LogXML(object): | ||||||
|     def append(self, obj): |     def append(self, obj): | ||||||
|         self.tests[-1].append(obj) |         self.tests[-1].append(obj) | ||||||
| 
 | 
 | ||||||
|     def append_custom_properties(self): |     def pop_custom_properties(self): | ||||||
|  |         """Return a Junit node containing custom properties set for | ||||||
|  |         the current test, if any, and reset the current custom properties. | ||||||
|  |         """ | ||||||
|         if self.custom_properties: |         if self.custom_properties: | ||||||
|             self.tests[-1].append( |             result = Junit.properties( | ||||||
|                 Junit.properties( |  | ||||||
|                 [ |                 [ | ||||||
|                     Junit.property(name=name, value=value) |                     Junit.property(name=name, value=value) | ||||||
|                     for name, value in self.custom_properties.items() |                     for name, value in self.custom_properties.items() | ||||||
|                 ] |                 ] | ||||||
|             ) |             ) | ||||||
|             ) |  | ||||||
|             self.custom_properties.clear() |             self.custom_properties.clear() | ||||||
|  |             return result | ||||||
|  |         return None | ||||||
| 
 | 
 | ||||||
|     def append_pass(self, report): |     def append_pass(self, report): | ||||||
|         self.passed += 1 |         self.passed += 1 | ||||||
|  | @ -206,20 +216,54 @@ class LogXML(object): | ||||||
|         self._write_captured_output(report) |         self._write_captured_output(report) | ||||||
| 
 | 
 | ||||||
|     def pytest_runtest_logreport(self, report): |     def pytest_runtest_logreport(self, report): | ||||||
|         if report.when == "setup": |         """handle a setup/call/teardown report, generating the appropriate | ||||||
|             self._opentestcase(report) |         xml tags as necessary. | ||||||
|         self.tests[-1].attr.time += getattr(report, 'duration', 0) | 
 | ||||||
|         self.append_custom_properties() |         note: due to plugins like xdist, this hook may be called in interlaced | ||||||
|  |         order with reports from other nodes. for example: | ||||||
|  | 
 | ||||||
|  |         usual call order: | ||||||
|  |             -> setup node1 | ||||||
|  |             -> call node1 | ||||||
|  |             -> teardown node1 | ||||||
|  |             -> setup node2 | ||||||
|  |             -> call node2 | ||||||
|  |             -> teardown node2 | ||||||
|  | 
 | ||||||
|  |         possible call order in xdist: | ||||||
|  |             -> setup node1 | ||||||
|  |             -> call node1 | ||||||
|  |             -> setup node2 | ||||||
|  |             -> call node2 | ||||||
|  |             -> teardown node2 | ||||||
|  |             -> teardown node1 | ||||||
|  |         """ | ||||||
|         if report.passed: |         if report.passed: | ||||||
|             if report.when == "call":  # ignore setup/teardown |             if report.when == "call":  # ignore setup/teardown | ||||||
|  |                 self._opentestcase(report) | ||||||
|                 self.append_pass(report) |                 self.append_pass(report) | ||||||
|         elif report.failed: |         elif report.failed: | ||||||
|  |             self._opentestcase(report) | ||||||
|             if report.when != "call": |             if report.when != "call": | ||||||
|                 self.append_error(report) |                 self.append_error(report) | ||||||
|             else: |             else: | ||||||
|                 self.append_failure(report) |                 self.append_failure(report) | ||||||
|         elif report.skipped: |         elif report.skipped: | ||||||
|  |             self._opentestcase(report) | ||||||
|             self.append_skipped(report) |             self.append_skipped(report) | ||||||
|  |         self.update_testcase_duration(report) | ||||||
|  | 
 | ||||||
|  |     def update_testcase_duration(self, report): | ||||||
|  |         """accumulates total duration for nodeid from given report and updates | ||||||
|  |         the Junit.testcase with the new total if already created. | ||||||
|  |         """ | ||||||
|  |         total = self.durations.get(report.nodeid, 0.0) | ||||||
|  |         total += getattr(report, 'duration', 0.0) | ||||||
|  |         self.durations[report.nodeid] = total | ||||||
|  | 
 | ||||||
|  |         testcase = self.tests_by_nodeid.get(report.nodeid) | ||||||
|  |         if testcase is not None: | ||||||
|  |             testcase.attr.time = total | ||||||
| 
 | 
 | ||||||
|     def pytest_collectreport(self, report): |     def pytest_collectreport(self, report): | ||||||
|         if not report.passed: |         if not report.passed: | ||||||
|  |  | ||||||
|  | @ -186,6 +186,8 @@ This will add an extra property ``example_key="1"`` to the generated | ||||||
|     by something more powerful and general in future versions. The |     by something more powerful and general in future versions. The | ||||||
|     functionality per-se will be kept, however. |     functionality per-se will be kept, however. | ||||||
| 
 | 
 | ||||||
|  |     Currently it does not work when used with the ``pytest-xdist`` plugin. | ||||||
|  | 
 | ||||||
|     Also please note that using this feature will break any schema verification. |     Also please note that using this feature will break any schema verification. | ||||||
|     This might be a problem when used with some CI servers. |     This might be a problem when used with some CI servers. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ from xml.dom import minidom | ||||||
| from _pytest.main import EXIT_NOTESTSCOLLECTED | from _pytest.main import EXIT_NOTESTSCOLLECTED | ||||||
| import py, sys, os | import py, sys, os | ||||||
| from _pytest.junitxml import LogXML | from _pytest.junitxml import LogXML | ||||||
|  | import pytest | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def runandparse(testdir, *args): | def runandparse(testdir, *args): | ||||||
|     resultpath = testdir.tmpdir.join("junit.xml") |     resultpath = testdir.tmpdir.join("junit.xml") | ||||||
|  | @ -553,6 +555,7 @@ def test_unicode_issue368(testdir): | ||||||
|     log.append_skipped(report) |     log.append_skipped(report) | ||||||
|     log.pytest_sessionfinish() |     log.pytest_sessionfinish() | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def test_record_property(testdir): | def test_record_property(testdir): | ||||||
|     testdir.makepyfile(""" |     testdir.makepyfile(""" | ||||||
|         def test_record(record_xml_property): |         def test_record(record_xml_property): | ||||||
|  | @ -565,3 +568,25 @@ def test_record_property(testdir): | ||||||
|     pnode = psnode.getElementsByTagName('property')[0] |     pnode = psnode.getElementsByTagName('property')[0] | ||||||
|     assert_attr(pnode, name="foo", value="<1") |     assert_attr(pnode, name="foo", value="<1") | ||||||
|     result.stdout.fnmatch_lines('*C3*test_record_property.py*experimental*') |     result.stdout.fnmatch_lines('*C3*test_record_property.py*experimental*') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_random_report_log_xdist(testdir): | ||||||
|  |     """xdist calls pytest_runtest_logreport as they are executed by the slaves, | ||||||
|  |     with nodes from several nodes overlapping, so junitxml must cope with that | ||||||
|  |     to produce correct reports. #1064 | ||||||
|  |     """ | ||||||
|  |     pytest.importorskip('xdist') | ||||||
|  |     testdir.makepyfile(""" | ||||||
|  |         import pytest, time | ||||||
|  |         @pytest.mark.parametrize('i', list(range(30))) | ||||||
|  |         def test_x(i): | ||||||
|  |             assert i != 22 | ||||||
|  |     """) | ||||||
|  |     _, dom = runandparse(testdir, '-n2') | ||||||
|  |     suite_node = dom.getElementsByTagName("testsuite")[0] | ||||||
|  |     failed = [] | ||||||
|  |     for case_node in suite_node.getElementsByTagName("testcase"): | ||||||
|  |         if case_node.getElementsByTagName('failure'): | ||||||
|  |             failed.append(case_node.getAttributeNode('name').value) | ||||||
|  | 
 | ||||||
|  |     assert failed == ['test_x[22]'] | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue