From: DongHun Kwak Date: Mon, 12 Apr 2021 06:19:03 +0000 (+0900) Subject: Imported Upstream version python3-pytest-cov 2.10.1 X-Git-Tag: upstream/2.10.1 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=56515f7087e7f8711d3b1d664a461c1c5fde985d;p=platform%2Fupstream%2Fpython3-pytest-cov.git Imported Upstream version python3-pytest-cov 2.10.1 --- 56515f7087e7f8711d3b1d664a461c1c5fde985d 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