diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index fde3a6aa..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,33 +0,0 @@ -# NOTE: this file is auto-generated via ci/bootstrap.py (ci/templates/.appveyor.yml). -version: '{branch}-{build}' -build: off -environment: - matrix: - - TOXENV: check - - TOXENV: 'py27-pytest46-xdist127-coverage55' - - TOXENV: 'py35-pytest46-xdist127-coverage55' - - TOXENV: 'py36-pytest46-xdist127-coverage55,py36-pytest46-xdist133-coverage55,py36-pytest54-xdist133-coverage55,py36-pytest62-xdist202-coverage55' - - TOXENV: 'py37-pytest46-xdist127-coverage55,py37-pytest46-xdist133-coverage55,py37-pytest54-xdist133-coverage55,py37-pytest62-xdist202-coverage55' - - TOXENV: 'pypy-pytest46-xdist127-coverage55' - -init: - - ps: echo $env:TOXENV - - ps: ls C:\Python* -install: - - IF "%TOXENV:~0,5%" == "pypy-" choco install --no-progress python.pypy - - IF "%TOXENV:~0,6%" == "pypy3-" choco install --no-progress pypy3 - - SET PATH=C:\tools\pypy\pypy;%PATH% - - C:\Python37\python -m pip install --progress-bar=off tox -rci/requirements.txt - -test_script: - - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd C:\Python37\python -m tox - -on_failure: - - ps: dir "env:" - - ps: get-content .tox\*\log\* -artifacts: - - path: dist\* - -### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): -# on_finish: -# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 7e15ff94..cca1163f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.12.0 +current_version = 5.0.0 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 9cad1178..18487fef 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -1,53 +1,47 @@ # Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) default_context: - allow_tests_inside_package: no - appveyor: yes + 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_token: '[Required for Appveyor, take it from https://coveralls.io/github/ionelmc/pytest-cov]' + coveralls: 'no' distribution_name: pytest-cov email: contact@ionelmc.ro + formatter_quote_style: single full_name: Ionel Cristian Mărieș - 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: '2020-06-12' + 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: yes - travis_osx: no - version: 2.10.1 + test_matrix_separate_coverage: 'no' + version: 5.0.0 + version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' - year_to: '2020' + 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 new file mode 100644 index 00000000..84753edb --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,240 @@ +name: Tests +on: [push, pull_request, workflow_dispatch] +jobs: + examples: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["pypy-3.9", "3.11"] + target: [ + "src-layout", + "adhoc-layout", + ] + include: + # Add new helper variables to existing jobs + - {python-version: "pypy-3.9", tox-python-version: "pypy3"} + - {python-version: "3.11", tox-python-version: "py311"} + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: + examples-v1-${{ hashFiles('**/tox.ini') }} + restore-keys: | + examples-v1- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade wheel + python -m pip install --progress-bar=off tox -rci/requirements.txt + + - name: Examples + run: | + cd examples/${{ matrix.target }} + tox -v -e ${{ matrix.tox-python-version }} + test: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: 'check' + python: '3.11' + toxpython: 'python3.11' + tox_env: 'check' + os: 'ubuntu-latest' + - name: 'docs' + python: '3.11' + toxpython: 'python3.11' + tox_env: 'docs' + os: 'ubuntu-latest' + - name: 'py38-pytest81-xdist350-coverage74 (ubuntu)' + python: '3.8' + toxpython: 'python3.8' + python_arch: 'x64' + tox_env: 'py38-pytest81-xdist350-coverage74' + os: 'ubuntu-latest' + - name: 'py38-pytest81-xdist350-coverage74 (windows)' + python: '3.8' + toxpython: 'python3.8' + python_arch: 'x64' + tox_env: 'py38-pytest81-xdist350-coverage74' + os: 'windows-latest' + - name: 'py38-pytest81-xdist350-coverage74 (macos)' + python: '3.8' + toxpython: 'python3.8' + python_arch: 'x64' + tox_env: 'py38-pytest81-xdist350-coverage74' + os: 'macos-latest' + - name: 'py39-pytest81-xdist350-coverage74 (ubuntu)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39-pytest81-xdist350-coverage74' + os: 'ubuntu-latest' + - name: 'py39-pytest81-xdist350-coverage74 (windows)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39-pytest81-xdist350-coverage74' + os: 'windows-latest' + - name: 'py39-pytest81-xdist350-coverage74 (macos)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39-pytest81-xdist350-coverage74' + os: 'macos-latest' + - name: 'py310-pytest81-xdist350-coverage74 (ubuntu)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest81-xdist350-coverage74' + os: 'ubuntu-latest' + - name: 'py310-pytest81-xdist350-coverage74 (windows)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest81-xdist350-coverage74' + os: 'windows-latest' + - name: 'py310-pytest81-xdist350-coverage74 (macos)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest81-xdist350-coverage74' + os: 'macos-latest' + - name: 'py311-pytest81-xdist350-coverage74 (ubuntu)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest81-xdist350-coverage74' + os: 'ubuntu-latest' + - name: 'py311-pytest81-xdist350-coverage74 (windows)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest81-xdist350-coverage74' + os: 'windows-latest' + - name: 'py311-pytest81-xdist350-coverage74 (macos)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest81-xdist350-coverage74' + os: 'macos-latest' + - name: 'py312-pytest81-xdist350-coverage74 (ubuntu)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'x64' + tox_env: 'py312-pytest81-xdist350-coverage74' + os: 'ubuntu-latest' + - name: 'py312-pytest81-xdist350-coverage74 (windows)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'x64' + tox_env: 'py312-pytest81-xdist350-coverage74' + os: 'windows-latest' + - name: 'py312-pytest81-xdist350-coverage74 (macos)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'x64' + tox_env: 'py312-pytest81-xdist350-coverage74' + os: 'macos-latest' + - name: 'pypy38-pytest81-xdist350-coverage74 (ubuntu)' + python: 'pypy-3.8' + toxpython: 'pypy3.8' + python_arch: 'x64' + tox_env: 'pypy38-pytest81-xdist350-coverage74' + os: 'ubuntu-latest' + - name: 'pypy38-pytest81-xdist350-coverage74 (windows)' + python: 'pypy-3.8' + toxpython: 'pypy3.8' + python_arch: 'x64' + tox_env: 'pypy38-pytest81-xdist350-coverage74' + os: 'windows-latest' + - name: 'pypy38-pytest81-xdist350-coverage74 (macos)' + python: 'pypy-3.8' + toxpython: 'pypy3.8' + python_arch: 'x64' + tox_env: 'pypy38-pytest81-xdist350-coverage74' + os: 'macos-latest' + - name: 'pypy39-pytest81-xdist350-coverage74 (ubuntu)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39-pytest81-xdist350-coverage74' + os: 'ubuntu-latest' + - name: 'pypy39-pytest81-xdist350-coverage74 (windows)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39-pytest81-xdist350-coverage74' + os: 'windows-latest' + - name: 'pypy39-pytest81-xdist350-coverage74 (macos)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39-pytest81-xdist350-coverage74' + os: 'macos-latest' + - name: 'pypy310-pytest81-xdist350-coverage74 (ubuntu)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest81-xdist350-coverage74' + os: 'ubuntu-latest' + - name: 'pypy310-pytest81-xdist350-coverage74 (windows)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest81-xdist350-coverage74' + os: 'windows-latest' + - name: 'pypy310-pytest81-xdist350-coverage74 (macos)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest81-xdist350-coverage74' + os: 'macos-latest' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + architecture: ${{ matrix.python_arch }} + - name: install dependencies + run: | + python -mpip install --progress-bar=off -r ci/requirements.txt + virtualenv --version + pip --version + tox --version + pip list --format=freeze + - name: test + env: + TOXPYTHON: '${{ matrix.toxpython }}' + run: > + tox -e ${{ matrix.tox_env }} -v + + successful: + # this provides a single status check for branch merge rules + # (use this in `Require status checks to pass before merging` in branch settings) + if: always() + needs: + - test + - examples + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9942328..6a9d1217 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,23 @@ -# To install the git pre-commit hook run: -# pre-commit install -# To update the pre-commit hooks run: -# pre-commit install-hooks -exclude: '^(src/.*\.pth|\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' +# 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.3.3 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] + - repo: https://github.com/psf/black + rev: 24.3.0 + hooks: + - id: black - repo: https://github.com/pre-commit/pre-commit-hooks - rev: master + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer + exclude: '.*\.pth$' - id: debug-statements - - repo: https://github.com/timothycrosley/isort - rev: master - hooks: - - id: isort - - repo: https://gitlab.com/pycqa/flake8 - rev: master - 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/.travis.yml b/.travis.yml deleted file mode 100644 index a9cafa4e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,89 +0,0 @@ -# NOTE: this file is auto-generated via ci/bootstrap.py (ci/templates/.travis.yml). -dist: xenial -language: python -cache: false -env: - global: - - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so - - SEGFAULT_SIGNALS=all -stages: - - lint - - examples - - tests -jobs: - fast_finish: true - allow_failures: - - python: '3.8' - include: - - stage: lint - env: TOXENV=check - - env: TOXENV=docs - - - stage: tests - env: TOXENV=py27-pytest46-xdist127-coverage55 - python: '2.7' - - env: TOXENV=py35-pytest46-xdist127-coverage55 - python: '3.5' - - env: TOXENV=py36-pytest46-xdist127-coverage55 - python: '3.6' - - env: TOXENV=py37-pytest46-xdist127-coverage55 - python: '3.7' - - env: TOXENV=pypy-pytest46-xdist127-coverage55 - python: 'pypy' - - env: TOXENV=pypy3-pytest46-xdist127-coverage55 - python: 'pypy3' - - env: TOXENV=py36-pytest46-xdist133-coverage55 - python: '3.6' - - env: TOXENV=py36-pytest54-xdist133-coverage55 - python: '3.6' - - env: TOXENV=py37-pytest46-xdist133-coverage55 - python: '3.7' - - env: TOXENV=py37-pytest54-xdist133-coverage55 - python: '3.7' - - env: TOXENV=py38-pytest46-xdist133-coverage55 - python: '3.8' - - env: TOXENV=py38-pytest54-xdist133-coverage55 - python: '3.8' - - env: TOXENV=pypy3-pytest46-xdist133-coverage55 - python: 'pypy3' - - env: TOXENV=pypy3-pytest54-xdist133-coverage55 - python: 'pypy3' - - env: TOXENV=py36-pytest62-xdist202-coverage55 - python: '3.6' - - env: TOXENV=py37-pytest62-xdist202-coverage55 - python: '3.7' - - env: TOXENV=py38-pytest62-xdist202-coverage55 - python: '3.8' - - env: TOXENV=py39-pytest62-xdist202-coverage55 - python: '3.9' - - env: TOXENV=pypy3-pytest62-xdist202-coverage55 - python: 'pypy3' - - - stage: examples - python: '3.8' - script: cd $TARGET; tox -v - env: - - TARGET=examples/src-layout - - python: '3.8' - script: cd $TARGET; tox -v - env: - - TARGET=examples/adhoc-layout -before_install: - - python --version - - uname -a - - lsb_release -a -install: - - python -mpip install --progress-bar=off tox -rci/requirements.txt - - virtualenv --version - - easy_install --version - - pip --version - - tox --version -script: - - tox -v -after_failure: - - more .tox/log/* | cat - - more .tox/*/log/* | cat -notifications: - email: - on_success: never - on_failure: always diff --git a/AUTHORS.rst b/AUTHORS.rst index d3d01d11..44e92874 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,15 +1,16 @@ + Authors ======= -* Marc Schlaich - http://www.schlamar.org +* Marc Schlaich - \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 +* 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 @@ -46,3 +47,18 @@ Authors * Pamela McA'Nulty - https://github.com/PamelaM * Christian Riedel - https://github.com/Cielquan * Chris Sreesangkom - https://github.com/csreesan +* Sorin Sbarnea - https://github.com/ssbarnea +* Brian Rutledge - https://github.com/bhrutledge +* Danilo Šegan - https://github.com/dsegan +* Michał Bielawski - https://github.com/D3X +* Zac Hatfield-Dodds - https://github.com/Zac-HD +* Ben Greiner - https://github.com/bnavigator +* Delgan - https://github.com/Delgan +* Andre Brisco - https://github.com/abrisco +* 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 +* Matthew Gamble - https://github.com/mwgamble +* Christian Clauss - https://github.com/cclauss +* Dawn James - https://github.com/dawngerpony diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 23e9a3b9..5dd46aa3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,107 @@ + Changelog ========= +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 `_. + +4.1.0 (2023-05-24) +------------------ + +* 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 `_. + + +4.0.0 (2022-09-28) +------------------ + +**Note that this release drops support for multiprocessing.** + + +* `--cov-fail-under` no longer causes `pytest --collect-only` to fail + Contributed by Zac Hatfield-Dodds in `#511 `_. +* Dropped support for multiprocessing (mostly because `issue 82408 `_). This feature was + mostly working but very broken in certain scenarios and made the test suite very flaky and slow. + + There is builtin multiprocessing support in coverage and you can migrate to that. All you need is this in your + ``.coveragerc``:: + + [run] + concurrency = multiprocessing + parallel = true + sigterm = true +* Fixed deprecation in ``setup.py`` by trying to import setuptools before distutils. + Contributed by Ben Greiner in `#545 `_. +* Removed undesirable new lines that were displayed while reporting was disabled. + Contributed by Delgan in `#540 `_. +* Documentation fixes. + 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 `_. +* Modernized pytest hook implementation. + Contributed by Bruno Oliveira in `#549 `_ + and Ronny Pfannschmidt in `#550 `_. + + +3.0.0 (2021-10-04) +------------------- + +**Note that this release drops support for Python 2.7 and Python 3.5.** + +* Added support for Python 3.10 and updated various test dependencies. + Contributed by Hugo van Kemenade in + `#500 `_. +* Switched from Travis CI to GitHub Actions. Contributed by Hugo van Kemenade in + `#494 `_ and + `#495 `_. +* Add a ``--cov-reset`` CLI option. + Contributed by Danilo Šegan in + `#459 `_. +* Improved validation of ``--cov-fail-under`` CLI option. + Contributed by ... Ronny Pfannschmidt's desire for skark in + `#480 `_. +* Dropped Python 2.7 support. + Contributed by Thomas Grainger in + `#488 `_. +* Updated trove classifiers. Contributed by Michał Bielawski in + `#481 `_. +* Reverted change for `toml` requirement. + Contributed by Thomas Grainger in + `#477 `_. + +2.12.1 (2021-06-01) +------------------- + +* Changed the `toml` requirement to be always be directly required (instead of being required through a coverage extra). + This fixes issues with pip-compile (`pip-tools#1300 `_). + Contributed by Sorin Sbarnea in `#472 `_. +* Documented ``show_contexts``. + Contributed by Brian Rutledge in `#473 `_. 2.12.0 (2021-05-14) ------------------- @@ -32,7 +133,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. @@ -61,7 +162,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 @@ -142,8 +243,6 @@ Changelog `#272 `_, `#271 `_ and `#269 `_. -* Improved documentation regarding subprocess and multiprocessing. - Contributed in `#265 `_. * Improved ``pytest_cov.embed.cleanup_on_sigterm`` to be reentrant (signal deliveries while signal handling is running won't break stuff). * Added ``pytest_cov.embed.cleanup_on_signal`` for customized cleanup. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f02562eb..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 Travis - 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 6db46114..22042a59 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,14 +6,19 @@ prune examples/*/*/htmlcov prune examples/adhoc-layout/*.egg-info prune examples/src-layout/src/*.egg-info +graft .github/workflows graft src graft ci graft tests include .bumpversion.cfg -include .coveragerc include .cookiecutterrc +include .coveragerc include .editorconfig +include .pre-commit-config.yaml +include .readthedocs.yml +include pytest.ini +include tox.ini include AUTHORS.rst include CHANGELOG.rst @@ -21,6 +26,4 @@ include CONTRIBUTING.rst include LICENSE include README.rst -include tox.ini .travis.yml .appveyor.yml .readthedocs.yml .pre-commit-config.yaml - -global-exclude *.py[cod] __pycache__/* *.so *.dylib .coverage .coverage.* +global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/README.rst b/README.rst index a0c44a71..19b24e9f 100644 --- a/README.rst +++ b/README.rst @@ -10,26 +10,16 @@ Overview * - docs - |docs| * - tests - - | |travis| |appveyor| |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 -.. |travis| image:: https://api.travis-ci.com/pytest-dev/pytest-cov.svg?branch=master - :alt: Travis-CI Build Status - :target: https://travis-ci.com/github/pytest-dev/pytest-cov - -.. |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 +.. |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 .. |version| image:: https://img.shields.io/pypi/v/pytest-cov.svg :alt: PyPI Package latest release @@ -37,11 +27,6 @@ Overview .. |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/v2.12.0.svg - :alt: Commits since latest release - :target: https://github.com/pytest-dev/pytest-cov/compare/v2.12.0...master - .. |wheel| image:: https://img.shields.io/pypi/wheel/pytest-cov.svg :alt: PyPI Wheel :target: https://pypi.org/project/pytest-cov @@ -54,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/v5.0.0.svg + :alt: Commits since latest release + :target: https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...master + .. end-badges This plugin produces coverage reports. Compared to just using ``coverage run`` this plugin does some extras: @@ -119,7 +108,7 @@ Would produce a report like:: Documentation ============= - http://pytest-cov.rtfd.org/ + https://pytest-cov.readthedocs.io/en/latest/ diff --git a/ci/appveyor-with-compiler.cmd b/ci/appveyor-with-compiler.cmd deleted file mode 100644 index 289585fc..00000000 --- a/ci/appveyor-with-compiler.cmd +++ /dev/null @@ -1,23 +0,0 @@ -:: Very simple setup: -:: - if WINDOWS_SDK_VERSION is set then activate the SDK. -:: - disable the WDK if it's around. - -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows -SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" -ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% - -IF EXIST %WIN_WDK% ( - REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ - REN %WIN_WDK% 0wdf -) -IF "%WINDOWS_SDK_VERSION%"=="" GOTO main - -SET DISTUTILS_USE_SDK=1 -SET MSSdk=1 -"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% -CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - -:main -ECHO Executing: %COMMAND_TO_RUN% -CALL %COMMAND_TO_RUN% || EXIT 1 diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 61747a15..08d6c90b 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -1,93 +1,83 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import os +import pathlib import subprocess import sys -from collections import defaultdict -from os.path import abspath -from os.path import dirname -from os.path import exists -from os.path import join -base_path = dirname(dirname(abspath(__file__))) +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("Making bootstrap env in: {0} ...".format(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("Re-executing with: {0}".format(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("Project path: {0}".format(base_path)) + print(f'Project path: {base_path}') jinja = jinja2.Environment( - loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), + loader=jinja2.FileSystemLoader(str(templates_path)), trim_blocks=True, lstrip_blocks=True, - keep_trailing_newline=True + keep_trailing_newline=True, ) - tox_environments = [ line.strip() - # WARNING: 'tox' must be installed globally or in the project's virtualenv - for line in subprocess.check_output(['tox', '--listenvs'], universal_newlines=True).splitlines() + # 'tox' need not be installed globally, but must be importable + # by the Python that is running this script. + # This uses sys.executable the same way that the call in + # cookiecutter-pylibrary/hooks/post_gen_project.py + # invokes this bootstrap.py itself. + for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], text=True).splitlines() ] - tox_environments = [line for line in tox_environments if line not in ['clean', 'report', 'docs', 'check']] - - template_vars = defaultdict(list) - template_vars['tox_environments'] = tox_environments - for env in tox_environments: - first, _ = env.split('-', 1) - template_vars['%s_environments' % first].append(env) - - for name in os.listdir(join("ci", "templates")): - with open(join(base_path, name), "w") as fh: - fh.write('# NOTE: this file is auto-generated via ci/bootstrap.py (ci/templates/%s).\n' % name) - fh.write(jinja.get_template(name).render(**template_vars)) - print("Wrote {}".format(name)) - print("DONE.") - - -if __name__ == "__main__": + tox_environments = [line for line in tox_environments if line.startswith('py')] + 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__': args = sys.argv[1:] - if args == ["--no-env"]: + if args == ['--no-env']: main() elif not args: exec_in_env() else: - print("Unexpected arguments {0}".format(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 d7f5177e..b4f18520 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -1,4 +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/.appveyor.yml b/ci/templates/.appveyor.yml deleted file mode 100644 index 92630e15..00000000 --- a/ci/templates/.appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: '{branch}-{build}' -build: off -environment: - matrix: - - TOXENV: check - - TOXENV: '{{ py27_environments|join(",") }}' - - TOXENV: '{{ py35_environments|join(",") }}' - - TOXENV: '{{ py36_environments|join(",") }}' - - TOXENV: '{{ py37_environments|join(",") }}' - - TOXENV: '{{ pypy_environments|join(",") }}' - -init: - - ps: echo $env:TOXENV - - ps: ls C:\Python* -install: - - IF "%TOXENV:~0,5%" == "pypy-" choco install --no-progress python.pypy - - IF "%TOXENV:~0,6%" == "pypy3-" choco install --no-progress pypy3 - - SET PATH=C:\tools\pypy\pypy;%PATH% - - C:\Python37\python -m pip install --progress-bar=off tox -rci/requirements.txt - -test_script: - - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd C:\Python37\python -m tox - -on_failure: - - ps: dir "env:" - - ps: get-content .tox\*\log\* -artifacts: - - path: dist\* - -### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): -# on_finish: -# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/ci/templates/.github/workflows/test.yml b/ci/templates/.github/workflows/test.yml new file mode 100644 index 00000000..39e64078 --- /dev/null +++ b/ci/templates/.github/workflows/test.yml @@ -0,0 +1,124 @@ +name: Tests +on: [push, pull_request, workflow_dispatch] +jobs: +{%- raw %} + examples: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["pypy-3.9", "3.11"] + target: [ + "src-layout", + "adhoc-layout", + ] + include: + # Add new helper variables to existing jobs + - {python-version: "pypy-3.9", tox-python-version: "pypy3"} + - {python-version: "3.11", tox-python-version: "py311"} + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: + examples-v1-${{ hashFiles('**/tox.ini') }} + restore-keys: | + examples-v1- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade wheel + python -m pip install --progress-bar=off tox -rci/requirements.txt + + - name: Examples + run: | + cd examples/${{ matrix.target }} + tox -v -e ${{ matrix.tox-python-version }} +{%- endraw %} + + test: + name: {{ '${{ matrix.name }}' }} + runs-on: {{ '${{ matrix.os }}' }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: 'check' + python: '3.11' + toxpython: 'python3.11' + tox_env: 'check' + os: 'ubuntu-latest' + - name: 'docs' + python: '3.11' + toxpython: 'python3.11' + tox_env: 'docs' + os: 'ubuntu-latest' +{% for env in tox_environments %} +{% set prefix = env.split('-')[0] -%} +{% if prefix.startswith('pypy') %} +{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} +{% set cpython %}pp{{ prefix[4:5] }}{% endset %} +{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} +{% else %} +{% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} +{% set cpython %}cp{{ prefix[2:] }}{% endset %} +{% set toxpython %}python{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} +{% endif %} +{% for os, python_arch in [ + ['ubuntu', 'x64'], + ['windows', 'x64'], + ['macos', 'x64'], +] %} + - name: '{{ env }} ({{ os }})' + python: '{{ python }}' + toxpython: '{{ toxpython }}' + python_arch: '{{ python_arch }}' + tox_env: '{{ env }}' + os: '{{ os }}-latest' +{% endfor %} +{% endfor %} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: {{ '${{ matrix.python }}' }} + architecture: {{ '${{ matrix.python_arch }}' }} + - name: install dependencies + run: | + python -mpip install --progress-bar=off -r ci/requirements.txt + virtualenv --version + pip --version + tox --version + pip list --format=freeze + - name: test + env: + TOXPYTHON: '{{ '${{ matrix.toxpython }}' }}' + run: > + tox -e {{ '${{ matrix.tox_env }}' }} -v +{% raw %} + successful: + # this provides a single status check for branch merge rules + # (use this in `Require status checks to pass before merging` in branch settings) + if: always() + needs: + - test + - examples + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} +{% endraw %} diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml deleted file mode 100644 index 8c7e3b93..00000000 --- a/ci/templates/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -dist: xenial -language: python -cache: false -env: - global: - - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so - - SEGFAULT_SIGNALS=all -stages: - - lint - - examples - - tests -jobs: - fast_finish: true - allow_failures: - - python: '3.8' - include: - - stage: lint - env: TOXENV=check - - env: TOXENV=docs - - - stage: tests -{% for env in tox_environments %} - {%+ if not loop.first %}- {% else %} {% endif -%} - env: TOXENV={{ env }} - {% if env.startswith("pypy-") %} - python: 'pypy' - {% elif env.startswith("pypy3-") %} - python: 'pypy3' - {% else %} - python: '{{ "{0[2]}.{0[3]}".format(env) }}' - {% endif -%} -{% endfor %} - - - stage: examples -{%- for example in ['src', 'adhoc'] %}{{ '' }} - {%+ if not loop.first %}- {% else %} {% endif -%} - python: '3.8' - script: cd $TARGET; tox -v - env: - - TARGET=examples/{{ example }}-layout -{%- endfor %} - -before_install: - - python --version - - uname -a - - lsb_release -a -install: - - python -mpip install --progress-bar=off tox -rci/requirements.txt - - virtualenv --version - - easy_install --version - - pip --version - - tox --version -script: - - tox -v -after_failure: - - more .tox/log/* | cat - - more .tox/*/log/* | cat -notifications: - email: - on_success: never - on_failure: always diff --git a/docs/conf.py b/docs/conf.py index 1da75b8a..f15a262c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,10 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os -import sphinx_py3doc_enhanced_theme - extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', @@ -16,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 = '{}, {}'.format(year, author) -version = release = '2.12.0' +copyright = f'{year}, {author}' +version = release = '5.0.0' pygments_style = 'trac' templates_path = ['.'] @@ -34,20 +29,15 @@ '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/' + 'githuburl': 'https://github.com/pytest-dev/pytest-cov/', } html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' -html_split_index = True -html_sidebars = { - '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], -} -html_short_title = '%s-%s' % (project, version) +html_split_index = False +html_short_title = f'{project}-{version}' napoleon_use_ivar = True napoleon_use_rtype = False diff --git a/docs/config.rst b/docs/config.rst index 0dfb17a0..852c014b 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -23,21 +23,28 @@ For full details refer to the `coverage config file`_ documentation. .. note:: Important Note - This plugin overrides the ``data_file`` and ``parallel`` options of coverage. Unless you also run coverage without - pytest-cov it's pointless to set those options in your ``.coveragerc``. - - If you use the ``--cov=something`` option (with a value) then coverage's ``source`` option will also get overriden. - If you have multiple sources it might be easier to set those in ``.coveragerc`` and always use ``--cov`` (wihout a value) + This plugin overrides the ``parallel`` option of coverage. Unless you also run coverage without pytest-cov it's + pointless to set those options in your ``.coveragerc``. + + If you use the ``--cov=something`` option (with a value) then coverage's ``source`` option will also get overridden. + If you have multiple sources it might be easier to set those in ``.coveragerc`` and always use ``--cov`` (without a value) instead of having a long command line with ``--cov=pkg1 --cov=pkg2 --cov=pkg3 ...``. - - If you use the ``--cov-branch`` option then coverage's ``branch`` option will also get overriden. -If you wish to always add pytest-cov with pytest, you can use ``addopts`` under ``pytest`` or ``tool:pytest`` section. -For example: :: + 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 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 ======= @@ -56,9 +63,9 @@ The complete list of command line options is: --cov=PATH Measure coverage for filesystem path. (multi-allowed) --cov-report=type Type of report to generate: term, term-missing, - annotate, html, xml (multi-allowed). term, term- + annotate, html, xml, json, lcov (multi-allowed). term, term- missing may be followed by ":skip-covered". annotate, - html and xml may be followed by ":DEST" where DEST + html, xml, json and lcov may be followed by ":DEST" where DEST specifies the output location. Use --cov-report= to not generate any output. --cov-config=path Config file for coverage. Default: .coveragerc @@ -66,6 +73,8 @@ The complete list of command line options is: False --no-cov Disable coverage report completely (useful for debuggers). Default: False + --cov-reset Reset cov sources accumulated in options so far. + Mostly useful for scripts and configuration files. --cov-fail-under=MIN Fail if the total coverage is less than MIN. --cov-append Do not delete coverage but append to current. Default: False diff --git a/docs/contexts.rst b/docs/contexts.rst index e5256fcc..2b447463 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -2,10 +2,12 @@ Contexts ======== -Coverage.py 5.0 can record separate coverage data for different contexts during +Coverage.py 5.0 can record separate coverage data for `different contexts`_ during 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/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 are separated with a pipe symbol. You might see contexts like:: @@ -16,3 +18,12 @@ are separated with a pipe symbol. You might see contexts like:: Note that parameterized tests include the values of the parameters in the test id, and each set of parameter values is recorded as a separate test. + +To view contexts when using ``--cov-report=html``, add this to your ``.coveragerc``:: + + [html] + show_contexts = True + +The HTML report will include an annotation on each covered line, indicating the +number of contexts that executed the line. Clicking the annotation displays a +list of the contexts. diff --git a/docs/debuggers.rst b/docs/debuggers.rst index 15c83218..603119bb 100644 --- a/docs/debuggers.rst +++ b/docs/debuggers.rst @@ -7,7 +7,13 @@ Debuggers and PyCharm When it comes to TDD one obviously would like to debug tests. Debuggers in Python use mostly the sys.settrace function to gain access to context. Coverage uses the same technique to get access to the lines executed. Coverage does not play well with other tracers simultaneously running. This manifests itself in behaviour that PyCharm might not hit a -breakpoint no matter what the user does. Since it is common practice to have coverage configuration in the pytest.ini +breakpoint no matter what the user does, or encountering an error like this:: + + PYDEV DEBUGGER WARNING: + sys.settrace() should not be used when the debugger is being used. + This may cause the debugger to stop working correctly. + +Since it is common practice to have coverage configuration in the pytest.ini file and pytest does not support removeopts or similar the `--no-cov` flag can disable coverage completely. At the reporting part a warning message will show on screen:: 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 245dca54..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 `Travis `_ 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/reporting.rst b/docs/reporting.rst index e9e4b06b..d3ae06b2 100644 --- a/docs/reporting.rst +++ b/docs/reporting.rst @@ -3,7 +3,7 @@ Reporting It is possible to generate any combination of the reports for a single test run. -The available reports are terminal (with or without missing line numbers shown), HTML, XML and +The available reports are terminal (with or without missing line numbers shown), HTML, XML, JSON, LCOV and annotated source code. The terminal report without line numbers (default):: @@ -49,19 +49,23 @@ The terminal report with skip covered:: You can use ``skip-covered`` with ``term-missing`` as well. e.g. ``--cov-report term-missing:skip-covered`` -These three report options output to files without showing anything on the terminal:: +These four report options output to files without showing anything on the terminal:: pytest --cov-report html --cov-report xml + --cov-report json + --cov-report lcov --cov-report annotate --cov=myproj tests/ -The output location for each of these reports can be specified. The output location for the XML +The output location for each of these reports can be specified. The output location for the XML, JSON and LCOV report is a file. Where as the output location for the HTML and annotated source code reports are directories:: pytest --cov-report html:cov_html --cov-report xml:cov.xml + --cov-report json:cov.json + --cov-report lcov:cov.info --cov-report annotate:cov_annotate --cov=myproj tests/ @@ -71,4 +75,4 @@ The final report option can also suppress printing to the terminal:: This mode can be especially useful on continuous integration servers, where a coverage file is needed for subsequent processing, but no local report needs to be viewed. For example, -tests run on Travis-CI could produce a .coverage file for use with Coveralls. +tests run on GitHub Actions could produce a .coverage file for use with Coveralls. diff --git a/docs/requirements.txt b/docs/requirements.txt index ccec79fd..4c1e3b7d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,3 @@ -sphinx==3.0.3 -sphinx-py3doc-enhanced-theme==2.4.0 -docutils==0.16 +sphinx +furo -e . 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 18080ac5..36b78a3d 100644 --- a/examples/adhoc-layout/example/__init__.py +++ b/examples/adhoc-layout/example/__init__.py @@ -1,13 +1,13 @@ +import platform -import sys +# test merging multiple tox runs with a platform +# based branch +if platform.python_implementation() == 'PyPy': -PY2 = sys.version_info[0] == 2 - - -if PY2: def add(a, b): - return b + a + 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/adhoc-layout/tox.ini b/examples/adhoc-layout/tox.ini index 6e299f24..e855c315 100644 --- a/examples/adhoc-layout/tox.ini +++ b/examples/adhoc-layout/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py38,report +envlist = pypy3,py39,report [tool:pytest] addopts = @@ -7,7 +7,7 @@ addopts = [testenv] setenv = - py{27,38}: COVERAGE_FILE = .coverage.{envname} + py{py3,39}: COVERAGE_FILE = .coverage.{envname} commands = pytest --cov --cov-config={toxinidir}/.coveragerc {posargs:-vv} deps = pytest @@ -19,7 +19,7 @@ deps = ../.. depends = - report: py27,py38 + report: pypy3,py39 # note that this is necessary to prevent the tests importing the code from your badly laid project changedir = tests diff --git a/examples/src-layout/src/example/__init__.py b/examples/src-layout/src/example/__init__.py index 18080ac5..36b78a3d 100644 --- a/examples/src-layout/src/example/__init__.py +++ b/examples/src-layout/src/example/__init__.py @@ -1,13 +1,13 @@ +import platform -import sys +# test merging multiple tox runs with a platform +# based branch +if platform.python_implementation() == 'PyPy': -PY2 = sys.version_info[0] == 2 - - -if PY2: def add(a, b): - return b + a + 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 6be8e73d..94b72730 100644 --- a/examples/src-layout/tox.ini +++ b/examples/src-layout/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py38,report +envlist = pypy3,py39,report [tool:pytest] testpaths = tests @@ -8,7 +8,7 @@ addopts = [testenv] setenv = - py{27,38}: COVERAGE_FILE = .coverage.{envname} + py{py3,39}: COVERAGE_FILE = .coverage.{envname} commands = pytest --cov {posargs:-vv} deps = pytest @@ -20,7 +20,7 @@ deps = ../.. depends = - report: py27,py38 + report: pypy3,py39 [testenv:report] skip_install = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..61be7748 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = [ + "setuptools>=30.3.0", +] + +[tool.ruff] +extend-exclude = ["static", "ci/templates"] +line-length = 140 +src = ["src", "tests"] +target-version = "py38" + +[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 + "Q", # flake8-quotes + "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.flake8-quotes] +inline-quotes = "single" + +[tool.ruff.lint.isort] +forced-separate = ["conftest"] +force-single-line = true + +[tool.black] +line-length = 140 +target-version = ["py38"] +skip-string-normalization = true 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 d6c36d12..00000000 --- a/setup.cfg +++ /dev/null @@ -1,22 +0,0 @@ -[bdist_wheel] -universal = 1 - -[flake8] -max-line-length = 140 -exclude = .tox,.eggs,ci/templates,build,dist - -[tool:pytest] -testpaths = tests -python_files = test_*.py -addopts = - -ra - --strict - -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 ca77760c..221111e8 100755 --- a/setup.py +++ b/setup.py @@ -1,72 +1,67 @@ #!/usr/bin/env python -# -*- encoding: utf-8 -*- -from __future__ import absolute_import -from __future__ import print_function -import io import re -from distutils.command.build import build -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 from setuptools import setup + +try: + # https://setuptools.pypa.io/en/latest/deprecated/distutils-legacy.html + from setuptools.command.build import build +except ImportError: + from distutils.command.build import build + from setuptools.command.develop import develop from setuptools.command.easy_install import easy_install from setuptools.command.install_lib import install_lib def read(*names, **kwargs): - with io.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): - build.run(self, *args, **kwargs) - path = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.build_lib, basename(path)) + super().run(*args, **kwargs) + path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') + dest = str(Path(self.build_lib) / Path(path).name) self.copy_file(path, dest) class EasyInstallWithPTH(easy_install): def run(self, *args, **kwargs): - easy_install.run(self, *args, **kwargs) - path = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.install_dir, basename(path)) + super().run(*args, **kwargs) + path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') + dest = str(Path(self.install_dir) / Path(path).name) self.copy_file(path, dest) class InstallLibWithPTH(install_lib): def run(self, *args, **kwargs): - install_lib.run(self, *args, **kwargs) - path = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.install_dir, basename(path)) + super().run(*args, **kwargs) + path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') + dest = str(Path(self.install_dir) / Path(path).name) self.copy_file(path, dest) self.outputs = [dest] def get_outputs(self): - return chain(install_lib.get_outputs(self), self.outputs) + return chain(super().get_outputs(), self.outputs) class DevelopWithPTH(develop): def run(self, *args, **kwargs): - develop.run(self, *args, **kwargs) - path = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.install_dir, basename(path)) + super().run(*args, **kwargs) + path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') + dest = str(Path(self.install_dir) / Path(path).name) self.copy_file(path, dest) class GeneratePTH(Command): - user_options = [] + user_options = () def initialize_options(self): pass @@ -75,26 +70,23 @@ 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( - 'import os, sys;' - 'exec(%r)' % sh.read().replace(' ', ' ') - ) + 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='2.12.0', + version='5.0.0', license='MIT', description='Pytest plugin for measuring coverage.', - long_description='%s\n%s' % (read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), + long_description='{}\n{}'.format(read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), author='Marc Schlaich', author_email='marc.schlaich@gmail.com', url='https://github.com/pytest-dev/pytest-cov', packages=find_packages('src'), package_dir={'': 'src'}, - py_modules=[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=[ @@ -107,32 +99,41 @@ def run(self): 'Operating System :: POSIX', 'Operating System :: Unix', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3 :: Only', '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', 'Topic :: Utilities', ], + project_urls={ + 'Documentation': 'https://pytest-cov.readthedocs.io/', + 'Changelog': 'https://pytest-cov.readthedocs.io/en/latest/changelog.html', + 'Issue Tracker': 'https://github.com/pytest-dev/pytest-cov/issues', + }, keywords=[ - 'cover', 'coverage', 'pytest', 'py.test', 'distributed', 'parallel', + 'cover', + 'coverage', + 'pytest', + 'py.test', + 'distributed', + 'parallel', ], + python_requires='>=3.8', install_requires=[ 'pytest>=4.6', - 'coverage[toml]>=5.2.1' + 'coverage[toml]>=5.2.1', ], - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', extras_require={ 'testing': [ 'fields', 'hunter', - 'process-tests==2.0.2', - 'six', + 'process-tests', 'pytest-xdist', 'virtualenv', ] diff --git a/src/pytest-cov.pth b/src/pytest-cov.pth index 91f2b7c7..8ed1a516 100644 --- a/src/pytest-cov.pth +++ b/src/pytest-cov.pth @@ -1 +1 @@ -import os, sys;exec('if \'COV_CORE_SOURCE\' in os.environ:\n try:\n from pytest_cov.embed import init\n init()\n except Exception as exc:\n sys.stderr.write(\n "pytest-cov: Failed to setup subprocess coverage. "\n "Environ: {0!r} "\n "Exception: {1!r}\\n".format(\n dict((k, v) for k, v in os.environ.items() if k.startswith(\'COV_CORE\')),\n exc\n )\n )\n') \ No newline at end of file +import os, sys;exec('if \'COV_CORE_SOURCE\' in os.environ:\n try:\n from pytest_cov.embed import init\n init()\n except Exception as exc:\n sys.stderr.write(\n "pytest-cov: Failed to setup subprocess coverage. "\n "Environ: {0!r} "\n "Exception: {1!r}\\n".format(\n dict((k, v) for k, v in os.environ.items() if k.startswith(\'COV_CORE\')),\n exc\n )\n )\n') diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py index a9f1cb62..82ce08bb 100644 --- a/src/pytest_cov/__init__.py +++ b/src/pytest_cov/__init__.py @@ -1,2 +1,3 @@ """pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE.""" -__version__ = '2.12.0' + +__version__ = '5.0.0' diff --git a/src/pytest_cov/compat.py b/src/pytest_cov/compat.py index 5b4a0bfb..453709d7 100644 --- a/src/pytest_cov/compat.py +++ b/src/pytest_cov/compat.py @@ -1,20 +1,4 @@ -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -import pytest - -StringIO # pyflakes, this is for re-export - - -if hasattr(pytest, 'hookimpl'): - hookwrapper = pytest.hookimpl(hookwrapper=True) -else: - hookwrapper = pytest.mark.hookwrapper - - -class SessionWrapper(object): +class SessionWrapper: def __init__(self, session): self._session = session if hasattr(session, 'testsfailed'): diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index 3adecdba..57c2f423 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 @@ -20,22 +21,6 @@ _active_cov = None -def multiprocessing_start(_): - global _active_cov - cov = init() - if cov: - _active_cov = cov - multiprocessing.util.Finalize(None, cleanup, exitpriority=1000) - - -try: - import multiprocessing.util -except ImportError: - pass -else: - multiprocessing.util.register_after_fork(multiprocessing_start, multiprocessing_start) - - def init(): # Only continue if ancestor process has set everything needed in # the env. @@ -68,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() @@ -86,7 +71,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 @@ -100,12 +85,10 @@ def cleanup(): _active_cov = None _cleanup_in_progress = False if _pending_signal: - pending_singal = _pending_signal + pending_signal = _pending_signal _pending_signal = None - _signal_cleanup_handler(*pending_singal) - + _signal_cleanup_handler(*pending_signal) -multiprocessing_finish = cleanup # in case someone dared to use this internal _previous_handlers = {} _pending_signal = None @@ -126,7 +109,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 084e92ea..0dab6c03 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -1,4 +1,5 @@ """Coverage controllers for use by pytest-cov and nose-cov.""" + import contextlib import copy import functools @@ -6,15 +7,16 @@ import random import socket import sys +from io import StringIO +from pathlib import Path import coverage from coverage.data import CoverageData -from .compat import StringIO from .embed import cleanup -class _NullFile(object): +class _NullFile: @staticmethod def write(v): pass @@ -34,7 +36,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,7 +51,7 @@ def ensure_topdir_wrapper(self, *args, **kwargs): return ensure_topdir_wrapper -class CovController(object): +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): @@ -67,12 +69,12 @@ 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 @contextlib.contextmanager def ensure_topdir(self): - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(self.topdir) yield os.chdir(original_cwd) @@ -94,12 +96,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,7 +119,7 @@ def unset_env(): def get_node_desc(platform, version_info): """Return a description of this node.""" - return 'platform %s, python %s' % (platform, '%s.%s.%s-%s-%s' % version_info[:5]) + return 'platform {}, python {}'.format(platform, '{}.{}.{}-{}-{}'.format(*version_info[:5])) @staticmethod def sep(stream, s, txt): @@ -126,7 +129,7 @@ def sep(stream, s, txt): sep_total = max((70 - 2 - len(txt)), 2) sep_len = sep_total // 2 sep_extra = sep_total % 2 - out = '%s %s %s\n' % (s * sep_len, txt, s * (sep_len + sep_extra)) + out = f'{s * sep_len} {txt} {s * (sep_len + sep_extra)}\n' stream.write(out) @_ensure_topdir @@ -135,24 +138,23 @@ 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, '-', 'coverage: %s' % ''.join(self.node_descs)) + self.sep(stream, '-', f"coverage: {''.join(self.node_descs)}") else: self.sep(stream, '-', 'coverage') for node_desc in sorted(self.node_descs): - self.sep(stream, ' ', '%s' % node_desc) + 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') + 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('%s\n' % node.gateway.id) + stream.write(f'{node.gateway.id}\n') # Produce terminal report if wanted. if any(x in self.cov_report for x in ['term', 'term-missing']): @@ -163,38 +165,57 @@ def summary(self, stream): } 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('Coverage annotated source written to dir %s\n' % annotate_dir) + stream.write(f'Coverage annotated source written to dir {annotate_dir}\n') else: stream.write('Coverage annotated source written next to source\n') # 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('Coverage HTML written to dir %s\n' % (self.cov.config.html_dir if output is None else 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('Coverage XML written to file %s\n' % (self.cov.config.xml_output if output is None else 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'): + 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'): + self.cov.lcov_report(ignore_errors=True, outfile=output) + + # We need to call Coverage.report here, just to get the total + # Coverage.lcov_report doesn't return any total and we need it for --cov-fail-under. + total = self.cov.report(ignore_errors=True, file=_NullFile) + + stream.write(f'Coverage LCOV written to file {self.cov.config.lcov_output if output is None else output}\n') return total @@ -206,15 +227,19 @@ 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=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), # noqa: PTH100 + config_file=self.cov_config, + ) # Erase or load any previous coverage data and start coverage. if not self.cov_append: @@ -246,22 +271,22 @@ 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): - 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=True, + config_file=self.cov_config, + ) 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=True, + 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() @@ -270,11 +295,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.""" @@ -290,15 +317,13 @@ def testnodedown(self, node, error): # 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'] + 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() @@ -340,23 +365,26 @@ 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=True, + config_file=self.cov_config, + ) self.cov.start() self.set_env() @@ -390,13 +418,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 b875f409..49413d46 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -1,7 +1,10 @@ """Coverage plugin for pytest.""" + import argparse import os import warnings +from io import StringIO +from pathlib import Path import coverage import pytest @@ -14,17 +17,34 @@ 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""" + + def validate_report(arg): - file_choices = ['annotate', 'html', 'xml'] + file_choices = ['annotate', 'html', 'xml', 'json', 'lcov'] term_choices = ['term', 'term-missing'] term_modifier_choices = ['skip-covered'] all_choices = term_choices + file_choices - values = arg.split(":", 1) + values = arg.split(':', 1) report_type = values[0] - if report_type not in all_choices + ['']: - msg = 'invalid choice: "{}" (choose from "{}")'.format(arg, all_choices) + if report_type not in [*all_choices, '']: + msg = f'invalid choice: "{arg}" (choose from "{all_choices}")' raise argparse.ArgumentTypeError(msg) + if report_type == 'lcov' and coverage.version_info <= (6, 3): + raise argparse.ArgumentTypeError('LCOV output is only supported with coverage.py >= 6.3') + if len(values) == 1: return report_type, None @@ -33,8 +53,7 @@ def validate_report(arg): return report_type, report_modifier if report_type not in file_choices: - msg = 'output specifier not supported for: "{}" (choose from "{}")'.format(arg, - file_choices) + msg = f'output specifier not supported for: "{arg}" (choose from "{file_choices}")' raise argparse.ArgumentTypeError(msg) return values @@ -42,16 +61,25 @@ def validate_report(arg): def validate_fail_under(num_str): try: - return int(num_str) + value = int(num_str) except ValueError: - return float(num_str) + try: + value = float(num_str) + except ValueError: + raise argparse.ArgumentTypeError('An integer or float value is required.') from None + if value > 100: + raise argparse.ArgumentTypeError( + 'Your desire for over-achievement is admirable but misplaced. ' + 'The maximum value is 100. Perhaps write more integration tests?' + ) + 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": - raise argparse.ArgumentTypeError('--cov-context=test is the only supported value') + if arg != 'test': + raise argparse.ArgumentTypeError('The only supported value is "test".') return arg @@ -64,40 +92,83 @@ 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-report', action=StoreReport, default={}, - metavar='TYPE', type=validate_report, - help='Type of report to generate: term, term-missing, ' - 'annotate, html, xml (multi-allowed). ' - 'term, term-missing may be followed by ":skip-covered". ' - 'annotate, html and xml 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-context', + action='store', + metavar='CONTEXT', + type=validate_context, + help='Dynamic contexts to use. "test" for now.', + ) def _prepare_cov_source(cov_source): @@ -110,7 +181,7 @@ def _prepare_cov_source(cov_source): return None if True in cov_source else [path for path in cov_source if path is not True] -@pytest.mark.tryfirst +@pytest.hookimpl(tryfirst=True) def pytest_load_initial_conftests(early_config, parser, args): options = early_config.known_args_namespace no_cov = options.no_cov_should_warn = False @@ -127,7 +198,7 @@ def pytest_load_initial_conftests(early_config, parser, args): early_config.pluginmanager.register(plugin, '_cov') -class CovPlugin(object): +class CovPlugin: """Use coverage package to produce code coverage reports. Delegates all work to a particular implementation based on whether @@ -146,7 +217,7 @@ 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 @@ -154,9 +225,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) self._disabled = False self.options = options - 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 @@ -182,7 +251,7 @@ def start(self, controller_cls, config=None, nodeid=None): if config is None: # fake config option for engine - class Config(object): + class Config: option = self.options config = Config() @@ -194,11 +263,11 @@ class Config(object): self.options.cov_append, self.options.cov_branch, config, - nodeid + 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 @@ -220,9 +289,7 @@ 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) @@ -230,6 +297,7 @@ def pytest_sessionstart(self, session): if self.options.cov_context == 'test': session.config.pluginmanager.register(TestContextPlugin(self.cov_controller.cov), '_cov_contexts') + @pytest.hookimpl(optionalhook=True) def pytest_configure_node(self, node): """Delegate to our implementation. @@ -237,8 +305,8 @@ def pytest_configure_node(self, node): """ if not self._disabled: self.cov_controller.configure_node(node) - pytest_configure_node.optionalhook = True + @pytest.hookimpl(optionalhook=True) def pytest_testnodedown(self, node, error): """Delegate to our implementation. @@ -246,10 +314,10 @@ def pytest_testnodedown(self, node, error): """ if not self._disabled: self.cov_controller.testnodedown(node, error) - pytest_testnodedown.optionalhook = True def _should_report(self): - return not (self.failed and self.options.no_cov_on_fail) + 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 @@ -257,7 +325,7 @@ def _failed_cov_total(self): # we need to wrap pytest_runtestloop. by the time pytest_sessionfinish # runs, it's too late to set testsfailed - @compat.hookwrapper + @pytest.hookimpl(hookwrapper=True) def pytest_runtestloop(self, session): yield @@ -279,13 +347,12 @@ def pytest_runtestloop(self, session): try: self.cov_total = self.cov_controller.summary(self.cov_report) except CoverageException as exc: - message = 'Failed to generate report: %s\n' % exc - session.config.pluginmanager.getplugin("terminalreporter").write( - 'WARNING: %s\n' % message, red=True, bold=True) - warnings.warn(pytest.PytestWarning(message)) + message = f'Failed to generate report: {exc}\n' + session.config.pluginmanager.getplugin('terminalreporter').write(f'WARNING: {message}\n', red=True, bold=True) + warnings.warn(CovReportWarning(message), stacklevel=1) self.cov_total = 0 assert self.cov_total is not None, 'Test coverage should never be `None`' - if self._failed_cov_total(): + if self._failed_cov_total() and not self.options.collectonly: # make sure we get the EXIT_TESTSFAILED exit code compat_session.testsfailed += 1 @@ -293,8 +360,8 @@ def pytest_terminal_summary(self, terminalreporter): if self._disabled: if self.options.no_cov_should_warn: message = 'Coverage disabled via --no-cov switch!' - terminalreporter.write('WARNING: %s\n' % message, red=True, bold=True) - warnings.warn(pytest.PytestWarning(message)) + terminalreporter.write(f'WARNING: {message}\n', red=True, bold=True) + warnings.warn(CovDisabledWarning(message), stacklevel=1) return if self.cov_controller is None: return @@ -303,20 +370,20 @@ def pytest_terminal_summary(self, terminalreporter): # we shouldn't report, or report generation failed (error raised above) return - terminalreporter.write('\n' + self.cov_report.getvalue() + '\n') + report = self.cov_report.getvalue() + + # Avoid undesirable new lines when output is disabled with "--cov-report=". + if report: + terminalreporter.write('\n' + report + '\n') if self.options.cov_fail_under is not None and self.options.cov_fail_under > 0: failed = self.cov_total < self.options.cov_fail_under markup = {'red': True, 'bold': True} if failed else {'green': True} - message = ( - '{fail}Required test coverage of {required}% {reached}. ' - 'Total coverage: {actual:.2f}%\n' - .format( - required=self.options.cov_fail_under, - actual=self.cov_total, - fail="FAIL " if failed else "", - reached="not reached" if failed else "reached" - ) + message = '{fail}Required test coverage of {required}% {reached}. ' 'Total coverage: {actual:.2f}%\n'.format( + required=self.options.cov_fail_under, + actual=self.cov_total, + fail='FAIL ' if failed else '', + reached='not reached' if failed else 'reached', ) terminalreporter.write(message, **markup) @@ -329,10 +396,9 @@ def pytest_runtest_setup(self, item): def pytest_runtest_teardown(self, item): embed.cleanup() - @compat.hookwrapper + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): - if (item.get_closest_marker('no_cover') - or 'no_cover' in getattr(item, 'fixturenames', ())): + if item.get_closest_marker('no_cover') or 'no_cover' in getattr(item, 'fixturenames', ()): self.cov_controller.pause() yield self.cov_controller.resume() @@ -340,7 +406,7 @@ def pytest_runtest_call(self, item): yield -class TestContextPlugin(object): +class TestContextPlugin: def __init__(self, cov): self.cov = cov @@ -354,15 +420,14 @@ def pytest_runtest_call(self, item): self.switch_context(item, 'run') def switch_context(self, item, when): - context = "{item.nodeid}|{when}".format(item=item, when=when) + context = f'{item.nodeid}|{when}' self.cov.switch_context(context) os.environ['COV_CORE_CONTEXT'] = context @pytest.fixture -def no_cover(): +def no_cover(): # noqa: PT004 """A pytest fixture to disable coverage.""" - pass @pytest.fixture @@ -378,4 +443,4 @@ def cov(request): def pytest_configure(config): - config.addinivalue_line("markers", "no_cover: disable coverage for this test.") + config.addinivalue_line('markers', 'no_cover: disable coverage for this test.') diff --git a/tests/contextful.py b/tests/contextful.py index 3527e499..b1d0804b 100644 --- a/tests/contextful.py +++ b/tests/contextful.py @@ -1,105 +1,113 @@ # A test file for test_pytest_cov.py:test_contexts import unittest +from typing import ClassVar import pytest def test_01(): - assert 1 == 1 # r1 + assert 1 == 1 # r1 def test_02(): - assert 2 == 2 # r2 + assert 2 == 2 # r2 class OldStyleTests(unittest.TestCase): - items = [] + items: ClassVar = [] @classmethod def setUpClass(cls): - cls.items.append("hello") # s3 + cls.items.append('hello') # s3 @classmethod def tearDownClass(cls): - cls.items.pop() # t4 + cls.items.pop() # t4 def setUp(self): - self.number = 1 # r3 r4 + self.number = 1 # r3 r4 def tearDown(self): - self.number = None # r3 r4 + self.number = None # r3 r4 def test_03(self): - assert self.number == 1 # r3 - assert self.items[0] == "hello" # r3 + assert self.number == 1 # r3 + assert self.items[0] == 'hello' # r3 def test_04(self): - assert self.number == 1 # r4 - assert self.items[0] == "hello" # r4 + assert self.number == 1 # r4 + assert self.items[0] == 'hello' # r4 @pytest.fixture def some_data(): - return [1, 2, 3] # s5 s6 + return [1, 2, 3] # s5 s6 def test_05(some_data): - assert len(some_data) == 3 # r5 + assert len(some_data) == 3 # r5 @pytest.fixture def more_data(some_data): - return [2*x for x in some_data] # s6 + return [2 * x for x in some_data] # s6 def test_06(some_data, more_data): - assert len(some_data) == len(more_data) # r6 + assert len(some_data) == len(more_data) # r6 @pytest.fixture(scope='session') def expensive_data(): - return list(range(10)) # s7 + return list(range(10)) # s7 def test_07(expensive_data): - assert len(expensive_data) == 10 # r7 + assert len(expensive_data) == 10 # r7 def test_08(expensive_data): - assert len(expensive_data) == 10 # r8 + assert len(expensive_data) == 10 # r8 @pytest.fixture(params=[1, 2, 3]) def parametrized_number(request): - return request.param # s9-1 s9-2 s9-3 + return request.param # s9-1 s9-2 s9-3 def test_09(parametrized_number): - assert parametrized_number > 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 0d1b5a23..bd9df38e 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 @@ -16,20 +18,14 @@ from process_tests import TestProcess as _TestProcess from process_tests import dump_on_error from process_tests import wait_for_strings -from six import exec_ 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): @@ -41,24 +37,24 @@ 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]) @@ -67,9 +63,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 @@ -87,9 +83,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 @@ -111,9 +107,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 @@ -134,29 +130,32 @@ 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%' DEST_DIR = 'cov_dest' -REPORT_NAME = 'cov.xml' +XML_REPORT_NAME = 'cov.xml' +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"'))], + ids=['nodist', 'xdist'], +) @pytest.fixture(scope='session', autouse=True) @@ -176,292 +175,302 @@ 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, code2=SCRIPT2, conf=request.param[0], - fullconf='[run]\n%s\n' % request.param[0], - prefixedfullconf='[coverage:run]\n%s\n' % request.param[0], + fullconf=f'[run]\n{request.param[0]}\n', + prefixedfullconf=f'[coverage:run]\n{request.param[0]}\n', args=request.param[1].split(), result=request.param[2], result2=request.param[3], ) -def test_central(testdir, prop): +def test_central(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - '--cov=%s' % 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 * -*', - 'test_central* %s *' % 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', - '--cov=%s' % 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', - '--cov=%s' % 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', - '--cov=%s' % 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', - '--cov=%s' % 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', - '--cov=%s' % 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() + 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() 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', - '--cov=%s' % 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', - '--cov=%s' % script.dirpath(), - '--cov-report=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 ' + REPORT_NAME, - '*10 passed*', - ]) - assert testdir.tmpdir.join(REPORT_NAME).check() + 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 + + +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.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)') +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.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)') +def test_lcov_not_supported(testdir): + script = testdir.makepyfile('a = 1') + result = testdir.runpytest( + '-v', + f'--cov={script.dirpath()}', + '--cov-report=lcov', + script, + ) + result.stderr.fnmatch_lines( + [ + '*argument --cov-report: LCOV output is only supported with coverage.py >= 6.3', + ] + ) + assert result.ret != 0 + + def test_term_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - '--cov=%s' % 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([ - '*argument --cov-report: output specifier not supported for: "term:%s"*' % 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', - '--cov=%s' % 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', - '--cov=%s' % 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 + ) + + assert result.ret == 0 def test_cov_min_50(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - '--cov=%s' % 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', - '--cov=%s' % 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) - result = testdir.runpytest('-v', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - '--cov-fail-under=88.89', - script) + 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.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%']) def test_cov_min_no_report(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - '--cov=%s' % 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(testdir, prop): +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 * -*', - 'test_central_nonspecific* %s *' % 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) @@ -471,46 +480,47 @@ def test_central_nonspecific(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', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret != 0 -def test_central_coveragerc(testdir, prop): +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 * -*', - 'test_central_coveragerc* %s *' % prop.result, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'test_central_coveragerc* {prop.result} *', + '*10 passed*', + ] + ) assert result.ret == 0 @xdist_params -def test_central_with_path_aliasing(testdir, monkeypatch, opts, prop): +def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): mod1 = testdir.mkdir('src').join('mod.py') 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(""" +""" + ) + testdir.tmpdir.join('setup.cfg').write( + f""" [coverage:paths] source = src @@ -518,31 +528,34 @@ def test_central_with_path_aliasing(testdir, monkeypatch, opts, prop): [coverage:run] source = mod parallel = true -%s -""" % prop.conf) +{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 * -*', - 'src[\\/]mod* %s *' % 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(testdir, monkeypatch, opts): - testdir.makepyfile(mod=''' +def test_borken_cwd(pytester, testdir, monkeypatch, opts): + testdir.makepyfile( + mod=""" def foobar(a, b): return a + b -''') +""" + ) - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import os import tempfile import pytest @@ -560,22 +573,22 @@ 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 -def test_subprocess_with_path_aliasing(testdir, monkeypatch): +def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): src = testdir.mkdir('src') src.join('parent_script.py').write(SCRIPT_PARENT) src.join('child_script.py').write(SCRIPT_CHILD) @@ -584,7 +597,8 @@ def test_subprocess_with_path_aliasing(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 @@ -594,125 +608,123 @@ def test_subprocess_with_path_aliasing(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 * -*', - 'src[\\/]child_script* %s*' % CHILD_SCRIPT_RESULT, - 'src[\\/]parent_script* %s*' % 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(testdir, prop): +def test_show_missing_coveragerc(pytester, testdir, prop): script = testdir.makepyfile(prop.code) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + f""" [run] source = . -%s +{prop.conf} [report] show_missing = true -""" % prop.conf) - - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term', - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Name * Stmts * Miss * Cover * Missing', - 'test_show_missing_coveragerc* %s * 11*' % prop.result, - '*10 passed*', - ]) +""" + ) + + 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*', + ] + ) 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', - '--cov=%s' % 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*']) -def test_no_cov(testdir, monkeypatch): +def test_no_cov(pytester, testdir, monkeypatch): script = testdir.makepyfile(SCRIPT) - testdir.makeini(""" + testdir.makeini( + """ [pytest] addopts=--no-cov - """) - result = testdir.runpytest('-vvv', - '--cov=%s' % 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', - '--cov=%s' % 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(""" - if foo == %s: +""" + + '\n'.join( + f""" + if foo == {i}: assert True -""" % i for i in range(1000))) - - result = testdir.runpytest('-v', - '--cov=%s' % 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 @@ -721,259 +733,266 @@ def test_foo(foo): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -def test_dist_collocated(testdir, prop): +def test_dist_collocated(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - '--cov=%s' % 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 * -*', - 'test_dist_collocated* %s *' % 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 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -def test_dist_not_collocated(testdir, prop): +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(''' + testdir.tmpdir.join('.coveragerc').write( + f""" [run] -%s +{prop.conf} [paths] source = . dir1 - dir2''' % prop.conf) - - result = testdir.runpytest('-v', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - '--dist=load', - '--tx=popen//chdir=%s' % dir1, - '--tx=popen//chdir=%s' % dir2, - '--rsyncdir=%s' % script.basename, - '--rsyncdir=.coveragerc', - max_worker_restart_0, '-s', - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_dist_not_collocated* %s *' % 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 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -def test_dist_not_collocated_coveragerc_source(testdir, prop): +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(''' + testdir.tmpdir.join('.coveragerc').write( + f""" [run] -%s -source = %s +{prop.conf} +source = {script.dirpath()} [paths] source = . dir1 - dir2''' % (prop.conf, script.dirpath())) - - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term-missing', - '--dist=load', - '--tx=popen//chdir=%s' % dir1, - '--tx=popen//chdir=%s' % dir2, - '--rsyncdir=%s' % script.basename, - '--rsyncdir=.coveragerc', - max_worker_restart_0, '-s', - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_dist_not_collocated* %s *' % 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', - '--cov=%s' % 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 * -*', - 'child_script* %s*' % CHILD_SCRIPT_RESULT, - 'parent_script* %s*' % 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', - '--cov=%s' % scripts.dirpath(), - '--cov-config=coveragerc', - '--cov-report=term-missing', - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - '*child_script* %s*' % CHILD_SCRIPT_RESULT, - '*parent_script* 100%*', - ]) +""", + ) + + 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%*', + ] + ) assert result.ret == 0 -def test_central_subprocess_change_cwd_with_pythonpath(testdir, monkeypatch): +def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkeypatch): stuff = testdir.mkdir('stuff') 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 * -*', - '*child_script* %s*' % 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', - '--cov=%s' % 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', - '--cov=%s' % scripts.dirpath(), - '--cov-report=term-missing', - '--dist=load', - '--tx=2*popen', - max_worker_restart_0, - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'child_script* %s*' % CHILD_SCRIPT_RESULT, - 'parent_script* %s*' % 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(testdir, tmpdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, - child_script=SCRIPT_CHILD) +def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): + 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(''' + testdir.tmpdir.join('.coveragerc').write( + f""" [paths] source = - %s + {scripts.dirpath()} */dir1 */dir2 -''' % scripts.dirpath()) - result = testdir.runpytest('-v', - '--cov=%s' % scripts.dirpath(), - '--dist=load', - '--tx=popen//chdir=%s' % dir1, - '--tx=popen//chdir=%s' % dir2, - '--rsyncdir=%s' % child_script, - '--rsyncdir=%s' % parent_script, - '--rsyncdir=.coveragerc', - max_worker_restart_0, - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'child_script* %s*' % CHILD_SCRIPT_RESULT, - 'parent_script* %s*' % 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.stdout.fnmatch_lines([ - '*10 passed*' - ]) - result.stderr.fnmatch_lines([ - 'Coverage.py warning: No data was collected.*' - ]) - result.stdout.fnmatch_lines([ - '*Failed to generate report: No data to report.', - ]) + """ + ) + 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.', + ] + ) assert result.ret == 0 matching_lines = [line for line in result.outlines if '%' in line] @@ -982,296 +1001,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', +) 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', - 'py==%s' % py.__version__, - 'pytest==%s' % pytest.__version__, - 'pytest_xdist==%s' % 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', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - '--dist=load', - '--tx=popen//python=%s' % exe, - max_worker_restart_0, - 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', - '--cov=%s' % 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.stdout.fnmatch_lines([ - '*1 passed*' - ]) - assert result.ret == 0 - - -@pytest.mark.skipif("sys.version_info[0] < 3", reason="no context manager api on Python 2") -@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") -@pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason="often deadlocks on PyPy") -@pytest.mark.skipif('sys.version_info[:2] >= (3, 8)', reason="deadlocks on Python 3.8+, see: https://bugs.python.org/issue38227") -def test_multiprocessing_pool(testdir): - pytest.importorskip('multiprocessing.util') - - script = testdir.makepyfile(''' -import multiprocessing - -def target_fn(a): - %sse: # pragma: nocover - return None - -def test_run_target(): - from pytest_cov.embed import cleanup_on_sigterm - cleanup_on_sigterm() - - for i in range(33): - with multiprocessing.Pool(3) as p: - p.map(target_fn, [i * 3 + j for j in range(3)]) - p.join() -''' % ''.join('''if a == %r: - return a - el''' % i for i in range(99))) - - result = testdir.runpytest('-v', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - script) - - assert "Doesn't seem to be a coverage.py data file" not in result.stdout.str() - assert "Doesn't seem to be a coverage.py data file" not in result.stderr.str() - assert not testdir.tmpdir.listdir(".coverage.*") - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_multiprocessing_pool* 100%*', - '*1 passed*' - ]) - assert result.ret == 0 - - -@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") -@pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason="often deadlocks on PyPy") -@pytest.mark.skipif('sys.version_info[:2] >= (3, 8)', reason="deadlocks on Python 3.8, see: https://bugs.python.org/issue38227") -def test_multiprocessing_pool_terminate(testdir): - pytest.importorskip('multiprocessing.util') - - script = testdir.makepyfile(''' -import multiprocessing - -def target_fn(a): - %sse: # pragma: nocover - return None - -def test_run_target(): - from pytest_cov.embed import cleanup_on_sigterm - cleanup_on_sigterm() - - for i in range(33): - p = multiprocessing.Pool(3) - try: - p.map(target_fn, [i * 3 + j for j in range(3)]) - finally: - p.terminate() - p.join() -''' % ''.join('''if a == %r: - return a - el''' % i for i in range(99))) - - result = testdir.runpytest('-v', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - script) - - assert "Doesn't seem to be a coverage.py data file" not in result.stdout.str() - assert "Doesn't seem to be a coverage.py data file" not in result.stderr.str() - assert not testdir.tmpdir.listdir(".coverage.*") - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_multiprocessing_pool* 100%*', - '*1 passed*' - ]) - assert result.ret == 0 - - -@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") -@pytest.mark.skipif('sys.version_info[0] > 2 and platform.python_implementation() == "PyPy"', reason="broken on PyPy3") -def test_multiprocessing_pool_close(testdir): - pytest.importorskip('multiprocessing.util') - - script = testdir.makepyfile(''' -import multiprocessing - -def target_fn(a): - %sse: # pragma: nocover - return None - -def test_run_target(): - for i in range(33): - p = multiprocessing.Pool(3) - try: - p.map(target_fn, [i * 3 + j for j in range(3)]) - finally: - p.close() - p.join() -''' % ''.join('''if a == %r: - return a - el''' % i for i in range(99))) - - result = testdir.runpytest('-v', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - script) - assert "Doesn't seem to be a coverage.py data file" not in result.stdout.str() - assert "Doesn't seem to be a coverage.py data file" not in result.stderr.str() - assert not testdir.tmpdir.listdir(".coverage.*") - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_multiprocessing_pool* 100%*', - '*1 passed*' - ]) - assert result.ret == 0 - - -@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") -def test_multiprocessing_process(testdir): - pytest.importorskip('multiprocessing.util') + result = testdir.runpytest('-v', script) - script = testdir.makepyfile(''' -import multiprocessing - -def target_fn(): - a = True - return a - -def test_run_target(): - p = multiprocessing.Process(target=target_fn) - p.start() - p.join() -''') - - result = testdir.runpytest('-v', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_multiprocessing_process* 8 * 100%*', - '*1 passed*' - ]) - assert result.ret == 0 - - -@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") -def test_multiprocessing_process_no_source(testdir): - pytest.importorskip('multiprocessing.util') - - script = testdir.makepyfile(''' -import multiprocessing - -def target_fn(): - a = True - return a - -def test_run_target(): - p = multiprocessing.Process(target=target_fn) - p.start() - p.join() -''') - - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term-missing', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_multiprocessing_process* 8 * 100%*', - '*1 passed*' - ]) - assert result.ret == 0 - - -@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") -def test_multiprocessing_process_with_terminate(testdir): - pytest.importorskip('multiprocessing.util') - - script = testdir.makepyfile(''' -import multiprocessing -import time -from pytest_cov.embed import cleanup_on_sigterm -cleanup_on_sigterm() - -event = multiprocessing.Event() - -def target_fn(): - a = True - event.set() - time.sleep(5) - -def test_run_target(): - p = multiprocessing.Process(target=target_fn) - p.start() - time.sleep(0.5) - event.wait(1) - p.terminate() - p.join() -''') - - result = testdir.runpytest('-v', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_multiprocessing_process* 16 * 100%*', - '*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') def test_cleanup_on_sigterm(testdir): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + ''' import os, signal, subprocess, sys, time def cleanup(num, frame): @@ -1299,30 +1086,28 @@ def test_run(): time.sleep(10) except BaseException as exc: print("captured %r" % exc) -''') +''' + ) - result = testdir.runpytest('-vv', - '--cov=%s' % 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'), -]) -def test_cleanup_on_sigterm_sig_break(testdir, setup): +@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(): @@ -1339,35 +1124,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', - '--cov=%s' % 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* %s' % 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.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(testdir, setup): - script = testdir.makepyfile(''' +@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( + """ import os, signal, subprocess, sys, time def test_run(): @@ -1381,31 +1168,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', - '--cov=%s' % script.dirpath(), - '--cov-report=term-missing', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_cleanup_on_sigterm* %s' % 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.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(): @@ -1426,25 +1211,20 @@ def test_run(): time.sleep(10) except BaseException as exc: print("captured %r" % exc) -''') - - result = testdir.runpytest('-vv', - '--assert=plain', - '--cov=%s' % 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.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(): @@ -1456,8 +1236,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) @@ -1469,42 +1248,35 @@ def test_run(): time.sleep(10) except BaseException as exc: print("captured %r" % exc) - ''') - - result = testdir.runpytest('-vv', - '--assert=plain', - '--cov=%s' % 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%*' @@ -1513,10 +1285,7 @@ def test_cover_conftest(testdir): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) script = testdir.makepyfile(BASIC_TEST) - result = testdir.runpytest('-v', - '--cov=%s' % 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]) @@ -1535,16 +1304,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', - '--cov=%s' % 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"') @@ -1552,20 +1314,17 @@ def test_cover_conftest_dist(testdir): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) script = testdir.makepyfile(BASIC_TEST) - result = testdir.runpytest('-v', - '--cov=%s' % 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 @@ -1575,18 +1334,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', - '--cov=%s' % 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 @@ -1594,24 +1352,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', - '--cov=%s' % 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 @@ -1620,7 +1382,7 @@ def test_basic(): x = True assert x -''' +""" EXCLUDED_RESULT = '4 * 100%*' @@ -1628,38 +1390,37 @@ def test_basic(): def test_coveragerc(testdir): testdir.makefile('', coveragerc=COVERAGERC) script = testdir.makepyfile(EXCLUDED_TEST) - result = testdir.runpytest('-v', - '--cov-config=coveragerc', - '--cov=%s' % 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(['test_coveragerc* %s' % EXCLUDED_RESULT]) + 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', - '--cov=%s' % 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( - ['test_coveragerc_dist* %s' % 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" @@ -1667,21 +1428,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']) -def test_skip_covered_cli(testdir, report_option): +@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', - '--cov=%s' % script.dirpath(), - '--cov-report=%s' % 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]) @@ -1689,79 +1445,58 @@ def test_skip_covered_cli(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', - '--cov=%s' % 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', - '--cov=%s' % 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('sys.platform == "win32"') def test_dist_boxed(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--assert=plain', - '--cov=%s' % script.dirpath(), - '--boxed', - script) + result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--boxed', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_dist_boxed* %s*' % 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 * -*', - 'test_dist_bare_cov* %s*' % 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 @@ -1769,6 +1504,7 @@ def test_not_started_plugin_does_not_fail(testdir): class ns: cov_source = [True] cov_report = '' + plugin = pytest_cov.plugin.CovPlugin(ns, None, start=False) plugin.pytest_runtestloop(None) plugin.pytest_terminal_summary(None) @@ -1777,28 +1513,21 @@ class ns: def test_default_output_setting(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - '--cov=%s' % 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', - '--cov=%s' % 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 @@ -1808,8 +1537,7 @@ def test_coverage_file(testdir): data_file_name = 'covdata' os.environ['COVERAGE_FILE'] = data_file_name try: - result = testdir.runpytest('-v', '--cov=%s' % 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() @@ -1819,14 +1547,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', - '--cov=%s' % 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*'))) @@ -1834,33 +1563,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', - '--cov=%s' % 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() @@ -1870,80 +1597,76 @@ 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', - '--cov=%s' % script.dirpath(), - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) @xdist_params -def test_append_coverage(testdir, opts, prop): +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', - '--cov=%s' % script.dirpath(), - script, - *opts.split()+prop.args) - result.stdout.fnmatch_lines([ - 'test_1* %s*' % 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', - '--cov=%s' % script2.dirpath(), - script2, - *opts.split()+prop.args) - result.stdout.fnmatch_lines([ - 'test_1* %s*' % prop.result, - 'test_2* %s*' % 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_do_not_append_coverage(testdir, opts, prop): +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', - '--cov=%s' % script.dirpath(), - script, - *opts.split()+prop.args) - result.stdout.fnmatch_lines([ - 'test_1* %s*' % 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=%s' % script2.dirpath(), - script2, - *opts.split()+prop.args) - result.stdout.fnmatch_lines([ - 'test_1* 0%', - 'test_2* %s*' % 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', - '--cov=%s' % 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 * -*', - 'child_script* %s*' % CHILD_SCRIPT_RESULT, - 'parent_script* %s*' % 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 @@ -1955,7 +1678,7 @@ class SpecificError(Exception): pass def bad_init(): - raise SpecificError() + raise SpecificError buff = StringIO() @@ -1964,49 +1687,45 @@ def bad_init(): monkeypatch.setattr(embed, 'init', bad_init) monkeypatch.setattr(sys, 'stderr', buff) monkeypatch.setitem(os.environ, 'COV_CORE_SOURCE', 'foobar') - exec_(payload) - assert buff.getvalue() == '''pytest-cov: Failed to setup subprocess coverage. Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError() -''' + exec(payload) + expected = "pytest-cov: Failed to setup subprocess coverage. " "Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" + assert buff.getvalue() == expected def test_double_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--assert=plain', - '--cov', '--cov=%s' % script.dirpath(), - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_double_cov* %s*' % 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 * -*', - 'test_double_cov2* %s*' % 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) + + 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*']) + + @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 @@ -2056,55 +1775,56 @@ def find_labels(text, pattern): } -@pytest.mark.skipif("coverage.version_info < (5, 0)") +@pytest.mark.skipif('coverage.version_info < (5, 0)') +@pytest.mark.skipif('coverage.version_info > (6, 4)') @xdist_params -def test_contexts(testdir, opts): - with open(os.path.join(os.path.dirname(__file__), "contextful.py")) as f: +def test_contexts(pytester, testdir, opts): + 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', - '--cov=%s' % script.dirpath(), - '--cov-context=test', - script, - *opts.split() - ) + 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.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, "Wrong lines for context {!r}".format(context) + 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', - '--cov=%s' % 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_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 6be986e9..4cbe6c8a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,26 +1,37 @@ [testenv:bootstrap] deps = jinja2 - matrix tox skip_install = true 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{27,35,36,37,py,py3}-pytest46-xdist127-coverage{55} - py{36,37,38,py3}-pytest{46,54}-xdist133-coverage{55} - py{36,37,38,39,py3}-pytest{62}-xdist202-coverage{55} - docs + clean, + check, + docs, + {py38,py39,py310,py311,py312,pypy38,pypy39,pypy310}-pytest{81}-xdist{350}-coverage{74}, + 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} + {bootstrap,clean,check,report,docs}: {env:TOXPYTHON:python3} extras = testing setenv = + PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes # Use env vars for (optional) pinning of deps. @@ -29,7 +40,14 @@ setenv = 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.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.3 + pytest74: _DEP_PYTEST=pytest==7.4.4 + pytest80: _DEP_PYTEST=pytest==8.0.2 + pytest81: _DEP_PYTEST=pytest==8.1.1 xdist127: _DEP_PYTESTXDIST=pytest-xdist==1.27.0 xdist129: _DEP_PYTESTXDIST=pytest-xdist==1.29.0 @@ -40,6 +58,11 @@ setenv = 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 + xdist340: _DEP_PYTESTXDIST=pytest-xdist==3.4.0 + xdist350: _DEP_PYTESTXDIST=pytest-xdist==3.5.0 xdistdev: _DEP_PYTESTXDIST=git+https://github.com/pytest-dev/pytest-xdist.git#egg=pytest-xdist coverage45: _DEP_COVERAGE=coverage==4.5.4 @@ -49,6 +72,15 @@ setenv = 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.7 + coverage73: _DEP_COVERAGE=coverage==7.3.4 + coverage74: _DEP_COVERAGE=coverage==7.4.4 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv = @@ -59,38 +91,40 @@ deps = {env:_DEP_COVERAGE:coverage} pip_pre = true commands = - pytest {posargs:-vv} + {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 = - docutils - check-manifest - flake8 - readme-renderer - pygments - isort + 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