Skip to content

coverage collection fails when using pytest-xdist and pytest 8.4.0 #693

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
rjdbcm opened this issue Jun 2, 2025 · 34 comments · May be fixed by #695
Open

coverage collection fails when using pytest-xdist and pytest 8.4.0 #693

rjdbcm opened this issue Jun 2, 2025 · 34 comments · May be fixed by #695

Comments

@rjdbcm
Copy link

rjdbcm commented Jun 2, 2025

Summary

pytest-xdist and pytest-cov plugins cause an internal error when used together but only on pytest version 8.4.0

Expected vs actual result

Expected pytest-xdist+pytest-cov to succeed.
See the following chart for the actual results:

⬇ pytest ♦ plugins ➡ pytest-xdist+pytest-cov pytest-cov pytest-xdist
pytest < 8.4.0
pytest == 8.4.0
with the following output:
...
created: 16/16 workers
16 workers [1 item]

scheduling tests via LoadScheduling

tests/test_metadata.py::test_metadata 
INTERNALERROR> def worker_internal_error(
INTERNALERROR>         self, node: WorkerController, formatted_error: str
INTERNALERROR>     ) -> None:
INTERNALERROR>         """
INTERNALERROR>         pytest_internalerror() was called on the worker.
INTERNALERROR>     
INTERNALERROR>         pytest_internalerror() arguments are an excinfo and an excrepr, which can't
INTERNALERROR>         be serialized, so we go with a poor man's solution of raising an exception
INTERNALERROR>         here ourselves using the formatted message.
INTERNALERROR>         """
INTERNALERROR>         self._active_nodes.remove(node)
INTERNALERROR>         try:
INTERNALERROR> >           assert False, formatted_error
INTERNALERROR> E           AssertionError: Traceback (most recent call last):
INTERNALERROR> E               File " /installdir/pluggy/_callers.py", line 43, in run_old_style_hookwrapper
INTERNALERROR> E                 teardown.send(result)
INTERNALERROR> E                 ~~~~~~~~~~~~~^^^^^^^^
INTERNALERROR> E               File " /installdir/pytest_cov/plugin.py", line 324, in pytest_runtestloop
INTERNALERROR> E                 self.cov_controller.finish()
INTERNALERROR> E                 ~~~~~~~~~~~~~~~~~~~~~~~~~~^^
INTERNALERROR> E               File " /installdir/pytest_cov/engine.py", line 57, in ensure_topdir_wrapper
INTERNALERROR> E                 return meth(self, *args, **kwargs)
INTERNALERROR> E               File " /installdir/pytest_cov/engine.py", line 469, in finish
INTERNALERROR> E                 self.cov.save()
INTERNALERROR> E                 ~~~~~~~~~~~~~^^
INTERNALERROR> E               File " /installdir/coverage/control.py", line 812, in save
INTERNALERROR> E                 data = self.get_data()
INTERNALERROR> E               File " /installdir/coverage/control.py", line 893, in get_data
INTERNALERROR> E                 self._post_save_work()
INTERNALERROR> E                 ~~~~~~~~~~~~~~~~~~~~^^
INTERNALERROR> E               File " /installdir/coverage/control.py", line 915, in _post_save_work
INTERNALERROR> E                 self._warn("No data was collected.", slug="no-data-collected")
INTERNALERROR> E                 ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR> E               File " /installdir/coverage/control.py", line 460, in _warn
INTERNALERROR> E                 warnings.warn(msg, category=CoverageWarning, stacklevel=2)
INTERNALERROR> E                 ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR> E             coverage.exceptions.CoverageWarning: No data was collected. (no-data-collected)
INTERNALERROR> E             
INTERNALERROR> E             During handling of the above exception, another exception occurred:
INTERNALERROR> E             
INTERNALERROR> E             Traceback (most recent call last):
INTERNALERROR> E               File " /installdir/_pytest/main.py", line 289, in wrap_session
INTERNALERROR> E                 session.exitstatus = doit(config, session) or 0
INTERNALERROR> E                                      ~~~~^^^^^^^^^^^^^^^^^
INTERNALERROR> E               File " /installdir/_pytest/main.py", line 343, in _main
INTERNALERROR> E                 config.hook.pytest_runtestloop(session=session)
INTERNALERROR> E                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
INTERNALERROR> E               File " /installdir/pluggy/_hooks.py", line 512, in __call__
INTERNALERROR> E                 return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
INTERNALERROR> E                        ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR> E               File " /installdir/pluggy/_manager.py", line 120, in _hookexec
INTERNALERROR> E                 return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR> E                        ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR> E               File " /installdir/pluggy/_callers.py", line 167, in _multicall
INTERNALERROR> E                 raise exception
INTERNALERROR> E               File " /installdir/pluggy/_callers.py", line 139, in _multicall
INTERNALERROR> E                 teardown.throw(exception)
INTERNALERROR> E                 ~~~~~~~~~~~~~~^^^^^^^^^^^
INTERNALERROR> E               File " /installdir/_pytest/logging.py", line 801, in pytest_runtestloop
INTERNALERROR> E                 return (yield)  # Run all the tests.
INTERNALERROR> E                         ^^^^^
INTERNALERROR> E               File " /installdir/pluggy/_callers.py", line 139, in _multicall
INTERNALERROR> E                 teardown.throw(exception)
INTERNALERROR> E                 ~~~~~~~~~~~~~~^^^^^^^^^^^
INTERNALERROR> E               File " /installdir/_pytest/terminal.py", line 685, in pytest_runtestloop
INTERNALERROR> E                 result = yield
INTERNALERROR> E                          ^^^^^
INTERNALERROR> E               File " /installdir/pluggy/_callers.py", line 152, in _multicall
INTERNALERROR> E                 teardown.send(result)
INTERNALERROR> E                 ~~~~~~~~~~~~~^^^^^^^^
INTERNALERROR> E               File " /installdir/pluggy/_callers.py", line 47, in run_old_style_hookwrapper
INTERNALERROR> E                 _warn_teardown_exception(hook_name, hook_impl, e)
INTERNALERROR> E                 ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR> E               File " /installdir/pluggy/_callers.py", line 73, in _warn_teardown_exception
INTERNALERROR> E                 warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=6)
INTERNALERROR> E                 ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR> E             pluggy.PluggyTeardownRaisedWarning: A plugin raised an exception during an old-style hookwrapper teardown.
INTERNALERROR> E             Plugin: _cov, Hook: pytest_runtestloop
INTERNALERROR> E             CoverageWarning: No data was collected. (no-data-collected)
INTERNALERROR> E             For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning
INTERNALERROR> E           assert False
INTERNALERROR> 
INTERNALERROR> /installdir/xdist/dsession.py:232: AssertionError

ERROR: Coverage failure: total of 0 is less than fail-under=100
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File " /installdir/_pytest/main.py", line 289, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>                          ~~~~^^^^^^^^^^^^^^^^^
INTERNALERROR>   File " /installdir/_pytest/main.py", line 343, in _main
INTERNALERROR>     config.hook.pytest_runtestloop(session=session)
INTERNALERROR>     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
INTERNALERROR>   File " /installdir/pluggy/_hooks.py", line 512, in __call__
INTERNALERROR>     return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
INTERNALERROR>            ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File " /installdir/pluggy/_manager.py", line 120, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>            ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File " /installdir/pluggy/_callers.py", line 167, in _multicall
INTERNALERROR>     raise exception
INTERNALERROR>   File " /installdir/pluggy/_callers.py", line 139, in _multicall
INTERNALERROR>     teardown.throw(exception)
INTERNALERROR>     ~~~~~~~~~~~~~~^^^^^^^^^^^
INTERNALERROR>   File " /installdir/_pytest/logging.py", line 801, in pytest_runtestloop
INTERNALERROR>     return (yield)  # Run all the tests.
INTERNALERROR>             ^^^^^
INTERNALERROR>   File " /installdir/pluggy/_callers.py", line 139, in _multicall
INTERNALERROR>     teardown.throw(exception)
INTERNALERROR>     ~~~~~~~~~~~~~~^^^^^^^^^^^
INTERNALERROR>   File " /installdir/_pytest/terminal.py", line 685, in pytest_runtestloop
INTERNALERROR>     result = yield
INTERNALERROR>              ^^^^^
INTERNALERROR>   File " /installdir/pluggy/_callers.py", line 139, in _multicall
INTERNALERROR>     teardown.throw(exception)
INTERNALERROR>     ~~~~~~~~~~~~~~^^^^^^^^^^^
INTERNALERROR>   File " /installdir/pluggy/_callers.py", line 53, in run_old_style_hookwrapper
INTERNALERROR>     return result.get_result()
INTERNALERROR>            ~~~~~~~~~~~~~~~~~^^
INTERNALERROR>   File " /installdir/pluggy/_result.py", line 103, in get_result
INTERNALERROR>     raise exc.with_traceback(tb)
INTERNALERROR>   File " /installdir/pluggy/_callers.py", line 38, in run_old_style_hookwrapper
INTERNALERROR>     res = yield
INTERNALERROR>           ^^^^^
INTERNALERROR>   File " /installdir/pluggy/_callers.py", line 121, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File " /installdir/xdist/dsession.py", line 138, in pytest_runtestloop
INTERNALERROR>     self.loop_once()
INTERNALERROR>     ~~~~~~~~~~~~~~^^
INTERNALERROR>   File " /installdir/xdist/dsession.py", line 163, in loop_once
INTERNALERROR>     call(**kwargs)
INTERNALERROR>     ~~~~^^^^^^^^^^
INTERNALERROR>   File " /installdir/xdist/dsession.py", line 218, in worker_workerfinished
INTERNALERROR>     self._active_nodes.remove(node)
INTERNALERROR>     ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
INTERNALERROR> KeyError: <WorkerController gw11>

