From 863ca1a8155da0250a8d992f7297a306cc78663e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 12:14:13 +0300 Subject: [PATCH 01/25] Remove the now crummy support for subprocesses. Users will need to set `patch = subprocess` or similar in their coverage config to get equivalent behavior. Minium required coverage is set the current patch release to avoid unexpected and untested problems. Also cleanup some old tests and support code. --- docs/subprocess-support.rst | 186 ++-------------------- setup.py | 72 +-------- src/pytest-cov.embed | 13 -- src/pytest-cov.pth | 1 - src/pytest_cov/compat.py | 15 -- src/pytest_cov/embed.py | 124 --------------- src/pytest_cov/engine.py | 80 ++-------- src/pytest_cov/plugin.py | 34 +--- tests/test_pytest_cov.py | 300 ++++++++++++++++-------------------- tox.ini | 9 +- 10 files changed, 166 insertions(+), 668 deletions(-) delete mode 100644 src/pytest-cov.embed delete mode 100644 src/pytest-cov.pth delete mode 100644 src/pytest_cov/compat.py delete mode 100644 src/pytest_cov/embed.py diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 56044392..7e552c45 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -2,189 +2,21 @@ Subprocess support ================== -Normally coverage writes the data via a pretty standard atexit handler. However, if the subprocess doesn't exit on its -own then the atexit handler might not run. Why that happens is best left to the adventurous to discover by waddling -through the Python bug tracker. - -pytest-cov supports subprocesses, and works around these atexit limitations. However, there are a few pitfalls that need to be explained. - -But first, how does pytest-cov's subprocess support works? - -pytest-cov packaging injects a pytest-cov.pth into the installation. This file effectively runs this at *every* python startup: - -.. code-block:: python - - if 'COV_CORE_SOURCE' in os.environ: - try: - from pytest_cov.embed import init - init() - except Exception as exc: - sys.stderr.write( - "pytest-cov: Failed to setup subprocess coverage. " - "Environ: {0!r} " - "Exception: {1!r}\n".format( - dict((k, v) for k, v in os.environ.items() if k.startswith('COV_CORE')), - exc - ) - ) - -The pytest plugin will set this ``COV_CORE_SOURCE`` environment variable thus any subprocess that inherits the environment variables -(the default behavior) will run ``pytest_cov.embed.init`` which in turn sets up coverage according to these variables: - -* ``COV_CORE_SOURCE`` -* ``COV_CORE_CONFIG`` -* ``COV_CORE_DATAFILE`` -* ``COV_CORE_BRANCH`` -* ``COV_CORE_CONTEXT`` - -Why does it have the ``COV_CORE`` you wonder? Well, it's mostly historical reasons: long time ago pytest-cov depended on a cov-core package -that implemented common functionality for pytest-cov, nose-cov and nose2-cov. The dependency is gone but the convention is kept. It could -be changed but it would break all projects that manually set these intended-to-be-internal-but-sadly-not-in-reality environment variables. - -Coverage's subprocess support -============================= - -Now that you understand how pytest-cov works you can easily figure out that using -`coverage's recommended `_ way of dealing with subprocesses, -by either having this in a ``.pth`` file or ``sitecustomize.py`` will break everything: - -.. code-block:: - - import coverage; coverage.process_startup() # this will break pytest-cov - -Do not do that as that will restart coverage with the wrong options. - -If you use ``multiprocessing`` -============================== - -Builtin support for multiprocessing was dropped in pytest-cov 4.0. -This support was mostly working but very broken in certain scenarios (see `issue 82408 `_) -and made the test suite very flaky and slow. - -However, there is `builtin multiprocessing support in coverage `_ -and you can migrate to that. All you need is this in your preferred configuration file (example: ``.coveragerc``): +Subprocess support was removed in pytest-cov 7.0 due to various complexities resulting from coverage's own subprocess support. +To migrate you should change your coverage config to have at least this: .. code-block:: ini [run] - concurrency = multiprocessing - parallel = true - sigterm = true - -Now as a side-note, it's a good idea in general to properly close your Pool by using ``Pool.join()``: - -.. code-block:: python - - from multiprocessing import Pool - - def f(x): - return x*x - - if __name__ == '__main__': - p = Pool(5) - try: - print(p.map(f, [1, 2, 3])) - finally: - p.close() # Marks the pool as closed. - p.join() # Waits for workers to exit. - - -.. _cleanup_on_sigterm: - -Signal handlers -=============== - -pytest-cov provides a signal handling routines, mostly for special situations where you'd have custom signal handling that doesn't -allow atexit to properly run and the now-gone multiprocessing support: - -* ``pytest_cov.embed.cleanup_on_sigterm()`` -* ``pytest_cov.embed.cleanup_on_signal(signum)`` (e.g.: ``cleanup_on_signal(signal.SIGHUP)``) - -If you use multiprocessing --------------------------- - -It is not recommanded to use these signal handlers with multiprocessing as registering signal handlers will cause deadlocks in the pool, -see: https://bugs.python.org/issue38227). - -If you got custom signal handling ---------------------------------- - -**pytest-cov 2.6** has a rudimentary ``pytest_cov.embed.cleanup_on_sigterm`` you can use to register a SIGTERM handler -that flushes the coverage data. - -**pytest-cov 2.7** adds a ``pytest_cov.embed.cleanup_on_signal`` function and changes the implementation to be more -robust: the handler will call the previous handler (if you had previously registered any), and is re-entrant (will -defer extra signals if delivered while the handler runs). - -For example, if you reload on SIGHUP you should have something like this: - -.. code-block:: python - - import os - import signal - - def restart_service(frame, signum): - os.exec( ... ) # or whatever your custom signal would do - signal.signal(signal.SIGHUP, restart_service) - - try: - from pytest_cov.embed import cleanup_on_signal - except ImportError: - pass - else: - cleanup_on_signal(signal.SIGHUP) - -Note that both ``cleanup_on_signal`` and ``cleanup_on_sigterm`` will run the previous signal handler. - -Alternatively you can do this: - -.. code-block:: python - - import os - import signal - - try: - from pytest_cov.embed import cleanup - except ImportError: - cleanup = None - - def restart_service(frame, signum): - if cleanup is not None: - cleanup() - - os.exec( ... ) # or whatever your custom signal would do - signal.signal(signal.SIGHUP, restart_service) - -If you use Windows ------------------- - -On Windows you can register a handler for SIGTERM but it doesn't actually work. It will work if you -`os.kill(os.getpid(), signal.SIGTERM)` (send SIGTERM to the current process) but for most intents and purposes that's -completely useless. - -Consequently this means that if you use multiprocessing you got no choice but to use the close/join pattern as described -above. Using the context manager API or `terminate` won't work as it relies on SIGTERM. - -However you can have a working handler for SIGBREAK (with some caveats): - -.. code-block:: python + patch = subprocess - import os - import signal +Or if you use pyproject.toml: - def shutdown(frame, signum): - # your app's shutdown or whatever - signal.signal(signal.SIGBREAK, shutdown) +.. code-block:: toml - try: - from pytest_cov.embed import cleanup_on_signal - except ImportError: - pass - else: - cleanup_on_signal(signal.SIGBREAK) + [tool.coverage.run] + patch = ["subprocess"] -The `caveats `_ being -roughly: +Note that if you enable the subprocess patch then ``parallel = true`` is automatically set. -* you need to deliver ``signal.CTRL_BREAK_EVENT`` -* it gets delivered to the whole process group, and that can have unforeseen consequences +If it still doesn't produce the same coverage as before you may need to enable more patches, see the `coverage config `_ and `subprocess `_ documentation. diff --git a/setup.py b/setup.py index 3532adac..46d25a81 100755 --- a/setup.py +++ b/setup.py @@ -1,80 +1,17 @@ #!/usr/bin/env python import re -from itertools import chain from pathlib import Path -from setuptools import Command from setuptools import find_packages from setuptools import setup -try: - # https://setuptools.pypa.io/en/latest/deprecated/distutils-legacy.html - from setuptools.command.build import build -except ImportError: - from distutils.command.build import build - -from setuptools.command.develop import develop -from setuptools.command.easy_install import easy_install -from setuptools.command.install_lib import install_lib - def read(*names, **kwargs): with Path(__file__).parent.joinpath(*names).open(encoding=kwargs.get('encoding', 'utf8')) as fh: return fh.read() -class BuildWithPTH(build): - def run(self, *args, **kwargs): - super().run(*args, **kwargs) - path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') - dest = str(Path(self.build_lib) / Path(path).name) - self.copy_file(path, dest) - - -class EasyInstallWithPTH(easy_install): - def run(self, *args, **kwargs): - super().run(*args, **kwargs) - path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') - dest = str(Path(self.install_dir) / Path(path).name) - self.copy_file(path, dest) - - -class InstallLibWithPTH(install_lib): - def run(self, *args, **kwargs): - super().run(*args, **kwargs) - path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') - dest = str(Path(self.install_dir) / Path(path).name) - self.copy_file(path, dest) - self.outputs = [dest] - - def get_outputs(self): - return chain(super().get_outputs(), self.outputs) - - -class DevelopWithPTH(develop): - def run(self, *args, **kwargs): - super().run(*args, **kwargs) - path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') - dest = str(Path(self.install_dir) / Path(path).name) - self.copy_file(path, dest) - - -class GeneratePTH(Command): - user_options = () - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - with Path(__file__).parent.joinpath('src', 'pytest-cov.pth').open('w') as fh: - with Path(__file__).parent.joinpath('src', 'pytest-cov.embed').open() as sh: - fh.write(f'import os, sys;exec({sh.read().replace(" ", " ")!r})') - - setup( name='pytest-cov', version='6.3.0', @@ -125,7 +62,7 @@ def run(self): python_requires='>=3.9', install_requires=[ 'pytest>=6.2.5', - 'coverage[toml]>=7.5', + 'coverage[toml]>=7.10.6', 'pluggy>=1.2', ], extras_require={ @@ -142,11 +79,4 @@ def run(self): 'pytest_cov = pytest_cov.plugin', ], }, - cmdclass={ - 'build': BuildWithPTH, - 'easy_install': EasyInstallWithPTH, - 'install_lib': InstallLibWithPTH, - 'develop': DevelopWithPTH, - 'genpth': GeneratePTH, - }, ) diff --git a/src/pytest-cov.embed b/src/pytest-cov.embed deleted file mode 100644 index 630a2a72..00000000 --- a/src/pytest-cov.embed +++ /dev/null @@ -1,13 +0,0 @@ -if 'COV_CORE_SOURCE' in os.environ: - try: - from pytest_cov.embed import init - init() - except Exception as exc: - sys.stderr.write( - "pytest-cov: Failed to setup subprocess coverage. " - "Environ: {0!r} " - "Exception: {1!r}\n".format( - dict((k, v) for k, v in os.environ.items() if k.startswith('COV_CORE')), - exc - ) - ) diff --git a/src/pytest-cov.pth b/src/pytest-cov.pth deleted file mode 100644 index 8ed1a516..00000000 --- a/src/pytest-cov.pth +++ /dev/null @@ -1 +0,0 @@ -import os, sys;exec('if \'COV_CORE_SOURCE\' in os.environ:\n try:\n from pytest_cov.embed import init\n init()\n except Exception as exc:\n sys.stderr.write(\n "pytest-cov: Failed to setup subprocess coverage. "\n "Environ: {0!r} "\n "Exception: {1!r}\\n".format(\n dict((k, v) for k, v in os.environ.items() if k.startswith(\'COV_CORE\')),\n exc\n )\n )\n') diff --git a/src/pytest_cov/compat.py b/src/pytest_cov/compat.py deleted file mode 100644 index 453709d7..00000000 --- a/src/pytest_cov/compat.py +++ /dev/null @@ -1,15 +0,0 @@ -class SessionWrapper: - def __init__(self, session): - self._session = session - if hasattr(session, 'testsfailed'): - self._attr = 'testsfailed' - else: - self._attr = '_testsfailed' - - @property - def testsfailed(self): - return getattr(self._session, self._attr) - - @testsfailed.setter - def testsfailed(self, value): - setattr(self._session, self._attr, value) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py deleted file mode 100644 index 153cb83d..00000000 --- a/src/pytest_cov/embed.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Activate coverage at python startup if appropriate. - -The python site initialisation will ensure that anything we import -will be removed and not visible at the end of python startup. However -we minimise all work by putting these init actions in this separate -module and only importing what is needed when needed. - -For normal python startup when coverage should not be activated the pth -file checks a single env var and does not import or call the init fn -here. - -For python startup when an ancestor process has set the env indicating -that code coverage is being collected we activate coverage based on -info passed via env vars. -""" - -import atexit -import os -import signal - -_active_cov = None - - -def init(): - # Only continue if ancestor process has set everything needed in - # the env. - global _active_cov - - cov_source = os.environ.get('COV_CORE_SOURCE') - cov_config = os.environ.get('COV_CORE_CONFIG') - cov_datafile = os.environ.get('COV_CORE_DATAFILE') - cov_branch = True if os.environ.get('COV_CORE_BRANCH') == 'enabled' else None - cov_context = os.environ.get('COV_CORE_CONTEXT') - - if cov_datafile: - if _active_cov: - cleanup() - # Import what we need to activate coverage. - import coverage - - # Determine all source roots. - if cov_source in os.pathsep: - cov_source = None - else: - cov_source = cov_source.split(os.pathsep) - if cov_config == os.pathsep: - cov_config = True - - # Activate coverage for this process. - cov = _active_cov = coverage.Coverage( - source=cov_source, - branch=cov_branch, - data_suffix=True, - config_file=cov_config, - auto_data=True, - data_file=cov_datafile, - ) - cov.load() - cov.start() - if cov_context: - cov.switch_context(cov_context) - cov._warn_no_data = False - cov._warn_unimported_source = False - cov._warn_preimported_source = False - return cov - - -def _cleanup(cov): - if cov is not None: - cov.stop() - cov.save() - cov._auto_save = False # prevent autosaving from cov._atexit in case the interpreter lacks atexit.unregister - try: - atexit.unregister(cov._atexit) - except Exception: # noqa: S110 - pass - - -def cleanup(): - global _active_cov - global _cleanup_in_progress - global _pending_signal - - _cleanup_in_progress = True - _cleanup(_active_cov) - _active_cov = None - _cleanup_in_progress = False - if _pending_signal: - pending_signal = _pending_signal - _pending_signal = None - _signal_cleanup_handler(*pending_signal) - - -_previous_handlers = {} -_pending_signal = None -_cleanup_in_progress = False - - -def _signal_cleanup_handler(signum, frame): - global _pending_signal - if _cleanup_in_progress: - _pending_signal = signum, frame - return - cleanup() - _previous_handler = _previous_handlers.get(signum) - if _previous_handler == signal.SIG_IGN: - return - elif _previous_handler and _previous_handler is not _signal_cleanup_handler: - _previous_handler(signum, frame) - elif signum == signal.SIGTERM: - os._exit(128 + signum) - elif signum == signal.SIGINT: - raise KeyboardInterrupt - - -def cleanup_on_signal(signum): - previous = signal.getsignal(signum) - if previous is not _signal_cleanup_handler: - _previous_handlers[signum] = previous - signal.signal(signum, _signal_cleanup_handler) - - -def cleanup_on_sigterm(): - cleanup_on_signal(signal.SIGTERM) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 99ea6ddd..ca631272 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -10,7 +10,6 @@ import socket import sys import warnings -from io import StringIO from pathlib import Path from typing import Union @@ -20,7 +19,6 @@ from . import CentralCovContextWarning from . import DistCovError -from .embed import cleanup class BrokenCovConfigError(Exception): @@ -62,10 +60,6 @@ def ensure_topdir_wrapper(self, *args, **kwargs): return ensure_topdir_wrapper -def _data_suffix(name): - return f'{filename_suffix(True)}.{name}' - - class CovController: """Base class for different plugin implementations.""" @@ -100,12 +94,10 @@ def ensure_topdir(self): def pause(self): self.started = False self.cov.stop() - self.unset_env() @_ensure_topdir def resume(self): self.cov.start() - self.set_env() self.started = True def start(self): @@ -114,32 +106,6 @@ def start(self): def finish(self): self.started = False - @_ensure_topdir - def set_env(self): - """Put info about coverage into the env so that subprocesses can activate coverage.""" - if self.cov_source is None: - os.environ['COV_CORE_SOURCE'] = os.pathsep - else: - os.environ['COV_CORE_SOURCE'] = os.pathsep.join(self.cov_source) - config_file = Path(self.cov_config) - if config_file.exists(): - os.environ['COV_CORE_CONFIG'] = os.fspath(config_file.resolve()) - else: - os.environ['COV_CORE_CONFIG'] = os.pathsep - # this still uses the old abspath cause apparently Python 3.9 on Windows has a buggy Path.resolve() - os.environ['COV_CORE_DATAFILE'] = os.path.abspath(self.cov.config.data_file) # noqa: PTH100 - if self.cov_branch: - os.environ['COV_CORE_BRANCH'] = 'enabled' - - @staticmethod - def unset_env(): - """Remove coverage info from env.""" - os.environ.pop('COV_CORE_SOURCE', None) - os.environ.pop('COV_CORE_CONFIG', None) - os.environ.pop('COV_CORE_DATAFILE', None) - os.environ.pop('COV_CORE_BRANCH', None) - os.environ.pop('COV_CORE_CONTEXT', None) - @staticmethod def get_node_desc(platform, version_info): """Return a description of this node.""" @@ -291,12 +257,10 @@ class Central(CovController): @_ensure_topdir def start(self): - cleanup() - self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix('c'), + data_suffix=True, config_file=self.cov_config, ) if self.cov.config.dynamic_context == 'test_function': @@ -309,7 +273,7 @@ def start(self): self.combining_cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix('cc'), + data_suffix=f'{filename_suffix(True)}.combine', data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 config_file=self.cov_config, ) @@ -318,7 +282,6 @@ def start(self): if not self.cov_append: self.cov.erase() self.cov.start() - self.set_env() super().start() @@ -327,7 +290,6 @@ def finish(self): """Stop coverage, save data to file and set the list of coverage objects to report on.""" super().finish() - self.unset_env() self.cov.stop() self.cov.save() @@ -345,12 +307,10 @@ class DistMaster(CovController): @_ensure_topdir def start(self): - cleanup() - self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix('m'), + data_suffix=True, config_file=self.cov_config, ) if self.cov.config.dynamic_context == 'test_function': @@ -365,7 +325,7 @@ def start(self): self.combining_cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix('mc'), + data_suffix=f'{filename_suffix(True)}.combine', data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 config_file=self.cov_config, ) @@ -405,18 +365,10 @@ def testnodedown(self, node, error): output['cov_worker_node_id'], ) - cov = coverage.Coverage(source=self.cov_source, branch=self.cov_branch, data_suffix=data_suffix, config_file=self.cov_config) - cov.start() - if coverage.version_info < (5, 0): - data = CoverageData() - data.read_fileobj(StringIO(output['cov_worker_data'])) - cov.data.update(data) - else: - data = CoverageData(no_disk=True, suffix='should-not-exist') - data.loads(output['cov_worker_data']) - cov.get_data().update(data) - cov.stop() - cov.save() + cov_data = CoverageData( + suffix=data_suffix, + ) + cov_data.loads(output['cov_worker_data']) path = output['cov_worker_path'] self.cov.config.paths['source'].append(path) @@ -443,15 +395,13 @@ class DistWorker(CovController): @_ensure_topdir def start(self): - cleanup() - # Determine whether we are collocated with master. self.is_collocated = ( socket.gethostname() == self.config.workerinput['cov_master_host'] and self.topdir == self.config.workerinput['cov_master_topdir'] ) - # If we are not collocated then rewrite master paths to worker paths. + # If we are not collocated, then rewrite master paths to worker paths. if not self.is_collocated: master_topdir = self.config.workerinput['cov_master_topdir'] worker_topdir = self.topdir @@ -463,13 +413,13 @@ def start(self): self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=_data_suffix(f'w{self.nodeid}'), + data_suffix=True, config_file=self.cov_config, ) # Prevent workers from issuing module-not-measured type of warnings (expected for a workers to not have coverage in all the files). self.cov._warn_unimported_source = False self.cov.start() - self.set_env() + super().start() @_ensure_topdir @@ -477,7 +427,6 @@ def finish(self): """Stop coverage and send relevant info back to the master.""" super().finish() - self.unset_env() self.cov.stop() if self.is_collocated: @@ -497,12 +446,7 @@ def finish(self): # it on the master node. # Send all the data to the master over the channel. - if coverage.version_info < (5, 0): - buff = StringIO() - self.cov.data.write_fileobj(buff) - data = buff.getvalue() - else: - data = self.cov.get_data().dumps() + data = self.cov.get_data().dumps() self.config.workeroutput.update( { diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index c49a655d..553a9203 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -8,17 +8,11 @@ from pathlib import Path from typing import TYPE_CHECKING -import coverage import pytest -from coverage.exceptions import CoverageWarning -from coverage.results import display_covered -from coverage.results import should_fail_under from . import CovDisabledWarning from . import CovReportWarning from . import PytestCovWarning -from . import compat -from . import embed if TYPE_CHECKING: from .engine import CovController @@ -37,9 +31,6 @@ def validate_report(arg): msg = f'invalid choice: "{arg}" (choose from "{all_choices}")' raise argparse.ArgumentTypeError(msg) - if report_type == 'lcov' and coverage.version_info <= (6, 3): - raise argparse.ArgumentTypeError('LCOV output is only supported with coverage.py >= 6.3') - if len(values) == 1: return report_type, None @@ -70,8 +61,6 @@ def validate_fail_under(num_str): def validate_context(arg): - if coverage.version_info <= (5, 0): - raise argparse.ArgumentTypeError('Contexts are only supported with coverage.py >= 5.x') if arg != 'test': raise argparse.ArgumentTypeError('The only supported value is "test".') return arg @@ -345,6 +334,8 @@ def pytest_runtestloop(self, session): break else: warnings.simplefilter('once', PytestCovWarning) + from coverage.exceptions import CoverageWarning + for _, _, category, _, _ in warnings.filters: if category is CoverageWarning: break @@ -353,9 +344,7 @@ def pytest_runtestloop(self, session): result = yield - compat_session = compat.SessionWrapper(session) - - self.failed = bool(compat_session.testsfailed) + self.failed = bool(session.testsfailed) if self.cov_controller is not None: self.cov_controller.finish() @@ -363,6 +352,8 @@ def pytest_runtestloop(self, session): # import coverage lazily here to avoid importing # it for unit tests that don't need it from coverage.misc import CoverageException + from coverage.results import display_covered + from coverage.results import should_fail_under try: self.cov_total = self.cov_controller.summary(self.cov_report) @@ -384,7 +375,7 @@ def pytest_runtestloop(self, session): ) session.config.pluginmanager.getplugin('terminalreporter').write(f'\nERROR: {message}\n', red=True, bold=True) # make sure we get the EXIT_TESTSFAILED exit code - compat_session.testsfailed += 1 + session.testsfailed += 1 return result @@ -426,15 +417,6 @@ def pytest_terminal_summary(self, terminalreporter): ) terminalreporter.write(message, **markup) - def pytest_runtest_setup(self, item): - if os.getpid() != self.pid: - # test is run in another process than session, run - # coverage manually - embed.init() - - def pytest_runtest_teardown(self, item): - embed.cleanup() - @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): if item.get_closest_marker('no_cover') or 'no_cover' in getattr(item, 'fixturenames', ()): @@ -462,9 +444,7 @@ def pytest_runtest_call(self, item): def switch_context(self, item, when): if self.cov_controller.started: - context = f'{item.nodeid}|{when}' - self.cov_controller.cov.switch_context(context) - os.environ['COV_CORE_CONTEXT'] = context + self.cov_controller.cov.switch_context(f'{item.nodeid}|{when}') @pytest.fixture diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index c89dbac6..15329438 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -299,9 +299,8 @@ def test_term_report_does_not_interact_with_html_output(testdir): dest_dir = testdir.tmpdir.join(DEST_DIR) assert dest_dir.check(dir=True) expected = [dest_dir.join('index.html'), dest_dir.join('test_funcarg_py.html')] - if coverage.version_info >= (7, 5): - expected.insert(0, dest_dir.join('function_index.html')) - expected.insert(0, dest_dir.join('class_index.html')) + expected.insert(0, dest_dir.join('function_index.html')) + expected.insert(0, dest_dir.join('class_index.html')) assert sorted(dest_dir.visit('**/*.html')) == expected assert dest_dir.join('index.html').check() assert result.ret == 0 @@ -432,7 +431,6 @@ def test_markdown_and_markdown_append_pointing_to_same_file_throws_error(testdir assert result.ret == 4 -@pytest.mark.skipif('coverage.version_info < (6, 3)') def test_lcov_output_dir(testdir): script = testdir.makepyfile(SCRIPT) @@ -449,23 +447,6 @@ def test_lcov_output_dir(testdir): assert result.ret == 0 -@pytest.mark.skipif('coverage.version_info >= (6, 3)') -def test_lcov_not_supported(testdir): - script = testdir.makepyfile('a = 1') - result = testdir.runpytest( - '-v', - f'--cov={script.dirpath()}', - '--cov-report=lcov', - script, - ) - result.stderr.fnmatch_lines( - [ - '*argument --cov-report: LCOV output is only supported with coverage.py >= 6.3', - ] - ) - assert result.ret != 0 - - def test_term_output_dir(testdir): script = testdir.makepyfile(SCRIPT) @@ -652,7 +633,6 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): aliased [coverage:run] source = mod -parallel = true {prop.conf} """ ) @@ -713,6 +693,73 @@ def test_foobar(bad): assert result.ret == 0 +def test_celery(pytester): + pytester.makepyfile( + small_celery=""" +import os + +from celery import Celery +from celery.contrib.testing import worker +from testcontainers.redis import RedisContainer +import pytest + +app = Celery("tasks", broker="redis://localhost:6379/0", backend="redis://localhost:6379/0") + +@app.task +def add(x, y): + return x + y + +@pytest.fixture(scope="session") +def redis_container(): + with RedisContainer() as container: + yield container + + +@pytest.fixture +def celery_app(redis_container): + host = redis_container.get_container_host_ip() + port = redis_container.get_exposed_port(6379) + redis_url = f"redis://{host}:{port}/0" + + app.conf.update(broker_url=redis_url, result_backend=redis_url) + return app + +@pytest.fixture +def celery_worker(celery_app): + with worker.start_worker( + celery_app, + pool="prefork", + perform_ping_check=False, + ): + yield + print('CELERY SHUTDOWN') + print('CELERY SHUTDOWN DONE') + print(os.listdir()) + + +def test_add_task(celery_worker): + result = add.delay(4, 4) + assert result.get() == 8 +""" + ) + + pytester.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess", "_exit"] +""" + ) + result = pytester.runpytest('-vv', '-s', '--cov', '--cov-report=term-missing', 'small_celery.py') + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'small_celery* 100%*', + ] + ) + assert result.ret == 0 + + def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): src = testdir.mkdir('src') src.join('parent_script.py').write(SCRIPT_PARENT) @@ -732,7 +779,7 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): source = parent_script child_script -parallel = true +patch = subprocess """ ) @@ -948,6 +995,12 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): def test_central_subprocess(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +""" + ) scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') @@ -971,7 +1024,7 @@ def test_central_subprocess_change_cwd(testdir): coveragerc=""" [run] branch = true -parallel = true +patch = subprocess """, ) @@ -998,7 +1051,7 @@ def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkey '', coveragerc=""" [run] -parallel = true +patch = subprocess """, ) @@ -1029,7 +1082,7 @@ def test_foo(): '', coveragerc=""" [run] -parallel = true +patch = subprocess """, ) result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-branch', script) @@ -1044,6 +1097,12 @@ def test_foo(): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_subprocess_collocated(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +""" + ) scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') @@ -1071,6 +1130,9 @@ def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): dir2 = tmpdir.mkdir('dir2') testdir.tmpdir.join('.coveragerc').write( f""" +[run] +patch = subprocess + [paths] source = {scripts.dirpath()} @@ -1124,43 +1186,6 @@ def test_invalid_coverage_source(testdir): assert not matching_lines -@pytest.mark.skipif("'dev' in pytest.__version__") -@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -@pytest.mark.skipif( - 'tuple(map(int, xdist.__version__.split("."))) >= (2, 3, 0)', - reason='Since pytest-xdist 2.3.0 the parent sys.path is copied in the child process', -) -def test_dist_missing_data(testdir): - """Test failure when using a worker without pytest-cov installed.""" - venv_path = os.path.join(str(testdir.tmpdir), 'venv') - virtualenv.cli_run([venv_path]) - if sys.platform == 'win32': - if platform.python_implementation() == 'PyPy': - exe = os.path.join(venv_path, 'bin', 'python.exe') - else: - exe = os.path.join(venv_path, 'Scripts', 'python.exe') - else: - exe = os.path.join(venv_path, 'bin', 'python') - subprocess.check_call( - [exe, '-mpip', 'install', f'py=={py.__version__}', f'pytest=={pytest.__version__}', f'pytest_xdist=={xdist.__version__}'] - ) - script = testdir.makepyfile(SCRIPT) - - result = testdir.runpytest( - '-v', - '--assert=plain', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - f'--tx=popen//python={exe}', - max_worker_restart_0, - str(script), - ) - result.stdout.fnmatch_lines( - ['The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.'] - ) - - def test_funcarg(testdir): script = testdir.makepyfile(SCRIPT_FUNCARG) @@ -1182,6 +1207,13 @@ def test_funcarg_not_active(testdir): @pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") @pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') def test_cleanup_on_sigterm(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess", "_exit"] +""" + ) + script = testdir.makepyfile( ''' import os, signal, subprocess, sys, time @@ -1204,9 +1236,6 @@ def test_run(): if __name__ == "__main__": signal.signal(signal.SIGTERM, cleanup) - from pytest_cov.embed import cleanup_on_sigterm - cleanup_on_sigterm() - try: time.sleep(10) except BaseException as exc: @@ -1216,7 +1245,13 @@ def test_run(): result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 26-27', '*1 passed*']) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'test_cleanup_on_sigterm* 100%', + '*1 passed*', + ] + ) assert result.ret == 0 @@ -1248,7 +1283,6 @@ def test_run(): assert stdout in [b"^C", b"", b"captured IOError(4, 'Interrupted function call')\\n"] if __name__ == "__main__": - from pytest_cov.embed import cleanup_on_signal, cleanup """ + setup[0] + """ @@ -1267,17 +1301,14 @@ def test_run(): @pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") -@pytest.mark.xfail('sys.platform == "darwin"', reason='Something weird going on Macs...') -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') -@pytest.mark.parametrize( - 'setup', - [ - ('signal.signal(signal.SIGTERM, signal.SIG_DFL); cleanup_on_sigterm()', '88% 18-19'), - ('cleanup_on_sigterm()', '88% 18-19'), - ('cleanup()', '75% 16-19'), - ], -) -def test_cleanup_on_sigterm_sig_dfl(pytester, testdir, setup): +def test_cleanup_on_sigterm_sig_dfl(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +sigterm = true +""" + ) script = testdir.makepyfile( """ import os, signal, subprocess, sys, time @@ -1288,15 +1319,13 @@ def test_run(): proc.terminate() stdout, stderr = proc.communicate() assert not stderr + print([stdout, stderr]) assert stdout == b"" + assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] if __name__ == "__main__": - from pytest_cov.embed import cleanup_on_sigterm, cleanup - """ - + setup[0] - + """ - + foobar = 123 try: time.sleep(10) except BaseException as exc: @@ -1304,16 +1333,23 @@ def test_run(): """ ) - result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + result = testdir.runpytest( + '-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-report=html', script + ) - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* 88% * 18-19', '*1 passed*']) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32"', reason='SIGINT is subtly broken on Windows') -@pytest.mark.xfail('sys.platform == "darwin"', reason='Something weird going on Macs...') -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') def test_cleanup_on_sigterm_sig_dfl_sigint(testdir): + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +sigterm = true +""" + ) script = testdir.makepyfile( ''' import os, signal, subprocess, sys, time @@ -1329,9 +1365,6 @@ def test_run(): assert proc.returncode == 0 if __name__ == "__main__": - from pytest_cov.embed import cleanup_on_signal - cleanup_on_signal(signal.SIGINT) - try: time.sleep(10) except BaseException as exc: @@ -1341,44 +1374,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) - assert result.ret == 0 - - -@pytest.mark.skipif('sys.platform == "win32"', reason='fork not available on Windows') -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') -def test_cleanup_on_sigterm_sig_ign(testdir): - script = testdir.makepyfile( - """ -import os, signal, subprocess, sys, time - -def test_run(): - proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - time.sleep(1) - proc.send_signal(signal.SIGINT) - time.sleep(1) - proc.terminate() - stdout, stderr = proc.communicate() - assert not stderr - assert stdout == b"" - assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] - -if __name__ == "__main__": - signal.signal(signal.SIGINT, signal.SIG_IGN) - - from pytest_cov.embed import cleanup_on_signal - cleanup_on_signal(signal.SIGINT) - - try: - time.sleep(10) - except BaseException as exc: - print("captured %r" % exc) - """ - ) - - result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 100%', '*1 passed*']) assert result.ret == 0 @@ -1804,7 +1800,6 @@ def test_dynamic_context(pytester, testdir, opts, prop): testdir.makepyprojecttoml(f""" [tool.coverage.run] dynamic_context = "test_function" -parallel = true {prop.conf} """) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) @@ -1824,7 +1819,6 @@ def test_simple(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.makepyprojecttoml(f""" [tool.coverage.run] -parallel = true {prop.conf} """) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) @@ -1857,6 +1851,10 @@ def test_do_not_append_coverage(pytester, testdir, opts, prop): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_append_coverage_subprocess(testdir): + testdir.makepyprojecttoml(f""" +[tool.coverage.run] +patch = ["subprocess"] +""") scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') @@ -1881,28 +1879,6 @@ def test_append_coverage_subprocess(testdir): assert result.ret == 0 -def test_pth_failure(monkeypatch): - with open('src/pytest-cov.pth') as fh: - payload = fh.read() - - class SpecificError(Exception): - pass - - def bad_init(): - raise SpecificError - - buff = StringIO() - - from pytest_cov import embed - - monkeypatch.setattr(embed, 'init', bad_init) - monkeypatch.setattr(sys, 'stderr', buff) - monkeypatch.setitem(os.environ, 'COV_CORE_SOURCE', 'foobar') - exec(payload) - expected = "pytest-cov: Failed to setup subprocess coverage. Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" - assert buff.getvalue() == expected - - def test_double_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', f'--cov={script.dirpath()}', script) @@ -1968,6 +1944,7 @@ def find_labels(text, pattern): 'test_contexts.py::test_07|setup': 's7', 'test_contexts.py::test_07|run': 'r7', 'test_contexts.py::test_08|run': 'r8', + 'test_contexts.py::test_08|setup': 'r8', 'test_contexts.py::test_09[1]|setup': 's9-1', 'test_contexts.py::test_09[1]|run': 'r9-1', 'test_contexts.py::test_09[2]|setup': 's9-2', @@ -2018,23 +1995,6 @@ def test_contexts(pytester, testdir, opts): assert line_data[label] == actual, f'Wrong lines for context {context!r}' -@pytest.mark.skipif('coverage.version_info >= (5, 0)') -def test_contexts_not_supported(testdir): - script = testdir.makepyfile('a = 1') - result = testdir.runpytest( - '-v', - f'--cov={script.dirpath()}', - '--cov-context=test', - script, - ) - result.stderr.fnmatch_lines( - [ - '*argument --cov-context: Contexts are only supported with coverage.py >= 5.x', - ] - ) - assert result.ret != 0 - - def test_contexts_no_cover(testdir): script = testdir.makepyfile(""" import pytest diff --git a/tox.ini b/tox.ini index 171e7b66..a9517bee 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ envlist = clean, check, docs, - {py39,py310,py311,py312,py313,pypy39,pypy310}-{pytest83,pytest84}-{xdist36,xdist37}-{coverage78}, + {py39,py310,py311,py312,py313,pypy39,pypy310}-{pytest84}-{xdist38}-{coverage710}, report ignore_basepython_conflict = true @@ -41,7 +41,7 @@ setenv = pytest81: _DEP_PYTEST=pytest==8.1.1 pytest82: _DEP_PYTEST=pytest==8.2.2 pytest83: _DEP_PYTEST=pytest==8.3.5 - pytest84: _DEP_PYTEST=pytest==8.4.0 + pytest84: _DEP_PYTEST=pytest==8.4.2 xdist32: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 xdist33: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 @@ -49,6 +49,7 @@ setenv = xdist35: _DEP_PYTESTXDIST=pytest-xdist==3.5.0 xdist36: _DEP_PYTESTXDIST=pytest-xdist==3.6.1 xdist37: _DEP_PYTESTXDIST=pytest-xdist==3.7.0 + xdist38: _DEP_PYTESTXDIST=pytest-xdist==3.8.0 xdistdev: _DEP_PYTESTXDIST=git+https://github.com/pytest-dev/pytest-xdist.git#egg=pytest-xdist coverage72: _DEP_COVERAGE=coverage==7.2.7 @@ -58,6 +59,8 @@ setenv = coverage76: _DEP_COVERAGE=coverage==7.6.12 coverage77: _DEP_COVERAGE=coverage==7.7.1 coverage78: _DEP_COVERAGE=coverage==7.8.2 + coverage79: _DEP_COVERAGE=coverage==7.9.2 + coverage710: _DEP_COVERAGE=coverage==7.10.6 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv = @@ -66,6 +69,8 @@ deps = {env:_DEP_PYTEST:pytest} {env:_DEP_PYTESTXDIST:pytest-xdist} {env:_DEP_COVERAGE:coverage} + celery[redis] + testcontainers[redis] pip_pre = true commands = {posargs:pytest -vv} From 2cf3703a0ba26c4d7ee5f4a4482ec792727a4832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 12:52:32 +0300 Subject: [PATCH 02/25] Unskip this test and remove the session scoping for a fixture because it would behave differently depending on the number of xdist nodes. --- tests/contextful.py | 2 +- tests/test_pytest_cov.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/contextful.py b/tests/contextful.py index b1d0804b..6e57a601 100644 --- a/tests/contextful.py +++ b/tests/contextful.py @@ -58,7 +58,7 @@ def test_06(some_data, more_data): assert len(some_data) == len(more_data) # r6 -@pytest.fixture(scope='session') +@pytest.fixture def expensive_data(): return list(range(10)) # s7 diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 15329438..522ba421 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1944,7 +1944,7 @@ def find_labels(text, pattern): 'test_contexts.py::test_07|setup': 's7', 'test_contexts.py::test_07|run': 'r7', 'test_contexts.py::test_08|run': 'r8', - 'test_contexts.py::test_08|setup': 'r8', + 'test_contexts.py::test_08|setup': 's7', 'test_contexts.py::test_09[1]|setup': 's9-1', 'test_contexts.py::test_09[1]|run': 'r9-1', 'test_contexts.py::test_09[2]|setup': 's9-2', @@ -1963,8 +1963,6 @@ def find_labels(text, pattern): } -@pytest.mark.skipif('coverage.version_info < (5, 0)') -@pytest.mark.skipif('coverage.version_info > (6, 4)') @xdist_params def test_contexts(pytester, testdir, opts): with open(os.path.join(os.path.dirname(__file__), 'contextful.py')) as f: From 9d4bb37b28c9dd8462e2bc95f92b89165ad804a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 13:48:32 +0300 Subject: [PATCH 03/25] Regenerate env list. --- .github/workflows/test.yml | 464 +++--------------------- ci/templates/.github/workflows/test.yml | 2 +- 2 files changed, 44 insertions(+), 422 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 631c2b31..a2d16da5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,512 +60,134 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist36-coverage78 (ubuntu)' + - name: 'py39-pytest84-xdist38-coverage710 (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest83-xdist36-coverage78' + tox_env: 'py39-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist36-coverage78 (windows)' + - name: 'py39-pytest84-xdist38-coverage710 (windows)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest83-xdist36-coverage78' + tox_env: 'py39-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py39-pytest83-xdist36-coverage78 (macos)' + - name: 'py39-pytest84-xdist38-coverage710 (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' - tox_env: 'py39-pytest83-xdist36-coverage78' + tox_env: 'py39-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'py39-pytest83-xdist37-coverage78 (ubuntu)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist37-coverage78 (windows)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'py39-pytest83-xdist37-coverage78 (macos)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'arm64' - tox_env: 'py39-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'py39-pytest84-xdist36-coverage78 (ubuntu)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py39-pytest84-xdist36-coverage78 (windows)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'py39-pytest84-xdist36-coverage78 (macos)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'arm64' - tox_env: 'py39-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'py39-pytest84-xdist37-coverage78 (ubuntu)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest84-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py39-pytest84-xdist37-coverage78 (windows)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'x64' - tox_env: 'py39-pytest84-xdist37-coverage78' - os: 'windows-latest' - - name: 'py39-pytest84-xdist37-coverage78 (macos)' - python: '3.9' - toxpython: 'python3.9' - python_arch: 'arm64' - tox_env: 'py39-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'py310-pytest83-xdist36-coverage78 (ubuntu)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest83-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py310-pytest83-xdist36-coverage78 (windows)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest83-xdist36-coverage78' - os: 'windows-latest' - - name: 'py310-pytest83-xdist36-coverage78 (macos)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'arm64' - tox_env: 'py310-pytest83-xdist36-coverage78' - os: 'macos-latest' - - name: 'py310-pytest83-xdist37-coverage78 (ubuntu)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py310-pytest83-xdist37-coverage78 (windows)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'py310-pytest83-xdist37-coverage78 (macos)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'arm64' - tox_env: 'py310-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'py310-pytest84-xdist36-coverage78 (ubuntu)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py310-pytest84-xdist36-coverage78 (windows)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'x64' - tox_env: 'py310-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'py310-pytest84-xdist36-coverage78 (macos)' - python: '3.10' - toxpython: 'python3.10' - python_arch: 'arm64' - tox_env: 'py310-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'py310-pytest84-xdist37-coverage78 (ubuntu)' + - name: 'py310-pytest84-xdist38-coverage710 (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest84-xdist37-coverage78' + tox_env: 'py310-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py310-pytest84-xdist37-coverage78 (windows)' + - name: 'py310-pytest84-xdist38-coverage710 (windows)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest84-xdist37-coverage78' + tox_env: 'py310-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py310-pytest84-xdist37-coverage78 (macos)' + - name: 'py310-pytest84-xdist38-coverage710 (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' - tox_env: 'py310-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'py311-pytest83-xdist36-coverage78 (ubuntu)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest83-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py311-pytest83-xdist36-coverage78 (windows)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest83-xdist36-coverage78' - os: 'windows-latest' - - name: 'py311-pytest83-xdist36-coverage78 (macos)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'arm64' - tox_env: 'py311-pytest83-xdist36-coverage78' + tox_env: 'py310-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'py311-pytest83-xdist37-coverage78 (ubuntu)' + - name: 'py311-pytest84-xdist38-coverage710 (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest83-xdist37-coverage78' + tox_env: 'py311-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py311-pytest83-xdist37-coverage78 (windows)' + - name: 'py311-pytest84-xdist38-coverage710 (windows)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest83-xdist37-coverage78' + tox_env: 'py311-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py311-pytest83-xdist37-coverage78 (macos)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'arm64' - tox_env: 'py311-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'py311-pytest84-xdist36-coverage78 (ubuntu)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py311-pytest84-xdist36-coverage78 (windows)' + - name: 'py311-pytest84-xdist38-coverage710 (macos)' python: '3.11' toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'py311-pytest84-xdist36-coverage78 (macos)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'arm64' - tox_env: 'py311-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'py311-pytest84-xdist37-coverage78 (ubuntu)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest84-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py311-pytest84-xdist37-coverage78 (windows)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'x64' - tox_env: 'py311-pytest84-xdist37-coverage78' - os: 'windows-latest' - - name: 'py311-pytest84-xdist37-coverage78 (macos)' - python: '3.11' - toxpython: 'python3.11' - python_arch: 'arm64' - tox_env: 'py311-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'py312-pytest83-xdist36-coverage78 (ubuntu)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest83-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py312-pytest83-xdist36-coverage78 (windows)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest83-xdist36-coverage78' - os: 'windows-latest' - - name: 'py312-pytest83-xdist36-coverage78 (macos)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'arm64' - tox_env: 'py312-pytest83-xdist36-coverage78' - os: 'macos-latest' - - name: 'py312-pytest83-xdist37-coverage78 (ubuntu)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py312-pytest83-xdist37-coverage78 (windows)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'py312-pytest83-xdist37-coverage78 (macos)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'arm64' - tox_env: 'py312-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'py312-pytest84-xdist36-coverage78 (ubuntu)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py312-pytest84-xdist36-coverage78 (windows)' - python: '3.12' - toxpython: 'python3.12' - python_arch: 'x64' - tox_env: 'py312-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'py312-pytest84-xdist36-coverage78 (macos)' - python: '3.12' - toxpython: 'python3.12' python_arch: 'arm64' - tox_env: 'py312-pytest84-xdist36-coverage78' + tox_env: 'py311-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'py312-pytest84-xdist37-coverage78 (ubuntu)' + - name: 'py312-pytest84-xdist38-coverage710 (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest84-xdist37-coverage78' + tox_env: 'py312-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py312-pytest84-xdist37-coverage78 (windows)' + - name: 'py312-pytest84-xdist38-coverage710 (windows)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest84-xdist37-coverage78' + tox_env: 'py312-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py312-pytest84-xdist37-coverage78 (macos)' + - name: 'py312-pytest84-xdist38-coverage710 (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' - tox_env: 'py312-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'py313-pytest83-xdist36-coverage78 (ubuntu)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest83-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py313-pytest83-xdist36-coverage78 (windows)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest83-xdist36-coverage78' - os: 'windows-latest' - - name: 'py313-pytest83-xdist36-coverage78 (macos)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'arm64' - tox_env: 'py313-pytest83-xdist36-coverage78' + tox_env: 'py312-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'py313-pytest83-xdist37-coverage78 (ubuntu)' + - name: 'py313-pytest84-xdist38-coverage710 (ubuntu)' python: '3.13' toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'py313-pytest83-xdist37-coverage78' + tox_env: 'py313-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'py313-pytest83-xdist37-coverage78 (windows)' + - name: 'py313-pytest84-xdist38-coverage710 (windows)' python: '3.13' toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'py313-pytest83-xdist37-coverage78' + tox_env: 'py313-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'py313-pytest83-xdist37-coverage78 (macos)' + - name: 'py313-pytest84-xdist38-coverage710 (macos)' python: '3.13' toxpython: 'python3.13' python_arch: 'arm64' - tox_env: 'py313-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'py313-pytest84-xdist36-coverage78 (ubuntu)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'py313-pytest84-xdist36-coverage78 (windows)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'py313-pytest84-xdist36-coverage78 (macos)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'arm64' - tox_env: 'py313-pytest84-xdist36-coverage78' - os: 'macos-latest' - - name: 'py313-pytest84-xdist37-coverage78 (ubuntu)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest84-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'py313-pytest84-xdist37-coverage78 (windows)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'x64' - tox_env: 'py313-pytest84-xdist37-coverage78' - os: 'windows-latest' - - name: 'py313-pytest84-xdist37-coverage78 (macos)' - python: '3.13' - toxpython: 'python3.13' - python_arch: 'arm64' - tox_env: 'py313-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'pypy39-pytest83-xdist36-coverage78 (ubuntu)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'pypy39-pytest83-xdist36-coverage78 (windows)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist36-coverage78' - os: 'windows-latest' - - name: 'pypy39-pytest83-xdist36-coverage78 (macos)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'arm64' - tox_env: 'pypy39-pytest83-xdist36-coverage78' - os: 'macos-latest' - - name: 'pypy39-pytest83-xdist37-coverage78 (ubuntu)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'pypy39-pytest83-xdist37-coverage78 (windows)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'pypy39-pytest83-xdist37-coverage78 (macos)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'arm64' - tox_env: 'pypy39-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'pypy39-pytest84-xdist36-coverage78 (ubuntu)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'pypy39-pytest84-xdist36-coverage78 (windows)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'x64' - tox_env: 'pypy39-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'pypy39-pytest84-xdist36-coverage78 (macos)' - python: 'pypy-3.9' - toxpython: 'pypy3.9' - python_arch: 'arm64' - tox_env: 'pypy39-pytest84-xdist36-coverage78' + tox_env: 'py313-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'pypy39-pytest84-xdist37-coverage78 (ubuntu)' + - name: 'pypy39-pytest84-xdist38-coverage710 (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest84-xdist37-coverage78' + tox_env: 'pypy39-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'pypy39-pytest84-xdist37-coverage78 (windows)' + - name: 'pypy39-pytest84-xdist38-coverage710 (windows)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest84-xdist37-coverage78' + tox_env: 'pypy39-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'pypy39-pytest84-xdist37-coverage78 (macos)' + - name: 'pypy39-pytest84-xdist38-coverage710 (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' - tox_env: 'pypy39-pytest84-xdist37-coverage78' - os: 'macos-latest' - - name: 'pypy310-pytest83-xdist36-coverage78 (ubuntu)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'pypy310-pytest83-xdist36-coverage78 (windows)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist36-coverage78' - os: 'windows-latest' - - name: 'pypy310-pytest83-xdist36-coverage78 (macos)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'arm64' - tox_env: 'pypy310-pytest83-xdist36-coverage78' - os: 'macos-latest' - - name: 'pypy310-pytest83-xdist37-coverage78 (ubuntu)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist37-coverage78' - os: 'ubuntu-latest' - - name: 'pypy310-pytest83-xdist37-coverage78 (windows)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist37-coverage78' - os: 'windows-latest' - - name: 'pypy310-pytest83-xdist37-coverage78 (macos)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'arm64' - tox_env: 'pypy310-pytest83-xdist37-coverage78' - os: 'macos-latest' - - name: 'pypy310-pytest84-xdist36-coverage78 (ubuntu)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest84-xdist36-coverage78' - os: 'ubuntu-latest' - - name: 'pypy310-pytest84-xdist36-coverage78 (windows)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'x64' - tox_env: 'pypy310-pytest84-xdist36-coverage78' - os: 'windows-latest' - - name: 'pypy310-pytest84-xdist36-coverage78 (macos)' - python: 'pypy-3.10' - toxpython: 'pypy3.10' - python_arch: 'arm64' - tox_env: 'pypy310-pytest84-xdist36-coverage78' + tox_env: 'pypy39-pytest84-xdist38-coverage710' os: 'macos-latest' - - name: 'pypy310-pytest84-xdist37-coverage78 (ubuntu)' + - name: 'pypy310-pytest84-xdist38-coverage710 (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest84-xdist37-coverage78' + tox_env: 'pypy310-pytest84-xdist38-coverage710' os: 'ubuntu-latest' - - name: 'pypy310-pytest84-xdist37-coverage78 (windows)' + - name: 'pypy310-pytest84-xdist38-coverage710 (windows)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest84-xdist37-coverage78' + tox_env: 'pypy310-pytest84-xdist38-coverage710' os: 'windows-latest' - - name: 'pypy310-pytest84-xdist37-coverage78 (macos)' + - name: 'pypy310-pytest84-xdist38-coverage710 (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' - tox_env: 'pypy310-pytest84-xdist37-coverage78' + tox_env: 'pypy310-pytest84-xdist38-coverage710' os: 'macos-latest' steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 diff --git a/ci/templates/.github/workflows/test.yml b/ci/templates/.github/workflows/test.yml index 22fec036..b1e9f361 100644 --- a/ci/templates/.github/workflows/test.yml +++ b/ci/templates/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: - {python-version: "pypy-3.9", tox-python-version: "pypy3"} - {python-version: "3.11", tox-python-version: "py311"} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 From fd3513d2dce5973ab10d987b73efdb4b5e348f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 20:45:58 +0300 Subject: [PATCH 04/25] Sort out windows test issues and make sigbreak test more complete. --- tests/test_pytest_cov.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 522ba421..581499b0 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -693,6 +693,7 @@ def test_foobar(bad): assert result.ret == 0 +@pytest.mark.skipif('sys.platform == "win32"', reason='No redis server on Windows') def test_celery(pytester): pytester.makepyfile( small_celery=""" @@ -1259,17 +1260,26 @@ def test_run(): @pytest.mark.parametrize( 'setup', [ - ('signal.signal(signal.SIGBREAK, signal.SIG_DFL); cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), - ('cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), - ('cleanup()', '73% 19-22'), + ('signal.signal(signal.SIGBREAK, signal.SIG_DFL)', '62% 4, 23-28'), + ('signal.signal(signal.SIGBREAK, cleanup)', '100%'), + ('', '67% 4, 25-28'), ], ) def test_cleanup_on_sigterm_sig_break(pytester, testdir, setup): # worth a read: https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/ + testdir.makepyprojecttoml( + """ +[tool.coverage.run] +patch = ["subprocess"] +""" + ) script = testdir.makepyfile( """ import os, signal, subprocess, sys, time +def cleanup(num, frame): + raise Exception() + def test_run(): proc = subprocess.Popen( [sys.executable, __file__], @@ -1280,7 +1290,11 @@ def test_run(): proc.send_signal(signal.CTRL_BREAK_EVENT) stdout, stderr = proc.communicate() assert not stderr - assert stdout in [b"^C", b"", b"captured IOError(4, 'Interrupted function call')\\n"] + assert stdout in [ + b"^C", + b"", + b"captured Exception()\\r\\n", + b"captured IOError(4, 'Interrupted function call')\\n"] if __name__ == "__main__": """ @@ -1620,17 +1634,6 @@ def test_foo(): SCRIPT_SIMPLE_RESULT = '4 * 100%' -@pytest.mark.skipif('tuple(map(int, xdist.__version__.split("."))) >= (3, 0, 2)', reason='--boxed option was removed in version 3.0.2') -@pytest.mark.skipif('sys.platform == "win32"') -def test_dist_boxed(testdir): - script = testdir.makepyfile(SCRIPT_SIMPLE) - - result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--boxed', script) - - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) - assert result.ret == 0 - - @pytest.mark.skipif('sys.platform == "win32"') @pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason='strange optimization on PyPy3') def test_dist_bare_cov(testdir): From fde12620d82e691be0841c5914bd6ad77a593335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 20:52:06 +0300 Subject: [PATCH 05/25] Skip the celery test on osx too, and improve the style on some skipifs. --- tests/test_pytest_cov.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 581499b0..77cb8d78 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -21,7 +21,6 @@ import pytest_cov.plugin -coverage, platform # required for skipif mark on test_cov_min_from_coveragerc max_worker_restart_0 = '--max-worker-restart=0' @@ -693,7 +692,8 @@ def test_foobar(bad): assert result.ret == 0 -@pytest.mark.skipif('sys.platform == "win32"', reason='No redis server on Windows') +@pytest.mark.skipif(sys.platform == 'win32', reason='No redis server on Windows') +@pytest.mark.skipif(sys.platform == 'darwin', reason='No redis server on OSX') def test_celery(pytester): pytester.makepyfile( small_celery=""" @@ -1205,8 +1205,7 @@ def test_funcarg_not_active(testdir): assert result.ret == 0 -@pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') +@pytest.mark.skipif(sys.platform == 'win32', reason="SIGTERM isn't really supported on Windows") def test_cleanup_on_sigterm(testdir): testdir.makepyprojecttoml( """ @@ -1256,7 +1255,7 @@ def test_run(): assert result.ret == 0 -@pytest.mark.skipif('sys.platform != "win32"') +@pytest.mark.skipif(sys.platform != 'win32', reason='SIGBREAK is Windows only') @pytest.mark.parametrize( 'setup', [ From 19bdf83394b564b245b36354b290252febe9e3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 21:36:14 +0300 Subject: [PATCH 06/25] Add pypy3.11 --- .github/workflows/test.yml | 18 ++++++++++++++++++ tox.ini | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a2d16da5..24e75193 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -186,6 +186,24 @@ jobs: python_arch: 'arm64' tox_env: 'pypy310-pytest84-xdist38-coverage710' os: 'macos-latest' + - name: 'pypy311-pytest84-xdist38-coverage710 (ubuntu)' + python: 'pypy-3.11' + toxpython: 'pypy3.11' + python_arch: 'x64' + tox_env: 'pypy311-pytest84-xdist38-coverage710' + os: 'ubuntu-latest' + - name: 'pypy311-pytest84-xdist38-coverage710 (windows)' + python: 'pypy-3.11' + toxpython: 'pypy3.11' + python_arch: 'x64' + tox_env: 'pypy311-pytest84-xdist38-coverage710' + os: 'windows-latest' + - name: 'pypy311-pytest84-xdist38-coverage710 (macos)' + python: 'pypy-3.11' + toxpython: 'pypy3.11' + python_arch: 'arm64' + tox_env: 'pypy311-pytest84-xdist38-coverage710' + os: 'macos-latest' steps: - uses: actions/checkout@v4 with: diff --git a/tox.ini b/tox.ini index a9517bee..73c7759d 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ envlist = clean, check, docs, - {py39,py310,py311,py312,py313,pypy39,pypy310}-{pytest84}-{xdist38}-{coverage710}, + {py39,py310,py311,py312,py313,pypy39,pypy310,pypy311}-{pytest84}-{xdist38}-{coverage710}, report ignore_basepython_conflict = true From baa7f23a77e3a89f5995881baacbb3133f79c9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 22:59:02 +0300 Subject: [PATCH 07/25] Specialize this assertion for pypy. --- tests/test_pytest_cov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 77cb8d78..26de393a 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1261,7 +1261,7 @@ def test_run(): [ ('signal.signal(signal.SIGBREAK, signal.SIG_DFL)', '62% 4, 23-28'), ('signal.signal(signal.SIGBREAK, cleanup)', '100%'), - ('', '67% 4, 25-28'), + ('', '80% 4, 27-28' if platform.python_implementation() == "PyPy" else '67% 4, 25-28'), ], ) def test_cleanup_on_sigterm_sig_break(pytester, testdir, setup): From 8b7c45aa2fcda6342f272055310d71025a184cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 8 Sep 2025 01:19:59 +0300 Subject: [PATCH 08/25] Fix assertion. --- tests/test_pytest_cov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 26de393a..36b1230b 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1261,7 +1261,7 @@ def test_run(): [ ('signal.signal(signal.SIGBREAK, signal.SIG_DFL)', '62% 4, 23-28'), ('signal.signal(signal.SIGBREAK, cleanup)', '100%'), - ('', '80% 4, 27-28' if platform.python_implementation() == "PyPy" else '67% 4, 25-28'), + ('', '81% 4, 27-28' if platform.python_implementation() == 'PyPy' else '67% 4, 25-28'), ], ) def test_cleanup_on_sigterm_sig_break(pytester, testdir, setup): From d7a38501bd66ad33b777f236f25fb18ceb1f0116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 8 Sep 2025 03:11:30 +0300 Subject: [PATCH 09/25] Loosen up assertion. --- tests/test_pytest_cov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 36b1230b..97e86192 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1261,7 +1261,7 @@ def test_run(): [ ('signal.signal(signal.SIGBREAK, signal.SIG_DFL)', '62% 4, 23-28'), ('signal.signal(signal.SIGBREAK, cleanup)', '100%'), - ('', '81% 4, 27-28' if platform.python_implementation() == 'PyPy' else '67% 4, 25-28'), + ('', '*% 4, 2*-28' if platform.python_implementation() == 'PyPy' else '67% 4, 25-28'), ], ) def test_cleanup_on_sigterm_sig_break(pytester, testdir, setup): From f18d03848f7e06f48efbc8a6b61f6f6c146ffd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 8 Sep 2025 03:35:43 +0300 Subject: [PATCH 10/25] Another pypy specialization. --- tests/test_pytest_cov.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 97e86192..a17c4aa3 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1259,7 +1259,10 @@ def test_run(): @pytest.mark.parametrize( 'setup', [ - ('signal.signal(signal.SIGBREAK, signal.SIG_DFL)', '62% 4, 23-28'), + ( + 'signal.signal(signal.SIGBREAK, signal.SIG_DFL)', + '*% 4, 2*-28' if platform.python_implementation() == 'PyPy' else '62% 4, 23-28', + ), ('signal.signal(signal.SIGBREAK, cleanup)', '100%'), ('', '*% 4, 2*-28' if platform.python_implementation() == 'PyPy' else '67% 4, 25-28'), ], From a75e65152f355a62416d1d4e46bf9e3ee9693308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 8 Sep 2025 03:35:53 +0300 Subject: [PATCH 11/25] Update changelog and plugin docs. --- docs/plugins.rst | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index 577870de..6c6b1c13 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -6,19 +6,5 @@ Getting coverage on pytest plugins is a very particular situation. Because of ho entrypoints) it doesn't allow controlling the order in which the plugins load. See `pytest/issues/935 `_ for technical details. -The current way of dealing with this problem is using the append feature and manually starting ``pytest-cov``'s engine, eg:: - - COV_CORE_SOURCE=src COV_CORE_CONFIG=.coveragerc COV_CORE_DATAFILE=.coverage.eager pytest --cov=src --cov-append - -Alternatively you can have this in ``tox.ini`` (if you're using `Tox `_ of course):: - - [testenv] - setenv = - COV_CORE_SOURCE= - COV_CORE_CONFIG={toxinidir}/.coveragerc - COV_CORE_DATAFILE={toxinidir}/.coverage - -And in ``pytest.ini`` / ``tox.ini`` / ``setup.cfg``:: - - [tool:pytest] - addopts = --cov --cov-append +**Currently there is no way to measure your pytest plugin if you use pytest-cov**. +You should change your test invocations to use ``coverage run -m pytest ...`` instead. From c7edd1b92b63097bccfaccafe5afd7a606211b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 8 Sep 2025 03:35:53 +0300 Subject: [PATCH 12/25] Update changelog and plugin docs. --- CHANGELOG.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9caaa8f5..3ac7b56a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,25 @@ Changelog ========= +7.0.0 (2025-09-09) +------------------ + +* Dropped support for subprocesses measurement. + + It was a feature added long time ago when coverage lacked a nice way to measure subprocesses created in tests. + It relied on a ``.pth`` file, there was no way to opt-out and it created bad interations + with `coverage's new patch system `_ added + in `7.10 `_. + + To migrate to this release you might need to enable the suprocess patch, example for ``.coveragerc``: + + .. code-block:: ini + + [run] + patch = subprocess + + This release also requires at least coverage 7.10.6. + 6.3.0 (2025-09-06) ------------------ From fa72cf3b21c30599eb1793d3f769e1b7090e76da Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Fri, 26 Aug 2022 01:35:12 -0400 Subject: [PATCH 13/25] Update package metadata --- .bumpversion.cfg | 6 ++-- docs/releasing.rst | 2 +- pyproject.toml | 78 ++++++++++++++++++++++++++++++++++++++++++++-- tox.ini | 1 + 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 83e32e00..20f27b99 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,9 +3,9 @@ current_version = 6.3.0 commit = True tag = True -[bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" [bumpversion:file (badge):README.rst] search = /v{current_version}.svg diff --git a/docs/releasing.rst b/docs/releasing.rst index 9afe600d..3032344d 100644 --- a/docs/releasing.rst +++ b/docs/releasing.rst @@ -23,7 +23,7 @@ The process for releasing should follow these steps: These files need to be removed to force distutils/setuptools to rebuild everything and recreate the egg-info metadata. #. Build the dists:: - python3 setup.py clean --all sdist bdist_wheel + python -m build #. Verify that the resulting archives (found in ``dist/``) are good. #. Upload the sdist and wheel with twine:: diff --git a/pyproject.toml b/pyproject.toml index e795c6de..b86bf29a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,79 @@ [build-system] -requires = [ - "setuptools>=30.3.0", +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "pytest-cov" +dynamic = ["readme"] +version = "6.3.0" +description = "Pytest plugin for measuring coverage." +license = "MIT" +requires-python = ">=3.9" +authors = [ + { name = "Marc Schlaich", email = "marc.schlaich@gmail.com" }, +] +maintainers = [ + { name = "Ionel Cristian Mărieș", email = "contact@ionelmc.ro" }, +] +keywords = [ + "cover", + "coverage", + "distributed", + "parallel", + "py.test", + "pytest", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Testing", + "Topic :: Utilities", +] +dependencies = [ + "coverage[toml]>=7.10.6", + "pytest>=7", + "pluggy>=1.2", +] + +[project.optional-dependencies] +testing = [ + "fields", + "hunter", + "process-tests", + "pytest-xdist", + "six", + "virtualenv", +] + +[project.entry-points.pytest11] +pytest_cov = "pytest_cov.plugin" + +[project.urls] +Changelog = "https://pytest-cov.readthedocs.io/en/latest/changelog.html" +Documentation = "https://pytest-cov.readthedocs.io/" +Homepage = "https://github.com/pytest-dev/pytest-cov" +"Issue Tracker" = "https://github.com/pytest-dev/pytest-cov/issues" + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/x-rst" +fragments = [ + { path = "README.rst" }, + { path = "CHANGELOG.rst" }, ] [tool.ruff] @@ -50,3 +123,4 @@ force-single-line = true [tool.ruff.format] quote-style = "single" + diff --git a/tox.ini b/tox.ini index 73c7759d..87da6d38 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ passenv = ; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments [tox] +isolated_build = true envlist = clean, check, From 69b0e04eaa5812f64444214f44eb4de6ee3545c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 13:42:33 +0300 Subject: [PATCH 14/25] Update precommit and add a formatter for pyproject.toml. --- .pre-commit-config.yaml | 12 +++++++++--- .taplo.toml | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .taplo.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d2f108a..e8687d3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,15 +6,21 @@ exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' # Note the order is intentional to avoid multiple passes of the hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.12.12 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] - id: ruff-format + - repo: https://github.com/ComPWA/taplo-pre-commit + rev: v0.9.3 + hooks: + - id: taplo-format + - id: taplo-lint - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - exclude: '.*\.pth$' + - id: mixed-line-ending + args: [--fix=lf] - id: debug-statements diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 00000000..8f8054d3 --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,3 @@ +[formatting] +array_auto_collapse = false +indent_string = " " From b5991fbaf25d07405a379a3a5675501bda06614f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 13:43:22 +0300 Subject: [PATCH 15/25] Fix broken/lost stuff after rebasing PR551. --- MANIFEST.in | 30 -------------------- setup.py | 82 ----------------------------------------------------- tox.ini | 10 +++---- 3 files changed, 4 insertions(+), 118 deletions(-) delete mode 100644 MANIFEST.in delete mode 100755 setup.py diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 529ba8f4..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,30 +0,0 @@ -graft docs -graft examples -prune examples/*/.tox -prune examples/*/htmlcov -prune examples/*/*/htmlcov -prune examples/adhoc-layout/*.egg-info -prune examples/src-layout/src/*.egg-info - -graft .github/workflows -graft src -graft ci -graft tests - -include .bumpversion.cfg -include .cookiecutterrc -include .coveragerc -include .editorconfig -include .pre-commit-config.yaml -include .readthedocs.yml -include pytest.ini -include tox.ini - -include AUTHORS.rst -include CHANGELOG.rst -include CONTRIBUTING.rst -include LICENSE -include README.rst -include SECURITY.md - -global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/setup.py b/setup.py deleted file mode 100755 index 46d25a81..00000000 --- a/setup.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python - -import re -from pathlib import Path - -from setuptools import find_packages -from setuptools import setup - - -def read(*names, **kwargs): - with Path(__file__).parent.joinpath(*names).open(encoding=kwargs.get('encoding', 'utf8')) as fh: - return fh.read() - - -setup( - name='pytest-cov', - version='6.3.0', - license='MIT', - description='Pytest plugin for measuring coverage.', - long_description='{}\n{}'.format(read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), - author='Marc Schlaich', - author_email='marc.schlaich@gmail.com', - url='https://github.com/pytest-dev/pytest-cov', - packages=find_packages('src'), - package_dir={'': 'src'}, - py_modules=[path.stem for path in Path('src').glob('*.py')], - include_package_data=True, - zip_safe=False, - classifiers=[ - # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers - 'Development Status :: 5 - Production/Stable', - 'Framework :: Pytest', - 'Intended Audience :: Developers', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Operating System :: Unix', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Software Development :: Testing', - 'Topic :: Utilities', - ], - project_urls={ - 'Documentation': 'https://pytest-cov.readthedocs.io/', - 'Changelog': 'https://pytest-cov.readthedocs.io/en/latest/changelog.html', - 'Issue Tracker': 'https://github.com/pytest-dev/pytest-cov/issues', - }, - keywords=[ - 'cover', - 'coverage', - 'pytest', - 'py.test', - 'distributed', - 'parallel', - ], - python_requires='>=3.9', - install_requires=[ - 'pytest>=6.2.5', - 'coverage[toml]>=7.10.6', - 'pluggy>=1.2', - ], - extras_require={ - 'testing': [ - 'fields', - 'hunter', - 'process-tests', - 'pytest-xdist', - 'virtualenv', - ] - }, - entry_points={ - 'pytest11': [ - 'pytest_cov = pytest_cov.plugin', - ], - }, -) diff --git a/tox.ini b/tox.ini index 87da6d38..0fcaf956 100644 --- a/tox.ini +++ b/tox.ini @@ -79,15 +79,13 @@ commands = [testenv:check] deps = docutils - check-manifest + twine + build pre-commit - readme-renderer - pygments - isort skip_install = true commands = - python setup.py check --strict --metadata --restructuredtext - check-manifest . + python -m build + twine check --strict dist/* pre-commit run --all-files --show-diff-on-failure [testenv:docs] From 97aadd74bcbc00a2078d240e8fe871dd62b83d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 7 Sep 2025 13:44:21 +0300 Subject: [PATCH 16/25] Update some ci config, reformat and apply some lint fixes. --- ci/bootstrap.py | 4 +-- ci/requirements.txt | 9 +++---- pyproject.toml | 37 +++++++++++++-------------- tests/test_pytest_cov.py | 54 +++++++++++++++++----------------------- 4 files changed, 45 insertions(+), 59 deletions(-) diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 08d6c90b..ee69e2c5 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -20,8 +20,6 @@ def exec_in_env(): else: bin_path = env_path / 'bin' if not env_path.exists(): - import subprocess - print(f'Making bootstrap env in: {env_path} ...') try: check_call([sys.executable, '-m', 'venv', env_path]) @@ -59,7 +57,7 @@ def main(): # This uses sys.executable the same way that the call in # cookiecutter-pylibrary/hooks/post_gen_project.py # invokes this bootstrap.py itself. - for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], text=True).splitlines() + for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines() ] tox_environments = [line for line in tox_environments if line.startswith('py')] for template in templates_path.rglob('*'): diff --git a/ci/requirements.txt b/ci/requirements.txt index b4f18520..fdb6b93a 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -1,5 +1,4 @@ -virtualenv>=16.6.0 -pip>=19.1.1 -setuptools>=18.0.1 -tox -twine +pip>=25 +setuptools>=80 +tox>=4 +virtualenv>=20.34 diff --git a/pyproject.toml b/pyproject.toml index b86bf29a..930b3b5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,11 +52,8 @@ dependencies = [ [project.optional-dependencies] testing = [ - "fields", - "hunter", "process-tests", "pytest-xdist", - "six", "virtualenv", ] @@ -64,9 +61,9 @@ testing = [ pytest_cov = "pytest_cov.plugin" [project.urls] -Changelog = "https://pytest-cov.readthedocs.io/en/latest/changelog.html" -Documentation = "https://pytest-cov.readthedocs.io/" -Homepage = "https://github.com/pytest-dev/pytest-cov" +"Sources" = "https://github.com/pytest-dev/pytest-cov" +"Documentation" = "https://pytest-cov.readthedocs.io/" +"Changelog" = "https://pytest-cov.readthedocs.io/en/latest/changelog.html" "Issue Tracker" = "https://github.com/pytest-dev/pytest-cov/issues" [tool.hatch.metadata.hooks.fancy-pypi-readme] @@ -87,30 +84,31 @@ target-version = "py39" [tool.ruff.lint] ignore = [ - "RUF001", # ruff-specific rules ambiguous-unicode-character-string - "S101", # flake8-bandit assert - "S308", # flake8-bandit suspicious-mark-safe-usage - "E501", # pycodestyle line-too-long + "PLC0415", # `import` should be at the top-level of a file + "RUF001", # ruff-specific rules ambiguous-unicode-character-string + "S101", # flake8-bandit assert + "S308", # flake8-bandit suspicious-mark-safe-usage + "E501", # pycodestyle line-too-long ] select = [ - "B", # flake8-bugbear - "C4", # flake8-comprehensions + "B", # flake8-bugbear + "C4", # flake8-comprehensions "DTZ", # flake8-datetimez - "E", # pycodestyle errors + "E", # pycodestyle errors "EXE", # flake8-executable - "F", # pyflakes - "I", # isort + "F", # pyflakes + "I", # isort "INT", # flake8-gettext "PIE", # flake8-pie "PLC", # pylint convention "PLE", # pylint errors - "PT", # flake8-pytest-style + "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib "RSE", # flake8-raise "RUF", # ruff-specific rules - "S", # flake8-bandit - "UP", # pyupgrade - "W", # pycodestyle warnings + "S", # flake8-bandit + "UP", # pyupgrade + "W", # pycodestyle warnings ] [tool.ruff.lint.flake8-pytest-style] @@ -123,4 +121,3 @@ force-single-line = true [tool.ruff.format] quote-style = "single" - diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index a17c4aa3..6ca09fe5 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1,27 +1,21 @@ -# ruff: noqa import collections import glob import os import platform import re -import subprocess import sys -from io import StringIO from itertools import chain +from pathlib import Path +from types import SimpleNamespace import coverage -import py import pytest -import virtualenv -import xdist -from fields import Namespace from process_tests import TestProcess as _TestProcess from process_tests import dump_on_error from process_tests import wait_for_strings import pytest_cov.plugin - max_worker_restart_0 = '--max-worker-restart=0' SCRIPT = """ @@ -167,7 +161,7 @@ def test_foo(cov): def adjust_sys_path(): """Adjust PYTHONPATH during tests to make "helper" importable in SCRIPT.""" orig_path = os.environ.get('PYTHONPATH', None) - new_path = os.path.dirname(__file__) + new_path = str(Path(__file__).parent) if orig_path is not None: new_path = os.pathsep.join([new_path, orig_path]) os.environ['PYTHONPATH'] = new_path @@ -190,7 +184,7 @@ def adjust_sys_path(): ids=['branch2x', 'branch1c', 'branch1a', 'nobranch'], ) def prop(request): - return Namespace( + return SimpleNamespace( code=SCRIPT, code2=SCRIPT2, conf=request.param[0], @@ -347,7 +341,7 @@ def test_xml_output_dir(testdir): def test_json_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=json:' + JSON_REPORT_NAME, script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=json:' + JSON_REPORT_NAME, script) result.stdout.fnmatch_lines( [ @@ -363,7 +357,7 @@ def test_json_output_dir(testdir): def test_markdown_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, script) result.stdout.fnmatch_lines( [ @@ -379,7 +373,7 @@ def test_markdown_output_dir(testdir): def test_markdown_append_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script) result.stdout.fnmatch_lines( [ @@ -397,7 +391,7 @@ def test_markdown_and_markdown_append_work_together(testdir): result = testdir.runpytest( '-v', - '--cov=%s' % script.dirpath(), + f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script, @@ -420,7 +414,7 @@ def test_markdown_and_markdown_append_pointing_to_same_file_throws_error(testdir result = testdir.runpytest( '-v', - '--cov=%s' % script.dirpath(), + f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, '--cov-report=markdown-append:' + MARKDOWN_REPORT_NAME, script, @@ -466,7 +460,7 @@ def test_term_missing_output_dir(testdir): result.stderr.fnmatch_lines( [ - '*argument --cov-report: output specifier not supported for: "term-missing:%s"*' % DEST_DIR, + f'*argument --cov-report: output specifier not supported for: "term-missing:{DEST_DIR}"*', ] ) assert result.ret != 0 @@ -755,7 +749,7 @@ def test_add_task(celery_worker): result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', - f'small_celery* 100%*', + 'small_celery* 100%*', ] ) assert result.ret == 0 @@ -1353,7 +1347,7 @@ def test_run(): '-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-report=html', script ) - result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* 88% * 18-19', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% * 18-19', '*1 passed*']) assert result.ret == 0 @@ -1649,7 +1643,7 @@ def test_dist_bare_cov(testdir): def test_not_started_plugin_does_not_fail(testdir): class ns: - cov_source = [True] + cov_source = (True,) cov_report = '' plugin = pytest_cov.plugin.CovPlugin(ns, None, start=False) @@ -1697,14 +1691,13 @@ def test_external_data_file(testdir): testdir.tmpdir.join('.coveragerc').write( """ [run] -data_file = %s -""" - % testdir.tmpdir.join('some/special/place/coverage-data').ensure() +data_file = {} +""".format(testdir.tmpdir.join('some/special/place/coverage-data').ensure()) ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 - assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) + assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) # noqa: PTH207 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') @@ -1714,14 +1707,13 @@ def test_external_data_file_xdist(testdir): """ [run] parallel = true -data_file = %s -""" - % testdir.tmpdir.join('some/special/place/coverage-data').ensure() +data_file = {} +""".format(testdir.tmpdir.join('some/special/place/coverage-data').ensure()) ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '-n', '1', max_worker_restart_0, script) assert result.ret == 0 - assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) + assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) # noqa: PTH207 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') @@ -1748,7 +1740,7 @@ def test_external_data_file_negative(testdir): result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 - assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) + assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) # noqa: PTH207 @xdist_params @@ -1856,7 +1848,7 @@ def test_do_not_append_coverage(pytester, testdir, opts, prop): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_append_coverage_subprocess(testdir): - testdir.makepyprojecttoml(f""" + testdir.makepyprojecttoml(""" [tool.coverage.run] patch = ["subprocess"] """) @@ -1970,7 +1962,7 @@ def find_labels(text, pattern): @xdist_params def test_contexts(pytester, testdir, opts): - with open(os.path.join(os.path.dirname(__file__), 'contextful.py')) as f: + with Path(__file__).parent.joinpath('contextful.py').open() as f: contextful_tests = f.read() script = testdir.makepyfile(contextful_tests) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-context=test', script, *opts.split()) @@ -1987,7 +1979,7 @@ def test_contexts(pytester, testdir, opts): measured = data.measured_files() assert len(measured) == 1 test_context_path = next(iter(measured)) - assert test_context_path.lower() == os.path.abspath('test_contexts.py').lower() + assert test_context_path.lower() == os.path.abspath('test_contexts.py').lower() # noqa: PTH100 line_data = find_labels(contextful_tests, r'[crst]\d+(?:-\d+)?') for context, label in EXPECTED_CONTEXTS.items(): From 211b5cd41c29916bc643b4a11b00578ba4fd6fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 8 Sep 2025 04:26:44 +0300 Subject: [PATCH 17/25] Fix links. --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3ac7b56a..8dc78caa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -277,8 +277,8 @@ Changelog ------------------ * Fixed ``RecursionError`` that can occur when using - `cleanup_on_signal `__ or - `cleanup_on_sigterm `__. + `cleanup_on_signal `__ or + `cleanup_on_sigterm `__. See: `#294 `_. The 2.7.x releases of pytest-cov should be considered broken regarding aforementioned cleanup API. * Added compatibility with future xdist release that deprecates some internals From 82f999391073f2fb8ae422af452602f310086a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 8 Sep 2025 04:41:10 +0300 Subject: [PATCH 18/25] Update changelog. --- AUTHORS.rst | 1 + CHANGELOG.rst | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index bcb66ddc..42933ffa 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -64,3 +64,4 @@ Authors * Dawn James - https://github.com/dawngerpony * Tsvika Shapira - https://github.com/tsvikas * Marcos Boger - https://github.com/marcosboger +* Ofek Lev - https://github.com/ofek diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8dc78caa..4524aad3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,11 @@ Changelog patch = subprocess This release also requires at least coverage 7.10.6. +* Switched packaging to have metadata completely in ``pyproject.toml`` and use `hatchling `_ for + building. + Contributed by Ofek Lev in `#551 `_ + with some extras in `#716 `_. +* Removed some not really necessary testing deps like ``six``. 6.3.0 (2025-09-06) ------------------ From a19531e91e1ab753ccf648a2d9bab08a6fddebb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 8 Sep 2025 04:44:12 +0300 Subject: [PATCH 19/25] Switch from build/pre-commit to uv/prek - this should make this faster. --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 0fcaf956..be756012 100644 --- a/tox.ini +++ b/tox.ini @@ -80,13 +80,13 @@ commands = deps = docutils twine - build - pre-commit + uv + prek skip_install = true commands = - python -m build + uv build --sdist --wheel twine check --strict dist/* - pre-commit run --all-files --show-diff-on-failure + prek run --all-files --show-diff-on-failure [testenv:docs] usedevelop = true From bb23eacc5531fb8f499213d8420407e0d72f88e3 Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:36:41 +0800 Subject: [PATCH 20/25] Improve configuration docs --- docs/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index ff6689d8..90c4aed5 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -48,14 +48,14 @@ Or for ``pyproject.toml``: :: Caveats ======= -A unfortunate consequence of coverage.py's history is that ``.coveragerc`` is a magic name: it's the default file but it also +An unfortunate consequence of coverage.py's history is that ``.coveragerc`` is a magic name: it's the default file but it also means "try to also lookup coverage configuration in ``tox.ini`` or ``setup.cfg``". In practical terms this means that if you have multiple configuration files around (``tox.ini``, ``pyproject.toml`` or ``setup.cfg``) you might need to use ``--cov-config`` to make coverage use the correct configuration file. Also, if you change the working directory and also use subprocesses in a test you might also need to use ``--cov-config`` to make pytest-cov -will use the expected configuration file in the subprocess. + use the expected configuration file in the subprocess. Reference ========= From 25f0b2e0cdbc345c0d3e49170f7a328c3e0d805f Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:07:41 +0800 Subject: [PATCH 21/25] Update docs/config.rst Co-authored-by: Ned Batchelder --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 90c4aed5..4369d1e7 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -55,7 +55,7 @@ In practical terms this means that if you have multiple configuration files arou might need to use ``--cov-config`` to make coverage use the correct configuration file. Also, if you change the working directory and also use subprocesses in a test you might also need to use ``--cov-config`` to make pytest-cov - use the expected configuration file in the subprocess. +use the expected configuration file in the subprocess. Reference ========= From f299c590a63a48e51e3ae949993dc7bb1f1d480d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:14:38 +0000 Subject: [PATCH 22/25] Bump the github-actions group with 2 updates Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/checkout` from 4 to 5 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) Updates `actions/setup-python` from 5 to 6 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24e75193..0b0ba059 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -205,10 +205,10 @@ jobs: tox_env: 'pypy311-pytest84-xdist38-coverage710' os: 'macos-latest' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.python_arch }} From 36f1cc2967831dbd4c8aa70086fc86dc495f8b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 9 Sep 2025 13:20:56 +0300 Subject: [PATCH 23/25] Bump pins in template. --- ci/templates/.github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/templates/.github/workflows/test.yml b/ci/templates/.github/workflows/test.yml index b1e9f361..df6150ae 100644 --- a/ci/templates/.github/workflows/test.yml +++ b/ci/templates/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -88,10 +88,10 @@ jobs: {% endfor %} {% endfor %} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: {{ '${{ matrix.python }}' }} architecture: {{ '${{ matrix.python_arch }}' }} From 73424e3999f865eac72e27f09d5fe11b9703cfd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 9 Sep 2025 13:49:03 +0300 Subject: [PATCH 24/25] Cleanup the docs a bit. --- README.rst | 25 ++++++----------- docs/reporting.rst | 67 +++++++++++++++++++++++++++------------------- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/README.rst b/README.rst index 750f8d30..56a7eba3 100644 --- a/README.rst +++ b/README.rst @@ -13,6 +13,7 @@ Overview - |github-actions| * - package - |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| |commits-since| + .. |docs| image:: https://readthedocs.org/projects/pytest-cov/badge/?style=flat :target: https://readthedocs.org/projects/pytest-cov/ :alt: Documentation Status @@ -45,10 +46,11 @@ Overview .. end-badges -This plugin produces coverage reports. Compared to just using ``coverage run`` this plugin does some extras: +This plugin provides coverage functionality as a pytest plugin. Compared to just using ``coverage run`` this plugin does some extras: -* Subprocess support: you can fork or run stuff in a subprocess and will get covered without any fuss. -* Xdist support: you can use all of pytest-xdist's features and still get coverage. +* Automatic erasing and combination of .coverage files and default reporting. +* Support for detailed coverage contexts (add ``--cov-context=test`` to have the full test name including parametrization as the context). +* Xdist support: you can use all of pytest-xdist's features including remote interpreters and still get coverage. * Consistent pytest behavior. If you run ``coverage run -m pytest`` you will have slightly different ``sys.path`` (CWD will be in it, unlike when running ``pytest``). @@ -68,11 +70,10 @@ For distributed testing support install pytest-xdist:: pip install pytest-xdist -Upgrading from ancient pytest-cov ---------------------------------- +Upgrading from pytest-cov 6.3 +----------------------------- -`pytest-cov 2.0` is using a new ``.pth`` file (``pytest-cov.pth``). You may want to manually remove the older -``init_cov_core.pth`` from site-packages as it's not automatically removed. +`pytest-cov 6.3` and older were using a ``.pth`` file to enable coverage measurements in subprocesses. This was removed in `pytest-cov 7` - use `coverage's patch options `_ to enable subprocess measurements. Uninstalling ------------ @@ -111,10 +112,6 @@ Documentation https://pytest-cov.readthedocs.io/en/latest/ - - - - Coverage Data File ================== @@ -132,12 +129,6 @@ For distributed testing the workers must have the pytest-cov package installed. the plugin must be registered through setuptools for pytest to start the plugin on the worker. -For subprocess measurement environment variables must make it from the main process to the -subprocess. The python used by the subprocess must have pytest-cov installed. The subprocess must -do normal site initialisation so that the environment variables can be detected and coverage -started. See the `subprocess support docs `_ -for more details of how this works. - Security ======== diff --git a/docs/reporting.rst b/docs/reporting.rst index a8da25d4..06bad7cc 100644 --- a/docs/reporting.rst +++ b/docs/reporting.rst @@ -3,12 +3,12 @@ Reporting It is possible to generate any combination of the reports for a single test run. -The available reports are terminal (with or without missing line numbers shown), HTML, XML, JSON, Markdown (either in 'write' or 'append' mode to file), LCOV and -annotated source code. +The available reports are terminal (with or without missing line numbers shown), HTML, XML, JSON, Markdown (either in 'write' or 'append' +mode to file), LCOV and annotated source code. -The terminal report without line numbers (default):: +The default is terminal report without line numbers:: - pytest --cov-report term --cov=myproj tests/ + pytest --cov=myproj tests/ -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- Name Stmts Miss Cover @@ -22,7 +22,7 @@ The terminal report without line numbers (default):: The terminal report with line numbers:: - pytest --cov-report term-missing --cov=myproj tests/ + pytest --cov-report=term-missing --cov=myproj tests/ -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- Name Stmts Miss Cover Missing @@ -49,32 +49,45 @@ The terminal report with skip covered:: You can use ``skip-covered`` with ``term-missing`` as well. e.g. ``--cov-report term-missing:skip-covered`` -The report options below output to files without showing anything on the terminal:: +If any reporting options are used then the default (``--cov-report=term`` is not added automatically). For example this would not show any +terminal output: + +.. code-block:: bash pytest --cov-report html - --cov-report xml - --cov-report json - --cov-report markdown - --cov-report markdown-append:cov-append.md - --cov-report lcov - --cov-report annotate - --cov=myproj tests/ - -The output location for each of these reports can be specified. The output location for the XML, JSON, Markdown and LCOV -report is a file. markdown-append option is specially useful for appending the report to an existing file. Example for GitHub Actions: ---cov-report=markdown-append:${GITHUB_STEP_SUMMARY}. Where as the output location for the HTML and annotated source code reports are -directories:: + --cov-report xml + --cov-report json + --cov-report markdown + --cov-report markdown-append:cov-append.md + --cov-report lcov + --cov-report annotate + --cov=myproj tests/ + +You can specify output paths for reports. The output location for the XML, JSON, Markdown and LCOV +report is a file. Where as the output location for the HTML and annotated source code reports are +directories: + +.. code-block:: bash pytest --cov-report html:cov_html - --cov-report xml:cov.xml - --cov-report json:cov.json - --cov-report markdown:cov.md - --cov-report markdown-append:cov-append.md - --cov-report lcov:cov.info - --cov-report annotate:cov_annotate - --cov=myproj tests/ - -The final report option can also suppress printing to the terminal:: + --cov-report xml:cov.xml + --cov-report json:cov.json + --cov-report markdown:cov.md + --cov-report markdown-append:cov-append.md + --cov-report lcov:cov.info + --cov-report annotate:cov_annotate + --cov=myproj tests/ + +Example for GitHub Actions with ``markdown-append``: + +.. code-block:: bash + + pytest --cov-report=markdown-append:${GITHUB_STEP_SUMMARY}. + --cov=myproj tests/ + +To disable the default ``term`` report provide an empty report: + +.. code-block:: bash pytest --cov-report= --cov=myproj tests/ From 224d8964caad90074a8cf6dc8720b8f70f31629b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 9 Sep 2025 13:55:35 +0300 Subject: [PATCH 25/25] =?UTF-8?q?Bump=20version:=206.3.0=20=E2=86=92=207.0?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- pyproject.toml | 2 +- src/pytest_cov/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 20f27b99..fc1f52cb 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.3.0 +current_version = 7.0.0 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index b28f38bb..8441f282 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -40,7 +40,7 @@ default_context: sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme test_matrix_separate_coverage: 'no' - version: 6.3.0 + version: 7.0.0 version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' diff --git a/README.rst b/README.rst index 56a7eba3..143d9422 100644 --- a/README.rst +++ b/README.rst @@ -40,9 +40,9 @@ Overview :alt: Supported implementations :target: https://pypi.org/project/pytest-cov -.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v6.3.0.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v7.0.0.svg :alt: Commits since latest release - :target: https://github.com/pytest-dev/pytest-cov/compare/v6.3.0...master + :target: https://github.com/pytest-dev/pytest-cov/compare/v7.0.0...master .. end-badges diff --git a/docs/conf.py b/docs/conf.py index 97a92563..f4250bc7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ year = '2010-2024' author = 'pytest-cov contributors' copyright = f'{year}, {author}' -version = release = '6.3.0' +version = release = '7.0.0' pygments_style = 'trac' templates_path = ['.'] diff --git a/pyproject.toml b/pyproject.toml index 930b3b5f..1125a4c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "pytest-cov" dynamic = ["readme"] -version = "6.3.0" +version = "7.0.0" description = "Pytest plugin for measuring coverage." license = "MIT" requires-python = ">=3.9" diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py index 1c10f58d..62d553a4 100644 --- a/src/pytest_cov/__init__.py +++ b/src/pytest_cov/__init__.py @@ -1,6 +1,6 @@ """pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE.""" -__version__ = '6.3.0' +__version__ = '7.0.0' import pytest