pax_global_header00006660000000000000000000000064151575741010014520gustar00rootroot0000000000000052 comment=66c8a526b1246b5eb8fb1bc218878131bc628622 pytest-cov-7.1.0/000077500000000000000000000000001515757410100136425ustar00rootroot00000000000000pytest-cov-7.1.0/.bumpversion.cfg000066400000000000000000000013551515757410100167560ustar00rootroot00000000000000[bumpversion] current_version = 7.1.0 commit = True tag = True [bumpversion:file:pyproject.toml] search = version = "{current_version}" replace = version = "{new_version}" [bumpversion:file (badge):README.rst] search = /v{current_version}.svg replace = /v{new_version}.svg [bumpversion:file (link):README.rst] search = /v{current_version}...master replace = /v{new_version}...master [bumpversion:file:docs/conf.py] search = version = release = '{current_version}' 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} pytest-cov-7.1.0/.cookiecutterrc000066400000000000000000000031731515757410100166740ustar00rootroot00000000000000# Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) default_context: allow_tests_inside_package: 'no' c_extension_function: '-' c_extension_module: '-' 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' command_line_interface_bin_name: '-' coveralls: 'no' distribution_name: pytest-cov email: contact@ionelmc.ro formatter_quote_style: single full_name: Ionel Cristian Mărieș github_actions: 'yes' github_actions_osx: 'yes' github_actions_windows: 'yes' license: MIT license package_name: pytest_cov 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: '2023-05-24' repo_hosting: github.com repo_hosting_domain: github.com repo_main_branch: master repo_name: pytest-cov repo_username: pytest-dev scrutinizer: 'no' setup_py_uses_setuptools_scm: 'no' sphinx_docs: 'yes' sphinx_docs_hosting: https://pytest-cov.readthedocs.io/ sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme test_matrix_separate_coverage: 'no' version: 7.1.0 version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' year_to: '2024' pytest-cov-7.1.0/.editorconfig000066400000000000000000000005771515757410100163300ustar00rootroot00000000000000# see https://editorconfig.org/ root = true [*] # Use Unix-style newlines for most files (except Windows files, see below). end_of_line = lf trim_trailing_whitespace = true indent_style = space insert_final_newline = true indent_size = 4 charset = utf-8 [*.py] max_line_length = 140 [*.{bat,cmd,ps1}] end_of_line = crlf [*.{yml,yaml}] indent_size = 2 [*.tsv] indent_style = tab pytest-cov-7.1.0/.github/000077500000000000000000000000001515757410100152025ustar00rootroot00000000000000pytest-cov-7.1.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001515757410100173655ustar00rootroot00000000000000pytest-cov-7.1.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000012071515757410100220570ustar00rootroot00000000000000--- name: 🐞 Bug report about: There a problem with how pytest-cov or coverage works --- # Summary ## Expected vs actual result # Reproducer ## Versions Output of relevant packages `pip list`, `python --version`, `pytest --version` etc. Make sure you include complete output of `tox` if you use it (it will show versions of various things). ## Config Include your `tox.ini`, `pytest.ini`, `.coveragerc`, `setup.cfg` or any relevant configuration. ## Code Link to your repository, gist, pastebin or just paste raw code that illustrates the issue. If you paste raw code make sure you quote it, eg: ```python def foobar(): pass ``` pytest-cov-7.1.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000006431515757410100231150ustar00rootroot00000000000000--- name: ✈ Feature request about: Proposal for a new feature in pytest-cov --- Before proposing please consider: * the maintenance cost of the feature * implementing it externally (like a shell/python script, pytest plugin or something else) # Summary These questions should be answered: * why is the feature needed? * what problem does it solve? * how it is better compared to past solutions to the problem? pytest-cov-7.1.0/.github/ISSUE_TEMPLATE/support_request.md000066400000000000000000000016521515757410100231770ustar00rootroot00000000000000--- name: 🤔 Support request about: Request help with setting up pytest-cov in your project --- Please go over all the sections and search https://pytest-cov.readthedocs.io/en/latest/ or https://coverage.readthedocs.io/en/latest/ before opening the issue. # Summary ## Expected vs actual result # Reproducer ## Versions Output of relevant packages `pip list`, `python --version`, `pytest --version` etc. Make sure you include complete output of `tox` if you use it (it will show versions of various things). ## Config Include your `tox.ini`, `pytest.ini`, `.coveragerc`, `setup.cfg` or any relevant configuration. ## Code Link to your repository, gist, pastebin or just paste raw code that illustrates the issue. If you paste raw code make sure you quote it, eg: ```python def foobar(): pass ``` # What has been tried to solve the problem You should outline the things you tried to solve the problem but didn't work. pytest-cov-7.1.0/.github/dependabot.yml000066400000000000000000000011031515757410100200250ustar00rootroot00000000000000# 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 pytest-cov-7.1.0/.github/workflows/000077500000000000000000000000001515757410100172375ustar00rootroot00000000000000pytest-cov-7.1.0/.github/workflows/test.yml000066400000000000000000000224021515757410100207410ustar00rootroot00000000000000name: Tests on: [push, pull_request, workflow_dispatch] jobs: examples: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["pypy-3.11", "3.11"] target: [ "src-layout", "adhoc-layout", ] include: # Add new helper variables to existing jobs - {python-version: "pypy-3.11", tox-python-version: "pypy11"} - {python-version: "3.11", tox-python-version: "py311"} steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Cache uses: actions/cache@v5 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: 'py310-pytest90-xdist38-coverage713 (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-pytest90-xdist38-coverage713' os: 'ubuntu-latest' - name: 'py310-pytest90-xdist38-coverage713 (windows)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-pytest90-xdist38-coverage713' os: 'windows-latest' - name: 'py310-pytest90-xdist38-coverage713 (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-pytest90-xdist38-coverage713' os: 'macos-latest' - name: 'py311-pytest90-xdist38-coverage713 (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-pytest90-xdist38-coverage713' os: 'ubuntu-latest' - name: 'py311-pytest90-xdist38-coverage713 (windows)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-pytest90-xdist38-coverage713' os: 'windows-latest' - name: 'py311-pytest90-xdist38-coverage713 (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-pytest90-xdist38-coverage713' os: 'macos-latest' - name: 'py312-pytest90-xdist38-coverage713 (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-pytest90-xdist38-coverage713' os: 'ubuntu-latest' - name: 'py312-pytest90-xdist38-coverage713 (windows)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-pytest90-xdist38-coverage713' os: 'windows-latest' - name: 'py312-pytest90-xdist38-coverage713 (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-pytest90-xdist38-coverage713' os: 'macos-latest' - name: 'py313-pytest90-xdist38-coverage713 (ubuntu)' python: '3.13' toxpython: 'python3.13' python_arch: 'x64' tox_env: 'py313-pytest90-xdist38-coverage713' os: 'ubuntu-latest' - name: 'py313-pytest90-xdist38-coverage713 (windows)' python: '3.13' toxpython: 'python3.13' python_arch: 'x64' tox_env: 'py313-pytest90-xdist38-coverage713' os: 'windows-latest' - name: 'py313-pytest90-xdist38-coverage713 (macos)' python: '3.13' toxpython: 'python3.13' python_arch: 'arm64' tox_env: 'py313-pytest90-xdist38-coverage713' os: 'macos-latest' - name: 'py314-pytest90-xdist38-coverage713 (ubuntu)' python: '3.14' toxpython: 'python3.14' python_arch: 'x64' tox_env: 'py314-pytest90-xdist38-coverage713' os: 'ubuntu-latest' - name: 'py314-pytest90-xdist38-coverage713 (windows)' python: '3.14' toxpython: 'python3.14' python_arch: 'x64' tox_env: 'py314-pytest90-xdist38-coverage713' os: 'windows-latest' - name: 'py314-pytest90-xdist38-coverage713 (macos)' python: '3.14' toxpython: 'python3.14' python_arch: 'arm64' tox_env: 'py314-pytest90-xdist38-coverage713' os: 'macos-latest' - name: 'pypy310-pytest90-xdist38-coverage713 (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-pytest90-xdist38-coverage713' os: 'ubuntu-latest' - name: 'pypy310-pytest90-xdist38-coverage713 (windows)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-pytest90-xdist38-coverage713' os: 'windows-latest' - name: 'pypy310-pytest90-xdist38-coverage713 (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-pytest90-xdist38-coverage713' os: 'macos-latest' - name: 'pypy311-pytest90-xdist38-coverage713 (ubuntu)' python: 'pypy-3.11' toxpython: 'pypy3.11' python_arch: 'x64' tox_env: 'pypy311-pytest90-xdist38-coverage713' os: 'ubuntu-latest' - name: 'pypy311-pytest90-xdist38-coverage713 (windows)' python: 'pypy-3.11' toxpython: 'pypy3.11' python_arch: 'x64' tox_env: 'pypy311-pytest90-xdist38-coverage713' os: 'windows-latest' - name: 'pypy311-pytest90-xdist38-coverage713 (macos)' python: 'pypy-3.11' toxpython: 'pypy3.11' python_arch: 'arm64' tox_env: 'pypy311-pytest90-xdist38-coverage713' os: 'macos-latest' - name: 'py39-pytest80-xdist38-coverage710 (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-pytest80-xdist38-coverage710' os: 'ubuntu-latest' - name: 'py39-pytest80-xdist38-coverage710 (windows)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-pytest80-xdist38-coverage710' os: 'windows-latest' - name: 'py39-pytest80-xdist38-coverage710 (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-pytest80-xdist38-coverage710' os: 'macos-latest' - name: 'pypy39-pytest80-xdist38-coverage710 (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-pytest80-xdist38-coverage710' os: 'ubuntu-latest' - name: 'pypy39-pytest80-xdist38-coverage710 (windows)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-pytest80-xdist38-coverage710' os: 'windows-latest' - name: 'pypy39-pytest80-xdist38-coverage710 (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-pytest80-xdist38-coverage710' os: 'macos-latest' steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-python@v6 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) }} pytest-cov-7.1.0/.gitignore000066400000000000000000000012321515757410100156300ustar00rootroot00000000000000*.py[cod] __pycache__ # C extensions *.so # Packages *.egg *.egg-info dist build eggs .eggs parts bin var sdist wheelhouse develop-eggs .installed.cfg lib lib64 venv*/ pyvenv*/ pip-wheel-metadata/ # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox .coverage.* .pytest_cache/ nosetests.xml coverage.xml htmlcov # Translations *.mo # Buildout .mr.developer.cfg # IDE project files .project .pydevproject .idea .vscode *.iml *.komodoproject # Complexity output/*.html output/*/index.html # Sphinx docs/_build .DS_Store *~ .*.sw[po] .build .ve .env .cache .pytest .benchmarks .bootstrap .appveyor.token *.bak # Mypy Cache .mypy_cache/ pytest-cov-7.1.0/.pre-commit-config.yaml000066400000000000000000000014721515757410100201270ustar00rootroot00000000000000# To install the git pre-commit hooks run: # prek install --install-hooks --overwrite # To update the versions: # prek 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.15.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] - id: ruff-format - repo: https://github.com/ComPWA/taplo-pre-commit rev: v0.9.3 hooks: - id: taplo-format - id: taplo-lint - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: mixed-line-ending args: [--fix=lf] - id: debug-statements pytest-cov-7.1.0/.readthedocs.yml000066400000000000000000000004321515757410100167270ustar00rootroot00000000000000# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 sphinx: configuration: docs/conf.py formats: all build: os: ubuntu-24.04 tools: python: "3" python: install: - requirements: docs/requirements.txt - method: pip path: . pytest-cov-7.1.0/.taplo.toml000066400000000000000000000001001515757410100157230ustar00rootroot00000000000000[formatting] array_auto_collapse = false indent_string = " " pytest-cov-7.1.0/AUTHORS.rst000066400000000000000000000060701515757410100155240ustar00rootroot00000000000000 Authors ======= * Marc Schlaich - https://github.com/schlamar (\http://www.schlamar.org) * Rick van Hattem - http://wol.ph * Buck Evan - https://github.com/bukzor * Eric Larson - http://larsoner.com * Marc Abramowitz - \http://marc-abramowitz.com * Thomas Kluyver - https://github.com/takluyver * Guillaume Ayoub - http://www.yabz.fr * Federico Ceratto - \http://firelet.net * Josh Kalderimis - \http://blog.cookiestack.com * Ionel Cristian Mărieș - https://blog.ionelmc.ro * Christian Ledermann - https://github.com/cleder * Alec Nikolas Reiter - https://github.com/justanr * Patrick Lannigan - https://github.com/plannigan * David Szotten - https://github.com/davidszotten * Michael Elovskikh - https://github.com/wronglink * Saurabh Kumar - https://github.com/theskumar * Michael Elovskikh - https://github.com/wronglink * Daniel Hahler - https://github.com/blueyed (\https://daniel.hahler.de) * Florian Bruhin - http://www.the-compiler.org * Zoltan Kozma - https://github.com/kozmaz87 * Francis Niu - https://flniu.github.io * Jannis Leidel - https://github.com/jezdez * Ryan Hiebert - http://ryanhiebert.com/ * Terence Honles - https://github.com/terencehonles * Jeremy Bowman - https://github.com/jmbowman * Samuel Giffard - https://github.com/Mulugruntz * Семён Марьясин - https://github.com/MarSoft * Alexander Shadchin - https://github.com/shadchin * Thomas Grainger - https://graingert.co.uk * Juanjo Bazán - https://github.com/xuanxu * Andrew Murray - https://github.com/radarhere * Ned Batchelder - https://nedbatchelder.com/ * Albert Tugushev - https://github.com/atugushev * Martín Gaitán - https://github.com/mgaitan * Hugo van Kemenade - https://github.com/hugovk * Michael Manganiello - https://github.com/adamantike * Anders Hovmöller - https://github.com/boxed * Zac Hatfield-Dodds - https://zhd.dev * Mateus Berardo de Souza Terra - https://github.com/MatTerra * Ganden Schaffner - https://github.com/gschaffner * Michał Górny - https://github.com/mgorny * Bernát Gábor - https://github.com/gaborbernat * 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 * Tsvika Shapira - https://github.com/tsvikas * Marcos Boger - https://github.com/marcosboger * Ofek Lev - https://github.com/ofek * Art Pelling - https://github.com/artpelling * Markéta Machová - https://github.com/MeggyCal pytest-cov-7.1.0/CHANGELOG.rst000066400000000000000000000620211515757410100156640ustar00rootroot00000000000000 Changelog ========= 7.1.0 (2026-03-21) ------------------ * Fixed total coverage computation to always be consistent, regardless of reporting settings. Previously some reports could produce different total counts, and consequently can make --cov-fail-under behave different depending on reporting options. See `#641 `_. * Improve handling of ResourceWarning from sqlite3. The plugin adds warning filter for sqlite3 ``ResourceWarning`` unclosed database (since 6.2.0). It checks if there is already existing plugin for this message by comparing filter regular expression. When filter is specified on command line the message is escaped and does not match an expected message. A check for an escaped regular expression is added to handle this case. With this fix one can suppress ``ResourceWarning`` from sqlite3 from command line:: pytest -W "ignore:unclosed database in `_ and "vivodi" in `#738 `_. Also closed `#736 `_. * Fixed some assertions in tests. Contributed by in Markéta Machová in `#722 `_. * Removed unnecessary coverage configuration copying (meant as a backup because reporting commands had configuration side-effects before coverage 5.0). 7.0.0 (2025-09-09) ------------------ * Dropped support for subprocesses measurement. It was a feature added long time ago when coverage lacked a nice way to measure subprocesses created in tests. It relied on a ``.pth`` file, there was no way to opt-out and it created bad interations with `coverage's new patch system `_ added in `7.10 `_. To migrate to this release you might need to enable the suprocess patch, example for ``.coveragerc``: .. code-block:: ini [run] patch = subprocess This release also requires at least coverage 7.10.6. * Switched packaging to have metadata completely in ``pyproject.toml`` and use `hatchling `_ for building. Contributed by Ofek Lev in `#551 `_ with some extras in `#716 `_. * Removed some not really necessary testing deps like ``six``. 6.3.0 (2025-09-06) ------------------ * Added support for markdown reports. Contributed by Marcos Boger in `#712 `_ and `#714 `_. * Fixed some formatting issues in docs. Anonymous contribution in `#706 `_. 6.2.1 (2025-06-12) ------------------ * Added a version requirement for pytest's pluggy dependency (1.2.0, released 2023-06-21) that has the required new-style hookwrapper API. * Removed deprecated license classifier (packaging). * Disabled coverage warnings in two more situations where they have no value: * "module-not-measured" in workers * "already-imported" in subprocesses 6.2.0 (2025-06-11) ------------------ * The plugin now adds 3 rules in the filter warnings configuration to prevent common coverage warnings being raised as obscure errors:: default:unclosed database in `_. * Removed unnecessary CovFailUnderWarning. Fixes `#675 `_. * Fixed the term report not using the precision specified via ``--cov-precision``. 6.0.0 (2024-10-29) ------------------ * Updated various documentation inaccuracies, especially on subprocess handling. * Changed fail under checks to use the precision set in the coverage configuration. Now it will perform the check just like ``coverage report`` would. * Added a ``--cov-precision`` cli option that can override the value set in your coverage configuration. * Dropped support for now EOL Python 3.8. 5.0.0 (2024-03-24) ------------------ * Removed support for xdist rsync (now deprecated). Contributed by Matthias Reichenbach in `#623 `_. * Switched docs theme to Furo. * Various legacy Python cleanup and CI improvements. Contributed by Christian Clauss and Hugo van Kemenade in `#630 `_, `#631 `_, `#632 `_ and `#633 `_. * Added a ``pyproject.toml`` example in the docs. Contributed by Dawn James in `#626 `_. * Modernized project's pre-commit hooks to use ruff. Initial POC contributed by Christian Clauss in `#584 `_. * Dropped support for Python 3.7. 4.1.0 (2023-05-24) ------------------ * Updated CI with new Pythons and dependencies. * Removed rsyncdir support. This makes pytest-cov compatible with xdist 3.0. Contributed by Sorin Sbarnea in `#558 `_. * Optimized summary generation to not be performed if no reporting is active (for example, when ``--cov-report=''`` is used without ``--cov-fail-under``). Contributed by Jonathan Stewmon in `#589 `_. * Added support for JSON reporting. Contributed by Matthew Gamble in `#582 `_. * Refactored code to use f-strings. Contributed by Mark Mayo in `#572 `_. * Fixed a skip in the test suite for some old xdist. Contributed by a bunch of people in `#565 `_. * Dropped support for Python 3.6. 4.0.0 (2022-09-28) ------------------ **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) ------------------- * Added coverage's `toml` extra to install requirements in setup.py. Contributed by Christian Riedel in `#410 `_. * Fixed ``pytest_cov.__version__`` to have the right value (string with version instead of a string including ``__version__ =``). * Fixed license classifier in ``setup.py``. Contributed by Chris Sreesangkom in `#467 `_. * Fixed *commits since* badge. Contributed by Terence Honles in `#470 `_. 2.11.1 (2021-01-20) ------------------- * Fixed support for newer setuptools (v42+). Contributed by Michał Górny in `#451 `_. 2.11.0 (2021-01-18) ------------------- * Bumped minimum coverage requirement to 5.2.1. This prevents reporting issues. Contributed by Mateus Berardo de Souza Terra in `#433 `_. * Improved sample projects (from the `examples `_ directory) to support running `tox -e pyXY`. Now the example configures a suffixed coverage data file, and that makes the cleanup environment unnecessary. Contributed by Ganden Schaffner in `#435 `_. * 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 `_ when using subprocesses. Contributed by Bernát Gábor in `#443 `_. * Updated the config section in the docs. Contributed by Pamela McA'Nulty in `#429 `_. * Migrated CI to travis-ci.com (from .org). 2.10.1 (2020-08-14) ------------------- * Support for ``pytest-xdist`` 2.0, which breaks compatibility with ``pytest-xdist`` before 1.22.3 (from 2017). Contributed by Zac Hatfield-Dodds in `#412 `_. * Fixed the ``LocalPath has no attribute startswith`` failure that occurred when using the ``pytester`` plugin in inline mode. 2.10.0 (2020-06-12) ------------------- * Improved the ``--no-cov`` warning. Now it's only shown if ``--no-cov`` is present before ``--cov``. * Removed legacy pytest support. Changed ``setup.py`` so that ``pytest>=4.6`` is required. 2.9.0 (2020-05-22) ------------------ * Fixed ``RemovedInPytest4Warning`` when using Pytest 3.10. Contributed by Michael Manganiello in `#354 `_. * 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 `#364 `_. * Various Python support updates (drop EOL 3.4, test against 3.8 final). Contributed by Hugo van Kemenade in `#336 `_ and `#367 `_. * Changed ``--cov-append`` to always enable ``data_suffix`` (a coverage setting). Contributed by Harm Geerts in `#387 `_. * Changed ``--cov-append`` to handle loading previous data better (fixes various path aliasing issues). * Various other testing improvements, github issue templates, example updates. * Fixed internal failures that are caused by tests that change the current working directory by ensuring a consistent working directory when coverage is called. See `#306 `_ and `coveragepy#881 `_ 2.8.1 (2019-10-05) ------------------ * Fixed `#348 `_ - regression when only certain reports (html or xml) are used then ``--cov-fail-under`` always fails. 2.8.0 (2019-10-04) ------------------ * Fixed ``RecursionError`` that can occur when using `cleanup_on_signal `__ or `cleanup_on_sigterm `__. See: `#294 `_. The 2.7.x releases of pytest-cov should be considered broken regarding aforementioned cleanup API. * Added compatibility with future xdist release that deprecates some internals (match pytest-xdist master/worker terminology). Contributed by Thomas Grainger in `#321 `_ * Fixed breakage that occurs when multiple reporting options are used. Contributed by Thomas Grainger in `#338 `_. * Changed internals to use a stub instead of ``os.devnull``. Contributed by Thomas Grainger in `#332 `_. * Added support for Coverage 5.0. Contributed by Ned Batchelder in `#319 `_. * Added support for float values in ``--cov-fail-under``. Contributed by Martín Gaitán in `#311 `_. * Various documentation fixes. Contributed by Juanjo Bazán, Andrew Murray and Albert Tugushev in `#298 `_, `#299 `_ and `#307 `_. * Various testing improvements. Contributed by Ned Batchelder, Daniel Hahler, Ionel Cristian Mărieș and Hugo van Kemenade in `#313 `_, `#314 `_, `#315 `_, `#316 `_, `#325 `_, `#326 `_, `#334 `_ and `#335 `_. * Added the ``--cov-context`` CLI options that enables coverage contexts. Only works with coverage 5.0+. Contributed by Ned Batchelder in `#345 `_. 2.7.1 (2019-05-03) ------------------ * Fixed source distribution manifest so that garbage ain't included in the tarball. 2.7.0 (2019-05-03) ------------------ * Fixed ``AttributeError: 'NoneType' object has no attribute 'configure_node'`` error when ``--no-cov`` is used. Contributed by Alexander Shadchin in `#263 `_. * Various testing and CI improvements. Contributed by Daniel Hahler in `#255 `_, `#266 `_, `#272 `_, `#271 `_ and `#269 `_. * 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. * Improved cleanup code and fixed various issues with leftover data files. All contributed in `#265 `_ or `#262 `_. * Improved examples. Now there are two examples for the common project layouts, complete with working coverage configuration. The examples have CI testing. Contributed in `#267 `_. * Improved help text for CLI options. 2.6.1 (2019-01-07) ------------------ * Added support for Pytest 4.1. Contributed by Daniel Hahler and Семён Марьясин in `#253 `_ and `#230 `_. * Various test and docs fixes. Contributed by Daniel Hahler in `#224 `_ and `#223 `_. * Fixed the "Module already imported" issue (`#211 `_). Contributed by Daniel Hahler in `#228 `_. 2.6.0 (2018-09-03) ------------------ * Dropped support for Python 3 < 3.4, Pytest < 3.5 and Coverage < 4.4. * Fixed some documentation formatting. Contributed by Jean Jordaan and Julian. * Added an example with ``addopts`` in documentation. Contributed by Samuel Giffard in `#195 `_. * Fixed ``TypeError: 'NoneType' object is not iterable`` in certain xdist configurations. Contributed by Jeremy Bowman in `#213 `_. * Added a ``no_cover`` marker and fixture. Fixes `#78 `_. * Fixed broken ``no_cover`` check when running doctests. Contributed by Terence Honles in `#200 `_. * Fixed various issues with path normalization in reports (when combining coverage data from parallel mode). Fixes `#130 `_. Contributed by Ryan Hiebert & Ionel Cristian Mărieș in `#178 `_. * Report generation failures don't raise exceptions anymore. A warning will be logged instead. Fixes `#161 `_. * Fixed multiprocessing issue on Windows (empty env vars are not passed). Fixes `#165 `_. 2.5.1 (2017-05-11) ------------------ * Fixed xdist breakage (regression in ``2.5.0``). Fixes `#157 `_. * Allow setting custom ``data_file`` name in ``.coveragerc``. Fixes `#145 `_. Contributed by Jannis Leidel & Ionel Cristian Mărieș in `#156 `_. 2.5.0 (2017-05-09) ------------------ * Always show a summary when ``--cov-fail-under`` is used. Contributed by Francis Niu in `PR#141 `_. * Added ``--cov-branch`` option. Fixes `#85 `_. * Improve exception handling in subprocess setup. Fixes `#144 `_. * Fixed handling when ``--cov`` is used multiple times. Fixes `#151 `_. 2.4.0 (2016-10-10) ------------------ * Added a "disarm" option: ``--no-cov``. It will disable coverage measurements. Contributed by Zoltan Kozma in `PR#135 `_. **WARNING: Do not put this in your configuration files, it's meant to be an one-off for situations where you want to disable coverage from command line.** * Fixed broken exception handling on ``.pth`` file. See `#136 `_. 2.3.1 (2016-08-07) ------------------ * Fixed regression causing spurious errors when xdist was used. See `#124 `_. * Fixed DeprecationWarning about incorrect `addoption` use. Contributed by Florian Bruhin in `PR#127 `_. * Fixed deprecated use of funcarg fixture API. Contributed by Daniel Hahler in `PR#125 `_. 2.3.0 (2016-07-05) ------------------ * Add support for specifying output location for html, xml, and annotate report. Contributed by Patrick Lannigan in `PR#113 `_. * Fix bug hiding test failure when cov-fail-under failed. * For coverage >= 4.0, match the default behaviour of `coverage report` and error if coverage fails to find the source instead of just printing a warning. Contributed by David Szotten in `PR#116 `_. * Fixed bug occurred when bare ``--cov`` parameter was used with xdist. Contributed by Michael Elovskikh in `PR#120 `_. * Add support for ``skip_covered`` and added ``--cov-report=term-skip-covered`` command line options. Contributed by Saurabh Kumar in `PR#115 `_. 2.2.1 (2016-01-30) ------------------ * Fixed incorrect merging of coverage data when xdist was used and coverage was ``>= 4.0``. 2.2.0 (2015-10-04) ------------------ * Added support for changing working directory in tests. Previously changing working directory would disable coverage measurements in suprocesses. * Fixed broken handling for ``--cov-report=annotate``. 2.1.0 (2015-08-23) ------------------ * Added support for `coverage 4.0b2`. * Added the ``--cov-append`` command line options. Contributed by Christian Ledermann in `PR#80 `_. 2.0.0 (2015-07-28) ------------------ * Added ``--cov-fail-under``, akin to the new ``fail_under`` option in `coverage-4.0` (automatically activated if there's a ``[report] fail_under = ...`` in ``.coveragerc``). * Changed ``--cov-report=term`` to automatically upgrade to ``--cov-report=term-missing`` if there's ``[run] show_missing = True`` in ``.coveragerc``. * Changed ``--cov`` so it can be used with no path argument (in which case the source settings from ``.coveragerc`` will be used instead). * Fixed `.pth` installation to work in all cases (install, easy_install, wheels, develop etc). * Fixed `.pth` uninstallation to work for wheel installs. * Support for coverage 4.0. * Data file suffixing changed to use coverage's ``data_suffix=True`` option (instead of the custom suffixing). * Avoid warning about missing coverage data (just like ``coverage.control.process_startup``). * Fixed a race condition when running with xdist (all the workers tried to combine the files). It's possible that this issue is not present in `pytest-cov 1.8.X`. 1.8.2 (2014-11-06) ------------------ * N/A pytest-cov-7.1.0/CONTRIBUTING.rst000066400000000000000000000044301515757410100163040ustar00rootroot00000000000000============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. Bug reports =========== When `reporting a bug `_ please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Documentation improvements ========================== pytest-cov could always use more documentation, whether as part of the official pytest-cov docs, in docstrings, or even on the web in blog posts, articles, and such. Feature requests and feedback ============================= The best way to send feedback is to file an issue at https://github.com/pytest-dev/pytest-cov/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that code contributions are welcome :) Development =========== To set up `pytest-cov` for local development: 1. Fork `pytest-cov `_ (look for the "Fork" button). 2. Clone your fork locally:: git clone git@github.com:YOURGITHUBNAME/pytest-cov.git 3. Create a branch for local development:: git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 4. When you're done making changes run all the checks and docs builder with one command:: tox 5. Commit your changes and push your branch to GitHub:: git add . git commit -m "Your detailed description of your changes." git push origin name-of-your-bugfix-or-feature 6. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- If you need some code review or feedback while you're developing the code just make the pull request. For merging, you should: 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``. Tips ---- To run a subset of tests:: tox -e envname -- pytest -k test_myfeature To run all the test environments in *parallel*:: tox -p auto pytest-cov-7.1.0/LICENSE000066400000000000000000000020571515757410100146530ustar00rootroot00000000000000The MIT License Copyright (c) 2010 Meme Dough Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pytest-cov-7.1.0/README.rst000066400000000000000000000121271515757410100153340ustar00rootroot00000000000000======== Overview ======== .. start-badges .. list-table:: :stub-columns: 1 * - docs - |docs| * - tests - |github-actions| * - package - |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| |commits-since| .. |docs| image:: https://readthedocs.org/projects/pytest-cov/badge/?style=flat :target: https://readthedocs.org/projects/pytest-cov/ :alt: Documentation Status .. |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 :target: https://pypi.org/project/pytest-cov .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-cov.svg :target: https://anaconda.org/conda-forge/pytest-cov .. |wheel| image:: https://img.shields.io/pypi/wheel/pytest-cov.svg :alt: PyPI Wheel :target: https://pypi.org/project/pytest-cov .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/pytest-cov.svg :alt: Supported versions :target: https://pypi.org/project/pytest-cov .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/pytest-cov.svg :alt: Supported implementations :target: https://pypi.org/project/pytest-cov .. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v7.1.0.svg :alt: Commits since latest release :target: https://github.com/pytest-dev/pytest-cov/compare/v7.1.0...master .. end-badges This plugin provides coverage functionality as a pytest plugin. Compared to just using ``coverage run`` this plugin does some extras: * Automatic erasing and combination of .coverage files and default reporting. * Support for detailed coverage contexts (add ``--cov-context=test`` to have the full test name including parametrization as the context). * Xdist support: you can use all of pytest-xdist's features including remote interpreters and still get coverage. * Consistent pytest behavior. If you run ``coverage run -m pytest`` you will have slightly different ``sys.path`` (CWD will be in it, unlike when running ``pytest``). All features offered by the coverage package should work, either through pytest-cov's command line options or through coverage's config file. * Free software: MIT license Installation ============ Install with pip:: pip install pytest-cov For distributed testing support install pytest-xdist:: pip install pytest-xdist Upgrading from pytest-cov 6.3 ----------------------------- `pytest-cov 6.3` and older were using a ``.pth`` file to enable coverage measurements in subprocesses. This was removed in `pytest-cov 7` - use `coverage's patch options `_ to enable subprocess measurements. Uninstalling ------------ Uninstall with pip:: pip uninstall pytest-cov Under certain scenarios a stray ``.pth`` file may be left around in site-packages. * `pytest-cov 2.0` may leave a ``pytest-cov.pth`` if you installed without wheels (``easy_install``, ``setup.py install`` etc). * `pytest-cov 1.8 or older` will leave a ``init_cov_core.pth``. Usage ===== :: pytest --cov=myproj tests/ Would produce a report like:: -------------------- coverage: ... --------------------- Name Stmts Miss Cover ---------------------------------------- myproj/__init__ 2 0 100% myproj/myproj 257 13 94% myproj/feature4286 94 7 92% ---------------------------------------- TOTAL 353 20 94% Documentation ============= https://pytest-cov.readthedocs.io/en/latest/ Coverage Data File ================== The data file is erased at the beginning of testing to ensure clean data for each test run. If you need to combine the coverage of several test runs you can use the ``--cov-append`` option to append this coverage data to coverage data from previous test runs. The data file is left at the end of testing so that it is possible to use normal coverage tools to examine it. Limitations =========== For distributed testing the workers must have the pytest-cov package installed. This is needed since the plugin must be registered through setuptools for pytest to start the plugin on the worker. Security ======== To report a security vulnerability please use the `Tidelift security contact `_. Tidelift will coordinate the fix and disclosure. Acknowledgements ================ Whilst this plugin has been built fresh from the ground up it has been influenced by the work done on pytest-coverage (Ross Lawley, James Mills, Holger Krekel) and nose-cover (Jason Pellerin) which are other coverage plugins. Ned Batchelder for coverage and its ability to combine the coverage results of parallel runs. Holger Krekel for pytest with its distributed testing support. Jason Pellerin for nose. Michael Foord for unittest2. No doubt others have contributed to these tools as well. pytest-cov-7.1.0/SECURITY.md000066400000000000000000000003011515757410100154250ustar00rootroot00000000000000## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. pytest-cov-7.1.0/ci/000077500000000000000000000000001515757410100142355ustar00rootroot00000000000000pytest-cov-7.1.0/ci/bootstrap.py000077500000000000000000000054121515757410100166310ustar00rootroot00000000000000#!/usr/bin/env python import os import pathlib import subprocess import sys base_path: pathlib.Path = pathlib.Path(__file__).resolve().parent.parent templates_path = base_path / 'ci' / 'templates' def check_call(args): print('+', *args) subprocess.check_call(args) def exec_in_env(): env_path = base_path / '.tox' / 'bootstrap' if sys.platform == 'win32': bin_path = env_path / 'Scripts' else: bin_path = env_path / 'bin' if not env_path.exists(): print(f'Making bootstrap env in: {env_path} ...') try: check_call([sys.executable, '-m', 'venv', env_path]) except subprocess.CalledProcessError: try: check_call([sys.executable, '-m', 'virtualenv', env_path]) except subprocess.CalledProcessError: check_call(['virtualenv', env_path]) print('Installing `jinja2` into bootstrap environment...') check_call([bin_path / 'pip', 'install', 'jinja2', 'tox']) python_executable = bin_path / 'python' if not python_executable.exists(): python_executable = python_executable.with_suffix('.exe') print(f'Re-executing with: {python_executable}') print('+ exec', python_executable, __file__, '--no-env') os.execv(python_executable, [python_executable, __file__, '--no-env']) def main(): import jinja2 print(f'Project path: {base_path}') jinja = jinja2.Environment( loader=jinja2.FileSystemLoader(str(templates_path)), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True, ) tox_environments = [ line.strip() # '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.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']: main() elif not args: exec_in_env() else: print(f'Unexpected arguments: {args}', file=sys.stderr) sys.exit(1) pytest-cov-7.1.0/ci/requirements.txt000066400000000000000000000000601515757410100175150ustar00rootroot00000000000000pip>=25 setuptools>=80 tox>=4 virtualenv>=20.34 pytest-cov-7.1.0/ci/templates/000077500000000000000000000000001515757410100162335ustar00rootroot00000000000000pytest-cov-7.1.0/ci/templates/.github/000077500000000000000000000000001515757410100175735ustar00rootroot00000000000000pytest-cov-7.1.0/ci/templates/.github/workflows/000077500000000000000000000000001515757410100216305ustar00rootroot00000000000000pytest-cov-7.1.0/ci/templates/.github/workflows/test.yml000066400000000000000000000071241515757410100233360ustar00rootroot00000000000000name: Tests on: [push, pull_request, workflow_dispatch] jobs: {%- raw %} examples: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["pypy-3.11", "3.11"] target: [ "src-layout", "adhoc-layout", ] include: # Add new helper variables to existing jobs - {python-version: "pypy-3.11", tox-python-version: "pypy11"} - {python-version: "3.11", tox-python-version: "py311"} steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Cache uses: actions/cache@v5 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', 'arm64'], ] %} - name: '{{ env }} ({{ os }})' python: '{{ python }}' toxpython: '{{ toxpython }}' python_arch: '{{ python_arch }}' tox_env: '{{ env }}' os: '{{ os }}-latest' {% endfor %} {% endfor %} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-python@v6 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 %} pytest-cov-7.1.0/docs/000077500000000000000000000000001515757410100145725ustar00rootroot00000000000000pytest-cov-7.1.0/docs/authors.rst000066400000000000000000000000341515757410100170060ustar00rootroot00000000000000.. include:: ../AUTHORS.rst pytest-cov-7.1.0/docs/changelog.rst000066400000000000000000000000361515757410100172520ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst pytest-cov-7.1.0/docs/conf.py000066400000000000000000000023341515757410100160730ustar00rootroot00000000000000import os extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinx.ext.extlinks', ] if os.getenv('SPELLCHECK'): extensions += ('sphinxcontrib.spelling',) spelling_show_suggestions = True spelling_lang = 'en_US' source_suffix = '.rst' master_doc = 'index' project = 'pytest-cov' year = '2010-2024' author = 'pytest-cov contributors' copyright = f'{year}, {author}' version = release = '7.1.0' pygments_style = 'trac' templates_path = ['.'] extlinks = { 'issue': ('https://github.com/pytest-dev/pytest-cov/issues/%s', '#'), 'pr': ('https://github.com/pytest-dev/pytest-cov/pull/%s', 'PR #'), } html_theme = 'furo' html_theme_options = { 'source_repository': 'https://github.com/pytest-dev/pytest-cov/', 'source_branch': 'master', 'source_directory': 'docs/', } html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' html_split_index = False html_short_title = f'{project}-{version}' linkcheck_anchors_ignore_for_url = [ r'^https?://(www\.)?github\.com/.*', ] napoleon_use_ivar = True napoleon_use_rtype = False napoleon_use_param = False pytest-cov-7.1.0/docs/config.rst000066400000000000000000000073571515757410100166050ustar00rootroot00000000000000============= Configuration ============= This plugin provides a clean minimal set of command line options that are added to pytest. For further control of coverage use a `coverage config file`_. CLI options: --cov [SOURCE] Path or package name to measure during execution (multi-allowed). Use --cov= to not do any source filtering and record everything. --cov-reset Reset cov sources accumulated in options so far. --cov-report TYPE Type of report to generate: term, term-missing, annotate, html, xml, json, markdown, markdown-append, lcov (multi-allowed). term, term-missing may be followed by ":skip-covered". annotate, html, xml, json, markdown, markdown-append 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 --no-cov-on-fail Do not report coverage if test run fails. Default: False --no-cov Disable coverage report completely (useful for debuggers). Default: False --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 --cov-branch Enable branch coverage. Can also be specified in the `coverage config file`_ ``[run]`` section. --cov-precision COV_PRECISION Override the reporting precision. Can also be specified in the `coverage config file`_ ``[report]`` section. --cov-context CONTEXT Dynamic contexts to use. "test" for now. .. note:: Important Note 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 overridden. If you wish to always run 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" .. note:: Important Note The ``--cov`` option has an optional argument. If it's your last option in addopts_ it might eat the next CLI argument, make sure to force it to take a blank value if that's what you wanted by using ``--cov=`` (essentially the same as ``--cov=""``). Caveats ======= An unfortunate consequence of coverage.py's history is that ``.coveragerc`` is a magic name: it's the default file but it also means "try to also lookup coverage configuration in ``tox.ini`` or ``setup.cfg``". In practical terms this means that if you have multiple configuration files around (``tox.ini``, ``pyproject.toml`` or ``setup.cfg``) you might need to use ``--cov-config`` to make coverage use the correct configuration file. Also, if you change the working directory and also use subprocesses in a test you might also need to use ``--cov-config`` to make pytest-cov use the expected configuration file in the subprocess. .. _`coverage config file`: https://coverage.readthedocs.io/en/latest/config.html .. _addopts: https://docs.pytest.org/en/stable/reference/reference.html#confval-addopts> pytest-cov-7.1.0/docs/contexts.rst000066400000000000000000000022131515757410100171710ustar00rootroot00000000000000======== Contexts ======== 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:: test_functions.py::test_addition|run test_fancy.py::test_parametrized[1-101]|setup test_oldschool.py::RegressionTests::test_error|run 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. pytest-cov-7.1.0/docs/contributing.rst000066400000000000000000000000411515757410100200260ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst pytest-cov-7.1.0/docs/debuggers.rst000066400000000000000000000017051515757410100172760ustar00rootroot00000000000000===================== Debuggers and PyCharm ===================== (or other IDEs) 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, 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:: Coverage disabled via --no-cov switch! pytest-cov-7.1.0/docs/index.rst000066400000000000000000000006271515757410100164400ustar00rootroot00000000000000Welcome to pytest-cov's documentation! ====================================== Contents: .. toctree:: :maxdepth: 2 readme config reporting debuggers xdist subprocess-support contexts tox plugins markers-fixtures changelog authors releasing contributing Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pytest-cov-7.1.0/docs/markers-fixtures.rst000066400000000000000000000014441515757410100206420ustar00rootroot00000000000000==================== Markers and fixtures ==================== There are some builtin markers and fixtures in ``pytest-cov``. Markers ======= ``no_cover`` ------------ Eg: .. code-block:: python @pytest.mark.no_cover def test_foobar(): # do some stuff that needs coverage disabled .. warning:: Caveat Note that subprocess coverage will also be disabled. Fixtures ======== ``no_cover`` ------------ Eg: .. code-block:: python def test_foobar(no_cover): # same as the marker ... ``cov`` ------- For reasons that no one can remember there is a ``cov`` fixture that provides access to the underlying Coverage instance. Some say this is a disguised foot-gun and should be removed, and some think mysteries make life more interesting and it should be left alone. pytest-cov-7.1.0/docs/plugins.rst000066400000000000000000000010421515757410100170020ustar00rootroot00000000000000=============== Plugin coverage =============== 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. **Currently there is no way to measure your pytest plugin if you use pytest-cov**. You should change your test invocations to use ``coverage run -m pytest ...`` instead. pytest-cov-7.1.0/docs/readme.rst000066400000000000000000000000331515757410100165550ustar00rootroot00000000000000.. include:: ../README.rst pytest-cov-7.1.0/docs/releasing.rst000066400000000000000000000022501515757410100172740ustar00rootroot00000000000000========= Releasing ========= The process for releasing should follow these steps: #. 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. #. Push changes and tags with:: git push git push --tags #. 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``):: rm -rf dist build src/*.egg-info These files need to be removed to force distutils/setuptools to rebuild everything and recreate the egg-info metadata. #. Build the dists:: python -m build #. Verify that the resulting archives (found in ``dist/``) are good. #. Upload the sdist and wheel with twine:: twine upload dist/* pytest-cov-7.1.0/docs/reporting.rst000066400000000000000000000067751515757410100173540ustar00rootroot00000000000000Reporting ========= It is possible to generate any combination of the reports for a single test run. The available reports are terminal (with or without missing line numbers shown), HTML, XML, JSON, Markdown (either in 'write' or 'append' mode to file), LCOV and annotated source code. The default is terminal report without line numbers:: pytest --cov=myproj tests/ -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- Name Stmts Miss Cover ---------------------------------------- myproj/__init__ 2 0 100% myproj/myproj 257 13 94% myproj/feature4286 94 7 92% ---------------------------------------- TOTAL 353 20 94% The terminal report with line numbers:: pytest --cov-report=term-missing --cov=myproj tests/ -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- Name Stmts Miss Cover Missing -------------------------------------------------- myproj/__init__ 2 0 100% myproj/myproj 257 13 94% 24-26, 99, 149, 233-236, 297-298, 369-370 myproj/feature4286 94 7 92% 183-188, 197 -------------------------------------------------- TOTAL 353 20 94% The terminal report with skip covered:: pytest --cov-report term:skip-covered --cov=myproj tests/ -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- Name Stmts Miss Cover ---------------------------------------- myproj/myproj 257 13 94% myproj/feature4286 94 7 92% ---------------------------------------- TOTAL 353 20 94% 1 files skipped due to complete coverage. You can use ``skip-covered`` with ``term-missing`` as well. e.g. ``--cov-report term-missing:skip-covered`` If any reporting options are used then the default (``--cov-report=term``) is not added automatically. For example this would not show any terminal output: .. code-block:: bash pytest --cov-report html --cov-report xml --cov-report json --cov-report markdown --cov-report markdown-append:cov-append.md --cov-report lcov --cov-report annotate --cov=myproj tests/ You can specify output paths for reports. The output location for the XML, JSON, Markdown and LCOV report is a file. Where as the output location for the HTML and annotated source code reports are directories: .. code-block:: bash pytest --cov-report html:cov_html --cov-report xml:cov.xml --cov-report json:cov.json --cov-report markdown:cov.md --cov-report markdown-append:cov-append.md --cov-report lcov:cov.info --cov-report annotate:cov_annotate --cov=myproj tests/ Example for GitHub Actions with ``markdown-append``: .. code-block:: bash pytest --cov-report markdown-append:$GITHUB_STEP_SUMMARY --cov=myproj tests/ To disable the default ``term`` report provide an empty report: .. code-block:: bash pytest --cov-report= --cov=myproj tests/ 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 GitHub Actions could produce a .coverage file for use with Coveralls. pytest-cov-7.1.0/docs/requirements.txt000066400000000000000000000000211515757410100200470ustar00rootroot00000000000000sphinx furo -e . pytest-cov-7.1.0/docs/spelling_wordlist.txt000066400000000000000000000001551515757410100211000ustar00rootroot00000000000000builtin builtins classmethod staticmethod classmethods staticmethods args kwargs callstack Changelog Indices pytest-cov-7.1.0/docs/subprocess-support.rst000066400000000000000000000014271515757410100212320ustar00rootroot00000000000000================== Subprocess support ================== Subprocess support was removed in pytest-cov 7.0 due to various complexities resulting from coverage's own subprocess support. To migrate you should change your coverage config to have at least this: .. code-block:: ini [run] patch = subprocess Or if you use pyproject.toml: .. code-block:: toml [tool.coverage.run] patch = ["subprocess"] Note that if you enable the subprocess patch then ``parallel = true`` is automatically set. If it still doesn't produce the same coverage as before you may need to enable more patches, see the `coverage config `_ and `subprocess `_ documentation. pytest-cov-7.1.0/docs/tox.rst000066400000000000000000000033001515757410100161320ustar00rootroot00000000000000=== Tox === When using `tox `_ you can have ultra-compact configuration - you can have all of it in ``tox.ini``:: [tox] envlist = ... [tool:pytest] ... [coverage:paths] ... [coverage:run] ... [coverage:report] .. [testenv] commands = ... An usual problem users have is that pytest-cov will erase the previous coverage data by default, thus if you run tox with multiple environments you'll get incomplete coverage at the end. To prevent this problem you need to use ``--cov-append``. It's still recommended to clean the previous coverage data to have consistent output. A ``tox.ini`` like this should be enough for sequential runs:: [tox] envlist = clean,py27,py36,... [testenv] commands = pytest --cov --cov-append --cov-report=term-missing ... deps = pytest pytest-cov [testenv:clean] deps = coverage skip_install = true commands = coverage erase For parallel runs we need to set some dependencies and have an extra report env like so:: [tox] envlist = clean,py27,py36,report [testenv] commands = pytest --cov --cov-append --cov-report=term-missing deps = pytest pytest-cov depends = {py27,py36}: clean report: py27,py36 [testenv:report] deps = coverage skip_install = true commands = coverage report coverage html [testenv:clean] deps = coverage skip_install = true commands = coverage erase Depending on your project layout you might need extra configuration, see the working examples at https://github.com/pytest-dev/pytest-cov/tree/master/examples for two common layouts. pytest-cov-7.1.0/docs/xdist.rst000066400000000000000000000054311515757410100164620ustar00rootroot00000000000000=========================== Distributed testing (xdist) =========================== "load" mode =========== Distributed testing with dist mode set to "load" will report on the combined coverage of all workers. The workers may be spread out over any number of hosts and each worker may be located anywhere on the file system. Each worker will have its subprocesses measured. Running distributed testing with dist mode set to load:: pytest --cov=myproj -n 2 tests/ Shows a terminal report:: -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- Name Stmts Miss Cover ---------------------------------------- myproj/__init__ 2 0 100% myproj/myproj 257 13 94% myproj/feature4286 94 7 92% ---------------------------------------- TOTAL 353 20 94% Again but spread over different hosts and different directories:: pytest --cov=myproj --dist load --tx ssh=memedough@host1//chdir=testenv1 --tx ssh=memedough@host2//chdir=/tmp/testenv2//python=/tmp/env1/bin/python --rsyncdir myproj --rsyncdir tests --rsync examples tests/ Shows a terminal report:: -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- Name Stmts Miss Cover ---------------------------------------- myproj/__init__ 2 0 100% myproj/myproj 257 13 94% myproj/feature4286 94 7 92% ---------------------------------------- TOTAL 353 20 94% "each" mode =========== Distributed testing with dist mode set to each will report on the combined coverage of all workers. Since each worker is running all tests this allows generating a combined coverage report for multiple environments. Running distributed testing with dist mode set to each:: pytest --cov=myproj --dist each --tx popen//chdir=/tmp/testenv3//python=/usr/local/python27/bin/python --tx ssh=memedough@host2//chdir=/tmp/testenv4//python=/tmp/env2/bin/python --rsyncdir myproj --rsyncdir tests --rsync examples tests/ Shows a terminal report:: ---------------------------------------- coverage ---------------------------------------- platform linux2, python 2.6.5-final-0 platform linux2, python 2.7.0-final-0 Name Stmts Miss Cover ---------------------------------------- myproj/__init__ 2 0 100% myproj/myproj 257 13 94% myproj/feature4286 94 7 92% ---------------------------------------- TOTAL 353 20 94% pytest-cov-7.1.0/examples/000077500000000000000000000000001515757410100154605ustar00rootroot00000000000000pytest-cov-7.1.0/examples/README.rst000066400000000000000000000010341515757410100171450ustar00rootroot00000000000000Simple examples with ``tox.ini`` ================================ These examples provide necessary configuration to: * aggregate coverage from multiple interpreters * support tox parallel mode * run tests on installed code The `adhoc` layout is the old and problematic layout where you can mix up the installed code with the source code. However, these examples will provide correct configuration even for the `adhoc` layout. The `src` layout configuration is less complicated, have that in mind when picking a layout for your project. pytest-cov-7.1.0/examples/adhoc-layout/000077500000000000000000000000001515757410100200515ustar00rootroot00000000000000pytest-cov-7.1.0/examples/adhoc-layout/.coveragerc000066400000000000000000000002301515757410100221650ustar00rootroot00000000000000[paths] source = ../example */site-packages/example [run] branch = true source = example . [report] precision = 2 show_missing = true pytest-cov-7.1.0/examples/adhoc-layout/example/000077500000000000000000000000001515757410100215045ustar00rootroot00000000000000pytest-cov-7.1.0/examples/adhoc-layout/example/__init__.py000066400000000000000000000003311515757410100236120ustar00rootroot00000000000000import platform # test merging multiple tox runs with a platform # based branch if platform.python_implementation() == 'PyPy': def add(a, b): return a + b else: def add(a, b): return a + b pytest-cov-7.1.0/examples/adhoc-layout/setup.py000066400000000000000000000002211515757410100215560ustar00rootroot00000000000000from setuptools import find_packages from setuptools import setup setup( name='example', packages=find_packages(include=['example']), ) pytest-cov-7.1.0/examples/adhoc-layout/tests/000077500000000000000000000000001515757410100212135ustar00rootroot00000000000000pytest-cov-7.1.0/examples/adhoc-layout/tests/test_example.py000066400000000000000000000001511515757410100242540ustar00rootroot00000000000000import example def test_add(): assert example.add(1, 1) == 2 assert not example.add(0, 1) == 2 pytest-cov-7.1.0/examples/adhoc-layout/tox.ini000066400000000000000000000014121515757410100213620ustar00rootroot00000000000000[tox] envlist = clean,pypy311,py311,report [tool:pytest] addopts = --cov-report=term-missing [testenv] commands = pytest --cov --cov-append --cov-config={toxinidir}/.coveragerc {posargs:-vv} deps = pytest coverage # Note: # This is here just to allow examples to be tested against # the current code of pytest-cov. If you copy this then # use "pytest-cov" instead of "../.." ../.. depends = report: pypy311,py311 {pypy311,py311}: clean # note that this is necessary to prevent the tests importing the code from your badly laid project changedir = tests [testenv:clean] skip_install = true deps = coverage commands = coverage erase [testenv:report] skip_install = true deps = coverage commands = coverage report --fail-under=100 pytest-cov-7.1.0/examples/src-layout/000077500000000000000000000000001515757410100175625ustar00rootroot00000000000000pytest-cov-7.1.0/examples/src-layout/.coveragerc000066400000000000000000000002171515757410100217030ustar00rootroot00000000000000[paths] source = src */site-packages [run] branch = true source = src/example tests [report] precision = 2 show_missing = true pytest-cov-7.1.0/examples/src-layout/setup.py000066400000000000000000000002401515757410100212700ustar00rootroot00000000000000from setuptools import find_packages from setuptools import setup setup( name='example', packages=find_packages('src'), package_dir={'': 'src'}, ) pytest-cov-7.1.0/examples/src-layout/src/000077500000000000000000000000001515757410100203515ustar00rootroot00000000000000pytest-cov-7.1.0/examples/src-layout/src/example/000077500000000000000000000000001515757410100220045ustar00rootroot00000000000000pytest-cov-7.1.0/examples/src-layout/src/example/__init__.py000066400000000000000000000003311515757410100241120ustar00rootroot00000000000000import platform # test merging multiple tox runs with a platform # based branch if platform.python_implementation() == 'PyPy': def add(a, b): return a + b else: def add(a, b): return a + b pytest-cov-7.1.0/examples/src-layout/tests/000077500000000000000000000000001515757410100207245ustar00rootroot00000000000000pytest-cov-7.1.0/examples/src-layout/tests/test_example.py000066400000000000000000000001511515757410100237650ustar00rootroot00000000000000import example def test_add(): assert example.add(1, 1) == 2 assert not example.add(0, 1) == 2 pytest-cov-7.1.0/examples/src-layout/tox.ini000066400000000000000000000012011515757410100210670ustar00rootroot00000000000000[tox] envlist = clean,pypy311,py311,report [tool:pytest] testpaths = tests addopts = --cov-report=term-missing [testenv] commands = pytest --cov --cov-append {posargs:-vv} deps = pytest coverage # Note: # This is here just to allow examples to be tested against # the current code of pytest-cov. If you copy this then # use "pytest-cov" instead of "../.." ../.. depends = report: pypy311,py311 {pypy311,py311}: clean [testenv:clean] skip_install = true deps = coverage commands = coverage erase [testenv:report] skip_install = true deps = coverage commands = coverage report --fail-under=100 pytest-cov-7.1.0/pyproject.toml000066400000000000000000000064251515757410100165650ustar00rootroot00000000000000[build-system] requires = ["hatchling", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [project] name = "pytest-cov" dynamic = ["readme"] version = "7.1.0" description = "Pytest plugin for measuring coverage." license = "MIT" requires-python = ">=3.9" authors = [ { name = "Marc Schlaich", email = "marc.schlaich@gmail.com" }, ] maintainers = [ { name = "Ionel Cristian Mărieș", email = "contact@ionelmc.ro" }, ] keywords = [ "cover", "coverage", "distributed", "parallel", "py.test", "pytest", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Pytest", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Utilities", ] dependencies = [ "coverage[toml]>=7.10.6", "pytest>=7", "pluggy>=1.2", ] [project.optional-dependencies] testing = [ "process-tests", "pytest-xdist", "virtualenv", ] [project.entry-points.pytest11] pytest_cov = "pytest_cov.plugin" [project.urls] "Sources" = "https://github.com/pytest-dev/pytest-cov" "Documentation" = "https://pytest-cov.readthedocs.io/" "Changelog" = "https://pytest-cov.readthedocs.io/en/latest/changelog.html" "Issue Tracker" = "https://github.com/pytest-dev/pytest-cov/issues" [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/x-rst" fragments = [ { path = "README.rst" }, { path = "CHANGELOG.rst" }, ] [tool.ruff] extend-exclude = ["static", "ci/templates"] line-length = 140 src = ["src", "tests"] target-version = "py39" [tool.ruff.lint.per-file-ignores] "ci/*" = ["S"] [tool.ruff.lint] ignore = [ "PLC0415", # `import` should be at the top-level of a file "RUF001", # ruff-specific rules ambiguous-unicode-character-string "S101", # flake8-bandit assert "S308", # flake8-bandit suspicious-mark-safe-usage "E501", # pycodestyle line-too-long ] select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions "DTZ", # flake8-datetimez "E", # pycodestyle errors "EXE", # flake8-executable "F", # pyflakes "I", # isort "INT", # flake8-gettext "PIE", # flake8-pie "PLC", # pylint convention "PLE", # pylint errors "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib "RSE", # flake8-raise "RUF", # ruff-specific rules "S", # flake8-bandit "UP", # pyupgrade "W", # pycodestyle warnings ] [tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false mark-parentheses = false [tool.ruff.lint.isort] forced-separate = ["conftest"] force-single-line = true [tool.ruff.format] quote-style = "single" pytest-cov-7.1.0/pytest.ini000066400000000000000000000014271515757410100156770ustar00rootroot00000000000000[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! pytest-cov-7.1.0/src/000077500000000000000000000000001515757410100144315ustar00rootroot00000000000000pytest-cov-7.1.0/src/pytest_cov/000077500000000000000000000000001515757410100166305ustar00rootroot00000000000000pytest-cov-7.1.0/src/pytest_cov/__init__.py000066400000000000000000000016141515757410100207430ustar00rootroot00000000000000"""pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE.""" __version__ = '7.1.0' import pytest class CoverageError(Exception): """Indicates that our coverage is too low""" class PytestCovWarning(pytest.PytestWarning): """ The base for all pytest-cov warnings, never raised directly. """ class CovDisabledWarning(PytestCovWarning): """ Indicates that Coverage was manually disabled. """ class CovReportWarning(PytestCovWarning): """ Indicates that we failed to generate a report. """ class CentralCovContextWarning(PytestCovWarning): """ Indicates that dynamic_context was set to test_function instead of using the builtin --cov-context. """ class DistCovError(Exception): """ Raised when dynamic_context is set to test_function and xdist is also used. See: https://github.com/pytest-dev/pytest-cov/issues/604 """ pytest-cov-7.1.0/src/pytest_cov/engine.py000066400000000000000000000377251515757410100204650ustar00rootroot00000000000000"""Coverage controllers for use by pytest-cov and nose-cov.""" import argparse import contextlib import functools import os import random import shutil import socket import sys import warnings from pathlib import Path from typing import Union import coverage from coverage.data import CoverageData from coverage.sqldata import filename_suffix from . import CentralCovContextWarning from . import DistCovError class BrokenCovConfigError(Exception): pass class _NullFile: @staticmethod def write(v): pass def _ensure_topdir(meth): @functools.wraps(meth) def ensure_topdir_wrapper(self, *args, **kwargs): try: 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. original_cwd = None os.chdir(self.topdir) try: return meth(self, *args, **kwargs) finally: if original_cwd is not None: os.chdir(original_cwd) return ensure_topdir_wrapper class CovController: """Base class for different plugin implementations.""" def __init__(self, options: argparse.Namespace, config: Union[None, object], nodeid: Union[None, str]): """Get some common config used by multiple derived classes.""" self.cov_source = options.cov_source self.cov_report = options.cov_report self.cov_config = options.cov_config self.cov_append = options.cov_append self.cov_branch = options.cov_branch self.cov_precision = options.cov_precision self.config = config self.nodeid = nodeid self.cov = None self.combining_cov = None self.data_file = None self.node_descs = set() self.failed_workers = [] self.topdir = os.fspath(Path.cwd()) self.is_collocated = None self.started = False @contextlib.contextmanager def ensure_topdir(self): original_cwd = Path.cwd() os.chdir(self.topdir) yield os.chdir(original_cwd) @_ensure_topdir def pause(self): self.started = False self.cov.stop() @_ensure_topdir def resume(self): self.cov.start() self.started = True def start(self): self.started = True def finish(self): self.started = False @staticmethod def get_node_desc(platform, version_info): """Return a description of this node.""" return 'platform {}, python {}'.format(platform, '{}.{}.{}-{}-{}'.format(*version_info[:5])) @staticmethod def get_width(): # taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L26 width, _ = shutil.get_terminal_size(fallback=(80, 24)) # The Windows get_terminal_size may be bogus, let's sanify a bit. if width < 40: width = 80 return width def sep(self, stream, s, txt): if hasattr(stream, 'sep'): stream.sep(s, txt) else: fullwidth = self.get_width() # taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L126 # The goal is to have the line be as long as possible # under the condition that len(line) <= fullwidth. if sys.platform == 'win32': # If we print in the last column on windows we are on a # new line but there is no way to verify/neutralize this # (we may not know the exact line width). # So let's be defensive to avoid empty lines in the output. fullwidth -= 1 N = max((fullwidth - len(txt) - 2) // (2 * len(s)), 1) fill = s * N line = f'{fill} {txt} {fill}' # In some situations there is room for an extra sepchar at the right, # in particular if we consider that with a sepchar like "_ " the # trailing space is not important at the end of the line. if len(line) + len(s.rstrip()) <= fullwidth: line += s.rstrip() # (end of terminalwriter borrowed code) line += '\n\n' stream.write(line) @_ensure_topdir def summary(self, stream): """Produce coverage reports.""" total = self.cov.report(ignore_errors=True, output_format='total', precision=self.cov_precision, file=_NullFile) # Output coverage section header. if len(self.node_descs) == 1: 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, ' ', 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') for node in self.failed_workers: 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']): options = { 'show_missing': ('term-missing' in self.cov_report) or None, 'ignore_errors': True, 'file': stream, 'precision': self.cov_precision, } skip_covered = isinstance(self.cov_report, dict) and 'skip-covered' in self.cov_report.values() options.update({'skip_covered': skip_covered or None}) self.cov.report(**options) # Produce annotated source code report if wanted. if 'annotate' in self.cov_report: annotate_dir = self.cov_report['annotate'] 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. self.cov.report(ignore_errors=True, file=_NullFile) if 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'] self.cov.html_report(ignore_errors=True, directory=output) stream.write(f'Coverage HTML written to dir {self.cov.config.html_dir if output is None else output}\n') # Produce xml report if wanted. if 'xml' in self.cov_report: output = self.cov_report['xml'] self.cov.xml_report(ignore_errors=True, outfile=output) stream.write(f'Coverage XML written to file {self.cov.config.xml_output if output is None else output}\n') # Produce json report if wanted if 'json' in self.cov_report: output = self.cov_report['json'] 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 Markdown report if wanted. if 'markdown' in self.cov_report: output = self.cov_report['markdown'] with Path(output).open('w') as output_file: self.cov.report(ignore_errors=True, file=output_file, output_format='markdown') stream.write(f'Coverage Markdown information written to file {output}\n') # Produce Markdown report if wanted, appending to output file if 'markdown-append' in self.cov_report: output = self.cov_report['markdown-append'] with Path(output).open('a') as output_file: self.cov.report(ignore_errors=True, file=output_file, output_format='markdown') stream.write(f'Coverage Markdown information appended to file {output}\n') # Produce lcov report if wanted. if 'lcov' in self.cov_report: output = self.cov_report['lcov'] 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. 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 class Central(CovController): """Implementation for centralised operation.""" @_ensure_topdir def start(self): self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, data_suffix=True, config_file=self.cov_config, ) if self.cov.config.dynamic_context == 'test_function': message = ( 'Detected dynamic_context=test_function in coverage configuration. ' 'This is unnecessary as this plugin provides the more complete --cov-context option.' ) warnings.warn(CentralCovContextWarning(message), stacklevel=1) self.combining_cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, data_suffix=f'{filename_suffix(True)}.combine', 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: self.cov.erase() self.cov.start() super().start() @_ensure_topdir def finish(self): """Stop coverage, save data to file and set the list of coverage objects to report on.""" super().finish() self.cov.stop() self.cov.save() self.cov = self.combining_cov self.cov.load() self.cov.combine() self.cov.save() node_desc = self.get_node_desc(sys.platform, sys.version_info) self.node_descs.add(node_desc) class DistMaster(CovController): """Implementation for distributed master.""" @_ensure_topdir def start(self): self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, data_suffix=True, config_file=self.cov_config, ) if self.cov.config.dynamic_context == 'test_function': raise DistCovError( 'Detected dynamic_context=test_function in coverage configuration. ' 'This is known to cause issues when using xdist, see: https://github.com/pytest-dev/pytest-cov/issues/604\n' 'It is recommended to use --cov-context instead.' ) self.cov._warn_no_data = False self.cov._warn_unimported_source = False self.cov._warn_preimported_source = False self.combining_cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, data_suffix=f'{filename_suffix(True)}.combine', 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() self.cov.config.paths['source'] = [self.topdir] 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], } ) def testnodedown(self, node, error): """Collect data file name from worker.""" # If worker doesn't return any data then it is likely that this # plugin didn't get activated on the worker side. output = getattr(node, 'workeroutput', {}) if 'cov_worker_node_id' not in output: self.failed_workers.append(node) return # If worker is not collocated then we must save the data file # that it returns to us. if 'cov_worker_data' in output: data_suffix = '%s.%s.%06d.%s' % ( # noqa: UP031 socket.gethostname(), os.getpid(), random.randint(0, 999999), # noqa: S311 output['cov_worker_node_id'], ) cov_data = CoverageData( suffix=data_suffix, ) cov_data.loads(output['cov_worker_data']) path = output['cov_worker_path'] self.cov.config.paths['source'].append(path) # Record the worker types that contribute to the data file. rinfo = node.gateway._rinfo() node_desc = self.get_node_desc(rinfo.platform, rinfo.version_info) self.node_descs.add(node_desc) @_ensure_topdir def finish(self): """Combines coverage data and sets the list of coverage objects to report on.""" # Combine all the suffix files into the data file. self.cov.stop() self.cov.save() self.cov = self.combining_cov self.cov.load() self.cov.combine() self.cov.save() class DistWorker(CovController): """Implementation for distributed workers.""" @_ensure_topdir def start(self): # Determine whether we are collocated with master. self.is_collocated = ( socket.gethostname() == self.config.workerinput['cov_master_host'] and self.topdir == self.config.workerinput['cov_master_topdir'] ) # If we are not collocated, then rewrite master paths to worker paths. if 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_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, ) # Prevent workers from issuing module-not-measured type of warnings (expected for a workers to not have coverage in all the files). self.cov._warn_unimported_source = False self.cov.start() super().start() @_ensure_topdir def finish(self): """Stop coverage and send relevant info back to the master.""" super().finish() self.cov.stop() if self.is_collocated: # We don't combine data if we're collocated - we can get # race conditions in the .combine() call (it's not atomic) # The data is going to be combined in the master. self.cov.save() # If we are collocated then just inform the master of our # data file to indicate that we have finished. self.config.workeroutput['cov_worker_node_id'] = self.nodeid else: self.cov.combine() self.cov.save() # If we are not collocated then add the current path # and coverage data to the output so we can combine # it on the master node. # Send all the data to the master over the channel. 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, } ) def summary(self, stream): """Only the master reports so do nothing.""" pytest-cov-7.1.0/src/pytest_cov/plugin.py000066400000000000000000000417361515757410100205130ustar00rootroot00000000000000"""Coverage plugin for pytest.""" import argparse import os import re import warnings from io import StringIO from pathlib import Path from typing import TYPE_CHECKING import pytest from . import CovDisabledWarning from . import CovReportWarning from . import PytestCovWarning if TYPE_CHECKING: from .engine import CovController # The message is unescaped if it comes from configuration file, and escaped if # it comes from command line option (-W) or PYTHONWARNINGS envvar. COVERAGE_SQLITE_WARNING_RE = re.compile('unclosed database in 100: raise argparse.ArgumentTypeError( 'Your desire for over-achievement is admirable but misplaced. The maximum value is 100. Perhaps write more integration tests?' ) return value def validate_context(arg): if arg != 'test': raise argparse.ArgumentTypeError('The only supported value is "test".') return arg class StoreReport(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): report_type, file = values namespace.cov_report[report_type] = file # coverage.py doesn't set a default file for markdown output_format if report_type in ['markdown', 'markdown-append'] and file is None: namespace.cov_report[report_type] = 'coverage.md' if all(x in namespace.cov_report for x in ['markdown', 'markdown-append']): self._validate_markdown_dest_files(namespace.cov_report, parser) def _validate_markdown_dest_files(self, cov_report_options, parser): markdown_file = cov_report_options['markdown'] markdown_append_file = cov_report_options['markdown-append'] if markdown_file == markdown_append_file: error_message = f"markdown and markdown-append options cannot point to the same file: '{markdown_file}'." error_message += ' Please redirect one of them using :DEST (e.g. --cov-report=markdown:dest_file.md)' parser.error(error_message) def pytest_addoption(parser): """Add options to control coverage.""" group = parser.getgroup('cov', 'coverage reporting with distributed testing support') group.addoption( '--cov', action='append', default=[], metavar='SOURCE', nargs='?', const=True, dest='cov_source', help='Path or package name to measure during execution (multi-allowed). ' 'Use --cov= to not do any source filtering and record everything.', ) group.addoption( '--cov-reset', action='store_const', const=[], dest='cov_source', help='Reset cov sources accumulated in options so far. ', ) group.addoption( '--cov-report', action=StoreReport, default={}, metavar='TYPE', type=validate_report, help='Type of report to generate: term, term-missing, ' 'annotate, html, xml, json, markdown, markdown-append, lcov (multi-allowed). ' 'term, term-missing may be followed by ":skip-covered". ' 'annotate, html, xml, json, markdown, markdown-append and lcov may be followed by ":DEST" ' 'where DEST specifies the output location. ' 'Use --cov-report= to not generate any output.', ) group.addoption( '--cov-config', action='store', default='.coveragerc', metavar='PATH', help='Config file for coverage. Default: .coveragerc', ) group.addoption( '--no-cov-on-fail', action='store_true', default=False, help='Do not report coverage if test run fails. Default: False', ) group.addoption( '--no-cov', action='store_true', default=False, help='Disable coverage report completely (useful for debuggers). Default: False', ) group.addoption( '--cov-fail-under', action='store', metavar='MIN', type=validate_fail_under, help='Fail if the total coverage is less than MIN.', ) group.addoption( '--cov-append', action='store_true', default=False, help='Do not delete coverage but append to current. Default: False', ) group.addoption( '--cov-branch', action='store_true', default=None, help='Enable branch coverage.', ) group.addoption( '--cov-precision', type=int, default=None, help='Override the reporting precision.', ) group.addoption( '--cov-context', action='store', metavar='CONTEXT', type=validate_context, help='Dynamic contexts to use. "test" for now.', ) def _prepare_cov_source(cov_source): """ Prepare cov_source so that: --cov --cov=foobar is equivalent to --cov (cov_source=None) --cov=foo --cov=bar is equivalent to cov_source=['foo', 'bar'] """ return None if True in cov_source else [path for path in cov_source if path is not True] @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 for arg in args: arg = str(arg) if arg == '--no-cov': no_cov = True elif arg.startswith('--cov') and no_cov: options.no_cov_should_warn = True break if early_config.known_args_namespace.cov_source: plugin = CovPlugin(options, early_config.pluginmanager) early_config.pluginmanager.register(plugin, '_cov') class CovPlugin: """Use coverage package to produce code coverage reports. Delegates all work to a particular implementation based on whether this test process is centralised, a distributed master or a distributed worker. """ def __init__(self, options: argparse.Namespace, pluginmanager, start=True, no_cov_should_warn=False): """Creates a coverage pytest plugin. We read the rc file that coverage uses to get the data file name. This is needed since we give coverage through it's API the data file name. """ # Our implementation is unknown at this time. self.pid = None self.cov_controller = None self.cov_report = StringIO() self.cov_total = None self.failed = False self._started = False self._start_path = None self._disabled = False self.options = options self._wrote_heading = False is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no' if getattr(options, 'no_cov', False): self._disabled = True return if not self.options.cov_report: self.options.cov_report = ['term'] elif len(self.options.cov_report) == 1 and '' in self.options.cov_report: self.options.cov_report = {} self.options.cov_source = _prepare_cov_source(self.options.cov_source) # import engine lazily here to avoid importing # it for unit tests that don't need it from . import engine if is_dist and start: self.start(engine.DistMaster) elif start: self.start(engine.Central) # worker is started in pytest hook def start(self, controller_cls: type['CovController'], config=None, nodeid=None): if config is None: # fake config option for engine class Config: option = self.options config = Config() self.cov_controller = controller_cls(self.options, config, nodeid) self.cov_controller.start() self._started = True self._start_path = Path.cwd() cov_config = self.cov_controller.cov.config if self.options.cov_fail_under is None and hasattr(cov_config, 'fail_under'): self.options.cov_fail_under = cov_config.fail_under if self.options.cov_precision is None: self.options.cov_precision = getattr(cov_config, 'precision', 0) def _is_worker(self, session): return getattr(session.config, 'workerinput', None) is not None def pytest_sessionstart(self, session): """At session start determine our implementation and delegate to it.""" if self.options.no_cov: # Coverage can be disabled because it does not cooperate with debuggers well. self._disabled = True return # import engine lazily here to avoid importing # it for unit tests that don't need it from . import engine self.pid = os.getpid() if self._is_worker(session): nodeid = session.config.workerinput.get('workerid', session.nodeid) self.start(engine.DistWorker, session.config, nodeid) elif not self._started: self.start(engine.Central) if self.options.cov_context == 'test': session.config.pluginmanager.register(TestContextPlugin(self.cov_controller), '_cov_contexts') @pytest.hookimpl(optionalhook=True) def pytest_configure_node(self, node): """Delegate to our implementation. Mark this hook as optional in case xdist is not installed. """ if not self._disabled: self.cov_controller.configure_node(node) @pytest.hookimpl(optionalhook=True) def pytest_testnodedown(self, node, error): """Delegate to our implementation. Mark this hook as optional in case xdist is not installed. """ if not self._disabled: self.cov_controller.testnodedown(node, error) def _should_report(self): needed = self.options.cov_report or self.options.cov_fail_under return needed and not (self.failed and self.options.no_cov_on_fail) # we need to wrap pytest_runtestloop. by the time pytest_sessionfinish # runs, it's too late to set testsfailed @pytest.hookimpl(wrapper=True) def pytest_runtestloop(self, session): if self._disabled: return (yield) # we add default warning configuration to prevent certain warnings to bubble up as errors due to rigid filterwarnings configuration for _, message, category, _, _ in warnings.filters: if category is ResourceWarning and message in (COVERAGE_SQLITE_WARNING_RE, COVERAGE_SQLITE_WARNING_RE2): break else: warnings.filterwarnings('default', 'unclosed database in 0: self.write_heading(terminalreporter) failed = self.cov_total < self.options.cov_fail_under markup = {'red': True, 'bold': True} if failed else {'green': True} message = '{fail}Required test coverage of {required}% {reached}. Total coverage: {actual:.2f}%\n'.format( 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) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): if item.get_closest_marker('no_cover') or 'no_cover' in getattr(item, 'fixturenames', ()): self.cov_controller.pause() yield self.cov_controller.resume() else: yield class TestContextPlugin: cov_controller: 'CovController' def __init__(self, cov_controller): self.cov_controller = cov_controller def pytest_runtest_setup(self, item): self.switch_context(item, 'setup') def pytest_runtest_teardown(self, item): self.switch_context(item, 'teardown') def pytest_runtest_call(self, item): self.switch_context(item, 'run') def switch_context(self, item, when): if self.cov_controller.started: self.cov_controller.cov.switch_context(f'{item.nodeid}|{when}') @pytest.fixture def no_cover(): """A pytest fixture to disable coverage.""" @pytest.fixture def cov(request): """A pytest fixture to provide access to the underlying coverage object.""" # Check with hasplugin to avoid getplugin exception in older pytest. if request.config.pluginmanager.hasplugin('_cov'): plugin = request.config.pluginmanager.getplugin('_cov') if plugin.cov_controller: return plugin.cov_controller.cov return None def pytest_configure(config): config.addinivalue_line('markers', 'no_cover: disable coverage for this test.') pytest-cov-7.1.0/tests/000077500000000000000000000000001515757410100150045ustar00rootroot00000000000000pytest-cov-7.1.0/tests/conftest.py000066400000000000000000000001111515757410100171740ustar00rootroot00000000000000def pytest_configure(config): config.option.runpytest = 'subprocess' pytest-cov-7.1.0/tests/contextful.py000066400000000000000000000037361515757410100175620ustar00rootroot00000000000000# 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 def test_02(): assert 2 == 2 # r2 class OldStyleTests(unittest.TestCase): items: ClassVar = [] @classmethod def setUpClass(cls): cls.items.append('hello') # s3 @classmethod def tearDownClass(cls): cls.items.pop() # t4 def setUp(self): self.number = 1 # r3 r4 def tearDown(self): self.number = None # r3 r4 def test_03(self): 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 @pytest.fixture def some_data(): return [1, 2, 3] # s5 s6 def test_05(some_data): assert len(some_data) == 3 # r5 @pytest.fixture def more_data(some_data): return [2 * x for x in some_data] # s6 def test_06(some_data, more_data): assert len(some_data) == len(more_data) # r6 @pytest.fixture def expensive_data(): return list(range(10)) # s7 def test_07(expensive_data): assert len(expensive_data) == 10 # r7 def test_08(expensive_data): 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 def test_09(parametrized_number): assert parametrized_number > 0 # r9-1 r9-2 r9-3 def test_10(): assert 1 == 1 # r10 @pytest.mark.parametrize( ('x', 'ans'), [ (1, 101), (2, 202), ], ) def test_11(x, ans): assert 100 * x + x == ans # r11-1 r11-2 @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 @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 pytest-cov-7.1.0/tests/helper.py000066400000000000000000000000471515757410100166360ustar00rootroot00000000000000def do_stuff(): a = 1 return a pytest-cov-7.1.0/tests/test_pytest_cov.py000066400000000000000000001657041515757410100206310ustar00rootroot00000000000000import collections import glob import os import platform import re import sys from itertools import chain from pathlib import Path from types import SimpleNamespace import coverage import pytest from process_tests import TestProcess as _TestProcess from process_tests import dump_on_error from process_tests import wait_for_strings import pytest_cov.plugin max_worker_restart_0 = '--max-worker-restart=0' SCRIPT = """ import sys, helper def pytest_generate_tests(metafunc): for i in [10]: metafunc.parametrize('p', range(i)) def test_foo(p): x = True helper.do_stuff() # get some coverage in some other completely different location if sys.version_info[0] > 5: assert False """ SCRIPT2 = """ # def test_bar(): x = True assert x """ COVERAGERC_SOURCE = """\ [run] source = . """ SCRIPT_CHILD = """ import sys idx = int(sys.argv[1]) if idx == 0: 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 = """ import os import subprocess import sys def pytest_generate_tests(metafunc): for i in [2]: metafunc.parametrize('idx', range(i)) def test_foo(idx): out, err = subprocess.Popen( [sys.executable, os.path.join(os.path.dirname(__file__), 'child_script.py'), str(idx)], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() # 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 subprocess import sys import os def pytest_generate_tests(metafunc): for i in [2]: metafunc.parametrize('idx', range(i)) def test_foo(idx): os.mkdir("foobar") os.chdir("foobar") subprocess.check_call([ sys.executable, os.path.join(os.path.dirname(__file__), 'child_script.py'), str(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 = """ import subprocess import sys import os def pytest_generate_tests(metafunc): for i in [2]: if metafunc.function is test_foo: metafunc.parametrize('idx', range(i)) def test_foo(idx): os.mkdir("foobar") os.chdir("foobar") subprocess.check_call([ sys.executable, '-c', 'import sys; sys.argv = ["", str(%s)]; import child_script' % 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 = """ import coverage def test_foo(cov): assert isinstance(cov, coverage.Coverage) """ 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' XML_REPORT_NAME = 'cov.xml' JSON_REPORT_NAME = 'cov.json' MARKDOWN_REPORT_NAME = 'cov.md' MARKDOWN_APPEND_REPORT_NAME = 'cov-append.md' 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"')), pytest.param('-n 2', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')), pytest.param('-n 3', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')), ], ids=['nodist', '1xdist', '2xdist', '3xdist'], ) @pytest.fixture(scope='session', autouse=True) def adjust_sys_path(): """Adjust PYTHONPATH during tests to make "helper" importable in SCRIPT.""" orig_path = os.environ.get('PYTHONPATH', None) new_path = str(Path(__file__).parent) if orig_path is not None: new_path = os.pathsep.join([new_path, orig_path]) os.environ['PYTHONPATH'] = new_path yield if orig_path is None: del os.environ['PYTHONPATH'] else: 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'], ) def prop(request): return SimpleNamespace( code=SCRIPT, code2=SCRIPT2, conf=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(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script, *prop.args) result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_central* {prop.result} *', '*10 passed*']) assert result.ret == 0 def test_annotate(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=annotate', script) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', 'Coverage annotated source written next to source', '*10 passed*', ] ) assert result.ret == 0 def test_annotate_output_dir(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=annotate:' + DEST_DIR, script) result.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 result.ret == 0 def test_html(testdir): script = testdir.makepyfile(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*', ] ) dest_dir = testdir.tmpdir.join('htmlcov') assert dest_dir.check(dir=True) assert dest_dir.join('index.html').check() assert result.ret == 0 def test_html_output_dir(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html:' + DEST_DIR, script) result.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 result.ret == 0 def test_term_report_does_not_interact_with_html_output(testdir): script = testdir.makepyfile(test_funcarg=SCRIPT_FUNCARG) result = testdir.runpytest( '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing:skip-covered', '--cov-report=html:' + DEST_DIR, script ) result.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) expected = [dest_dir.join('index.html'), dest_dir.join('test_funcarg_py.html')] expected.insert(0, dest_dir.join('function_index.html')) expected.insert(0, dest_dir.join('class_index.html')) assert sorted(dest_dir.visit('**/*.html')) == expected assert dest_dir.join('index.html').check() assert result.ret == 0 def test_html_configured_output_dir(testdir): script = testdir.makepyfile(SCRIPT) testdir.tmpdir.join('.coveragerc').write( """ [html] directory = somewhere """ ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html', script) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir somewhere', '*10 passed*', ] ) dest_dir = testdir.tmpdir.join('somewhere') assert dest_dir.check(dir=True) assert dest_dir.join('index.html').check() assert result.ret == 0 def test_xml_output_dir(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=xml:' + XML_REPORT_NAME, script) result.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', f'--cov={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 def test_markdown_output_dir(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, script) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', 'Coverage Markdown information written to file ' + MARKDOWN_REPORT_NAME, '*10 passed*', ] ) assert testdir.tmpdir.join(MARKDOWN_REPORT_NAME).check() assert result.ret == 0 def test_markdown_append_output_dir(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', 'Coverage Markdown information appended to file ' + MARKDOWN_APPEND_REPORT_NAME, '*10 passed*', ] ) assert testdir.tmpdir.join(MARKDOWN_APPEND_REPORT_NAME).check() assert result.ret == 0 def test_markdown_and_markdown_append_work_together(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest( '-v', f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, '--cov-report=markdown-append:' + MARKDOWN_APPEND_REPORT_NAME, script, ) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', 'Coverage Markdown information written to file ' + MARKDOWN_REPORT_NAME, 'Coverage Markdown information appended to file ' + MARKDOWN_APPEND_REPORT_NAME, '*10 passed*', ] ) assert testdir.tmpdir.join(MARKDOWN_APPEND_REPORT_NAME).check() assert result.ret == 0 def test_markdown_and_markdown_append_pointing_to_same_file_throws_error(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest( '-v', f'--cov={script.dirpath()}', '--cov-report=markdown:' + MARKDOWN_REPORT_NAME, '--cov-report=markdown-append:' + MARKDOWN_REPORT_NAME, script, ) result.stderr.fnmatch_lines(['* error: markdown and markdown-append options cannot point to the same file*']) assert result.ret == 4 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 def test_term_output_dir(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term:' + DEST_DIR, script) result.stderr.fnmatch_lines( [ f'*argument --cov-report: output specifier not supported for: "term:{DEST_DIR}"*', ] ) assert result.ret != 0 def test_term_missing_output_dir(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing:' + DEST_DIR, script) result.stderr.fnmatch_lines( [ f'*argument --cov-report: output specifier not supported for: "term-missing:{DEST_DIR}"*', ] ) assert result.ret != 0 def test_cov_min_100(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=100', script) assert result.ret != 0 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', 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: *%']) def test_cov_reporting_bug_641(testdir, monkeypatch): testdir.tmpdir.mkdir('src').join('foo.py').write('') testdir.tmpdir.mkdir('tests').join('test_foo.py').write(""" import foo def test_foo(): pass """) testdir.tmpdir.join('.coveragerc').write(""" [run] relative_files = True source = src, tests """) monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([os.environ.get('PYTHONPATH', ''), 'src'])) result = testdir.runpytest('-v', '--cov=src', '--cov-report=xml', '--cov-fail-under=100') assert result.ret == 0 result.stdout.fnmatch_lines(['Required test coverage of 100% reached. Total coverage: *%']) result = testdir.runpytest('-v', '--cov=src', '--cov-report=', '--cov-fail-under=100') assert result.ret == 0 result.stdout.fnmatch_lines(['Required test coverage of 100% reached. Total coverage: *%']) def test_cov_min_float_value(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=88.88', script) assert result.ret == 0 result.stdout.fnmatch_lines(['Required test coverage of 88.88% reached. Total coverage: 88.89%']) def test_cov_min_float_value_not_reached(testdir): script = testdir.makepyfile(SCRIPT) testdir.tmpdir.join('.coveragerc').write(""" [report] precision = 3 """) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=88.89', script) assert result.ret == 1 result.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%']) def test_cov_min_float_value_not_reached_cli(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest( '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-precision=3', '--cov-fail-under=88.89', script ) assert result.ret == 1 result.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%']) def test_cov_precision(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-precision=6', script) assert result.ret == 0 result.stdout.fnmatch_lines( [ 'Name Stmts Miss Cover Missing', '----------------------------------------------------------', 'test_cov_precision.py 9 1 88.888889% 11', '----------------------------------------------------------', 'TOTAL 9 1 88.888889%', ] ) def test_cov_precision_from_config(testdir): script = testdir.makepyfile(SCRIPT) testdir.tmpdir.join('pyproject.toml').write(""" [tool.coverage.report] precision = 6""") result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines( [ 'Name Stmts Miss Cover Missing', '----------------------------------------------------------------------', 'test_cov_precision_from_config.py 9 1 88.888889% 11', '----------------------------------------------------------------------', 'TOTAL 9 1 88.888889%', ] ) def test_cov_min_no_report(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=', '--cov-fail-under=50', script) assert result.ret == 0 result.stdout.fnmatch_lines(['Required test coverage of 50% reached. Total coverage: *%']) def test_central_nonspecific(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', script, *prop.args) result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) # multi-module coverage report assert any(line.startswith('TOTAL ') for line in result.stdout.lines) assert result.ret == 0 def test_cov_min_from_coveragerc(testdir): script = testdir.makepyfile(SCRIPT) testdir.tmpdir.join('.coveragerc').write( """ [report] fail_under = 100 """ ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret != 0 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.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(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( """ from mod import * """ ) testdir.tmpdir.join('setup.cfg').write( f""" [coverage:paths] source = src aliased [coverage:run] source = mod {prop.conf} """ ) monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([os.environ.get('PYTHONPATH', ''), 'aliased'])) result = testdir.runpytest('-v', '-s', '--cov', '--cov-report=term-missing', script, *opts.split() + prop.args) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', f'src[\\/]mod* {prop.result} *', '*10 passed*', ] ) assert result.ret == 0 @xdist_params def test_borken_cwd(pytester, testdir, monkeypatch, opts): testdir.makepyfile( mod=""" def foobar(a, b): return a + b """ ) script = testdir.makepyfile( """ import os import tempfile import pytest import mod @pytest.fixture def bad(): path = tempfile.mkdtemp('test_borken_cwd') os.chdir(path) yield try: os.rmdir(path) except OSError: pass 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*', ] ) assert result.ret == 0 @pytest.mark.skipif(sys.platform == 'win32', reason='No redis server on Windows') @pytest.mark.skipif(sys.platform == 'darwin', reason='No redis server on OSX') def test_celery(pytester): pytester.makepyfile( small_celery=""" import os from celery import Celery from celery.contrib.testing import worker from testcontainers.redis import RedisContainer import pytest app = Celery("tasks", broker="redis://localhost:6379/0", backend="redis://localhost:6379/0") @app.task def add(x, y): return x + y @pytest.fixture(scope="session") def redis_container(): with RedisContainer() as container: yield container @pytest.fixture def celery_app(redis_container): host = redis_container.get_container_host_ip() port = redis_container.get_exposed_port(6379) redis_url = f"redis://{host}:{port}/0" app.conf.update(broker_url=redis_url, result_backend=redis_url) return app @pytest.fixture def celery_worker(celery_app): with worker.start_worker( celery_app, pool="prefork", perform_ping_check=False, ): yield print('CELERY SHUTDOWN') print('CELERY SHUTDOWN DONE') print(os.listdir()) def test_add_task(celery_worker): result = add.delay(4, 4) assert result.get() == 8 """ ) pytester.makepyprojecttoml( """ [tool.coverage.run] patch = ["subprocess", "_exit"] """ ) result = pytester.runpytest('-vv', '-s', '--cov', '--cov-report=term-missing', 'small_celery.py') result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', 'small_celery* 100%*', ] ) assert result.ret == 0 def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): src = testdir.mkdir('src') src.join('parent_script.py').write(SCRIPT_PARENT) src.join('child_script.py').write(SCRIPT_CHILD) aliased = testdir.mkdir('aliased') parent_script = aliased.join('parent_script.py') parent_script.write(SCRIPT_PARENT) aliased.join('child_script.py').write(SCRIPT_CHILD) testdir.tmpdir.join('.coveragerc').write( """ [paths] source = src aliased [run] source = parent_script child_script patch = subprocess """ ) monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([os.environ.get('PYTHONPATH', ''), 'aliased'])) result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', parent_script) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', f'src[\\/]child_script* {CHILD_SCRIPT_RESULT}*', f'src[\\/]parent_script* {PARENT_SCRIPT_RESULT}*', ] ) assert result.ret == 0 def test_show_missing_coveragerc(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write( f""" [run] source = . {prop.conf} [report] show_missing = true """ ) 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( """ def test_fail(): assert False """ ) 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(pytester, testdir, monkeypatch): script = testdir.makepyfile(SCRIPT) testdir.makeini( """ [pytest] addopts=--no-cov """ ) result = testdir.runpytest('-vvv', f'--cov={script.dirpath()}', '--cov-report=term-missing', '-rw', script) result.stdout.fnmatch_lines_random( [ 'WARNING: Coverage disabled via --no-cov switch!', '*Coverage disabled via --no-cov switch!', ] ) def test_cov_and_failure_report_on_fail(testdir): script = testdir.makepyfile( SCRIPT + """ def test_fail(p): assert False """ ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-fail-under=100', '--cov-report=html', script) result.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( """ import pytest @pytest.mark.parametrize("foo", range(1000)) def test_foo(foo): """ + '\n'.join( f""" if foo == {i}: assert True """ for i in range(1000) ) ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '-n', '5', '-s', script) result.stdout.fnmatch_lines(['test_dist_combine_racecondition* 0 * 100%*', '*1000 passed*']) for line in chain(result.stdout.lines, result.stderr.lines): assert 'The following workers failed to return coverage data' not in line assert 'INTERNALERROR' not in line assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_collocated(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest( '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--dist=load', '--tx=2*popen', max_worker_restart_0, script, *prop.args, ) result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_not_collocated(pytester, testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') dir2 = testdir.mkdir('dir2') testdir.tmpdir.join('.coveragerc').write( f""" [run] {prop.conf} [paths] source = . dir1 dir2""" ) result = testdir.runpytest( '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--dist=load', f'--tx=popen//chdir={dir1}', f'--tx=popen//chdir={dir2}', f'--rsyncdir={script.basename}', '--rsyncdir=.coveragerc', max_worker_restart_0, '-s', script, *prop.args, ) result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') dir2 = testdir.mkdir('dir2') testdir.tmpdir.join('.coveragerc').write( f""" [run] {prop.conf} source = {script.dirpath()} [paths] source = . dir1 dir2""" ) result = testdir.runpytest( '-v', '--cov', '--cov-report=term-missing', '--dist=load', f'--tx=popen//chdir={dir1}', f'--tx=popen//chdir={dir2}', f'--rsyncdir={script.basename}', '--rsyncdir=.coveragerc', max_worker_restart_0, '-s', script, *prop.args, ) result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 def test_central_subprocess(testdir): testdir.makepyprojecttoml( """ [tool.coverage.run] patch = ["subprocess"] """ ) scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') result = testdir.runpytest('-v', f'--cov={scripts.dirpath()}', '--cov-report=term-missing', parent_script) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] ) assert result.ret == 0 def test_central_subprocess_change_cwd(testdir): scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT_CHANGE_CWD, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') testdir.makefile( '', coveragerc=""" [run] branch = true patch = subprocess """, ) 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(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=""" [run] patch = subprocess """, ) monkeypatch.setitem(os.environ, 'PYTHONPATH', str(stuff)) result = testdir.runpytest( '-vv', '-s', '--cov=child_script', '--cov-config=coveragerc', '--cov-report=term-missing', '--cov-branch', parent_script ) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', f'*child_script* {CHILD_SCRIPT_RESULT}*', ] ) assert result.ret == 0 def test_central_subprocess_no_subscript(testdir): script = testdir.makepyfile( """ import subprocess, sys def test_foo(): subprocess.check_call([sys.executable, '-c', 'print("Hello World")']) """ ) testdir.makefile( '', coveragerc=""" [run] patch = subprocess """, ) 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): testdir.makepyprojecttoml( """ [tool.coverage.run] patch = ["subprocess"] """ ) scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') result = testdir.runpytest( '-v', f'--cov={scripts.dirpath()}', '--cov-report=term-missing', '--dist=load', '--tx=2*popen', max_worker_restart_0, parent_script ) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] ) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') child_script = scripts.dirpath().join('child_script.py') dir1 = tmpdir.mkdir('dir1') dir2 = tmpdir.mkdir('dir2') testdir.tmpdir.join('.coveragerc').write( f""" [run] patch = subprocess [paths] source = {scripts.dirpath()} */dir1 */dir2 """ ) result = testdir.runpytest( '-v', f'--cov={scripts.dirpath()}', '--dist=load', f'--tx=popen//chdir={dir1}', f'--tx=popen//chdir={dir2}', f'--rsyncdir={child_script}', f'--rsyncdir={parent_script}', '--rsyncdir=.coveragerc', max_worker_restart_0, parent_script, ) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] ) assert result.ret == 0 def test_invalid_coverage_source(testdir): script = testdir.makepyfile(SCRIPT) 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(['*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] assert not matching_lines def test_funcarg(testdir): script = testdir.makepyfile(SCRIPT_FUNCARG) 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*']) 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.platform == 'win32', reason="SIGTERM isn't really supported on Windows") def test_cleanup_on_sigterm(testdir): testdir.makepyprojecttoml( """ [tool.coverage.run] patch = ["subprocess", "_exit"] """ ) script = testdir.makepyfile( ''' import os, signal, subprocess, sys, time def cleanup(num, frame): print("num == signal.SIGTERM => %s" % (num == signal.SIGTERM)) raise Exception() def test_run(): proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) time.sleep(1) proc.terminate() stdout, stderr = proc.communicate() assert not stderr assert stdout == b"""num == signal.SIGTERM => True captured Exception() """ assert proc.returncode == 0 if __name__ == "__main__": signal.signal(signal.SIGTERM, cleanup) try: time.sleep(10) except BaseException as exc: print("captured %r" % exc) ''' ) result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) result.stdout.fnmatch_lines( [ '*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 100%', '*1 passed*', ] ) assert result.ret == 0 @pytest.mark.skipif(sys.platform != 'win32', reason='SIGBREAK is Windows only') @pytest.mark.parametrize( 'setup', [ ( 'signal.signal(signal.SIGBREAK, signal.SIG_DFL)', '*% 4, 2*-28' if platform.python_implementation() == 'PyPy' else '62% 4, 23-28', ), ('signal.signal(signal.SIGBREAK, cleanup)', '100%'), ('', '*% 4, 2*-28' if platform.python_implementation() == 'PyPy' else '67% 4, 25-28'), ], ) def test_cleanup_on_sigterm_sig_break(pytester, testdir, setup): # worth a read: https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/ testdir.makepyprojecttoml( """ [tool.coverage.run] patch = ["subprocess"] """ ) script = testdir.makepyfile( """ import os, signal, subprocess, sys, time def cleanup(num, frame): raise Exception() def test_run(): proc = subprocess.Popen( [sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, shell=True ) time.sleep(1) proc.send_signal(signal.CTRL_BREAK_EVENT) stdout, stderr = proc.communicate() assert not stderr assert stdout in [ b"^C", b"", b"captured Exception()\\r\\n", b"captured IOError(4, 'Interrupted function call')\\n"] if __name__ == "__main__": """ + setup[0] + """ try: time.sleep(10) except BaseException as exc: print("captured %r" % exc) """ ) result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) result.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") def test_cleanup_on_sigterm_sig_dfl(testdir): testdir.makepyprojecttoml( """ [tool.coverage.run] patch = ["subprocess"] sigterm = true """ ) script = testdir.makepyfile( """ import os, signal, subprocess, sys, time def test_run(): proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) time.sleep(1) proc.terminate() stdout, stderr = proc.communicate() assert not stderr print([stdout, stderr]) assert stdout == b"" assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] if __name__ == "__main__": foobar = 123 try: time.sleep(10) except BaseException as exc: print("captured %r" % exc) """ ) result = testdir.runpytest( '-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-report=html', script ) result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% * 18-19', '*1 passed*']) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32"', reason='SIGINT is subtly broken on Windows') def test_cleanup_on_sigterm_sig_dfl_sigint(testdir): testdir.makepyprojecttoml( """ [tool.coverage.run] patch = ["subprocess"] sigterm = true """ ) script = testdir.makepyfile( ''' import os, signal, subprocess, sys, time def test_run(): proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) time.sleep(1) proc.send_signal(signal.SIGINT) stdout, stderr = proc.communicate() assert not stderr assert stdout == b"""captured KeyboardInterrupt() """ assert proc.returncode == 0 if __name__ == "__main__": try: time.sleep(10) except BaseException as exc: print("captured %r" % exc) ''' ) result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 100%', '*1 passed*']) assert result.ret == 0 MODULE = """ def func(): return 1 """ CONFTEST = """ import mod mod.func() """ BASIC_TEST = """ def test_basic(): x = True assert x """ CONF_RESULT = 'mod* 2 * 100%*' def test_cover_conftest(testdir): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) script = testdir.makepyfile(BASIC_TEST) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines([CONF_RESULT]) def test_filterwarnings_error(testdir): testdir.makeini(r""" [pytest] filterwarnings = error """) testdir.makepyfile(mod=MODULE) testdir.makepyfile(plug=CONFTEST) script = testdir.makepyfile(BASIC_TEST) result = testdir.runpytest('-v', '--cov=mod', '--cov-report=xml', '--cov-report=term-missing', '-p', 'plug', script) assert result.ret == 0 result.stdout.fnmatch_lines(['* 1 passed *']) result.stderr.fnmatch_lines( [ '* (module-not-measured)*', '* (no-data-collected)*', '* CovReportWarning: Failed to generate report: No data to report.', ] ) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_cover_looponfail(testdir, monkeypatch): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) script = testdir.makepyfile(BASIC_TEST) def mock_run(*args, **kwargs): return _TestProcess(*map(str, args)) monkeypatch.setattr(testdir, 'run', mock_run) assert testdir.run is mock_run if hasattr(testdir, '_pytester'): monkeypatch.setattr(testdir._pytester, 'run', mock_run) assert testdir._pytester.run is mock_run with testdir.runpytest('-v', f'--cov={script.dirpath()}', '--looponfail', script) as process: with dump_on_error(process.read): wait_for_strings(process.read, 30, 'Stmts Miss Cover') # 30 seconds @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_cover_conftest_dist(testdir): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) script = testdir.makepyfile(BASIC_TEST) result = testdir.runpytest( '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--dist=load', '--tx=2*popen', max_worker_restart_0, script ) assert result.ret == 0 result.stdout.fnmatch_lines([CONF_RESULT]) def test_no_cover_marker(testdir): testdir.makepyfile(mod=MODULE) script = testdir.makepyfile( """ import pytest import mod import subprocess import sys @pytest.mark.no_cover def test_basic(): mod.func() subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) """ ) result = testdir.runpytest('-v', '-ra', '--strict', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) 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( """ import mod import subprocess import sys def test_basic(no_cover): mod.func() subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) """ ) result = testdir.runpytest('-v', '-ra', '--strict', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines(['mod* 2 * 1 * 50% * 2']) 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 = """ def func(): raise NotImplementedError def test_basic(): x = True assert x """ EXCLUDED_RESULT = '4 * 100%*' def test_coveragerc(testdir): testdir.makefile('', coveragerc=COVERAGERC) script = testdir.makepyfile(EXCLUDED_TEST) result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines([f'test_coveragerc* {EXCLUDED_RESULT}']) def test_pyproject_toml(testdir): testdir.makefile('.toml', pyproject=PYPROJECTTOML) script = testdir.makepyfile(EXCLUDED_TEST) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines([f'test_pyproject_toml* {EXCLUDED_RESULT}']) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_coveragerc_dist(testdir): testdir.makefile('', coveragerc=COVERAGERC) script = testdir.makepyfile(EXCLUDED_TEST) result = testdir.runpytest( '-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-report=term-missing', '-n', '2', max_worker_restart_0, script ) assert result.ret == 0 result.stdout.fnmatch_lines([f'test_coveragerc_dist* {EXCLUDED_RESULT}']) SKIP_COVERED_COVERAGERC = """ [report] skip_covered = True """ SKIP_COVERED_TEST = """ def func(): return "full coverage" 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(pytester, testdir, report_option): testdir.makefile('', coveragerc=SKIP_COVERED_COVERAGERC) script = testdir.makepyfile(SKIP_COVERED_TEST) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', f'--cov-report={report_option}', script) assert result.ret == 0 result.stdout.fnmatch_lines([SKIP_COVERED_RESULT]) def test_skip_covered_coveragerc_config(testdir): testdir.makefile('', coveragerc=SKIP_COVERED_COVERAGERC) script = testdir.makepyfile(SKIP_COVERED_TEST) result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', script) assert result.ret == 0 result.stdout.fnmatch_lines([SKIP_COVERED_RESULT]) CLEAR_ENVIRON_TEST = """ import os def test_basic(): os.environ.clear() """ def test_clear_environ(testdir): script = testdir.makepyfile(CLEAR_ENVIRON_TEST) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 SCRIPT_SIMPLE = """ def test_foo(): assert 1 == 1 x = True assert x """ SCRIPT_SIMPLE_RESULT = '4 * 100%' @pytest.mark.skipif('sys.platform == "win32"') @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.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 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) def test_default_output_setting(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) result.stdout.fnmatch_lines(['*coverage*']) assert result.ret == 0 def test_disabled_output(testdir): script = testdir.makepyfile(SCRIPT) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=', script) 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, '') assert 'coverage' not in stdout assert result.ret == 0 def test_coverage_file(testdir): script = testdir.makepyfile(SCRIPT) data_file_name = 'covdata' os.environ['COVERAGE_FILE'] = data_file_name try: 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() finally: os.environ.pop('COVERAGE_FILE') def test_external_data_file(testdir): script = testdir.makepyfile(SCRIPT) testdir.tmpdir.join('.coveragerc').write( """ [run] data_file = {} """.format(testdir.tmpdir.join('some/special/place/coverage-data').ensure()) ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) # noqa: PTH207 @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( """ [run] parallel = true data_file = {} """.format(testdir.tmpdir.join('some/special/place/coverage-data').ensure()) ) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '-n', '1', max_worker_restart_0, script) assert result.ret == 0 assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) # noqa: PTH207 @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( """ import target def test_foobar(): assert target.x == 123 """ ) 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() assert 'module-not-imported' not in result.stdout.str() assert result.ret == 0 def test_external_data_file_negative(testdir): script = testdir.makepyfile(SCRIPT) testdir.tmpdir.join('.coveragerc').write('') result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) # noqa: PTH207 @xdist_params def test_append_coverage(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) result.stdout.fnmatch_lines( [ f'test_1* {prop.result}*', ] ) script2 = testdir.makepyfile(test_2=prop.code2) result = testdir.runpytest('-v', '--cov-append', f'--cov={script2.dirpath()}', script2, *opts.split() + prop.args) result.stdout.fnmatch_lines( [ f'test_1* {prop.result}*', f'test_2* {prop.result2}*', ] ) @xdist_params def test_coverage_plugin(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.makepyfile( coverageplugin=""" import coverage class ExamplePlugin(coverage.CoveragePlugin): pass def coverage_init(reg, options): reg.add_file_tracer(ExamplePlugin()) """ ) testdir.makepyprojecttoml(f""" [tool.coverage.run] plugins = ["coverageplugin"] concurrency = ["thread", "multiprocessing"] {prop.conf} """) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) result.stdout.fnmatch_lines( [ f'test_1* {prop.result}*', ] ) @xdist_params def test_dynamic_context(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.makepyprojecttoml(f""" [tool.coverage.run] dynamic_context = "test_function" {prop.conf} """) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) if opts: result.stderr.fnmatch_lines(['pytest_cov.DistCovError: Detected dynamic_context=test_function*']) else: result.stdout.fnmatch_lines( [ '* CentralCovContextWarning: Detected dynamic_context=test_function*', f'test_1* {prop.result}*', ] ) @xdist_params def test_simple(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.makepyprojecttoml(f""" [tool.coverage.run] {prop.conf} """) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) result.stdout.fnmatch_lines( [ f'test_1* {prop.result}*', ] ) @xdist_params def test_do_not_append_coverage(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) result.stdout.fnmatch_lines( [ f'test_1* {prop.result}*', ] ) script2 = testdir.makepyfile(test_2=prop.code2) result = testdir.runpytest('-v', f'--cov={script2.dirpath()}', script2, *opts.split() + prop.args) result.stdout.fnmatch_lines( [ 'test_1* 0%', f'test_2* {prop.result2}*', ] ) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_append_coverage_subprocess(testdir): testdir.makepyprojecttoml(""" [tool.coverage.run] patch = ["subprocess"] """) scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') 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 def test_double_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', f'--cov={script.dirpath()}', script) result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 def test_double_cov2(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', '--cov', script) result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) 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) 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 def find_labels(text, pattern): all_labels = collections.defaultdict(set) lines = text.splitlines() for lineno, line in enumerate(lines, start=1): labels = re.findall(pattern, line) for label in labels: all_labels[label].add(lineno) return all_labels # The contexts and their labels in contextful.py EXPECTED_CONTEXTS = { '': 'c0', 'test_contexts.py::test_01|run': 'r1', 'test_contexts.py::test_02|run': 'r2', 'test_contexts.py::OldStyleTests::test_03|setup': 's3', 'test_contexts.py::OldStyleTests::test_03|run': 'r3', 'test_contexts.py::OldStyleTests::test_04|run': 'r4', 'test_contexts.py::OldStyleTests::test_04|teardown': 't4', 'test_contexts.py::test_05|setup': 's5', 'test_contexts.py::test_05|run': 'r5', 'test_contexts.py::test_06|setup': 's6', 'test_contexts.py::test_06|run': 'r6', 'test_contexts.py::test_07|setup': 's7', 'test_contexts.py::test_07|run': 'r7', 'test_contexts.py::test_08|run': 'r8', 'test_contexts.py::test_08|setup': 's7', 'test_contexts.py::test_09[1]|setup': 's9-1', 'test_contexts.py::test_09[1]|run': 'r9-1', 'test_contexts.py::test_09[2]|setup': 's9-2', 'test_contexts.py::test_09[2]|run': 'r9-2', 'test_contexts.py::test_09[3]|setup': 's9-3', 'test_contexts.py::test_09[3]|run': 'r9-3', 'test_contexts.py::test_10|run': 'r10', 'test_contexts.py::test_11[1-101]|run': 'r11-1', 'test_contexts.py::test_11[2-202]|run': 'r11-2', 'test_contexts.py::test_12[one]|run': 'r12-1', 'test_contexts.py::test_12[two]|run': 'r12-2', 'test_contexts.py::test_13[3-1]|run': 'r13-1', 'test_contexts.py::test_13[3-2]|run': 'r13-2', 'test_contexts.py::test_13[4-1]|run': 'r13-3', 'test_contexts.py::test_13[4-2]|run': 'r13-4', } @xdist_params def test_contexts(pytester, testdir, opts): with Path(__file__).parent.joinpath('contextful.py').open() as f: contextful_tests = f.read() script = testdir.makepyfile(contextful_tests) testdir.tmpdir.join('.coveragerc').write("""[run] core = ctrace""") result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-context=test', script, *opts.split()) assert result.ret == 0 result.stdout.fnmatch_lines( [ 'test_contexts* 100%*', ] ) data = coverage.CoverageData('.coverage') data.read() assert data.measured_contexts() == set(EXPECTED_CONTEXTS) measured = data.measured_files() assert len(measured) == 1 test_context_path = next(iter(measured)) assert test_context_path.lower() == os.path.abspath('test_contexts.py').lower() # noqa: PTH100 line_data = find_labels(contextful_tests, r'[crst]\d+(?:-\d+)?') for context, label in EXPECTED_CONTEXTS.items(): if context == '': continue data.set_query_context(context) actual = set(data.lines(test_context_path)) assert line_data[label] == actual, f'Wrong lines for context {context!r}' def test_contexts_no_cover(testdir): script = testdir.makepyfile(""" import pytest def foobar(): return 1 def test_with_coverage(): foobar() @pytest.mark.no_cover() def test_without_coverage(): foobar() """) result = testdir.runpytest( '-v', '--cov-context=test', '--cov=test_contexts_no_cover', script, ) result.stdout.fnmatch_lines( [ 'test_contexts_no_cover.py 8 1 88%', 'TOTAL 8 1 88%', ] ) assert result.stderr.lines == [] assert result.ret == 0 def test_issue_417(testdir): # https://github.com/pytest-dev/pytest-cov/issues/417 whatever = testdir.maketxtfile(whatever='') testdir.inline_genitems(whatever) pytest-cov-7.1.0/tox.ini000066400000000000000000000054421515757410100151620ustar00rootroot00000000000000[testenv:bootstrap] deps = jinja2 tox skip_install = true commands = python ci/bootstrap.py --no-env passenv = * ; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments [tox] isolated_build = true envlist = clean, check, docs, {py310,py311,py312,py313,py314,pypy310,pypy311}-{pytest90}-{xdist38}-{coverage713}, {py39,pypy39}-{pytest80}-{xdist38}-{coverage710}, report ignore_basepython_conflict = true [testenv] extras = testing setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes # Use env vars for (optional) pinning of deps. pytest62: _DEP_PYTEST=pytest==6.2.5 pytest80: _DEP_PYTEST=pytest==8.0.2 pytest81: _DEP_PYTEST=pytest==8.1.1 pytest82: _DEP_PYTEST=pytest==8.2.2 pytest83: _DEP_PYTEST=pytest==8.3.5 pytest84: _DEP_PYTEST=pytest==8.4.2 pytest90: _DEP_PYTEST=pytest==9.0.2 xdist32: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 xdist33: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 xdist34: _DEP_PYTESTXDIST=pytest-xdist==3.4.0 xdist35: _DEP_PYTESTXDIST=pytest-xdist==3.5.0 xdist36: _DEP_PYTESTXDIST=pytest-xdist==3.6.1 xdist37: _DEP_PYTESTXDIST=pytest-xdist==3.7.0 xdist38: _DEP_PYTESTXDIST=pytest-xdist==3.8.0 xdistdev: _DEP_PYTESTXDIST=git+https://github.com/pytest-dev/pytest-xdist.git#egg=pytest-xdist coverage72: _DEP_COVERAGE=coverage==7.2.7 coverage73: _DEP_COVERAGE=coverage==7.3.4 coverage74: _DEP_COVERAGE=coverage==7.4.4 coverage75: _DEP_COVERAGE=coverage==7.5.4 coverage76: _DEP_COVERAGE=coverage==7.6.12 coverage77: _DEP_COVERAGE=coverage==7.7.1 coverage78: _DEP_COVERAGE=coverage==7.8.2 coverage79: _DEP_COVERAGE=coverage==7.9.2 coverage710: _DEP_COVERAGE=coverage==7.10.7 coverage711: _DEP_COVERAGE=coverage==7.11.3 coverage712: _DEP_COVERAGE=coverage==7.12.0 coverage713: _DEP_COVERAGE=coverage==7.13.5 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv = * deps = {env:_DEP_PYTEST:pytest} {env:_DEP_PYTESTXDIST:pytest-xdist} {env:_DEP_COVERAGE:coverage} celery[redis] testcontainers[redis] pip_pre = true commands = {posargs:pytest -vv} [testenv:check] deps = docutils twine uv prek skip_install = true commands = uv build --sdist --wheel twine check --strict dist/* prek 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:report] deps = coverage skip_install = true commands = coverage report coverage html [testenv:clean] commands = coverage erase skip_install = true deps = coverage