Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/1140.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deduplicate identical collection errors from multiple workers while preserving different errors and errors from different modules.
8 changes: 5 additions & 3 deletions src/xdist/dsession.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __init__(self, config: pytest.Config) -> None:
self.maxfail: int = config.getvalue("maxfail")
self.queue: Queue[tuple[str, dict[str, Any]]] = Queue()
self._session: pytest.Session | None = None
self._failed_collection_errors: dict[object, bool] = {}
self._failed_collection_errors: set[tuple[str, str]] = set()
self._active_nodes: set[WorkerController] = set()
self._failed_nodes_count = 0
self._max_worker_restart = get_default_max_worker_restart(self.config)
Expand Down Expand Up @@ -410,8 +410,10 @@ def _failed_worker_collectreport(
) -> None:
# Check we haven't already seen this report (from
# another worker).
if rep.longrepr not in self._failed_collection_errors:
self._failed_collection_errors[rep.longrepr] = True
# Exception representations from different workers need not compare equal.
report_key = (rep.nodeid, str(rep.longrepr))
if report_key not in self._failed_collection_errors:
self._failed_collection_errors.add(report_key)
self.config.hook.pytest_collectreport(report=rep)
self._handlefailures(rep)

Expand Down
34 changes: 34 additions & 0 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
import shutil
from typing import cast
import xml.etree.ElementTree as ET

import pytest

Expand Down Expand Up @@ -1722,6 +1723,39 @@ def test_get_xdist_worker_id(self, fake_request: pytest.FixtureRequest) -> None:
assert xdist.get_xdist_worker_id(fake_request) == "master"


@pytest.mark.parametrize("error_source", ["same", "worker", "module"])
def test_collection_error_deduplication(
pytester: pytest.Pytester, error_source: str
) -> None:
source = "raise RuntimeError('collection failure')"
if error_source == "worker":
source = (
"import os\n"
"raise RuntimeError('failure from ' + os.environ['PYTEST_XDIST_WORKER'])"
)
pytester.makepyfile(test_bad=source)
if error_source == "module":
pytester.makepyfile(test_other=source)

result = pytester.runpytest("-n2", "--junitxml=report.xml")
errors = 1 if error_source == "same" else 2
result.assert_outcomes(errors=errors)
assert result.ret == pytest.ExitCode.TESTS_FAILED
suite = ET.parse(pytester.path / "report.xml").getroot().find("testsuite")
assert suite is not None
assert suite.get("errors") == str(errors)
reports = suite.findall(".//error")
assert len(reports) == errors
if error_source == "worker":
assert any("failure from gw0" in (report.text or "") for report in reports)
assert any("failure from gw1" in (report.text or "") for report in reports)
elif error_source == "module":
assert {case.get("name") for case in suite.findall("testcase")} == {
"test_bad",
"test_other",
}


def test_collection_crash(pytester: pytest.Pytester) -> None:
p1 = pytester.makepyfile(
"""
Expand Down
Loading