Reproducer

seems to fail for any test I try given the above conditions

Versions

platform linux -- Python 3.13.3, pytest-8.4.0, pluggy-1.6.0

Config

tox config
[tool.tox]
legacy_tox_ini = """
[tox]
skipsdist = True
env_list =
     dist
     lint
     test

[gh]
python =
     3.12 = dist,lint,test
     3.11 = dist,lint,test
     3.10 = dist,lint,test

[testenv]
allowlist_externals = 
    rm
    pipx
    meson
    python
package = wheel
deps =
     uv
commands_pre =
     uv pip install --color=never --no-progress OZI.build[uv,core]~=2.0.7
     uv tool install --python={env_python} --force meson
commands =
     meson setup {env_tmp_dir} -D{env_name}=enabled -Dtox-env-dir={env_dir}
     meson compile -C {env_tmp_dir}
     rm -rf {env_tmp_dir}/.gitignore
commands_post =
     {env_python} -m invoke --search-root={env_tmp_dir}/ozi checkpoint --suite={env_name} --ozi {posargs}

[testenv:dist]
description = OZI distribution checkpoint

[testenv:lint]
description = OZI format/lint checkpoint

[testenv:test]
description = OZI unit tests checkpoint
commands =
     meson setup {env_tmp_dir} -Dozi-blastpipe=disabled -Dtest=enabled -Dtox-env-dir={env_dir}
     meson compile -C {env_tmp_dir}
     rm -rf {env_tmp_dir}/.gitignore

[testenv:fix]
description = OZI project fix issues utility (black, isort, autoflake, ruff)
deps = uv
skip_install = true
commands_pre =
commands =
     uv tool run --python {env_python} black -S .
     uv tool run --python {env_python} isort .
     uv tool run --python {env_python} autoflake -i -r .
commands_post =

[testenv:scm]
description = OZI supply chain management (setuptools_scm)
commands =
     {env_python} -m setuptools_scm {posargs}
commands_post =

[testenv:invoke]
description = OZI invoke task entrypoint, for more info use "tox -e invoke -- --list"
no_package = true
commands_post =
     {env_python} -m invoke --search-root={env_tmp_dir}/ozi {posargs} --ozi
"""
pytest config
[tool.pytest.ini_options]  #[tool.pytest] # This will be used by pytest in the future
filterwarnings      = [
"error",
"ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning",
]
asyncio_mode = "auto"
log_cli = true
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
log_cli_format = "%(asctime)s [%(levelname)8s] %(name)s: %(message)s (%(filename)s:%(lineno)s)"
log_cli_level = "INFO"

My failing test

import sys
import warnings


def test_metadata() -> None:
    if sys.version_info < (3, 11):
        warnings.filterwarnings('ignore')
        from ozi_spec import METADATA
    else:
        from ozi_spec import METADATA
    METADATA.asdict()
@rjdbcm rjdbcm changed the title coverage fails when using pytest-xdist and pytest 8.4.0 coverage collection fails when using pytest-xdist and pytest 8.4.0 Jun 2, 2025
@dycw
Copy link

dycw commented Jun 3, 2025

Thanks, was tracking this down on my own side too. I use a combination of xdist and asyncio and cov, and was trying to push to the latest version of pytest before running into this.

@rjdbcm
Copy link
Author

rjdbcm commented Jun 3, 2025

interesting, all of my test setups are similar, will check if taking asyncio out of the equation changes anything

@fzimmermann89
Copy link

xdist + cov results in a module-not-measured warning. this was also the case in 8.3.0
new is, that this warning now causes the internalerror (and broke our CI).

[tool.coverage.run]
disable_warnings = ["module-not-measured"]

in pyproject.toml (or whereever you configure pytest-cov) silences the warning and makes the tests pass again. we still get good coverage reports out of it.

@johnthagen
Copy link

johnthagen commented Jun 4, 2025

After upgrading to pytest 8.4.0, our test suite also started failing, though with a different traceback.

@fzimmermann89's solution of disable_warnings = ["module-not-measured"] did not suppress the INTERNALERROR for us.

Platform: macOS ARM64 (also fails in containerized Linux x86 CI)

============================================================================================================================== test session starts ==============================================================================================================================
platform darwin -- Python 3.12.10, pytest-8.4.0, pluggy-1.6.0
Using --randomly-seed=1977618935
rootdir: <MYAPP>
configfile: pyproject.toml
plugins: xdist-3.7.0, randomly-3.16.0, anyio-4.9.0, mock-3.14.1, cov-6.1.1
16 workers [405 items]    
.......INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/coverage/sqldata.py", line 293, in _read_db
INTERNALERROR>     assert row is not None
INTERNALERROR>            ^^^^^^^^^^^^^^^
INTERNALERROR> AssertionError
INTERNALERROR> 
INTERNALERROR> The above exception was the direct cause of the following exception:
INTERNALERROR> 
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/coverage/data.py", line 176, in combine_parallel_data
INTERNALERROR>     new_data.read()
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/coverage/sqldata.py", line 845, in read
INTERNALERROR>     with self._connect():
INTERNALERROR>          ^^^^^^^^^^^^^^^
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/coverage/sqldata.py", line 343, in _connect
INTERNALERROR>     self._open_db()
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/coverage/sqldata.py", line 286, in _open_db
INTERNALERROR>     self._read_db()
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/coverage/sqldata.py", line 298, in _read_db
INTERNALERROR>     raise DataError(
INTERNALERROR> coverage.exceptions.DataError: Data file '.coverage.laptop.5693.XYEgnKpx' doesn't seem to be a coverage data file: 
INTERNALERROR> 
INTERNALERROR> During handling of the above exception, another exception occurred:
INTERNALERROR> 
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pluggy/_callers.py", line 43, in run_old_style_hookwrapper
INTERNALERROR>     teardown.send(result)
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pytest_cov/plugin.py", line 324, in pytest_runtestloop
INTERNALERROR>     self.cov_controller.finish()
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pytest_cov/engine.py", line 57, in ensure_topdir_wrapper
INTERNALERROR>     return meth(self, *args, **kwargs)
INTERNALERROR>            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pytest_cov/engine.py", line 421, in finish
INTERNALERROR>     self.cov.combine()
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/coverage/control.py", line 864, in combine
INTERNALERROR>     combine_parallel_data(
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/coverage/data.py", line 181, in combine_parallel_data
INTERNALERROR>     data._warn(str(exc))
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/coverage/control.py", line 460, in _warn
INTERNALERROR>     warnings.warn(msg, category=CoverageWarning, stacklevel=2)
INTERNALERROR> coverage.exceptions.CoverageWarning: Data file '.coverage.laptop.5693.XYEgnKpx' doesn't seem to be a coverage data file: 
INTERNALERROR> 
INTERNALERROR> During handling of the above exception, another exception occurred:
INTERNALERROR> 
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/_pytest/main.py", line 289, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>                          ^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/_pytest/main.py", line 343, in _main
INTERNALERROR>     config.hook.pytest_runtestloop(session=session)
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pluggy/_hooks.py", line 512, in __call__
INTERNALERROR>     return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
INTERNALERROR>            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pluggy/_manager.py", line 120, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pluggy/_callers.py", line 167, in _multicall
INTERNALERROR>     raise exception
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pluggy/_callers.py", line 139, in _multicall
INTERNALERROR>     teardown.throw(exception)
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/_pytest/logging.py", line 801, in pytest_runtestloop
INTERNALERROR>     return (yield)  # Run all the tests.
INTERNALERROR>             ^^^^^
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pluggy/_callers.py", line 139, in _multicall
INTERNALERROR>     teardown.throw(exception)
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/_pytest/terminal.py", line 685, in pytest_runtestloop
INTERNALERROR>     result = yield
INTERNALERROR>              ^^^^^
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pluggy/_callers.py", line 152, in _multicall
INTERNALERROR>     teardown.send(result)
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pluggy/_callers.py", line 47, in run_old_style_hookwrapper
INTERNALERROR>     _warn_teardown_exception(hook_name, hook_impl, e)
INTERNALERROR>   File ".nox/test-3-12/lib/python3.12/site-packages/pluggy/_callers.py", line 73, in _warn_teardown_exception
INTERNALERROR>     warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=6)
INTERNALERROR> pluggy.PluggyTeardownRaisedWarning: A plugin raised an exception during an old-style hookwrapper teardown.
INTERNALERROR> Plugin: _cov, Hook: pytest_runtestloop
INTERNALERROR> CoverageWarning: Data file '.coverage.laptop.5693.XYEgnKpx' doesn't seem to be a coverage data file: 
INTERNALERROR> For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning

@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

So that's why my jobs started failing suddenly!

I was just about to report the same… No asyncio in my case: https://github.com/ansible/pylibssh/actions/runs/15444790399/job/43471764922#step:16:645. The module I'm measuring is a C-extension, FWIW.

The reason for failures is that we have filterwarnings = error that raises exceptions for any warnings during testing. I think that pytest 8.4 might've stopped suppressing things that weren't previously visible. It'd be good to bisect https://docs.pytest.org/en/stable/changelog.html#pytest-8-4-0-2025-06-02 and understand it a bit better.

@johnthagen you have a different underlying error (a corrupted SQLite file instead of an unmeasured module), so it'd have a different code to suppress (if coveragepy has one for that).

cc @nedbat @ionelmc need some 👀 on this, to understand the underlying cause of module-not-measured, though.

Should the warnings be suppressed around https://github.com/pytest-dev/pytest-cov/blob/9463242/src/pytest_cov/engine.py#L469?

@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

Having #675 flashbacks, I'm curious whether it'd make sense for pytest-cov to capture all the warnings in coveragepy and only resurface them at the end of test sessions, shielding the end-users from rather nasty tracebacks that can be difficult to parse for many.

@billbrod
Copy link

billbrod commented Jun 4, 2025

I also suddenly started having this problem, and noticed the same set of interactions: pytest 8.4, pytest-cov, pytest-xdist (versions don't seem to matter). Then filterwarnings=error causes the issue because of this WARNING: Failed to generate report: No data to report. (behavior is identical whether filterwarnings is specified in pyproject.toml or via -W error on the command line).

Adding disable_warnings = ["module-not-measured"] does not solve my issue.

I do not have asyncio either.

Pinning pytest<8.4 fixes the issue but is obviously not ideal.

I have no idea why the module not measured warning is being raised -- I had noticed this earlier (and tried and failed to fix it), but I seem to get proper coverage outputs regardless. I assume it has something to do with the interaction between pytest-xdist and pytest-cov, with the individual workers not computing the coverage (and thus raising the warning)?

EDIT: Okay, adding the suggested disable_warnings=["module-not-measured"] AND adding, "ignore:Failed to generate report:", to pytest's filterwarnings (after the line with "error"), seems to solve this problem for me, even with pytest==8.4

@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

@billbrod only warnings in this list have codes that can be suppressed through the coverage config: https://coverage.readthedocs.io/en/latest/cmd.html#warnings

A workaround on the runtime level would be adding an ignore: (or default:, or once:) entry into filterwarnings in the pytest config or another -W to ignore things from the coverage library, for example.

I'm sure if you were to reveal the warnings with older pytests, they'd show up in the log.

P.S. I've also noticed in the past that this warning was being printed out while coverage was actually collected. So the assumption that this could be related to pytest-xdist / pytest-cov not processing coverage in workers seems likely.

@bsipocz
Copy link

bsipocz commented Jun 4, 2025

I'm not certain this is pytest-xdist related as I see similar errors with this combination of plugins when using pytest 8.4:

plugins: astropy-0.11.0, remotedata-0.4.1, filter-subpackage-0.2.0, hypothesis-6.135.0, doctestplus-1.4.0, rerunfailures-15.1, astropy-header-0.2.2, dependency-0.6.0, mock-3.14.1, arraydiff-0.6.1, cov-6.1.1

I'm looking into amending the configs to silence this issue as suggested above, but also wonder if this could have been picked up in CI before 8.4 release time if there was a job testing with the pytest dev version?

@fzimmermann89
Copy link

This seems to apply to all warnings.
So if you previously had some warning in pytest coverage but it completed anyways, now it fails.

You would have to adjust what warning to ignore or fix the root cause.

@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

@bsipocz do any of your plugins run subprocesses or threads? There was something in the pytest change log about resurfacing exceptions in threads, I think. My working theory is that it's one of the previously unraisable cases.

raydouglass pushed a commit to rapidsai/kvikio that referenced this issue Jun 4, 2025
To address [coverage collection fails when using pytest-xdist and pytest
8.4.0](pytest-dev/pytest-cov#693), disable the
no-data-collected warning.
@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

I assume it has something to do with the interaction between pytest-xdist and pytest-cov, with the individual workers not computing the coverage (and thus raising the warning)?

@billbrod it's actually the chicken-egg race that occurs because it's difficult to guarantee the plugin load order: #437.

@bsipocz
Copy link

bsipocz commented Jun 4, 2025

@webknjaz - not that I know of, but now forcing it to use the older pytest, I see the CoverageWarning of module-not-measures. It makes little sense as it complains about the package in question, but nevertheless I would have expected this warning to be raised as exception rather than hidden in the log of a passing job.

/Users/bsipocz/munka/devel/astroquery/.tox/devdeps-alldeps-cov/lib/python3.12/site-packages/coverage/inorout.py:525: CoverageWarning: Module astroquery was previously imported, but not measured (module-not-measured)
  self.warn(msg, slug="module-not-measured")

@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

https://pytest-cov.readthedocs.io/en/latest/plugins.html claims that using --cov-append and setting a few env vars may help. I haven't tested it, though.

@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

I've found that coveragepy itself disables the "No data was collected." warning (no-data-collected) in subprocesses: https://github.com/nedbat/coveragepy/blob/52407da7b9ed99c8bd1d2536df82ba8749664f50/coverage/control.py#L1428 (process_startup() code path). Apparently, this technique was introduced over a decade ago: nedbat/coveragepy@c33865d#diff-0f9558d13863f76ad1d6988ff39118c8db7890ec69f03b161dab6b9bee97debfR701.

Now, pytest-cov actually uses the same private attribute in a few places too:

cov._warn_no_data = False
/
self.cov._warn_no_data = False
. They were added in https://github.com/pytest-dev/pytest-cov/pull/59/files#diff-73d4e0c674cc10d9864eee89ecf8e20947f78aaaa887e222dc3905f04e540bbeR61 and https://github.com/pytest-dev/pytest-cov/pull/382/files#diff-22f0be082461b1610c5c5775f43a91376d2159591987cc3ffe9550a120170459R223.

So my feeling is that some subclasses in engine.py might be lacking the same hack.

cc @ionelmc @nedbat opinions?

@bsipocz
Copy link

bsipocz commented Jun 4, 2025

FWIW, the explicit ignore of that one warning works, so I emerge from the rabbit hole of figuring out which plugin combination is responsible.

ignore:Module astroquery was previously imported, but not measured:coverage.exceptions.CoverageWarning

@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

FWIW, the explicit ignore of that one warning works, so I emerge from the rabbit hole of figuring out which plugin combination is responsible.

ignore:Module astroquery was previously imported, but not measured:coverage.exceptions.CoverageWarning

Yeah, I've been using this same workaround whenever there were problems with pytest-cov / coveragepy in the past. Just wanted to lurk into the source in hopes of understanding more..

@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

So my feeling is that some subclasses in engine.py might be lacking the same hack.

I quickly verified that copying a few of those private ignores does work locally.

@billbrod
Copy link

billbrod commented Jun 4, 2025

https://pytest-cov.readthedocs.io/en/latest/plugins.html claims that using --cov-append and setting a few env vars may help. I haven't tested it, though.

FWIW this didn't work for me, I still get the warnings with --cov-append.

@webknjaz
Copy link
Member

webknjaz commented Jun 4, 2025

@billbrod with env vars set along with the CLI flag?

@ionelmc
Copy link
Member

ionelmc commented Jun 5, 2025

Seems to me like all the breakages have in common is some form of "filterwarnings = error". When warnings were added in pytest-cov (and I believe coverage as well) nobody thought that this "filterwarnings = error" pattern would be so prevalent, or dare I say, so abused. The intention was we add some warning to help the user figure out if their project setup is fine or needs some attention in certain areas (like help the user figure out that the code they want to cover is ran before coverage starts, and that can't be measured).

The way I see it "filterwarnings = error" is for temporary or situational usage. You want to upgrade django and want to figure out easy what exactly triggers your warning? You use "filterwarnings = error" cause stacklevel is rarely that helpful unless you know exactly where to look.

@bsipocz
Copy link

bsipocz commented Jun 5, 2025

nobody thought that this "filterwarnings = error" pattern would be so prevalent, or dare I say, so abused.

I would very strongly push back on this notion, it's far from being abuse, instead it is best practice.

Flipping the warnings to errors in CI is part of the best practices a lot of libraries follow, that would allow us maintainers to pick up deprecation and other changes in behaviour. This follows the notion that warnings have the purpose of informing the users about things they can do about something (otherwise it's just noise), and therefore all warnings should be handled appropriately in the test suite. And once that is done, we can flip the switch and raise the remaining (==new) ones as errors to notice them in CI.

(one can use the logger to give verbose advise and notices about stuff the user can improve, and keep the warnings for actual warnings)

@webknjaz
Copy link
Member

webknjaz commented Jun 5, 2025

I was just about to post the same. filterwarnings = error has been a widely used best practice for so long that I can't even remember when I first heard of it.. It's an important bit of maintenance hygiene that helps deal with forward compatibility proactively.

I think that pytest itself was one of the places preaching it. To me, it's a fundamental principle even.

@billbrod
Copy link

billbrod commented Jun 5, 2025

@billbrod with env vars set along with the CLI flag?

@webknjaz yep! adding either the env vars or the CLI flag doesn't change the behavior -- I always get the module-not-measured warning.

@webknjaz
Copy link
Member

webknjaz commented Jun 5, 2025

@billbrod I meant not one or the other but both.

@billbrod
Copy link

billbrod commented Jun 5, 2025

@billbrod I meant not one or the other but both.

One or the other, or both, behavior is the same.

To make sure I'm being clear: changing both filterwarnings and disable_warnings as described elsewhere in this thread does remove that module-not-measured warning, while adding either the --cov-append flag or COV_CORE_SOURCE, COV_CORE_CONFIG, COV_CORE_DATAFILE enviroment variables (or both, as described in pytest-cov docs) does not affect the warning.

@webknjaz
Copy link
Member

webknjaz commented Jun 5, 2025

On the topic of pytest-cov + pytest-xdist interaction, here's something I've been experimenting with many years ago: https://github.com/aio-libs/aiohttp/blob/b4e716eedbf98f3ef57842c3a27266be60643390/tests/_pytest_plugin.py#L43-L68. It was supposed to reorder plugin loading on the project level.

@webknjaz
Copy link
Member

webknjaz commented Jun 5, 2025

pytest-cov/src/pytest_cov/engine.py

Line 346 in 9463242

UPD: with a little bit of experimentation, I'm now convinced that the cov._warn_no_data = False hack is misplaced. Instead of DistMaster, it should be in DistWorker. And it makes more sense to me — workers might end up importing parts of the project and never import other parts. With that, it's pretty much expected that at least one of the workers would hit the condition of not importing the measured module. In my case, I have source = . and source_pkgs = pylibsshext in .coveragerc's [run] section. And pytest is executed with a bare --cov which delegates the module + source discovery to coveragepy through its config.

I moved those hacks from the master to the worker class (in my tox's site-packages) and the warning stopped being triggered. Additionally, I checked that if I set source_pkgs = pylibsshext.asdf (as in, something non-existent), then coveragepy would (correctly!) emit the module-not-imported warning.

This is something that I've been thinking through since yesterday and so far, it seems like a reliable solution to the bug.

webknjaz added a commit to webknjaz/pytest-dev--pytest-cov that referenced this issue Jun 6, 2025
@webknjaz webknjaz linked a pull request Jun 6, 2025 that will close this issue
@webknjaz
Copy link
Member

webknjaz commented Jun 6, 2025

I made a oneliner PR that would disable module-not-imported (#695). It's probably controversial, but if somebody wants to see it in action — feel free to grab that patch and experiment.

ionelmc added a commit that referenced this issue Jun 6, 2025
@ionelmc
Copy link
Member

ionelmc commented Jun 6, 2025

I was just about to post the same. filterwarnings = error has been a widely used best practice for so long that I can't even remember when I first heard of it.. It's an important bit of maintenance hygiene that helps deal with forward compatibility proactively.

It's not that old. I think it only became really popular circa 2022: https://til.simonwillison.net/pytest/treat-warnings-as-errors

I would very strongly push back on this notion, it's far from being abuse, instead it is best practice.

It may be best practice now, but it's still an abuse of the warnings system. If I wanted to raise exceptions all the time from pytest-cov I'd just raise it. You need to understand that filterwarnings = error is a debug pattern. Yes, I use it too, in all the projects that I can reasonably make an warning ignore list - an inevitable tradeoff for this pattern. But I understand and accept the fact that I am exposing myself to all sorts of crazy tracebacks from my dependencies. I am ok with adding a ignore:Stuff I don't care about in there. Why is that not an acceptable solution?

This breakage only occurs because an extremely strict filterwarnings is used. Pytest 8.4 made some changes so now filterwarnings is applied way earlier than before, this these warnings are errors now (cause yall asked for it by using filterwarnings = error ).

But alas, pytest-cov is a convenience tool full of workarounds anyway, so it's not unreasonable to have this pattern supported somehow. What can we do here? The way I see it there are few ways to resolve this issue:

  1. Just disable the warning by using _warn_unimported_source like in Prevent unimported warnings @ pytest-xdist workers #695. This will make situations where coverage is misconfigured even harder to debug.
  2. Automatically inject a once:.*:CoverageWarning or similar in the filter system to prevent raising as error. I expect this will be followed by complaints from users that actually want to ignore that warning.
  3. Document it and explain that if you use filterwarnings = error you should also add a once:.*:CoverageWarning or similar. Maybe even capture CoverageWarning and instruct the user what to add in their filterwarnings.

I have created a reproducing test in https://github.com/pytest-dev/pytest-cov/pull/696/files#diff-462f05b103e164981d6272e222b8579cf45f6ba30f8b1d76b2e00909e09ecd91

Which do you prefer, or do you see a different solution?

@webknjaz
Copy link
Member

webknjaz commented Jun 6, 2025

I have created a reproducing test in https://github.com/pytest-dev/pytest-cov/pull/696/files#diff-462f05b103e164981d6272e222b8579cf45f6ba30f8b1d76b2e00909e09ecd91

Which do you prefer, or do you see a different solution?

@ionelmc so this reproducer seems to trigger the outer layer of the problem I'm having, not the underlying root cause. I'd be perfectly fine not disabling the coveragepy warnings and addressing them instead, if they are legit. The problem, at least for my case (some of the others above might be slightly different), is that the warning is emitted in the first place. All the source is measured correctly and the module appears in sys.modules that coveragepy specifically checks for. This is check is happening in multiple subprocesses and so it sometimes hits cases where a one of the workers doesn't hit the import, perhaps. But in terms of coverage combined from all the workers, it doesn't matter and the warning becomes a false-positive.

And with that, the overall experience is that the warning is misleading, causing the end-users to second-guess themselves.

I'd argue that as long as it's unreliable in such a setup, it should be disabled by the plugin, not by the users.
The docs might mention that to see the warnings, one might want to run pytest -n0 which would report them reliably.

The filterwarnings = error is secondary in this context and would work well with the mechanism that only suppresses the warnings when they're misleading.

I wish pytest-cov could somehow intercept the warnings that coveragepy emits and resurface them upon its own analysis. For example, if each of the workers produced a warning, that would be reliable. If some or none, said warning would be disregarded.. But while the idea is simple, I'm sure it's difficult to implement in the world of xdist.

So let's agree that practicality beats purity and go for the option 1.

@webknjaz
Copy link
Member

webknjaz commented Jun 6, 2025

  1. [...] This will make situations where coverage is misconfigured even harder to debug.

To explicitly address this, there could be another flag to display this or it could be made conditional on -vvvvv. Otherwise, documenting that the warnings are still going to be visible in modes that disable xdist, as I suggested above, seems reasonable.

webknjaz added a commit to webknjaz/pylibssh that referenced this issue Jun 6, 2025
They are only surfaced under pytest 8.4, with `pytest-cov` and
`pytest-xdist` being both active [[1]].

[1]: pytest-dev/pytest-cov#693
@webknjaz webknjaz moved this to 🕵️ Rabbit hole maze 🕵 in 📅 Procrastinating in public Jun 6, 2025
@ionelmc
Copy link
Member

ionelmc commented Jun 8, 2025

So I am trying to figure out if the original issue reported here by @rjdbcm is indeed caused by a warning that should not be issued. I have looked at the tox config and I can't figure out how pytest is ran from that. Also, https://github.com/OZI-Project/OZI doesn't seem to have a tox.ini so I am not sure what to run to reproduce this.

@bsipocz
Copy link

bsipocz commented Jun 8, 2025

Fwiw, I think the warning I see is also spurious, it warns about the package I'm testing (I suspect the other plugins do an import early and that confuses the cov plugin)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants