Skip to content

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

Open
@rjdbcm

Description

@rjdbcm

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()

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions