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
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
=========

Unreleased
----------

* Coverage's configuration file (and the data file) are now resolved relative to
pytest's rootdir instead of the current working directory, so running pytest
from a subdirectory no longer ignores the project's ``pyproject.toml`` /
``setup.cfg`` / ``tox.ini`` / ``.coveragerc``.
See `#692 <https://github.com/pytest-dev/pytest-cov/issues/692>`_.

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

Expand Down
19 changes: 16 additions & 3 deletions src/pytest_cov/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,14 @@ def ensure_topdir_wrapper(self, *args, **kwargs):
class CovController:
"""Base class for different plugin implementations."""

def __init__(self, options: argparse.Namespace, config: Union[None, object], nodeid: Union[None, str]):
"""Get some common config used by multiple derived classes."""
def __init__(
self,
options: argparse.Namespace,
config: Union[None, object],
nodeid: Union[None, str],
rootdir: Union[None, str, Path] = None,
):
"""Get some common config used to get to the various derived classes."""
self.cov_source = options.cov_source
self.cov_report = options.cov_report
self.cov_config = options.cov_config
Expand All @@ -68,7 +74,14 @@ def __init__(self, options: argparse.Namespace, config: Union[None, object], nod
self.data_file = None
self.node_descs = set()
self.failed_workers = []
self.topdir = os.fspath(Path.cwd())
# Coverage resolves a relative ``config_file`` (the default
# ``.coveragerc``, and the ``pyproject.toml`` / ``setup.cfg`` /
# ``tox.ini`` fallbacks) and a relative ``data_file`` against the
# current working directory. pytest's rootdir is what the user means by
# "the project", so anchor to it when we know it -- otherwise running
# from a subdirectory silently ignores the project's coverage config
# (#692). It only differs from ``cwd`` in exactly that case.
self.topdir = os.fspath(rootdir) if rootdir is not None else os.fspath(Path.cwd())
self.is_collocated = None
self.started = False

Expand Down
21 changes: 18 additions & 3 deletions src/pytest_cov/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,11 @@ def pytest_load_initial_conftests(early_config, parser, args):
break

if early_config.known_args_namespace.cov_source:
plugin = CovPlugin(options, early_config.pluginmanager)
plugin = CovPlugin(
options,
early_config.pluginmanager,
rootdir=getattr(early_config, 'rootpath', None),
)
early_config.pluginmanager.register(plugin, '_cov')


Expand All @@ -212,7 +216,14 @@ class CovPlugin:
distributed worker.
"""

def __init__(self, options: argparse.Namespace, pluginmanager, start=True, no_cov_should_warn=False):
def __init__(
self,
options: argparse.Namespace,
pluginmanager,
start=True,
no_cov_should_warn=False,
rootdir=None,
):
"""Creates a coverage pytest plugin.

We read the rc file that coverage uses to get the data file
Expand All @@ -231,6 +242,10 @@ def __init__(self, options: argparse.Namespace, pluginmanager, start=True, no_co
self._disabled = False
self.options = options
self._wrote_heading = False
# pytest's rootdir, used by the controllers to anchor coverage's
# config/data file lookup to the project root rather than the current
# working directory (#692).
self.rootdir = rootdir

is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no'
if getattr(options, 'no_cov', False):
Expand Down Expand Up @@ -262,7 +277,7 @@ class Config:

config = Config()

self.cov_controller = controller_cls(self.options, config, nodeid)
self.cov_controller = controller_cls(self.options, config, nodeid, self.rootdir)
self.cov_controller.start()
self._started = True
self._start_path = Path.cwd()
Expand Down
16 changes: 16 additions & 0 deletions tests/test_pytest_cov.py
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,22 @@ def test_pyproject_toml(testdir):
result.stdout.fnmatch_lines([f'test_pyproject_toml* {EXCLUDED_RESULT}'])


def test_config_at_rootdir_used_from_subdirectory(testdir, monkeypatch):
# The project's coverage config lives at the rootdir; running pytest from a
# subdirectory must still pick it up, instead of looking only in the cwd
# (#692).
testdir.makefile('.toml', pyproject=PYPROJECTTOML)
subdir = testdir.mkdir('subdir')
script = subdir.join('test_in_subdir.py')
script.write(EXCLUDED_TEST)
monkeypatch.chdir(subdir)
result = testdir.runpytest(
'-v', f'--cov={subdir}', '--cov-report=term-missing', 'test_in_subdir.py'
)
assert result.ret == 0
result.stdout.fnmatch_lines([f'*test_in_subdir* {EXCLUDED_RESULT}'])


@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')
def test_coveragerc_dist(testdir):
testdir.makefile('', coveragerc=COVERAGERC)
Expand Down