From 56515f7087e7f8711d3b1d664a461c1c5fde985d Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Mon, 12 Apr 2021 15:19:03 +0900 Subject: [PATCH] Imported Upstream version python3-pytest-cov 2.10.1 --- .appveyor.yml | 33 + .bumpversion.cfg | 21 + .cookiecutterrc | 56 + .editorconfig | 16 + .github/ISSUE_TEMPLATE/bug_report.md | 31 + .github/ISSUE_TEMPLATE/feature_request.md | 18 + .github/ISSUE_TEMPLATE/support_request.md | 40 + .pre-commit-config.yaml | 20 + .readthedocs.yml | 11 + .travis.yml | 115 + AUTHORS.rst | 41 + CHANGELOG.rst | 247 ++ CONTRIBUTING.rst | 90 + LICENSE | 21 + MANIFEST.in | 26 + PKG-INFO | 447 ++++ README.rst | 167 ++ ci/appveyor-with-compiler.cmd | 23 + ci/bootstrap.py | 93 + ci/requirements.txt | 4 + ci/templates/.appveyor.yml | 32 + ci/templates/.travis.yml | 61 + docs/authors.rst | 1 + docs/changelog.rst | 1 + docs/conf.py | 54 + docs/config.rst | 66 + docs/contexts.rst | 18 + docs/contributing.rst | 1 + docs/debuggers.rst | 15 + docs/index.rst | 29 + docs/markers-fixtures.rst | 43 + docs/plugins.rst | 24 + docs/readme.rst | 1 + docs/releasing.rst | 34 + docs/reporting.rst | 74 + docs/requirements.txt | 4 + docs/spelling_wordlist.txt | 11 + docs/subprocess-support.rst | 174 ++ docs/tox.rst | 73 + docs/xdist.rst | 74 + examples/README.rst | 15 + examples/adhoc-layout/.coveragerc | 15 + examples/adhoc-layout/example/__init__.py | 13 + examples/adhoc-layout/setup.py | 7 + examples/adhoc-layout/tests/test_example.py | 6 + examples/adhoc-layout/tox.ini | 36 + examples/src-layout/.coveragerc | 15 + examples/src-layout/setup.py | 8 + examples/src-layout/src/example/__init__.py | 13 + examples/src-layout/tests/test_example.py | 6 + examples/src-layout/tox.ini | 34 + setup.cfg | 27 + setup.py | 154 ++ src/pytest-cov.embed | 13 + src/pytest-cov.pth | 1 + src/pytest_cov.egg-info/PKG-INFO | 447 ++++ src/pytest_cov.egg-info/SOURCES.txt | 72 + src/pytest_cov.egg-info/dependency_links.txt | 1 + src/pytest_cov.egg-info/entry_points.txt | 6 + src/pytest_cov.egg-info/not-zip-safe | 1 + src/pytest_cov.egg-info/requires.txt | 10 + src/pytest_cov.egg-info/top_level.txt | 1 + src/pytest_cov/__init__.py | 2 + src/pytest_cov/compat.py | 31 + src/pytest_cov/embed.py | 137 ++ src/pytest_cov/engine.py | 401 ++++ src/pytest_cov/plugin.py | 380 ++++ tests/conftest.py | 2 + tests/contextful.py | 105 + tests/helper.py | 3 + tests/test_pytest_cov.py | 2115 ++++++++++++++++++ tox.ini | 79 + 72 files changed, 6466 insertions(+) create mode 100644 .appveyor.yml create mode 100644 .bumpversion.cfg create mode 100644 .cookiecutterrc create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/support_request.md create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yml create mode 100644 .travis.yml create mode 100644 AUTHORS.rst create mode 100644 CHANGELOG.rst create mode 100644 CONTRIBUTING.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 PKG-INFO create mode 100644 README.rst create mode 100644 ci/appveyor-with-compiler.cmd create mode 100755 ci/bootstrap.py create mode 100644 ci/requirements.txt create mode 100644 ci/templates/.appveyor.yml create mode 100644 ci/templates/.travis.yml create mode 100644 docs/authors.rst create mode 100644 docs/changelog.rst create mode 100644 docs/conf.py create mode 100644 docs/config.rst create mode 100644 docs/contexts.rst create mode 100644 docs/contributing.rst create mode 100644 docs/debuggers.rst create mode 100644 docs/index.rst create mode 100644 docs/markers-fixtures.rst create mode 100644 docs/plugins.rst create mode 100644 docs/readme.rst create mode 100644 docs/releasing.rst create mode 100644 docs/reporting.rst create mode 100644 docs/requirements.txt create mode 100644 docs/spelling_wordlist.txt create mode 100644 docs/subprocess-support.rst create mode 100644 docs/tox.rst create mode 100644 docs/xdist.rst create mode 100644 examples/README.rst create mode 100644 examples/adhoc-layout/.coveragerc create mode 100644 examples/adhoc-layout/example/__init__.py create mode 100644 examples/adhoc-layout/setup.py create mode 100644 examples/adhoc-layout/tests/test_example.py create mode 100644 examples/adhoc-layout/tox.ini create mode 100644 examples/src-layout/.coveragerc create mode 100644 examples/src-layout/setup.py create mode 100644 examples/src-layout/src/example/__init__.py create mode 100644 examples/src-layout/tests/test_example.py create mode 100644 examples/src-layout/tox.ini create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 src/pytest-cov.embed create mode 100644 src/pytest-cov.pth create mode 100644 src/pytest_cov.egg-info/PKG-INFO create mode 100644 src/pytest_cov.egg-info/SOURCES.txt create mode 100644 src/pytest_cov.egg-info/dependency_links.txt create mode 100644 src/pytest_cov.egg-info/entry_points.txt create mode 100644 src/pytest_cov.egg-info/not-zip-safe create mode 100644 src/pytest_cov.egg-info/requires.txt create mode 100644 src/pytest_cov.egg-info/top_level.txt create mode 100644 src/pytest_cov/__init__.py create mode 100644 src/pytest_cov/compat.py create mode 100644 src/pytest_cov/embed.py create mode 100644 src/pytest_cov/engine.py create mode 100644 src/pytest_cov/plugin.py create mode 100644 tests/conftest.py create mode 100644 tests/contextful.py create mode 100644 tests/helper.py create mode 100644 tests/test_pytest_cov.py create mode 100644 tox.ini diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..b5c6654 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,33 @@ +# NOTE: this file is auto-generated via ci/bootstrap.py (ci/templates/.appveyor.yml). +version: '{branch}-{build}' +build: off +environment: + matrix: + - TOXENV: check + - TOXENV: 'py27-pytest46-xdist27-coverage45,py27-pytest46-xdist27-coverage52' + - TOXENV: 'py35-pytest46-xdist27-coverage45,py35-pytest46-xdist27-coverage52' + - TOXENV: 'py36-pytest46-xdist27-coverage45,py36-pytest46-xdist27-coverage52,py36-pytest46-xdist33-coverage45,py36-pytest46-xdist33-coverage52,py36-pytest54-xdist33-coverage45,py36-pytest54-xdist33-coverage52,py36-pytest60-xdist200-coverage52' + - TOXENV: 'py37-pytest46-xdist27-coverage45,py37-pytest46-xdist27-coverage52,py37-pytest46-xdist33-coverage45,py37-pytest46-xdist33-coverage52,py37-pytest54-xdist33-coverage45,py37-pytest54-xdist33-coverage52,py37-pytest60-xdist200-coverage52' + - TOXENV: 'pypy-pytest46-xdist27-coverage45,pypy-pytest46-xdist27-coverage52' + +init: + - ps: echo $env:TOXENV + - ps: ls C:\Python* +install: + - IF "%TOXENV:~0,5%" == "pypy-" choco install --no-progress python.pypy + - IF "%TOXENV:~0,6%" == "pypy3-" choco install --no-progress pypy3 + - SET PATH=C:\tools\pypy\pypy;%PATH% + - C:\Python37\python -m pip install --progress-bar=off tox -rci/requirements.txt + +test_script: + - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd C:\Python37\python -m tox + +on_failure: + - ps: dir "env:" + - ps: get-content .tox\*\log\* +artifacts: + - path: dist\* + +### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..fc1162d --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,21 @@ +[bumpversion] +current_version = 2.10.1 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version='{current_version}' +replace = version='{new_version}' + +[bumpversion:file:README.rst] +search = v{current_version}. +replace = v{new_version}. + +[bumpversion:file:docs/conf.py] +search = version = release = '{current_version}' +replace = version = release = '{new_version}' + +[bumpversion:file:src/pytest_cov/__init__.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + diff --git a/.cookiecutterrc b/.cookiecutterrc new file mode 100644 index 0000000..c477a6f --- /dev/null +++ b/.cookiecutterrc @@ -0,0 +1,56 @@ +# Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) + +cookiecutter: + _extensions: + - jinja2_time.TimeExtension + _template: /home/ionel/open-source/cookiecutter-pylibrary + allow_tests_inside_package: no + appveyor: yes + 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 + codacy_projectid: '[Get ID from https://app.codacy.com/app/ionelmc/pytest-cov/settings]' + codeclimate: no + codecov: no + command_line_interface: no + command_line_interface_bin_name: '-' + coveralls: no + coveralls_token: '[Required for Appveyor, take it from https://coveralls.io/github/ionelmc/pytest-cov]' + distribution_name: pytest-cov + email: contact@ionelmc.ro + full_name: Ionel Cristian Mărieș + landscape: no + license: MIT license + linter: flake8 + package_name: pytest_cov + pre_commit: yes + project_name: pytest-cov + project_short_description: This plugin produces coverage reports. It supports centralised testing and distributed testing in both load and each modes. It also supports coverage of subprocesses. + pypi_badge: yes + pypi_disable_upload: no + release_date: '2020-06-12' + repo_hosting: github.com + repo_hosting_domain: github.com + 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 + sphinx_docs_hosting: https://pytest-cov.readthedocs.io/ + sphinx_doctest: no + sphinx_theme: sphinx-py3doc-enhanced-theme + test_matrix_configurator: no + test_matrix_separate_coverage: no + test_runner: pytest + travis: yes + travis_osx: no + version: 2.10.0 + website: http://blog.ionelmc.ro + year_from: '2010' + year_to: '2020' diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a9c7977 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# see https://editorconfig.org/ +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 +charset = utf-8 + +[*.{bat,cmd,ps1}] +end_of_line = crlf + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8cdb373 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: 🐞 Bug report +about: There a problem with how pytest-cov or coverage works +--- + +# Summary + +## Expected vs actual result + +# Reproducer + +## Versions + +Output of relevant packages `pip list`, `python --version`, `pytest --version` etc. + +Make sure you include complete output of `tox` if you use it (it will show versions of various things). + +## Config + +Include your `tox.ini`, `pytest.ini`, `.coveragerc`, `setup.cfg` or any relevant configuration. + +## Code + +Link to your repository, gist, pastebin or just paste raw code that illustrates the issue. + +If you paste raw code make sure you quote it, eg: + +```python +def foobar(): + pass +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..5e7d56e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: ✈ Feature request +about: Proposal for a new feature in pytest-cov +--- + +Before proposing please consider: + +* the maintenance cost of the feature +* implementing it externally (like a shell/python script, + pytest plugin or something else) + +# Summary + +These questions should be answered: + +* why is the feature needed? +* what problem does it solve? +* how it is better compared to past solutions to the problem? diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 0000000..7ea731e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,40 @@ +--- +name: 🤔 Support request +about: Request help with setting up pytest-cov in your project +--- + +Please go over all the sections and search +https://pytest-cov.readthedocs.io/en/latest/ or +https://coverage.readthedocs.io/en/latest/ +before opening the issue. + +# Summary + +## Expected vs actual result + +# Reproducer + +## Versions + +Output of relevant packages `pip list`, `python --version`, `pytest --version` etc. + +Make sure you include complete output of `tox` if you use it (it will show versions of various things). + +## Config + +Include your `tox.ini`, `pytest.ini`, `.coveragerc`, `setup.cfg` or any relevant configuration. + +## Code + +Link to your repository, gist, pastebin or just paste raw code that illustrates the issue. + +If you paste raw code make sure you quote it, eg: + +```python +def foobar(): + pass +``` + +# What has been tried to solve the problem + +You should outline the things you tried to solve the problem but didn't work. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f994232 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +# To install the git pre-commit hook run: +# pre-commit install +# To update the pre-commit hooks run: +# pre-commit install-hooks +exclude: '^(src/.*\.pth|\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: master + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements + - repo: https://github.com/timothycrosley/isort + rev: master + hooks: + - id: isort + - repo: https://gitlab.com/pycqa/flake8 + rev: master + hooks: + - id: flake8 diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..ac76971 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,11 @@ +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 +sphinx: + configuration: docs/conf.py +formats: all +python: + version: 3 + install: + - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3c96aad --- /dev/null +++ b/.travis.yml @@ -0,0 +1,115 @@ +# NOTE: this file is auto-generated via ci/bootstrap.py (ci/templates/.travis.yml). +dist: xenial +language: python +cache: false +env: + global: + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all +stages: + - lint + - examples + - tests +jobs: + fast_finish: true + allow_failures: + - python: '3.8' + include: + - stage: lint + env: TOXENV=check + - env: TOXENV=docs + + - stage: tests + env: TOXENV=py27-pytest46-xdist27-coverage45 + python: '2.7' + - env: TOXENV=py27-pytest46-xdist27-coverage52 + python: '2.7' + - env: TOXENV=py35-pytest46-xdist27-coverage45 + python: '3.5' + - env: TOXENV=py35-pytest46-xdist27-coverage52 + python: '3.5' + - env: TOXENV=py36-pytest46-xdist27-coverage45 + python: '3.6' + - env: TOXENV=py36-pytest46-xdist27-coverage52 + python: '3.6' + - env: TOXENV=py37-pytest46-xdist27-coverage45 + python: '3.7' + - env: TOXENV=py37-pytest46-xdist27-coverage52 + python: '3.7' + - env: TOXENV=pypy-pytest46-xdist27-coverage45 + python: 'pypy' + - env: TOXENV=pypy-pytest46-xdist27-coverage52 + python: 'pypy' + - env: TOXENV=pypy3-pytest46-xdist27-coverage45 + python: 'pypy3' + - env: TOXENV=pypy3-pytest46-xdist27-coverage52 + python: 'pypy3' + - env: TOXENV=py36-pytest46-xdist33-coverage45 + python: '3.6' + - env: TOXENV=py36-pytest46-xdist33-coverage52 + python: '3.6' + - env: TOXENV=py36-pytest54-xdist33-coverage45 + python: '3.6' + - env: TOXENV=py36-pytest54-xdist33-coverage52 + python: '3.6' + - env: TOXENV=py37-pytest46-xdist33-coverage45 + python: '3.7' + - env: TOXENV=py37-pytest46-xdist33-coverage52 + python: '3.7' + - env: TOXENV=py37-pytest54-xdist33-coverage45 + python: '3.7' + - env: TOXENV=py37-pytest54-xdist33-coverage52 + python: '3.7' + - env: TOXENV=py38-pytest46-xdist33-coverage45 + python: '3.8' + - env: TOXENV=py38-pytest46-xdist33-coverage52 + python: '3.8' + - env: TOXENV=py38-pytest54-xdist33-coverage45 + python: '3.8' + - env: TOXENV=py38-pytest54-xdist33-coverage52 + python: '3.8' + - env: TOXENV=pypy3-pytest46-xdist33-coverage45 + python: 'pypy3' + - env: TOXENV=pypy3-pytest46-xdist33-coverage52 + python: 'pypy3' + - env: TOXENV=pypy3-pytest54-xdist33-coverage45 + python: 'pypy3' + - env: TOXENV=pypy3-pytest54-xdist33-coverage52 + python: 'pypy3' + - env: TOXENV=py36-pytest60-xdist200-coverage52 + python: '3.6' + - env: TOXENV=py37-pytest60-xdist200-coverage52 + python: '3.7' + - env: TOXENV=py38-pytest60-xdist200-coverage52 + python: '3.8' + - env: TOXENV=pypy3-pytest60-xdist200-coverage52 + python: 'pypy3' + + - stage: examples + python: '3.8' + script: cd $TARGET; tox -v + env: + - TARGET=examples/src-layout + - python: '3.8' + script: cd $TARGET; tox -v + env: + - TARGET=examples/adhoc-layout +before_install: + - python --version + - uname -a + - lsb_release -a +install: + - python -mpip install --progress-bar=off tox -rci/requirements.txt + - virtualenv --version + - easy_install --version + - pip --version + - tox --version +script: + - tox -v +after_failure: + - more .tox/log/* | cat + - more .tox/*/log/* | cat +notifications: + email: + on_success: never + on_failure: always diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..a768868 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,41 @@ +Authors +======= + +* 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 +* Thomas Kluyver - https://github.com/takluyver +* Guillaume Ayoub - http://www.yabz.fr +* Federico Ceratto - http://firelet.net +* Josh Kalderimis - http://blog.cookiestack.com +* Ionel Cristian Mărieș - https://blog.ionelmc.ro +* Christian Ledermann - https://github.com/cleder +* Alec Nikolas Reiter - https://github.com/justanr +* Patrick Lannigan - https://github.com/plannigan +* David Szotten - https://github.com/davidszotten +* Michael Elovskikh - https://github.com/wronglink +* Saurabh Kumar - https://github.com/theskumar +* Michael Elovskikh - https://github.com/wronglink +* Daniel Hahler - https://daniel.hahler.de +* Florian Bruhin - http://www.the-compiler.org +* Zoltan Kozma - https://github.com/kozmaz87 +* Francis Niu - https://flniu.github.io +* Jannis Leidel - https://github.com/jezdez +* Ryan Hiebert - http://ryanhiebert.com/ +* Terence Honles - https://github.com/terencehonles +* Jeremy Bowman - https://github.com/jmbowman +* Samuel Giffard - https://github.com/Mulugruntz +* Семён Марьясин - https://github.com/MarSoft +* Alexander Shadchin - https://github.com/shadchin +* Thomas Grainger - https://graingert.co.uk +* Juanjo Bazán - https://github.com/xuanxu +* Andrew Murray - https://github.com/radarhere +* Ned Batchelder - https://nedbatchelder.com/ +* Albert Tugushev - https://github.com/atugushev +* Martín Gaitán - https://github.com/mgaitan +* Hugo van Kemenade - https://github.com/hugovk +* Michael Manganiello - https://github.com/adamantike +* Anders Hovmöller - https://github.com/boxed +* Zac Hatfield-Dodds - https://zhd.dev diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..3366645 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,247 @@ +Changelog +========= + +2.10.1 (2020-08-14) +------------------- + +* Support for ``pytest-xdist`` 2.0, which breaks compatibility with ``pytest-xdist`` before 1.22.3 (from 2017). + Contributed by Zac Hatfield-Dodds in `#412 `_. +* Fixed the ``LocalPath has no attribute startswith`` failure that occurred when using the ``pytester`` plugin + in inline mode. + +2.10.0 (2020-06-12) +------------------- + +* Improved the ``--no-cov`` warning. Now it's only shown if ``--no-cov`` is present before ``--cov``. +* Removed legacy pytest support. Changed ``setup.py`` so that ``pytest>=4.6`` is required. + +2.9.0 (2020-05-22) +------------------ + +* Fixed ``RemovedInPytest4Warning`` when using Pytest 3.10. + Contributed by Michael Manganiello in `#354 `_. +* Made pytest startup faster when plugin not active by lazy-importing. + Contributed by Anders Hovmöller in `#339 `_. +* Various CI improvements. + Contributed by Daniel Hahler in `#363 `_ and + `#364 `_. +* Various Python support updates (drop EOL 3.4, test against 3.8 final). + Contributed by Hugo van Kemenade in + `#336 `_ and + `#367 `_. +* Changed ``--cov-append`` to always enable ``data_suffix`` (a coverage setting). + Contributed by Harm Geerts in + `#387 `_. +* Changed ``--cov-append`` to handle loading previous data better + (fixes various path aliasing issues). +* Various other testing improvements, github issue templates, example updates. +* Fixed internal failures that are caused by tests that change the current working directory by + ensuring a consistent working directory when coverage is called. + See `#306 `_ and + `coveragepy#881 `_ + +2.8.1 (2019-10-05) +------------------ + +* Fixed `#348 `_ - + regression when only certain reports (html or xml) are used then ``--cov-fail-under`` always fails. + +2.8.0 (2019-10-04) +------------------ + +* Fixed ``RecursionError`` that can occur when using + `cleanup_on_signal `__ or + `cleanup_on_sigterm `__. + See: `#294 `_. + The 2.7.x releases of pytest-cov should be considered broken regarding aforementioned cleanup API. +* Added compatibility with future xdist release that deprecates some internals + (match pytest-xdist master/worker terminology). + Contributed by Thomas Grainger in `#321 `_ +* Fixed breakage that occurs when multiple reporting options are used. + Contributed by Thomas Grainger in `#338 `_. +* Changed internals to use a stub instead of ``os.devnull``. + Contributed by Thomas Grainger in `#332 `_. +* Added support for Coverage 5.0. + Contributed by Ned Batchelder in `#319 `_. +* Added support for float values in ``--cov-fail-under``. + Contributed by Martín Gaitán in `#311 `_. +* Various documentation fixes. Contributed by + Juanjo Bazán, + Andrew Murray and + Albert Tugushev in + `#298 `_, + `#299 `_ and + `#307 `_. +* Various testing improvements. Contributed by + Ned Batchelder, + Daniel Hahler, + Ionel Cristian Mărieș and + Hugo van Kemenade in + `#313 `_, + `#314 `_, + `#315 `_, + `#316 `_, + `#325 `_, + `#326 `_, + `#334 `_ and + `#335 `_. +* Added the ``--cov-context`` CLI options that enables coverage contexts. Only works with coverage 5.0+. + Contributed by Ned Batchelder in `#345 `_. + +2.7.1 (2019-05-03) +------------------ + +* Fixed source distribution manifest so that garbage ain't included in the tarball. + +2.7.0 (2019-05-03) +------------------ + +* Fixed ``AttributeError: 'NoneType' object has no attribute 'configure_node'`` error when ``--no-cov`` is used. + Contributed by Alexander Shadchin in `#263 `_. +* Various testing and CI improvements. Contributed by Daniel Hahler in + `#255 `_, + `#266 `_, + `#272 `_, + `#271 `_ and + `#269 `_. +* Improved documentation regarding subprocess and multiprocessing. + Contributed in `#265 `_. +* Improved ``pytest_cov.embed.cleanup_on_sigterm`` to be reentrant (signal deliveries while signal handling is + running won't break stuff). +* Added ``pytest_cov.embed.cleanup_on_signal`` for customized cleanup. +* Improved cleanup code and fixed various issues with leftover data files. All contributed in + `#265 `_ or + `#262 `_. +* Improved examples. Now there are two examples for the common project layouts, complete with working coverage + configuration. The examples have CI testing. Contributed in + `#267 `_. +* Improved help text for CLI options. + +2.6.1 (2019-01-07) +------------------ + +* Added support for Pytest 4.1. Contributed by Daniel Hahler and Семён Марьясин in + `#253 `_ and + `#230 `_. +* Various test and docs fixes. Contributed by Daniel Hahler in + `#224 `_ and + `#223 `_. +* Fixed the "Module already imported" issue (`#211 `_). + Contributed by Daniel Hahler in `#228 `_. + +2.6.0 (2018-09-03) +------------------ + +* Dropped support for Python 3 < 3.4, Pytest < 3.5 and Coverage < 4.4. +* Fixed some documentation formatting. Contributed by Jean Jordaan and Julian. +* Added an example with ``addopts`` in documentation. Contributed by Samuel Giffard in + `#195 `_. +* Fixed ``TypeError: 'NoneType' object is not iterable`` in certain xdist configurations. Contributed by Jeremy Bowman in + `#213 `_. +* Added a ``no_cover`` marker and fixture. Fixes + `#78 `_. +* Fixed broken ``no_cover`` check when running doctests. Contributed by Terence Honles in + `#200 `_. +* Fixed various issues with path normalization in reports (when combining coverage data from parallel mode). Fixes + `#130 `_. + Contributed by Ryan Hiebert & Ionel Cristian Mărieș in + `#178 `_. +* Report generation failures don't raise exceptions anymore. A warning will be logged instead. Fixes + `#161 `_. +* Fixed multiprocessing issue on Windows (empty env vars are not passed). Fixes + `#165 `_. + +2.5.1 (2017-05-11) +------------------ + +* Fixed xdist breakage (regression in ``2.5.0``). + Fixes `#157 `_. +* Allow setting custom ``data_file`` name in ``.coveragerc``. + Fixes `#145 `_. + Contributed by Jannis Leidel & Ionel Cristian Mărieș in + `#156 `_. + +2.5.0 (2017-05-09) +------------------ + +* Always show a summary when ``--cov-fail-under`` is used. Contributed by Francis Niu in `PR#141 + `_. +* Added ``--cov-branch`` option. Fixes `#85 `_. +* Improve exception handling in subprocess setup. Fixes `#144 `_. +* Fixed handling when ``--cov`` is used multiple times. Fixes `#151 `_. + +2.4.0 (2016-10-10) +------------------ + +* Added a "disarm" option: ``--no-cov``. It will disable coverage measurements. Contributed by Zoltan Kozma in + `PR#135 `_. + + **WARNING: Do not put this in your configuration files, it's meant to be an one-off for situations where you want to + disable coverage from command line.** +* Fixed broken exception handling on ``.pth`` file. See `#136 `_. + +2.3.1 (2016-08-07) +------------------ + +* Fixed regression causing spurious errors when xdist was used. See `#124 + `_. +* Fixed DeprecationWarning about incorrect `addoption` use. Contributed by Florian Bruhin in `PR#127 + `_. +* Fixed deprecated use of funcarg fixture API. Contributed by Daniel Hahler in `PR#125 + `_. + +2.3.0 (2016-07-05) +------------------ + +* Add support for specifying output location for html, xml, and annotate report. + Contributed by Patrick Lannigan in `PR#113 `_. +* Fix bug hiding test failure when cov-fail-under failed. +* For coverage >= 4.0, match the default behaviour of `coverage report` and + error if coverage fails to find the source instead of just printing a warning. + Contributed by David Szotten in `PR#116 `_. +* Fixed bug occurred when bare ``--cov`` parameter was used with xdist. + Contributed by Michael Elovskikh in `PR#120 `_. +* Add support for ``skip_covered`` and added ``--cov-report=term-skip-covered`` command + line options. Contributed by Saurabh Kumar in `PR#115 `_. + +2.2.1 (2016-01-30) +------------------ + +* Fixed incorrect merging of coverage data when xdist was used and coverage was ``>= 4.0``. + +2.2.0 (2015-10-04) +------------------ + +* Added support for changing working directory in tests. Previously changing working + directory would disable coverage measurements in suprocesses. +* Fixed broken handling for ``--cov-report=annotate``. + +2.1.0 (2015-08-23) +------------------ + +* Added support for `coverage 4.0b2`. +* Added the ``--cov-append`` command line options. Contributed by Christian Ledermann + in `PR#80 `_. + +2.0.0 (2015-07-28) +------------------ + +* Added ``--cov-fail-under``, akin to the new ``fail_under`` option in `coverage-4.0` + (automatically activated if there's a ``[report] fail_under = ...`` in ``.coveragerc``). +* Changed ``--cov-report=term`` to automatically upgrade to ``--cov-report=term-missing`` + if there's ``[run] show_missing = True`` in ``.coveragerc``. +* Changed ``--cov`` so it can be used with no path argument (in which case the source + settings from ``.coveragerc`` will be used instead). +* Fixed `.pth` installation to work in all cases (install, easy_install, wheels, develop etc). +* Fixed `.pth` uninstallation to work for wheel installs. +* Support for coverage 4.0. +* Data file suffixing changed to use coverage's ``data_suffix=True`` option (instead of the + custom suffixing). +* Avoid warning about missing coverage data (just like ``coverage.control.process_startup``). +* Fixed a race condition when running with xdist (all the workers tried to combine the files). + It's possible that this issue is not present in `pytest-cov 1.8.X`. + +1.8.2 (2014-11-06) +------------------ + +* N/A diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..53f5597 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,90 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +Bug reports +=========== + +When `reporting a bug `_ please include: + + * Your operating system name and version. + * Any details about your local setup that might be helpful in troubleshooting. + * Detailed steps to reproduce the bug. + +Documentation improvements +========================== + +pytest-cov could always use more documentation, whether as part of the +official pytest-cov docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Feature requests and feedback +============================= + +The best way to send feedback is to file an issue at https://github.com/pytest-dev/pytest-cov/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that code contributions are welcome :) + +Development +=========== + +To set up `pytest-cov` for local development: + +1. Fork `pytest-cov `_ + (look for the "Fork" button). +2. Clone your fork locally:: + + git clone git@github.com:YOURGITHUBNAME/pytest-cov.git + +3. Create a branch for local development:: + + git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +4. When you're done making changes run all the checks and docs builder with `tox `_ one command:: + + tox + +5. Commit your changes and push your branch to GitHub:: + + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + +6. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +If you need some code review or feedback while you're developing the code just make the pull request. + +For merging, you should: + +1. Include passing tests (run ``tox``) [1]_. +2. Update documentation when there's new API, functionality etc. +3. Add a note to ``CHANGELOG.rst`` about the changes. +4. Add yourself to ``AUTHORS.rst``. + +.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will + `run the tests `_ for each change you add in the pull request. + + It will be slower though ... + +Tips +---- + +To run a subset of tests:: + + tox -e envname -- pytest -k test_myfeature + +To run the test environments in *parallel*:: + + tox -p auto diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5b3634b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2010 Meme Dough + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..6db4611 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,26 @@ +graft docs +graft examples +prune examples/*/.tox +prune examples/*/htmlcov +prune examples/*/*/htmlcov +prune examples/adhoc-layout/*.egg-info +prune examples/src-layout/src/*.egg-info + +graft src +graft ci +graft tests + +include .bumpversion.cfg +include .coveragerc +include .cookiecutterrc +include .editorconfig + +include AUTHORS.rst +include CHANGELOG.rst +include CONTRIBUTING.rst +include LICENSE +include README.rst + +include tox.ini .travis.yml .appveyor.yml .readthedocs.yml .pre-commit-config.yaml + +global-exclude *.py[cod] __pycache__/* *.so *.dylib .coverage .coverage.* diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..c036dc8 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,447 @@ +Metadata-Version: 2.1 +Name: pytest-cov +Version: 2.10.1 +Summary: Pytest plugin for measuring coverage. +Home-page: https://github.com/pytest-dev/pytest-cov +Author: Marc Schlaich +Author-email: marc.schlaich@gmail.com +License: MIT +Description: ======== + Overview + ======== + + .. start-badges + + .. list-table:: + :stub-columns: 1 + + * - docs + - |docs| + * - tests + - | |travis| |appveyor| |requires| + * - package + - | |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| + | |commits-since| + + .. |docs| image:: https://readthedocs.org/projects/pytest-cov/badge/?style=flat + :target: https://readthedocs.org/projects/pytest-cov + :alt: Documentation Status + + .. |travis| image:: https://api.travis-ci.org/pytest-dev/pytest-cov.svg?branch=master + :alt: Travis-CI Build Status + :target: https://travis-ci.org/pytest-dev/pytest-cov + + .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/pytest-dev/pytest-cov?branch=master&svg=true + :alt: AppVeyor Build Status + :target: https://ci.appveyor.com/project/pytestbot/pytest-cov + + .. |requires| image:: https://requires.io/github/pytest-dev/pytest-cov/requirements.svg?branch=master + :alt: Requirements Status + :target: https://requires.io/github/pytest-dev/pytest-cov/requirements/?branch=master + + .. |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/v2.10.1.svg + :alt: Commits since latest release + :target: https://github.com/pytest-dev/pytest-cov/compare/v2.10.1...master + + .. |wheel| image:: https://img.shields.io/pypi/wheel/pytest-cov.svg + :alt: PyPI Wheel + :target: https://pypi.org/project/pytest-cov + + .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/pytest-cov.svg + :alt: Supported versions + :target: https://pypi.org/project/pytest-cov + + .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/pytest-cov.svg + :alt: Supported implementations + :target: https://pypi.org/project/pytest-cov + + .. end-badges + + This plugin produces coverage reports. Compared to just using ``coverage run`` this plugin does some extras: + + * Subprocess support: you can fork or run stuff in a subprocess and will get covered without any fuss. + * Xdist support: you can use all of pytest-xdist's features and still get coverage. + * Consistent pytest behavior. If you run ``coverage run -m pytest`` you will have slightly different ``sys.path`` (CWD will be + in it, unlike when running ``pytest``). + + All features offered by the coverage package should work, either through pytest-cov's command line options or + through coverage's config file. + + * Free software: MIT license + + Installation + ============ + + Install with pip:: + + pip install pytest-cov + + For distributed testing support install pytest-xdist:: + + pip install pytest-xdist + + Upgrading from ancient pytest-cov + --------------------------------- + + `pytest-cov 2.0` is using a new ``.pth`` file (``pytest-cov.pth``). You may want to manually remove the older + ``init_cov_core.pth`` from site-packages as it's not automatically removed. + + Uninstalling + ------------ + + Uninstall with pip:: + + pip uninstall pytest-cov + + Under certain scenarios a stray ``.pth`` file may be left around in site-packages. + + * `pytest-cov 2.0` may leave a ``pytest-cov.pth`` if you installed without wheels + (``easy_install``, ``setup.py install`` etc). + * `pytest-cov 1.8 or older` will leave a ``init_cov_core.pth``. + + Usage + ===== + + :: + + pytest --cov=myproj tests/ + + Would produce a report like:: + + -------------------- coverage: ... --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% + + Documentation + ============= + + http://pytest-cov.rtfd.org/ + + + + + + + Coverage Data File + ================== + + The data file is erased at the beginning of testing to ensure clean data for each test run. If you + need to combine the coverage of several test runs you can use the ``--cov-append`` option to append + this coverage data to coverage data from previous test runs. + + The data file is left at the end of testing so that it is possible to use normal coverage tools to + examine it. + + Limitations + =========== + + For distributed testing the workers must have the pytest-cov package installed. This is needed since + the plugin must be registered through setuptools for pytest to start the plugin on the + worker. + + For subprocess measurement environment variables must make it from the main process to the + subprocess. The python used by the subprocess must have pytest-cov installed. The subprocess must + do normal site initialisation so that the environment variables can be detected and coverage + started. + + + Acknowledgements + ================ + + Whilst this plugin has been built fresh from the ground up it has been influenced by the work done + on pytest-coverage (Ross Lawley, James Mills, Holger Krekel) and nose-cover (Jason Pellerin) which are + other coverage plugins. + + Ned Batchelder for coverage and its ability to combine the coverage results of parallel runs. + + Holger Krekel for pytest with its distributed testing support. + + Jason Pellerin for nose. + + Michael Foord for unittest2. + + No doubt others have contributed to these tools as well. + + Changelog + ========= + + 2.10.1 (2020-08-14) + ------------------- + + * Support for ``pytest-xdist`` 2.0, which breaks compatibility with ``pytest-xdist`` before 1.22.3 (from 2017). + Contributed by Zac Hatfield-Dodds in `#412 `_. + * Fixed the ``LocalPath has no attribute startswith`` failure that occurred when using the ``pytester`` plugin + in inline mode. + + 2.10.0 (2020-06-12) + ------------------- + + * Improved the ``--no-cov`` warning. Now it's only shown if ``--no-cov`` is present before ``--cov``. + * Removed legacy pytest support. Changed ``setup.py`` so that ``pytest>=4.6`` is required. + + 2.9.0 (2020-05-22) + ------------------ + + * Fixed ``RemovedInPytest4Warning`` when using Pytest 3.10. + Contributed by Michael Manganiello in `#354 `_. + * Made pytest startup faster when plugin not active by lazy-importing. + Contributed by Anders Hovmöller in `#339 `_. + * Various CI improvements. + Contributed by Daniel Hahler in `#363 `_ and + `#364 `_. + * Various Python support updates (drop EOL 3.4, test against 3.8 final). + Contributed by Hugo van Kemenade in + `#336 `_ and + `#367 `_. + * Changed ``--cov-append`` to always enable ``data_suffix`` (a coverage setting). + Contributed by Harm Geerts in + `#387 `_. + * Changed ``--cov-append`` to handle loading previous data better + (fixes various path aliasing issues). + * Various other testing improvements, github issue templates, example updates. + * Fixed internal failures that are caused by tests that change the current working directory by + ensuring a consistent working directory when coverage is called. + See `#306 `_ and + `coveragepy#881 `_ + + 2.8.1 (2019-10-05) + ------------------ + + * Fixed `#348 `_ - + regression when only certain reports (html or xml) are used then ``--cov-fail-under`` always fails. + + 2.8.0 (2019-10-04) + ------------------ + + * Fixed ``RecursionError`` that can occur when using + `cleanup_on_signal `__ or + `cleanup_on_sigterm `__. + See: `#294 `_. + The 2.7.x releases of pytest-cov should be considered broken regarding aforementioned cleanup API. + * Added compatibility with future xdist release that deprecates some internals + (match pytest-xdist master/worker terminology). + Contributed by Thomas Grainger in `#321 `_ + * Fixed breakage that occurs when multiple reporting options are used. + Contributed by Thomas Grainger in `#338 `_. + * Changed internals to use a stub instead of ``os.devnull``. + Contributed by Thomas Grainger in `#332 `_. + * Added support for Coverage 5.0. + Contributed by Ned Batchelder in `#319 `_. + * Added support for float values in ``--cov-fail-under``. + Contributed by Martín Gaitán in `#311 `_. + * Various documentation fixes. Contributed by + Juanjo Bazán, + Andrew Murray and + Albert Tugushev in + `#298 `_, + `#299 `_ and + `#307 `_. + * Various testing improvements. Contributed by + Ned Batchelder, + Daniel Hahler, + Ionel Cristian Mărieș and + Hugo van Kemenade in + `#313 `_, + `#314 `_, + `#315 `_, + `#316 `_, + `#325 `_, + `#326 `_, + `#334 `_ and + `#335 `_. + * Added the ``--cov-context`` CLI options that enables coverage contexts. Only works with coverage 5.0+. + Contributed by Ned Batchelder in `#345 `_. + + 2.7.1 (2019-05-03) + ------------------ + + * Fixed source distribution manifest so that garbage ain't included in the tarball. + + 2.7.0 (2019-05-03) + ------------------ + + * Fixed ``AttributeError: 'NoneType' object has no attribute 'configure_node'`` error when ``--no-cov`` is used. + Contributed by Alexander Shadchin in `#263 `_. + * Various testing and CI improvements. Contributed by Daniel Hahler in + `#255 `_, + `#266 `_, + `#272 `_, + `#271 `_ and + `#269 `_. + * Improved documentation regarding subprocess and multiprocessing. + Contributed in `#265 `_. + * Improved ``pytest_cov.embed.cleanup_on_sigterm`` to be reentrant (signal deliveries while signal handling is + running won't break stuff). + * Added ``pytest_cov.embed.cleanup_on_signal`` for customized cleanup. + * Improved cleanup code and fixed various issues with leftover data files. All contributed in + `#265 `_ or + `#262 `_. + * Improved examples. Now there are two examples for the common project layouts, complete with working coverage + configuration. The examples have CI testing. Contributed in + `#267 `_. + * Improved help text for CLI options. + + 2.6.1 (2019-01-07) + ------------------ + + * Added support for Pytest 4.1. Contributed by Daniel Hahler and Семён Марьясин in + `#253 `_ and + `#230 `_. + * Various test and docs fixes. Contributed by Daniel Hahler in + `#224 `_ and + `#223 `_. + * Fixed the "Module already imported" issue (`#211 `_). + Contributed by Daniel Hahler in `#228 `_. + + 2.6.0 (2018-09-03) + ------------------ + + * Dropped support for Python 3 < 3.4, Pytest < 3.5 and Coverage < 4.4. + * Fixed some documentation formatting. Contributed by Jean Jordaan and Julian. + * Added an example with ``addopts`` in documentation. Contributed by Samuel Giffard in + `#195 `_. + * Fixed ``TypeError: 'NoneType' object is not iterable`` in certain xdist configurations. Contributed by Jeremy Bowman in + `#213 `_. + * Added a ``no_cover`` marker and fixture. Fixes + `#78 `_. + * Fixed broken ``no_cover`` check when running doctests. Contributed by Terence Honles in + `#200 `_. + * Fixed various issues with path normalization in reports (when combining coverage data from parallel mode). Fixes + `#130 `_. + Contributed by Ryan Hiebert & Ionel Cristian Mărieș in + `#178 `_. + * Report generation failures don't raise exceptions anymore. A warning will be logged instead. Fixes + `#161 `_. + * Fixed multiprocessing issue on Windows (empty env vars are not passed). Fixes + `#165 `_. + + 2.5.1 (2017-05-11) + ------------------ + + * Fixed xdist breakage (regression in ``2.5.0``). + Fixes `#157 `_. + * Allow setting custom ``data_file`` name in ``.coveragerc``. + Fixes `#145 `_. + Contributed by Jannis Leidel & Ionel Cristian Mărieș in + `#156 `_. + + 2.5.0 (2017-05-09) + ------------------ + + * Always show a summary when ``--cov-fail-under`` is used. Contributed by Francis Niu in `PR#141 + `_. + * Added ``--cov-branch`` option. Fixes `#85 `_. + * Improve exception handling in subprocess setup. Fixes `#144 `_. + * Fixed handling when ``--cov`` is used multiple times. Fixes `#151 `_. + + 2.4.0 (2016-10-10) + ------------------ + + * Added a "disarm" option: ``--no-cov``. It will disable coverage measurements. Contributed by Zoltan Kozma in + `PR#135 `_. + + **WARNING: Do not put this in your configuration files, it's meant to be an one-off for situations where you want to + disable coverage from command line.** + * Fixed broken exception handling on ``.pth`` file. See `#136 `_. + + 2.3.1 (2016-08-07) + ------------------ + + * Fixed regression causing spurious errors when xdist was used. See `#124 + `_. + * Fixed DeprecationWarning about incorrect `addoption` use. Contributed by Florian Bruhin in `PR#127 + `_. + * Fixed deprecated use of funcarg fixture API. Contributed by Daniel Hahler in `PR#125 + `_. + + 2.3.0 (2016-07-05) + ------------------ + + * Add support for specifying output location for html, xml, and annotate report. + Contributed by Patrick Lannigan in `PR#113 `_. + * Fix bug hiding test failure when cov-fail-under failed. + * For coverage >= 4.0, match the default behaviour of `coverage report` and + error if coverage fails to find the source instead of just printing a warning. + Contributed by David Szotten in `PR#116 `_. + * Fixed bug occurred when bare ``--cov`` parameter was used with xdist. + Contributed by Michael Elovskikh in `PR#120 `_. + * Add support for ``skip_covered`` and added ``--cov-report=term-skip-covered`` command + line options. Contributed by Saurabh Kumar in `PR#115 `_. + + 2.2.1 (2016-01-30) + ------------------ + + * Fixed incorrect merging of coverage data when xdist was used and coverage was ``>= 4.0``. + + 2.2.0 (2015-10-04) + ------------------ + + * Added support for changing working directory in tests. Previously changing working + directory would disable coverage measurements in suprocesses. + * Fixed broken handling for ``--cov-report=annotate``. + + 2.1.0 (2015-08-23) + ------------------ + + * Added support for `coverage 4.0b2`. + * Added the ``--cov-append`` command line options. Contributed by Christian Ledermann + in `PR#80 `_. + + 2.0.0 (2015-07-28) + ------------------ + + * Added ``--cov-fail-under``, akin to the new ``fail_under`` option in `coverage-4.0` + (automatically activated if there's a ``[report] fail_under = ...`` in ``.coveragerc``). + * Changed ``--cov-report=term`` to automatically upgrade to ``--cov-report=term-missing`` + if there's ``[run] show_missing = True`` in ``.coveragerc``. + * Changed ``--cov`` so it can be used with no path argument (in which case the source + settings from ``.coveragerc`` will be used instead). + * Fixed `.pth` installation to work in all cases (install, easy_install, wheels, develop etc). + * Fixed `.pth` uninstallation to work for wheel installs. + * Support for coverage 4.0. + * Data file suffixing changed to use coverage's ``data_suffix=True`` option (instead of the + custom suffixing). + * Avoid warning about missing coverage data (just like ``coverage.control.process_startup``). + * Fixed a race condition when running with xdist (all the workers tried to combine the files). + It's possible that this issue is not present in `pytest-cov 1.8.X`. + + 1.8.2 (2014-11-06) + ------------------ + + * N/A + +Keywords: cover,coverage,pytest,py.test,distributed,parallel +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: Pytest +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Operating System :: Unix +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Testing +Classifier: Topic :: Utilities +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +Provides-Extra: testing diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..2b1284d --- /dev/null +++ b/README.rst @@ -0,0 +1,167 @@ +======== +Overview +======== + +.. start-badges + +.. list-table:: + :stub-columns: 1 + + * - docs + - |docs| + * - tests + - | |travis| |appveyor| |requires| + * - package + - | |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| + | |commits-since| + +.. |docs| image:: https://readthedocs.org/projects/pytest-cov/badge/?style=flat + :target: https://readthedocs.org/projects/pytest-cov + :alt: Documentation Status + +.. |travis| image:: https://api.travis-ci.org/pytest-dev/pytest-cov.svg?branch=master + :alt: Travis-CI Build Status + :target: https://travis-ci.org/pytest-dev/pytest-cov + +.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/pytest-dev/pytest-cov?branch=master&svg=true + :alt: AppVeyor Build Status + :target: https://ci.appveyor.com/project/pytestbot/pytest-cov + +.. |requires| image:: https://requires.io/github/pytest-dev/pytest-cov/requirements.svg?branch=master + :alt: Requirements Status + :target: https://requires.io/github/pytest-dev/pytest-cov/requirements/?branch=master + +.. |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/v2.10.1.svg + :alt: Commits since latest release + :target: https://github.com/pytest-dev/pytest-cov/compare/v2.10.1...master + +.. |wheel| image:: https://img.shields.io/pypi/wheel/pytest-cov.svg + :alt: PyPI Wheel + :target: https://pypi.org/project/pytest-cov + +.. |supported-versions| image:: https://img.shields.io/pypi/pyversions/pytest-cov.svg + :alt: Supported versions + :target: https://pypi.org/project/pytest-cov + +.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/pytest-cov.svg + :alt: Supported implementations + :target: https://pypi.org/project/pytest-cov + +.. end-badges + +This plugin produces coverage reports. Compared to just using ``coverage run`` this plugin does some extras: + +* Subprocess support: you can fork or run stuff in a subprocess and will get covered without any fuss. +* Xdist support: you can use all of pytest-xdist's features and still get coverage. +* Consistent pytest behavior. If you run ``coverage run -m pytest`` you will have slightly different ``sys.path`` (CWD will be + in it, unlike when running ``pytest``). + +All features offered by the coverage package should work, either through pytest-cov's command line options or +through coverage's config file. + +* Free software: MIT license + +Installation +============ + +Install with pip:: + + pip install pytest-cov + +For distributed testing support install pytest-xdist:: + + pip install pytest-xdist + +Upgrading from ancient pytest-cov +--------------------------------- + +`pytest-cov 2.0` is using a new ``.pth`` file (``pytest-cov.pth``). You may want to manually remove the older +``init_cov_core.pth`` from site-packages as it's not automatically removed. + +Uninstalling +------------ + +Uninstall with pip:: + + pip uninstall pytest-cov + +Under certain scenarios a stray ``.pth`` file may be left around in site-packages. + +* `pytest-cov 2.0` may leave a ``pytest-cov.pth`` if you installed without wheels + (``easy_install``, ``setup.py install`` etc). +* `pytest-cov 1.8 or older` will leave a ``init_cov_core.pth``. + +Usage +===== + +:: + + pytest --cov=myproj tests/ + +Would produce a report like:: + + -------------------- coverage: ... --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% + +Documentation +============= + + http://pytest-cov.rtfd.org/ + + + + + + +Coverage Data File +================== + +The data file is erased at the beginning of testing to ensure clean data for each test run. If you +need to combine the coverage of several test runs you can use the ``--cov-append`` option to append +this coverage data to coverage data from previous test runs. + +The data file is left at the end of testing so that it is possible to use normal coverage tools to +examine it. + +Limitations +=========== + +For distributed testing the workers must have the pytest-cov package installed. This is needed since +the plugin must be registered through setuptools for pytest to start the plugin on the +worker. + +For subprocess measurement environment variables must make it from the main process to the +subprocess. The python used by the subprocess must have pytest-cov installed. The subprocess must +do normal site initialisation so that the environment variables can be detected and coverage +started. + + +Acknowledgements +================ + +Whilst this plugin has been built fresh from the ground up it has been influenced by the work done +on pytest-coverage (Ross Lawley, James Mills, Holger Krekel) and nose-cover (Jason Pellerin) which are +other coverage plugins. + +Ned Batchelder for coverage and its ability to combine the coverage results of parallel runs. + +Holger Krekel for pytest with its distributed testing support. + +Jason Pellerin for nose. + +Michael Foord for unittest2. + +No doubt others have contributed to these tools as well. diff --git a/ci/appveyor-with-compiler.cmd b/ci/appveyor-with-compiler.cmd new file mode 100644 index 0000000..289585f --- /dev/null +++ b/ci/appveyor-with-compiler.cmd @@ -0,0 +1,23 @@ +:: Very simple setup: +:: - if WINDOWS_SDK_VERSION is set then activate the SDK. +:: - disable the WDK if it's around. + +SET COMMAND_TO_RUN=%* +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows +SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" +ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% + +IF EXIST %WIN_WDK% ( + REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ + REN %WIN_WDK% 0wdf +) +IF "%WINDOWS_SDK_VERSION%"=="" GOTO main + +SET DISTUTILS_USE_SDK=1 +SET MSSdk=1 +"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% +CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release + +:main +ECHO Executing: %COMMAND_TO_RUN% +CALL %COMMAND_TO_RUN% || EXIT 1 diff --git a/ci/bootstrap.py b/ci/bootstrap.py new file mode 100755 index 0000000..61747a1 --- /dev/null +++ b/ci/bootstrap.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import os +import subprocess +import sys +from collections import defaultdict +from os.path import abspath +from os.path import dirname +from os.path import exists +from os.path import join + +base_path = dirname(dirname(abspath(__file__))) + + +def check_call(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") + else: + bin_path = join(env_path, "bin") + if not exists(env_path): + import subprocess + + print("Making bootstrap env in: {0} ...".format(env_path)) + try: + check_call([sys.executable, "-m", "venv", env_path]) + except subprocess.CalledProcessError: + try: + check_call([sys.executable, "-m", "virtualenv", env_path]) + except subprocess.CalledProcessError: + check_call(["virtualenv", env_path]) + print("Installing `jinja2` into bootstrap environment...") + check_call([join(bin_path, "pip"), "install", "jinja2", "tox"]) + python_executable = join(bin_path, "python") + if not os.path.exists(python_executable): + python_executable += '.exe' + + print("Re-executing with: {0}".format(python_executable)) + print("+ exec", python_executable, __file__, "--no-env") + os.execv(python_executable, [python_executable, __file__, "--no-env"]) + + +def main(): + import jinja2 + + print("Project path: {0}".format(base_path)) + + jinja = jinja2.Environment( + loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True + ) + + tox_environments = [ + line.strip() + # WARNING: 'tox' must be installed globally or in the project's virtualenv + for line in subprocess.check_output(['tox', '--listenvs'], universal_newlines=True).splitlines() + ] + tox_environments = [line for line in tox_environments if line not in ['clean', 'report', 'docs', 'check']] + + template_vars = defaultdict(list) + template_vars['tox_environments'] = tox_environments + for env in tox_environments: + first, _ = env.split('-', 1) + template_vars['%s_environments' % first].append(env) + + for name in os.listdir(join("ci", "templates")): + with open(join(base_path, name), "w") as fh: + fh.write('# NOTE: this file is auto-generated via ci/bootstrap.py (ci/templates/%s).\n' % name) + fh.write(jinja.get_template(name).render(**template_vars)) + print("Wrote {}".format(name)) + print("DONE.") + + +if __name__ == "__main__": + args = sys.argv[1:] + if args == ["--no-env"]: + main() + elif not args: + exec_in_env() + else: + print("Unexpected arguments {0}".format(args), file=sys.stderr) + sys.exit(1) diff --git a/ci/requirements.txt b/ci/requirements.txt new file mode 100644 index 0000000..d7f5177 --- /dev/null +++ b/ci/requirements.txt @@ -0,0 +1,4 @@ +virtualenv>=16.6.0 +pip>=19.1.1 +setuptools>=18.0.1 +six>=1.14.0 diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml new file mode 100644 index 0000000..92630e1 --- /dev/null +++ b/ci/templates/.appveyor.yml @@ -0,0 +1,32 @@ +version: '{branch}-{build}' +build: off +environment: + matrix: + - TOXENV: check + - TOXENV: '{{ py27_environments|join(",") }}' + - TOXENV: '{{ py35_environments|join(",") }}' + - TOXENV: '{{ py36_environments|join(",") }}' + - TOXENV: '{{ py37_environments|join(",") }}' + - TOXENV: '{{ pypy_environments|join(",") }}' + +init: + - ps: echo $env:TOXENV + - ps: ls C:\Python* +install: + - IF "%TOXENV:~0,5%" == "pypy-" choco install --no-progress python.pypy + - IF "%TOXENV:~0,6%" == "pypy3-" choco install --no-progress pypy3 + - SET PATH=C:\tools\pypy\pypy;%PATH% + - C:\Python37\python -m pip install --progress-bar=off tox -rci/requirements.txt + +test_script: + - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd C:\Python37\python -m tox + +on_failure: + - ps: dir "env:" + - ps: get-content .tox\*\log\* +artifacts: + - path: dist\* + +### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml new file mode 100644 index 0000000..8c7e3b9 --- /dev/null +++ b/ci/templates/.travis.yml @@ -0,0 +1,61 @@ +dist: xenial +language: python +cache: false +env: + global: + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all +stages: + - lint + - examples + - tests +jobs: + fast_finish: true + allow_failures: + - python: '3.8' + include: + - stage: lint + env: TOXENV=check + - env: TOXENV=docs + + - stage: tests +{% for env in tox_environments %} + {%+ if not loop.first %}- {% else %} {% endif -%} + env: TOXENV={{ env }} + {% if env.startswith("pypy-") %} + python: 'pypy' + {% elif env.startswith("pypy3-") %} + python: 'pypy3' + {% else %} + python: '{{ "{0[2]}.{0[3]}".format(env) }}' + {% endif -%} +{% endfor %} + + - stage: examples +{%- for example in ['src', 'adhoc'] %}{{ '' }} + {%+ if not loop.first %}- {% else %} {% endif -%} + python: '3.8' + script: cd $TARGET; tox -v + env: + - TARGET=examples/{{ example }}-layout +{%- endfor %} + +before_install: + - python --version + - uname -a + - lsb_release -a +install: + - python -mpip install --progress-bar=off tox -rci/requirements.txt + - virtualenv --version + - easy_install --version + - pip --version + - tox --version +script: + - tox -v +after_failure: + - more .tox/log/* | cat + - more .tox/*/log/* | cat +notifications: + email: + on_success: never + on_failure: always diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..e122f91 --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..565b052 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..353927d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os + +import sphinx_py3doc_enhanced_theme + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', + 'sphinx.ext.extlinks', +] +if os.getenv('SPELLCHECK'): + extensions += 'sphinxcontrib.spelling', + spelling_show_suggestions = True + spelling_lang = 'en_US' + +source_suffix = '.rst' +master_doc = 'index' +project = 'pytest-cov' +year = '2016' +author = 'pytest-cov contributors' +copyright = '{}, {}'.format(year, author) +version = release = '2.10.1' + +pygments_style = 'trac' +templates_path = ['.'] +extlinks = { + 'issue': ('https://github.com/pytest-dev/pytest-cov/issues/%s', '#'), + 'pr': ('https://github.com/pytest-dev/pytest-cov/pull/%s', 'PR #'), +} + +html_theme = "sphinx_py3doc_enhanced_theme" +html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] +html_theme_options = { + 'githuburl': 'https://github.com/pytest-dev/pytest-cov/' +} + +html_use_smartypants = True +html_last_updated_fmt = '%b %d, %Y' +html_split_index = True +html_sidebars = { + '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], +} +html_short_title = '%s-%s' % (project, version) + +napoleon_use_ivar = True +napoleon_use_rtype = False +napoleon_use_param = False diff --git a/docs/config.rst b/docs/config.rst new file mode 100644 index 0000000..3e5bf93 --- /dev/null +++ b/docs/config.rst @@ -0,0 +1,66 @@ +============= +Configuration +============= + +This plugin provides a clean minimal set of command line options that are added to pytest. For +further control of coverage use a coverage config file. + +For example if tests are contained within the directory tree being measured the tests may be +excluded if desired by using a .coveragerc file with the omit option set:: + + pytest --cov-config=.coveragerc + --cov=myproj + myproj/tests/ + +Where the .coveragerc file contains file globs:: + + [run] + omit = tests/* + +For full details refer to the `coverage config file`_ documentation. + +.. _`coverage config file`: https://coverage.readthedocs.io/en/latest/config.html + +Note that this plugin controls some options and setting the option in the config file will have no +effect. These include specifying source to be measured (source option) and all data file handling +(data_file and parallel options). + +If you wish to always add pytest-cov with pytest, you can use ``addopts`` under ``pytest`` or ``tool:pytest`` section. +For example: :: + + [tool:pytest] + addopts = --cov= --cov-report html + +Caveats +======= + +A unfortunate consequence of coverage.py's history is that ``.coveragerc`` is a magic name: it's the default file but it also +means "try to also lookup coverage configuration in ``tox.ini`` or ``setup.cfg``". + +In practical terms this means that if you have your coverage configuration in ``tox.ini`` or ``setup.cfg`` it is paramount +that you also use ``--cov-config=tox.ini`` or ``--cov-config=setup.cfg``. + +You might not be affected but it's unlikely that you won't ever use ``chdir`` in a test. + +Reference +========= + +The complete list of command line options is: + + --cov=PATH Measure coverage for filesystem path. (multi-allowed) + --cov-report=type Type of report to generate: term, term-missing, + annotate, html, xml (multi-allowed). term, term- + missing may be followed by ":skip-covered". annotate, + html and xml may be followed by ":DEST" where DEST + specifies the output location. Use --cov-report= to + not generate any output. + --cov-config=path Config file for coverage. Default: .coveragerc + --no-cov-on-fail Do not report coverage if test run fails. Default: + False + --no-cov Disable coverage report completely (useful for + debuggers). Default: False + --cov-fail-under=MIN Fail if the total coverage is less than MIN. + --cov-append Do not delete coverage but append to current. Default: + False + --cov-branch Enable branch coverage. + --cov-context Choose the method for setting the dynamic context. diff --git a/docs/contexts.rst b/docs/contexts.rst new file mode 100644 index 0000000..e5256fc --- /dev/null +++ b/docs/contexts.rst @@ -0,0 +1,18 @@ +======== +Contexts +======== + +Coverage.py 5.0 can record separate coverage data for different contexts during +one run of a test suite. Pytest-cov can use this feature to record coverage +data for each test individually, with the ``--cov-context=test`` option. + +The context name recorded in the coverage.py database is the pytest test id, +and the phase of execution, one of "setup", "run", or "teardown". These two +are separated with a pipe symbol. You might see contexts like:: + + test_functions.py::test_addition|run + test_fancy.py::test_parametrized[1-101]|setup + test_oldschool.py::RegressionTests::test_error|run + +Note that parameterized tests include the values of the parameters in the test +id, and each set of parameter values is recorded as a separate test. diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/debuggers.rst b/docs/debuggers.rst new file mode 100644 index 0000000..15c8321 --- /dev/null +++ b/docs/debuggers.rst @@ -0,0 +1,15 @@ +===================== +Debuggers and PyCharm +===================== + +(or other IDEs) + +When it comes to TDD one obviously would like to debug tests. Debuggers in Python use mostly the sys.settrace function +to gain access to context. Coverage uses the same technique to get access to the lines executed. Coverage does not play +well with other tracers simultaneously running. This manifests itself in behaviour that PyCharm might not hit a +breakpoint no matter what the user does. Since it is common practice to have coverage configuration in the pytest.ini +file and pytest does not support removeopts or similar the `--no-cov` flag can disable coverage completely. + +At the reporting part a warning message will show on screen:: + + Coverage disabled via --no-cov switch! diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..32fb1ca --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,29 @@ +Welcome to pytest-cov's documentation! +====================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + config + reporting + debuggers + xdist + subprocess-support + contexts + tox + plugins + markers-fixtures + changelog + authors + releasing + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/markers-fixtures.rst b/docs/markers-fixtures.rst new file mode 100644 index 0000000..d0eef23 --- /dev/null +++ b/docs/markers-fixtures.rst @@ -0,0 +1,43 @@ +==================== +Markers and fixtures +==================== + +There are some builtin markers and fixtures in ``pytest-cov``. + +Markers +======= + +``no_cover`` +------------ + +Eg: + +.. code-block:: python + + @pytest.mark.no_cover + def test_foobar(): + # do some stuff that needs coverage disabled + +.. warning:: Caveat + + Note that subprocess coverage will also be disabled. + +Fixtures +======== + +``no_cover`` +------------ + +Eg: + +.. code-block:: python + + def test_foobar(no_cover): + # same as the marker ... + +``cov`` +------- + +For reasons that no one can remember there is a ``cov`` fixture that provides access to the underlying Coverage instance. +Some say this is a disguised foot-gun and should be removed, and some think mysteries make life more interesting and it should +be left alone. diff --git a/docs/plugins.rst b/docs/plugins.rst new file mode 100644 index 0000000..d06c4ff --- /dev/null +++ b/docs/plugins.rst @@ -0,0 +1,24 @@ +=============== +Plugin coverage +=============== + +Getting coverage on pytest plugins is a very particular situation. Because 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. + +The current way of dealing with this problem is using the append feature and manually starting ``pytest-cov``'s engine, eg:: + + 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):: + + [testenv] + setenv = + COV_CORE_SOURCE= + COV_CORE_CONFIG={toxinidir}/.coveragerc + COV_CORE_DATAFILE={toxinidir}/.coverage + +And in ``pytest.ini`` / ``tox.ini`` / ``setup.cfg``:: + + [tool:pytest] + addopts = --cov --cov-append diff --git a/docs/readme.rst b/docs/readme.rst new file mode 100644 index 0000000..72a3355 --- /dev/null +++ b/docs/readme.rst @@ -0,0 +1 @@ +.. include:: ../README.rst diff --git a/docs/releasing.rst b/docs/releasing.rst new file mode 100644 index 0000000..245dca5 --- /dev/null +++ b/docs/releasing.rst @@ -0,0 +1,34 @@ +========= +Releasing +========= + +The process for releasing should follow these steps: + +#. Test that docs build and render properly by running ``tox -e docs,spell``. + + If there are bogus spelling issues add the words in ``spelling_wordlist.txt``. +#. Update ``CHANGELOG.rst`` and ``AUTHORS.rst`` to be up to date. +#. Bump the version by running ``bumpversion [ major | minor | patch ]``. This will automatically add a tag. + + Alternatively, you can manually edit the files and run ``git tag v1.2.3`` yourself. +#. Push changes and tags with:: + + git push + git push --tags +#. Wait for `AppVeyor `_ + and `Travis `_ to give the green builds. +#. Check that the docs on `ReadTheDocs `_ are built. +#. Make sure you have a clean checkout, run ``git status`` to verify. +#. Manually clean temporary files (that are ignored and won't show up in ``git status``):: + + rm -rf dist build src/*.egg-info + + These files need to be removed to force distutils/setuptools to rebuild everything and recreate the egg-info metadata. +#. Build the dists:: + + python3 setup.py clean --all sdist bdist_wheel + +#. Verify that the resulting archives (found in ``dist/``) are good. +#. Upload the sdist and wheel with twine:: + + twine upload dist/* diff --git a/docs/reporting.rst b/docs/reporting.rst new file mode 100644 index 0000000..e9e4b06 --- /dev/null +++ b/docs/reporting.rst @@ -0,0 +1,74 @@ +Reporting +========= + +It is possible to generate any combination of the reports for a single test run. + +The available reports are terminal (with or without missing line numbers shown), HTML, XML and +annotated source code. + +The terminal report without line numbers (default):: + + pytest --cov-report term --cov=myproj tests/ + + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% + + +The terminal report with line numbers:: + + pytest --cov-report term-missing --cov=myproj tests/ + + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover Missing + -------------------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% 24-26, 99, 149, 233-236, 297-298, 369-370 + myproj/feature4286 94 7 92% 183-188, 197 + -------------------------------------------------- + TOTAL 353 20 94% + +The terminal report with skip covered:: + + pytest --cov-report term:skip-covered --cov=myproj tests/ + + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% + + 1 files skipped due to complete coverage. + +You can use ``skip-covered`` with ``term-missing`` as well. e.g. ``--cov-report term-missing:skip-covered`` + +These three report options output to files without showing anything on the terminal:: + + pytest --cov-report html + --cov-report xml + --cov-report annotate + --cov=myproj tests/ + +The output location for each of these reports can be specified. The output location for the XML +report is a file. Where as the output location for the HTML and annotated source code reports are +directories:: + + pytest --cov-report html:cov_html + --cov-report xml:cov.xml + --cov-report annotate:cov_annotate + --cov=myproj tests/ + +The final report option can also suppress printing to the terminal:: + + pytest --cov-report= --cov=myproj tests/ + +This mode can be especially useful on continuous integration servers, where a coverage file +is needed for subsequent processing, but no local report needs to be viewed. For example, +tests run on Travis-CI could produce a .coverage file for use with Coveralls. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..ccec79f --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +sphinx==3.0.3 +sphinx-py3doc-enhanced-theme==2.4.0 +docutils==0.16 +-e . diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt new file mode 100644 index 0000000..f95eb78 --- /dev/null +++ b/docs/spelling_wordlist.txt @@ -0,0 +1,11 @@ +builtin +builtins +classmethod +staticmethod +classmethods +staticmethods +args +kwargs +callstack +Changelog +Indices diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst new file mode 100644 index 0000000..331db7d --- /dev/null +++ b/docs/subprocess-support.rst @@ -0,0 +1,174 @@ +================== +Subprocess support +================== + +Normally coverage writes the data via a pretty standard atexit handler. However, if the subprocess doesn't exit on its +own then the atexit handler might not run. Why that happens is best left to the adventurous to discover by waddling +through the Python bug tracker. + +pytest-cov supports subprocesses and multiprocessing, and works around these atexit limitations. However, there are a +few pitfalls that need to be explained. + +If you use ``multiprocessing.Pool`` +=================================== + +**pytest-cov** automatically registers a multiprocessing finalizer. The finalizer will only run reliably if the pool is +closed. Closing the pool basically signals the workers that there will be no more work, and they will eventually exit. +Thus one also needs to call `join` on the pool. + +If you use ``multiprocessing.Pool.terminate`` or the context manager API (``__exit__`` +will just call ``terminate``) then the workers can get SIGTERM and then the finalizers won't run or complete in time. +Thus you need to make sure your ``multiprocessing.Pool`` gets a nice and clean exit: + +.. code-block:: python + + from multiprocessing import Pool + + def f(x): + return x*x + + if __name__ == '__main__': + p = Pool(5) + try: + print(p.map(f, [1, 2, 3])) + finally: + p.close() # Marks the pool as closed. + p.join() # Waits for workers to exit. + + +If you must use the context manager API (e.g.: the pool is managed in third party code you can't change) then you can +register a cleaning SIGTERM handler like so: + +.. warning:: + + **This technique cannot be used on Python 3.8** (registering signal handlers will cause deadlocks in the pool, + see: https://bugs.python.org/issue38227). + +.. code-block:: python + + from multiprocessing import Pool + + def f(x): + return x*x + + if __name__ == '__main__': + try: + from pytest_cov.embed import cleanup_on_sigterm + except ImportError: + pass + else: + cleanup_on_sigterm() + + with Pool(5) as p: + print(p.map(f, [1, 2, 3])) + +If you use ``multiprocessing.Process`` +====================================== + +There's similar issue when using the ``Process`` objects. Don't forget to use ``.join()``: + +.. code-block:: python + + from multiprocessing import Process + + def f(name): + print('hello', name) + + if __name__ == '__main__': + try: + from pytest_cov.embed import cleanup_on_sigterm + except ImportError: + pass + else: + cleanup_on_sigterm() + + p = Process(target=f, args=('bob',)) + try: + p.start() + finally: + p.join() # necessary so that the Process exists before the test suite exits (thus coverage is collected) + +.. _cleanup_on_sigterm: + +If you got custom signal handling +================================= + +**pytest-cov 2.6** has a rudimentary ``pytest_cov.embed.cleanup_on_sigterm`` you can use to register a SIGTERM handler +that flushes the coverage data. + +**pytest-cov 2.7** adds a ``pytest_cov.embed.cleanup_on_signal`` function and changes the implementation to be more +robust: the handler will call the previous handler (if you had previously registered any), and is re-entrant (will +defer extra signals if delivered while the handler runs). + +For example, if you reload on SIGHUP you should have something like this: + +.. code-block:: python + + import os + import signal + + def restart_service(frame, signum): + os.exec( ... ) # or whatever your custom signal would do + signal.signal(signal.SIGHUP, restart_service) + + try: + from pytest_cov.embed import cleanup_on_signal + except ImportError: + pass + else: + cleanup_on_signal(signal.SIGHUP) + +Note that both ``cleanup_on_signal`` and ``cleanup_on_sigterm`` will run the previous signal handler. + +Alternatively you can do this: + +.. code-block:: python + + import os + import signal + + try: + from pytest_cov.embed import cleanup + except ImportError: + cleanup = None + + def restart_service(frame, signum): + if cleanup is not None: + cleanup() + + os.exec( ... ) # or whatever your custom signal would do + signal.signal(signal.SIGHUP, restart_service) + +If you use Windows +================== + +On Windows you can register a handler for SIGTERM but it doesn't actually work. It will work if you +`os.kill(os.getpid(), signal.SIGTERM)` (send SIGTERM to the current process) but for most intents and purposes that's +completely useless. + +Consequently this means that if you use multiprocessing you got no choice but to use the close/join pattern as described +above. Using the context manager API or `terminate` won't work as it relies on SIGTERM. + +However you can have a working handler for SIGBREAK (with some caveats): + +.. code-block:: python + + import os + import signal + + def shutdown(frame, signum): + # your app's shutdown or whatever + signal.signal(signal.SIGBREAK, shutdown) + + try: + from pytest_cov.embed import cleanup_on_signal + except ImportError: + pass + else: + cleanup_on_signal(signal.SIGBREAK) + +The `caveats `_ being +roughly: + +* you need to deliver ``signal.CTRL_BREAK_EVENT`` +* it gets delivered to the whole process group, and that can have unforeseen consequences diff --git a/docs/tox.rst b/docs/tox.rst new file mode 100644 index 0000000..18f9137 --- /dev/null +++ b/docs/tox.rst @@ -0,0 +1,73 @@ +=== +Tox +=== + +When using `tox `_ you can have ultra-compact configuration - you can have all of it in +``tox.ini``:: + + [tox] + envlist = ... + + [tool:pytest] + ... + + [coverage:paths] + ... + + [coverage:run] + ... + + [coverage:report] + .. + + [testenv] + commands = ... + +An usual problem users have is that pytest-cov will erase the previous coverage data by default, thus if you run tox +with multiple environments you'll get incomplete coverage at the end. + +To prevent this problem you need to use ``--cov-append``. It's still recommended to clean the previous coverage data to +have consistent output. A ``tox.ini`` like this should be enough for sequential runs:: + + [tox] + envlist = clean,py27,py36,... + + [testenv] + commands = pytest --cov --cov-append --cov-report=term-missing ... + deps = + pytest + pytest-cov + + [testenv:clean] + deps = coverage + skip_install = true + commands = coverage erase + +For parallel runs we need to set some dependencies and have an extra report env like so:: + + [tox] + envlist = clean,py27,py36,report + + [testenv] + commands = pytest --cov --cov-append --cov-report=term-missing + deps = + pytest + pytest-cov + depends = + {py27,py36}: clean + report: py27,py36 + + [testenv:report] + deps = coverage + skip_install = true + commands = + coverage report + coverage html + + [testenv:clean] + deps = coverage + skip_install = true + commands = coverage erase + +Depending on your project layout you might need extra configuration, see the working examples at +https://github.com/pytest-dev/pytest-cov/tree/master/examples for two common layouts. diff --git a/docs/xdist.rst b/docs/xdist.rst new file mode 100644 index 0000000..a2da50e --- /dev/null +++ b/docs/xdist.rst @@ -0,0 +1,74 @@ +=========================== +Distributed testing (xdist) +=========================== + +"load" mode +=========== + +Distributed testing with dist mode set to "load" will report on the combined coverage of all workers. +The workers may be spread out over any number of hosts and each worker may be located anywhere on the +file system. Each worker will have its subprocesses measured. + +Running distributed testing with dist mode set to load:: + + pytest --cov=myproj -n 2 tests/ + +Shows a terminal report:: + + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% + + +Again but spread over different hosts and different directories:: + + pytest --cov=myproj --dist load + --tx ssh=memedough@host1//chdir=testenv1 + --tx ssh=memedough@host2//chdir=/tmp/testenv2//python=/tmp/env1/bin/python + --rsyncdir myproj --rsyncdir tests --rsync examples + tests/ + +Shows a terminal report:: + + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% + + +"each" mode +=========== + +Distributed testing with dist mode set to each will report on the combined coverage of all workers. +Since each worker is running all tests this allows generating a combined coverage report for multiple +environments. + +Running distributed testing with dist mode set to each:: + + pytest --cov=myproj --dist each + --tx popen//chdir=/tmp/testenv3//python=/usr/local/python27/bin/python + --tx ssh=memedough@host2//chdir=/tmp/testenv4//python=/tmp/env2/bin/python + --rsyncdir myproj --rsyncdir tests --rsync examples + tests/ + +Shows a terminal report:: + + ---------------------------------------- coverage ---------------------------------------- + platform linux2, python 2.6.5-final-0 + platform linux2, python 2.7.0-final-0 + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% diff --git a/examples/README.rst b/examples/README.rst new file mode 100644 index 0000000..a232f2a --- /dev/null +++ b/examples/README.rst @@ -0,0 +1,15 @@ +Simple examples with ``tox.ini`` +================================ + +These examples provide necessary configuration to: + +* aggregate coverage from multiple interpreters +* support tox parallel mode +* run tests on installed code + +The `adhoc` layout is the old and problematic layout where you can mix up the installed code +with the source code. However, these examples will provide correct configuration even for +the `adhoc` layout. + +The `src` layout configuration is less complicated, have that in mind when picking a layout +for your project. diff --git a/examples/adhoc-layout/.coveragerc b/examples/adhoc-layout/.coveragerc new file mode 100644 index 0000000..199d42e --- /dev/null +++ b/examples/adhoc-layout/.coveragerc @@ -0,0 +1,15 @@ +[paths] +source = + ../example + */site-packages/example + +[run] +branch = true +parallel = true +source = + example + . + +[report] +show_missing = true +precision = 2 diff --git a/examples/adhoc-layout/example/__init__.py b/examples/adhoc-layout/example/__init__.py new file mode 100644 index 0000000..18080ac --- /dev/null +++ b/examples/adhoc-layout/example/__init__.py @@ -0,0 +1,13 @@ + +import sys + +PY2 = sys.version_info[0] == 2 + + +if PY2: + def add(a, b): + return b + a + +else: + def add(a, b): + return a + b diff --git a/examples/adhoc-layout/setup.py b/examples/adhoc-layout/setup.py new file mode 100644 index 0000000..e52b68d --- /dev/null +++ b/examples/adhoc-layout/setup.py @@ -0,0 +1,7 @@ +from setuptools import find_packages +from setuptools import setup + +setup( + name='example', + packages=find_packages(include=['example']) +) diff --git a/examples/adhoc-layout/tests/test_example.py b/examples/adhoc-layout/tests/test_example.py new file mode 100644 index 0000000..f4948e6 --- /dev/null +++ b/examples/adhoc-layout/tests/test_example.py @@ -0,0 +1,6 @@ +import example + + +def test_add(): + assert example.add(1, 1) == 2 + assert not example.add(0, 1) == 2 diff --git a/examples/adhoc-layout/tox.ini b/examples/adhoc-layout/tox.ini new file mode 100644 index 0000000..a2472d0 --- /dev/null +++ b/examples/adhoc-layout/tox.ini @@ -0,0 +1,36 @@ +[tox] +envlist = clean,py27,py38,report + +[tool:pytest] +addopts = + --cov-report=term-missing + +[testenv] +commands = pytest --cov --cov-append --cov-config={toxinidir}/.coveragerc {posargs:-vv} +deps = + pytest + coverage +# Note: +# This is here just to allow examples to be tested against +# the current code of pytest-cov. If you copy this then +# use "pytest-cov" instead of "../.." + ../.. + +depends = + {py27,py38}: clean + report: py27,py38 + +# note that this is necessary to prevent the tests importing the code from your badly laid project +changedir = tests + +[testenv:report] +skip_install = true +deps = coverage +commands = + coverage html + coverage report --fail-under=100 + +[testenv:clean] +skip_install = true +deps = coverage +commands = coverage erase diff --git a/examples/src-layout/.coveragerc b/examples/src-layout/.coveragerc new file mode 100644 index 0000000..b4c80de --- /dev/null +++ b/examples/src-layout/.coveragerc @@ -0,0 +1,15 @@ +[paths] +source = + src + */site-packages + +[run] +branch = true +parallel = true +source = + example + tests + +[report] +show_missing = true +precision = 2 diff --git a/examples/src-layout/setup.py b/examples/src-layout/setup.py new file mode 100644 index 0000000..c8ded81 --- /dev/null +++ b/examples/src-layout/setup.py @@ -0,0 +1,8 @@ +from setuptools import find_packages +from setuptools import setup + +setup( + name='example', + packages=find_packages('src'), + package_dir={'': 'src'}, +) diff --git a/examples/src-layout/src/example/__init__.py b/examples/src-layout/src/example/__init__.py new file mode 100644 index 0000000..18080ac --- /dev/null +++ b/examples/src-layout/src/example/__init__.py @@ -0,0 +1,13 @@ + +import sys + +PY2 = sys.version_info[0] == 2 + + +if PY2: + def add(a, b): + return b + a + +else: + def add(a, b): + return a + b diff --git a/examples/src-layout/tests/test_example.py b/examples/src-layout/tests/test_example.py new file mode 100644 index 0000000..f4948e6 --- /dev/null +++ b/examples/src-layout/tests/test_example.py @@ -0,0 +1,6 @@ +import example + + +def test_add(): + assert example.add(1, 1) == 2 + assert not example.add(0, 1) == 2 diff --git a/examples/src-layout/tox.ini b/examples/src-layout/tox.ini new file mode 100644 index 0000000..054b4b8 --- /dev/null +++ b/examples/src-layout/tox.ini @@ -0,0 +1,34 @@ +[tox] +envlist = clean,py27,py38,report + +[tool:pytest] +testpaths = tests +addopts = + --cov-report=term-missing + +[testenv] +commands = pytest --cov --cov-append {posargs:-vv} +deps = + pytest + coverage +# Note: +# This is here just to allow examples to be tested against +# the current code of pytest-cov. If you copy this then +# use "pytest-cov" instead of "../.." + ../.. + +depends = + {py27,py38}: clean + report: py27,py38 + +[testenv:report] +skip_install = true +deps = coverage +commands = + coverage html + coverage report --fail-under=100 + +[testenv:clean] +skip_install = true +deps = coverage +commands = coverage erase diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..25b8349 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,27 @@ +[bdist_wheel] +universal = 1 + +[flake8] +max-line-length = 140 +exclude = .tox,.eggs,ci/templates,build,dist + +[tool:pytest] +testpaths = tests +python_files = test_*.py +addopts = + -ra + --strict + -p pytester + +[tool:isort] +force_single_line = True +line_length = 120 +known_first_party = pytest_cov +default_section = THIRDPARTY +forced_separate = test_pytest_cov +skip = .tox,.eggs,ci/templates,build,dist + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..6796670 --- /dev/null +++ b/setup.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function + +import io +import re +from distutils.command.build import build +from glob import glob +from itertools import chain +from os.path import basename +from os.path import dirname +from os.path import join +from os.path import splitext + +from setuptools import Command +from setuptools import find_packages +from setuptools import setup +from setuptools.command.develop import develop +from setuptools.command.easy_install import easy_install +from setuptools.command.install_lib import install_lib + + +def read(*names, **kwargs): + with io.open( + join(dirname(__file__), *names), + encoding=kwargs.get('encoding', 'utf8') + ) as fh: + return fh.read() + + +class BuildWithPTH(build): + def run(self): + build.run(self) + path = join(dirname(__file__), 'src', 'pytest-cov.pth') + dest = join(self.build_lib, basename(path)) + self.copy_file(path, dest) + + +class EasyInstallWithPTH(easy_install): + def run(self): + easy_install.run(self) + path = join(dirname(__file__), 'src', 'pytest-cov.pth') + dest = join(self.install_dir, basename(path)) + self.copy_file(path, dest) + + +class InstallLibWithPTH(install_lib): + def run(self): + install_lib.run(self) + path = join(dirname(__file__), 'src', 'pytest-cov.pth') + dest = join(self.install_dir, basename(path)) + self.copy_file(path, dest) + self.outputs = [dest] + + def get_outputs(self): + return chain(install_lib.get_outputs(self), self.outputs) + + +class DevelopWithPTH(develop): + def run(self): + develop.run(self) + path = join(dirname(__file__), 'src', 'pytest-cov.pth') + dest = join(self.install_dir, basename(path)) + self.copy_file(path, dest) + + +class GeneratePTH(Command): + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + with open(join(dirname(__file__), 'src', 'pytest-cov.pth'), 'w') as fh: + with open(join(dirname(__file__), 'src', 'pytest-cov.embed')) as sh: + fh.write( + 'import os, sys;' + 'exec(%r)' % sh.read().replace(' ', ' ') + ) + + +setup( + name='pytest-cov', + version='2.10.1', + license='MIT', + description='Pytest plugin for measuring coverage.', + long_description='%s\n%s' % (read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), + author='Marc Schlaich', + author_email='marc.schlaich@gmail.com', + url='https://github.com/pytest-dev/pytest-cov', + packages=find_packages('src'), + package_dir={'': 'src'}, + py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + include_package_data=True, + zip_safe=False, + classifiers=[ + # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Development Status :: 5 - Production/Stable', + 'Framework :: Pytest', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: Unix', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Software Development :: Testing', + 'Topic :: Utilities', + ], + keywords=[ + 'cover', 'coverage', 'pytest', 'py.test', 'distributed', 'parallel', + ], + install_requires=[ + 'pytest>=4.6', + 'coverage>=4.4' + ], + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', + extras_require={ + 'testing': [ + 'fields', + 'hunter', + 'process-tests==2.0.2', + 'six', + 'pytest-xdist', + 'virtualenv', + ] + }, + entry_points={ + 'pytest11': [ + 'pytest_cov = pytest_cov.plugin', + ], + 'console_scripts': [ + ] + }, + cmdclass={ + 'build': BuildWithPTH, + 'easy_install': EasyInstallWithPTH, + 'install_lib': InstallLibWithPTH, + 'develop': DevelopWithPTH, + 'genpth': GeneratePTH, + }, +) diff --git a/src/pytest-cov.embed b/src/pytest-cov.embed new file mode 100644 index 0000000..630a2a7 --- /dev/null +++ b/src/pytest-cov.embed @@ -0,0 +1,13 @@ +if 'COV_CORE_SOURCE' in os.environ: + try: + from pytest_cov.embed import init + init() + except Exception as exc: + sys.stderr.write( + "pytest-cov: Failed to setup subprocess coverage. " + "Environ: {0!r} " + "Exception: {1!r}\n".format( + dict((k, v) for k, v in os.environ.items() if k.startswith('COV_CORE')), + exc + ) + ) diff --git a/src/pytest-cov.pth b/src/pytest-cov.pth new file mode 100644 index 0000000..91f2b7c --- /dev/null +++ b/src/pytest-cov.pth @@ -0,0 +1 @@ +import os, sys;exec('if \'COV_CORE_SOURCE\' in os.environ:\n try:\n from pytest_cov.embed import init\n init()\n except Exception as exc:\n sys.stderr.write(\n "pytest-cov: Failed to setup subprocess coverage. "\n "Environ: {0!r} "\n "Exception: {1!r}\\n".format(\n dict((k, v) for k, v in os.environ.items() if k.startswith(\'COV_CORE\')),\n exc\n )\n )\n') \ No newline at end of file diff --git a/src/pytest_cov.egg-info/PKG-INFO b/src/pytest_cov.egg-info/PKG-INFO new file mode 100644 index 0000000..c036dc8 --- /dev/null +++ b/src/pytest_cov.egg-info/PKG-INFO @@ -0,0 +1,447 @@ +Metadata-Version: 2.1 +Name: pytest-cov +Version: 2.10.1 +Summary: Pytest plugin for measuring coverage. +Home-page: https://github.com/pytest-dev/pytest-cov +Author: Marc Schlaich +Author-email: marc.schlaich@gmail.com +License: MIT +Description: ======== + Overview + ======== + + .. start-badges + + .. list-table:: + :stub-columns: 1 + + * - docs + - |docs| + * - tests + - | |travis| |appveyor| |requires| + * - package + - | |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| + | |commits-since| + + .. |docs| image:: https://readthedocs.org/projects/pytest-cov/badge/?style=flat + :target: https://readthedocs.org/projects/pytest-cov + :alt: Documentation Status + + .. |travis| image:: https://api.travis-ci.org/pytest-dev/pytest-cov.svg?branch=master + :alt: Travis-CI Build Status + :target: https://travis-ci.org/pytest-dev/pytest-cov + + .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/pytest-dev/pytest-cov?branch=master&svg=true + :alt: AppVeyor Build Status + :target: https://ci.appveyor.com/project/pytestbot/pytest-cov + + .. |requires| image:: https://requires.io/github/pytest-dev/pytest-cov/requirements.svg?branch=master + :alt: Requirements Status + :target: https://requires.io/github/pytest-dev/pytest-cov/requirements/?branch=master + + .. |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/v2.10.1.svg + :alt: Commits since latest release + :target: https://github.com/pytest-dev/pytest-cov/compare/v2.10.1...master + + .. |wheel| image:: https://img.shields.io/pypi/wheel/pytest-cov.svg + :alt: PyPI Wheel + :target: https://pypi.org/project/pytest-cov + + .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/pytest-cov.svg + :alt: Supported versions + :target: https://pypi.org/project/pytest-cov + + .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/pytest-cov.svg + :alt: Supported implementations + :target: https://pypi.org/project/pytest-cov + + .. end-badges + + This plugin produces coverage reports. Compared to just using ``coverage run`` this plugin does some extras: + + * Subprocess support: you can fork or run stuff in a subprocess and will get covered without any fuss. + * Xdist support: you can use all of pytest-xdist's features and still get coverage. + * Consistent pytest behavior. If you run ``coverage run -m pytest`` you will have slightly different ``sys.path`` (CWD will be + in it, unlike when running ``pytest``). + + All features offered by the coverage package should work, either through pytest-cov's command line options or + through coverage's config file. + + * Free software: MIT license + + Installation + ============ + + Install with pip:: + + pip install pytest-cov + + For distributed testing support install pytest-xdist:: + + pip install pytest-xdist + + Upgrading from ancient pytest-cov + --------------------------------- + + `pytest-cov 2.0` is using a new ``.pth`` file (``pytest-cov.pth``). You may want to manually remove the older + ``init_cov_core.pth`` from site-packages as it's not automatically removed. + + Uninstalling + ------------ + + Uninstall with pip:: + + pip uninstall pytest-cov + + Under certain scenarios a stray ``.pth`` file may be left around in site-packages. + + * `pytest-cov 2.0` may leave a ``pytest-cov.pth`` if you installed without wheels + (``easy_install``, ``setup.py install`` etc). + * `pytest-cov 1.8 or older` will leave a ``init_cov_core.pth``. + + Usage + ===== + + :: + + pytest --cov=myproj tests/ + + Would produce a report like:: + + -------------------- coverage: ... --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% + + Documentation + ============= + + http://pytest-cov.rtfd.org/ + + + + + + + Coverage Data File + ================== + + The data file is erased at the beginning of testing to ensure clean data for each test run. If you + need to combine the coverage of several test runs you can use the ``--cov-append`` option to append + this coverage data to coverage data from previous test runs. + + The data file is left at the end of testing so that it is possible to use normal coverage tools to + examine it. + + Limitations + =========== + + For distributed testing the workers must have the pytest-cov package installed. This is needed since + the plugin must be registered through setuptools for pytest to start the plugin on the + worker. + + For subprocess measurement environment variables must make it from the main process to the + subprocess. The python used by the subprocess must have pytest-cov installed. The subprocess must + do normal site initialisation so that the environment variables can be detected and coverage + started. + + + Acknowledgements + ================ + + Whilst this plugin has been built fresh from the ground up it has been influenced by the work done + on pytest-coverage (Ross Lawley, James Mills, Holger Krekel) and nose-cover (Jason Pellerin) which are + other coverage plugins. + + Ned Batchelder for coverage and its ability to combine the coverage results of parallel runs. + + Holger Krekel for pytest with its distributed testing support. + + Jason Pellerin for nose. + + Michael Foord for unittest2. + + No doubt others have contributed to these tools as well. + + Changelog + ========= + + 2.10.1 (2020-08-14) + ------------------- + + * Support for ``pytest-xdist`` 2.0, which breaks compatibility with ``pytest-xdist`` before 1.22.3 (from 2017). + Contributed by Zac Hatfield-Dodds in `#412 `_. + * Fixed the ``LocalPath has no attribute startswith`` failure that occurred when using the ``pytester`` plugin + in inline mode. + + 2.10.0 (2020-06-12) + ------------------- + + * Improved the ``--no-cov`` warning. Now it's only shown if ``--no-cov`` is present before ``--cov``. + * Removed legacy pytest support. Changed ``setup.py`` so that ``pytest>=4.6`` is required. + + 2.9.0 (2020-05-22) + ------------------ + + * Fixed ``RemovedInPytest4Warning`` when using Pytest 3.10. + Contributed by Michael Manganiello in `#354 `_. + * Made pytest startup faster when plugin not active by lazy-importing. + Contributed by Anders Hovmöller in `#339 `_. + * Various CI improvements. + Contributed by Daniel Hahler in `#363 `_ and + `#364 `_. + * Various Python support updates (drop EOL 3.4, test against 3.8 final). + Contributed by Hugo van Kemenade in + `#336 `_ and + `#367 `_. + * Changed ``--cov-append`` to always enable ``data_suffix`` (a coverage setting). + Contributed by Harm Geerts in + `#387 `_. + * Changed ``--cov-append`` to handle loading previous data better + (fixes various path aliasing issues). + * Various other testing improvements, github issue templates, example updates. + * Fixed internal failures that are caused by tests that change the current working directory by + ensuring a consistent working directory when coverage is called. + See `#306 `_ and + `coveragepy#881 `_ + + 2.8.1 (2019-10-05) + ------------------ + + * Fixed `#348 `_ - + regression when only certain reports (html or xml) are used then ``--cov-fail-under`` always fails. + + 2.8.0 (2019-10-04) + ------------------ + + * Fixed ``RecursionError`` that can occur when using + `cleanup_on_signal `__ or + `cleanup_on_sigterm `__. + See: `#294 `_. + The 2.7.x releases of pytest-cov should be considered broken regarding aforementioned cleanup API. + * Added compatibility with future xdist release that deprecates some internals + (match pytest-xdist master/worker terminology). + Contributed by Thomas Grainger in `#321 `_ + * Fixed breakage that occurs when multiple reporting options are used. + Contributed by Thomas Grainger in `#338 `_. + * Changed internals to use a stub instead of ``os.devnull``. + Contributed by Thomas Grainger in `#332 `_. + * Added support for Coverage 5.0. + Contributed by Ned Batchelder in `#319 `_. + * Added support for float values in ``--cov-fail-under``. + Contributed by Martín Gaitán in `#311 `_. + * Various documentation fixes. Contributed by + Juanjo Bazán, + Andrew Murray and + Albert Tugushev in + `#298 `_, + `#299 `_ and + `#307 `_. + * Various testing improvements. Contributed by + Ned Batchelder, + Daniel Hahler, + Ionel Cristian Mărieș and + Hugo van Kemenade in + `#313 `_, + `#314 `_, + `#315 `_, + `#316 `_, + `#325 `_, + `#326 `_, + `#334 `_ and + `#335 `_. + * Added the ``--cov-context`` CLI options that enables coverage contexts. Only works with coverage 5.0+. + Contributed by Ned Batchelder in `#345 `_. + + 2.7.1 (2019-05-03) + ------------------ + + * Fixed source distribution manifest so that garbage ain't included in the tarball. + + 2.7.0 (2019-05-03) + ------------------ + + * Fixed ``AttributeError: 'NoneType' object has no attribute 'configure_node'`` error when ``--no-cov`` is used. + Contributed by Alexander Shadchin in `#263 `_. + * Various testing and CI improvements. Contributed by Daniel Hahler in + `#255 `_, + `#266 `_, + `#272 `_, + `#271 `_ and + `#269 `_. + * Improved documentation regarding subprocess and multiprocessing. + Contributed in `#265 `_. + * Improved ``pytest_cov.embed.cleanup_on_sigterm`` to be reentrant (signal deliveries while signal handling is + running won't break stuff). + * Added ``pytest_cov.embed.cleanup_on_signal`` for customized cleanup. + * Improved cleanup code and fixed various issues with leftover data files. All contributed in + `#265 `_ or + `#262 `_. + * Improved examples. Now there are two examples for the common project layouts, complete with working coverage + configuration. The examples have CI testing. Contributed in + `#267 `_. + * Improved help text for CLI options. + + 2.6.1 (2019-01-07) + ------------------ + + * Added support for Pytest 4.1. Contributed by Daniel Hahler and Семён Марьясин in + `#253 `_ and + `#230 `_. + * Various test and docs fixes. Contributed by Daniel Hahler in + `#224 `_ and + `#223 `_. + * Fixed the "Module already imported" issue (`#211 `_). + Contributed by Daniel Hahler in `#228 `_. + + 2.6.0 (2018-09-03) + ------------------ + + * Dropped support for Python 3 < 3.4, Pytest < 3.5 and Coverage < 4.4. + * Fixed some documentation formatting. Contributed by Jean Jordaan and Julian. + * Added an example with ``addopts`` in documentation. Contributed by Samuel Giffard in + `#195 `_. + * Fixed ``TypeError: 'NoneType' object is not iterable`` in certain xdist configurations. Contributed by Jeremy Bowman in + `#213 `_. + * Added a ``no_cover`` marker and fixture. Fixes + `#78 `_. + * Fixed broken ``no_cover`` check when running doctests. Contributed by Terence Honles in + `#200 `_. + * Fixed various issues with path normalization in reports (when combining coverage data from parallel mode). Fixes + `#130 `_. + Contributed by Ryan Hiebert & Ionel Cristian Mărieș in + `#178 `_. + * Report generation failures don't raise exceptions anymore. A warning will be logged instead. Fixes + `#161 `_. + * Fixed multiprocessing issue on Windows (empty env vars are not passed). Fixes + `#165 `_. + + 2.5.1 (2017-05-11) + ------------------ + + * Fixed xdist breakage (regression in ``2.5.0``). + Fixes `#157 `_. + * Allow setting custom ``data_file`` name in ``.coveragerc``. + Fixes `#145 `_. + Contributed by Jannis Leidel & Ionel Cristian Mărieș in + `#156 `_. + + 2.5.0 (2017-05-09) + ------------------ + + * Always show a summary when ``--cov-fail-under`` is used. Contributed by Francis Niu in `PR#141 + `_. + * Added ``--cov-branch`` option. Fixes `#85 `_. + * Improve exception handling in subprocess setup. Fixes `#144 `_. + * Fixed handling when ``--cov`` is used multiple times. Fixes `#151 `_. + + 2.4.0 (2016-10-10) + ------------------ + + * Added a "disarm" option: ``--no-cov``. It will disable coverage measurements. Contributed by Zoltan Kozma in + `PR#135 `_. + + **WARNING: Do not put this in your configuration files, it's meant to be an one-off for situations where you want to + disable coverage from command line.** + * Fixed broken exception handling on ``.pth`` file. See `#136 `_. + + 2.3.1 (2016-08-07) + ------------------ + + * Fixed regression causing spurious errors when xdist was used. See `#124 + `_. + * Fixed DeprecationWarning about incorrect `addoption` use. Contributed by Florian Bruhin in `PR#127 + `_. + * Fixed deprecated use of funcarg fixture API. Contributed by Daniel Hahler in `PR#125 + `_. + + 2.3.0 (2016-07-05) + ------------------ + + * Add support for specifying output location for html, xml, and annotate report. + Contributed by Patrick Lannigan in `PR#113 `_. + * Fix bug hiding test failure when cov-fail-under failed. + * For coverage >= 4.0, match the default behaviour of `coverage report` and + error if coverage fails to find the source instead of just printing a warning. + Contributed by David Szotten in `PR#116 `_. + * Fixed bug occurred when bare ``--cov`` parameter was used with xdist. + Contributed by Michael Elovskikh in `PR#120 `_. + * Add support for ``skip_covered`` and added ``--cov-report=term-skip-covered`` command + line options. Contributed by Saurabh Kumar in `PR#115 `_. + + 2.2.1 (2016-01-30) + ------------------ + + * Fixed incorrect merging of coverage data when xdist was used and coverage was ``>= 4.0``. + + 2.2.0 (2015-10-04) + ------------------ + + * Added support for changing working directory in tests. Previously changing working + directory would disable coverage measurements in suprocesses. + * Fixed broken handling for ``--cov-report=annotate``. + + 2.1.0 (2015-08-23) + ------------------ + + * Added support for `coverage 4.0b2`. + * Added the ``--cov-append`` command line options. Contributed by Christian Ledermann + in `PR#80 `_. + + 2.0.0 (2015-07-28) + ------------------ + + * Added ``--cov-fail-under``, akin to the new ``fail_under`` option in `coverage-4.0` + (automatically activated if there's a ``[report] fail_under = ...`` in ``.coveragerc``). + * Changed ``--cov-report=term`` to automatically upgrade to ``--cov-report=term-missing`` + if there's ``[run] show_missing = True`` in ``.coveragerc``. + * Changed ``--cov`` so it can be used with no path argument (in which case the source + settings from ``.coveragerc`` will be used instead). + * Fixed `.pth` installation to work in all cases (install, easy_install, wheels, develop etc). + * Fixed `.pth` uninstallation to work for wheel installs. + * Support for coverage 4.0. + * Data file suffixing changed to use coverage's ``data_suffix=True`` option (instead of the + custom suffixing). + * Avoid warning about missing coverage data (just like ``coverage.control.process_startup``). + * Fixed a race condition when running with xdist (all the workers tried to combine the files). + It's possible that this issue is not present in `pytest-cov 1.8.X`. + + 1.8.2 (2014-11-06) + ------------------ + + * N/A + +Keywords: cover,coverage,pytest,py.test,distributed,parallel +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: Pytest +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Operating System :: Unix +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Testing +Classifier: Topic :: Utilities +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +Provides-Extra: testing diff --git a/src/pytest_cov.egg-info/SOURCES.txt b/src/pytest_cov.egg-info/SOURCES.txt new file mode 100644 index 0000000..e06f275 --- /dev/null +++ b/src/pytest_cov.egg-info/SOURCES.txt @@ -0,0 +1,72 @@ +.appveyor.yml +.bumpversion.cfg +.cookiecutterrc +.editorconfig +.gitignore +.pre-commit-config.yaml +.readthedocs.yml +.travis.yml +AUTHORS.rst +CHANGELOG.rst +CONTRIBUTING.rst +LICENSE +MANIFEST.in +README.rst +setup.cfg +setup.py +tox.ini +.github/ISSUE_TEMPLATE/bug_report.md +.github/ISSUE_TEMPLATE/feature_request.md +.github/ISSUE_TEMPLATE/support_request.md +ci/appveyor-with-compiler.cmd +ci/bootstrap.py +ci/requirements.txt +ci/templates/.appveyor.yml +ci/templates/.travis.yml +docs/authors.rst +docs/changelog.rst +docs/conf.py +docs/config.rst +docs/contexts.rst +docs/contributing.rst +docs/debuggers.rst +docs/index.rst +docs/markers-fixtures.rst +docs/plugins.rst +docs/readme.rst +docs/releasing.rst +docs/reporting.rst +docs/requirements.txt +docs/spelling_wordlist.txt +docs/subprocess-support.rst +docs/tox.rst +docs/xdist.rst +examples/README.rst +examples/adhoc-layout/.coveragerc +examples/adhoc-layout/setup.py +examples/adhoc-layout/tox.ini +examples/adhoc-layout/example/__init__.py +examples/adhoc-layout/tests/test_example.py +examples/src-layout/.coveragerc +examples/src-layout/setup.py +examples/src-layout/tox.ini +examples/src-layout/src/example/__init__.py +examples/src-layout/tests/test_example.py +src/pytest-cov.embed +src/pytest-cov.pth +src/pytest_cov/__init__.py +src/pytest_cov/compat.py +src/pytest_cov/embed.py +src/pytest_cov/engine.py +src/pytest_cov/plugin.py +src/pytest_cov.egg-info/PKG-INFO +src/pytest_cov.egg-info/SOURCES.txt +src/pytest_cov.egg-info/dependency_links.txt +src/pytest_cov.egg-info/entry_points.txt +src/pytest_cov.egg-info/not-zip-safe +src/pytest_cov.egg-info/requires.txt +src/pytest_cov.egg-info/top_level.txt +tests/conftest.py +tests/contextful.py +tests/helper.py +tests/test_pytest_cov.py \ No newline at end of file diff --git a/src/pytest_cov.egg-info/dependency_links.txt b/src/pytest_cov.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pytest_cov.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/pytest_cov.egg-info/entry_points.txt b/src/pytest_cov.egg-info/entry_points.txt new file mode 100644 index 0000000..d351f96 --- /dev/null +++ b/src/pytest_cov.egg-info/entry_points.txt @@ -0,0 +1,6 @@ +[console_scripts] + + +[pytest11] +pytest_cov = pytest_cov.plugin + diff --git a/src/pytest_cov.egg-info/not-zip-safe b/src/pytest_cov.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pytest_cov.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/src/pytest_cov.egg-info/requires.txt b/src/pytest_cov.egg-info/requires.txt new file mode 100644 index 0000000..025f7c4 --- /dev/null +++ b/src/pytest_cov.egg-info/requires.txt @@ -0,0 +1,10 @@ +pytest>=4.6 +coverage>=4.4 + +[testing] +fields +hunter +process-tests==2.0.2 +six +pytest-xdist +virtualenv diff --git a/src/pytest_cov.egg-info/top_level.txt b/src/pytest_cov.egg-info/top_level.txt new file mode 100644 index 0000000..a2fe281 --- /dev/null +++ b/src/pytest_cov.egg-info/top_level.txt @@ -0,0 +1 @@ +pytest_cov diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py new file mode 100644 index 0000000..61668ca --- /dev/null +++ b/src/pytest_cov/__init__.py @@ -0,0 +1,2 @@ +"""pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE.""" +__version__ = "__version__ = '2.10.1'" diff --git a/src/pytest_cov/compat.py b/src/pytest_cov/compat.py new file mode 100644 index 0000000..5b4a0bf --- /dev/null +++ b/src/pytest_cov/compat.py @@ -0,0 +1,31 @@ +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +import pytest + +StringIO # pyflakes, this is for re-export + + +if hasattr(pytest, 'hookimpl'): + hookwrapper = pytest.hookimpl(hookwrapper=True) +else: + hookwrapper = pytest.mark.hookwrapper + + +class SessionWrapper(object): + def __init__(self, session): + self._session = session + if hasattr(session, 'testsfailed'): + self._attr = 'testsfailed' + else: + self._attr = '_testsfailed' + + @property + def testsfailed(self): + return getattr(self._session, self._attr) + + @testsfailed.setter + def testsfailed(self, value): + setattr(self._session, self._attr, value) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py new file mode 100644 index 0000000..7e081fe --- /dev/null +++ b/src/pytest_cov/embed.py @@ -0,0 +1,137 @@ +"""Activate coverage at python startup if appropriate. + +The python site initialisation will ensure that anything we import +will be removed and not visible at the end of python startup. However +we minimise all work by putting these init actions in this separate +module and only importing what is needed when needed. + +For normal python startup when coverage should not be activated the pth +file checks a single env var and does not import or call the init fn +here. + +For python startup when an ancestor process has set the env indicating +that code coverage is being collected we activate coverage based on +info passed via env vars. +""" +import atexit +import os +import signal + +_active_cov = None + + +def multiprocessing_start(_): + global _active_cov + cov = init() + if cov: + _active_cov = cov + multiprocessing.util.Finalize(None, cleanup, exitpriority=1000) + + +try: + import multiprocessing.util +except ImportError: + pass +else: + multiprocessing.util.register_after_fork(multiprocessing_start, multiprocessing_start) + + +def init(): + # Only continue if ancestor process has set everything needed in + # the env. + global _active_cov + + cov_source = os.environ.get('COV_CORE_SOURCE') + cov_config = os.environ.get('COV_CORE_CONFIG') + cov_datafile = os.environ.get('COV_CORE_DATAFILE') + cov_branch = True if os.environ.get('COV_CORE_BRANCH') == 'enabled' else None + + if cov_datafile: + if _active_cov: + cleanup() + # Import what we need to activate coverage. + import coverage + + # Determine all source roots. + if cov_source in os.pathsep: + cov_source = None + else: + cov_source = cov_source.split(os.pathsep) + if cov_config == os.pathsep: + cov_config = True + + # Activate coverage for this process. + cov = _active_cov = coverage.Coverage( + source=cov_source, + branch=cov_branch, + data_suffix=True, + config_file=cov_config, + auto_data=True, + data_file=cov_datafile + ) + cov.load() + cov.start() + cov._warn_no_data = False + cov._warn_unimported_source = False + return cov + + +def _cleanup(cov): + if cov is not None: + cov.stop() + cov.save() + cov._auto_save = False # prevent autosaving from cov._atexit in case the interpreter lacks atexit.unregister + try: + atexit.unregister(cov._atexit) + except Exception: + pass + + +def cleanup(): + global _active_cov + global _cleanup_in_progress + global _pending_signal + + _cleanup_in_progress = True + _cleanup(_active_cov) + _active_cov = None + _cleanup_in_progress = False + if _pending_signal: + pending_singal = _pending_signal + _pending_signal = None + _signal_cleanup_handler(*pending_singal) + + +multiprocessing_finish = cleanup # in case someone dared to use this internal + +_previous_handlers = {} +_pending_signal = None +_cleanup_in_progress = False + + +def _signal_cleanup_handler(signum, frame): + global _pending_signal + if _cleanup_in_progress: + _pending_signal = signum, frame + return + cleanup() + _previous_handler = _previous_handlers.get(signum) + if _previous_handler == signal.SIG_IGN: + return + elif _previous_handler and _previous_handler is not _signal_cleanup_handler: + _previous_handler(signum, frame) + elif signum == signal.SIGTERM: + os._exit(128 + signum) + elif signum == signal.SIGINT: + raise KeyboardInterrupt() + + +def cleanup_on_signal(signum): + previous = signal.getsignal(signum) + if previous is not _signal_cleanup_handler: + _previous_handlers[signum] = previous + signal.signal(signum, _signal_cleanup_handler) + + +def cleanup_on_sigterm(): + cleanup_on_signal(signal.SIGTERM) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py new file mode 100644 index 0000000..eab0656 --- /dev/null +++ b/src/pytest_cov/engine.py @@ -0,0 +1,401 @@ +"""Coverage controllers for use by pytest-cov and nose-cov.""" +import contextlib +import copy +import functools +import os +import random +import socket +import sys + +import coverage +from coverage.data import CoverageData + +from .compat import StringIO +from .embed import cleanup + + +class _NullFile(object): + @staticmethod + def write(v): + pass + + +@contextlib.contextmanager +def _backup(obj, attr): + backup = getattr(obj, attr) + try: + setattr(obj, attr, copy.copy(backup)) + yield + finally: + setattr(obj, attr, backup) + + +def _ensure_topdir(meth): + @functools.wraps(meth) + def ensure_topdir_wrapper(self, *args, **kwargs): + try: + original_cwd = os.getcwd() + except OSError: + # Looks like it's gone, this is non-ideal because a side-effect will + # be introduced in the tests here but we can't do anything about it. + original_cwd = None + os.chdir(self.topdir) + try: + return meth(self, *args, **kwargs) + finally: + if original_cwd is not None: + os.chdir(original_cwd) + + return ensure_topdir_wrapper + + +class CovController(object): + """Base class for different plugin implementations.""" + + def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, config=None, nodeid=None): + """Get some common config used by multiple derived classes.""" + self.cov_source = cov_source + self.cov_report = cov_report + self.cov_config = cov_config + self.cov_append = cov_append + self.cov_branch = cov_branch + self.config = config + self.nodeid = nodeid + + self.cov = None + self.combining_cov = None + self.data_file = None + self.node_descs = set() + self.failed_workers = [] + self.topdir = os.getcwd() + self.is_collocated = None + + @contextlib.contextmanager + def ensure_topdir(self): + original_cwd = os.getcwd() + os.chdir(self.topdir) + yield + os.chdir(original_cwd) + + @_ensure_topdir + def pause(self): + self.cov.stop() + self.unset_env() + + @_ensure_topdir + def resume(self): + self.cov.start() + self.set_env() + + @_ensure_topdir + def set_env(self): + """Put info about coverage into the env so that subprocesses can activate coverage.""" + if self.cov_source is None: + 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 + else: + os.environ['COV_CORE_CONFIG'] = os.pathsep + os.environ['COV_CORE_DATAFILE'] = os.path.abspath(self.cov.config.data_file) + if self.cov_branch: + os.environ['COV_CORE_BRANCH'] = 'enabled' + + @staticmethod + def unset_env(): + """Remove coverage info from env.""" + os.environ.pop('COV_CORE_SOURCE', None) + os.environ.pop('COV_CORE_CONFIG', None) + os.environ.pop('COV_CORE_DATAFILE', None) + os.environ.pop('COV_CORE_BRANCH', None) + + @staticmethod + def get_node_desc(platform, version_info): + """Return a description of this node.""" + + return 'platform %s, python %s' % (platform, '%s.%s.%s-%s-%s' % version_info[:5]) + + @staticmethod + def sep(stream, s, txt): + if hasattr(stream, 'sep'): + stream.sep(s, txt) + else: + sep_total = max((70 - 2 - len(txt)), 2) + sep_len = sep_total // 2 + sep_extra = sep_total % 2 + out = '%s %s %s\n' % (s * sep_len, txt, s * (sep_len + sep_extra)) + stream.write(out) + + @_ensure_topdir + def summary(self, stream): + """Produce coverage reports.""" + total = None + + if not self.cov_report: + with _backup(self.cov, "config"): + return self.cov.report(show_missing=True, ignore_errors=True, file=_NullFile) + + # Output coverage section header. + if len(self.node_descs) == 1: + self.sep(stream, '-', 'coverage: %s' % ''.join(self.node_descs)) + else: + self.sep(stream, '-', 'coverage') + for node_desc in sorted(self.node_descs): + self.sep(stream, ' ', '%s' % node_desc) + + # Report on any failed workers. + if self.failed_workers: + self.sep(stream, '-', 'coverage: failed workers') + stream.write('The following workers failed to return coverage data, ' + 'ensure that pytest-cov is installed on these workers.\n') + for node in self.failed_workers: + stream.write('%s\n' % node.gateway.id) + + # Produce terminal report if wanted. + if any(x in self.cov_report for x in ['term', 'term-missing']): + options = { + 'show_missing': ('term-missing' in self.cov_report) or None, + 'ignore_errors': True, + 'file': stream, + } + 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"): + 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"): + 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"): + total = self.cov.report(ignore_errors=True, file=_NullFile) + if annotate_dir: + stream.write('Coverage annotated source written to dir %s\n' % annotate_dir) + else: + stream.write('Coverage annotated source written next to source\n') + + # Produce html report if wanted. + if 'html' in self.cov_report: + output = self.cov_report['html'] + with _backup(self.cov, "config"): + total = self.cov.html_report(ignore_errors=True, directory=output) + stream.write('Coverage HTML written to dir %s\n' % (self.cov.config.html_dir if output is None else output)) + + # Produce xml report if wanted. + if 'xml' in self.cov_report: + output = self.cov_report['xml'] + with _backup(self.cov, "config"): + total = self.cov.xml_report(ignore_errors=True, outfile=output) + stream.write('Coverage XML written to file %s\n' % (self.cov.config.xml_output if output is None else output)) + + return total + + +class Central(CovController): + """Implementation for centralised operation.""" + + @_ensure_topdir + 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) + + # Erase or load any previous coverage data and start coverage. + if not self.cov_append: + self.cov.erase() + self.cov.start() + self.set_env() + + @_ensure_topdir + def finish(self): + """Stop coverage, save data to file and set the list of coverage objects to report on.""" + + self.unset_env() + self.cov.stop() + self.cov.save() + + self.cov = self.combining_cov + self.cov.load() + self.cov.combine() + self.cov.save() + + node_desc = self.get_node_desc(sys.platform, sys.version_info) + self.node_descs.add(node_desc) + + +class DistMaster(CovController): + """Implementation for distributed master.""" + + @_ensure_topdir + def start(self): + cleanup() + + # Ensure coverage rc file rsynced if appropriate. + if self.cov_config and os.path.exists(self.cov_config): + self.config.option.rsyncdir.append(self.cov_config) + + self.cov = coverage.Coverage(source=self.cov_source, + branch=self.cov_branch, + data_suffix=True, + config_file=self.cov_config) + self.cov._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) + if not self.cov_append: + self.cov.erase() + self.cov.start() + self.cov.config.paths['source'] = [self.topdir] + + def configure_node(self, node): + """Workers need to know if they are collocated and what files have moved.""" + + node.workerinput.update({ + 'cov_master_host': socket.gethostname(), + 'cov_master_topdir': self.topdir, + 'cov_master_rsync_roots': [str(root) for root in node.nodemanager.roots], + }) + + def testnodedown(self, node, error): + """Collect data file name from worker.""" + + # If worker doesn't return any data then it is likely that this + # plugin didn't get activated on the worker side. + output = getattr(node, 'workeroutput', {}) + if 'cov_worker_node_id' not in output: + self.failed_workers.append(node) + return + + # If worker is not collocated then we must save the data file + # that it returns to us. + if 'cov_worker_data' in output: + data_suffix = '%s.%s.%06d.%s' % ( + socket.gethostname(), os.getpid(), + random.randint(0, 999999), + 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.start() + if coverage.version_info < (5, 0): + data = CoverageData() + data.read_fileobj(StringIO(output['cov_worker_data'])) + cov.data.update(data) + else: + data = CoverageData(no_disk=True) + data.loads(output['cov_worker_data']) + cov.get_data().update(data) + cov.stop() + cov.save() + path = output['cov_worker_path'] + self.cov.config.paths['source'].append(path) + + # Record the worker types that contribute to the data file. + rinfo = node.gateway._rinfo() + node_desc = self.get_node_desc(rinfo.platform, rinfo.version_info) + self.node_descs.add(node_desc) + + @_ensure_topdir + def finish(self): + """Combines coverage data and sets the list of coverage objects to report on.""" + + # Combine all the suffix files into the data file. + self.cov.stop() + self.cov.save() + self.cov = self.combining_cov + self.cov.load() + self.cov.combine() + self.cov.save() + + +class DistWorker(CovController): + """Implementation for distributed workers.""" + + @_ensure_topdir + def start(self): + + 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']) + + # If we are not collocated then rewrite master paths to worker paths. + if not self.is_collocated: + master_topdir = self.config.workerinput['cov_master_topdir'] + worker_topdir = self.topdir + if self.cov_source is not None: + self.cov_source = [source.replace(master_topdir, worker_topdir) + for source in self.cov_source] + self.cov_config = self.cov_config.replace(master_topdir, worker_topdir) + + # Erase any previous data and start coverage. + self.cov = coverage.Coverage(source=self.cov_source, + branch=self.cov_branch, + data_suffix=True, + config_file=self.cov_config) + self.cov.start() + self.set_env() + + @_ensure_topdir + def finish(self): + """Stop coverage and send relevant info back to the master.""" + self.unset_env() + self.cov.stop() + + if self.is_collocated: + # We don't combine data if we're collocated - we can get + # race conditions in the .combine() call (it's not atomic) + # The data is going to be combined in the master. + self.cov.save() + + # If we are collocated then just inform the master of our + # data file to indicate that we have finished. + self.config.workeroutput['cov_worker_node_id'] = self.nodeid + else: + self.cov.combine() + self.cov.save() + # If we are not collocated then add the current path + # and coverage data to the output so we can combine + # it on the master node. + + # Send all the data to the master over the channel. + if coverage.version_info < (5, 0): + buff = StringIO() + self.cov.data.write_fileobj(buff) + data = buff.getvalue() + 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, + }) + + 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 new file mode 100644 index 0000000..2d22b30 --- /dev/null +++ b/src/pytest_cov/plugin.py @@ -0,0 +1,380 @@ +"""Coverage plugin for pytest.""" +import argparse +import os +import warnings + +import coverage +import pytest + +from . import compat +from . import embed + + +class CoverageError(Exception): + """Indicates that our coverage is too low""" + + +def validate_report(arg): + file_choices = ['annotate', 'html', 'xml'] + term_choices = ['term', 'term-missing'] + term_modifier_choices = ['skip-covered'] + all_choices = term_choices + file_choices + values = arg.split(":", 1) + report_type = values[0] + if report_type not in all_choices + ['']: + msg = 'invalid choice: "{}" (choose from "{}")'.format(arg, all_choices) + raise argparse.ArgumentTypeError(msg) + + if len(values) == 1: + return report_type, None + + report_modifier = values[1] + if report_type in term_choices and report_modifier in term_modifier_choices: + return report_type, report_modifier + + if report_type not in file_choices: + msg = 'output specifier not supported for: "{}" (choose from "{}")'.format(arg, + file_choices) + raise argparse.ArgumentTypeError(msg) + + return values + + +def validate_fail_under(num_str): + try: + return int(num_str) + except ValueError: + return float(num_str) + + +def validate_context(arg): + if coverage.version_info <= (5, 0): + raise argparse.ArgumentTypeError('Contexts are only supported with coverage.py >= 5.x') + if arg != "test": + raise argparse.ArgumentTypeError('--cov-context=test is the only supported value') + return arg + + +class StoreReport(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + report_type, file = values + namespace.cov_report[report_type] = file + + +def pytest_addoption(parser): + """Add options to control coverage.""" + + group = parser.getgroup( + 'cov', 'coverage reporting with distributed testing support') + group.addoption('--cov', action='append', default=[], metavar='SOURCE', + nargs='?', const=True, dest='cov_source', + help='Path or package name to measure during execution (multi-allowed). ' + 'Use --cov= to not do any source filtering and record everything.') + group.addoption('--cov-report', action=StoreReport, default={}, + metavar='TYPE', type=validate_report, + help='Type of report to generate: term, term-missing, ' + 'annotate, html, xml (multi-allowed). ' + 'term, term-missing may be followed by ":skip-covered". ' + 'annotate, html and xml may be followed by ":DEST" ' + 'where DEST specifies the output location. ' + 'Use --cov-report= to not generate any output.') + group.addoption('--cov-config', action='store', default='.coveragerc', + metavar='PATH', + help='Config file for coverage. Default: .coveragerc') + group.addoption('--no-cov-on-fail', action='store_true', default=False, + help='Do not report coverage if test run fails. ' + 'Default: False') + group.addoption('--no-cov', action='store_true', default=False, + help='Disable coverage report completely (useful for debuggers). ' + 'Default: False') + group.addoption('--cov-fail-under', action='store', metavar='MIN', + type=validate_fail_under, + help='Fail if the total coverage is less than MIN.') + group.addoption('--cov-append', action='store_true', default=False, + help='Do not delete coverage but append to current. ' + 'Default: False') + group.addoption('--cov-branch', action='store_true', default=None, + help='Enable branch coverage.') + group.addoption('--cov-context', action='store', metavar='CONTEXT', + type=validate_context, + help='Dynamic contexts to use. "test" for now.') + + +def _prepare_cov_source(cov_source): + """ + Prepare cov_source so that: + + --cov --cov=foobar is equivalent to --cov (cov_source=None) + --cov=foo --cov=bar is equivalent to cov_source=['foo', 'bar'] + """ + return None if True in cov_source else [path for path in cov_source if path is not True] + + +@pytest.mark.tryfirst +def pytest_load_initial_conftests(early_config, parser, args): + options = early_config.known_args_namespace + no_cov = options.no_cov_should_warn = False + for arg in args: + arg = str(arg) + if arg == '--no-cov': + no_cov = True + elif arg.startswith('--cov') and no_cov: + options.no_cov_should_warn = True + break + + if early_config.known_args_namespace.cov_source: + plugin = CovPlugin(options, early_config.pluginmanager) + early_config.pluginmanager.register(plugin, '_cov') + + +class CovPlugin(object): + """Use coverage package to produce code coverage reports. + + Delegates all work to a particular implementation based on whether + this test process is centralised, a distributed master or a + distributed worker. + """ + + def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False): + """Creates a coverage pytest plugin. + + We read the rc file that coverage uses to get the data file + name. This is needed since we give coverage through it's API + the data file name. + """ + + # Our implementation is unknown at this time. + self.pid = None + self.cov_controller = None + self.cov_report = compat.StringIO() + self.cov_total = None + self.failed = False + self._started = False + self._start_path = None + self._disabled = False + self.options = options + + is_dist = (getattr(options, 'numprocesses', False) or + getattr(options, 'distload', False) or + getattr(options, 'dist', 'no') != 'no') + if getattr(options, 'no_cov', False): + self._disabled = True + return + + if not self.options.cov_report: + self.options.cov_report = ['term'] + elif len(self.options.cov_report) == 1 and '' in self.options.cov_report: + self.options.cov_report = {} + self.options.cov_source = _prepare_cov_source(self.options.cov_source) + + # import engine lazily here to avoid importing + # it for unit tests that don't need it + from . import engine + + if is_dist and start: + self.start(engine.DistMaster) + elif start: + self.start(engine.Central) + + # worker is started in pytest hook + + def start(self, controller_cls, config=None, nodeid=None): + + if config is None: + # fake config option for engine + class Config(object): + option = self.options + + config = Config() + + self.cov_controller = controller_cls( + self.options.cov_source, + self.options.cov_report, + self.options.cov_config, + self.options.cov_append, + self.options.cov_branch, + config, + nodeid + ) + self.cov_controller.start() + self._started = True + self._start_path = os.getcwd() + 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 + + def _is_worker(self, session): + return getattr(session.config, 'workerinput', None) is not None + + def pytest_sessionstart(self, session): + """At session start determine our implementation and delegate to it.""" + + if self.options.no_cov: + # Coverage can be disabled because it does not cooperate with debuggers well. + self._disabled = True + return + + # import engine lazily here to avoid importing + # it for unit tests that don't need it + from . import engine + + self.pid = os.getpid() + if self._is_worker(session): + nodeid = ( + session.config.workerinput.get('workerid', getattr(session, 'nodeid')) + ) + self.start(engine.DistWorker, session.config, nodeid) + elif not self._started: + self.start(engine.Central) + + if self.options.cov_context == 'test': + session.config.pluginmanager.register(TestContextPlugin(self.cov_controller.cov), '_cov_contexts') + + def pytest_configure_node(self, node): + """Delegate to our implementation. + + Mark this hook as optional in case xdist is not installed. + """ + if not self._disabled: + self.cov_controller.configure_node(node) + pytest_configure_node.optionalhook = True + + def pytest_testnodedown(self, node, error): + """Delegate to our implementation. + + Mark this hook as optional in case xdist is not installed. + """ + if not self._disabled: + self.cov_controller.testnodedown(node, error) + pytest_testnodedown.optionalhook = True + + def _should_report(self): + return not (self.failed and self.options.no_cov_on_fail) + + def _failed_cov_total(self): + cov_fail_under = self.options.cov_fail_under + return cov_fail_under is not None and self.cov_total < cov_fail_under + + # we need to wrap pytest_runtestloop. by the time pytest_sessionfinish + # runs, it's too late to set testsfailed + @compat.hookwrapper + def pytest_runtestloop(self, session): + yield + + if self._disabled: + return + + compat_session = compat.SessionWrapper(session) + + self.failed = bool(compat_session.testsfailed) + if self.cov_controller is not None: + self.cov_controller.finish() + + if not self._is_worker(session) and self._should_report(): + + # import coverage lazily here to avoid importing + # it for unit tests that don't need it + from coverage.misc import CoverageException + + try: + self.cov_total = self.cov_controller.summary(self.cov_report) + except CoverageException as exc: + message = 'Failed to generate report: %s\n' % exc + session.config.pluginmanager.getplugin("terminalreporter").write( + 'WARNING: %s\n' % message, red=True, bold=True) + warnings.warn(pytest.PytestWarning(message)) + self.cov_total = 0 + assert self.cov_total is not None, 'Test coverage should never be `None`' + if self._failed_cov_total(): + # make sure we get the EXIT_TESTSFAILED exit code + compat_session.testsfailed += 1 + + def pytest_terminal_summary(self, terminalreporter): + if self._disabled: + if self.options.no_cov_should_warn: + message = 'Coverage disabled via --no-cov switch!' + terminalreporter.write('WARNING: %s\n' % message, red=True, bold=True) + warnings.warn(pytest.PytestWarning(message)) + return + if self.cov_controller is None: + return + + if self.cov_total is None: + # we shouldn't report, or report generation failed (error raised above) + return + + terminalreporter.write('\n' + self.cov_report.getvalue() + '\n') + + if self.options.cov_fail_under is not None and self.options.cov_fail_under > 0: + failed = self.cov_total < self.options.cov_fail_under + markup = {'red': True, 'bold': True} if failed else {'green': True} + message = ( + '{fail}Required test coverage of {required}% {reached}. ' + 'Total coverage: {actual:.2f}%\n' + .format( + required=self.options.cov_fail_under, + actual=self.cov_total, + fail="FAIL " if failed else "", + reached="not reached" if failed else "reached" + ) + ) + terminalreporter.write(message, **markup) + + def pytest_runtest_setup(self, item): + if os.getpid() != self.pid: + # test is run in another process than session, run + # coverage manually + embed.init() + + def pytest_runtest_teardown(self, item): + embed.cleanup() + + @compat.hookwrapper + def pytest_runtest_call(self, item): + if (item.get_closest_marker('no_cover') + or 'no_cover' in getattr(item, 'fixturenames', ())): + self.cov_controller.pause() + yield + self.cov_controller.resume() + else: + yield + + +class TestContextPlugin(object): + def __init__(self, cov): + self.cov = cov + + def pytest_runtest_setup(self, item): + self.switch_context(item, 'setup') + + def pytest_runtest_teardown(self, item): + self.switch_context(item, 'teardown') + + def pytest_runtest_call(self, item): + self.switch_context(item, 'run') + + def switch_context(self, item, when): + context = "{item.nodeid}|{when}".format(item=item, when=when) + self.cov.switch_context(context) + + +@pytest.fixture +def no_cover(): + """A pytest fixture to disable coverage.""" + pass + + +@pytest.fixture +def cov(request): + """A pytest fixture to provide access to the underlying coverage object.""" + + # Check with hasplugin to avoid getplugin exception in older pytest. + if request.config.pluginmanager.hasplugin('_cov'): + plugin = request.config.pluginmanager.getplugin('_cov') + if plugin.cov_controller: + return plugin.cov_controller.cov + return None + + +def pytest_configure(config): + config.addinivalue_line("markers", "no_cover: disable coverage for this test.") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2fb0f9b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,2 @@ +def pytest_configure(config): + config.option.runpytest = 'subprocess' diff --git a/tests/contextful.py b/tests/contextful.py new file mode 100644 index 0000000..3527e49 --- /dev/null +++ b/tests/contextful.py @@ -0,0 +1,105 @@ +# A test file for test_pytest_cov.py:test_contexts + +import unittest + +import pytest + + +def test_01(): + assert 1 == 1 # r1 + + +def test_02(): + assert 2 == 2 # r2 + + +class OldStyleTests(unittest.TestCase): + items = [] + + @classmethod + def setUpClass(cls): + cls.items.append("hello") # s3 + + @classmethod + def tearDownClass(cls): + cls.items.pop() # t4 + + def setUp(self): + self.number = 1 # r3 r4 + + def tearDown(self): + self.number = None # r3 r4 + + def test_03(self): + assert self.number == 1 # r3 + assert self.items[0] == "hello" # r3 + + def test_04(self): + assert self.number == 1 # r4 + assert self.items[0] == "hello" # r4 + + +@pytest.fixture +def some_data(): + return [1, 2, 3] # s5 s6 + + +def test_05(some_data): + assert len(some_data) == 3 # r5 + + +@pytest.fixture +def more_data(some_data): + return [2*x for x in some_data] # s6 + + +def test_06(some_data, more_data): + assert len(some_data) == len(more_data) # r6 + + +@pytest.fixture(scope='session') +def expensive_data(): + return list(range(10)) # s7 + + +def test_07(expensive_data): + assert len(expensive_data) == 10 # r7 + + +def test_08(expensive_data): + assert len(expensive_data) == 10 # r8 + + +@pytest.fixture(params=[1, 2, 3]) +def parametrized_number(request): + return request.param # s9-1 s9-2 s9-3 + + +def test_09(parametrized_number): + assert parametrized_number > 0 # r9-1 r9-2 r9-3 + + +def test_10(): + assert 1 == 1 # r10 + + +@pytest.mark.parametrize("x, ans", [ + (1, 101), + (2, 202), +]) +def test_11(x, ans): + assert 100 * x + x == ans # r11-1 r11-2 + + +@pytest.mark.parametrize("x, ans", [ + (1, 101), + (2, 202), +], ids=['one', 'two']) +def test_12(x, ans): + assert 100 * x + x == ans # r12-1 r12-2 + + +@pytest.mark.parametrize("x", [1, 2]) +@pytest.mark.parametrize("y", [3, 4]) +def test_13(x, y): + assert x + y > 0 # r13-1 r13-2 r13-3 r13-4 diff --git a/tests/helper.py b/tests/helper.py new file mode 100644 index 0000000..3e7da4b --- /dev/null +++ b/tests/helper.py @@ -0,0 +1,3 @@ +def do_stuff(): + a = 1 + return a diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py new file mode 100644 index 0000000..f0fd6ca --- /dev/null +++ b/tests/test_pytest_cov.py @@ -0,0 +1,2115 @@ +import collections +import glob +import os +import platform +import re +import subprocess +import sys +from itertools import chain + +import coverage +import py +import pytest +import virtualenv +import xdist +from fields import Namespace +from process_tests import TestProcess as _TestProcess +from process_tests import dump_on_error +from process_tests import wait_for_strings +from six import exec_ + +import pytest_cov.plugin + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +coverage, platform # required for skipif mark on test_cov_min_from_coveragerc + +max_worker_restart_0 = "--max-worker-restart=0" + +SCRIPT = ''' +import sys, helper + +def pytest_generate_tests(metafunc): + for i in [10]: + metafunc.parametrize('p', range(i)) + +def test_foo(p): + x = True + helper.do_stuff() # get some coverage in some other completely different location + if sys.version_info[0] > 5: + assert False +''' + +SCRIPT2 = ''' +# + +def test_bar(): + x = True + assert x + +''' + + +COVERAGERC_SOURCE = '''\ +[run] +source = . +''' + +SCRIPT_CHILD = ''' +import sys + +idx = int(sys.argv[1]) + +if idx == 0: + foo = "a" # previously there was a "pass" here but Python 3.5 optimizes it away. +if idx == 1: + foo = "b" # previously there was a "pass" here but Python 3.5 optimizes it away. +''' + +SCRIPT_PARENT = ''' +import os +import subprocess +import sys + +def pytest_generate_tests(metafunc): + for i in [2]: + metafunc.parametrize('idx', range(i)) + +def test_foo(idx): + out, err = subprocess.Popen( + [sys.executable, os.path.join(os.path.dirname(__file__), 'child_script.py'), str(idx)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + +# there is a issue in coverage.py with multiline statements at +# end of file: https://bitbucket.org/ned/coveragepy/issue/293 +pass +''' + +SCRIPT_PARENT_CHANGE_CWD = ''' +import subprocess +import sys +import os + +def pytest_generate_tests(metafunc): + for i in [2]: + metafunc.parametrize('idx', range(i)) + +def test_foo(idx): + os.mkdir("foobar") + os.chdir("foobar") + + subprocess.check_call([ + sys.executable, + os.path.join(os.path.dirname(__file__), 'child_script.py'), + str(idx) + ]) + +# there is a issue in coverage.py with multiline statements at +# end of file: https://bitbucket.org/ned/coveragepy/issue/293 +pass +''' + +SCRIPT_PARENT_CHANGE_CWD_IMPORT_CHILD = ''' +import subprocess +import sys +import os + +def pytest_generate_tests(metafunc): + for i in [2]: + if metafunc.function is test_foo: metafunc.parametrize('idx', range(i)) + +def test_foo(idx): + os.mkdir("foobar") + os.chdir("foobar") + + subprocess.check_call([ + sys.executable, + '-c', 'import sys; sys.argv = ["", str(%s)]; import child_script' % idx + ]) + +# there is a issue in coverage.py with multiline statements at +# end of file: https://bitbucket.org/ned/coveragepy/issue/293 +pass +''' + +SCRIPT_FUNCARG = ''' +import coverage + +def test_foo(cov): + assert isinstance(cov, coverage.Coverage) +''' + +SCRIPT_FUNCARG_NOT_ACTIVE = ''' +def test_foo(cov): + assert cov is None +''' + +CHILD_SCRIPT_RESULT = '[56] * 100%' +PARENT_SCRIPT_RESULT = '9 * 100%' +DEST_DIR = 'cov_dest' +REPORT_NAME = 'cov.xml' + +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) +def adjust_sys_path(): + """Adjust PYTHONPATH during tests to make "helper" importable in SCRIPT.""" + orig_path = os.environ.get('PYTHONPATH', None) + new_path = os.path.dirname(__file__) + if orig_path is not None: + new_path = os.pathsep.join([new_path, orig_path]) + os.environ['PYTHONPATH'] = new_path + + yield + + if orig_path is None: + del os.environ['PYTHONPATH'] + else: + os.environ['PYTHONPATH'] = orig_path + + +@pytest.fixture(params=[ + ('branch=true', '--cov-branch', '9 * 85%', '3 * 100%'), + ('branch=true', '', '9 * 85%', '3 * 100%'), + ('', '--cov-branch', '9 * 85%', '3 * 100%'), + ('', '', '9 * 89%', '3 * 100%'), +], ids=['branch2x', 'branch1c', 'branch1a', 'nobranch']) +def prop(request): + return Namespace( + code=SCRIPT, + code2=SCRIPT2, + conf=request.param[0], + fullconf='[run]\n%s\n' % request.param[0], + prefixedfullconf='[coverage:run]\n%s\n' % request.param[0], + args=request.param[1].split(), + result=request.param[2], + result2=request.param[3], + ) + + +def test_central(testdir, prop): + script = testdir.makepyfile(prop.code) + testdir.tmpdir.join('.coveragerc').write(prop.fullconf) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script, + *prop.args) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_central* %s *' % prop.result, + '*10 passed*' + ]) + assert result.ret == 0 + + +def test_annotate(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=annotate', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage annotated source written next to source', + '*10 passed*', + ]) + assert result.ret == 0 + + +def test_annotate_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=annotate:' + DEST_DIR, + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage annotated source written to dir ' + DEST_DIR, + '*10 passed*', + ]) + dest_dir = testdir.tmpdir.join(DEST_DIR) + assert dest_dir.check(dir=True) + assert dest_dir.join(script.basename + ",cover").check() + assert result.ret == 0 + + +def test_html(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=html', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage HTML written to dir htmlcov', + '*10 passed*', + ]) + dest_dir = testdir.tmpdir.join('htmlcov') + assert dest_dir.check(dir=True) + assert dest_dir.join("index.html").check() + assert result.ret == 0 + + +def test_html_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=html:' + DEST_DIR, + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage HTML written to dir ' + DEST_DIR, + '*10 passed*', + ]) + dest_dir = testdir.tmpdir.join(DEST_DIR) + assert dest_dir.check(dir=True) + assert dest_dir.join("index.html").check() + assert result.ret == 0 + + +def test_term_report_does_not_interact_with_html_output(testdir): + script = testdir.makepyfile(test_funcarg=SCRIPT_FUNCARG) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing:skip-covered', + '--cov-report=html:' + DEST_DIR, + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage HTML written to dir ' + DEST_DIR, + '*1 passed*', + ]) + dest_dir = testdir.tmpdir.join(DEST_DIR) + assert dest_dir.check(dir=True) + 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(""" +[html] +directory = somewhere +""") + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=html', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage HTML written to dir somewhere', + '*10 passed*', + ]) + dest_dir = testdir.tmpdir.join('somewhere') + assert dest_dir.check(dir=True) + assert dest_dir.join("index.html").check() + assert result.ret == 0 + + +def test_xml_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=xml:' + REPORT_NAME, + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage XML written to file ' + REPORT_NAME, + '*10 passed*', + ]) + assert testdir.tmpdir.join(REPORT_NAME).check() + assert result.ret == 0 + + +def test_term_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term:' + DEST_DIR, + script) + + result.stderr.fnmatch_lines([ + '*argument --cov-report: output specifier not supported for: "term:%s"*' % DEST_DIR, + ]) + assert result.ret != 0 + + +def test_term_missing_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing:' + DEST_DIR, + script) + + result.stderr.fnmatch_lines([ + '*argument --cov-report: output specifier not supported for: ' + '"term-missing:%s"*' % DEST_DIR, + ]) + assert result.ret != 0 + + +def test_cov_min_100(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '--cov-fail-under=100', + script) + + assert result.ret != 0 + result.stdout.fnmatch_lines([ + 'FAIL Required test coverage of 100% not reached. Total coverage: *%' + ]) + + +def test_cov_min_50(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=html', + '--cov-report=xml', + '--cov-fail-under=50', + script) + + assert result.ret == 0 + result.stdout.fnmatch_lines([ + 'Required test coverage of 50% reached. Total coverage: *%' + ]) + + +def test_cov_min_float_value(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '--cov-fail-under=88.88', + script) + assert result.ret == 0 + result.stdout.fnmatch_lines([ + 'Required test coverage of 88.88% reached. Total coverage: 88.89%' + ]) + + +def test_cov_min_float_value_not_reached(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '--cov-fail-under=88.89', + script) + assert result.ret == 1 + result.stdout.fnmatch_lines([ + 'FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%' + ]) + + +def test_cov_min_no_report(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=', + '--cov-fail-under=50', + script) + + assert result.ret == 0 + result.stdout.fnmatch_lines([ + 'Required test coverage of 50% reached. Total coverage: *%' + ]) + + +def test_central_nonspecific(testdir, prop): + script = testdir.makepyfile(prop.code) + testdir.tmpdir.join('.coveragerc').write(prop.fullconf) + result = testdir.runpytest('-v', + '--cov', + '--cov-report=term-missing', + script, *prop.args) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_central_nonspecific* %s *' % prop.result, + '*10 passed*' + ]) + + # multi-module coverage report + assert any(line.startswith('TOTAL ') for line in result.stdout.lines) + + assert result.ret == 0 + + +def test_cov_min_from_coveragerc(testdir): + script = testdir.makepyfile(SCRIPT) + testdir.tmpdir.join('.coveragerc').write(""" +[report] +fail_under = 100 +""") + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + assert result.ret != 0 + + +def test_central_coveragerc(testdir, prop): + script = testdir.makepyfile(prop.code) + testdir.tmpdir.join('.coveragerc').write(COVERAGERC_SOURCE + prop.conf) + + result = testdir.runpytest('-v', + '--cov', + '--cov-report=term-missing', + script, *prop.args) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_central_coveragerc* %s *' % prop.result, + '*10 passed*', + ]) + + # single-module coverage report + assert all(not line.startswith('TOTAL ') for line in result.stdout.lines[-4:]) + + assert result.ret == 0 + + +@xdist_params +def test_central_with_path_aliasing(testdir, monkeypatch, opts, prop): + mod1 = testdir.mkdir('src').join('mod.py') + mod1.write(SCRIPT) + mod2 = testdir.mkdir('aliased').join('mod.py') + mod2.write(SCRIPT) + script = testdir.makepyfile(''' +from mod import * +''') + testdir.tmpdir.join('setup.cfg').write(""" +[coverage:paths] +source = + src + aliased +[coverage:run] +source = mod +parallel = true +%s +""" % prop.conf) + + monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([os.environ.get('PYTHONPATH', ''), 'aliased'])) + result = testdir.runpytest('-v', '-s', + '--cov', + '--cov-report=term-missing', + script, *opts.split()+prop.args) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'src[\\/]mod* %s *' % prop.result, + '*10 passed*', + ]) + + # single-module coverage report + assert all(not line.startswith('TOTAL ') for line in result.stdout.lines[-4:]) + + assert result.ret == 0 + + +@xdist_params +def test_borken_cwd(testdir, monkeypatch, opts): + testdir.makepyfile(mod=''' +def foobar(a, b): + return a + b +''') + + script = testdir.makepyfile(''' +import os +import tempfile +import pytest +import mod + +@pytest.fixture +def bad(): + path = tempfile.mkdtemp('test_borken_cwd') + os.chdir(path) + yield + try: + os.rmdir(path) + except OSError: + pass + +def test_foobar(bad): + assert mod.foobar(1, 2) == 3 +''') + result = testdir.runpytest('-v', '-s', + '--cov=mod', + '--cov-branch', + script, *opts.split()) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + '*mod* 100%', + '*1 passed*', + ]) + + assert result.ret == 0 + + +def test_subprocess_with_path_aliasing(testdir, monkeypatch): + src = testdir.mkdir('src') + src.join('parent_script.py').write(SCRIPT_PARENT) + src.join('child_script.py').write(SCRIPT_CHILD) + aliased = testdir.mkdir('aliased') + parent_script = aliased.join('parent_script.py') + parent_script.write(SCRIPT_PARENT) + aliased.join('child_script.py').write(SCRIPT_CHILD) + + testdir.tmpdir.join('.coveragerc').write(""" +[paths] +source = + src + aliased +[run] +source = + parent_script + child_script +parallel = true +""") + + monkeypatch.setitem(os.environ, 'PYTHONPATH', os.pathsep.join([ + os.environ.get('PYTHONPATH', ''), 'aliased'])) + result = testdir.runpytest('-v', + '--cov', + '--cov-report=term-missing', + parent_script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'src[\\/]child_script* %s*' % CHILD_SCRIPT_RESULT, + 'src[\\/]parent_script* %s*' % PARENT_SCRIPT_RESULT, + ]) + assert result.ret == 0 + + +def test_show_missing_coveragerc(testdir, prop): + script = testdir.makepyfile(prop.code) + testdir.tmpdir.join('.coveragerc').write(""" +[run] +source = . +%s + +[report] +show_missing = true +""" % prop.conf) + + result = testdir.runpytest('-v', + '--cov', + '--cov-report=term', + script, *prop.args) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Name * Stmts * Miss * Cover * Missing', + 'test_show_missing_coveragerc* %s * 11*' % prop.result, + '*10 passed*', + ]) + + # single-module coverage report + assert all(not line.startswith('TOTAL ') for line in result.stdout.lines[-4:]) + + assert result.ret == 0 + + +def test_no_cov_on_fail(testdir): + script = testdir.makepyfile(''' +def test_fail(): + assert False + +''') + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '--no-cov-on-fail', + script) + + assert 'coverage: platform' not in result.stdout.str() + result.stdout.fnmatch_lines(['*1 failed*']) + + +def test_no_cov(testdir, monkeypatch): + script = testdir.makepyfile(SCRIPT) + testdir.makeini(""" + [pytest] + addopts=--no-cov + """) + result = testdir.runpytest('-vvv', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '-rw', + script) + result.stdout.fnmatch_lines_random([ + 'WARNING: Coverage disabled via --no-cov switch!', + '*Coverage disabled via --no-cov switch!', + ]) + + +def test_cov_and_failure_report_on_fail(testdir): + script = testdir.makepyfile(SCRIPT + ''' +def test_fail(p): + assert False + +''') + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-fail-under=100', + '--cov-report=html', + script) + + result.stdout.fnmatch_lines_random([ + '*10 failed*', + '*coverage: platform*', + '*FAIL Required test coverage of 100% not reached*', + '*assert False*', + ]) + + +@pytest.mark.skipif('sys.platform == "win32" or platform.python_implementation() == "PyPy"') +def test_dist_combine_racecondition(testdir): + script = testdir.makepyfile(""" +import pytest + +@pytest.mark.parametrize("foo", range(1000)) +def test_foo(foo): +""" + "\n".join(""" + if foo == %s: + assert True +""" % i for i in range(1000))) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '-n', '5', '-s', + script) + result.stdout.fnmatch_lines([ + 'test_dist_combine_racecondition* 0 * 100%*', + '*1000 passed*' + ]) + + for line in chain(result.stdout.lines, result.stderr.lines): + assert 'The following workers failed to return coverage data' not in line + assert 'INTERNALERROR' not in line + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +def test_dist_collocated(testdir, prop): + script = testdir.makepyfile(prop.code) + testdir.tmpdir.join('.coveragerc').write(prop.fullconf) + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '--dist=load', + '--tx=2*popen', + max_worker_restart_0, + script, *prop.args) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_dist_collocated* %s *' % prop.result, + '*10 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +def test_dist_not_collocated(testdir, prop): + script = testdir.makepyfile(prop.code) + dir1 = testdir.mkdir('dir1') + dir2 = testdir.mkdir('dir2') + testdir.tmpdir.join('.coveragerc').write(''' +[run] +%s +[paths] +source = + . + dir1 + dir2''' % prop.conf) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '--dist=load', + '--tx=popen//chdir=%s' % dir1, + '--tx=popen//chdir=%s' % dir2, + '--rsyncdir=%s' % script.basename, + '--rsyncdir=.coveragerc', + max_worker_restart_0, '-s', + script, *prop.args) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_dist_not_collocated* %s *' % prop.result, + '*10 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +def test_dist_not_collocated_coveragerc_source(testdir, prop): + script = testdir.makepyfile(prop.code) + dir1 = testdir.mkdir('dir1') + dir2 = testdir.mkdir('dir2') + testdir.tmpdir.join('.coveragerc').write(''' +[run] +%s +source = %s +[paths] +source = + . + dir1 + dir2''' % (prop.conf, script.dirpath())) + + result = testdir.runpytest('-v', + '--cov', + '--cov-report=term-missing', + '--dist=load', + '--tx=popen//chdir=%s' % dir1, + '--tx=popen//chdir=%s' % dir2, + '--rsyncdir=%s' % script.basename, + '--rsyncdir=.coveragerc', + max_worker_restart_0, '-s', + script, *prop.args) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_dist_not_collocated* %s *' % prop.result, + '*10 passed*' + ]) + assert result.ret == 0 + + +def test_central_subprocess(testdir): + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, + child_script=SCRIPT_CHILD) + parent_script = scripts.dirpath().join('parent_script.py') + + result = testdir.runpytest('-v', + '--cov=%s' % scripts.dirpath(), + '--cov-report=term-missing', + parent_script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'child_script* %s*' % CHILD_SCRIPT_RESULT, + 'parent_script* %s*' % PARENT_SCRIPT_RESULT, + ]) + assert result.ret == 0 + + +def test_central_subprocess_change_cwd(testdir): + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT_CHANGE_CWD, + child_script=SCRIPT_CHILD) + parent_script = scripts.dirpath().join('parent_script.py') + testdir.makefile('', coveragerc=""" +[run] +branch = true +parallel = true +""") + + result = testdir.runpytest('-v', '-s', + '--cov=%s' % scripts.dirpath(), + '--cov-config=coveragerc', + '--cov-report=term-missing', + parent_script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + '*child_script* %s*' % CHILD_SCRIPT_RESULT, + '*parent_script* 100%*', + ]) + assert result.ret == 0 + + +def test_central_subprocess_change_cwd_with_pythonpath(testdir, monkeypatch): + stuff = testdir.mkdir('stuff') + parent_script = stuff.join('parent_script.py') + parent_script.write(SCRIPT_PARENT_CHANGE_CWD_IMPORT_CHILD) + stuff.join('child_script.py').write(SCRIPT_CHILD) + testdir.makefile('', coveragerc=""" +[run] +parallel = true +""") + + monkeypatch.setitem(os.environ, 'PYTHONPATH', str(stuff)) + result = testdir.runpytest('-vv', '-s', + '--cov=child_script', + '--cov-config=coveragerc', + '--cov-report=term-missing', + '--cov-branch', + parent_script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + '*child_script* %s*' % CHILD_SCRIPT_RESULT, + ]) + assert result.ret == 0 + + +def test_central_subprocess_no_subscript(testdir): + script = testdir.makepyfile(""" +import subprocess, sys + +def test_foo(): + subprocess.check_call([sys.executable, '-c', 'print("Hello World")']) +""") + testdir.makefile('', coveragerc=""" +[run] +parallel = true +""") + result = testdir.runpytest('-v', + '--cov-config=coveragerc', + '--cov=%s' % script.dirpath(), + '--cov-branch', + script) + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_central_subprocess_no_subscript* * 3 * 0 * 100%*', + ]) + 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) + parent_script = scripts.dirpath().join('parent_script.py') + + result = testdir.runpytest('-v', + '--cov=%s' % scripts.dirpath(), + '--cov-report=term-missing', + '--dist=load', + '--tx=2*popen', + max_worker_restart_0, + parent_script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'child_script* %s*' % CHILD_SCRIPT_RESULT, + 'parent_script* %s*' % PARENT_SCRIPT_RESULT, + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +def test_dist_subprocess_not_collocated(testdir, tmpdir): + scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, + child_script=SCRIPT_CHILD) + 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(''' +[paths] +source = + %s + */dir1 + */dir2 +''' % scripts.dirpath()) + result = testdir.runpytest('-v', + '--cov=%s' % scripts.dirpath(), + '--dist=load', + '--tx=popen//chdir=%s' % dir1, + '--tx=popen//chdir=%s' % dir2, + '--rsyncdir=%s' % child_script, + '--rsyncdir=%s' % parent_script, + '--rsyncdir=.coveragerc', + max_worker_restart_0, + parent_script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'child_script* %s*' % CHILD_SCRIPT_RESULT, + 'parent_script* %s*' % PARENT_SCRIPT_RESULT, + ]) + assert result.ret == 0 + + +def test_invalid_coverage_source(testdir): + script = testdir.makepyfile(SCRIPT) + testdir.makeini(""" + [pytest] + console_output_style=classic + """) + result = testdir.runpytest('-v', + '--cov=non_existent_module', + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*10 passed*' + ]) + result.stderr.fnmatch_lines([ + 'Coverage.py warning: No data was collected.*' + ]) + result.stdout.fnmatch_lines([ + '*Failed to generate report: No data to report.', + ]) + assert result.ret == 0 + + matching_lines = [line for line in result.outlines if '%' in line] + assert not matching_lines + + +@pytest.mark.skipif("'dev' in pytest.__version__") +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +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": + exe = os.path.join(venv_path, 'bin', 'python.exe') + else: + exe = os.path.join(venv_path, 'Scripts', 'python.exe') + else: + exe = os.path.join(venv_path, 'bin', 'python') + subprocess.check_call([ + exe, + '-mpip', + 'install', + 'py==%s' % py.__version__, + 'pytest==%s' % pytest.__version__, + 'pytest_xdist==%s' % xdist.__version__ + + ]) + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--assert=plain', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '--dist=load', + '--tx=popen//python=%s' % exe, + max_worker_restart_0, + script) + result.stdout.fnmatch_lines([ + 'The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.' + ]) + + +def test_funcarg(testdir): + script = testdir.makepyfile(SCRIPT_FUNCARG) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_funcarg* 3 * 100%*', + '*1 passed*' + ]) + assert result.ret == 0 + + +def test_funcarg_not_active(testdir): + script = testdir.makepyfile(SCRIPT_FUNCARG_NOT_ACTIVE) + + result = testdir.runpytest('-v', + script) + + result.stdout.fnmatch_lines([ + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif("sys.version_info[0] < 3", reason="no context manager api on Python 2") +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +@pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason="often deadlocks on PyPy") +@pytest.mark.skipif('sys.version_info[:2] == (3, 8)', reason="deadlocks on Python 3.8, see: https://bugs.python.org/issue38227") +def test_multiprocessing_pool(testdir): + pytest.importorskip('multiprocessing.util') + + script = testdir.makepyfile(''' +import multiprocessing + +def target_fn(a): + %sse: # pragma: nocover + return None + +def test_run_target(): + from pytest_cov.embed import cleanup_on_sigterm + cleanup_on_sigterm() + + for i in range(33): + with multiprocessing.Pool(3) as p: + p.map(target_fn, [i * 3 + j for j in range(3)]) + p.join() +''' % ''.join('''if a == %r: + return a + el''' % i for i in range(99))) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + assert "Doesn't seem to be a coverage.py data file" not in result.stdout.str() + assert "Doesn't seem to be a coverage.py data file" not in result.stderr.str() + assert not testdir.tmpdir.listdir(".coverage.*") + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_multiprocessing_pool* 100%*', + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +@pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason="often deadlocks on PyPy") +@pytest.mark.skipif('sys.version_info[:2] == (3, 8)', reason="deadlocks on Python 3.8, see: https://bugs.python.org/issue38227") +def test_multiprocessing_pool_terminate(testdir): + pytest.importorskip('multiprocessing.util') + + script = testdir.makepyfile(''' +import multiprocessing + +def target_fn(a): + %sse: # pragma: nocover + return None + +def test_run_target(): + from pytest_cov.embed import cleanup_on_sigterm + cleanup_on_sigterm() + + for i in range(33): + p = multiprocessing.Pool(3) + try: + p.map(target_fn, [i * 3 + j for j in range(3)]) + finally: + p.terminate() + p.join() +''' % ''.join('''if a == %r: + return a + el''' % i for i in range(99))) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + assert "Doesn't seem to be a coverage.py data file" not in result.stdout.str() + assert "Doesn't seem to be a coverage.py data file" not in result.stderr.str() + assert not testdir.tmpdir.listdir(".coverage.*") + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_multiprocessing_pool* 100%*', + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +@pytest.mark.skipif('sys.version_info[0] > 2 and platform.python_implementation() == "PyPy"', reason="broken on PyPy3") +def test_multiprocessing_pool_close(testdir): + pytest.importorskip('multiprocessing.util') + + script = testdir.makepyfile(''' +import multiprocessing + +def target_fn(a): + %sse: # pragma: nocover + return None + +def test_run_target(): + for i in range(33): + p = multiprocessing.Pool(3) + try: + p.map(target_fn, [i * 3 + j for j in range(3)]) + finally: + p.close() + p.join() +''' % ''.join('''if a == %r: + return a + el''' % i for i in range(99))) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + assert "Doesn't seem to be a coverage.py data file" not in result.stdout.str() + assert "Doesn't seem to be a coverage.py data file" not in result.stderr.str() + assert not testdir.tmpdir.listdir(".coverage.*") + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_multiprocessing_pool* 100%*', + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +def test_multiprocessing_process(testdir): + pytest.importorskip('multiprocessing.util') + + script = testdir.makepyfile(''' +import multiprocessing + +def target_fn(): + a = True + return a + +def test_run_target(): + p = multiprocessing.Process(target=target_fn) + p.start() + p.join() +''') + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_multiprocessing_process* 8 * 100%*', + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +def test_multiprocessing_process_no_source(testdir): + pytest.importorskip('multiprocessing.util') + + script = testdir.makepyfile(''' +import multiprocessing + +def target_fn(): + a = True + return a + +def test_run_target(): + p = multiprocessing.Process(target=target_fn) + p.start() + p.join() +''') + + result = testdir.runpytest('-v', + '--cov', + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_multiprocessing_process* 8 * 100%*', + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +def test_multiprocessing_process_with_terminate(testdir): + pytest.importorskip('multiprocessing.util') + + script = testdir.makepyfile(''' +import multiprocessing +import time +from pytest_cov.embed import cleanup_on_sigterm +cleanup_on_sigterm() + +event = multiprocessing.Event() + +def target_fn(): + a = True + event.set() + time.sleep(5) + +def test_run_target(): + p = multiprocessing.Process(target=target_fn) + p.start() + time.sleep(0.5) + event.wait(1) + p.terminate() + p.join() +''') + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_multiprocessing_process* 16 * 100%*', + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") +def test_cleanup_on_sigterm(testdir): + script = testdir.makepyfile(''' +import os, signal, subprocess, sys, time + +def cleanup(num, frame): + print("num == signal.SIGTERM => %s" % (num == signal.SIGTERM)) + raise Exception() + +def test_run(): + proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + time.sleep(1) + proc.terminate() + stdout, stderr = proc.communicate() + assert not stderr + assert stdout == b"""num == signal.SIGTERM => True +captured Exception() +""" + assert proc.returncode == 0 + +if __name__ == "__main__": + signal.signal(signal.SIGTERM, cleanup) + + from pytest_cov.embed import cleanup_on_sigterm + cleanup_on_sigterm() + + try: + time.sleep(10) + except BaseException as exc: + print("captured %r" % exc) +''') + + result = testdir.runpytest('-vv', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_cleanup_on_sigterm* 26-27', + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform != "win32"') +@pytest.mark.parametrize('setup', [ + ('signal.signal(signal.SIGBREAK, signal.SIG_DFL); cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), + ('cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), + ('cleanup()', '73% 19-22'), +]) +def test_cleanup_on_sigterm_sig_break(testdir, setup): + # worth a read: https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/ + script = testdir.makepyfile(''' +import os, signal, subprocess, sys, time + +def test_run(): + proc = subprocess.Popen( + [sys.executable, __file__], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, shell=True + ) + time.sleep(1) + proc.send_signal(signal.CTRL_BREAK_EVENT) + stdout, stderr = proc.communicate() + assert not stderr + assert stdout in [b"^C", b"", b"captured IOError(4, 'Interrupted function call')\\n"] + +if __name__ == "__main__": + from pytest_cov.embed import cleanup_on_signal, cleanup + ''' + setup[0] + ''' + + try: + time.sleep(10) + except BaseException as exc: + print("captured %r" % exc) +''') + + result = testdir.runpytest('-vv', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_cleanup_on_sigterm* %s' % setup[1], + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") +@pytest.mark.parametrize('setup', [ + ('signal.signal(signal.SIGTERM, signal.SIG_DFL); cleanup_on_sigterm()', '88% 18-19'), + ('cleanup_on_sigterm()', '88% 18-19'), + ('cleanup()', '75% 16-19'), +]) +def test_cleanup_on_sigterm_sig_dfl(testdir, setup): + script = testdir.makepyfile(''' +import os, signal, subprocess, sys, time + +def test_run(): + proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + time.sleep(1) + proc.terminate() + stdout, stderr = proc.communicate() + assert not stderr + assert stdout == b"" + assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM] + +if __name__ == "__main__": + from pytest_cov.embed import cleanup_on_sigterm, cleanup + ''' + setup[0] + ''' + + try: + time.sleep(10) + except BaseException as exc: + print("captured %r" % exc) +''') + + result = testdir.runpytest('-vv', + '--assert=plain', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_cleanup_on_sigterm* %s' % setup[1], + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32"', reason="SIGINT is subtly broken on Windows") +def test_cleanup_on_sigterm_sig_dfl_sigint(testdir): + script = testdir.makepyfile(''' +import os, signal, subprocess, sys, time + +def test_run(): + proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + time.sleep(1) + proc.send_signal(signal.SIGINT) + stdout, stderr = proc.communicate() + assert not stderr + assert stdout == b"""captured KeyboardInterrupt() +""" + assert proc.returncode == 0 + +if __name__ == "__main__": + from pytest_cov.embed import cleanup_on_signal + cleanup_on_signal(signal.SIGINT) + + try: + time.sleep(10) + except BaseException as exc: + print("captured %r" % exc) +''') + + result = testdir.runpytest('-vv', + '--assert=plain', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_cleanup_on_sigterm* 88% 19-20', + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32"', reason="fork not available on Windows") +def test_cleanup_on_sigterm_sig_ign(testdir): + script = testdir.makepyfile(''' +import os, signal, subprocess, sys, time + +def test_run(): + proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + time.sleep(1) + proc.send_signal(signal.SIGINT) + time.sleep(1) + proc.terminate() + 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] + +if __name__ == "__main__": + signal.signal(signal.SIGINT, signal.SIG_IGN) + + from pytest_cov.embed import cleanup_on_signal + cleanup_on_signal(signal.SIGINT) + + try: + time.sleep(10) + except BaseException as exc: + print("captured %r" % exc) + ''') + + result = testdir.runpytest('-vv', + '--assert=plain', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_cleanup_on_sigterm* 89% 23-24', + '*1 passed*' + ]) + assert result.ret == 0 + + +MODULE = ''' +def func(): + return 1 + +''' + +CONFTEST = ''' + +import mod +mod.func() + +''' + +BASIC_TEST = ''' + +def test_basic(): + x = True + assert x + +''' + +CONF_RESULT = 'mod* 2 * 100%*' + + +def test_cover_conftest(testdir): + testdir.makepyfile(mod=MODULE) + testdir.makeconftest(CONFTEST) + script = testdir.makepyfile(BASIC_TEST) + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + assert result.ret == 0 + result.stdout.fnmatch_lines([CONF_RESULT]) + + +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +def test_cover_looponfail(testdir, monkeypatch): + testdir.makepyfile(mod=MODULE) + testdir.makeconftest(CONFTEST) + script = testdir.makepyfile(BASIC_TEST) + + monkeypatch.setattr(testdir, 'run', + lambda *args, **kwargs: _TestProcess(*map(str, args))) + with testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--looponfail', + script) as process: + with dump_on_error(process.read): + wait_for_strings( + process.read, + 30, # 30 seconds + 'Stmts Miss Cover' + ) + + +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +def test_cover_conftest_dist(testdir): + testdir.makepyfile(mod=MODULE) + testdir.makeconftest(CONFTEST) + script = testdir.makepyfile(BASIC_TEST) + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '--dist=load', + '--tx=2*popen', + max_worker_restart_0, + script) + assert result.ret == 0 + result.stdout.fnmatch_lines([CONF_RESULT]) + + +def test_no_cover_marker(testdir): + testdir.makepyfile(mod=MODULE) + script = testdir.makepyfile(''' +import pytest +import mod +import subprocess +import sys + +@pytest.mark.no_cover +def test_basic(): + mod.func() + subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) +''') + result = testdir.runpytest('-v', '-ra', '--strict', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + assert result.ret == 0 + result.stdout.fnmatch_lines(['mod* 2 * 1 * 50% * 2']) + + +def test_no_cover_fixture(testdir): + testdir.makepyfile(mod=MODULE) + script = testdir.makepyfile(''' +import mod +import subprocess +import sys + +def test_basic(no_cover): + mod.func() + subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) +''') + result = testdir.runpytest('-v', '-ra', '--strict', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + assert result.ret == 0 + result.stdout.fnmatch_lines(['mod* 2 * 1 * 50% * 2']) + + +COVERAGERC = ''' +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + raise NotImplementedError + +''' + +EXCLUDED_TEST = ''' + +def func(): + raise NotImplementedError + +def test_basic(): + x = True + assert x + +''' + +EXCLUDED_RESULT = '4 * 100%*' + + +def test_coveragerc(testdir): + testdir.makefile('', coveragerc=COVERAGERC) + script = testdir.makepyfile(EXCLUDED_TEST) + result = testdir.runpytest('-v', + '--cov-config=coveragerc', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + assert result.ret == 0 + result.stdout.fnmatch_lines(['test_coveragerc* %s' % EXCLUDED_RESULT]) + + +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +def test_coveragerc_dist(testdir): + testdir.makefile('', coveragerc=COVERAGERC) + script = testdir.makepyfile(EXCLUDED_TEST) + result = testdir.runpytest('-v', + '--cov-config=coveragerc', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + '-n', '2', + max_worker_restart_0, + script) + assert result.ret == 0 + result.stdout.fnmatch_lines( + ['test_coveragerc_dist* %s' % EXCLUDED_RESULT]) + + +SKIP_COVERED_COVERAGERC = ''' +[report] +skip_covered = True + +''' + +SKIP_COVERED_TEST = ''' + +def func(): + return "full coverage" + +def test_basic(): + assert func() == "full coverage" + +''' + +SKIP_COVERED_RESULT = '1 file skipped due to complete coverage.' + + +@pytest.mark.parametrize('report_option', [ + 'term-missing:skip-covered', + 'term:skip-covered']) +def test_skip_covered_cli(testdir, report_option): + testdir.makefile('', coveragerc=SKIP_COVERED_COVERAGERC) + script = testdir.makepyfile(SKIP_COVERED_TEST) + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=%s' % report_option, + script) + assert result.ret == 0 + result.stdout.fnmatch_lines([SKIP_COVERED_RESULT]) + + +def test_skip_covered_coveragerc_config(testdir): + testdir.makefile('', coveragerc=SKIP_COVERED_COVERAGERC) + script = testdir.makepyfile(SKIP_COVERED_TEST) + result = testdir.runpytest('-v', + '--cov-config=coveragerc', + '--cov=%s' % script.dirpath(), + script) + assert result.ret == 0 + result.stdout.fnmatch_lines([SKIP_COVERED_RESULT]) + + +CLEAR_ENVIRON_TEST = ''' + +import os + +def test_basic(): + os.environ.clear() + +''' + + +def test_clear_environ(testdir): + script = testdir.makepyfile(CLEAR_ENVIRON_TEST) + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + assert result.ret == 0 + + +SCRIPT_SIMPLE = ''' + +def test_foo(): + assert 1 == 1 + x = True + assert x + +''' + +SCRIPT_SIMPLE_RESULT = '4 * 100%' + + +@pytest.mark.skipif('sys.platform == "win32"') +def test_dist_boxed(testdir): + script = testdir.makepyfile(SCRIPT_SIMPLE) + + result = testdir.runpytest('-v', + '--assert=plain', + '--cov=%s' % script.dirpath(), + '--boxed', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_dist_boxed* %s*' % 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") +def test_dist_bare_cov(testdir): + script = testdir.makepyfile(SCRIPT_SIMPLE) + + result = testdir.runpytest('-v', + '--cov', + '-n', '1', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_dist_bare_cov* %s*' % SCRIPT_SIMPLE_RESULT, + '*1 passed*' + ]) + assert result.ret == 0 + + +def test_not_started_plugin_does_not_fail(testdir): + class ns: + cov_source = [True] + cov_report = '' + plugin = pytest_cov.plugin.CovPlugin(ns, None, start=False) + plugin.pytest_runtestloop(None) + plugin.pytest_terminal_summary(None) + + +def test_default_output_setting(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + script) + + result.stdout.fnmatch_lines([ + '*coverage*' + ]) + assert result.ret == 0 + + +def test_disabled_output(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=', + script) + + stdout = result.stdout.str() + # We don't want the path to the executable to fail the test if we happen + # to put the project in a directory with "coverage" in it. + stdout = stdout.replace(sys.executable, "") + assert 'coverage' not in stdout + assert result.ret == 0 + + +def test_coverage_file(testdir): + script = testdir.makepyfile(SCRIPT) + data_file_name = 'covdata' + os.environ['COVERAGE_FILE'] = data_file_name + try: + result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), + script) + assert result.ret == 0 + data_file = testdir.tmpdir.join(data_file_name) + assert data_file.check() + finally: + os.environ.pop('COVERAGE_FILE') + + +def test_external_data_file(testdir): + script = testdir.makepyfile(SCRIPT) + testdir.tmpdir.join('.coveragerc').write(""" +[run] +data_file = %s +""" % testdir.tmpdir.join('some/special/place/coverage-data').ensure()) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + 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_external_data_file_xdist(testdir): + script = testdir.makepyfile(SCRIPT) + testdir.tmpdir.join('.coveragerc').write(""" +[run] +parallel = true +data_file = %s +""" % testdir.tmpdir.join('some/special/place/coverage-data').ensure()) + + result = testdir.runpytest('-v', + '--cov=%s' % 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(""" +import target +def test_foobar(): + assert target.x == 123 +""") + result = testdir.runpytest('-v', + '--cov=target', + '-n', '1', + script) + assert 'no-data-collected' not in result.stderr.str() + assert 'no-data-collected' not in result.stdout.str() + assert 'module-not-imported' not in result.stderr.str() + assert 'module-not-imported' not in result.stdout.str() + assert result.ret == 0 + + +def test_external_data_file_negative(testdir): + script = testdir.makepyfile(SCRIPT) + testdir.tmpdir.join('.coveragerc').write("") + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + script) + assert result.ret == 0 + assert glob.glob(str(testdir.tmpdir.join('.coverage*'))) + + +@xdist_params +def test_append_coverage(testdir, opts, prop): + script = testdir.makepyfile(test_1=prop.code) + testdir.tmpdir.join('.coveragerc').write(prop.fullconf) + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + script, + *opts.split()+prop.args) + result.stdout.fnmatch_lines([ + 'test_1* %s*' % prop.result, + ]) + script2 = testdir.makepyfile(test_2=prop.code2) + result = testdir.runpytest('-v', + '--cov-append', + '--cov=%s' % script2.dirpath(), + script2, + *opts.split()+prop.args) + result.stdout.fnmatch_lines([ + 'test_1* %s*' % prop.result, + 'test_2* %s*' % prop.result2, + ]) + + +@xdist_params +def test_do_not_append_coverage(testdir, opts, prop): + script = testdir.makepyfile(test_1=prop.code) + testdir.tmpdir.join('.coveragerc').write(prop.fullconf) + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + script, + *opts.split()+prop.args) + result.stdout.fnmatch_lines([ + 'test_1* %s*' % prop.result, + ]) + script2 = testdir.makepyfile(test_2=prop.code2) + result = testdir.runpytest('-v', + '--cov=%s' % script2.dirpath(), + script2, + *opts.split()+prop.args) + result.stdout.fnmatch_lines([ + 'test_1* 0%', + 'test_2* %s*' % prop.result2, + ]) + + +@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) + parent_script = scripts.dirpath().join('parent_script.py') + + result = testdir.runpytest('-v', + '--cov=%s' % scripts.dirpath(), + '--cov-append', + '--cov-report=term-missing', + '--dist=load', + '--tx=2*popen', + max_worker_restart_0, + parent_script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'child_script* %s*' % CHILD_SCRIPT_RESULT, + 'parent_script* %s*' % PARENT_SCRIPT_RESULT, + ]) + assert result.ret == 0 + + +def test_pth_failure(monkeypatch): + with open('src/pytest-cov.pth') as fh: + payload = fh.read() + + class SpecificError(Exception): + pass + + def bad_init(): + raise SpecificError() + + buff = StringIO() + + from pytest_cov import embed + + monkeypatch.setattr(embed, 'init', bad_init) + monkeypatch.setattr(sys, 'stderr', buff) + monkeypatch.setitem(os.environ, 'COV_CORE_SOURCE', 'foobar') + exec_(payload) + assert buff.getvalue() == '''pytest-cov: Failed to setup subprocess coverage. Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError() +''' + + +def test_double_cov(testdir): + script = testdir.makepyfile(SCRIPT_SIMPLE) + result = testdir.runpytest('-v', + '--assert=plain', + '--cov', '--cov=%s' % script.dirpath(), + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_double_cov* %s*' % SCRIPT_SIMPLE_RESULT, + '*1 passed*' + ]) + assert result.ret == 0 + + +def test_double_cov2(testdir): + script = testdir.makepyfile(SCRIPT_SIMPLE) + result = testdir.runpytest('-v', + '--assert=plain', + '--cov', '--cov', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_double_cov2* %s*' % SCRIPT_SIMPLE_RESULT, + '*1 passed*' + ]) + assert result.ret == 0 + + +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +def test_cov_and_no_cov(testdir): + script = testdir.makepyfile(SCRIPT_SIMPLE) + result = testdir.runpytest('-v', + '--cov', '--no-cov', + '-n', '1', + '-s', + script) + assert 'Coverage disabled via --no-cov switch!' not in result.stdout.str() + assert 'Coverage disabled via --no-cov switch!' not in result.stderr.str() + assert result.ret == 0 + + +def find_labels(text, pattern): + all_labels = collections.defaultdict(set) + lines = text.splitlines() + for lineno, line in enumerate(lines, start=1): + labels = re.findall(pattern, line) + for label in labels: + all_labels[label].add(lineno) + return all_labels + + +# The contexts and their labels in contextful.py +EXPECTED_CONTEXTS = { + '': 'c0', + 'test_contexts.py::test_01|run': 'r1', + 'test_contexts.py::test_02|run': 'r2', + 'test_contexts.py::OldStyleTests::test_03|setup': 's3', + 'test_contexts.py::OldStyleTests::test_03|run': 'r3', + 'test_contexts.py::OldStyleTests::test_04|run': 'r4', + 'test_contexts.py::OldStyleTests::test_04|teardown': 't4', + 'test_contexts.py::test_05|setup': 's5', + 'test_contexts.py::test_05|run': 'r5', + 'test_contexts.py::test_06|setup': 's6', + 'test_contexts.py::test_06|run': 'r6', + 'test_contexts.py::test_07|setup': 's7', + 'test_contexts.py::test_07|run': 'r7', + 'test_contexts.py::test_08|run': 'r8', + 'test_contexts.py::test_09[1]|setup': 's9-1', + 'test_contexts.py::test_09[1]|run': 'r9-1', + 'test_contexts.py::test_09[2]|setup': 's9-2', + 'test_contexts.py::test_09[2]|run': 'r9-2', + 'test_contexts.py::test_09[3]|setup': 's9-3', + 'test_contexts.py::test_09[3]|run': 'r9-3', + 'test_contexts.py::test_10|run': 'r10', + 'test_contexts.py::test_11[1-101]|run': 'r11-1', + 'test_contexts.py::test_11[2-202]|run': 'r11-2', + 'test_contexts.py::test_12[one]|run': 'r12-1', + 'test_contexts.py::test_12[two]|run': 'r12-2', + 'test_contexts.py::test_13[3-1]|run': 'r13-1', + 'test_contexts.py::test_13[3-2]|run': 'r13-2', + 'test_contexts.py::test_13[4-1]|run': 'r13-3', + 'test_contexts.py::test_13[4-2]|run': 'r13-4', +} + + +@pytest.mark.skipif("coverage.version_info < (5, 0)") +@xdist_params +def test_contexts(testdir, opts): + with open(os.path.join(os.path.dirname(__file__), "contextful.py")) as f: + contextful_tests = f.read() + script = testdir.makepyfile(contextful_tests) + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-context=test', + script, + *opts.split() + ) + assert result.ret == 0 + result.stdout.fnmatch_lines([ + 'test_contexts* 100%*', + ]) + + data = coverage.CoverageData(".coverage") + data.read() + assert data.measured_contexts() == set(EXPECTED_CONTEXTS) + measured = data.measured_files() + assert len(measured) == 1 + test_context_path = list(measured)[0] + assert test_context_path.lower() == os.path.abspath("test_contexts.py").lower() + + line_data = find_labels(contextful_tests, r"[crst]\d+(?:-\d+)?") + for context, label in EXPECTED_CONTEXTS.items(): + if context == '': + continue + data.set_query_context(context) + actual = set(data.lines(test_context_path)) + assert line_data[label] == actual, "Wrong lines for context {!r}".format(context) + + +@pytest.mark.skipif("coverage.version_info >= (5, 0)") +def test_contexts_not_supported(testdir): + script = testdir.makepyfile("a = 1") + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-context=test', + script, + ) + result.stderr.fnmatch_lines([ + '*argument --cov-context: Contexts are only supported with coverage.py >= 5.x', + ]) + assert result.ret != 0 + + +def test_issue_417(testdir): + # https://github.com/pytest-dev/pytest-cov/issues/417 + whatever = testdir.maketxtfile(whatever="") + testdir.inline_genitems(whatever) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ec08819 --- /dev/null +++ b/tox.ini @@ -0,0 +1,79 @@ +; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist + +[tox] +envlist = + check + py{27,35,36,37,py,py3}-pytest46-xdist27-coverage{45,52} + py{36,37,38,py3}-pytest{46,54}-xdist33-coverage{45,52} + py{36,37,38,py3}-pytest{60}-xdist200-coverage{52} + docs + +[testenv] +extras = testing +setenv = + PYTHONUNBUFFERED=yes + + # Use env vars for (optional) pinning of deps. + pytest46: _DEP_PYTEST=pytest==4.6.10 + pytest53: _DEP_PYTEST=pytest==5.3.2 + pytest54: _DEP_PYTEST=pytest==5.4.3 + pytest60: _DEP_PYTEST=pytest==6.0.1 + + xdist27: _DEP_PYTESTXDIST=pytest-xdist==1.27.0 + xdist29: _DEP_PYTESTXDIST=pytest-xdist==1.29.0 + xdist31: _DEP_PYTESTXDIST=pytest-xdist==1.31.0 + xdist32: _DEP_PYTESTXDIST=pytest-xdist==1.32.0 + xdist33: _DEP_PYTESTXDIST=pytest-xdist==1.33.0 + xdist34: _DEP_PYTESTXDIST=pytest-xdist==1.34.0 + xdist200: _DEP_PYTESTXDIST=pytest-xdist==2.0.0 + xdistdev: _DEP_PYTESTXDIST=git+https://github.com/pytest-dev/pytest-xdist.git#egg=pytest-xdist + + coverage45: _DEP_COVERAGE=coverage==4.5.4 + coverage50: _DEP_COVERAGE=coverage==5.0.4 + coverage51: _DEP_COVERAGE=coverage==5.1 + coverage52: _DEP_COVERAGE=coverage==5.2.1 + # For testing against a coverage.py working tree. + coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} +passenv = + * +deps = + {env:_DEP_PYTEST:pytest} + {env:_DEP_PYTESTXDIST:pytest-xdist} + {env:_DEP_COVERAGE:coverage} +pip_pre = true +commands = + pytest {posargs:-vv} + +[testenv:spell] +setenv = + SPELLCHECK=1 +commands = + sphinx-build -b spelling docs dist/docs +skip_install = true +usedevelop = false +deps = + -r{toxinidir}/docs/requirements.txt + sphinxcontrib-spelling + pyenchant + +[testenv:docs] +deps = + -r{toxinidir}/docs/requirements.txt +commands = + sphinx-build {posargs:-E} -b html docs dist/docs + +[testenv:check] +deps = + docutils + check-manifest + flake8 + readme-renderer + pygments + isort +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 -- 2.34.1