diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4f2843c8..98e4fe0e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.0.0 +current_version = 6.1.0 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 1a6fdf34..46b134a9 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.0.0 + version: 6.1.0 version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26c8500b..3f6b8dc4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,113 +60,131 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py39-pytest83-xdist36-coverage77 (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest83-xdist36-coverage76' + tox_env: 'py39-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'py39-pytest83-xdist36-coverage76 (windows)' + - name: 'py39-pytest83-xdist36-coverage77 (windows)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest83-xdist36-coverage76' + tox_env: 'py39-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'py39-pytest83-xdist36-coverage76 (macos)' + - name: 'py39-pytest83-xdist36-coverage77 (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' - tox_env: 'py39-pytest83-xdist36-coverage76' + tox_env: 'py39-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'py310-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py310-pytest83-xdist36-coverage77 (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest83-xdist36-coverage76' + tox_env: 'py310-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'py310-pytest83-xdist36-coverage76 (windows)' + - name: 'py310-pytest83-xdist36-coverage77 (windows)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest83-xdist36-coverage76' + tox_env: 'py310-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'py310-pytest83-xdist36-coverage76 (macos)' + - name: 'py310-pytest83-xdist36-coverage77 (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' - tox_env: 'py310-pytest83-xdist36-coverage76' + tox_env: 'py310-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'py311-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py311-pytest83-xdist36-coverage77 (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest83-xdist36-coverage76' + tox_env: 'py311-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'py311-pytest83-xdist36-coverage76 (windows)' + - name: 'py311-pytest83-xdist36-coverage77 (windows)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest83-xdist36-coverage76' + tox_env: 'py311-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'py311-pytest83-xdist36-coverage76 (macos)' + - name: 'py311-pytest83-xdist36-coverage77 (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' - tox_env: 'py311-pytest83-xdist36-coverage76' + tox_env: 'py311-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'py312-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py312-pytest83-xdist36-coverage77 (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest83-xdist36-coverage76' + tox_env: 'py312-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'py312-pytest83-xdist36-coverage76 (windows)' + - name: 'py312-pytest83-xdist36-coverage77 (windows)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest83-xdist36-coverage76' + tox_env: 'py312-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'py312-pytest83-xdist36-coverage76 (macos)' + - name: 'py312-pytest83-xdist36-coverage77 (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' - tox_env: 'py312-pytest83-xdist36-coverage76' + tox_env: 'py312-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'pypy39-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'py313-pytest83-xdist36-coverage77 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest83-xdist36-coverage77' + os: 'ubuntu-latest' + - name: 'py313-pytest83-xdist36-coverage77 (windows)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest83-xdist36-coverage77' + os: 'windows-latest' + - name: 'py313-pytest83-xdist36-coverage77 (macos)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'arm64' + tox_env: 'py313-pytest83-xdist36-coverage77' + os: 'macos-latest' + - name: 'pypy39-pytest83-xdist36-coverage77 (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist36-coverage76' + tox_env: 'pypy39-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'pypy39-pytest83-xdist36-coverage76 (windows)' + - name: 'pypy39-pytest83-xdist36-coverage77 (windows)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest83-xdist36-coverage76' + tox_env: 'pypy39-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'pypy39-pytest83-xdist36-coverage76 (macos)' + - name: 'pypy39-pytest83-xdist36-coverage77 (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' - tox_env: 'pypy39-pytest83-xdist36-coverage76' + tox_env: 'pypy39-pytest83-xdist36-coverage77' os: 'macos-latest' - - name: 'pypy310-pytest83-xdist36-coverage76 (ubuntu)' + - name: 'pypy310-pytest83-xdist36-coverage77 (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist36-coverage76' + tox_env: 'pypy310-pytest83-xdist36-coverage77' os: 'ubuntu-latest' - - name: 'pypy310-pytest83-xdist36-coverage76 (windows)' + - name: 'pypy310-pytest83-xdist36-coverage77 (windows)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest83-xdist36-coverage76' + tox_env: 'pypy310-pytest83-xdist36-coverage77' os: 'windows-latest' - - name: 'pypy310-pytest83-xdist36-coverage76 (macos)' + - name: 'pypy310-pytest83-xdist36-coverage77 (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' - tox_env: 'pypy310-pytest83-xdist36-coverage76' + tox_env: 'pypy310-pytest83-xdist36-coverage77' os: 'macos-latest' steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6bc8d8cd..4cd7d3e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,13 +6,13 @@ 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.6.5 + rev: v0.11.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/AUTHORS.rst b/AUTHORS.rst index 81e0b8e6..5a2dc63d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -9,7 +9,7 @@ Authors * Marc Abramowitz - \http://marc-abramowitz.com * Thomas Kluyver - https://github.com/takluyver * Guillaume Ayoub - http://www.yabz.fr -* Federico Ceratto - http://firelet.net +* Federico Ceratto - \http://firelet.net * Josh Kalderimis - \http://blog.cookiestack.com * Ionel Cristian Mărieș - https://blog.ionelmc.ro * Christian Ledermann - https://github.com/cleder @@ -62,3 +62,4 @@ Authors * Matthew Gamble - https://github.com/mwgamble * Christian Clauss - https://github.com/cclauss * Dawn James - https://github.com/dawngerpony +* Tsvika Shapira - https://github.com/tsvikas diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d1d60fa3..7de457fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,15 @@ Changelog ========= +6.1.0 (2025-04-01) +------------------ + +* Change terminal output to use full width lines for the coverage header. + Contributed by Tsvika Shapira in `#678 `_. +* Removed unnecessary CovFailUnderWarning. Fixes `#675 `_. +* Fixed the term report not using the precision specified via ``--cov-precision``. + + 6.0.0 (2024-10-29) ------------------ @@ -27,6 +36,7 @@ Changelog Contributed by Dawn James in `#626 `_. * Modernized project's pre-commit hooks to use ruff. Initial POC contributed by Christian Clauss in `#584 `_. +* Dropped support for Python 3.7. 4.1.0 (2023-05-24) ------------------ @@ -43,6 +53,7 @@ Changelog Contributed by Mark Mayo in `#572 `_. * Fixed a skip in the test suite for some old xdist. Contributed by a bunch of people in `#565 `_. +* Dropped support for Python 3.6. 4.0.0 (2022-09-28) diff --git a/README.rst b/README.rst index d7af95a4..d0b3464d 100644 --- a/README.rst +++ b/README.rst @@ -39,9 +39,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.0.0.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v6.1.0.svg :alt: Commits since latest release - :target: https://github.com/pytest-dev/pytest-cov/compare/v6.0.0...master + :target: https://github.com/pytest-dev/pytest-cov/compare/v6.1.0...master .. end-badges diff --git a/docs/conf.py b/docs/conf.py index 0c054b29..f73bf5c2 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.0.0' +version = release = '6.1.0' pygments_style = 'trac' templates_path = ['.'] @@ -39,6 +39,10 @@ html_split_index = False html_short_title = f'{project}-{version}' +linkcheck_anchors_ignore_for_url = [ + r'^https?://(www\.)?github\.com/.*', +] + napoleon_use_ivar = True napoleon_use_rtype = False napoleon_use_param = False diff --git a/setup.py b/setup.py index bc520b80..d773a455 100755 --- a/setup.py +++ b/setup.py @@ -72,12 +72,12 @@ def finalize_options(self): 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})") + fh.write(f'import os, sys;exec({sh.read().replace(" ", " ")!r})') setup( name='pytest-cov', - version='6.0.0', + version='6.1.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'))), diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py index 4caae9dc..716bc6a9 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.0.0' +__version__ = '6.1.0' import pytest @@ -27,12 +27,6 @@ class CovReportWarning(PytestCovWarning): """ -class CovFailUnderWarning(PytestCovWarning): - """ - Indicates that we failed to generate a report. - """ - - class CentralCovContextWarning(PytestCovWarning): """ Indicates that dynamic_context was set to test_function instead of using the builtin --cov-context. diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 650dbcc9..5acafec0 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -1,15 +1,18 @@ """Coverage controllers for use by pytest-cov and nose-cov.""" +import argparse import contextlib import copy import functools import os import random +import shutil import socket import sys import warnings from io import StringIO from pathlib import Path +from typing import Union import coverage from coverage.data import CoverageData @@ -66,13 +69,14 @@ def _data_suffix(name): class CovController: """Base class for different plugin implementations.""" - def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, config=None, nodeid=None): + def __init__(self, options: argparse.Namespace, config: Union[None, object], nodeid: Union[None, str]): """Get some common config used by multiple derived classes.""" - self.cov_source = cov_source - self.cov_report = cov_report - self.cov_config = cov_config - self.cov_append = cov_append - self.cov_branch = cov_branch + self.cov_source = options.cov_source + self.cov_report = options.cov_report + self.cov_config = options.cov_config + self.cov_append = options.cov_append + self.cov_branch = options.cov_branch + self.cov_precision = options.cov_precision self.config = config self.nodeid = nodeid @@ -134,15 +138,39 @@ def get_node_desc(platform, version_info): return 'platform {}, python {}'.format(platform, '{}.{}.{}-{}-{}'.format(*version_info[:5])) @staticmethod - def sep(stream, s, txt): + def get_width(): + # taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L26 + width, _ = shutil.get_terminal_size(fallback=(80, 24)) + # The Windows get_terminal_size may be bogus, let's sanify a bit. + if width < 40: + width = 80 + return width + + def sep(self, stream, s, txt): if hasattr(stream, 'sep'): stream.sep(s, txt) else: - sep_total = max((70 - 2 - len(txt)), 2) - sep_len = sep_total // 2 - sep_extra = sep_total % 2 - out = f'{s * sep_len} {txt} {s * (sep_len + sep_extra)}\n' - stream.write(out) + fullwidth = self.get_width() + # taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L126 + # The goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth. + if sys.platform == 'win32': + # If we print in the last column on windows we are on a + # new line but there is no way to verify/neutralize this + # (we may not know the exact line width). + # So let's be defensive to avoid empty lines in the output. + fullwidth -= 1 + N = max((fullwidth - len(txt) - 2) // (2 * len(s)), 1) + fill = s * N + line = f'{fill} {txt} {fill}' + # In some situations there is room for an extra sepchar at the right, + # in particular if we consider that with a sepchar like "_ " the + # trailing space is not important at the end of the line. + if len(line) + len(s.rstrip()) <= fullwidth: + line += s.rstrip() + # (end of terminalwriter borrowed code) + line += '\n\n' + stream.write(line) @_ensure_topdir def summary(self, stream): @@ -155,15 +183,15 @@ def summary(self, stream): # Output coverage section header. if len(self.node_descs) == 1: - self.sep(stream, '-', f"coverage: {''.join(self.node_descs)}") + self.sep(stream, '_', f'coverage: {"".join(self.node_descs)}') else: - self.sep(stream, '-', 'coverage') + self.sep(stream, '_', 'coverage') for node_desc in sorted(self.node_descs): self.sep(stream, ' ', f'{node_desc}') # Report on any failed workers. if self.failed_workers: - self.sep(stream, '-', 'coverage: failed workers') + self.sep(stream, '_', 'coverage: failed workers') stream.write('The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.\n') for node in self.failed_workers: stream.write(f'{node.gateway.id}\n') @@ -174,6 +202,7 @@ def summary(self, stream): 'show_missing': ('term-missing' in self.cov_report) or None, 'ignore_errors': True, 'file': stream, + 'precision': self.cov_precision, } skip_covered = isinstance(self.cov_report, dict) and 'skip-covered' in self.cov_report.values() options.update({'skip_covered': skip_covered or None}) @@ -341,7 +370,7 @@ def testnodedown(self, node, error): # If worker is not collocated then we must save the data file # that it returns to us. if 'cov_worker_data' in output: - data_suffix = '%s.%s.%06d.%s' % ( + data_suffix = '%s.%s.%06d.%s' % ( # noqa: UP031 socket.gethostname(), os.getpid(), random.randint(0, 999999), # noqa: S311 diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 916efc8e..b131742b 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -5,6 +5,7 @@ import warnings from io import StringIO from pathlib import Path +from typing import TYPE_CHECKING import coverage import pytest @@ -12,11 +13,13 @@ from coverage.results import should_fail_under from . import CovDisabledWarning -from . import CovFailUnderWarning from . import CovReportWarning from . import compat from . import embed +if TYPE_CHECKING: + from .engine import CovController + def validate_report(arg): file_choices = ['annotate', 'html', 'xml', 'json', 'lcov'] @@ -56,8 +59,7 @@ def validate_fail_under(num_str): raise argparse.ArgumentTypeError('An integer or float value is required.') from None if value > 100: raise argparse.ArgumentTypeError( - 'Your desire for over-achievement is admirable but misplaced. ' - 'The maximum value is 100. Perhaps write more integration tests?' + 'Your desire for over-achievement is admirable but misplaced. The maximum value is 100. Perhaps write more integration tests?' ) return value @@ -199,7 +201,7 @@ class CovPlugin: distributed worker. """ - def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False): + def __init__(self, options: argparse.Namespace, pluginmanager, start=True, no_cov_should_warn=False): """Creates a coverage pytest plugin. We read the rc file that coverage uses to get the data file @@ -217,6 +219,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) self._start_path = None self._disabled = False self.options = options + self._wrote_heading = False is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no' if getattr(options, 'no_cov', False): @@ -240,7 +243,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) # worker is started in pytest hook - def start(self, controller_cls, config=None, nodeid=None): + def start(self, controller_cls: type['CovController'], config=None, nodeid=None): if config is None: # fake config option for engine class Config: @@ -248,15 +251,7 @@ class Config: config = Config() - self.cov_controller = controller_cls( - self.options.cov_source, - self.options.cov_report, - self.options.cov_config, - self.options.cov_append, - self.options.cov_branch, - config, - nodeid, - ) + self.cov_controller = controller_cls(self.options, config, nodeid) self.cov_controller.start() self._started = True self._start_path = Path.cwd() @@ -352,13 +347,18 @@ def pytest_runtestloop(self, session): p=cov_precision, ) session.config.pluginmanager.getplugin('terminalreporter').write(f'\nERROR: {message}\n', red=True, bold=True) - warnings.warn(CovFailUnderWarning(message), stacklevel=1) # make sure we get the EXIT_TESTSFAILED exit code compat_session.testsfailed += 1 + def write_heading(self, terminalreporter): + if not self._wrote_heading: + terminalreporter.write_sep('=', 'tests coverage') + self._wrote_heading = True + def pytest_terminal_summary(self, terminalreporter): if self._disabled: if self.options.no_cov_should_warn: + self.write_heading(terminalreporter) message = 'Coverage disabled via --no-cov switch!' terminalreporter.write(f'WARNING: {message}\n', red=True, bold=True) warnings.warn(CovDisabledWarning(message), stacklevel=1) @@ -372,14 +372,15 @@ def pytest_terminal_summary(self, terminalreporter): report = self.cov_report.getvalue() - # Avoid undesirable new lines when output is disabled with "--cov-report=". if report: - terminalreporter.write('\n' + report + '\n') + self.write_heading(terminalreporter) + terminalreporter.write(report) if self.options.cov_fail_under is not None and self.options.cov_fail_under > 0: + self.write_heading(terminalreporter) failed = self.cov_total < self.options.cov_fail_under markup = {'red': True, 'bold': True} if failed else {'green': True} - message = '{fail}Required test coverage of {required}% {reached}. ' 'Total coverage: {actual:.2f}%\n'.format( + message = '{fail}Required test coverage of {required}% {reached}. Total coverage: {actual:.2f}%\n'.format( required=self.options.cov_fail_under, actual=self.cov_total, fail='FAIL ' if failed else '', @@ -426,7 +427,7 @@ def switch_context(self, item, when): @pytest.fixture -def no_cover(): # noqa: PT004 +def no_cover(): """A pytest fixture to disable coverage.""" diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 8aa2a339..81184f8b 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -207,7 +207,7 @@ def test_central(pytester, testdir, prop): result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_central* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_central* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -218,7 +218,7 @@ def test_annotate(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage annotated source written next to source', '*10 passed*', ] @@ -233,7 +233,7 @@ def test_annotate_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage annotated source written to dir ' + DEST_DIR, '*10 passed*', ] @@ -251,7 +251,7 @@ def test_html(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir htmlcov', '*10 passed*', ] @@ -269,7 +269,7 @@ def test_html_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir ' + DEST_DIR, '*10 passed*', ] @@ -289,7 +289,7 @@ def test_term_report_does_not_interact_with_html_output(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir ' + DEST_DIR, '*1 passed*', ] @@ -317,7 +317,7 @@ def test_html_configured_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir somewhere', '*10 passed*', ] @@ -335,7 +335,7 @@ def test_xml_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage XML written to file ' + XML_REPORT_NAME, '*10 passed*', ] @@ -351,7 +351,7 @@ def test_json_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage JSON written to file ' + JSON_REPORT_NAME, '*10 passed*', ] @@ -368,7 +368,7 @@ def test_lcov_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage LCOV written to file ' + LCOV_REPORT_NAME, '*10 passed*', ] @@ -414,7 +414,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, + '*argument --cov-report: output specifier not supported for: "term-missing:%s"*' % DEST_DIR, ] ) assert result.ret != 0 @@ -476,6 +476,39 @@ def test_cov_min_float_value_not_reached_cli(testdir): result.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%']) +def test_cov_precision(testdir): + script = testdir.makepyfile(SCRIPT) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-precision=6', script) + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + 'Name Stmts Miss Cover Missing', + '----------------------------------------------------------', + 'test_cov_precision.py 9 1 88.888889% 11', + '----------------------------------------------------------', + 'TOTAL 9 1 88.888889%', + ] + ) + + +def test_cov_precision_from_config(testdir): + script = testdir.makepyfile(SCRIPT) + testdir.tmpdir.join('pyproject.toml').write(""" +[tool.coverage.report] +precision = 6""") + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + 'Name Stmts Miss Cover Missing', + '----------------------------------------------------------------------', + 'test_cov_precision_from_config.py 9 1 88.888889% 11', + '----------------------------------------------------------------------', + 'TOTAL 9 1 88.888889%', + ] + ) + + def test_cov_min_no_report(testdir): script = testdir.makepyfile(SCRIPT) @@ -490,7 +523,7 @@ def test_central_nonspecific(pytester, testdir, prop): testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) # multi-module coverage report assert any(line.startswith('TOTAL ') for line in result.stdout.lines) @@ -520,7 +553,7 @@ def test_central_coveragerc(pytester, testdir, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'test_central_coveragerc* {prop.result} *', '*10 passed*', ] @@ -557,7 +590,7 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'src[\\/]mod* {prop.result} *', '*10 passed*', ] @@ -599,7 +632,7 @@ def test_foobar(bad): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', '*mod* 100%', '*1 passed*', ] @@ -636,7 +669,7 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'src[\\/]child_script* {CHILD_SCRIPT_RESULT}*', f'src[\\/]parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -661,7 +694,7 @@ def test_show_missing_coveragerc(pytester, testdir, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Name * Stmts * Miss * Cover * Missing', f'test_show_missing_coveragerc* {prop.result} * 11*', '*10 passed*', @@ -767,7 +800,7 @@ def test_dist_collocated(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -802,7 +835,7 @@ def test_dist_not_collocated(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -838,7 +871,7 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -850,7 +883,7 @@ def test_central_subprocess(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -876,7 +909,7 @@ def test_central_subprocess_change_cwd(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'*child_script* {CHILD_SCRIPT_RESULT}*', '*parent_script* 100%*', ] @@ -904,7 +937,7 @@ def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkey result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'*child_script* {CHILD_SCRIPT_RESULT}*', ] ) @@ -930,7 +963,7 @@ def test_foo(): result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-branch', script) result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'test_central_subprocess_no_subscript* * 3 * 0 * 100%*', ] ) @@ -948,7 +981,7 @@ def test_dist_subprocess_collocated(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -988,7 +1021,7 @@ def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -1061,7 +1094,7 @@ def test_funcarg(testdir): result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_funcarg* 3 * 100%*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_funcarg* 3 * 100%*', '*1 passed*']) assert result.ret == 0 @@ -1111,7 +1144,7 @@ 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* 26-27', '*1 passed*']) assert result.ret == 0 @@ -1157,7 +1190,7 @@ def test_run(): result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', 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* {setup[1]}', '*1 passed*']) assert result.ret == 0 @@ -1201,7 +1234,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 * -*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) assert result.ret == 0 @@ -1236,7 +1269,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*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) assert result.ret == 0 @@ -1273,7 +1306,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* 89% 22-23', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) assert result.ret == 0 @@ -1505,7 +1538,7 @@ def test_dist_boxed(testdir): 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*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1516,7 +1549,7 @@ def test_dist_bare_cov(testdir): result = testdir.runpytest('-v', '--cov', '-n', '1', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1747,7 +1780,7 @@ def test_append_coverage_subprocess(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -1773,7 +1806,7 @@ def 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" + expected = "pytest-cov: Failed to setup subprocess coverage. Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" assert buff.getvalue() == expected @@ -1781,7 +1814,7 @@ def test_double_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1789,7 +1822,7 @@ def test_double_cov2(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', '--cov', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1804,7 +1837,7 @@ def test_cov_reset_then_set(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-reset', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') diff --git a/tox.ini b/tox.ini index 5dc537e3..3880ea27 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ envlist = clean, check, docs, - {py39,py310,py311,py312,pypy39,pypy310}-{pytest83}-{xdist36}-{coverage76}, + {py39,py310,py311,py312,py313,pypy39,pypy310}-{pytest83}-{xdist36}-{coverage77}, report ignore_basepython_conflict = true @@ -28,6 +28,7 @@ basepython = py310: {env:TOXPYTHON:python3.10} py311: {env:TOXPYTHON:python3.11} py312: {env:TOXPYTHON:python3.12} + py313: {env:TOXPYTHON:python3.13} {bootstrap,clean,check,report,docs}: {env:TOXPYTHON:python3} extras = testing setenv = @@ -38,7 +39,7 @@ setenv = pytest80: _DEP_PYTEST=pytest==8.0.2 pytest81: _DEP_PYTEST=pytest==8.1.1 pytest82: _DEP_PYTEST=pytest==8.2.2 - pytest83: _DEP_PYTEST=pytest==8.3.3 + pytest83: _DEP_PYTEST=pytest==8.3.5 xdist32: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 xdist33: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 @@ -51,7 +52,8 @@ setenv = coverage73: _DEP_COVERAGE=coverage==7.3.4 coverage74: _DEP_COVERAGE=coverage==7.4.4 coverage75: _DEP_COVERAGE=coverage==7.5.4 - coverage76: _DEP_COVERAGE=coverage==7.6.4 + coverage76: _DEP_COVERAGE=coverage==7.6.12 + coverage77: _DEP_COVERAGE=coverage==7.7.1 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv =