Skip to content
Draft
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 AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ Authors
* Ofek Lev - https://github.com/ofek
* Art Pelling - https://github.com/artpelling
* Markéta Machová - https://github.com/MeggyCal
* pctablet505 - https://github.com/pctablet505
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
Changelog
=========

7.1.1 (unreleased)
------------------

* Fixed a confusing ``AttributeError: 'Central' object has no attribute 'configure_node'`` crash
that could happen when another plugin (for example one driven by ``addopts`` in a config file)
enables pytest-xdist only after pytest-cov has already decided to run in centralised mode.
This now raises a clear ``DistCovError`` instead.
See `#740 <https://github.com/pytest-dev/pytest-cov/issues/740>`_.

7.1.0 (2026-03-21)
------------------

Expand Down
30 changes: 30 additions & 0 deletions src/pytest_cov/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from . import CovDisabledWarning
from . import CovReportWarning
from . import DistCovError
from . import PytestCovWarning

if TYPE_CHECKING:
Expand Down Expand Up @@ -304,6 +305,7 @@ def pytest_configure_node(self, node):
Mark this hook as optional in case xdist is not installed.
"""
if not self._disabled:
self._check_is_distributed()
self.cov_controller.configure_node(node)

@pytest.hookimpl(optionalhook=True)
Expand All @@ -313,8 +315,36 @@ def pytest_testnodedown(self, node, error):
Mark this hook as optional in case xdist is not installed.
"""
if not self._disabled:
self._check_is_distributed()
self.cov_controller.testnodedown(node, error)

def _check_is_distributed(self):
"""Guard against xdist hooks firing on a non-distributed controller.

Whether we run centralised or distributed is decided very early on, in
``pytest_load_initial_conftests``, based on the xdist options known at that
point. If some other plugin (e.g. one that enables xdist based on
``pyproject.toml``/``addopts``) only adds ``-n``/``--dist`` afterwards, we can
end up having already started a centralised controller by the time xdist
calls one of its distributed-only hooks. Recovering from that would mean
discarding coverage already collected, so we fail with a clear error instead
of a confusing AttributeError.
"""
# import engine lazily here to avoid importing
# it for unit tests that don't need it
from . import engine

if not isinstance(self.cov_controller, engine.DistMaster):
raise DistCovError(
'pytest-xdist workers were started but pytest-cov had already initialized itself for '
'centralised (non-distributed) coverage measurement. This usually happens when another '
'plugin enables pytest-xdist (e.g. via "addopts" coming from a config file or a plugin '
'like pytest-enabler) after pytest-cov has already inspected the command line. '
'Make sure the xdist options (-n/--dist) are visible on the command line or in the pytest '
'configuration file before pytest-cov starts, for example by adding them to the "addopts" '
'pytest reads directly, rather than relying on a plugin that injects them later.'
)

def _should_report(self):
needed = self.options.cov_report or self.options.cov_fail_under
return needed and not (self.failed and self.options.no_cov_on_fail)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_pytest_cov.py
Original file line number Diff line number Diff line change
Expand Up @@ -1833,6 +1833,29 @@ def test_dynamic_context(pytester, testdir, opts, prop):
)


def test_xdist_enabled_after_initial_conftests(pytester, testdir):
"""A plugin that only enables xdist after pytest-cov looked at the command
line (e.g. one that reacts to "addopts" from a config file) used to crash
with a cryptic ``AttributeError: 'Central' object has no attribute
'configure_node'``. See #740.
"""
testdir.makepyfile(test_1='def test_1():\n assert True\n')
testdir.makepyfile(
late_xdist_plugin="""
def pytest_load_initial_conftests(early_config, parser, args):
# Simulate a plugin that decides to enable xdist only after
# pytest-cov's own `tryfirst=True` pytest_load_initial_conftests
# has already run and started coverage measurement centrally.
args.extend(['-n', '2'])
"""
)
testdir.syspathinsert()
result = testdir.runpytest('-v', '--cov=.', '--cov-report=', '-p', 'late_xdist_plugin')
output = '\n'.join(result.outlines + result.errlines)
assert 'pytest_cov.DistCovError: pytest-xdist workers were started' in output
assert 'AttributeError' not in output


@xdist_params
def test_simple(pytester, testdir, opts, prop):
script = testdir.makepyfile(test_1=prop.code)
Expand Down