diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 38f20c66..cca1163f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.1.0 +current_version = 5.0.0 commit = True tag = True @@ -22,3 +22,7 @@ replace = version = release = '{new_version}' [bumpversion:file:src/pytest_cov/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' + +[bumpversion:file:.cookiecutterrc] +search = version: {current_version} +replace = version: {new_version} diff --git a/.cookiecutterrc b/.cookiecutterrc index 49e9880e..18487fef 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -1,55 +1,47 @@ # Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) default_context: - allow_tests_inside_package: no - appveyor: no + allow_tests_inside_package: 'no' c_extension_function: '-' c_extension_module: '-' - c_extension_optional: no - c_extension_support: no - c_extension_test_pypi: no - c_extension_test_pypi_username: '-' - codacy: no + c_extension_optional: 'no' + c_extension_support: 'no' + codacy: 'no' codacy_projectid: '[Get ID from https://app.codacy.com/app/ionelmc/pytest-cov/settings]' - codeclimate: no - codecov: no - command_line_interface: no + codeclimate: 'no' + codecov: 'no' + command_line_interface: 'no' command_line_interface_bin_name: '-' - coveralls: no + coveralls: 'no' distribution_name: pytest-cov email: contact@ionelmc.ro + formatter_quote_style: single full_name: Ionel Cristian Mărieș - github_actions: yes - legacy_python: yes + github_actions: 'yes' + github_actions_osx: 'yes' + github_actions_windows: 'yes' license: MIT license - linter: flake8 package_name: pytest_cov - pre_commit: yes + pre_commit: 'yes' project_name: pytest-cov project_short_description: This plugin produces coverage reports. It supports centralised testing and distributed testing in both load and each modes. It also supports coverage of subprocesses. - pypi_badge: yes - pypi_disable_upload: no - release_date: '2021-10-04' + pypi_badge: 'yes' + pypi_disable_upload: 'no' + release_date: '2023-05-24' repo_hosting: github.com repo_hosting_domain: github.com repo_main_branch: master repo_name: pytest-cov repo_username: pytest-dev - requiresio: yes - scrutinizer: no - setup_py_uses_setuptools_scm: no - setup_py_uses_test_runner: no - sphinx_docs: yes + scrutinizer: 'no' + setup_py_uses_setuptools_scm: 'no' + sphinx_docs: 'yes' sphinx_docs_hosting: https://pytest-cov.readthedocs.io/ - sphinx_doctest: no + sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme - test_matrix_configurator: no - test_matrix_separate_coverage: no - test_runner: pytest - travis: no - travis_osx: no - version: 3.0.0 + test_matrix_separate_coverage: 'no' + version: 5.0.0 version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' - year_to: '2022' + year_to: '2024' diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..be006de9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fade785..84753edb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,15 +16,15 @@ jobs: - {python-version: "pypy-3.9", tox-python-version: "pypy3"} - {python-version: "3.11", tox-python-version: "py311"} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: @@ -60,155 +60,155 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py37-pytest73-xdist330-coverage72 (ubuntu)' - python: '3.7' - toxpython: 'python3.7' - python_arch: 'x64' - tox_env: 'py37-pytest73-xdist330-coverage72' - os: 'ubuntu-latest' - - name: 'py37-pytest73-xdist330-coverage72 (windows)' - python: '3.7' - toxpython: 'python3.7' - python_arch: 'x64' - tox_env: 'py37-pytest73-xdist330-coverage72' - os: 'windows-latest' - - name: 'py37-pytest73-xdist330-coverage72 (macos)' - python: '3.7' - toxpython: 'python3.7' - python_arch: 'x64' - tox_env: 'py37-pytest73-xdist330-coverage72' - os: 'macos-latest' - - name: 'py38-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'py38-pytest81-xdist350-coverage74 (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' - tox_env: 'py38-pytest73-xdist330-coverage72' + tox_env: 'py38-pytest81-xdist350-coverage74' os: 'ubuntu-latest' - - name: 'py38-pytest73-xdist330-coverage72 (windows)' + - name: 'py38-pytest81-xdist350-coverage74 (windows)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' - tox_env: 'py38-pytest73-xdist330-coverage72' + tox_env: 'py38-pytest81-xdist350-coverage74' os: 'windows-latest' - - name: 'py38-pytest73-xdist330-coverage72 (macos)' + - name: 'py38-pytest81-xdist350-coverage74 (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' - tox_env: 'py38-pytest73-xdist330-coverage72' + tox_env: 'py38-pytest81-xdist350-coverage74' os: 'macos-latest' - - name: 'py39-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'py39-pytest81-xdist350-coverage74 (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest73-xdist330-coverage72' + tox_env: 'py39-pytest81-xdist350-coverage74' os: 'ubuntu-latest' - - name: 'py39-pytest73-xdist330-coverage72 (windows)' + - name: 'py39-pytest81-xdist350-coverage74 (windows)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest73-xdist330-coverage72' + tox_env: 'py39-pytest81-xdist350-coverage74' os: 'windows-latest' - - name: 'py39-pytest73-xdist330-coverage72 (macos)' + - name: 'py39-pytest81-xdist350-coverage74 (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest73-xdist330-coverage72' + tox_env: 'py39-pytest81-xdist350-coverage74' os: 'macos-latest' - - name: 'py310-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'py310-pytest81-xdist350-coverage74 (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest73-xdist330-coverage72' + tox_env: 'py310-pytest81-xdist350-coverage74' os: 'ubuntu-latest' - - name: 'py310-pytest73-xdist330-coverage72 (windows)' + - name: 'py310-pytest81-xdist350-coverage74 (windows)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest73-xdist330-coverage72' + tox_env: 'py310-pytest81-xdist350-coverage74' os: 'windows-latest' - - name: 'py310-pytest73-xdist330-coverage72 (macos)' + - name: 'py310-pytest81-xdist350-coverage74 (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest73-xdist330-coverage72' + tox_env: 'py310-pytest81-xdist350-coverage74' os: 'macos-latest' - - name: 'py311-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'py311-pytest81-xdist350-coverage74 (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest73-xdist330-coverage72' + tox_env: 'py311-pytest81-xdist350-coverage74' os: 'ubuntu-latest' - - name: 'py311-pytest73-xdist330-coverage72 (windows)' + - name: 'py311-pytest81-xdist350-coverage74 (windows)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest73-xdist330-coverage72' + tox_env: 'py311-pytest81-xdist350-coverage74' os: 'windows-latest' - - name: 'py311-pytest73-xdist330-coverage72 (macos)' + - name: 'py311-pytest81-xdist350-coverage74 (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest73-xdist330-coverage72' + tox_env: 'py311-pytest81-xdist350-coverage74' os: 'macos-latest' - - name: 'pypy37-pytest73-xdist330-coverage72 (ubuntu)' - python: 'pypy-3.7' - toxpython: 'pypy3.7' + - name: 'py312-pytest81-xdist350-coverage74 (ubuntu)' + python: '3.12' + toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'pypy37-pytest73-xdist330-coverage72' + tox_env: 'py312-pytest81-xdist350-coverage74' os: 'ubuntu-latest' - - name: 'pypy37-pytest73-xdist330-coverage72 (windows)' - python: 'pypy-3.7' - toxpython: 'pypy3.7' + - name: 'py312-pytest81-xdist350-coverage74 (windows)' + python: '3.12' + toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'pypy37-pytest73-xdist330-coverage72' + tox_env: 'py312-pytest81-xdist350-coverage74' os: 'windows-latest' - - name: 'pypy37-pytest73-xdist330-coverage72 (macos)' - python: 'pypy-3.7' - toxpython: 'pypy3.7' + - name: 'py312-pytest81-xdist350-coverage74 (macos)' + python: '3.12' + toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'pypy37-pytest73-xdist330-coverage72' + tox_env: 'py312-pytest81-xdist350-coverage74' os: 'macos-latest' - - name: 'pypy38-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'pypy38-pytest81-xdist350-coverage74 (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' - tox_env: 'pypy38-pytest73-xdist330-coverage72' + tox_env: 'pypy38-pytest81-xdist350-coverage74' os: 'ubuntu-latest' - - name: 'pypy38-pytest73-xdist330-coverage72 (windows)' + - name: 'pypy38-pytest81-xdist350-coverage74 (windows)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' - tox_env: 'pypy38-pytest73-xdist330-coverage72' + tox_env: 'pypy38-pytest81-xdist350-coverage74' os: 'windows-latest' - - name: 'pypy38-pytest73-xdist330-coverage72 (macos)' + - name: 'pypy38-pytest81-xdist350-coverage74 (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' - tox_env: 'pypy38-pytest73-xdist330-coverage72' + tox_env: 'pypy38-pytest81-xdist350-coverage74' os: 'macos-latest' - - name: 'pypy39-pytest73-xdist330-coverage72 (ubuntu)' + - name: 'pypy39-pytest81-xdist350-coverage74 (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest73-xdist330-coverage72' + tox_env: 'pypy39-pytest81-xdist350-coverage74' os: 'ubuntu-latest' - - name: 'pypy39-pytest73-xdist330-coverage72 (windows)' + - name: 'pypy39-pytest81-xdist350-coverage74 (windows)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest73-xdist330-coverage72' + tox_env: 'pypy39-pytest81-xdist350-coverage74' os: 'windows-latest' - - name: 'pypy39-pytest73-xdist330-coverage72 (macos)' + - name: 'pypy39-pytest81-xdist350-coverage74 (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest73-xdist330-coverage72' + tox_env: 'pypy39-pytest81-xdist350-coverage74' + os: 'macos-latest' + - name: 'pypy310-pytest81-xdist350-coverage74 (ubuntu)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest81-xdist350-coverage74' + os: 'ubuntu-latest' + - name: 'pypy310-pytest81-xdist350-coverage74 (windows)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest81-xdist350-coverage74' + os: 'windows-latest' + - name: 'pypy310-pytest81-xdist350-coverage74 (macos)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest81-xdist350-coverage74' os: 'macos-latest' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.python_arch }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b105884..6a9d1217 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,23 @@ -# To install the git pre-commit hook run: -# pre-commit install -# To update the pre-commit hooks run: +# To install the git pre-commit hooks run: +# pre-commit install --install-hooks +# To update the versions: # pre-commit autoupdate +exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' +# Note the order is intentional to avoid multiple passes of the hooks repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.3 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] + - repo: https://github.com/psf/black + rev: 24.3.0 + hooks: + - id: black - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer exclude: '.*\.pth$' - id: debug-statements - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: [--py37-plus] - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 diff --git a/.readthedocs.yml b/.readthedocs.yml index ac76971c..009a913c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,8 +3,11 @@ version: 2 sphinx: configuration: docs/conf.py formats: all +build: + os: ubuntu-22.04 + tools: + python: "3" python: - version: 3 install: - requirements: docs/requirements.txt - method: pip diff --git a/AUTHORS.rst b/AUTHORS.rst index 61e7915c..44e92874 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,15 +1,16 @@ + Authors ======= -* Marc Schlaich - http://www.schlamar.org +* Marc Schlaich - \http://www.schlamar.org * Rick van Hattem - http://wol.ph * Buck Evan - https://github.com/bukzor * Eric Larson - http://larsoner.com -* Marc Abramowitz - http://marc-abramowitz.com +* Marc Abramowitz - \http://marc-abramowitz.com * Thomas Kluyver - https://github.com/takluyver * Guillaume Ayoub - http://www.yabz.fr * Federico Ceratto - http://firelet.net -* Josh Kalderimis - http://blog.cookiestack.com +* Josh Kalderimis - \http://blog.cookiestack.com * Ionel Cristian Mărieș - https://blog.ionelmc.ro * Christian Ledermann - https://github.com/cleder * Alec Nikolas Reiter - https://github.com/justanr @@ -59,3 +60,5 @@ Authors * Christian Fetzer - https://github.com/fetzerch * Jonathan Stewmon - https://github.com/jstewmon * Matthew Gamble - https://github.com/mwgamble +* Christian Clauss - https://github.com/cclauss +* Dawn James - https://github.com/dawngerpony diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d00d95e..5dd46aa3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,24 @@ + Changelog ========= +5.0.0 (2024-03-24) +------------------ + +* Removed support for xdist rsync (now deprecated). + Contributed by Matthias Reichenbach in `#623 `_. +* Switched docs theme to Furo. +* Various legacy Python cleanup and CI improvements. + Contributed by Christian Clauss and Hugo van Kemenade in + `#630 `_, + `#631 `_, + `#632 `_ and + `#633 `_. +* Added a ``pyproject.toml`` example in the docs. + Contributed by Dawn James in `#626 `_. +* Modernized project's pre-commit hooks to use ruff. Initial POC contributed by + Christian Clauss in `#584 `_. + 4.1.0 (2023-05-24) ------------------ @@ -44,7 +62,7 @@ Changelog Contributed by Andre Brisco in `#543 `_ and Colin O'Dell in `#525 `_. * Added support for LCOV output format via `--cov-report=lcov`. Only works with coverage 6.3+. - Contributed by Christian Fetzer in `#536 `_. + Contributed by Christian Fetzer in `#536 `_. * Modernized pytest hook implementation. Contributed by Bruno Oliveira in `#549 `_ and Ronny Pfannschmidt in `#550 `_. @@ -115,7 +133,7 @@ Changelog * Removed the empty `console_scripts` entrypoint that confused some Gentoo build script. I didn't ask why it was so broken cause I didn't want to ruin my day. Contributed by Michał Górny in `#434 `_. -* Fixed the missing `coverage context `_ +* Fixed the missing `coverage context `_ when using subprocesses. Contributed by Bernát Gábor in `#443 `_. * Updated the config section in the docs. @@ -144,7 +162,7 @@ Changelog * Made pytest startup faster when plugin not active by lazy-importing. Contributed by Anders Hovmöller in `#339 `_. * Various CI improvements. - Contributed by Daniel Hahler in `#363 `_ and + Contributed by Daniel Hahler in `#363 `_ and `#364 `_. * Various Python support updates (drop EOL 3.4, test against 3.8 final). Contributed by Hugo van Kemenade in diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e742be47..8e19ab39 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -49,7 +49,7 @@ To set up `pytest-cov` for local development: Now you can make your changes locally. -4. When you're done making changes run all the checks and docs builder with `tox `_ one command:: +4. When you're done making changes run all the checks and docs builder with one command:: tox @@ -68,17 +68,11 @@ If you need some code review or feedback while you're developing the code just m For merging, you should: -1. Include passing tests (run ``tox``) [1]_. +1. Include passing tests (run ``tox``). 2. Update documentation when there's new API, functionality etc. 3. Add a note to ``CHANGELOG.rst`` about the changes. 4. Add yourself to ``AUTHORS.rst``. -.. [1] If you don't have all the necessary Python versions available locally you can rely on GitHub Actions - it will - `run the tests `_ - for each change you add in the pull request. - - It will be slower though ... - Tips ---- diff --git a/MANIFEST.in b/MANIFEST.in index cbb88f74..22042a59 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,14 +15,15 @@ include .bumpversion.cfg include .cookiecutterrc include .coveragerc include .editorconfig -include tox.ini -include .readthedocs.yml include .pre-commit-config.yaml +include .readthedocs.yml +include pytest.ini +include tox.ini + include AUTHORS.rst include CHANGELOG.rst include CONTRIBUTING.rst include LICENSE include README.rst - global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/README.rst b/README.rst index 9a78ffd6..19b24e9f 100644 --- a/README.rst +++ b/README.rst @@ -10,39 +10,23 @@ Overview * - docs - |docs| * - tests - - | |github-actions| |requires| - | + - |github-actions| * - package - - | |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| - | |commits-since| - + - |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| |commits-since| .. |docs| image:: https://readthedocs.org/projects/pytest-cov/badge/?style=flat - :target: https://readthedocs.org/projects/pytest-cov + :target: https://readthedocs.org/projects/pytest-cov/ :alt: Documentation Status .. |github-actions| image:: https://github.com/pytest-dev/pytest-cov/actions/workflows/test.yml/badge.svg :alt: GitHub Actions Status :target: https://github.com/pytest-dev/pytest-cov/actions -.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/pytest-dev/pytest-cov?branch=master&svg=true - :alt: AppVeyor Build Status - :target: https://ci.appveyor.com/project/pytestbot/pytest-cov - -.. |requires| image:: https://requires.io/github/pytest-dev/pytest-cov/requirements.svg?branch=master - :alt: Requirements Status - :target: https://requires.io/github/pytest-dev/pytest-cov/requirements/?branch=master - .. |version| image:: https://img.shields.io/pypi/v/pytest-cov.svg :alt: PyPI Package latest release :target: https://pypi.org/project/pytest-cov .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-cov.svg :target: https://anaconda.org/conda-forge/pytest-cov - -.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v4.1.0.svg - :alt: Commits since latest release - :target: https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...master - .. |wheel| image:: https://img.shields.io/pypi/wheel/pytest-cov.svg :alt: PyPI Wheel :target: https://pypi.org/project/pytest-cov @@ -55,6 +39,10 @@ Overview :alt: Supported implementations :target: https://pypi.org/project/pytest-cov +.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v5.0.0.svg + :alt: Commits since latest release + :target: https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...master + .. end-badges This plugin produces coverage reports. Compared to just using ``coverage run`` this plugin does some extras: @@ -120,7 +108,7 @@ Would produce a report like:: Documentation ============= - http://pytest-cov.rtfd.org/ + https://pytest-cov.readthedocs.io/en/latest/ diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 48c46640..08d6c90b 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -1,63 +1,57 @@ #!/usr/bin/env python - import os +import pathlib import subprocess import sys -from os.path import abspath -from os.path import dirname -from os.path import exists -from os.path import join -from os.path import relpath -base_path = dirname(dirname(abspath(__file__))) -templates_path = join(base_path, "ci", "templates") +base_path: pathlib.Path = pathlib.Path(__file__).resolve().parent.parent +templates_path = base_path / 'ci' / 'templates' def check_call(args): - print("+", *args) + print('+', *args) subprocess.check_call(args) def exec_in_env(): - env_path = join(base_path, ".tox", "bootstrap") - if sys.platform == "win32": - bin_path = join(env_path, "Scripts") + env_path = base_path / '.tox' / 'bootstrap' + if sys.platform == 'win32': + bin_path = env_path / 'Scripts' else: - bin_path = join(env_path, "bin") - if not exists(env_path): + bin_path = env_path / 'bin' + if not env_path.exists(): import subprocess - print(f"Making bootstrap env in: {env_path} ...") + print(f'Making bootstrap env in: {env_path} ...') try: - check_call([sys.executable, "-m", "venv", env_path]) + check_call([sys.executable, '-m', 'venv', env_path]) except subprocess.CalledProcessError: try: - check_call([sys.executable, "-m", "virtualenv", env_path]) + check_call([sys.executable, '-m', 'virtualenv', env_path]) except subprocess.CalledProcessError: - check_call(["virtualenv", env_path]) - print("Installing `jinja2` into bootstrap environment...") - check_call([join(bin_path, "pip"), "install", "jinja2", "tox"]) - python_executable = join(bin_path, "python") - if not os.path.exists(python_executable): - python_executable += '.exe' + check_call(['virtualenv', env_path]) + print('Installing `jinja2` into bootstrap environment...') + check_call([bin_path / 'pip', 'install', 'jinja2', 'tox']) + python_executable = bin_path / 'python' + if not python_executable.exists(): + python_executable = python_executable.with_suffix('.exe') - print(f"Re-executing with: {python_executable}") - print("+ exec", python_executable, __file__, "--no-env") - os.execv(python_executable, [python_executable, __file__, "--no-env"]) + print(f'Re-executing with: {python_executable}') + print('+ exec', python_executable, __file__, '--no-env') + os.execv(python_executable, [python_executable, __file__, '--no-env']) def main(): import jinja2 - print(f"Project path: {base_path}") + print(f'Project path: {base_path}') jinja = jinja2.Environment( - loader=jinja2.FileSystemLoader(templates_path), + loader=jinja2.FileSystemLoader(str(templates_path)), trim_blocks=True, lstrip_blocks=True, - keep_trailing_newline=True + keep_trailing_newline=True, ) - tox_environments = [ line.strip() # 'tox' need not be installed globally, but must be importable @@ -68,22 +62,22 @@ def main(): for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], text=True).splitlines() ] tox_environments = [line for line in tox_environments if line.startswith('py')] - - for root, _, files in os.walk(templates_path): - for name in files: - relative = relpath(root, templates_path) - with open(join(base_path, relative, name), "w") as fh: - fh.write(jinja.get_template(join(relative, name)).render(tox_environments=tox_environments)) - print(f"Wrote {name}") - print("DONE.") + for template in templates_path.rglob('*'): + if template.is_file(): + template_path = template.relative_to(templates_path).as_posix() + destination = base_path / template_path + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(jinja.get_template(template_path).render(tox_environments=tox_environments)) + print(f'Wrote {template_path}') + print('DONE.') -if __name__ == "__main__": +if __name__ == '__main__': args = sys.argv[1:] - if args == ["--no-env"]: + if args == ['--no-env']: main() elif not args: exec_in_env() else: - print(f"Unexpected arguments {args}", file=sys.stderr) + print(f'Unexpected arguments: {args}', file=sys.stderr) sys.exit(1) diff --git a/ci/requirements.txt b/ci/requirements.txt index a0ef106f..b4f18520 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -1,5 +1,5 @@ virtualenv>=16.6.0 pip>=19.1.1 setuptools>=18.0.1 -six>=1.14.0 tox +twine diff --git a/ci/templates/.github/workflows/test.yml b/ci/templates/.github/workflows/test.yml index 6aaf1dbc..39e64078 100644 --- a/ci/templates/.github/workflows/test.yml +++ b/ci/templates/.github/workflows/test.yml @@ -66,9 +66,9 @@ jobs: {% for env in tox_environments %} {% set prefix = env.split('-')[0] -%} {% if prefix.startswith('pypy') %} -{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} {% set cpython %}pp{{ prefix[4:5] }}{% endset %} -{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} {% else %} {% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} {% set cpython %}cp{{ prefix[2:] }}{% endset %} @@ -88,10 +88,10 @@ jobs: {% endfor %} {% endfor %} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: {{ '${{ matrix.python }}' }} architecture: {{ '${{ matrix.python_arch }}' }} diff --git a/docs/conf.py b/docs/conf.py index e897c5e9..f15a262c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,5 @@ import os -import sphinx_py3doc_enhanced_theme - extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', @@ -13,17 +11,17 @@ 'sphinx.ext.extlinks', ] if os.getenv('SPELLCHECK'): - extensions += 'sphinxcontrib.spelling', + extensions += ('sphinxcontrib.spelling',) spelling_show_suggestions = True spelling_lang = 'en_US' source_suffix = '.rst' master_doc = 'index' project = 'pytest-cov' -year = '2016' +year = '2010-2024' author = 'pytest-cov contributors' copyright = f'{year}, {author}' -version = release = '4.1.0' +version = release = '5.0.0' pygments_style = 'trac' templates_path = ['.'] @@ -31,19 +29,14 @@ 'issue': ('https://github.com/pytest-dev/pytest-cov/issues/%s', '#'), 'pr': ('https://github.com/pytest-dev/pytest-cov/pull/%s', 'PR #'), } - -html_theme = "sphinx_py3doc_enhanced_theme" -html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] +html_theme = 'furo' html_theme_options = { - 'githuburl': 'https://github.com/pytest-dev/pytest-cov/' + 'githuburl': 'https://github.com/pytest-dev/pytest-cov/', } html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' -html_split_index = True -html_sidebars = { - '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], -} +html_split_index = False html_short_title = f'{project}-{version}' napoleon_use_ivar = True diff --git a/docs/config.rst b/docs/config.rst index c7bef037..852c014b 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -32,12 +32,19 @@ For full details refer to the `coverage config file`_ documentation. If you use the ``--cov-branch`` option then coverage's ``branch`` option will also get overridden. -If you wish to always add pytest-cov with pytest, you can use ``addopts`` under ``pytest`` or ``tool:pytest`` section. -For example: :: +If you wish to always add pytest-cov with pytest, you can use ``addopts`` under the ``pytest`` or ``tool:pytest`` section of +your ``setup.cfg``, or the ``tool.pytest.ini_options`` section of your ``pyproject.toml`` file. + +For example, in ``setup.cfg``: :: [tool:pytest] addopts = --cov= --cov-report html +Or for ``pyproject.toml``: :: + + [tool.pytest.ini_options] + addopts = "--cov= --cov-report html" + Caveats ======= diff --git a/docs/contexts.rst b/docs/contexts.rst index cde920d5..2b447463 100644 --- a/docs/contexts.rst +++ b/docs/contexts.rst @@ -6,7 +6,7 @@ Coverage.py 5.0 can record separate coverage data for `different contexts`_ duri one run of a test suite. Pytest-cov can use this feature to record coverage data for each test individually, with the ``--cov-context=test`` option. -.. _different contexts: https://coverage.readthedocs.io/en/stable/contexts.html +.. _different contexts: https://coverage.readthedocs.io/en/latest/contexts.html The context name recorded in the coverage.py database is the pytest test id, and the phase of execution, one of "setup", "run", or "teardown". These two diff --git a/docs/plugins.rst b/docs/plugins.rst index d06c4ffe..577870de 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -2,7 +2,7 @@ Plugin coverage =============== -Getting coverage on pytest plugins is a very particular situation. Because how pytest implements plugins (using setuptools +Getting coverage on pytest plugins is a very particular situation. Because of how pytest implements plugins (using setuptools entrypoints) it doesn't allow controlling the order in which the plugins load. See `pytest/issues/935 `_ for technical details. @@ -10,7 +10,7 @@ The current way of dealing with this problem is using the append feature and man COV_CORE_SOURCE=src COV_CORE_CONFIG=.coveragerc COV_CORE_DATAFILE=.coverage.eager pytest --cov=src --cov-append -Alternatively you can have this in ``tox.ini`` (if you're using `Tox `_ of course):: +Alternatively you can have this in ``tox.ini`` (if you're using `Tox `_ of course):: [testenv] setenv = diff --git a/docs/releasing.rst b/docs/releasing.rst index ae78228d..9afe600d 100644 --- a/docs/releasing.rst +++ b/docs/releasing.rst @@ -4,19 +4,16 @@ Releasing The process for releasing should follow these steps: -#. Test that docs build and render properly by running ``tox -e docs,spell``. +#. Test that docs build and render properly by running ``tox -e docs``. If there are bogus spelling issues add the words in ``spelling_wordlist.txt``. #. Update ``CHANGELOG.rst`` and ``AUTHORS.rst`` to be up to date. #. Bump the version by running ``bumpversion [ major | minor | patch ]``. This will automatically add a tag. - - Alternatively, you can manually edit the files and run ``git tag v1.2.3`` yourself. #. Push changes and tags with:: git push git push --tags -#. Wait for `AppVeyor `_ - and `GitHub Actions `_ to give the green builds. +#. Wait `GitHub Actions `_ to give the green builds. #. Check that the docs on `ReadTheDocs `_ are built. #. Make sure you have a clean checkout, run ``git status`` to verify. #. Manually clean temporary files (that are ignored and won't show up in ``git status``):: diff --git a/docs/requirements.txt b/docs/requirements.txt index 6fdf26f9..4c1e3b7d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,3 @@ -sphinx==3.0.3 -sphinx-py3doc-enhanced-theme==2.4.0 -docutils==0.16 -jinja2<3.1 +sphinx +furo -e . diff --git a/docs/tox.rst b/docs/tox.rst index 18f9137e..e44de028 100644 --- a/docs/tox.rst +++ b/docs/tox.rst @@ -2,7 +2,7 @@ Tox === -When using `tox `_ you can have ultra-compact configuration - you can have all of it in +When using `tox `_ you can have ultra-compact configuration - you can have all of it in ``tox.ini``:: [tox] diff --git a/examples/adhoc-layout/example/__init__.py b/examples/adhoc-layout/example/__init__.py index 684905a1..36b78a3d 100644 --- a/examples/adhoc-layout/example/__init__.py +++ b/examples/adhoc-layout/example/__init__.py @@ -2,10 +2,12 @@ # test merging multiple tox runs with a platform # based branch -if platform.python_implementation() == "PyPy": +if platform.python_implementation() == 'PyPy': + def add(a, b): return a + b else: + def add(a, b): return a + b diff --git a/examples/adhoc-layout/setup.py b/examples/adhoc-layout/setup.py index e52b68d1..86a4bf68 100644 --- a/examples/adhoc-layout/setup.py +++ b/examples/adhoc-layout/setup.py @@ -3,5 +3,5 @@ setup( name='example', - packages=find_packages(include=['example']) + packages=find_packages(include=['example']), ) diff --git a/examples/src-layout/src/example/__init__.py b/examples/src-layout/src/example/__init__.py index 684905a1..36b78a3d 100644 --- a/examples/src-layout/src/example/__init__.py +++ b/examples/src-layout/src/example/__init__.py @@ -2,10 +2,12 @@ # test merging multiple tox runs with a platform # based branch -if platform.python_implementation() == "PyPy": +if platform.python_implementation() == 'PyPy': + def add(a, b): return a + b else: + def add(a, b): return a + b diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..61be7748 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = [ + "setuptools>=30.3.0", +] + +[tool.ruff] +extend-exclude = ["static", "ci/templates"] +line-length = 140 +src = ["src", "tests"] +target-version = "py38" + +[tool.ruff.lint.per-file-ignores] +"ci/*" = ["S"] + +[tool.ruff.lint] +ignore = [ + "RUF001", # ruff-specific rules ambiguous-unicode-character-string + "S101", # flake8-bandit assert + "S308", # flake8-bandit suspicious-mark-safe-usage + "E501", # pycodestyle line-too-long +] +select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "E", # pycodestyle errors + "EXE", # flake8-executable + "F", # pyflakes + "I", # isort + "INT", # flake8-gettext + "PIE", # flake8-pie + "PLC", # pylint convention + "PLE", # pylint errors + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RSE", # flake8-raise + "RUF", # ruff-specific rules + "S", # flake8-bandit + "UP", # pyupgrade + "W", # pycodestyle warnings +] + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "single" + +[tool.ruff.lint.isort] +forced-separate = ["conftest"] +force-single-line = true + +[tool.black] +line-length = 140 +target-version = ["py38"] +skip-string-normalization = true diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..0f32c842 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,29 @@ +[pytest] +# If a pytest section is found in one of the possible config files +# (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, +# so if you add a pytest config section elsewhere, +# you will need to delete this section from setup.cfg. +norecursedirs = + migrations + +python_files = + test_*.py + *_test.py + tests.py +addopts = + -ra + --strict-markers + --doctest-modules + --doctest-glob=\*.rst + --tb=short + -p pytester +testpaths = + tests + +# Idea from: https://til.simonwillison.net/pytest/treat-warnings-as-errors +filterwarnings = + error +# You can add exclusions, some examples: +# ignore:'pytest_cov' defines default_app_config:PendingDeprecationWarning:: +# ignore:The {{% if::: +# ignore:Coverage disabled via --no-cov switch! diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index c33a9042..00000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[flake8] -max-line-length = 140 -exclude = .tox,.eggs,ci/templates,build,dist - -[tool:pytest] -testpaths = tests -python_files = test_*.py -addopts = - -ra - --strict-markers - -p pytester - -[tool:isort] -force_single_line = True -line_length = 120 -known_first_party = pytest_cov -default_section = THIRDPARTY -forced_separate = test_pytest_cov -skip = .tox,.eggs,ci/templates,build,dist diff --git a/setup.py b/setup.py index 70224965..221111e8 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,8 @@ #!/usr/bin/env python import re -from glob import glob from itertools import chain -from os.path import basename -from os.path import dirname -from os.path import join -from os.path import splitext +from pathlib import Path from setuptools import Command from setuptools import find_packages @@ -24,34 +20,31 @@ def read(*names, **kwargs): - with open( - join(dirname(__file__), *names), - encoding=kwargs.get('encoding', 'utf8') - ) as fh: + with Path(__file__).parent.joinpath(*names).open(encoding=kwargs.get('encoding', 'utf8')) as fh: return fh.read() class BuildWithPTH(build): def run(self, *args, **kwargs): super().run(*args, **kwargs) - path = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.build_lib, basename(path)) + path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') + dest = str(Path(self.build_lib) / Path(path).name) self.copy_file(path, dest) class EasyInstallWithPTH(easy_install): def run(self, *args, **kwargs): super().run(*args, **kwargs) - path = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.install_dir, basename(path)) + path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') + dest = str(Path(self.install_dir) / Path(path).name) self.copy_file(path, dest) class InstallLibWithPTH(install_lib): def run(self, *args, **kwargs): super().run(*args, **kwargs) - path = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.install_dir, basename(path)) + path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') + dest = str(Path(self.install_dir) / Path(path).name) self.copy_file(path, dest) self.outputs = [dest] @@ -62,13 +55,13 @@ def get_outputs(self): class DevelopWithPTH(develop): def run(self, *args, **kwargs): super().run(*args, **kwargs) - path = join(dirname(__file__), 'src', 'pytest-cov.pth') - dest = join(self.install_dir, basename(path)) + path = str(Path(__file__).parent / 'src' / 'pytest-cov.pth') + dest = str(Path(self.install_dir) / Path(path).name) self.copy_file(path, dest) class GeneratePTH(Command): - user_options = [] + user_options = () def initialize_options(self): pass @@ -77,16 +70,14 @@ def finalize_options(self): pass def run(self): - with open(join(dirname(__file__), 'src', 'pytest-cov.pth'), 'w') as fh: - with open(join(dirname(__file__), 'src', 'pytest-cov.embed')) as sh: - fh.write( - f"import os, sys;exec({sh.read().replace(' ', ' ')!r})" - ) + with Path(__file__).parent.joinpath('src', 'pytest-cov.pth').open('w') as fh: + with Path(__file__).parent.joinpath('src', 'pytest-cov.embed').open() as sh: + fh.write(f"import os, sys;exec({sh.read().replace(' ', ' ')!r})") setup( name='pytest-cov', - version='4.1.0', + version='5.0.0', license='MIT', description='Pytest plugin for measuring coverage.', long_description='{}\n{}'.format(read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), @@ -95,7 +86,7 @@ def run(self): url='https://github.com/pytest-dev/pytest-cov', packages=find_packages('src'), package_dir={'': 'src'}, - py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + py_modules=[path.stem for path in Path('src').glob('*.py')], include_package_data=True, zip_safe=False, classifiers=[ @@ -110,11 +101,11 @@ def run(self): 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Testing', @@ -126,19 +117,23 @@ def run(self): 'Issue Tracker': 'https://github.com/pytest-dev/pytest-cov/issues', }, keywords=[ - 'cover', 'coverage', 'pytest', 'py.test', 'distributed', 'parallel', + 'cover', + 'coverage', + 'pytest', + 'py.test', + 'distributed', + 'parallel', ], + python_requires='>=3.8', install_requires=[ 'pytest>=4.6', - 'coverage[toml]>=5.2.1' + 'coverage[toml]>=5.2.1', ], - python_requires='>=3.7', extras_require={ 'testing': [ 'fields', 'hunter', 'process-tests', - 'six', 'pytest-xdist', 'virtualenv', ] diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py index 8839990b..82ce08bb 100644 --- a/src/pytest_cov/__init__.py +++ b/src/pytest_cov/__init__.py @@ -1,2 +1,3 @@ """pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE.""" -__version__ = '4.1.0' + +__version__ = '5.0.0' diff --git a/src/pytest_cov/compat.py b/src/pytest_cov/compat.py index 614419cb..453709d7 100644 --- a/src/pytest_cov/compat.py +++ b/src/pytest_cov/compat.py @@ -1,12 +1,3 @@ -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - - -StringIO # pyflakes, this is for re-export - - class SessionWrapper: def __init__(self, session): self._session = session diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index f8a2749f..57c2f423 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -13,6 +13,7 @@ that code coverage is being collected we activate coverage based on info passed via env vars. """ + import atexit import os import signal @@ -52,7 +53,7 @@ def init(): data_suffix=True, config_file=cov_config, auto_data=True, - data_file=cov_datafile + data_file=cov_datafile, ) cov.load() cov.start() @@ -70,7 +71,7 @@ def _cleanup(cov): cov._auto_save = False # prevent autosaving from cov._atexit in case the interpreter lacks atexit.unregister try: atexit.unregister(cov._atexit) - except Exception: + except Exception: # noqa: S110 pass @@ -108,7 +109,7 @@ def _signal_cleanup_handler(signum, frame): elif signum == signal.SIGTERM: os._exit(128 + signum) elif signum == signal.SIGINT: - raise KeyboardInterrupt() + raise KeyboardInterrupt def cleanup_on_signal(signum): diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 97d4d017..0dab6c03 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -1,4 +1,5 @@ """Coverage controllers for use by pytest-cov and nose-cov.""" + import contextlib import copy import functools @@ -6,11 +7,12 @@ import random import socket import sys +from io import StringIO +from pathlib import Path import coverage from coverage.data import CoverageData -from .compat import StringIO from .embed import cleanup @@ -34,7 +36,7 @@ def _ensure_topdir(meth): @functools.wraps(meth) def ensure_topdir_wrapper(self, *args, **kwargs): try: - original_cwd = os.getcwd() + original_cwd = Path.cwd() except OSError: # Looks like it's gone, this is non-ideal because a side-effect will # be introduced in the tests here but we can't do anything about it. @@ -67,12 +69,12 @@ def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, c self.data_file = None self.node_descs = set() self.failed_workers = [] - self.topdir = os.getcwd() + self.topdir = os.fspath(Path.cwd()) self.is_collocated = None @contextlib.contextmanager def ensure_topdir(self): - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(self.topdir) yield os.chdir(original_cwd) @@ -94,12 +96,13 @@ def set_env(self): os.environ['COV_CORE_SOURCE'] = os.pathsep else: os.environ['COV_CORE_SOURCE'] = os.pathsep.join(self.cov_source) - config_file = os.path.abspath(self.cov_config) - if os.path.exists(config_file): - os.environ['COV_CORE_CONFIG'] = config_file + config_file = Path(self.cov_config) + if config_file.exists(): + os.environ['COV_CORE_CONFIG'] = os.fspath(config_file.resolve()) else: os.environ['COV_CORE_CONFIG'] = os.pathsep - os.environ['COV_CORE_DATAFILE'] = os.path.abspath(self.cov.config.data_file) + # this still uses the old abspath cause apparently Python 3.9 on Windows has a buggy Path.resolve() + os.environ['COV_CORE_DATAFILE'] = os.path.abspath(self.cov.config.data_file) # noqa: PTH100 if self.cov_branch: os.environ['COV_CORE_BRANCH'] = 'enabled' @@ -116,7 +119,7 @@ def unset_env(): def get_node_desc(platform, version_info): """Return a description of this node.""" - return 'platform {}, python {}'.format(platform, '%s.%s.%s-%s-%s' % version_info[:5]) + return 'platform {}, python {}'.format(platform, '{}.{}.{}-{}-{}'.format(*version_info[:5])) @staticmethod def sep(stream, s, txt): @@ -135,7 +138,7 @@ def summary(self, stream): total = None if not self.cov_report: - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): return self.cov.report(show_missing=True, ignore_errors=True, file=_NullFile) # Output coverage section header. @@ -149,8 +152,7 @@ def summary(self, stream): # Report on any failed workers. if self.failed_workers: self.sep(stream, '-', 'coverage: failed workers') - stream.write('The following workers failed to return coverage data, ' - 'ensure that pytest-cov is installed on these workers.\n') + stream.write('The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.\n') for node in self.failed_workers: stream.write(f'{node.gateway.id}\n') @@ -163,19 +165,19 @@ def summary(self, stream): } skip_covered = isinstance(self.cov_report, dict) and 'skip-covered' in self.cov_report.values() options.update({'skip_covered': skip_covered or None}) - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.report(**options) # Produce annotated source code report if wanted. if 'annotate' in self.cov_report: annotate_dir = self.cov_report['annotate'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): self.cov.annotate(ignore_errors=True, directory=annotate_dir) # We need to call Coverage.report here, just to get the total # Coverage.annotate don't return any total and we need it for --cov-fail-under. - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.report(ignore_errors=True, file=_NullFile) if annotate_dir: stream.write(f'Coverage annotated source written to dir {annotate_dir}\n') @@ -185,28 +187,28 @@ def summary(self, stream): # Produce html report if wanted. if 'html' in self.cov_report: output = self.cov_report['html'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.html_report(ignore_errors=True, directory=output) stream.write(f'Coverage HTML written to dir {self.cov.config.html_dir if output is None else output}\n') # Produce xml report if wanted. if 'xml' in self.cov_report: output = self.cov_report['xml'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.xml_report(ignore_errors=True, outfile=output) stream.write(f'Coverage XML written to file {self.cov.config.xml_output if output is None else output}\n') # Produce json report if wanted if 'json' in self.cov_report: output = self.cov_report['json'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): total = self.cov.json_report(ignore_errors=True, outfile=output) stream.write('Coverage JSON written to file %s\n' % (self.cov.config.json_output if output is None else output)) # Produce lcov report if wanted. if 'lcov' in self.cov_report: output = self.cov_report['lcov'] - with _backup(self.cov, "config"): + with _backup(self.cov, 'config'): self.cov.lcov_report(ignore_errors=True, outfile=output) # We need to call Coverage.report here, just to get the total @@ -225,15 +227,19 @@ class Central(CovController): def start(self): cleanup() - self.cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - config_file=self.cov_config) - self.combining_cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - data_file=os.path.abspath(self.cov.config.data_file), - config_file=self.cov_config) + self.cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=True, + config_file=self.cov_config, + ) + self.combining_cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=True, + data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 + config_file=self.cov_config, + ) # Erase or load any previous coverage data and start coverage. if not self.cov_append: @@ -265,24 +271,22 @@ class DistMaster(CovController): def start(self): cleanup() - # Ensure coverage rc file rsynced if appropriate. - if self.cov_config and os.path.exists(self.cov_config): - # rsyncdir is going away in pytest-xdist 4.0, already deprecated - if hasattr(self.config.option, 'rsyncdir'): - self.config.option.rsyncdir.append(self.cov_config) - - self.cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - config_file=self.cov_config) + self.cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=True, + config_file=self.cov_config, + ) self.cov._warn_no_data = False self.cov._warn_unimported_source = False self.cov._warn_preimported_source = False - self.combining_cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - data_file=os.path.abspath(self.cov.config.data_file), - config_file=self.cov_config) + self.combining_cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=True, + data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 + config_file=self.cov_config, + ) if not self.cov_append: self.cov.erase() self.cov.start() @@ -291,11 +295,13 @@ def start(self): def configure_node(self, node): """Workers need to know if they are collocated and what files have moved.""" - node.workerinput.update({ - 'cov_master_host': socket.gethostname(), - 'cov_master_topdir': self.topdir, - 'cov_master_rsync_roots': [str(root) for root in node.nodemanager.roots], - }) + node.workerinput.update( + { + 'cov_master_host': socket.gethostname(), + 'cov_master_topdir': self.topdir, + 'cov_master_rsync_roots': [str(root) for root in node.nodemanager.roots], + } + ) def testnodedown(self, node, error): """Collect data file name from worker.""" @@ -311,15 +317,13 @@ def testnodedown(self, node, error): # that it returns to us. if 'cov_worker_data' in output: data_suffix = '%s.%s.%06d.%s' % ( - socket.gethostname(), os.getpid(), - random.randint(0, 999999), - output['cov_worker_node_id'] + socket.gethostname(), + os.getpid(), + random.randint(0, 999999), # noqa: S311 + output['cov_worker_node_id'], ) - cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=data_suffix, - config_file=self.cov_config) + cov = coverage.Coverage(source=self.cov_source, branch=self.cov_branch, data_suffix=data_suffix, config_file=self.cov_config) cov.start() if coverage.version_info < (5, 0): data = CoverageData() @@ -361,23 +365,26 @@ def start(self): cleanup() # Determine whether we are collocated with master. - self.is_collocated = (socket.gethostname() == self.config.workerinput['cov_master_host'] and - self.topdir == self.config.workerinput['cov_master_topdir']) + self.is_collocated = ( + socket.gethostname() == self.config.workerinput['cov_master_host'] + and self.topdir == self.config.workerinput['cov_master_topdir'] + ) # If we are not collocated then rewrite master paths to worker paths. if not self.is_collocated: master_topdir = self.config.workerinput['cov_master_topdir'] worker_topdir = self.topdir if self.cov_source is not None: - self.cov_source = [source.replace(master_topdir, worker_topdir) - for source in self.cov_source] + self.cov_source = [source.replace(master_topdir, worker_topdir) for source in self.cov_source] self.cov_config = self.cov_config.replace(master_topdir, worker_topdir) # Erase any previous data and start coverage. - self.cov = coverage.Coverage(source=self.cov_source, - branch=self.cov_branch, - data_suffix=True, - config_file=self.cov_config) + self.cov = coverage.Coverage( + source=self.cov_source, + branch=self.cov_branch, + data_suffix=True, + config_file=self.cov_config, + ) self.cov.start() self.set_env() @@ -411,13 +418,13 @@ def finish(self): else: data = self.cov.get_data().dumps() - self.config.workeroutput.update({ - 'cov_worker_path': self.topdir, - 'cov_worker_node_id': self.nodeid, - 'cov_worker_data': data, - }) + self.config.workeroutput.update( + { + 'cov_worker_path': self.topdir, + 'cov_worker_node_id': self.nodeid, + 'cov_worker_data': data, + } + ) def summary(self, stream): """Only the master reports so do nothing.""" - - pass diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 2a1544a6..49413d46 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -1,7 +1,10 @@ """Coverage plugin for pytest.""" + import argparse import os import warnings +from io import StringIO +from pathlib import Path import coverage import pytest @@ -33,9 +36,9 @@ def validate_report(arg): term_choices = ['term', 'term-missing'] term_modifier_choices = ['skip-covered'] all_choices = term_choices + file_choices - values = arg.split(":", 1) + values = arg.split(':', 1) report_type = values[0] - if report_type not in all_choices + ['']: + if report_type not in [*all_choices, '']: msg = f'invalid choice: "{arg}" (choose from "{all_choices}")' raise argparse.ArgumentTypeError(msg) @@ -50,8 +53,7 @@ def validate_report(arg): return report_type, report_modifier if report_type not in file_choices: - msg = 'output specifier not supported for: "{}" (choose from "{}")'.format(arg, - file_choices) + msg = f'output specifier not supported for: "{arg}" (choose from "{file_choices}")' raise argparse.ArgumentTypeError(msg) return values @@ -64,17 +66,19 @@ def validate_fail_under(num_str): try: value = float(num_str) except ValueError: - raise argparse.ArgumentTypeError('An integer or float value is required.') + raise argparse.ArgumentTypeError('An integer or float value is required.') from None if value > 100: - raise argparse.ArgumentTypeError('Your desire for over-achievement is admirable but misplaced. ' - 'The maximum value is 100. Perhaps write more integration tests?') + raise argparse.ArgumentTypeError( + 'Your desire for over-achievement is admirable but misplaced. ' + 'The maximum value is 100. Perhaps write more integration tests?' + ) return value def validate_context(arg): if coverage.version_info <= (5, 0): raise argparse.ArgumentTypeError('Contexts are only supported with coverage.py >= 5.x') - if arg != "test": + if arg != 'test': raise argparse.ArgumentTypeError('The only supported value is "test".') return arg @@ -88,42 +92,83 @@ def __call__(self, parser, namespace, values, option_string=None): def pytest_addoption(parser): """Add options to control coverage.""" - group = parser.getgroup( - 'cov', 'coverage reporting with distributed testing support') - group.addoption('--cov', action='append', default=[], metavar='SOURCE', - nargs='?', const=True, dest='cov_source', - help='Path or package name to measure during execution (multi-allowed). ' - 'Use --cov= to not do any source filtering and record everything.') - group.addoption('--cov-reset', action='store_const', const=[], dest='cov_source', - help='Reset cov sources accumulated in options so far. ') - group.addoption('--cov-report', action=StoreReport, default={}, - metavar='TYPE', type=validate_report, - help='Type of report to generate: term, term-missing, ' - 'annotate, html, xml, json, lcov (multi-allowed). ' - 'term, term-missing may be followed by ":skip-covered". ' - 'annotate, html, xml, json and lcov may be followed by ":DEST" ' - 'where DEST specifies the output location. ' - 'Use --cov-report= to not generate any output.') - group.addoption('--cov-config', action='store', default='.coveragerc', - metavar='PATH', - help='Config file for coverage. Default: .coveragerc') - group.addoption('--no-cov-on-fail', action='store_true', default=False, - help='Do not report coverage if test run fails. ' - 'Default: False') - group.addoption('--no-cov', action='store_true', default=False, - help='Disable coverage report completely (useful for debuggers). ' - 'Default: False') - group.addoption('--cov-fail-under', action='store', metavar='MIN', - type=validate_fail_under, - help='Fail if the total coverage is less than MIN.') - group.addoption('--cov-append', action='store_true', default=False, - help='Do not delete coverage but append to current. ' - 'Default: False') - group.addoption('--cov-branch', action='store_true', default=None, - help='Enable branch coverage.') - group.addoption('--cov-context', action='store', metavar='CONTEXT', - type=validate_context, - help='Dynamic contexts to use. "test" for now.') + group = parser.getgroup('cov', 'coverage reporting with distributed testing support') + group.addoption( + '--cov', + action='append', + default=[], + metavar='SOURCE', + nargs='?', + const=True, + dest='cov_source', + help='Path or package name to measure during execution (multi-allowed). ' + 'Use --cov= to not do any source filtering and record everything.', + ) + group.addoption( + '--cov-reset', + action='store_const', + const=[], + dest='cov_source', + help='Reset cov sources accumulated in options so far. ', + ) + group.addoption( + '--cov-report', + action=StoreReport, + default={}, + metavar='TYPE', + type=validate_report, + help='Type of report to generate: term, term-missing, ' + 'annotate, html, xml, json, lcov (multi-allowed). ' + 'term, term-missing may be followed by ":skip-covered". ' + 'annotate, html, xml, json and lcov may be followed by ":DEST" ' + 'where DEST specifies the output location. ' + 'Use --cov-report= to not generate any output.', + ) + group.addoption( + '--cov-config', + action='store', + default='.coveragerc', + metavar='PATH', + help='Config file for coverage. Default: .coveragerc', + ) + group.addoption( + '--no-cov-on-fail', + action='store_true', + default=False, + help='Do not report coverage if test run fails. Default: False', + ) + group.addoption( + '--no-cov', + action='store_true', + default=False, + help='Disable coverage report completely (useful for debuggers). Default: False', + ) + group.addoption( + '--cov-fail-under', + action='store', + metavar='MIN', + type=validate_fail_under, + help='Fail if the total coverage is less than MIN.', + ) + group.addoption( + '--cov-append', + action='store_true', + default=False, + help='Do not delete coverage but append to current. Default: False', + ) + group.addoption( + '--cov-branch', + action='store_true', + default=None, + help='Enable branch coverage.', + ) + group.addoption( + '--cov-context', + action='store', + metavar='CONTEXT', + type=validate_context, + help='Dynamic contexts to use. "test" for now.', + ) def _prepare_cov_source(cov_source): @@ -172,7 +217,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) # Our implementation is unknown at this time. self.pid = None self.cov_controller = None - self.cov_report = compat.StringIO() + self.cov_report = StringIO() self.cov_total = None self.failed = False self._started = False @@ -180,9 +225,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) self._disabled = False self.options = options - is_dist = (getattr(options, 'numprocesses', False) or - getattr(options, 'distload', False) or - getattr(options, 'dist', 'no') != 'no') + is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no' if getattr(options, 'no_cov', False): self._disabled = True return @@ -220,11 +263,11 @@ class Config: self.options.cov_append, self.options.cov_branch, config, - nodeid + nodeid, ) self.cov_controller.start() self._started = True - self._start_path = os.getcwd() + self._start_path = Path.cwd() cov_config = self.cov_controller.cov.config if self.options.cov_fail_under is None and hasattr(cov_config, 'fail_under'): self.options.cov_fail_under = cov_config.fail_under @@ -246,9 +289,7 @@ def pytest_sessionstart(self, session): self.pid = os.getpid() if self._is_worker(session): - nodeid = ( - session.config.workerinput.get('workerid', getattr(session, 'nodeid')) - ) + nodeid = session.config.workerinput.get('workerid', session.nodeid) self.start(engine.DistWorker, session.config, nodeid) elif not self._started: self.start(engine.Central) @@ -307,9 +348,8 @@ def pytest_runtestloop(self, session): self.cov_total = self.cov_controller.summary(self.cov_report) except CoverageException as exc: message = f'Failed to generate report: {exc}\n' - session.config.pluginmanager.getplugin("terminalreporter").write( - f'WARNING: {message}\n', red=True, bold=True) - warnings.warn(CovReportWarning(message)) + session.config.pluginmanager.getplugin('terminalreporter').write(f'WARNING: {message}\n', red=True, bold=True) + warnings.warn(CovReportWarning(message), stacklevel=1) self.cov_total = 0 assert self.cov_total is not None, 'Test coverage should never be `None`' if self._failed_cov_total() and not self.options.collectonly: @@ -321,7 +361,7 @@ def pytest_terminal_summary(self, terminalreporter): if self.options.no_cov_should_warn: message = 'Coverage disabled via --no-cov switch!' terminalreporter.write(f'WARNING: {message}\n', red=True, bold=True) - warnings.warn(CovDisabledWarning(message)) + warnings.warn(CovDisabledWarning(message), stacklevel=1) return if self.cov_controller is None: return @@ -339,15 +379,11 @@ def pytest_terminal_summary(self, terminalreporter): if self.options.cov_fail_under is not None and self.options.cov_fail_under > 0: failed = self.cov_total < self.options.cov_fail_under markup = {'red': True, 'bold': True} if failed else {'green': True} - message = ( - '{fail}Required test coverage of {required}% {reached}. ' - 'Total coverage: {actual:.2f}%\n' - .format( - required=self.options.cov_fail_under, - actual=self.cov_total, - fail="FAIL " if failed else "", - reached="not reached" if failed else "reached" - ) + message = '{fail}Required test coverage of {required}% {reached}. ' 'Total coverage: {actual:.2f}%\n'.format( + required=self.options.cov_fail_under, + actual=self.cov_total, + fail='FAIL ' if failed else '', + reached='not reached' if failed else 'reached', ) terminalreporter.write(message, **markup) @@ -362,8 +398,7 @@ def pytest_runtest_teardown(self, item): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): - if (item.get_closest_marker('no_cover') - or 'no_cover' in getattr(item, 'fixturenames', ())): + if item.get_closest_marker('no_cover') or 'no_cover' in getattr(item, 'fixturenames', ()): self.cov_controller.pause() yield self.cov_controller.resume() @@ -385,15 +420,14 @@ def pytest_runtest_call(self, item): self.switch_context(item, 'run') def switch_context(self, item, when): - context = f"{item.nodeid}|{when}" + context = f'{item.nodeid}|{when}' self.cov.switch_context(context) os.environ['COV_CORE_CONTEXT'] = context @pytest.fixture -def no_cover(): +def no_cover(): # noqa: PT004 """A pytest fixture to disable coverage.""" - pass @pytest.fixture @@ -409,4 +443,4 @@ def cov(request): def pytest_configure(config): - config.addinivalue_line("markers", "no_cover: disable coverage for this test.") + config.addinivalue_line('markers', 'no_cover: disable coverage for this test.') diff --git a/tests/contextful.py b/tests/contextful.py index 3527e499..b1d0804b 100644 --- a/tests/contextful.py +++ b/tests/contextful.py @@ -1,105 +1,113 @@ # A test file for test_pytest_cov.py:test_contexts import unittest +from typing import ClassVar import pytest def test_01(): - assert 1 == 1 # r1 + assert 1 == 1 # r1 def test_02(): - assert 2 == 2 # r2 + assert 2 == 2 # r2 class OldStyleTests(unittest.TestCase): - items = [] + items: ClassVar = [] @classmethod def setUpClass(cls): - cls.items.append("hello") # s3 + cls.items.append('hello') # s3 @classmethod def tearDownClass(cls): - cls.items.pop() # t4 + cls.items.pop() # t4 def setUp(self): - self.number = 1 # r3 r4 + self.number = 1 # r3 r4 def tearDown(self): - self.number = None # r3 r4 + self.number = None # r3 r4 def test_03(self): - assert self.number == 1 # r3 - assert self.items[0] == "hello" # r3 + assert self.number == 1 # r3 + assert self.items[0] == 'hello' # r3 def test_04(self): - assert self.number == 1 # r4 - assert self.items[0] == "hello" # r4 + assert self.number == 1 # r4 + assert self.items[0] == 'hello' # r4 @pytest.fixture def some_data(): - return [1, 2, 3] # s5 s6 + return [1, 2, 3] # s5 s6 def test_05(some_data): - assert len(some_data) == 3 # r5 + assert len(some_data) == 3 # r5 @pytest.fixture def more_data(some_data): - return [2*x for x in some_data] # s6 + return [2 * x for x in some_data] # s6 def test_06(some_data, more_data): - assert len(some_data) == len(more_data) # r6 + assert len(some_data) == len(more_data) # r6 @pytest.fixture(scope='session') def expensive_data(): - return list(range(10)) # s7 + return list(range(10)) # s7 def test_07(expensive_data): - assert len(expensive_data) == 10 # r7 + assert len(expensive_data) == 10 # r7 def test_08(expensive_data): - assert len(expensive_data) == 10 # r8 + assert len(expensive_data) == 10 # r8 @pytest.fixture(params=[1, 2, 3]) def parametrized_number(request): - return request.param # s9-1 s9-2 s9-3 + return request.param # s9-1 s9-2 s9-3 def test_09(parametrized_number): - assert parametrized_number > 0 # r9-1 r9-2 r9-3 + assert parametrized_number > 0 # r9-1 r9-2 r9-3 def test_10(): - assert 1 == 1 # r10 + assert 1 == 1 # r10 -@pytest.mark.parametrize("x, ans", [ - (1, 101), - (2, 202), -]) +@pytest.mark.parametrize( + ('x', 'ans'), + [ + (1, 101), + (2, 202), + ], +) def test_11(x, ans): - assert 100 * x + x == ans # r11-1 r11-2 + assert 100 * x + x == ans # r11-1 r11-2 -@pytest.mark.parametrize("x, ans", [ - (1, 101), - (2, 202), -], ids=['one', 'two']) +@pytest.mark.parametrize( + ('x', 'ans'), + [ + (1, 101), + (2, 202), + ], + ids=['one', 'two'], +) def test_12(x, ans): - assert 100 * x + x == ans # r12-1 r12-2 + assert 100 * x + x == ans # r12-1 r12-2 -@pytest.mark.parametrize("x", [1, 2]) -@pytest.mark.parametrize("y", [3, 4]) +@pytest.mark.parametrize('x', [1, 2]) +@pytest.mark.parametrize('y', [3, 4]) def test_13(x, y): - assert x + y > 0 # r13-1 r13-2 r13-3 r13-4 + assert x + y > 0 # r13-1 r13-2 r13-3 r13-4 diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 84f959fb..bd9df38e 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1,3 +1,4 @@ +# ruff: noqa import collections import glob import os @@ -5,6 +6,7 @@ import re import subprocess import sys +from io import StringIO from itertools import chain import coverage @@ -19,16 +21,11 @@ import pytest_cov.plugin -try: - from StringIO import StringIO -except ImportError: - from io import StringIO +coverage, platform # required for skipif mark on test_cov_min_from_coveragerc -coverage, platform # required for skipif mark on test_cov_min_from_coveragerc +max_worker_restart_0 = '--max-worker-restart=0' -max_worker_restart_0 = "--max-worker-restart=0" - -SCRIPT = ''' +SCRIPT = """ import sys, helper def pytest_generate_tests(metafunc): @@ -40,24 +37,24 @@ def test_foo(p): helper.do_stuff() # get some coverage in some other completely different location if sys.version_info[0] > 5: assert False -''' +""" -SCRIPT2 = ''' +SCRIPT2 = """ # def test_bar(): x = True assert x -''' +""" -COVERAGERC_SOURCE = '''\ +COVERAGERC_SOURCE = """\ [run] source = . -''' +""" -SCRIPT_CHILD = ''' +SCRIPT_CHILD = """ import sys idx = int(sys.argv[1]) @@ -66,9 +63,9 @@ def test_bar(): foo = "a" # previously there was a "pass" here but Python 3.5 optimizes it away. if idx == 1: foo = "b" # previously there was a "pass" here but Python 3.5 optimizes it away. -''' +""" -SCRIPT_PARENT = ''' +SCRIPT_PARENT = """ import os import subprocess import sys @@ -86,9 +83,9 @@ def test_foo(idx): # there is a issue in coverage.py with multiline statements at # end of file: https://bitbucket.org/ned/coveragepy/issue/293 pass -''' +""" -SCRIPT_PARENT_CHANGE_CWD = ''' +SCRIPT_PARENT_CHANGE_CWD = """ import subprocess import sys import os @@ -110,9 +107,9 @@ def test_foo(idx): # there is a issue in coverage.py with multiline statements at # end of file: https://bitbucket.org/ned/coveragepy/issue/293 pass -''' +""" -SCRIPT_PARENT_CHANGE_CWD_IMPORT_CHILD = ''' +SCRIPT_PARENT_CHANGE_CWD_IMPORT_CHILD = """ import subprocess import sys import os @@ -133,19 +130,19 @@ def test_foo(idx): # there is a issue in coverage.py with multiline statements at # end of file: https://bitbucket.org/ned/coveragepy/issue/293 pass -''' +""" -SCRIPT_FUNCARG = ''' +SCRIPT_FUNCARG = """ import coverage def test_foo(cov): assert isinstance(cov, coverage.Coverage) -''' +""" -SCRIPT_FUNCARG_NOT_ACTIVE = ''' +SCRIPT_FUNCARG_NOT_ACTIVE = """ def test_foo(cov): assert cov is None -''' +""" CHILD_SCRIPT_RESULT = '[56] * 100%' PARENT_SCRIPT_RESULT = '9 * 100%' @@ -154,10 +151,11 @@ def test_foo(cov): JSON_REPORT_NAME = 'cov.json' LCOV_REPORT_NAME = 'cov.info' -xdist_params = pytest.mark.parametrize('opts', [ - '', - pytest.param('-n 1', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')) -], ids=['nodist', 'xdist']) +xdist_params = pytest.mark.parametrize( + 'opts', + ['', pytest.param('-n 1', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"'))], + ids=['nodist', 'xdist'], +) @pytest.fixture(scope='session', autouse=True) @@ -177,12 +175,15 @@ def adjust_sys_path(): os.environ['PYTHONPATH'] = orig_path -@pytest.fixture(params=[ - ('branch=true', '--cov-branch', '9 * 85%', '3 * 100%'), - ('branch=true', '', '9 * 85%', '3 * 100%'), - ('', '--cov-branch', '9 * 85%', '3 * 100%'), - ('', '', '9 * 89%', '3 * 100%'), -], ids=['branch2x', 'branch1c', 'branch1a', 'nobranch']) +@pytest.fixture( + params=[ + ('branch=true', '--cov-branch', '9 * 85%', '3 * 100%'), + ('branch=true', '', '9 * 85%', '3 * 100%'), + ('', '--cov-branch', '9 * 85%', '3 * 100%'), + ('', '', '9 * 89%', '3 * 100%'), + ], + ids=['branch2x', 'branch1c', 'branch1a', 'nobranch'], +) def prop(request): return Namespace( code=SCRIPT, @@ -200,149 +201,137 @@ def test_central(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script, - *prop.args) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_central* {prop.result} *', - '*10 passed*' - ]) + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_central* {prop.result} *', '*10 passed*']) assert result.ret == 0 def test_annotate(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=annotate', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=annotate', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage annotated source written next to source', - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Coverage annotated source written next to source', + '*10 passed*', + ] + ) assert result.ret == 0 def test_annotate_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=annotate:' + DEST_DIR, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=annotate:' + DEST_DIR, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage annotated source written to dir ' + DEST_DIR, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Coverage annotated source written to dir ' + DEST_DIR, + '*10 passed*', + ] + ) dest_dir = testdir.tmpdir.join(DEST_DIR) assert dest_dir.check(dir=True) - assert dest_dir.join(script.basename + ",cover").check() + assert dest_dir.join(script.basename + ',cover').check() assert result.ret == 0 def test_html(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=html', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage HTML written to dir htmlcov', - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Coverage HTML written to dir htmlcov', + '*10 passed*', + ] + ) dest_dir = testdir.tmpdir.join('htmlcov') assert dest_dir.check(dir=True) - assert dest_dir.join("index.html").check() + assert dest_dir.join('index.html').check() assert result.ret == 0 def test_html_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=html:' + DEST_DIR, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html:' + DEST_DIR, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage HTML written to dir ' + DEST_DIR, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Coverage HTML written to dir ' + DEST_DIR, + '*10 passed*', + ] + ) dest_dir = testdir.tmpdir.join(DEST_DIR) assert dest_dir.check(dir=True) - assert dest_dir.join("index.html").check() + assert dest_dir.join('index.html').check() assert result.ret == 0 def test_term_report_does_not_interact_with_html_output(testdir): script = testdir.makepyfile(test_funcarg=SCRIPT_FUNCARG) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing:skip-covered', - '--cov-report=html:' + DEST_DIR, - script) + result = testdir.runpytest( + '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing:skip-covered', '--cov-report=html:' + DEST_DIR, script + ) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage HTML written to dir ' + DEST_DIR, - '*1 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Coverage HTML written to dir ' + DEST_DIR, + '*1 passed*', + ] + ) dest_dir = testdir.tmpdir.join(DEST_DIR) assert dest_dir.check(dir=True) - assert sorted(dest_dir.visit("**/*.html")) == [dest_dir.join("index.html"), dest_dir.join("test_funcarg_py.html")] - assert dest_dir.join("index.html").check() + assert sorted(dest_dir.visit('**/*.html')) == [dest_dir.join('index.html'), dest_dir.join('test_funcarg_py.html')] + assert dest_dir.join('index.html').check() assert result.ret == 0 def test_html_configured_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [html] directory = somewhere -""") - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=html', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage HTML written to dir somewhere', - '*10 passed*', - ]) +""" + ) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html', script) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Coverage HTML written to dir somewhere', + '*10 passed*', + ] + ) dest_dir = testdir.tmpdir.join('somewhere') assert dest_dir.check(dir=True) - assert dest_dir.join("index.html").check() + assert dest_dir.join('index.html').check() assert result.ret == 0 def test_xml_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=xml:' + XML_REPORT_NAME, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=xml:' + XML_REPORT_NAME, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage XML written to file ' + XML_REPORT_NAME, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Coverage XML written to file ' + XML_REPORT_NAME, + '*10 passed*', + ] + ) assert testdir.tmpdir.join(XML_REPORT_NAME).check() assert result.ret == 0 @@ -350,105 +339,94 @@ def test_xml_output_dir(testdir): def test_json_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - '--cov=%s' % script.dirpath(), - '--cov-report=json:' + JSON_REPORT_NAME, - script) + result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=json:' + JSON_REPORT_NAME, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage JSON written to file ' + JSON_REPORT_NAME, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Coverage JSON written to file ' + JSON_REPORT_NAME, + '*10 passed*', + ] + ) assert testdir.tmpdir.join(JSON_REPORT_NAME).check() assert result.ret == 0 -@pytest.mark.skipif("coverage.version_info < (6, 3)") +@pytest.mark.skipif('coverage.version_info < (6, 3)') def test_lcov_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=lcov:' + LCOV_REPORT_NAME, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=lcov:' + LCOV_REPORT_NAME, script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Coverage LCOV written to file ' + LCOV_REPORT_NAME, - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Coverage LCOV written to file ' + LCOV_REPORT_NAME, + '*10 passed*', + ] + ) assert testdir.tmpdir.join(LCOV_REPORT_NAME).check() assert result.ret == 0 -@pytest.mark.skipif("coverage.version_info >= (6, 3)") +@pytest.mark.skipif('coverage.version_info >= (6, 3)') def test_lcov_not_supported(testdir): - script = testdir.makepyfile("a = 1") - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=lcov', - script, - ) - result.stderr.fnmatch_lines([ - '*argument --cov-report: LCOV output is only supported with coverage.py >= 6.3', - ]) + script = testdir.makepyfile('a = 1') + result = testdir.runpytest( + '-v', + f'--cov={script.dirpath()}', + '--cov-report=lcov', + script, + ) + result.stderr.fnmatch_lines( + [ + '*argument --cov-report: LCOV output is only supported with coverage.py >= 6.3', + ] + ) assert result.ret != 0 def test_term_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term:' + DEST_DIR, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term:' + DEST_DIR, script) - result.stderr.fnmatch_lines([ - f'*argument --cov-report: output specifier not supported for: "term:{DEST_DIR}"*', - ]) + result.stderr.fnmatch_lines( + [ + f'*argument --cov-report: output specifier not supported for: "term:{DEST_DIR}"*', + ] + ) assert result.ret != 0 def test_term_missing_output_dir(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing:' + DEST_DIR, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing:' + DEST_DIR, script) - result.stderr.fnmatch_lines([ - '*argument --cov-report: output specifier not supported for: ' - '"term-missing:%s"*' % DEST_DIR, - ]) + result.stderr.fnmatch_lines( + [ + '*argument --cov-report: output specifier not supported for: ' '"term-missing:%s"*' % DEST_DIR, + ] + ) assert result.ret != 0 def test_cov_min_100(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--cov-fail-under=100', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=100', script) assert result.ret != 0 - result.stdout.fnmatch_lines([ - 'FAIL Required test coverage of 100% not reached. Total coverage: *%' - ]) + result.stdout.fnmatch_lines(['FAIL Required test coverage of 100% not reached. Total coverage: *%']) def test_cov_min_100_passes_if_collectonly(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--cov-fail-under=100', - '--collect-only', - script) + result = testdir.runpytest( + '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=100', '--collect-only', script + ) assert result.ret == 0 @@ -456,75 +434,43 @@ def test_cov_min_100_passes_if_collectonly(testdir): def test_cov_min_50(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=html', - '--cov-report=xml', - '--cov-fail-under=50', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=html', '--cov-report=xml', '--cov-fail-under=50', script) assert result.ret == 0 - result.stdout.fnmatch_lines([ - 'Required test coverage of 50% reached. Total coverage: *%' - ]) + result.stdout.fnmatch_lines(['Required test coverage of 50% reached. Total coverage: *%']) def test_cov_min_float_value(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--cov-fail-under=88.88', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=88.88', script) assert result.ret == 0 - result.stdout.fnmatch_lines([ - 'Required test coverage of 88.88% reached. Total coverage: 88.89%' - ]) + result.stdout.fnmatch_lines(['Required test coverage of 88.88% reached. Total coverage: 88.89%']) def test_cov_min_float_value_not_reached(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--cov-fail-under=88.89', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=88.89', script) assert result.ret == 1 - result.stdout.fnmatch_lines([ - 'FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%' - ]) + result.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%']) def test_cov_min_no_report(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=', - '--cov-fail-under=50', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=', '--cov-fail-under=50', script) assert result.ret == 0 - result.stdout.fnmatch_lines([ - 'Required test coverage of 50% reached. Total coverage: *%' - ]) + result.stdout.fnmatch_lines(['Required test coverage of 50% reached. Total coverage: *%']) def test_central_nonspecific(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term-missing', - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_central_nonspecific* {prop.result} *', - '*10 passed*' - ]) + result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', script, *prop.args) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) # multi-module coverage report assert any(line.startswith('TOTAL ') for line in result.stdout.lines) @@ -534,15 +480,14 @@ def test_central_nonspecific(pytester, testdir, prop): def test_cov_min_from_coveragerc(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [report] fail_under = 100 -""") +""" + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret != 0 @@ -551,16 +496,15 @@ def test_central_coveragerc(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(COVERAGERC_SOURCE + prop.conf) - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term-missing', - script, *prop.args) + result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_central_coveragerc* {prop.result} *', - '*10 passed*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'test_central_coveragerc* {prop.result} *', + '*10 passed*', + ] + ) assert result.ret == 0 @@ -570,10 +514,13 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): mod1.write(SCRIPT) mod2 = testdir.mkdir('aliased').join('mod.py') mod2.write(SCRIPT) - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ from mod import * -''') - testdir.tmpdir.join('setup.cfg').write(f""" +""" + ) + testdir.tmpdir.join('setup.cfg').write( + f""" [coverage:paths] source = src @@ -582,30 +529,33 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): source = mod parallel = true {prop.conf} -""") +""" + ) monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([os.environ.get('PYTHONPATH', ''), 'aliased'])) - result = testdir.runpytest('-v', '-s', - '--cov', - '--cov-report=term-missing', - script, *opts.split()+prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'src[\\/]mod* {prop.result} *', - '*10 passed*', - ]) + result = testdir.runpytest('-v', '-s', '--cov', '--cov-report=term-missing', script, *opts.split() + prop.args) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'src[\\/]mod* {prop.result} *', + '*10 passed*', + ] + ) assert result.ret == 0 @xdist_params def test_borken_cwd(pytester, testdir, monkeypatch, opts): - testdir.makepyfile(mod=''' + testdir.makepyfile( + mod=""" def foobar(a, b): return a + b -''') +""" + ) - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import os import tempfile import pytest @@ -623,17 +573,17 @@ def bad(): def test_foobar(bad): assert mod.foobar(1, 2) == 3 -''') - result = testdir.runpytest('-v', '-s', - '--cov=mod', - '--cov-branch', - script, *opts.split()) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - '*mod* 100%', - '*1 passed*', - ]) +""" + ) + result = testdir.runpytest('-v', '-s', '--cov=mod', '--cov-branch', script, *opts.split()) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + '*mod* 100%', + '*1 passed*', + ] + ) assert result.ret == 0 @@ -647,7 +597,8 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): parent_script.write(SCRIPT_PARENT) aliased.join('child_script.py').write(SCRIPT_CHILD) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [paths] source = src @@ -657,61 +608,59 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): parent_script child_script parallel = true -""") - - monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([ - os.environ.get('PYTHONPATH', ''), 'aliased'])) - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term-missing', - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'src[\\/]child_script* {CHILD_SCRIPT_RESULT}*', - f'src[\\/]parent_script* {PARENT_SCRIPT_RESULT}*', - ]) +""" + ) + + monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([os.environ.get('PYTHONPATH', ''), 'aliased'])) + result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', parent_script) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'src[\\/]child_script* {CHILD_SCRIPT_RESULT}*', + f'src[\\/]parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 def test_show_missing_coveragerc(pytester, testdir, prop): script = testdir.makepyfile(prop.code) - testdir.tmpdir.join('.coveragerc').write(f""" + testdir.tmpdir.join('.coveragerc').write( + f""" [run] source = . {prop.conf} [report] show_missing = true -""") - - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term', - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'Name * Stmts * Miss * Cover * Missing', - f'test_show_missing_coveragerc* {prop.result} * 11*', - '*10 passed*', - ]) +""" + ) + + result = testdir.runpytest('-v', '--cov', '--cov-report=term', script, *prop.args) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'Name * Stmts * Miss * Cover * Missing', + f'test_show_missing_coveragerc* {prop.result} * 11*', + '*10 passed*', + ] + ) assert result.ret == 0 def test_no_cov_on_fail(testdir): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ def test_fail(): assert False -''') +""" + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--no-cov-on-fail', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--no-cov-on-fail', script) assert 'coverage: platform' not in result.stdout.str() result.stdout.fnmatch_lines(['*1 failed*']) @@ -719,63 +668,63 @@ def test_fail(): def test_no_cov(pytester, testdir, monkeypatch): script = testdir.makepyfile(SCRIPT) - testdir.makeini(""" + testdir.makeini( + """ [pytest] addopts=--no-cov - """) - result = testdir.runpytest('-vvv', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '-rw', - script) - result.stdout.fnmatch_lines_random([ - 'WARNING: Coverage disabled via --no-cov switch!', - '*Coverage disabled via --no-cov switch!', - ]) + """ + ) + result = testdir.runpytest('-vvv', f'--cov={script.dirpath()}', '--cov-report=term-missing', '-rw', script) + result.stdout.fnmatch_lines_random( + [ + 'WARNING: Coverage disabled via --no-cov switch!', + '*Coverage disabled via --no-cov switch!', + ] + ) def test_cov_and_failure_report_on_fail(testdir): - script = testdir.makepyfile(SCRIPT + ''' + script = testdir.makepyfile( + SCRIPT + + """ def test_fail(p): assert False -''') +""" + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-fail-under=100', - '--cov-report=html', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-fail-under=100', '--cov-report=html', script) - result.stdout.fnmatch_lines_random([ - '*10 failed*', - '*coverage: platform*', - '*FAIL Required test coverage of 100% not reached*', - '*assert False*', - ]) + result.stdout.fnmatch_lines_random( + [ + '*10 failed*', + '*coverage: platform*', + '*FAIL Required test coverage of 100% not reached*', + '*assert False*', + ] + ) @pytest.mark.skipif('sys.platform == "win32" or platform.python_implementation() == "PyPy"') def test_dist_combine_racecondition(testdir): - script = testdir.makepyfile(""" + script = testdir.makepyfile( + """ import pytest @pytest.mark.parametrize("foo", range(1000)) def test_foo(foo): -""" + "\n".join(f""" +""" + + '\n'.join( + f""" if foo == {i}: assert True -""" for i in range(1000))) - - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '-n', '5', '-s', - script) - result.stdout.fnmatch_lines([ - 'test_dist_combine_racecondition* 0 * 100%*', - '*1000 passed*' - ]) +""" + for i in range(1000) + ) + ) + + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '-n', '5', '-s', script) + result.stdout.fnmatch_lines(['test_dist_combine_racecondition* 0 * 100%*', '*1000 passed*']) for line in chain(result.stdout.lines, result.stderr.lines): assert 'The following workers failed to return coverage data' not in line @@ -787,19 +736,18 @@ def test_foo(foo): def test_dist_collocated(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - '--tx=2*popen', - max_worker_restart_0, - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_dist_collocated* {prop.result} *', - '*10 passed*' - ]) + result = testdir.runpytest( + '-v', + f'--cov={script.dirpath()}', + '--cov-report=term-missing', + '--dist=load', + '--tx=2*popen', + max_worker_restart_0, + script, + *prop.args, + ) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -808,31 +756,33 @@ def test_dist_not_collocated(pytester, testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') dir2 = testdir.mkdir('dir2') - testdir.tmpdir.join('.coveragerc').write(f''' + testdir.tmpdir.join('.coveragerc').write( + f""" [run] {prop.conf} [paths] source = . dir1 - dir2''') - - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - f'--tx=popen//chdir={dir1}', - f'--tx=popen//chdir={dir2}', - f'--rsyncdir={script.basename}', - '--rsyncdir=.coveragerc', - max_worker_restart_0, '-s', - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_dist_not_collocated* {prop.result} *', - '*10 passed*' - ]) + dir2""" + ) + + result = testdir.runpytest( + '-v', + f'--cov={script.dirpath()}', + '--cov-report=term-missing', + '--dist=load', + f'--tx=popen//chdir={dir1}', + f'--tx=popen//chdir={dir2}', + f'--rsyncdir={script.basename}', + '--rsyncdir=.coveragerc', + max_worker_restart_0, + '-s', + script, + *prop.args, + ) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -841,7 +791,8 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') dir2 = testdir.mkdir('dir2') - testdir.tmpdir.join('.coveragerc').write(f''' + testdir.tmpdir.join('.coveragerc').write( + f""" [run] {prop.conf} source = {script.dirpath()} @@ -849,66 +800,67 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): source = . dir1 - dir2''') - - result = testdir.runpytest('-v', - '--cov', - '--cov-report=term-missing', - '--dist=load', - f'--tx=popen//chdir={dir1}', - f'--tx=popen//chdir={dir2}', - f'--rsyncdir={script.basename}', - '--rsyncdir=.coveragerc', - max_worker_restart_0, '-s', - script, *prop.args) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_dist_not_collocated* {prop.result} *', - '*10 passed*' - ]) + dir2""" + ) + + result = testdir.runpytest( + '-v', + '--cov', + '--cov-report=term-missing', + '--dist=load', + f'--tx=popen//chdir={dir1}', + f'--tx=popen//chdir={dir2}', + f'--rsyncdir={script.basename}', + '--rsyncdir=.coveragerc', + max_worker_restart_0, + '-s', + script, + *prop.args, + ) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 def test_central_subprocess(testdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') - result = testdir.runpytest('-v', - f'--cov={scripts.dirpath()}', - '--cov-report=term-missing', - parent_script) + result = testdir.runpytest('-v', f'--cov={scripts.dirpath()}', '--cov-report=term-missing', parent_script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'child_script* {CHILD_SCRIPT_RESULT}*', - f'parent_script* {PARENT_SCRIPT_RESULT}*', - ]) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'child_script* {CHILD_SCRIPT_RESULT}*', + f'parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 def test_central_subprocess_change_cwd(testdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT_CHANGE_CWD, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT_CHANGE_CWD, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') - testdir.makefile('', coveragerc=""" + testdir.makefile( + '', + coveragerc=""" [run] branch = true parallel = true -""") - - result = testdir.runpytest('-v', '-s', - f'--cov={scripts.dirpath()}', - '--cov-config=coveragerc', - '--cov-report=term-missing', - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'*child_script* {CHILD_SCRIPT_RESULT}*', - '*parent_script* 100%*', - ]) +""", + ) + + result = testdir.runpytest( + '-v', '-s', f'--cov={scripts.dirpath()}', '--cov-config=coveragerc', '--cov-report=term-missing', parent_script + ) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'*child_script* {CHILD_SCRIPT_RESULT}*', + '*parent_script* 100%*', + ] + ) assert result.ret == 0 @@ -917,126 +869,130 @@ def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkey parent_script = stuff.join('parent_script.py') parent_script.write(SCRIPT_PARENT_CHANGE_CWD_IMPORT_CHILD) stuff.join('child_script.py').write(SCRIPT_CHILD) - testdir.makefile('', coveragerc=""" + testdir.makefile( + '', + coveragerc=""" [run] parallel = true -""") +""", + ) monkeypatch.setitem(os.environ, 'PYTHONPATH', str(stuff)) - result = testdir.runpytest('-vv', '-s', - '--cov=child_script', - '--cov-config=coveragerc', - '--cov-report=term-missing', - '--cov-branch', - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'*child_script* {CHILD_SCRIPT_RESULT}*', - ]) + result = testdir.runpytest( + '-vv', '-s', '--cov=child_script', '--cov-config=coveragerc', '--cov-report=term-missing', '--cov-branch', parent_script + ) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'*child_script* {CHILD_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 def test_central_subprocess_no_subscript(testdir): - script = testdir.makepyfile(""" + script = testdir.makepyfile( + """ import subprocess, sys def test_foo(): subprocess.check_call([sys.executable, '-c', 'print("Hello World")']) -""") - testdir.makefile('', coveragerc=""" +""" + ) + testdir.makefile( + '', + coveragerc=""" [run] parallel = true -""") - result = testdir.runpytest('-v', - '--cov-config=coveragerc', - f'--cov={script.dirpath()}', - '--cov-branch', - script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_central_subprocess_no_subscript* * 3 * 0 * 100%*', - ]) +""", + ) + result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-branch', script) + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + 'test_central_subprocess_no_subscript* * 3 * 0 * 100%*', + ] + ) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_subprocess_collocated(testdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') - result = testdir.runpytest('-v', - f'--cov={scripts.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - '--tx=2*popen', - max_worker_restart_0, - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'child_script* {CHILD_SCRIPT_RESULT}*', - f'parent_script* {PARENT_SCRIPT_RESULT}*', - ]) + result = testdir.runpytest( + '-v', f'--cov={scripts.dirpath()}', '--cov-report=term-missing', '--dist=load', '--tx=2*popen', max_worker_restart_0, parent_script + ) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'child_script* {CHILD_SCRIPT_RESULT}*', + f'parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') child_script = scripts.dirpath().join('child_script.py') dir1 = tmpdir.mkdir('dir1') dir2 = tmpdir.mkdir('dir2') - testdir.tmpdir.join('.coveragerc').write(f''' + testdir.tmpdir.join('.coveragerc').write( + f""" [paths] source = {scripts.dirpath()} */dir1 */dir2 -''') - result = testdir.runpytest('-v', - f'--cov={scripts.dirpath()}', - '--dist=load', - f'--tx=popen//chdir={dir1}', - f'--tx=popen//chdir={dir2}', - f'--rsyncdir={child_script}', - f'--rsyncdir={parent_script}', - '--rsyncdir=.coveragerc', - max_worker_restart_0, - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'child_script* {CHILD_SCRIPT_RESULT}*', - f'parent_script* {PARENT_SCRIPT_RESULT}*', - ]) +""" + ) + result = testdir.runpytest( + '-v', + f'--cov={scripts.dirpath()}', + '--dist=load', + f'--tx=popen//chdir={dir1}', + f'--tx=popen//chdir={dir2}', + f'--rsyncdir={child_script}', + f'--rsyncdir={parent_script}', + '--rsyncdir=.coveragerc', + max_worker_restart_0, + parent_script, + ) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'child_script* {CHILD_SCRIPT_RESULT}*', + f'parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 def test_invalid_coverage_source(testdir): script = testdir.makepyfile(SCRIPT) - testdir.makeini(""" + testdir.makeini( + """ [pytest] console_output_style=classic - """) - result = testdir.runpytest('-v', - '--cov=non_existent_module', - '--cov-report=term-missing', - script) - - result.stdout.fnmatch_lines([ - '*10 passed*' - ]) - result.stderr.fnmatch_lines([ - '*No data was collected.*' - ]) - result.stdout.fnmatch_lines([ - '*Failed to generate report: No data to report.', - ]) + """ + ) + result = 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] @@ -1045,75 +1001,64 @@ def test_invalid_coverage_source(testdir): @pytest.mark.skipif("'dev' in pytest.__version__") @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -@pytest.mark.skipif('tuple(map(int, xdist.__version__.split("."))) >= (2, 3, 0)', - reason="Since pytest-xdist 2.3.0 the parent sys.path is copied in the child process") +@pytest.mark.skipif( + 'tuple(map(int, xdist.__version__.split("."))) >= (2, 3, 0)', + reason='Since pytest-xdist 2.3.0 the parent sys.path is copied in the child process', +) def test_dist_missing_data(testdir): """Test failure when using a worker without pytest-cov installed.""" venv_path = os.path.join(str(testdir.tmpdir), 'venv') virtualenv.cli_run([venv_path]) if sys.platform == 'win32': - if platform.python_implementation() == "PyPy": + if platform.python_implementation() == 'PyPy': exe = os.path.join(venv_path, 'bin', 'python.exe') else: exe = os.path.join(venv_path, 'Scripts', 'python.exe') else: exe = os.path.join(venv_path, 'bin', 'python') - subprocess.check_call([ - exe, - '-mpip', - 'install', - f'py=={py.__version__}', - f'pytest=={pytest.__version__}', - f'pytest_xdist=={xdist.__version__}' - - ]) + subprocess.check_call( + [exe, '-mpip', 'install', f'py=={py.__version__}', f'pytest=={pytest.__version__}', f'pytest_xdist=={xdist.__version__}'] + ) script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - '--assert=plain', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - f'--tx=popen//python={exe}', - max_worker_restart_0, - str(script)) - result.stdout.fnmatch_lines([ - 'The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.' - ]) + result = testdir.runpytest( + '-v', + '--assert=plain', + f'--cov={script.dirpath()}', + '--cov-report=term-missing', + '--dist=load', + f'--tx=popen//python={exe}', + max_worker_restart_0, + str(script), + ) + result.stdout.fnmatch_lines( + ['The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.'] + ) def test_funcarg(testdir): script = testdir.makepyfile(SCRIPT_FUNCARG) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_funcarg* 3 * 100%*', - '*1 passed*' - ]) + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_funcarg* 3 * 100%*', '*1 passed*']) assert result.ret == 0 def test_funcarg_not_active(testdir): script = testdir.makepyfile(SCRIPT_FUNCARG_NOT_ACTIVE) - result = testdir.runpytest('-v', - script) + result = testdir.runpytest('-v', script) - result.stdout.fnmatch_lines([ - '*1 passed*' - ]) + result.stdout.fnmatch_lines(['*1 passed*']) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason="Interpreter seems buggy") +@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') def test_cleanup_on_sigterm(testdir): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + ''' import os, signal, subprocess, sys, time def cleanup(num, frame): @@ -1141,30 +1086,28 @@ def test_run(): time.sleep(10) except BaseException as exc: print("captured %r" % exc) -''') +''' + ) - result = testdir.runpytest('-vv', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_cleanup_on_sigterm* 26-27', - '*1 passed*' - ]) + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 26-27', '*1 passed*']) assert result.ret == 0 @pytest.mark.skipif('sys.platform != "win32"') -@pytest.mark.parametrize('setup', [ - ('signal.signal(signal.SIGBREAK, signal.SIG_DFL); cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), - ('cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), - ('cleanup()', '73% 19-22'), -]) +@pytest.mark.parametrize( + 'setup', + [ + ('signal.signal(signal.SIGBREAK, signal.SIG_DFL); cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), + ('cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), + ('cleanup()', '73% 19-22'), + ], +) def test_cleanup_on_sigterm_sig_break(pytester, testdir, setup): # worth a read: https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/ - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import os, signal, subprocess, sys, time def test_run(): @@ -1181,37 +1124,37 @@ def test_run(): if __name__ == "__main__": from pytest_cov.embed import cleanup_on_signal, cleanup - ''' + setup[0] + ''' + """ + + setup[0] + + """ try: time.sleep(10) except BaseException as exc: print("captured %r" % exc) -''') +""" + ) - result = testdir.runpytest('-vv', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_cleanup_on_sigterm* {setup[1]}', - '*1 passed*' - ]) + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") -@pytest.mark.xfail('sys.platform == "darwin"', reason="Something weird going on Macs...") -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason="Interpreter seems buggy") -@pytest.mark.parametrize('setup', [ - ('signal.signal(signal.SIGTERM, signal.SIG_DFL); cleanup_on_sigterm()', '88% 18-19'), - ('cleanup_on_sigterm()', '88% 18-19'), - ('cleanup()', '75% 16-19'), -]) +@pytest.mark.xfail('sys.platform == "darwin"', reason='Something weird going on Macs...') +@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') +@pytest.mark.parametrize( + 'setup', + [ + ('signal.signal(signal.SIGTERM, signal.SIG_DFL); cleanup_on_sigterm()', '88% 18-19'), + ('cleanup_on_sigterm()', '88% 18-19'), + ('cleanup()', '75% 16-19'), + ], +) def test_cleanup_on_sigterm_sig_dfl(pytester, testdir, setup): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import os, signal, subprocess, sys, time def test_run(): @@ -1225,33 +1168,29 @@ def test_run(): if __name__ == "__main__": from pytest_cov.embed import cleanup_on_sigterm, cleanup - ''' + setup[0] + ''' + """ + + setup[0] + + """ try: time.sleep(10) except BaseException as exc: print("captured %r" % exc) -''') - - result = testdir.runpytest('-vv', - '--assert=plain', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_cleanup_on_sigterm* {setup[1]}', - '*1 passed*' - ]) +""" + ) + + result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) assert result.ret == 0 -@pytest.mark.skipif('sys.platform == "win32"', reason="SIGINT is subtly broken on Windows") -@pytest.mark.xfail('sys.platform == "darwin"', reason="Something weird going on Macs...") -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason="Interpreter seems buggy") +@pytest.mark.skipif('sys.platform == "win32"', reason='SIGINT is subtly broken on Windows') +@pytest.mark.xfail('sys.platform == "darwin"', reason='Something weird going on Macs...') +@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') def test_cleanup_on_sigterm_sig_dfl_sigint(testdir): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + ''' import os, signal, subprocess, sys, time def test_run(): @@ -1272,26 +1211,20 @@ def test_run(): time.sleep(10) except BaseException as exc: print("captured %r" % exc) -''') - - result = testdir.runpytest('-vv', - '--assert=plain', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_cleanup_on_sigterm* 88% 19-20', - '*1 passed*' - ]) +''' + ) + + result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) assert result.ret == 0 -@pytest.mark.skipif('sys.platform == "win32"', reason="fork not available on Windows") -@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason="Interpreter seems buggy") +@pytest.mark.skipif('sys.platform == "win32"', reason='fork not available on Windows') +@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason='Interpreter seems buggy') def test_cleanup_on_sigterm_sig_ign(testdir): - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import os, signal, subprocess, sys, time def test_run(): @@ -1303,8 +1236,7 @@ def test_run(): stdout, stderr = proc.communicate() assert not stderr assert stdout == b"" - # it appears signal handling is buggy on python 2? - if sys.version_info == 3: assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] + assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] if __name__ == "__main__": signal.signal(signal.SIGINT, signal.SIG_IGN) @@ -1316,42 +1248,35 @@ def test_run(): time.sleep(10) except BaseException as exc: print("captured %r" % exc) - ''') - - result = testdir.runpytest('-vv', - '--assert=plain', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - 'test_cleanup_on_sigterm* 89% 23-24', - '*1 passed*' - ]) + """ + ) + + result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) assert result.ret == 0 -MODULE = ''' +MODULE = """ def func(): return 1 -''' +""" -CONFTEST = ''' +CONFTEST = """ import mod mod.func() -''' +""" -BASIC_TEST = ''' +BASIC_TEST = """ def test_basic(): x = True assert x -''' +""" CONF_RESULT = 'mod* 2 * 100%*' @@ -1360,10 +1285,7 @@ def test_cover_conftest(testdir): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) script = testdir.makepyfile(BASIC_TEST) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines([CONF_RESULT]) @@ -1382,16 +1304,9 @@ def mock_run(*args, **kwargs): if hasattr(testdir, '_pytester'): monkeypatch.setattr(testdir._pytester, 'run', mock_run) assert testdir._pytester.run is mock_run - with testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--looponfail', - script) as process: + with testdir.runpytest('-v', f'--cov={script.dirpath()}', '--looponfail', script) as process: with dump_on_error(process.read): - wait_for_strings( - process.read, - 30, # 30 seconds - 'Stmts Miss Cover' - ) + wait_for_strings(process.read, 30, 'Stmts Miss Cover') # 30 seconds @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') @@ -1399,20 +1314,17 @@ def test_cover_conftest_dist(testdir): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) script = testdir.makepyfile(BASIC_TEST) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '--dist=load', - '--tx=2*popen', - max_worker_restart_0, - script) + result = testdir.runpytest( + '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--dist=load', '--tx=2*popen', max_worker_restart_0, script + ) assert result.ret == 0 result.stdout.fnmatch_lines([CONF_RESULT]) def test_no_cover_marker(testdir): testdir.makepyfile(mod=MODULE) - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import pytest import mod import subprocess @@ -1422,18 +1334,17 @@ def test_no_cover_marker(testdir): def test_basic(): mod.func() subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) -''') - result = testdir.runpytest('-v', '-ra', '--strict', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) +""" + ) + result = testdir.runpytest('-v', '-ra', '--strict', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines(['mod* 2 * 1 * 50% * 2']) def test_no_cover_fixture(testdir): testdir.makepyfile(mod=MODULE) - script = testdir.makepyfile(''' + script = testdir.makepyfile( + """ import mod import subprocess import sys @@ -1441,23 +1352,28 @@ def test_no_cover_fixture(testdir): def test_basic(no_cover): mod.func() subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) -''') - result = testdir.runpytest('-v', '-ra', '--strict', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) +""" + ) + result = testdir.runpytest('-v', '-ra', '--strict', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines(['mod* 2 * 1 * 50% * 2']) -COVERAGERC = ''' +COVERAGERC = """ [report] # Regexes for lines to exclude from consideration exclude_lines = raise NotImplementedError -''' +""" +PYPROJECTTOML = """ +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_lines = [ + 'raise NotImplementedError', +] +""" -EXCLUDED_TEST = ''' +EXCLUDED_TEST = """ def func(): raise NotImplementedError @@ -1466,7 +1382,7 @@ def test_basic(): x = True assert x -''' +""" EXCLUDED_RESULT = '4 * 100%*' @@ -1474,38 +1390,37 @@ def test_basic(): def test_coveragerc(testdir): testdir.makefile('', coveragerc=COVERAGERC) script = testdir.makepyfile(EXCLUDED_TEST) - result = testdir.runpytest('-v', - '--cov-config=coveragerc', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 result.stdout.fnmatch_lines([f'test_coveragerc* {EXCLUDED_RESULT}']) +def test_pyproject_toml(testdir): + testdir.makefile('.toml', pyproject=PYPROJECTTOML) + script = testdir.makepyfile(EXCLUDED_TEST) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + assert result.ret == 0 + result.stdout.fnmatch_lines([f'test_pyproject_toml* {EXCLUDED_RESULT}']) + + @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_coveragerc_dist(testdir): testdir.makefile('', coveragerc=COVERAGERC) script = testdir.makepyfile(EXCLUDED_TEST) - result = testdir.runpytest('-v', - '--cov-config=coveragerc', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - '-n', '2', - max_worker_restart_0, - script) + result = testdir.runpytest( + '-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-report=term-missing', '-n', '2', max_worker_restart_0, script + ) assert result.ret == 0 - result.stdout.fnmatch_lines( - [f'test_coveragerc_dist* {EXCLUDED_RESULT}']) + result.stdout.fnmatch_lines([f'test_coveragerc_dist* {EXCLUDED_RESULT}']) -SKIP_COVERED_COVERAGERC = ''' +SKIP_COVERED_COVERAGERC = """ [report] skip_covered = True -''' +""" -SKIP_COVERED_TEST = ''' +SKIP_COVERED_TEST = """ def func(): return "full coverage" @@ -1513,21 +1428,16 @@ def func(): def test_basic(): assert func() == "full coverage" -''' +""" SKIP_COVERED_RESULT = '1 file skipped due to complete coverage.' -@pytest.mark.parametrize('report_option', [ - 'term-missing:skip-covered', - 'term:skip-covered']) +@pytest.mark.parametrize('report_option', ['term-missing:skip-covered', 'term:skip-covered']) def test_skip_covered_cli(pytester, testdir, report_option): testdir.makefile('', coveragerc=SKIP_COVERED_COVERAGERC) script = testdir.makepyfile(SKIP_COVERED_TEST) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - f'--cov-report={report_option}', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', f'--cov-report={report_option}', script) assert result.ret == 0 result.stdout.fnmatch_lines([SKIP_COVERED_RESULT]) @@ -1535,81 +1445,58 @@ def test_skip_covered_cli(pytester, testdir, report_option): def test_skip_covered_coveragerc_config(testdir): testdir.makefile('', coveragerc=SKIP_COVERED_COVERAGERC) script = testdir.makepyfile(SKIP_COVERED_TEST) - result = testdir.runpytest('-v', - '--cov-config=coveragerc', - f'--cov={script.dirpath()}', - script) + result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', script) assert result.ret == 0 result.stdout.fnmatch_lines([SKIP_COVERED_RESULT]) -CLEAR_ENVIRON_TEST = ''' +CLEAR_ENVIRON_TEST = """ import os def test_basic(): os.environ.clear() -''' +""" def test_clear_environ(testdir): script = testdir.makepyfile(CLEAR_ENVIRON_TEST) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=term-missing', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) assert result.ret == 0 -SCRIPT_SIMPLE = ''' +SCRIPT_SIMPLE = """ def test_foo(): assert 1 == 1 x = True assert x -''' +""" SCRIPT_SIMPLE_RESULT = '4 * 100%' -@pytest.mark.skipif('tuple(map(int, xdist.__version__.split("."))) >= (3, 0, 2)', - reason="--boxed option was removed in version 3.0.2") +@pytest.mark.skipif('tuple(map(int, xdist.__version__.split("."))) >= (3, 0, 2)', reason='--boxed option was removed in version 3.0.2') @pytest.mark.skipif('sys.platform == "win32"') def test_dist_boxed(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--assert=plain', - f'--cov={script.dirpath()}', - '--boxed', - script) + result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--boxed', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', - '*1 passed*' - ]) + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @pytest.mark.skipif('sys.platform == "win32"') -@pytest.mark.skipif('sys.version_info[0] > 2 and platform.python_implementation() == "PyPy"', - reason="strange optimization on PyPy3") +@pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason='strange optimization on PyPy3') def test_dist_bare_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--cov', - '-n', '1', - script) + result = testdir.runpytest('-v', '--cov', '-n', '1', script) - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', - '*1 passed*' - ]) + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1626,28 +1513,21 @@ class ns: def test_default_output_setting(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines([ - '*coverage*' - ]) + result.stdout.fnmatch_lines(['*coverage*']) assert result.ret == 0 def test_disabled_output(testdir): script = testdir.makepyfile(SCRIPT) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-report=', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=', script) stdout = result.stdout.str() # We don't want the path to the executable to fail the test if we happen # to put the project in a directory with "coverage" in it. - stdout = stdout.replace(sys.executable, "") + stdout = stdout.replace(sys.executable, '') assert 'coverage' not in stdout assert result.ret == 0 @@ -1657,8 +1537,7 @@ def test_coverage_file(testdir): data_file_name = 'covdata' os.environ['COVERAGE_FILE'] = data_file_name try: - result = testdir.runpytest('-v', f'--cov={script.dirpath()}', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 data_file = testdir.tmpdir.join(data_file_name) assert data_file.check() @@ -1668,14 +1547,15 @@ def test_coverage_file(testdir): def test_external_data_file(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [run] data_file = %s -""" % testdir.tmpdir.join('some/special/place/coverage-data').ensure()) +""" + % testdir.tmpdir.join('some/special/place/coverage-data').ensure() + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) @@ -1683,33 +1563,31 @@ def test_external_data_file(testdir): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_external_data_file_xdist(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write(""" + testdir.tmpdir.join('.coveragerc').write( + """ [run] parallel = true data_file = %s -""" % testdir.tmpdir.join('some/special/place/coverage-data').ensure()) +""" + % testdir.tmpdir.join('some/special/place/coverage-data').ensure() + ) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '-n', '1', - max_worker_restart_0, - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '-n', '1', max_worker_restart_0, script) assert result.ret == 0 assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_xdist_no_data_collected(testdir): - testdir.makepyfile(target="x = 123") - script = testdir.makepyfile(""" + testdir.makepyfile(target='x = 123') + script = testdir.makepyfile( + """ import target def test_foobar(): assert target.x == 123 -""") - result = testdir.runpytest('-v', - '--cov=target', - '-n', '1', - script) +""" + ) + result = testdir.runpytest('-v', '--cov=target', '-n', '1', script) assert 'no-data-collected' not in result.stderr.str() assert 'no-data-collected' not in result.stdout.str() assert 'module-not-imported' not in result.stderr.str() @@ -1719,11 +1597,9 @@ def test_foobar(): def test_external_data_file_negative(testdir): script = testdir.makepyfile(SCRIPT) - testdir.tmpdir.join('.coveragerc').write("") + testdir.tmpdir.join('.coveragerc').write('') - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script) assert result.ret == 0 assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) @@ -1732,67 +1608,65 @@ def test_external_data_file_negative(testdir): def test_append_coverage(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script, - *opts.split() + prop.args) - result.stdout.fnmatch_lines([ - f'test_1* {prop.result}*', - ]) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + ] + ) script2 = testdir.makepyfile(test_2=prop.code2) - result = testdir.runpytest('-v', - '--cov-append', - f'--cov={script2.dirpath()}', - script2, - *opts.split() + prop.args) - result.stdout.fnmatch_lines([ - f'test_1* {prop.result}*', - f'test_2* {prop.result2}*', - ]) + result = testdir.runpytest('-v', '--cov-append', f'--cov={script2.dirpath()}', script2, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + f'test_2* {prop.result2}*', + ] + ) @xdist_params def test_do_not_append_coverage(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - script, - *opts.split()+prop.args) - result.stdout.fnmatch_lines([ - f'test_1* {prop.result}*', - ]) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + ] + ) script2 = testdir.makepyfile(test_2=prop.code2) - result = testdir.runpytest('-v', - f'--cov={script2.dirpath()}', - script2, - *opts.split()+prop.args) - result.stdout.fnmatch_lines([ - 'test_1* 0%', - f'test_2* {prop.result2}*', - ]) + result = testdir.runpytest('-v', f'--cov={script2.dirpath()}', script2, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + 'test_1* 0%', + f'test_2* {prop.result2}*', + ] + ) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_append_coverage_subprocess(testdir): - scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, - child_script=SCRIPT_CHILD) + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') - result = testdir.runpytest('-v', - f'--cov={scripts.dirpath()}', - '--cov-append', - '--cov-report=term-missing', - '--dist=load', - '--tx=2*popen', - max_worker_restart_0, - parent_script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'child_script* {CHILD_SCRIPT_RESULT}*', - f'parent_script* {PARENT_SCRIPT_RESULT}*', - ]) + result = testdir.runpytest( + '-v', + f'--cov={scripts.dirpath()}', + '--cov-append', + '--cov-report=term-missing', + '--dist=load', + '--tx=2*popen', + max_worker_restart_0, + parent_script, + ) + + result.stdout.fnmatch_lines( + [ + '*- coverage: platform *, python * -*', + f'child_script* {CHILD_SCRIPT_RESULT}*', + f'parent_script* {PARENT_SCRIPT_RESULT}*', + ] + ) assert result.ret == 0 @@ -1804,7 +1678,7 @@ class SpecificError(Exception): pass def bad_init(): - raise SpecificError() + raise SpecificError buff = StringIO() @@ -1814,78 +1688,44 @@ def bad_init(): monkeypatch.setattr(sys, 'stderr', buff) monkeypatch.setitem(os.environ, 'COV_CORE_SOURCE', 'foobar') exec(payload) - expected = ( - "pytest-cov: Failed to setup subprocess coverage. " - "Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" - ) + expected = "pytest-cov: Failed to setup subprocess coverage. " "Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" assert buff.getvalue() == expected def test_double_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--assert=plain', - '--cov', f'--cov={script.dirpath()}', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', - '*1 passed*' - ]) + result = testdir.runpytest('-v', '--assert=plain', '--cov', f'--cov={script.dirpath()}', script) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 def test_double_cov2(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--assert=plain', - '--cov', '--cov', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', - '*1 passed*' - ]) + result = testdir.runpytest('-v', '--assert=plain', '--cov', '--cov', script) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 def test_cov_reset(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--assert=plain', - f'--cov={script.dirpath()}', - '--cov-reset', - script) + result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-reset', script) assert 'coverage: platform' not in result.stdout.str() def test_cov_reset_then_set(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--assert=plain', - f'--cov={script.dirpath()}', - '--cov-reset', - f'--cov={script.dirpath()}', - script) - - result.stdout.fnmatch_lines([ - '*- coverage: platform *, python * -*', - f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', - '*1 passed*' - ]) + result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-reset', f'--cov={script.dirpath()}', script) + + result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_cov_and_no_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) - result = testdir.runpytest('-v', - '--cov', '--no-cov', - '-n', '1', - '-s', - script) + result = testdir.runpytest('-v', '--cov', '--no-cov', '-n', '1', '-s', script) assert 'Coverage disabled via --no-cov switch!' not in result.stdout.str() assert 'Coverage disabled via --no-cov switch!' not in result.stderr.str() assert result.ret == 0 @@ -1935,56 +1775,56 @@ def find_labels(text, pattern): } -@pytest.mark.skipif("coverage.version_info < (5, 0)") -@pytest.mark.skipif("coverage.version_info > (6, 4)") +@pytest.mark.skipif('coverage.version_info < (5, 0)') +@pytest.mark.skipif('coverage.version_info > (6, 4)') @xdist_params def test_contexts(pytester, testdir, opts): - with open(os.path.join(os.path.dirname(__file__), "contextful.py")) as f: + with open(os.path.join(os.path.dirname(__file__), 'contextful.py')) as f: contextful_tests = f.read() script = testdir.makepyfile(contextful_tests) - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-context=test', - script, - *opts.split() - ) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-context=test', script, *opts.split()) assert result.ret == 0 - result.stdout.fnmatch_lines([ - 'test_contexts* 100%*', - ]) + result.stdout.fnmatch_lines( + [ + 'test_contexts* 100%*', + ] + ) - data = coverage.CoverageData(".coverage") + data = coverage.CoverageData('.coverage') data.read() assert data.measured_contexts() == set(EXPECTED_CONTEXTS) measured = data.measured_files() assert len(measured) == 1 - test_context_path = list(measured)[0] - assert test_context_path.lower() == os.path.abspath("test_contexts.py").lower() + test_context_path = next(iter(measured)) + assert test_context_path.lower() == os.path.abspath('test_contexts.py').lower() - line_data = find_labels(contextful_tests, r"[crst]\d+(?:-\d+)?") + line_data = find_labels(contextful_tests, r'[crst]\d+(?:-\d+)?') for context, label in EXPECTED_CONTEXTS.items(): if context == '': continue data.set_query_context(context) actual = set(data.lines(test_context_path)) - assert line_data[label] == actual, f"Wrong lines for context {context!r}" + assert line_data[label] == actual, f'Wrong lines for context {context!r}' -@pytest.mark.skipif("coverage.version_info >= (5, 0)") +@pytest.mark.skipif('coverage.version_info >= (5, 0)') def test_contexts_not_supported(testdir): - script = testdir.makepyfile("a = 1") - result = testdir.runpytest('-v', - f'--cov={script.dirpath()}', - '--cov-context=test', - script, - ) - result.stderr.fnmatch_lines([ - '*argument --cov-context: Contexts are only supported with coverage.py >= 5.x', - ]) + script = testdir.makepyfile('a = 1') + result = testdir.runpytest( + '-v', + f'--cov={script.dirpath()}', + '--cov-context=test', + script, + ) + result.stderr.fnmatch_lines( + [ + '*argument --cov-context: Contexts are only supported with coverage.py >= 5.x', + ] + ) assert result.ret != 0 def test_issue_417(testdir): # https://github.com/pytest-dev/pytest-cov/issues/417 - whatever = testdir.maketxtfile(whatever="") + whatever = testdir.maketxtfile(whatever='') testdir.inline_genitems(whatever) diff --git a/tox.ini b/tox.ini index 7d49a3be..4cbe6c8a 100644 --- a/tox.ini +++ b/tox.ini @@ -7,17 +7,31 @@ commands = python ci/bootstrap.py --no-env passenv = * -; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist +; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments [tox] envlist = - check - py{37,38,39,310,311,py37,py38,py39}-pytest{73}-xdist330-coverage{72} - docs + clean, + check, + docs, + {py38,py39,py310,py311,py312,pypy38,pypy39,pypy310}-pytest{81}-xdist{350}-coverage{74}, + report +ignore_basepython_conflict = true [testenv] +basepython = + pypy38: {env:TOXPYTHON:pypy3.8} + pypy39: {env:TOXPYTHON:pypy3.9} + pypy310: {env:TOXPYTHON:pypy3.10} + py38: {env:TOXPYTHON:python3.8} + py39: {env:TOXPYTHON:python3.9} + py310: {env:TOXPYTHON:python3.10} + py311: {env:TOXPYTHON:python3.11} + py312: {env:TOXPYTHON:python3.12} + {bootstrap,clean,check,report,docs}: {env:TOXPYTHON:python3} extras = testing setenv = + PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes # Use env vars for (optional) pinning of deps. @@ -30,7 +44,10 @@ setenv = pytest70: _DEP_PYTEST=pytest==7.0.1 pytest71: _DEP_PYTEST=pytest==7.1.2 pytest72: _DEP_PYTEST=pytest==7.2.0 - pytest73: _DEP_PYTEST=pytest==7.3.1 + pytest73: _DEP_PYTEST=pytest==7.3.3 + pytest74: _DEP_PYTEST=pytest==7.4.4 + pytest80: _DEP_PYTEST=pytest==8.0.2 + pytest81: _DEP_PYTEST=pytest==8.1.1 xdist127: _DEP_PYTESTXDIST=pytest-xdist==1.27.0 xdist129: _DEP_PYTESTXDIST=pytest-xdist==1.29.0 @@ -44,6 +61,8 @@ setenv = xdist250: _DEP_PYTESTXDIST=pytest-xdist==2.5.0 xdist320: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 xdist330: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 + xdist340: _DEP_PYTESTXDIST=pytest-xdist==3.4.0 + xdist350: _DEP_PYTESTXDIST=pytest-xdist==3.5.0 xdistdev: _DEP_PYTESTXDIST=git+https://github.com/pytest-dev/pytest-xdist.git#egg=pytest-xdist coverage45: _DEP_COVERAGE=coverage==4.5.4 @@ -59,7 +78,9 @@ setenv = coverage63: _DEP_COVERAGE=coverage==6.3.3 coverage64: _DEP_COVERAGE=coverage==6.4.2 coverage65: _DEP_COVERAGE=coverage==6.5.0 - coverage72: _DEP_COVERAGE=coverage==7.2.5 + coverage72: _DEP_COVERAGE=coverage==7.2.7 + coverage73: _DEP_COVERAGE=coverage==7.3.4 + coverage74: _DEP_COVERAGE=coverage==7.4.4 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv = @@ -72,37 +93,38 @@ pip_pre = true commands = {posargs:pytest -vv} -[testenv:spell] -setenv = - SPELLCHECK=1 -commands = - sphinx-build -b spelling docs dist/docs -skip_install = true -usedevelop = false +[testenv:check] deps = - -r{toxinidir}/docs/requirements.txt - sphinxcontrib-spelling - pyenchant + docutils + check-manifest + pre-commit + readme-renderer + pygments + isort +skip_install = true +commands = + python setup.py check --strict --metadata --restructuredtext + check-manifest . + pre-commit run --all-files --show-diff-on-failure [testenv:docs] +usedevelop = true deps = -r{toxinidir}/docs/requirements.txt commands = sphinx-build {posargs:-E} -b html docs dist/docs + sphinx-build -b linkcheck docs dist/docs -[testenv:check] +[testenv:report] deps = - check-manifest - colorama # TODO Remove when isort > v6.0.0b2 is released. - docutils - flake8 - isort - pygments - readme-renderer + coverage skip_install = true -usedevelop = false commands = - python setup.py check --strict --metadata --restructuredtext - check-manifest {toxinidir} - flake8 src tests setup.py - isort --check-only --diff src tests setup.py + coverage report + coverage html + +[testenv:clean] +commands = coverage erase +skip_install = true +deps = + coverage