diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 09bfcde0..d2649188 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.0.0 +current_version = 6.2.1 commit = True tag = True @@ -22,3 +22,7 @@ replace = version = release = '{new_version}' [bumpversion:file:src/pytest_cov/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' + +[bumpversion:file:.cookiecutterrc] +search = version: {current_version} +replace = version: {new_version} diff --git a/.cookiecutterrc b/.cookiecutterrc index 49e9880e..324a99de 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -1,55 +1,47 @@ # Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) default_context: - allow_tests_inside_package: no - appveyor: no + allow_tests_inside_package: 'no' c_extension_function: '-' c_extension_module: '-' - c_extension_optional: no - c_extension_support: no - c_extension_test_pypi: no - c_extension_test_pypi_username: '-' - codacy: no + c_extension_optional: 'no' + c_extension_support: 'no' + codacy: 'no' codacy_projectid: '[Get ID from https://app.codacy.com/app/ionelmc/pytest-cov/settings]' - codeclimate: no - codecov: no - command_line_interface: no + codeclimate: 'no' + codecov: 'no' + command_line_interface: 'no' command_line_interface_bin_name: '-' - coveralls: no + coveralls: 'no' distribution_name: pytest-cov email: contact@ionelmc.ro + formatter_quote_style: single full_name: Ionel Cristian Mărieș - github_actions: yes - legacy_python: yes + github_actions: 'yes' + github_actions_osx: 'yes' + github_actions_windows: 'yes' license: MIT license - linter: flake8 package_name: pytest_cov - pre_commit: yes + pre_commit: 'yes' project_name: pytest-cov project_short_description: This plugin produces coverage reports. It supports centralised testing and distributed testing in both load and each modes. It also supports coverage of subprocesses. - pypi_badge: yes - pypi_disable_upload: no - release_date: '2021-10-04' + pypi_badge: 'yes' + pypi_disable_upload: 'no' + release_date: '2023-05-24' repo_hosting: github.com repo_hosting_domain: github.com repo_main_branch: master repo_name: pytest-cov repo_username: pytest-dev - requiresio: yes - scrutinizer: no - setup_py_uses_setuptools_scm: no - setup_py_uses_test_runner: no - sphinx_docs: yes + scrutinizer: 'no' + setup_py_uses_setuptools_scm: 'no' + sphinx_docs: 'yes' sphinx_docs_hosting: https://pytest-cov.readthedocs.io/ - sphinx_doctest: no + sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme - test_matrix_configurator: no - test_matrix_separate_coverage: no - test_runner: pytest - travis: no - travis_osx: no - version: 3.0.0 + test_matrix_separate_coverage: 'no' + version: 6.2.1 version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' - year_to: '2022' + year_to: '2024' diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..be006de9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fade785..0a81ed49 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,15 +16,15 @@ jobs: - {python-version: "pypy-3.9", tox-python-version: "pypy3"} - {python-version: "3.11", tox-python-version: "py311"} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: @@ -60,155 +60,515 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py37-pytest73-xdist330-coverage72 (ubuntu)' - python: '3.7' - toxpython: 'python3.7' + - name: 'py39-pytest83-xdist36-coverage78 (ubuntu)' + python: '3.9' + toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py37-pytest73-xdist330-coverage72' + tox_env: 'py39-pytest83-xdist36-coverage78' os: 'ubuntu-latest' - - name: 'py37-pytest73-xdist330-coverage72 (windows)' - python: '3.7' - toxpython: 'python3.7' + - name: 'py39-pytest83-xdist36-coverage78 (windows)' + python: '3.9' + toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py37-pytest73-xdist330-coverage72' + tox_env: 'py39-pytest83-xdist36-coverage78' os: 'windows-latest' - - name: 'py37-pytest73-xdist330-coverage72 (macos)' - python: '3.7' - toxpython: 'python3.7' - python_arch: 'x64' - tox_env: 'py37-pytest73-xdist330-coverage72' + - name: 'py39-pytest83-xdist36-coverage78 (macos)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'arm64' + tox_env: 'py39-pytest83-xdist36-coverage78' os: 'macos-latest' - - name: 'py38-pytest73-xdist330-coverage72 (ubuntu)' - python: '3.8' - toxpython: 'python3.8' + - name: 'py39-pytest83-xdist37-coverage78 (ubuntu)' + python: '3.9' + toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py38-pytest73-xdist330-coverage72' + tox_env: 'py39-pytest83-xdist37-coverage78' os: 'ubuntu-latest' - - name: 'py38-pytest73-xdist330-coverage72 (windows)' - python: '3.8' - toxpython: 'python3.8' + - name: 'py39-pytest83-xdist37-coverage78 (windows)' + python: '3.9' + toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py38-pytest73-xdist330-coverage72' + tox_env: 'py39-pytest83-xdist37-coverage78' os: 'windows-latest' - - name: 'py38-pytest73-xdist330-coverage72 (macos)' - python: '3.8' - toxpython: 'python3.8' + - 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: 'py38-pytest73-xdist330-coverage72' + 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-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'py39-pytest84-xdist37-coverage78 (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest73-xdist330-coverage72' + tox_env: 'py39-pytest84-xdist37-coverage78' os: 'ubuntu-latest' - - name: 'py39-pytest73-xdist330-coverage72 (windows)' + - name: 'py39-pytest84-xdist37-coverage78 (windows)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest73-xdist330-coverage72' + tox_env: 'py39-pytest84-xdist37-coverage78' os: 'windows-latest' - - name: 'py39-pytest73-xdist330-coverage72 (macos)' + - 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: 'py39-pytest73-xdist330-coverage72' + 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-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'py310-pytest83-xdist37-coverage78 (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest73-xdist330-coverage72' + tox_env: 'py310-pytest83-xdist37-coverage78' os: 'ubuntu-latest' - - name: 'py310-pytest73-xdist330-coverage72 (windows)' + - name: 'py310-pytest83-xdist37-coverage78 (windows)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest73-xdist330-coverage72' + tox_env: 'py310-pytest83-xdist37-coverage78' os: 'windows-latest' - - name: 'py310-pytest73-xdist330-coverage72 (macos)' + - 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-pytest73-xdist330-coverage72' + 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)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest84-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py310-pytest84-xdist37-coverage78 (windows)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest84-xdist37-coverage78' + os: 'windows-latest' + - name: 'py310-pytest84-xdist37-coverage78 (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' + os: 'macos-latest' + - name: 'py311-pytest83-xdist37-coverage78 (ubuntu)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest83-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py311-pytest83-xdist37-coverage78 (windows)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest83-xdist37-coverage78' + 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)' + 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-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'py311-pytest84-xdist37-coverage78 (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest73-xdist330-coverage72' + tox_env: 'py311-pytest84-xdist37-coverage78' os: 'ubuntu-latest' - - name: 'py311-pytest73-xdist330-coverage72 (windows)' + - name: 'py311-pytest84-xdist37-coverage78 (windows)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest73-xdist330-coverage72' + tox_env: 'py311-pytest84-xdist37-coverage78' os: 'windows-latest' - - name: 'py311-pytest73-xdist330-coverage72 (macos)' + - 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' + os: 'macos-latest' + - name: 'py312-pytest84-xdist37-coverage78 (ubuntu)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'x64' + tox_env: 'py312-pytest84-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py312-pytest84-xdist37-coverage78 (windows)' + python: '3.12' + toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py311-pytest73-xdist330-coverage72' + tox_env: 'py312-pytest84-xdist37-coverage78' + os: 'windows-latest' + - name: 'py312-pytest84-xdist37-coverage78 (macos)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'arm64' + tox_env: 'py312-pytest84-xdist37-coverage78' os: 'macos-latest' - - name: 'pypy37-pytest73-xdist330-coverage72 (ubuntu)' - python: 'pypy-3.7' - toxpython: 'pypy3.7' + - name: 'py313-pytest83-xdist36-coverage78 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'pypy37-pytest73-xdist330-coverage72' + tox_env: 'py313-pytest83-xdist36-coverage78' os: 'ubuntu-latest' - - name: 'pypy37-pytest73-xdist330-coverage72 (windows)' - python: 'pypy-3.7' - toxpython: 'pypy3.7' + - name: 'py313-pytest83-xdist36-coverage78 (windows)' + python: '3.13' + toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'pypy37-pytest73-xdist330-coverage72' + tox_env: 'py313-pytest83-xdist36-coverage78' os: 'windows-latest' - - name: 'pypy37-pytest73-xdist330-coverage72 (macos)' - python: 'pypy-3.7' - toxpython: 'pypy3.7' + - name: 'py313-pytest83-xdist36-coverage78 (macos)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'arm64' + tox_env: 'py313-pytest83-xdist36-coverage78' + os: 'macos-latest' + - name: 'py313-pytest83-xdist37-coverage78 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'pypy37-pytest73-xdist330-coverage72' + tox_env: 'py313-pytest83-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py313-pytest83-xdist37-coverage78 (windows)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest83-xdist37-coverage78' + os: 'windows-latest' + - name: 'py313-pytest83-xdist37-coverage78 (macos)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'arm64' + tox_env: 'py313-pytest83-xdist37-coverage78' os: 'macos-latest' - - name: 'pypy38-pytest73-xdist330-coverage72 (ubuntu)' - python: 'pypy-3.8' - toxpython: 'pypy3.8' + - name: 'py313-pytest84-xdist36-coverage78 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'pypy38-pytest73-xdist330-coverage72' + tox_env: 'py313-pytest84-xdist36-coverage78' os: 'ubuntu-latest' - - name: 'pypy38-pytest73-xdist330-coverage72 (windows)' - python: 'pypy-3.8' - toxpython: 'pypy3.8' + - name: 'py313-pytest84-xdist36-coverage78 (windows)' + python: '3.13' + toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'pypy38-pytest73-xdist330-coverage72' + tox_env: 'py313-pytest84-xdist36-coverage78' os: 'windows-latest' - - name: 'pypy38-pytest73-xdist330-coverage72 (macos)' - python: 'pypy-3.8' - toxpython: 'pypy3.8' + - 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: 'pypy38-pytest73-xdist330-coverage72' + 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-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'pypy39-pytest83-xdist36-coverage78 (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest73-xdist330-coverage72' + tox_env: 'pypy39-pytest83-xdist36-coverage78' os: 'ubuntu-latest' - - name: 'pypy39-pytest73-xdist330-coverage72 (windows)' + - name: 'pypy39-pytest83-xdist36-coverage78 (windows)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest73-xdist330-coverage72' + tox_env: 'pypy39-pytest83-xdist36-coverage78' os: 'windows-latest' - - name: 'pypy39-pytest73-xdist330-coverage72 (macos)' + - 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-pytest73-xdist330-coverage72' + 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' + os: 'macos-latest' + - name: 'pypy39-pytest84-xdist37-coverage78 (ubuntu)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39-pytest84-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'pypy39-pytest84-xdist37-coverage78 (windows)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39-pytest84-xdist37-coverage78' + os: 'windows-latest' + - name: 'pypy39-pytest84-xdist37-coverage78 (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' + os: 'macos-latest' + - name: 'pypy310-pytest84-xdist37-coverage78 (ubuntu)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest84-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'pypy310-pytest84-xdist37-coverage78 (windows)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest84-xdist37-coverage78' + os: 'windows-latest' + - name: 'pypy310-pytest84-xdist37-coverage78 (macos)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'arm64' + tox_env: 'pypy310-pytest84-xdist37-coverage78' os: 'macos-latest' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.python_arch }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b105884..1d2f108a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,20 @@ -# To install the git pre-commit hook run: -# pre-commit install -# To update the pre-commit hooks run: +# To install the git pre-commit hooks run: +# pre-commit install --install-hooks +# To update the versions: # pre-commit autoupdate +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 + 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.4.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer exclude: '.*\.pth$' - id: debug-statements - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: [--py37-plus] - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 diff --git a/.readthedocs.yml b/.readthedocs.yml index ac76971c..009a913c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,8 +3,11 @@ version: 2 sphinx: configuration: docs/conf.py formats: all +build: + os: ubuntu-22.04 + tools: + python: "3" python: - version: 3 install: - requirements: docs/requirements.txt - method: pip diff --git a/AUTHORS.rst b/AUTHORS.rst index 22ae71f5..5a2dc63d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,15 +1,16 @@ + Authors ======= -* Marc Schlaich - http://www.schlamar.org +* Marc Schlaich - https://github.com/schlamar (\http://www.schlamar.org) * Rick van Hattem - http://wol.ph * Buck Evan - https://github.com/bukzor * Eric Larson - http://larsoner.com -* Marc Abramowitz - http://marc-abramowitz.com +* Marc Abramowitz - \http://marc-abramowitz.com * Thomas Kluyver - https://github.com/takluyver * Guillaume Ayoub - http://www.yabz.fr -* Federico Ceratto - http://firelet.net -* Josh Kalderimis - http://blog.cookiestack.com +* 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 * Alec Nikolas Reiter - https://github.com/justanr @@ -18,7 +19,7 @@ Authors * Michael Elovskikh - https://github.com/wronglink * Saurabh Kumar - https://github.com/theskumar * Michael Elovskikh - https://github.com/wronglink -* Daniel Hahler - https://daniel.hahler.de +* Daniel Hahler - https://github.com/blueyed (\https://daniel.hahler.de) * Florian Bruhin - http://www.the-compiler.org * Zoltan Kozma - https://github.com/kozmaz87 * Francis Niu - https://flniu.github.io @@ -57,4 +58,8 @@ Authors * Colin O'Dell - https://github.com/colinodell * Ronny Pfannschmidt - https://github.com/RonnyPfannschmidt * Christian Fetzer - https://github.com/fetzerch -* Jonathan Stewmon = https://github.com/jstewmon +* Jonathan Stewmon - https://github.com/jstewmon +* 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 efeaf34c..beac9eea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,12 +1,100 @@ + Changelog ========= -4.0.1 (2023-03-27) +6.2.1 (2025-06-12) +------------------ + +* Added a version requirement for pytest's pluggy dependency (1.2.0, released 2023-06-21) that has the required new-style hookwrapper API. +* Removed deprecated license classifier (packaging). +* Disabled coverage warnings in two more situations where they have no value: + + * "module-not-measured" in workers + * "already-imported" in subprocesses + +6.2.0 (2025-06-11) +------------------ + +* The plugin now adds 3 rules in the filter warnings configuration to prevent common coverage warnings being raised as obscure errors:: + + default:unclosed database in `_. +* Removed unnecessary CovFailUnderWarning. Fixes `#675 `_. +* Fixed the term report not using the precision specified via ``--cov-precision``. + + +6.0.0 (2024-10-29) +------------------ + +* Updated various documentation inaccuracies, especially on subprocess handling. +* Changed fail under checks to use the precision set in the coverage configuration. + Now it will perform the check just like ``coverage report`` would. +* Added a ``--cov-precision`` cli option that can override the value set in your coverage configuration. +* Dropped support for now EOL Python 3.8. + +5.0.0 (2024-03-24) +------------------ + +* Removed support for xdist rsync (now deprecated). + Contributed by Matthias Reichenbach in `#623 `_. +* Switched docs theme to Furo. +* Various legacy Python cleanup and CI improvements. + Contributed by Christian Clauss and Hugo van Kemenade in + `#630 `_, + `#631 `_, + `#632 `_ and + `#633 `_. +* Added a ``pyproject.toml`` example in the docs. + 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) ------------------ -* Skip generating the in-memory coverage report when it will not be used. For example, - when ``--cov-report=''`` is used without ``--cov-fail-under``. +* Updated CI with new Pythons and dependencies. +* Removed rsyncdir support. This makes pytest-cov compatible with xdist 3.0. + Contributed by Sorin Sbarnea in `#558 `_. +* Optimized summary generation to not be performed if no reporting is active (for example, + when ``--cov-report=''`` is used without ``--cov-fail-under``). + Contributed by Jonathan Stewmon in `#589 `_. +* Added support for JSON reporting. + Contributed by Matthew Gamble in `#582 `_. +* Refactored code to use f-strings. + 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) @@ -35,7 +123,7 @@ Changelog Contributed by Andre Brisco in `#543 `_ and Colin O'Dell in `#525 `_. * Added support for LCOV output format via `--cov-report=lcov`. Only works with coverage 6.3+. - Contributed by Christian Fetzer in `#536 `_. + Contributed by Christian Fetzer in `#536 `_. * Modernized pytest hook implementation. Contributed by Bruno Oliveira in `#549 `_ and Ronny Pfannschmidt in `#550 `_. @@ -106,7 +194,7 @@ Changelog * Removed the empty `console_scripts` entrypoint that confused some Gentoo build script. I didn't ask why it was so broken cause I didn't want to ruin my day. Contributed by Michał Górny in `#434 `_. -* Fixed the missing `coverage context `_ +* Fixed the missing `coverage context `_ when using subprocesses. Contributed by Bernát Gábor in `#443 `_. * Updated the config section in the docs. @@ -135,7 +223,7 @@ Changelog * Made pytest startup faster when plugin not active by lazy-importing. Contributed by Anders Hovmöller in `#339 `_. * Various CI improvements. - Contributed by Daniel Hahler in `#363 `_ and + Contributed by Daniel Hahler in `#363 `_ and `#364 `_. * Various Python support updates (drop EOL 3.4, test against 3.8 final). Contributed by Hugo van Kemenade in diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e742be47..8e19ab39 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -49,7 +49,7 @@ To set up `pytest-cov` for local development: Now you can make your changes locally. -4. When you're done making changes run all the checks and docs builder with `tox `_ one command:: +4. When you're done making changes run all the checks and docs builder with one command:: tox @@ -68,17 +68,11 @@ If you need some code review or feedback while you're developing the code just m For merging, you should: -1. Include passing tests (run ``tox``) [1]_. +1. Include passing tests (run ``tox``). 2. Update documentation when there's new API, functionality etc. 3. Add a note to ``CHANGELOG.rst`` about the changes. 4. Add yourself to ``AUTHORS.rst``. -.. [1] If you don't have all the necessary Python versions available locally you can rely on GitHub Actions - it will - `run the tests `_ - for each change you add in the pull request. - - It will be slower though ... - Tips ---- diff --git a/MANIFEST.in b/MANIFEST.in index cbb88f74..529ba8f4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,14 +15,16 @@ include .bumpversion.cfg include .cookiecutterrc include .coveragerc include .editorconfig -include tox.ini -include .readthedocs.yml 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/README.rst b/README.rst index 84a85786..61b0b4aa 100644 --- a/README.rst +++ b/README.rst @@ -10,39 +10,23 @@ Overview * - docs - |docs| * - tests - - | |github-actions| |requires| - | + - |github-actions| * - package - - | |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| - | |commits-since| - + - |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 + :target: https://readthedocs.org/projects/pytest-cov/ :alt: Documentation Status .. |github-actions| image:: https://github.com/pytest-dev/pytest-cov/actions/workflows/test.yml/badge.svg :alt: GitHub Actions Status :target: https://github.com/pytest-dev/pytest-cov/actions -.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/pytest-dev/pytest-cov?branch=master&svg=true - :alt: AppVeyor Build Status - :target: https://ci.appveyor.com/project/pytestbot/pytest-cov - -.. |requires| image:: https://requires.io/github/pytest-dev/pytest-cov/requirements.svg?branch=master - :alt: Requirements Status - :target: https://requires.io/github/pytest-dev/pytest-cov/requirements/?branch=master - .. |version| image:: https://img.shields.io/pypi/v/pytest-cov.svg :alt: PyPI Package latest release :target: https://pypi.org/project/pytest-cov .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-cov.svg :target: https://anaconda.org/conda-forge/pytest-cov - -.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v4.0.0.svg - :alt: Commits since latest release - :target: https://github.com/pytest-dev/pytest-cov/compare/v4.0.0...master - .. |wheel| image:: https://img.shields.io/pypi/wheel/pytest-cov.svg :alt: PyPI Wheel :target: https://pypi.org/project/pytest-cov @@ -55,6 +39,10 @@ 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.2.1.svg + :alt: Commits since latest release + :target: https://github.com/pytest-dev/pytest-cov/compare/v6.2.1...master + .. end-badges This plugin produces coverage reports. Compared to just using ``coverage run`` this plugin does some extras: @@ -120,7 +108,7 @@ Would produce a report like:: Documentation ============= - http://pytest-cov.rtfd.org/ + https://pytest-cov.readthedocs.io/en/latest/ @@ -140,15 +128,21 @@ examine it. Limitations =========== -For distributed testing the workers must have the pytest-cov package installed. This is needed since +For distributed testing the workers must have the pytest-cov package installed. This is needed since 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 +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. +started. See the `subprocess support docs `_ +for more details of how this works. + +Security +======== +To report a security vulnerability please use the `Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. Acknowledgements ================ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..da9c516d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 48c46640..08d6c90b 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -1,63 +1,57 @@ #!/usr/bin/env python - import os +import pathlib import subprocess import sys -from os.path import abspath -from os.path import dirname -from os.path import exists -from os.path import join -from os.path import relpath -base_path = dirname(dirname(abspath(__file__))) -templates_path = join(base_path, "ci", "templates") +base_path: pathlib.Path = pathlib.Path(__file__).resolve().parent.parent +templates_path = base_path / 'ci' / 'templates' def check_call(args): - print("+", *args) + print('+', *args) subprocess.check_call(args) def exec_in_env(): - env_path = join(base_path, ".tox", "bootstrap") - if sys.platform == "win32": - bin_path = join(env_path, "Scripts") + env_path = base_path / '.tox' / 'bootstrap' + if sys.platform == 'win32': + bin_path = env_path / 'Scripts' else: - bin_path = join(env_path, "bin") - if not exists(env_path): + bin_path = env_path / 'bin' + if not env_path.exists(): import subprocess - print(f"Making bootstrap env in: {env_path} ...") + print(f'Making bootstrap env in: {env_path} ...') try: - check_call([sys.executable, "-m", "venv", env_path]) + check_call([sys.executable, '-m', 'venv', env_path]) except subprocess.CalledProcessError: try: - check_call([sys.executable, "-m", "virtualenv", env_path]) + check_call([sys.executable, '-m', 'virtualenv', env_path]) except subprocess.CalledProcessError: - check_call(["virtualenv", env_path]) - print("Installing `jinja2` into bootstrap environment...") - check_call([join(bin_path, "pip"), "install", "jinja2", "tox"]) - python_executable = join(bin_path, "python") - if not os.path.exists(python_executable): - python_executable += '.exe' + check_call(['virtualenv', env_path]) + print('Installing `jinja2` into bootstrap environment...') + check_call([bin_path / 'pip', 'install', 'jinja2', 'tox']) + python_executable = bin_path / 'python' + if not python_executable.exists(): + python_executable = python_executable.with_suffix('.exe') - print(f"Re-executing with: {python_executable}") - print("+ exec", python_executable, __file__, "--no-env") - os.execv(python_executable, [python_executable, __file__, "--no-env"]) + print(f'Re-executing with: {python_executable}') + print('+ exec', python_executable, __file__, '--no-env') + os.execv(python_executable, [python_executable, __file__, '--no-env']) def main(): import jinja2 - print(f"Project path: {base_path}") + print(f'Project path: {base_path}') jinja = jinja2.Environment( - loader=jinja2.FileSystemLoader(templates_path), + loader=jinja2.FileSystemLoader(str(templates_path)), trim_blocks=True, lstrip_blocks=True, - keep_trailing_newline=True + keep_trailing_newline=True, ) - tox_environments = [ line.strip() # 'tox' need not be installed globally, but must be importable @@ -68,22 +62,22 @@ def main(): for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], text=True).splitlines() ] tox_environments = [line for line in tox_environments if line.startswith('py')] - - for root, _, files in os.walk(templates_path): - for name in files: - relative = relpath(root, templates_path) - with open(join(base_path, relative, name), "w") as fh: - fh.write(jinja.get_template(join(relative, name)).render(tox_environments=tox_environments)) - print(f"Wrote {name}") - print("DONE.") + for template in templates_path.rglob('*'): + if template.is_file(): + template_path = template.relative_to(templates_path).as_posix() + destination = base_path / template_path + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(jinja.get_template(template_path).render(tox_environments=tox_environments)) + print(f'Wrote {template_path}') + print('DONE.') -if __name__ == "__main__": +if __name__ == '__main__': args = sys.argv[1:] - if args == ["--no-env"]: + if args == ['--no-env']: main() elif not args: exec_in_env() else: - print(f"Unexpected arguments {args}", file=sys.stderr) + print(f'Unexpected arguments: {args}', file=sys.stderr) sys.exit(1) diff --git a/ci/requirements.txt b/ci/requirements.txt index a0ef106f..b4f18520 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -1,5 +1,5 @@ virtualenv>=16.6.0 pip>=19.1.1 setuptools>=18.0.1 -six>=1.14.0 tox +twine diff --git a/ci/templates/.github/workflows/test.yml b/ci/templates/.github/workflows/test.yml index 6aaf1dbc..22fec036 100644 --- a/ci/templates/.github/workflows/test.yml +++ b/ci/templates/.github/workflows/test.yml @@ -17,15 +17,15 @@ jobs: - {python-version: "pypy-3.9", tox-python-version: "pypy3"} - {python-version: "3.11", tox-python-version: "py311"} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: @@ -66,9 +66,9 @@ jobs: {% for env in tox_environments %} {% set prefix = env.split('-')[0] -%} {% if prefix.startswith('pypy') %} -{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} {% set cpython %}pp{{ prefix[4:5] }}{% endset %} -{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} {% else %} {% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} {% set cpython %}cp{{ prefix[2:] }}{% endset %} @@ -77,7 +77,7 @@ jobs: {% for os, python_arch in [ ['ubuntu', 'x64'], ['windows', 'x64'], - ['macos', 'x64'], + ['macos', 'arm64'], ] %} - name: '{{ env }} ({{ os }})' python: '{{ python }}' @@ -88,10 +88,10 @@ jobs: {% endfor %} {% endfor %} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: {{ '${{ matrix.python }}' }} architecture: {{ '${{ matrix.python_arch }}' }} diff --git a/docs/conf.py b/docs/conf.py index d417ed00..9ec7494d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,5 @@ import os -import sphinx_py3doc_enhanced_theme - extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', @@ -13,17 +11,17 @@ 'sphinx.ext.extlinks', ] if os.getenv('SPELLCHECK'): - extensions += 'sphinxcontrib.spelling', + extensions += ('sphinxcontrib.spelling',) spelling_show_suggestions = True spelling_lang = 'en_US' source_suffix = '.rst' master_doc = 'index' project = 'pytest-cov' -year = '2016' +year = '2010-2024' author = 'pytest-cov contributors' copyright = f'{year}, {author}' -version = release = '4.0.0' +version = release = '6.2.1' pygments_style = 'trac' templates_path = ['.'] @@ -31,21 +29,22 @@ 'issue': ('https://github.com/pytest-dev/pytest-cov/issues/%s', '#'), 'pr': ('https://github.com/pytest-dev/pytest-cov/pull/%s', 'PR #'), } - -html_theme = "sphinx_py3doc_enhanced_theme" -html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] +html_theme = 'furo' html_theme_options = { - 'githuburl': 'https://github.com/pytest-dev/pytest-cov/' + 'source_repository': 'https://github.com/pytest-dev/pytest-cov/', + 'source_branch': 'master', + 'source_directory': 'docs/', } html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' -html_split_index = True -html_sidebars = { - '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], -} +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/docs/config.rst b/docs/config.rst index c7bef037..0d3bfacf 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -32,22 +32,30 @@ For full details refer to the `coverage config file`_ documentation. If you use the ``--cov-branch`` option then coverage's ``branch`` option will also get overridden. -If you wish to always add pytest-cov with pytest, you can use ``addopts`` under ``pytest`` or ``tool:pytest`` section. -For example: :: +If you wish to always add pytest-cov with pytest, you can use ``addopts`` under the ``pytest`` or ``tool:pytest`` section of +your ``setup.cfg``, or the ``tool.pytest.ini_options`` section of your ``pyproject.toml`` file. + +For example, in ``setup.cfg``: :: [tool:pytest] addopts = --cov= --cov-report html +Or for ``pyproject.toml``: :: + + [tool.pytest.ini_options] + addopts = "--cov= --cov-report html" + Caveats ======= A 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 your coverage configuration in ``tox.ini`` or ``setup.cfg`` it is paramount -that you also use ``--cov-config=tox.ini`` or ``--cov-config=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. -You might not be affected but it's unlikely that you won't ever use ``chdir`` in a test. +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. Reference ========= diff --git a/docs/contexts.rst b/docs/contexts.rst index cde920d5..2b447463 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -6,7 +6,7 @@ Coverage.py 5.0 can record separate coverage data for `different contexts`_ duri one run of a test suite. Pytest-cov can use this feature to record coverage data for each test individually, with the ``--cov-context=test`` option. -.. _different contexts: https://coverage.readthedocs.io/en/stable/contexts.html +.. _different contexts: https://coverage.readthedocs.io/en/latest/contexts.html The context name recorded in the coverage.py database is the pytest test id, and the phase of execution, one of "setup", "run", or "teardown". These two diff --git a/docs/plugins.rst b/docs/plugins.rst index d06c4ffe..577870de 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -2,7 +2,7 @@ Plugin coverage =============== -Getting coverage on pytest plugins is a very particular situation. Because how pytest implements plugins (using setuptools +Getting coverage on pytest plugins is a very particular situation. Because of how pytest implements plugins (using setuptools entrypoints) it doesn't allow controlling the order in which the plugins load. See `pytest/issues/935 `_ for technical details. @@ -10,7 +10,7 @@ The current way of dealing with this problem is using the append feature and man 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):: +Alternatively you can have this in ``tox.ini`` (if you're using `Tox `_ of course):: [testenv] setenv = diff --git a/docs/releasing.rst b/docs/releasing.rst index ae78228d..9afe600d 100644 --- a/docs/releasing.rst +++ b/docs/releasing.rst @@ -4,19 +4,16 @@ Releasing The process for releasing should follow these steps: -#. Test that docs build and render properly by running ``tox -e docs,spell``. +#. Test that docs build and render properly by running ``tox -e docs``. If there are bogus spelling issues add the words in ``spelling_wordlist.txt``. #. Update ``CHANGELOG.rst`` and ``AUTHORS.rst`` to be up to date. #. Bump the version by running ``bumpversion [ major | minor | patch ]``. This will automatically add a tag. - - Alternatively, you can manually edit the files and run ``git tag v1.2.3`` yourself. #. Push changes and tags with:: git push git push --tags -#. Wait for `AppVeyor `_ - and `GitHub Actions `_ to give the green builds. +#. Wait `GitHub Actions `_ to give the green builds. #. Check that the docs on `ReadTheDocs `_ are built. #. Make sure you have a clean checkout, run ``git status`` to verify. #. Manually clean temporary files (that are ignored and won't show up in ``git status``):: diff --git a/docs/requirements.txt b/docs/requirements.txt index 6fdf26f9..4c1e3b7d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,3 @@ -sphinx==3.0.3 -sphinx-py3doc-enhanced-theme==2.4.0 -docutils==0.16 -jinja2<3.1 +sphinx +furo -e . diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 331db7d4..56044392 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -6,43 +6,72 @@ Normally coverage writes the data via a pretty standard atexit handler. However, 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 multiprocessing, and works around these atexit limitations. However, there are a -few pitfalls that need to be explained. +pytest-cov supports subprocesses, and works around these atexit limitations. However, there are a few pitfalls that need to be explained. -If you use ``multiprocessing.Pool`` -=================================== +But first, how does pytest-cov's subprocess support works? -**pytest-cov** automatically registers a multiprocessing finalizer. The finalizer will only run reliably if the pool is -closed. Closing the pool basically signals the workers that there will be no more work, and they will eventually exit. -Thus one also needs to call `join` on the pool. - -If you use ``multiprocessing.Pool.terminate`` or the context manager API (``__exit__`` -will just call ``terminate``) then the workers can get SIGTERM and then the finalizers won't run or complete in time. -Thus you need to make sure your ``multiprocessing.Pool`` gets a nice and clean exit: +pytest-cov packaging injects a pytest-cov.pth into the installation. This file effectively runs this at *every* python startup: .. code-block:: python - from multiprocessing import Pool + 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 + ) + ) - def f(x): - return x*x +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: - 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. +* ``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 must use the context manager API (e.g.: the pool is managed in third party code you can't change) then you can -register a cleaning SIGTERM handler like so: +If you use ``multiprocessing`` +============================== -.. warning:: +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. - **This technique cannot be used on Python 3.8** (registering signal handlers will cause deadlocks in the pool, - see: https://bugs.python.org/issue38227). +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``): + +.. 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 @@ -52,46 +81,33 @@ register a cleaning SIGTERM handler like so: return x*x if __name__ == '__main__': + p = Pool(5) try: - from pytest_cov.embed import cleanup_on_sigterm - except ImportError: - pass - else: - cleanup_on_sigterm() - - with Pool(5) as p: print(p.map(f, [1, 2, 3])) + finally: + p.close() # Marks the pool as closed. + p.join() # Waits for workers to exit. -If you use ``multiprocessing.Process`` -====================================== - -There's similar issue when using the ``Process`` objects. Don't forget to use ``.join()``: -.. code-block:: python +.. _cleanup_on_sigterm: - from multiprocessing import Process +Signal handlers +=============== - def f(name): - print('hello', name) +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: - if __name__ == '__main__': - try: - from pytest_cov.embed import cleanup_on_sigterm - except ImportError: - pass - else: - cleanup_on_sigterm() +* ``pytest_cov.embed.cleanup_on_sigterm()`` +* ``pytest_cov.embed.cleanup_on_signal(signum)`` (e.g.: ``cleanup_on_signal(signal.SIGHUP)``) - p = Process(target=f, args=('bob',)) - try: - p.start() - finally: - p.join() # necessary so that the Process exists before the test suite exits (thus coverage is collected) +If you use multiprocessing +-------------------------- -.. _cleanup_on_sigterm: +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. @@ -140,7 +156,7 @@ Alternatively you can do this: 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 diff --git a/docs/tox.rst b/docs/tox.rst index 18f9137e..e44de028 100644 --- a/docs/tox.rst +++ b/docs/tox.rst @@ -2,7 +2,7 @@ Tox === -When using `tox `_ you can have ultra-compact configuration - you can have all of it in +When using `tox `_ you can have ultra-compact configuration - you can have all of it in ``tox.ini``:: [tox] diff --git a/examples/adhoc-layout/example/__init__.py b/examples/adhoc-layout/example/__init__.py index 684905a1..36b78a3d 100644 --- a/examples/adhoc-layout/example/__init__.py +++ b/examples/adhoc-layout/example/__init__.py @@ -2,10 +2,12 @@ # test merging multiple tox runs with a platform # based branch -if platform.python_implementation() == "PyPy": +if platform.python_implementation() == 'PyPy': + def add(a, b): return a + b else: + def add(a, b): return a + b diff --git a/examples/adhoc-layout/setup.py b/examples/adhoc-layout/setup.py index e52b68d1..86a4bf68 100644 --- a/examples/adhoc-layout/setup.py +++ b/examples/adhoc-layout/setup.py @@ -3,5 +3,5 @@ setup( name='example', - packages=find_packages(include=['example']) + packages=find_packages(include=['example']), ) diff --git a/examples/src-layout/.coveragerc b/examples/src-layout/.coveragerc index b4c80de2..7ecf0087 100644 --- a/examples/src-layout/.coveragerc +++ b/examples/src-layout/.coveragerc @@ -7,7 +7,7 @@ source = branch = true parallel = true source = - example + src/example tests [report] diff --git a/examples/src-layout/src/example/__init__.py b/examples/src-layout/src/example/__init__.py index 684905a1..36b78a3d 100644 --- a/examples/src-layout/src/example/__init__.py +++ b/examples/src-layout/src/example/__init__.py @@ -2,10 +2,12 @@ # test merging multiple tox runs with a platform # based branch -if platform.python_implementation() == "PyPy": +if platform.python_implementation() == 'PyPy': + def add(a, b): return a + b else: + def add(a, b): return a + b diff --git a/examples/src-layout/tox.ini b/examples/src-layout/tox.ini index 94b72730..953955c9 100644 --- a/examples/src-layout/tox.ini +++ b/examples/src-layout/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pypy3,py39,report +envlist = clean,pypy310,py310,report [tool:pytest] testpaths = tests @@ -7,9 +7,7 @@ addopts = --cov-report=term-missing [testenv] -setenv = - py{py3,39}: COVERAGE_FILE = .coverage.{envname} -commands = pytest --cov {posargs:-vv} +commands = pytest --cov --cov-append {posargs:-vv} deps = pytest coverage @@ -20,12 +18,17 @@ deps = ../.. depends = - report: pypy3,py39 + report: pypy310,py310 + {pypy310,py310}: clean + +[testenv:clean] +skip_install = true +deps = coverage +commands = + coverage erase [testenv:report] skip_install = true deps = coverage commands = - coverage combine - coverage html coverage report --fail-under=100 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..e795c6de --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = [ + "setuptools>=30.3.0", +] + +[tool.ruff] +extend-exclude = ["static", "ci/templates"] +line-length = 140 +src = ["src", "tests"] +target-version = "py39" + +[tool.ruff.lint.per-file-ignores] +"ci/*" = ["S"] + +[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 +] +select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "E", # pycodestyle errors + "EXE", # flake8-executable + "F", # pyflakes + "I", # isort + "INT", # flake8-gettext + "PIE", # flake8-pie + "PLC", # pylint convention + "PLE", # pylint errors + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "RSE", # flake8-raise + "RUF", # ruff-specific rules + "S", # flake8-bandit + "UP", # pyupgrade + "W", # pycodestyle warnings +] + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.isort] +forced-separate = ["conftest"] +force-single-line = true + +[tool.ruff.format] +quote-style = "single" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..0f32c842 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,29 @@ +[pytest] +# If a pytest section is found in one of the possible config files +# (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, +# so if you add a pytest config section elsewhere, +# you will need to delete this section from setup.cfg. +norecursedirs = + migrations + +python_files = + test_*.py + *_test.py + tests.py +addopts = + -ra + --strict-markers + --doctest-modules + --doctest-glob=\*.rst + --tb=short + -p pytester +testpaths = + tests + +# Idea from: https://til.simonwillison.net/pytest/treat-warnings-as-errors +filterwarnings = + error +# You can add exclusions, some examples: +# ignore:'pytest_cov' defines default_app_config:PendingDeprecationWarning:: +# ignore:The {{% if::: +# ignore:Coverage disabled via --no-cov switch! diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index c33a9042..00000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[flake8] -max-line-length = 140 -exclude = .tox,.eggs,ci/templates,build,dist - -[tool:pytest] -testpaths = tests -python_files = test_*.py -addopts = - -ra - --strict-markers - -p pytester - -[tool:isort] -force_single_line = True -line_length = 120 -known_first_party = pytest_cov -default_section = THIRDPARTY -forced_separate = test_pytest_cov -skip = .tox,.eggs,ci/templates,build,dist diff --git a/setup.py b/setup.py index e2cafde7..f95279d1 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,8 @@ #!/usr/bin/env python import re -from glob import glob from itertools import chain -from os.path import basename -from os.path import dirname -from os.path import join -from os.path import splitext +from pathlib import Path from setuptools import Command from setuptools import find_packages @@ -24,34 +20,31 @@ def read(*names, **kwargs): - with open( - join(dirname(__file__), *names), - encoding=kwargs.get('encoding', 'utf8') - ) as fh: + 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 = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.build_lib, basename(path)) + 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 = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.install_dir, basename(path)) + 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 = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.install_dir, basename(path)) + 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] @@ -62,13 +55,13 @@ def get_outputs(self): class DevelopWithPTH(develop): def run(self, *args, **kwargs): super().run(*args, **kwargs) - path = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.install_dir, basename(path)) + 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 = [] + user_options = () def initialize_options(self): pass @@ -77,16 +70,14 @@ def finalize_options(self): pass def run(self): - with open(join(dirname(__file__), 'src', 'pytest-cov.pth'), 'w') as fh: - with open(join(dirname(__file__), 'src', 'pytest-cov.embed')) as sh: - fh.write( - f"import os, sys;exec({sh.read().replace(' ', ' ')!r})" - ) + 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='4.0.0', + version='6.2.1', license='MIT', description='Pytest plugin for measuring coverage.', long_description='{}\n{}'.format(read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), @@ -95,7 +86,7 @@ def run(self): url='https://github.com/pytest-dev/pytest-cov', packages=find_packages('src'), package_dir={'': 'src'}, - py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + py_modules=[path.stem for path in Path('src').glob('*.py')], include_package_data=True, zip_safe=False, classifiers=[ @@ -103,18 +94,16 @@ def run(self): '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.7', - 'Programming Language :: Python :: 3.8', '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', @@ -126,19 +115,24 @@ def run(self): 'Issue Tracker': 'https://github.com/pytest-dev/pytest-cov/issues', }, keywords=[ - 'cover', 'coverage', 'pytest', 'py.test', 'distributed', 'parallel', + 'cover', + 'coverage', + 'pytest', + 'py.test', + 'distributed', + 'parallel', ], + python_requires='>=3.9', install_requires=[ - 'pytest>=4.6', - 'coverage[toml]>=5.2.1' + 'pytest>=6.2.5', + 'coverage[toml]>=7.5', + 'pluggy>=1.2', ], - python_requires='>=3.7', extras_require={ 'testing': [ 'fields', 'hunter', 'process-tests', - 'six', 'pytest-xdist', 'virtualenv', ] diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py index 9dfd9823..0487a25b 100644 --- a/src/pytest_cov/__init__.py +++ b/src/pytest_cov/__init__.py @@ -1,2 +1,41 @@ """pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE.""" -__version__ = '4.0.0' + +__version__ = '6.2.1' + +import pytest + + +class CoverageError(Exception): + """Indicates that our coverage is too low""" + + +class PytestCovWarning(pytest.PytestWarning): + """ + The base for all pytest-cov warnings, never raised directly. + """ + + +class CovDisabledWarning(PytestCovWarning): + """ + Indicates that Coverage was manually disabled. + """ + + +class CovReportWarning(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. + """ + + +class DistCovError(Exception): + """ + Raised when dynamic_context is set to test_function and xdist is also used. + + See: https://github.com/pytest-dev/pytest-cov/issues/604 + """ diff --git a/src/pytest_cov/compat.py b/src/pytest_cov/compat.py index 614419cb..453709d7 100644 --- a/src/pytest_cov/compat.py +++ b/src/pytest_cov/compat.py @@ -1,12 +1,3 @@ -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - - -StringIO # pyflakes, this is for re-export - - class SessionWrapper: def __init__(self, session): self._session = session diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index f8a2749f..153cb83d 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -13,6 +13,7 @@ that code coverage is being collected we activate coverage based on info passed via env vars. """ + import atexit import os import signal @@ -52,7 +53,7 @@ def init(): data_suffix=True, config_file=cov_config, auto_data=True, - data_file=cov_datafile + data_file=cov_datafile, ) cov.load() cov.start() @@ -60,6 +61,7 @@ def init(): cov.switch_context(cov_context) cov._warn_no_data = False cov._warn_unimported_source = False + cov._warn_preimported_source = False return cov @@ -70,7 +72,7 @@ def _cleanup(cov): cov._auto_save = False # prevent autosaving from cov._atexit in case the interpreter lacks atexit.unregister try: atexit.unregister(cov._atexit) - except Exception: + except Exception: # noqa: S110 pass @@ -108,7 +110,7 @@ def _signal_cleanup_handler(signum, frame): elif signum == signal.SIGTERM: os._exit(128 + signum) elif signum == signal.SIGINT: - raise KeyboardInterrupt() + raise KeyboardInterrupt def cleanup_on_signal(signum): diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 97d4d017..4d53c2fc 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -1,19 +1,32 @@ """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 +from coverage.sqldata import filename_suffix -from .compat import StringIO +from . import CentralCovContextWarning +from . import DistCovError from .embed import cleanup +class BrokenCovConfigError(Exception): + pass + + class _NullFile: @staticmethod def write(v): @@ -34,7 +47,7 @@ def _ensure_topdir(meth): @functools.wraps(meth) def ensure_topdir_wrapper(self, *args, **kwargs): try: - original_cwd = os.getcwd() + original_cwd = Path.cwd() except OSError: # Looks like it's gone, this is non-ideal because a side-effect will # be introduced in the tests here but we can't do anything about it. @@ -49,16 +62,21 @@ 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.""" - 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 @@ -67,18 +85,20 @@ def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, c self.data_file = None self.node_descs = set() self.failed_workers = [] - self.topdir = os.getcwd() + self.topdir = os.fspath(Path.cwd()) self.is_collocated = None + self.started = False @contextlib.contextmanager def ensure_topdir(self): - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(self.topdir) yield os.chdir(original_cwd) @_ensure_topdir def pause(self): + self.started = False self.cov.stop() self.unset_env() @@ -86,6 +106,13 @@ def pause(self): def resume(self): self.cov.start() self.set_env() + self.started = True + + def start(self): + self.started = True + + def finish(self): + self.started = False @_ensure_topdir def set_env(self): @@ -94,12 +121,13 @@ def set_env(self): os.environ['COV_CORE_SOURCE'] = os.pathsep else: os.environ['COV_CORE_SOURCE'] = os.pathsep.join(self.cov_source) - config_file = os.path.abspath(self.cov_config) - if os.path.exists(config_file): - os.environ['COV_CORE_CONFIG'] = config_file + 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 - os.environ['COV_CORE_DATAFILE'] = os.path.abspath(self.cov.config.data_file) + # 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' @@ -116,18 +144,42 @@ def unset_env(): def get_node_desc(platform, version_info): """Return a description of this node.""" - return 'platform {}, python {}'.format(platform, '%s.%s.%s-%s-%s' % version_info[:5]) + 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): @@ -135,22 +187,21 @@ def summary(self, stream): total = None if not self.cov_report: - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): return self.cov.report(show_missing=True, ignore_errors=True, file=_NullFile) # 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') - stream.write('The following workers failed to return coverage data, ' - 'ensure that pytest-cov is installed on these workers.\n') + 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') @@ -160,22 +211,23 @@ 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}) - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.report(**options) # Produce annotated source code report if wanted. if 'annotate' in self.cov_report: annotate_dir = self.cov_report['annotate'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): self.cov.annotate(ignore_errors=True, directory=annotate_dir) # We need to call Coverage.report here, just to get the total # Coverage.annotate don't return any total and we need it for --cov-fail-under. - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.report(ignore_errors=True, file=_NullFile) if annotate_dir: stream.write(f'Coverage annotated source written to dir {annotate_dir}\n') @@ -185,28 +237,28 @@ def summary(self, stream): # Produce html report if wanted. if 'html' in self.cov_report: output = self.cov_report['html'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.html_report(ignore_errors=True, directory=output) stream.write(f'Coverage HTML written to dir {self.cov.config.html_dir if output is None else output}\n') # Produce xml report if wanted. if 'xml' in self.cov_report: output = self.cov_report['xml'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.xml_report(ignore_errors=True, outfile=output) stream.write(f'Coverage XML written to file {self.cov.config.xml_output if output is None else output}\n') # Produce json report if wanted if 'json' in self.cov_report: output = self.cov_report['json'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.json_report(ignore_errors=True, outfile=output) stream.write('Coverage JSON written to file %s\n' % (self.cov.config.json_output if output is None else output)) # Produce lcov report if wanted. if 'lcov' in self.cov_report: output = self.cov_report['lcov'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): self.cov.lcov_report(ignore_errors=True, outfile=output) # We need to call Coverage.report here, just to get the total @@ -225,15 +277,26 @@ class Central(CovController): def start(self): cleanup() - self.cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - config_file=self.cov_config) - self.combining_cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - data_file=os.path.abspath(self.cov.config.data_file), - config_file=self.cov_config) + self.cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=_data_suffix('c'), + config_file=self.cov_config, + ) + if self.cov.config.dynamic_context == 'test_function': + message = ( + 'Detected dynamic_context=test_function in coverage configuration. ' + 'This is unnecessary as this plugin provides the more complete --cov-context option.' + ) + warnings.warn(CentralCovContextWarning(message), stacklevel=1) + + self.combining_cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=_data_suffix('cc'), + data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 + config_file=self.cov_config, + ) # Erase or load any previous coverage data and start coverage. if not self.cov_append: @@ -241,9 +304,12 @@ def start(self): self.cov.start() self.set_env() + super().start() + @_ensure_topdir 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() @@ -265,24 +331,28 @@ class DistMaster(CovController): def start(self): cleanup() - # Ensure coverage rc file rsynced if appropriate. - if self.cov_config and os.path.exists(self.cov_config): - # rsyncdir is going away in pytest-xdist 4.0, already deprecated - if hasattr(self.config.option, 'rsyncdir'): - self.config.option.rsyncdir.append(self.cov_config) - - self.cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - config_file=self.cov_config) + self.cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=_data_suffix('m'), + config_file=self.cov_config, + ) + if self.cov.config.dynamic_context == 'test_function': + raise DistCovError( + 'Detected dynamic_context=test_function in coverage configuration. ' + 'This is known to cause issues when using xdist, see: https://github.com/pytest-dev/pytest-cov/issues/604\n' + 'It is recommended to use --cov-context instead.' + ) self.cov._warn_no_data = False self.cov._warn_unimported_source = False self.cov._warn_preimported_source = False - self.combining_cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - data_file=os.path.abspath(self.cov.config.data_file), - config_file=self.cov_config) + self.combining_cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=_data_suffix('mc'), + data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 + config_file=self.cov_config, + ) if not self.cov_append: self.cov.erase() self.cov.start() @@ -291,11 +361,13 @@ def start(self): def configure_node(self, node): """Workers need to know if they are collocated and what files have moved.""" - node.workerinput.update({ - 'cov_master_host': socket.gethostname(), - 'cov_master_topdir': self.topdir, - 'cov_master_rsync_roots': [str(root) for root in node.nodemanager.roots], - }) + node.workerinput.update( + { + 'cov_master_host': socket.gethostname(), + 'cov_master_topdir': self.topdir, + 'cov_master_rsync_roots': [str(root) for root in node.nodemanager.roots], + } + ) def testnodedown(self, node, error): """Collect data file name from worker.""" @@ -310,23 +382,21 @@ 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' % ( - socket.gethostname(), os.getpid(), - random.randint(0, 999999), - output['cov_worker_node_id'] + data_suffix = '%s.%s.%06d.%s' % ( # noqa: UP031 + socket.gethostname(), + os.getpid(), + random.randint(0, 999999), # noqa: S311 + 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 = 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) + data = CoverageData(no_disk=True, suffix='should-not-exist') data.loads(output['cov_worker_data']) cov.get_data().update(data) cov.stop() @@ -357,33 +427,40 @@ 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']) + 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 not self.is_collocated: master_topdir = self.config.workerinput['cov_master_topdir'] worker_topdir = self.topdir if self.cov_source is not None: - self.cov_source = [source.replace(master_topdir, worker_topdir) - for source in self.cov_source] + self.cov_source = [source.replace(master_topdir, worker_topdir) for source in self.cov_source] self.cov_config = self.cov_config.replace(master_topdir, worker_topdir) # Erase any previous data and start coverage. - self.cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - config_file=self.cov_config) + self.cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=_data_suffix(f'w{self.nodeid}'), + 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 def finish(self): """Stop coverage and send relevant info back to the master.""" + super().finish() + self.unset_env() self.cov.stop() @@ -411,13 +488,13 @@ def finish(self): else: data = self.cov.get_data().dumps() - self.config.workeroutput.update({ - 'cov_worker_path': self.topdir, - 'cov_worker_node_id': self.nodeid, - 'cov_worker_data': data, - }) + self.config.workeroutput.update( + { + 'cov_worker_path': self.topdir, + 'cov_worker_node_id': self.nodeid, + 'cov_worker_data': data, + } + ) def summary(self, stream): """Only the master reports so do nothing.""" - - pass diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 2a1544a6..6efc4694 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -1,31 +1,29 @@ """Coverage plugin for pytest.""" + import argparse import os +import re import warnings +from io import StringIO +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 -class CoverageError(Exception): - """Indicates that our coverage is too low""" - - -class PytestCovWarning(pytest.PytestWarning): - """ - The base for all pytest-cov warnings, never raised directly - """ - - -class CovDisabledWarning(PytestCovWarning): - """Indicates that Coverage was manually disabled""" - - -class CovReportWarning(PytestCovWarning): - """Indicates that we failed to generate a report""" +COVERAGE_SQLITE_WARNING_RE = re.compile('unclosed database in 100: - raise argparse.ArgumentTypeError('Your desire for over-achievement is admirable but misplaced. ' - 'The maximum value is 100. Perhaps write more integration tests?') + raise argparse.ArgumentTypeError( + 'Your desire for over-achievement is admirable but misplaced. The maximum value is 100. Perhaps write more integration tests?' + ) return value 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": + if arg != 'test': raise argparse.ArgumentTypeError('The only supported value is "test".') return arg @@ -88,42 +86,89 @@ def __call__(self, parser, namespace, values, option_string=None): def pytest_addoption(parser): """Add options to control coverage.""" - group = parser.getgroup( - 'cov', 'coverage reporting with distributed testing support') - group.addoption('--cov', action='append', default=[], metavar='SOURCE', - nargs='?', const=True, dest='cov_source', - help='Path or package name to measure during execution (multi-allowed). ' - 'Use --cov= to not do any source filtering and record everything.') - group.addoption('--cov-reset', action='store_const', const=[], dest='cov_source', - help='Reset cov sources accumulated in options so far. ') - group.addoption('--cov-report', action=StoreReport, default={}, - metavar='TYPE', type=validate_report, - help='Type of report to generate: term, term-missing, ' - 'annotate, html, xml, json, lcov (multi-allowed). ' - 'term, term-missing may be followed by ":skip-covered". ' - 'annotate, html, xml, json and lcov may be followed by ":DEST" ' - 'where DEST specifies the output location. ' - 'Use --cov-report= to not generate any output.') - group.addoption('--cov-config', action='store', default='.coveragerc', - metavar='PATH', - help='Config file for coverage. Default: .coveragerc') - group.addoption('--no-cov-on-fail', action='store_true', default=False, - help='Do not report coverage if test run fails. ' - 'Default: False') - group.addoption('--no-cov', action='store_true', default=False, - help='Disable coverage report completely (useful for debuggers). ' - 'Default: False') - group.addoption('--cov-fail-under', action='store', metavar='MIN', - type=validate_fail_under, - help='Fail if the total coverage is less than MIN.') - group.addoption('--cov-append', action='store_true', default=False, - help='Do not delete coverage but append to current. ' - 'Default: False') - group.addoption('--cov-branch', action='store_true', default=None, - help='Enable branch coverage.') - group.addoption('--cov-context', action='store', metavar='CONTEXT', - type=validate_context, - help='Dynamic contexts to use. "test" for now.') + group = parser.getgroup('cov', 'coverage reporting with distributed testing support') + group.addoption( + '--cov', + action='append', + default=[], + metavar='SOURCE', + nargs='?', + const=True, + dest='cov_source', + help='Path or package name to measure during execution (multi-allowed). ' + 'Use --cov= to not do any source filtering and record everything.', + ) + group.addoption( + '--cov-reset', + action='store_const', + const=[], + dest='cov_source', + help='Reset cov sources accumulated in options so far. ', + ) + group.addoption( + '--cov-report', + action=StoreReport, + default={}, + metavar='TYPE', + type=validate_report, + help='Type of report to generate: term, term-missing, ' + 'annotate, html, xml, json, lcov (multi-allowed). ' + 'term, term-missing may be followed by ":skip-covered". ' + 'annotate, html, xml, json and lcov may be followed by ":DEST" ' + 'where DEST specifies the output location. ' + 'Use --cov-report= to not generate any output.', + ) + group.addoption( + '--cov-config', + action='store', + default='.coveragerc', + metavar='PATH', + help='Config file for coverage. Default: .coveragerc', + ) + group.addoption( + '--no-cov-on-fail', + action='store_true', + default=False, + help='Do not report coverage if test run fails. Default: False', + ) + group.addoption( + '--no-cov', + action='store_true', + default=False, + help='Disable coverage report completely (useful for debuggers). Default: False', + ) + group.addoption( + '--cov-fail-under', + action='store', + metavar='MIN', + type=validate_fail_under, + help='Fail if the total coverage is less than MIN.', + ) + group.addoption( + '--cov-append', + action='store_true', + default=False, + help='Do not delete coverage but append to current. Default: False', + ) + group.addoption( + '--cov-branch', + action='store_true', + default=None, + help='Enable branch coverage.', + ) + group.addoption( + '--cov-precision', + type=int, + default=None, + help='Override the reporting precision.', + ) + group.addoption( + '--cov-context', + action='store', + metavar='CONTEXT', + type=validate_context, + help='Dynamic contexts to use. "test" for now.', + ) def _prepare_cov_source(cov_source): @@ -161,7 +206,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 @@ -172,17 +217,16 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) # Our implementation is unknown at this time. self.pid = None self.cov_controller = None - self.cov_report = compat.StringIO() + self.cov_report = StringIO() self.cov_total = None self.failed = False self._started = 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') + is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no' if getattr(options, 'no_cov', False): self._disabled = True return @@ -204,8 +248,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: @@ -213,21 +256,15 @@ 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 = os.getcwd() + self._start_path = Path.cwd() cov_config = self.cov_controller.cov.config if self.options.cov_fail_under is None and hasattr(cov_config, 'fail_under'): self.options.cov_fail_under = cov_config.fail_under + if self.options.cov_precision is None: + self.options.cov_precision = getattr(cov_config, 'precision', 0) def _is_worker(self, session): return getattr(session.config, 'workerinput', None) is not None @@ -246,15 +283,13 @@ def pytest_sessionstart(self, session): self.pid = os.getpid() if self._is_worker(session): - nodeid = ( - session.config.workerinput.get('workerid', getattr(session, 'nodeid')) - ) + nodeid = session.config.workerinput.get('workerid', session.nodeid) self.start(engine.DistWorker, session.config, nodeid) elif not self._started: self.start(engine.Central) if self.options.cov_context == 'test': - session.config.pluginmanager.register(TestContextPlugin(self.cov_controller.cov), '_cov_contexts') + session.config.pluginmanager.register(TestContextPlugin(self.cov_controller), '_cov_contexts') @pytest.hookimpl(optionalhook=True) def pytest_configure_node(self, node): @@ -278,18 +313,31 @@ def _should_report(self): needed = self.options.cov_report or self.options.cov_fail_under return needed and not (self.failed and self.options.no_cov_on_fail) - def _failed_cov_total(self): - cov_fail_under = self.options.cov_fail_under - return cov_fail_under is not None and self.cov_total < cov_fail_under - # we need to wrap pytest_runtestloop. by the time pytest_sessionfinish # runs, it's too late to set testsfailed - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtestloop(self, session): - yield - if self._disabled: - return + return (yield) + + # we add default warning configuration to prevent certain warnings to bubble up as errors due to rigid filterwarnings configuration + for _, message, category, _, _ in warnings.filters: + if category is ResourceWarning and message == COVERAGE_SQLITE_WARNING_RE: + break + else: + warnings.filterwarnings('default', 'unclosed database in 0 # r9-1 r9-2 r9-3 + assert parametrized_number > 0 # r9-1 r9-2 r9-3 def test_10(): - assert 1 == 1 # r10 + assert 1 == 1 # r10 -@pytest.mark.parametrize("x, ans", [ - (1, 101), - (2, 202), -]) +@pytest.mark.parametrize( + ('x', 'ans'), + [ + (1, 101), + (2, 202), + ], +) def test_11(x, ans): - assert 100 * x + x == ans # r11-1 r11-2 + assert 100 * x + x == ans # r11-1 r11-2 -@pytest.mark.parametrize("x, ans", [ - (1, 101), - (2, 202), -], ids=['one', 'two']) +@pytest.mark.parametrize( + ('x', 'ans'), + [ + (1, 101), + (2, 202), + ], + ids=['one', 'two'], +) def test_12(x, ans): - assert 100 * x + x == ans # r12-1 r12-2 + assert 100 * x + x == ans # r12-1 r12-2 -@pytest.mark.parametrize("x", [1, 2]) -@pytest.mark.parametrize("y", [3, 4]) +@pytest.mark.parametrize('x', [1, 2]) +@pytest.mark.parametrize('y', [3, 4]) def test_13(x, y): - assert x + y > 0 # r13-1 r13-2 r13-3 r13-4 + assert x + y > 0 # r13-1 r13-2 r13-3 r13-4 diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 84f959fb..9a003bee 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1,3 +1,4 @@ +# ruff: noqa import collections import glob import os @@ -5,6 +6,7 @@ import re import subprocess import sys +from io import StringIO from itertools import chain import coverage @@ -19,16 +21,11 @@ import pytest_cov.plugin -try: - from StringIO import StringIO -except ImportError: - from io import StringIO +coverage, platform # required for skipif mark on test_cov_min_from_coveragerc -coverage, platform # required for skipif mark on test_cov_min_from_coveragerc +max_worker_restart_0 = '--max-worker-restart=0' -max_worker_restart_0 = "--max-worker-restart=0" - -SCRIPT = ''' +SCRIPT = """ import sys, helper def pytest_generate_tests(metafunc): @@ -40,24 +37,23 @@ def test_foo(p): helper.do_stuff() # get some coverage in some other completely different location if sys.version_info[0] > 5: assert False -''' +""" -SCRIPT2 = ''' +SCRIPT2 = """ # def test_bar(): x = True assert x -''' - +""" -COVERAGERC_SOURCE = '''\ +COVERAGERC_SOURCE = """\ [run] source = . -''' +""" -SCRIPT_CHILD = ''' +SCRIPT_CHILD = """ import sys idx = int(sys.argv[1]) @@ -66,9 +62,9 @@ def test_bar(): foo = "a" # previously there was a "pass" here but Python 3.5 optimizes it away. if idx == 1: foo = "b" # previously there was a "pass" here but Python 3.5 optimizes it away. -''' +""" -SCRIPT_PARENT = ''' +SCRIPT_PARENT = """ import os import subprocess import sys @@ -86,9 +82,9 @@ def test_foo(idx): # there is a issue in coverage.py with multiline statements at # end of file: https://bitbucket.org/ned/coveragepy/issue/293 pass -''' +""" -SCRIPT_PARENT_CHANGE_CWD = ''' +SCRIPT_PARENT_CHANGE_CWD = """ import subprocess import sys import os @@ -110,9 +106,9 @@ def test_foo(idx): # there is a issue in coverage.py with multiline statements at # end of file: https://bitbucket.org/ned/coveragepy/issue/293 pass -''' +""" -SCRIPT_PARENT_CHANGE_CWD_IMPORT_CHILD = ''' +SCRIPT_PARENT_CHANGE_CWD_IMPORT_CHILD = """ import subprocess import sys import os @@ -133,19 +129,19 @@ def test_foo(idx): # there is a issue in coverage.py with multiline statements at # end of file: https://bitbucket.org/ned/coveragepy/issue/293 pass -''' +""" -SCRIPT_FUNCARG = ''' +SCRIPT_FUNCARG = """ import coverage def test_foo(cov): assert isinstance(cov, coverage.Coverage) -''' +""" -SCRIPT_FUNCARG_NOT_ACTIVE = ''' +SCRIPT_FUNCARG_NOT_ACTIVE = """ def test_foo(cov): assert cov is None -''' +""" CHILD_SCRIPT_RESULT = '[56] * 100%' PARENT_SCRIPT_RESULT = '9 * 100%' @@ -154,10 +150,16 @@ def test_foo(cov): JSON_REPORT_NAME = 'cov.json' LCOV_REPORT_NAME = 'cov.info' -xdist_params = pytest.mark.parametrize('opts', [ - '', - pytest.param('-n 1', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')) -], ids=['nodist', 'xdist']) +xdist_params = pytest.mark.parametrize( + 'opts', + [ + '', + pytest.param('-n 1', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')), + pytest.param('-n 2', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')), + pytest.param('-n 3', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')), + ], + ids=['nodist', '1xdist', '2xdist', '3xdist'], +) @pytest.fixture(scope='session', autouse=True) @@ -177,12 +179,15 @@ def adjust_sys_path(): os.environ['PYTHONPATH'] = orig_path -@pytest.fixture(params=[ - ('branch=true', '--cov-branch', '9 * 85%', '3 * 100%'), - ('branch=true', '', '9 * 85%', '3 * 100%'), - ('', '--cov-branch', '9 * 85%', '3 * 100%'), - ('', '', '9 * 89%', '3 * 100%'), -], ids=['branch2x', 'branch1c', 'branch1a', 'nobranch']) +@pytest.fixture( + params=[ + ('branch=true', '--cov-branch', '9 * 85%', '3 * 100%'), + ('branch=true', '', '9 * 85%', '3 * 100%'), + ('', '--cov-branch', '9 * 85%', '3 * 100%'), + ('', '', '9 * 89%', '3 * 100%'), + ], + ids=['branch2x', 'branch1c', 'branch1a', 'nobranch'], +) def prop(request): return Namespace( code=SCRIPT, @@ -200,149 +205,141 @@ def test_central(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script, - *prop.args) + 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 def test_annotate(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=annotate', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=annotate', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage annotated source written next to source', - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Coverage annotated source written next to source', + '*10 passed*', + ] + ) assert result.ret == 0 def test_annotate_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=annotate:' + DEST_DIR, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=annotate:' + DEST_DIR, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage annotated source written to dir ' + DEST_DIR, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Coverage annotated source written to dir ' + DEST_DIR, + '*10 passed*', + ] + ) dest_dir = testdir.tmpdir.join(DEST_DIR) assert dest_dir.check(dir=True) - assert dest_dir.join(script.basename + ",cover").check() + assert dest_dir.join(script.basename + ',cover').check() assert result.ret == 0 def test_html(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=html', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage HTML written to dir htmlcov', - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Coverage HTML written to dir htmlcov', + '*10 passed*', + ] + ) dest_dir = testdir.tmpdir.join('htmlcov') assert dest_dir.check(dir=True) - assert dest_dir.join("index.html").check() + assert dest_dir.join('index.html').check() assert result.ret == 0 def test_html_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=html:' + DEST_DIR, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html:' + DEST_DIR, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage HTML written to dir ' + DEST_DIR, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Coverage HTML written to dir ' + DEST_DIR, + '*10 passed*', + ] + ) dest_dir = testdir.tmpdir.join(DEST_DIR) assert dest_dir.check(dir=True) - assert dest_dir.join("index.html").check() + assert dest_dir.join('index.html').check() assert result.ret == 0 def test_term_report_does_not_interact_with_html_output(testdir): script = testdir.makepyfile(test_funcarg=SCRIPT_FUNCARG) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing:skip-covered', - '--cov-report=html:' + DEST_DIR, - script) + result = testdir.runpytest( + '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing:skip-covered', '--cov-report=html:' + DEST_DIR, script + ) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage HTML written to dir ' + DEST_DIR, - '*1 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Coverage HTML written to dir ' + DEST_DIR, + '*1 passed*', + ] + ) dest_dir = testdir.tmpdir.join(DEST_DIR) assert dest_dir.check(dir=True) - assert sorted(dest_dir.visit("**/*.html")) == [dest_dir.join("index.html"), dest_dir.join("test_funcarg_py.html")] - assert dest_dir.join("index.html").check() + 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')) + assert sorted(dest_dir.visit('**/*.html')) == expected + assert dest_dir.join('index.html').check() assert result.ret == 0 def test_html_configured_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [html] directory = somewhere -""") - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=html', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage HTML written to dir somewhere', - '*10 passed*', - ]) +""" + ) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html', script) + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Coverage HTML written to dir somewhere', + '*10 passed*', + ] + ) dest_dir = testdir.tmpdir.join('somewhere') assert dest_dir.check(dir=True) - assert dest_dir.join("index.html").check() + assert dest_dir.join('index.html').check() assert result.ret == 0 def test_xml_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=xml:' + XML_REPORT_NAME, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=xml:' + XML_REPORT_NAME, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage XML written to file ' + XML_REPORT_NAME, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Coverage XML written to file ' + XML_REPORT_NAME, + '*10 passed*', + ] + ) assert testdir.tmpdir.join(XML_REPORT_NAME).check() assert result.ret == 0 @@ -350,105 +347,94 @@ 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', '--cov=%s' % script.dirpath(), '--cov-report=json:' + JSON_REPORT_NAME, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage JSON written to file ' + JSON_REPORT_NAME, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Coverage JSON written to file ' + JSON_REPORT_NAME, + '*10 passed*', + ] + ) assert testdir.tmpdir.join(JSON_REPORT_NAME).check() assert result.ret == 0 -@pytest.mark.skipif("coverage.version_info < (6, 3)") +@pytest.mark.skipif('coverage.version_info < (6, 3)') def test_lcov_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=lcov:' + LCOV_REPORT_NAME, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=lcov:' + LCOV_REPORT_NAME, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage LCOV written to file ' + LCOV_REPORT_NAME, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Coverage LCOV written to file ' + LCOV_REPORT_NAME, + '*10 passed*', + ] + ) assert testdir.tmpdir.join(LCOV_REPORT_NAME).check() assert result.ret == 0 -@pytest.mark.skipif("coverage.version_info >= (6, 3)") +@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', - ]) + 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) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term:' + DEST_DIR, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term:' + DEST_DIR, script) - result.stderr.fnmatch_lines([ - f'*argument --cov-report: output specifier not supported for: "term:{DEST_DIR}"*', - ]) + result.stderr.fnmatch_lines( + [ + f'*argument --cov-report: output specifier not supported for: "term:{DEST_DIR}"*', + ] + ) assert result.ret != 0 def test_term_missing_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing:' + DEST_DIR, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing:' + DEST_DIR, script) - result.stderr.fnmatch_lines([ - '*argument --cov-report: output specifier not supported for: ' - '"term-missing:%s"*' % DEST_DIR, - ]) + result.stderr.fnmatch_lines( + [ + '*argument --cov-report: output specifier not supported for: "term-missing:%s"*' % DEST_DIR, + ] + ) assert result.ret != 0 def test_cov_min_100(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--cov-fail-under=100', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=100', script) assert result.ret != 0 - result.stdout.fnmatch_lines([ - 'FAIL Required test coverage of 100% not reached. Total coverage: *%' - ]) + result.stdout.fnmatch_lines(['FAIL Required test coverage of 100% not reached. Total coverage: *%']) def test_cov_min_100_passes_if_collectonly(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--cov-fail-under=100', - '--collect-only', - script) + result = testdir.runpytest( + '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=100', '--collect-only', script + ) assert result.ret == 0 @@ -456,75 +442,88 @@ def test_cov_min_100_passes_if_collectonly(testdir): def test_cov_min_50(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=html', - '--cov-report=xml', - '--cov-fail-under=50', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html', '--cov-report=xml', '--cov-fail-under=50', script) assert result.ret == 0 - result.stdout.fnmatch_lines([ - 'Required test coverage of 50% reached. Total coverage: *%' - ]) + result.stdout.fnmatch_lines(['Required test coverage of 50% reached. Total coverage: *%']) def test_cov_min_float_value(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--cov-fail-under=88.88', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=88.88', script) assert result.ret == 0 - result.stdout.fnmatch_lines([ - 'Required test coverage of 88.88% reached. Total coverage: 88.89%' - ]) + result.stdout.fnmatch_lines(['Required test coverage of 88.88% reached. Total coverage: 88.89%']) def test_cov_min_float_value_not_reached(testdir): script = testdir.makepyfile(SCRIPT) + testdir.tmpdir.join('.coveragerc').write(""" +[report] +precision = 3 +""") + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=88.89', script) + assert result.ret == 1 + result.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%']) + - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--cov-fail-under=88.89', - script) +def test_cov_min_float_value_not_reached_cli(testdir): + script = testdir.makepyfile(SCRIPT) + result = testdir.runpytest( + '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-precision=3', '--cov-fail-under=88.89', script + ) assert result.ret == 1 - result.stdout.fnmatch_lines([ - 'FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%' - ]) + 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) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=', - '--cov-fail-under=50', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=', '--cov-fail-under=50', script) assert result.ret == 0 - result.stdout.fnmatch_lines([ - 'Required test coverage of 50% reached. Total coverage: *%' - ]) + result.stdout.fnmatch_lines(['Required test coverage of 50% reached. Total coverage: *%']) def test_central_nonspecific(pytester, testdir, prop): script = testdir.makepyfile(prop.code) 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 = 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*']) # multi-module coverage report assert any(line.startswith('TOTAL ') for line in result.stdout.lines) @@ -534,15 +533,14 @@ def test_central_nonspecific(pytester, testdir, prop): def test_cov_min_from_coveragerc(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [report] fail_under = 100 -""") +""" + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret != 0 @@ -551,16 +549,15 @@ def test_central_coveragerc(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(COVERAGERC_SOURCE + prop.conf) - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term-missing', - script, *prop.args) + result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_central_coveragerc* {prop.result} *', - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'test_central_coveragerc* {prop.result} *', + '*10 passed*', + ] + ) assert result.ret == 0 @@ -570,10 +567,13 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): mod1.write(SCRIPT) mod2 = testdir.mkdir('aliased').join('mod.py') mod2.write(SCRIPT) - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ from mod import * -''') - testdir.tmpdir.join('setup.cfg').write(f""" +""" + ) + testdir.tmpdir.join('setup.cfg').write( + f""" [coverage:paths] source = src @@ -582,30 +582,33 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): source = mod parallel = true {prop.conf} -""") +""" + ) monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([os.environ.get('PYTHONPATH', ''), 'aliased'])) - result = testdir.runpytest('-v', '-s', - '--cov', - '--cov-report=term-missing', - script, *opts.split()+prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'src[\\/]mod* {prop.result} *', - '*10 passed*', - ]) + result = testdir.runpytest('-v', '-s', '--cov', '--cov-report=term-missing', script, *opts.split() + prop.args) + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'src[\\/]mod* {prop.result} *', + '*10 passed*', + ] + ) assert result.ret == 0 @xdist_params def test_borken_cwd(pytester, testdir, monkeypatch, opts): - testdir.makepyfile(mod=''' + testdir.makepyfile( + mod=""" def foobar(a, b): return a + b -''') +""" + ) - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import os import tempfile import pytest @@ -623,17 +626,17 @@ def bad(): def test_foobar(bad): assert mod.foobar(1, 2) == 3 -''') - result = testdir.runpytest('-v', '-s', - '--cov=mod', - '--cov-branch', - script, *opts.split()) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - '*mod* 100%', - '*1 passed*', - ]) +""" + ) + result = testdir.runpytest('-v', '-s', '--cov=mod', '--cov-branch', script, *opts.split()) + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + '*mod* 100%', + '*1 passed*', + ] + ) assert result.ret == 0 @@ -647,7 +650,8 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): parent_script.write(SCRIPT_PARENT) aliased.join('child_script.py').write(SCRIPT_CHILD) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [paths] source = src @@ -657,61 +661,59 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): parent_script child_script parallel = true -""") +""" + ) - monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([ - os.environ.get('PYTHONPATH', ''), 'aliased'])) - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term-missing', - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'src[\\/]child_script* {CHILD_SCRIPT_RESULT}*', - f'src[\\/]parent_script* {PARENT_SCRIPT_RESULT}*', - ]) + monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([os.environ.get('PYTHONPATH', ''), 'aliased'])) + result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', parent_script) + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'src[\\/]child_script* {CHILD_SCRIPT_RESULT}*', + f'src[\\/]parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 def test_show_missing_coveragerc(pytester, testdir, prop): script = testdir.makepyfile(prop.code) - testdir.tmpdir.join('.coveragerc').write(f""" + testdir.tmpdir.join('.coveragerc').write( + f""" [run] source = . {prop.conf} [report] show_missing = true -""") +""" + ) - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term', - script, *prop.args) + result = testdir.runpytest('-v', '--cov', '--cov-report=term', script, *prop.args) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Name * Stmts * Miss * Cover * Missing', - f'test_show_missing_coveragerc* {prop.result} * 11*', - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'Name * Stmts * Miss * Cover * Missing', + f'test_show_missing_coveragerc* {prop.result} * 11*', + '*10 passed*', + ] + ) assert result.ret == 0 def test_no_cov_on_fail(testdir): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ def test_fail(): assert False -''') +""" + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--no-cov-on-fail', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--no-cov-on-fail', script) assert 'coverage: platform' not in result.stdout.str() result.stdout.fnmatch_lines(['*1 failed*']) @@ -719,63 +721,63 @@ def test_fail(): def test_no_cov(pytester, testdir, monkeypatch): script = testdir.makepyfile(SCRIPT) - testdir.makeini(""" + testdir.makeini( + """ [pytest] addopts=--no-cov - """) - result = testdir.runpytest('-vvv', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '-rw', - script) - result.stdout.fnmatch_lines_random([ - 'WARNING: Coverage disabled via --no-cov switch!', - '*Coverage disabled via --no-cov switch!', - ]) + """ + ) + result = testdir.runpytest('-vvv', f'--cov={script.dirpath()}', '--cov-report=term-missing', '-rw', script) + result.stdout.fnmatch_lines_random( + [ + 'WARNING: Coverage disabled via --no-cov switch!', + '*Coverage disabled via --no-cov switch!', + ] + ) def test_cov_and_failure_report_on_fail(testdir): - script = testdir.makepyfile(SCRIPT + ''' + script = testdir.makepyfile( + SCRIPT + + """ def test_fail(p): assert False -''') +""" + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-fail-under=100', - '--cov-report=html', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-fail-under=100', '--cov-report=html', script) - result.stdout.fnmatch_lines_random([ - '*10 failed*', - '*coverage: platform*', - '*FAIL Required test coverage of 100% not reached*', - '*assert False*', - ]) + result.stdout.fnmatch_lines_random( + [ + '*10 failed*', + '*coverage: platform*', + '*FAIL Required test coverage of 100% not reached*', + '*assert False*', + ] + ) @pytest.mark.skipif('sys.platform == "win32" or platform.python_implementation() == "PyPy"') def test_dist_combine_racecondition(testdir): - script = testdir.makepyfile(""" + script = testdir.makepyfile( + """ import pytest @pytest.mark.parametrize("foo", range(1000)) def test_foo(foo): -""" + "\n".join(f""" +""" + + '\n'.join( + f""" if foo == {i}: assert True -""" for i in range(1000))) - - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '-n', '5', '-s', - script) - result.stdout.fnmatch_lines([ - 'test_dist_combine_racecondition* 0 * 100%*', - '*1000 passed*' - ]) +""" + for i in range(1000) + ) + ) + + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '-n', '5', '-s', script) + result.stdout.fnmatch_lines(['test_dist_combine_racecondition* 0 * 100%*', '*1000 passed*']) for line in chain(result.stdout.lines, result.stderr.lines): assert 'The following workers failed to return coverage data' not in line @@ -787,19 +789,18 @@ def test_foo(foo): def test_dist_collocated(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - '--tx=2*popen', - max_worker_restart_0, - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_dist_collocated* {prop.result} *', - '*10 passed*' - ]) + result = testdir.runpytest( + '-v', + f'--cov={script.dirpath()}', + '--cov-report=term-missing', + '--dist=load', + '--tx=2*popen', + max_worker_restart_0, + script, + *prop.args, + ) + + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -808,31 +809,33 @@ def test_dist_not_collocated(pytester, testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') dir2 = testdir.mkdir('dir2') - testdir.tmpdir.join('.coveragerc').write(f''' + testdir.tmpdir.join('.coveragerc').write( + f""" [run] {prop.conf} [paths] source = . dir1 - dir2''') - - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - f'--tx=popen//chdir={dir1}', - f'--tx=popen//chdir={dir2}', - f'--rsyncdir={script.basename}', - '--rsyncdir=.coveragerc', - max_worker_restart_0, '-s', - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_dist_not_collocated* {prop.result} *', - '*10 passed*' - ]) + dir2""" + ) + + result = testdir.runpytest( + '-v', + f'--cov={script.dirpath()}', + '--cov-report=term-missing', + '--dist=load', + f'--tx=popen//chdir={dir1}', + f'--tx=popen//chdir={dir2}', + f'--rsyncdir={script.basename}', + '--rsyncdir=.coveragerc', + max_worker_restart_0, + '-s', + script, + *prop.args, + ) + + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -841,7 +844,8 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') dir2 = testdir.mkdir('dir2') - testdir.tmpdir.join('.coveragerc').write(f''' + testdir.tmpdir.join('.coveragerc').write( + f""" [run] {prop.conf} source = {script.dirpath()} @@ -849,66 +853,67 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): source = . dir1 - dir2''') - - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term-missing', - '--dist=load', - f'--tx=popen//chdir={dir1}', - f'--tx=popen//chdir={dir2}', - f'--rsyncdir={script.basename}', - '--rsyncdir=.coveragerc', - max_worker_restart_0, '-s', - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_dist_not_collocated* {prop.result} *', - '*10 passed*' - ]) + dir2""" + ) + + result = testdir.runpytest( + '-v', + '--cov', + '--cov-report=term-missing', + '--dist=load', + f'--tx=popen//chdir={dir1}', + f'--tx=popen//chdir={dir2}', + f'--rsyncdir={script.basename}', + '--rsyncdir=.coveragerc', + max_worker_restart_0, + '-s', + script, + *prop.args, + ) + + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 def test_central_subprocess(testdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') - result = testdir.runpytest('-v', - f'--cov={scripts.dirpath()}', - '--cov-report=term-missing', - parent_script) + result = testdir.runpytest('-v', f'--cov={scripts.dirpath()}', '--cov-report=term-missing', parent_script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'child_script* {CHILD_SCRIPT_RESULT}*', - f'parent_script* {PARENT_SCRIPT_RESULT}*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'child_script* {CHILD_SCRIPT_RESULT}*', + f'parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 def test_central_subprocess_change_cwd(testdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT_CHANGE_CWD, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT_CHANGE_CWD, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') - testdir.makefile('', coveragerc=""" + testdir.makefile( + '', + coveragerc=""" [run] branch = true parallel = true -""") +""", + ) - result = testdir.runpytest('-v', '-s', - f'--cov={scripts.dirpath()}', - '--cov-config=coveragerc', - '--cov-report=term-missing', - parent_script) + result = testdir.runpytest( + '-v', '-s', f'--cov={scripts.dirpath()}', '--cov-config=coveragerc', '--cov-report=term-missing', parent_script + ) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'*child_script* {CHILD_SCRIPT_RESULT}*', - '*parent_script* 100%*', - ]) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'*child_script* {CHILD_SCRIPT_RESULT}*', + '*parent_script* 100%*', + ] + ) assert result.ret == 0 @@ -917,126 +922,130 @@ def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkey parent_script = stuff.join('parent_script.py') parent_script.write(SCRIPT_PARENT_CHANGE_CWD_IMPORT_CHILD) stuff.join('child_script.py').write(SCRIPT_CHILD) - testdir.makefile('', coveragerc=""" + testdir.makefile( + '', + coveragerc=""" [run] parallel = true -""") +""", + ) monkeypatch.setitem(os.environ, 'PYTHONPATH', str(stuff)) - result = testdir.runpytest('-vv', '-s', - '--cov=child_script', - '--cov-config=coveragerc', - '--cov-report=term-missing', - '--cov-branch', - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'*child_script* {CHILD_SCRIPT_RESULT}*', - ]) + result = testdir.runpytest( + '-vv', '-s', '--cov=child_script', '--cov-config=coveragerc', '--cov-report=term-missing', '--cov-branch', parent_script + ) + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'*child_script* {CHILD_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 def test_central_subprocess_no_subscript(testdir): - script = testdir.makepyfile(""" + script = testdir.makepyfile( + """ import subprocess, sys def test_foo(): subprocess.check_call([sys.executable, '-c', 'print("Hello World")']) -""") - testdir.makefile('', coveragerc=""" +""" + ) + testdir.makefile( + '', + coveragerc=""" [run] parallel = true -""") - result = testdir.runpytest('-v', - '--cov-config=coveragerc', - f'--cov={script.dirpath()}', - '--cov-branch', - script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_central_subprocess_no_subscript* * 3 * 0 * 100%*', - ]) +""", + ) + result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-branch', script) + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + 'test_central_subprocess_no_subscript* * 3 * 0 * 100%*', + ] + ) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_subprocess_collocated(testdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') - result = testdir.runpytest('-v', - f'--cov={scripts.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - '--tx=2*popen', - max_worker_restart_0, - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'child_script* {CHILD_SCRIPT_RESULT}*', - f'parent_script* {PARENT_SCRIPT_RESULT}*', - ]) + result = testdir.runpytest( + '-v', f'--cov={scripts.dirpath()}', '--cov-report=term-missing', '--dist=load', '--tx=2*popen', max_worker_restart_0, parent_script + ) + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'child_script* {CHILD_SCRIPT_RESULT}*', + f'parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') child_script = scripts.dirpath().join('child_script.py') dir1 = tmpdir.mkdir('dir1') dir2 = tmpdir.mkdir('dir2') - testdir.tmpdir.join('.coveragerc').write(f''' + testdir.tmpdir.join('.coveragerc').write( + f""" [paths] source = {scripts.dirpath()} */dir1 */dir2 -''') - result = testdir.runpytest('-v', - f'--cov={scripts.dirpath()}', - '--dist=load', - f'--tx=popen//chdir={dir1}', - f'--tx=popen//chdir={dir2}', - f'--rsyncdir={child_script}', - f'--rsyncdir={parent_script}', - '--rsyncdir=.coveragerc', - max_worker_restart_0, - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'child_script* {CHILD_SCRIPT_RESULT}*', - f'parent_script* {PARENT_SCRIPT_RESULT}*', - ]) +""" + ) + result = testdir.runpytest( + '-v', + f'--cov={scripts.dirpath()}', + '--dist=load', + f'--tx=popen//chdir={dir1}', + f'--tx=popen//chdir={dir2}', + f'--rsyncdir={child_script}', + f'--rsyncdir={parent_script}', + '--rsyncdir=.coveragerc', + max_worker_restart_0, + parent_script, + ) + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'child_script* {CHILD_SCRIPT_RESULT}*', + f'parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 def test_invalid_coverage_source(testdir): script = testdir.makepyfile(SCRIPT) - testdir.makeini(""" + testdir.makeini( + """ [pytest] console_output_style=classic - """) - result = testdir.runpytest('-v', - '--cov=non_existent_module', - '--cov-report=term-missing', - script) + """ + ) + result = testdir.runpytest('-v', '--cov=non_existent_module', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines([ - '*10 passed*' - ]) - result.stderr.fnmatch_lines([ - '*No data was collected.*' - ]) - result.stdout.fnmatch_lines([ - '*Failed to generate report: No data to report.', - ]) + result.stdout.fnmatch_lines(['*10 passed*']) + result.stderr.fnmatch_lines(['*No data was collected.*']) + result.stdout.fnmatch_lines( + [ + '*Failed to generate report: No data to report.', + ] + ) assert result.ret == 0 matching_lines = [line for line in result.outlines if '%' in line] @@ -1045,75 +1054,64 @@ def test_invalid_coverage_source(testdir): @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") +@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": + 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__}' - - ]) + 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.' - ]) + 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) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + 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 def test_funcarg_not_active(testdir): script = testdir.makepyfile(SCRIPT_FUNCARG_NOT_ACTIVE) - result = testdir.runpytest('-v', - script) + result = testdir.runpytest('-v', script) - result.stdout.fnmatch_lines([ - '*1 passed*' - ]) + result.stdout.fnmatch_lines(['*1 passed*']) 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.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') def test_cleanup_on_sigterm(testdir): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + ''' import os, signal, subprocess, sys, time def cleanup(num, frame): @@ -1141,30 +1139,28 @@ def test_run(): time.sleep(10) except BaseException as exc: print("captured %r" % exc) -''') +''' + ) - result = testdir.runpytest('-vv', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + 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 @pytest.mark.skipif('sys.platform != "win32"') -@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'), -]) +@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'), + ], +) 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/ - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import os, signal, subprocess, sys, time def test_run(): @@ -1181,37 +1177,37 @@ def test_run(): if __name__ == "__main__": from pytest_cov.embed import cleanup_on_signal, cleanup - ''' + setup[0] + ''' + """ + + setup[0] + + """ try: time.sleep(10) except BaseException as exc: print("captured %r" % exc) -''') +""" + ) - result = testdir.runpytest('-vv', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + 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 @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'), -]) +@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): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import os, signal, subprocess, sys, time def test_run(): @@ -1225,33 +1221,29 @@ def test_run(): if __name__ == "__main__": from pytest_cov.embed import cleanup_on_sigterm, cleanup - ''' + setup[0] + ''' + """ + + setup[0] + + """ 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 * -*', - f'test_cleanup_on_sigterm* {setup[1]}', - '*1 passed*' - ]) +""" + ) + + 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*']) 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") +@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): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + ''' import os, signal, subprocess, sys, time def test_run(): @@ -1272,26 +1264,20 @@ def test_run(): 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* 88% 19-20', - '*1 passed*' - ]) +''' + ) + + 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") +@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(''' + script = testdir.makepyfile( + """ import os, signal, subprocess, sys, time def test_run(): @@ -1303,8 +1289,7 @@ def test_run(): stdout, stderr = proc.communicate() assert not stderr assert stdout == b"" - # it appears signal handling is buggy on python 2? - if sys.version_info == 3: assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] + assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] if __name__ == "__main__": signal.signal(signal.SIGINT, signal.SIG_IGN) @@ -1316,42 +1301,35 @@ def test_run(): 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% 23-24', - '*1 passed*' - ]) + """ + ) + + 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*']) assert result.ret == 0 -MODULE = ''' +MODULE = """ def func(): return 1 -''' +""" -CONFTEST = ''' +CONFTEST = """ import mod mod.func() -''' +""" -BASIC_TEST = ''' +BASIC_TEST = """ def test_basic(): x = True assert x -''' +""" CONF_RESULT = 'mod* 2 * 100%*' @@ -1360,14 +1338,32 @@ def test_cover_conftest(testdir): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) script = testdir.makepyfile(BASIC_TEST) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines([CONF_RESULT]) +def test_filterwarnings_error(testdir): + testdir.makeini(r""" + [pytest] + filterwarnings = + error + """) + testdir.makepyfile(mod=MODULE) + testdir.makepyfile(plug=CONFTEST) + script = testdir.makepyfile(BASIC_TEST) + result = testdir.runpytest('-v', '--cov=mod', '--cov-report=xml', '--cov-report=term-missing', '-p', 'plug', script) + assert result.ret == 0 + result.stdout.fnmatch_lines(['* 1 passed *']) + result.stderr.fnmatch_lines( + [ + '* (module-not-measured)', + '* (no-data-collected)', + '* CovReportWarning: Failed to generate report: No data to report.', + ] + ) + + @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_cover_looponfail(testdir, monkeypatch): testdir.makepyfile(mod=MODULE) @@ -1382,16 +1378,9 @@ def mock_run(*args, **kwargs): if hasattr(testdir, '_pytester'): monkeypatch.setattr(testdir._pytester, 'run', mock_run) assert testdir._pytester.run is mock_run - with testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--looponfail', - script) as process: + with testdir.runpytest('-v', f'--cov={script.dirpath()}', '--looponfail', script) as process: with dump_on_error(process.read): - wait_for_strings( - process.read, - 30, # 30 seconds - 'Stmts Miss Cover' - ) + wait_for_strings(process.read, 30, 'Stmts Miss Cover') # 30 seconds @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') @@ -1399,20 +1388,17 @@ def test_cover_conftest_dist(testdir): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) script = testdir.makepyfile(BASIC_TEST) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - '--tx=2*popen', - max_worker_restart_0, - script) + result = testdir.runpytest( + '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--dist=load', '--tx=2*popen', max_worker_restart_0, script + ) assert result.ret == 0 result.stdout.fnmatch_lines([CONF_RESULT]) def test_no_cover_marker(testdir): testdir.makepyfile(mod=MODULE) - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import pytest import mod import subprocess @@ -1422,18 +1408,17 @@ def test_no_cover_marker(testdir): def test_basic(): mod.func() subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) -''') - result = testdir.runpytest('-v', '-ra', '--strict', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) +""" + ) + result = testdir.runpytest('-v', '-ra', '--strict', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines(['mod* 2 * 1 * 50% * 2']) def test_no_cover_fixture(testdir): testdir.makepyfile(mod=MODULE) - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import mod import subprocess import sys @@ -1441,23 +1426,28 @@ def test_no_cover_fixture(testdir): def test_basic(no_cover): mod.func() subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) -''') - result = testdir.runpytest('-v', '-ra', '--strict', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) +""" + ) + result = testdir.runpytest('-v', '-ra', '--strict', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines(['mod* 2 * 1 * 50% * 2']) -COVERAGERC = ''' +COVERAGERC = """ [report] # Regexes for lines to exclude from consideration exclude_lines = raise NotImplementedError -''' +""" +PYPROJECTTOML = """ +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_lines = [ + 'raise NotImplementedError', +] +""" -EXCLUDED_TEST = ''' +EXCLUDED_TEST = """ def func(): raise NotImplementedError @@ -1466,7 +1456,7 @@ def test_basic(): x = True assert x -''' +""" EXCLUDED_RESULT = '4 * 100%*' @@ -1474,38 +1464,37 @@ def test_basic(): def test_coveragerc(testdir): testdir.makefile('', coveragerc=COVERAGERC) script = testdir.makepyfile(EXCLUDED_TEST) - result = testdir.runpytest('-v', - '--cov-config=coveragerc', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines([f'test_coveragerc* {EXCLUDED_RESULT}']) +def test_pyproject_toml(testdir): + testdir.makefile('.toml', pyproject=PYPROJECTTOML) + script = testdir.makepyfile(EXCLUDED_TEST) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + assert result.ret == 0 + result.stdout.fnmatch_lines([f'test_pyproject_toml* {EXCLUDED_RESULT}']) + + @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_coveragerc_dist(testdir): testdir.makefile('', coveragerc=COVERAGERC) script = testdir.makepyfile(EXCLUDED_TEST) - result = testdir.runpytest('-v', - '--cov-config=coveragerc', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '-n', '2', - max_worker_restart_0, - script) + result = testdir.runpytest( + '-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-report=term-missing', '-n', '2', max_worker_restart_0, script + ) assert result.ret == 0 - result.stdout.fnmatch_lines( - [f'test_coveragerc_dist* {EXCLUDED_RESULT}']) + result.stdout.fnmatch_lines([f'test_coveragerc_dist* {EXCLUDED_RESULT}']) -SKIP_COVERED_COVERAGERC = ''' +SKIP_COVERED_COVERAGERC = """ [report] skip_covered = True -''' +""" -SKIP_COVERED_TEST = ''' +SKIP_COVERED_TEST = """ def func(): return "full coverage" @@ -1513,21 +1502,16 @@ def func(): def test_basic(): assert func() == "full coverage" -''' +""" SKIP_COVERED_RESULT = '1 file skipped due to complete coverage.' -@pytest.mark.parametrize('report_option', [ - 'term-missing:skip-covered', - 'term:skip-covered']) +@pytest.mark.parametrize('report_option', ['term-missing:skip-covered', 'term:skip-covered']) def test_skip_covered_cli(pytester, testdir, report_option): testdir.makefile('', coveragerc=SKIP_COVERED_COVERAGERC) script = testdir.makepyfile(SKIP_COVERED_TEST) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - f'--cov-report={report_option}', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', f'--cov-report={report_option}', script) assert result.ret == 0 result.stdout.fnmatch_lines([SKIP_COVERED_RESULT]) @@ -1535,81 +1519,58 @@ def test_skip_covered_cli(pytester, testdir, report_option): def test_skip_covered_coveragerc_config(testdir): testdir.makefile('', coveragerc=SKIP_COVERED_COVERAGERC) script = testdir.makepyfile(SKIP_COVERED_TEST) - result = testdir.runpytest('-v', - '--cov-config=coveragerc', - f'--cov={script.dirpath()}', - script) + result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', script) assert result.ret == 0 result.stdout.fnmatch_lines([SKIP_COVERED_RESULT]) -CLEAR_ENVIRON_TEST = ''' +CLEAR_ENVIRON_TEST = """ import os def test_basic(): os.environ.clear() -''' +""" def test_clear_environ(testdir): script = testdir.makepyfile(CLEAR_ENVIRON_TEST) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 -SCRIPT_SIMPLE = ''' +SCRIPT_SIMPLE = """ def test_foo(): assert 1 == 1 x = True assert x -''' +""" 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('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 = 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 @pytest.mark.skipif('sys.platform == "win32"') -@pytest.mark.skipif('sys.version_info[0] > 2 and platform.python_implementation() == "PyPy"', - reason="strange optimization on PyPy3") +@pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason='strange optimization on PyPy3') def test_dist_bare_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--cov', - '-n', '1', - script) + 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 @@ -1626,28 +1587,21 @@ class ns: def test_default_output_setting(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines([ - '*coverage*' - ]) + result.stdout.fnmatch_lines(['*coverage*']) assert result.ret == 0 def test_disabled_output(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=', script) stdout = result.stdout.str() # We don't want the path to the executable to fail the test if we happen # to put the project in a directory with "coverage" in it. - stdout = stdout.replace(sys.executable, "") + stdout = stdout.replace(sys.executable, '') assert 'coverage' not in stdout assert result.ret == 0 @@ -1657,8 +1611,7 @@ def test_coverage_file(testdir): data_file_name = 'covdata' os.environ['COVERAGE_FILE'] = data_file_name try: - result = testdir.runpytest('-v', f'--cov={script.dirpath()}', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 data_file = testdir.tmpdir.join(data_file_name) assert data_file.check() @@ -1668,14 +1621,15 @@ def test_coverage_file(testdir): def test_external_data_file(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [run] data_file = %s -""" % testdir.tmpdir.join('some/special/place/coverage-data').ensure()) +""" + % testdir.tmpdir.join('some/special/place/coverage-data').ensure() + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script) + 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*'))) @@ -1683,33 +1637,31 @@ def test_external_data_file(testdir): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_external_data_file_xdist(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [run] parallel = true data_file = %s -""" % testdir.tmpdir.join('some/special/place/coverage-data').ensure()) +""" + % testdir.tmpdir.join('some/special/place/coverage-data').ensure() + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '-n', '1', - max_worker_restart_0, - script) + 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*'))) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_xdist_no_data_collected(testdir): - testdir.makepyfile(target="x = 123") - script = testdir.makepyfile(""" + testdir.makepyfile(target='x = 123') + script = testdir.makepyfile( + """ import target def test_foobar(): assert target.x == 123 -""") - result = testdir.runpytest('-v', - '--cov=target', - '-n', '1', - script) +""" + ) + result = testdir.runpytest('-v', '--cov=target', '-n', '1', script) assert 'no-data-collected' not in result.stderr.str() assert 'no-data-collected' not in result.stdout.str() assert 'module-not-imported' not in result.stderr.str() @@ -1719,11 +1671,9 @@ def test_foobar(): def test_external_data_file_negative(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write("") + testdir.tmpdir.join('.coveragerc').write('') - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) @@ -1732,67 +1682,130 @@ def test_external_data_file_negative(testdir): def test_append_coverage(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script, - *opts.split() + prop.args) - result.stdout.fnmatch_lines([ - f'test_1* {prop.result}*', - ]) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + ] + ) script2 = testdir.makepyfile(test_2=prop.code2) - result = testdir.runpytest('-v', - '--cov-append', - f'--cov={script2.dirpath()}', - script2, - *opts.split() + prop.args) - result.stdout.fnmatch_lines([ - f'test_1* {prop.result}*', - f'test_2* {prop.result2}*', - ]) + result = testdir.runpytest('-v', '--cov-append', f'--cov={script2.dirpath()}', script2, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + f'test_2* {prop.result2}*', + ] + ) + + +@xdist_params +def test_coverage_plugin(pytester, testdir, opts, prop): + script = testdir.makepyfile(test_1=prop.code) + testdir.makepyfile( + coverageplugin=""" +import coverage + +class ExamplePlugin(coverage.CoveragePlugin): + pass + +def coverage_init(reg, options): + reg.add_file_tracer(ExamplePlugin()) +""" + ) + testdir.makepyprojecttoml(f""" +[tool.coverage.run] +plugins = ["coverageplugin"] +concurrency = ["thread", "multiprocessing"] +{prop.conf} +""") + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + ] + ) + + +@xdist_params +def test_dynamic_context(pytester, testdir, opts, prop): + script = testdir.makepyfile(test_1=prop.code) + 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) + if opts: + result.stderr.fnmatch_lines(['pytest_cov.DistCovError: Detected dynamic_context=test_function*']) + else: + result.stdout.fnmatch_lines( + [ + '* CentralCovContextWarning: Detected dynamic_context=test_function*', + f'test_1* {prop.result}*', + ] + ) + + +@xdist_params +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) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + ] + ) @xdist_params def test_do_not_append_coverage(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script, - *opts.split()+prop.args) - result.stdout.fnmatch_lines([ - f'test_1* {prop.result}*', - ]) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + ] + ) script2 = testdir.makepyfile(test_2=prop.code2) - result = testdir.runpytest('-v', - f'--cov={script2.dirpath()}', - script2, - *opts.split()+prop.args) - result.stdout.fnmatch_lines([ - 'test_1* 0%', - f'test_2* {prop.result2}*', - ]) + result = testdir.runpytest('-v', f'--cov={script2.dirpath()}', script2, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + 'test_1* 0%', + f'test_2* {prop.result2}*', + ] + ) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_append_coverage_subprocess(testdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') - result = testdir.runpytest('-v', - f'--cov={scripts.dirpath()}', - '--cov-append', - '--cov-report=term-missing', - '--dist=load', - '--tx=2*popen', - max_worker_restart_0, - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'child_script* {CHILD_SCRIPT_RESULT}*', - f'parent_script* {PARENT_SCRIPT_RESULT}*', - ]) + result = testdir.runpytest( + '-v', + f'--cov={scripts.dirpath()}', + '--cov-append', + '--cov-report=term-missing', + '--dist=load', + '--tx=2*popen', + max_worker_restart_0, + parent_script, + ) + + result.stdout.fnmatch_lines( + [ + '*_ coverage: platform *, python * _*', + f'child_script* {CHILD_SCRIPT_RESULT}*', + f'parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 @@ -1804,7 +1817,7 @@ class SpecificError(Exception): pass def bad_init(): - raise SpecificError() + raise SpecificError buff = StringIO() @@ -1814,78 +1827,44 @@ 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 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 = 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*']) assert result.ret == 0 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 = testdir.runpytest('-v', '--assert=plain', '--cov', '--cov', script) + + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 def test_cov_reset(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--assert=plain', - f'--cov={script.dirpath()}', - '--cov-reset', - script) + result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-reset', script) assert 'coverage: platform' not in result.stdout.str() 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 = 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*']) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_cov_and_no_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--cov', '--no-cov', - '-n', '1', - '-s', - script) + result = testdir.runpytest('-v', '--cov', '--no-cov', '-n', '1', '-s', script) assert 'Coverage disabled via --no-cov switch!' not in result.stdout.str() assert 'Coverage disabled via --no-cov switch!' not in result.stderr.str() assert result.ret == 0 @@ -1935,56 +1914,86 @@ def find_labels(text, pattern): } -@pytest.mark.skipif("coverage.version_info < (5, 0)") -@pytest.mark.skipif("coverage.version_info > (6, 4)") +@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: + with open(os.path.join(os.path.dirname(__file__), 'contextful.py')) 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() - ) - assert result.ret == 0 - result.stdout.fnmatch_lines([ - 'test_contexts* 100%*', - ]) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-context=test', script, *opts.split()) + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + 'test_contexts* 100%*', + ] + ) - data = coverage.CoverageData(".coverage") + data = coverage.CoverageData('.coverage') data.read() assert data.measured_contexts() == set(EXPECTED_CONTEXTS) measured = data.measured_files() assert len(measured) == 1 - test_context_path = list(measured)[0] - assert test_context_path.lower() == os.path.abspath("test_contexts.py").lower() + test_context_path = next(iter(measured)) + assert test_context_path.lower() == os.path.abspath('test_contexts.py').lower() - line_data = find_labels(contextful_tests, r"[crst]\d+(?:-\d+)?") + line_data = find_labels(contextful_tests, r'[crst]\d+(?:-\d+)?') for context, label in EXPECTED_CONTEXTS.items(): if context == '': continue data.set_query_context(context) actual = set(data.lines(test_context_path)) - assert line_data[label] == actual, f"Wrong lines for context {context!r}" + assert line_data[label] == actual, f'Wrong lines for context {context!r}' -@pytest.mark.skipif("coverage.version_info >= (5, 0)") +@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', - ]) + 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 + +def foobar(): + return 1 + +def test_with_coverage(): + foobar() + +@pytest.mark.no_cover() +def test_without_coverage(): + foobar() +""") + result = testdir.runpytest( + '-v', + '--cov-context=test', + '--cov=test_contexts_no_cover', + script, + ) + result.stdout.fnmatch_lines( + [ + 'test_contexts_no_cover.py 8 1 88%', + 'TOTAL 8 1 88%', + ] + ) + assert result.stderr.lines == [] + assert result.ret == 0 + + def test_issue_417(testdir): # https://github.com/pytest-dev/pytest-cov/issues/417 - whatever = testdir.maketxtfile(whatever="") + whatever = testdir.maketxtfile(whatever='') testdir.inline_genitems(whatever) diff --git a/tox.ini b/tox.ini index 7d49a3be..a4465b5a 100644 --- a/tox.ini +++ b/tox.ini @@ -7,59 +7,57 @@ commands = python ci/bootstrap.py --no-env passenv = * -; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist +; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments [tox] envlist = - check - py{37,38,39,310,311,py37,py38,py39}-pytest{73}-xdist330-coverage{72} - docs + clean, + check, + docs, + {py39,py310,py311,py312,py313,pypy39,pypy310}-{pytest83,pytest84}-{xdist36,xdist37}-{coverage78}, + report +ignore_basepython_conflict = true [testenv] +basepython = + pypy38: {env:TOXPYTHON:pypy3.8} + pypy39: {env:TOXPYTHON:pypy3.9} + pypy310: {env:TOXPYTHON:pypy3.10} + py38: {env:TOXPYTHON:python3.8} + py39: {env:TOXPYTHON:python3.9} + 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 = + PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes # Use env vars for (optional) pinning of deps. - pytest46: _DEP_PYTEST=pytest==4.6.10 - pytest53: _DEP_PYTEST=pytest==5.3.2 - pytest54: _DEP_PYTEST=pytest==5.4.3 - pytest60: _DEP_PYTEST=pytest==6.0.2 - pytest61: _DEP_PYTEST=pytest==6.1.2 pytest62: _DEP_PYTEST=pytest==6.2.5 - pytest70: _DEP_PYTEST=pytest==7.0.1 - pytest71: _DEP_PYTEST=pytest==7.1.2 - pytest72: _DEP_PYTEST=pytest==7.2.0 - pytest73: _DEP_PYTEST=pytest==7.3.1 + 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.5 + pytest84: _DEP_PYTEST=pytest==8.4.0 - xdist127: _DEP_PYTESTXDIST=pytest-xdist==1.27.0 - xdist129: _DEP_PYTESTXDIST=pytest-xdist==1.29.0 - xdist131: _DEP_PYTESTXDIST=pytest-xdist==1.31.0 - xdist132: _DEP_PYTESTXDIST=pytest-xdist==1.32.0 - xdist133: _DEP_PYTESTXDIST=pytest-xdist==1.33.0 - xdist134: _DEP_PYTESTXDIST=pytest-xdist==1.34.0 - xdist200: _DEP_PYTESTXDIST=pytest-xdist==2.0.0 - xdist201: _DEP_PYTESTXDIST=pytest-xdist==2.1.0 - xdist202: _DEP_PYTESTXDIST=pytest-xdist==2.2.0 - xdist250: _DEP_PYTESTXDIST=pytest-xdist==2.5.0 - xdist320: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 - xdist330: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 + xdist32: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 + xdist33: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 + xdist34: _DEP_PYTESTXDIST=pytest-xdist==3.4.0 + xdist35: _DEP_PYTESTXDIST=pytest-xdist==3.5.0 + xdist36: _DEP_PYTESTXDIST=pytest-xdist==3.6.1 + xdist37: _DEP_PYTESTXDIST=pytest-xdist==3.7.0 xdistdev: _DEP_PYTESTXDIST=git+https://github.com/pytest-dev/pytest-xdist.git#egg=pytest-xdist - coverage45: _DEP_COVERAGE=coverage==4.5.4 - coverage50: _DEP_COVERAGE=coverage==5.0.4 - coverage51: _DEP_COVERAGE=coverage==5.1 - coverage52: _DEP_COVERAGE=coverage==5.2.1 - coverage53: _DEP_COVERAGE=coverage==5.3.1 - coverage54: _DEP_COVERAGE=coverage==5.4 - coverage55: _DEP_COVERAGE=coverage==5.5 - coverage60: _DEP_COVERAGE=coverage==6.0.2 - coverage61: _DEP_COVERAGE=coverage==6.1.2 - coverage62: _DEP_COVERAGE=coverage==6.2 - coverage63: _DEP_COVERAGE=coverage==6.3.3 - coverage64: _DEP_COVERAGE=coverage==6.4.2 - coverage65: _DEP_COVERAGE=coverage==6.5.0 - coverage72: _DEP_COVERAGE=coverage==7.2.5 + coverage72: _DEP_COVERAGE=coverage==7.2.7 + 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.12 + coverage77: _DEP_COVERAGE=coverage==7.7.1 + coverage77: _DEP_COVERAGE=coverage==7.8.2 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv = @@ -72,37 +70,38 @@ pip_pre = true commands = {posargs:pytest -vv} -[testenv:spell] -setenv = - SPELLCHECK=1 -commands = - sphinx-build -b spelling docs dist/docs -skip_install = true -usedevelop = false +[testenv:check] deps = - -r{toxinidir}/docs/requirements.txt - sphinxcontrib-spelling - pyenchant + docutils + check-manifest + pre-commit + readme-renderer + pygments + isort +skip_install = true +commands = + python setup.py check --strict --metadata --restructuredtext + check-manifest . + pre-commit run --all-files --show-diff-on-failure [testenv:docs] +usedevelop = true deps = -r{toxinidir}/docs/requirements.txt commands = sphinx-build {posargs:-E} -b html docs dist/docs + sphinx-build -b linkcheck docs dist/docs -[testenv:check] +[testenv:report] deps = - check-manifest - colorama # TODO Remove when isort > v6.0.0b2 is released. - docutils - flake8 - isort - pygments - readme-renderer + coverage skip_install = true -usedevelop = false commands = - python setup.py check --strict --metadata --restructuredtext - check-manifest {toxinidir} - flake8 src tests setup.py - isort --check-only --diff src tests setup.py + coverage report + coverage html + +[testenv:clean] +commands = coverage erase +skip_install = true +deps = + coverage pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy