diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 9f176ee5..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,46 +0,0 @@ -# NOTE: this file is auto-generated via ci/bootstrap.py (ci/templates/.appveyor.yml). -version: '{branch}-{build}' -build: off -image: - - Visual Studio 2015 - - Visual Studio 2019 -environment: - matrix: - - TOXENV: check - - TOXENV: 'py36-pytest46-xdist127-coverage55,py36-pytest46-xdist133-coverage55,py36-pytest54-xdist133-coverage55,py36-pytest62-xdist202-coverage55' - - TOXENV: 'py37-pytest46-xdist127-coverage55,py37-pytest46-xdist133-coverage55,py37-pytest54-xdist133-coverage55,py37-pytest62-xdist202-coverage55' - - TOXENV: 'py38-pytest46-xdist133-coverage55,py38-pytest54-xdist133-coverage55,py38-pytest62-xdist202-coverage55' - - TOXENV: 'py39-pytest62-xdist202-coverage55' - - TOXENV: 'pypy3-pytest46-xdist127-coverage55,pypy3-pytest46-xdist133-coverage55,pypy3-pytest54-xdist133-coverage55,pypy3-pytest62-xdist202-coverage55' -matrix: - exclude: - - image: Visual Studio 2015 - TOXENV: 'py36-pytest46-xdist127-coverage55,py36-pytest46-xdist133-coverage55,py36-pytest54-xdist133-coverage55,py36-pytest62-xdist202-coverage55' - - image: Visual Studio 2015 - TOXENV: 'py37-pytest46-xdist127-coverage55,py37-pytest46-xdist133-coverage55,py37-pytest54-xdist133-coverage55,py37-pytest62-xdist202-coverage55' - - image: Visual Studio 2015 - TOXENV: 'py38-pytest46-xdist133-coverage55,py38-pytest54-xdist133-coverage55,py38-pytest62-xdist202-coverage55' - - image: Visual Studio 2015 - TOXENV: 'py39-pytest62-xdist202-coverage55' - - image: Visual Studio 2015 - TOXENV: 'pypy3-pytest46-xdist127-coverage55,pypy3-pytest46-xdist133-coverage55,pypy3-pytest54-xdist133-coverage55,pypy3-pytest62-xdist202-coverage55' -init: - - ps: echo $env:TOXENV - - ps: ls C:\Python* -install: - - 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/.cookiecutterrc b/.cookiecutterrc index 9cad1178..49e9880e 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -2,7 +2,7 @@ default_context: allow_tests_inside_package: no - appveyor: yes + appveyor: no c_extension_function: '-' c_extension_module: '-' c_extension_optional: no @@ -16,10 +16,10 @@ default_context: 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ș + github_actions: yes legacy_python: yes license: MIT license linter: flake8 @@ -29,9 +29,10 @@ default_context: 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' + release_date: '2021-10-04' repo_hosting: github.com repo_hosting_domain: github.com + repo_main_branch: master repo_name: pytest-cov repo_username: pytest-dev requiresio: yes @@ -45,9 +46,10 @@ default_context: test_matrix_configurator: no test_matrix_separate_coverage: no test_runner: pytest - travis: yes + travis: no travis_osx: no - version: 2.10.1 + version: 3.0.0 + version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' - year_to: '2020' + year_to: '2022' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9f7c11c..3ad0bb49 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,81 +1,167 @@ -name: Test - -on: [push, pull_request, workflow_dispatch] - -env: - FORCE_COLOR: 1 - +name: Tests +on: [push, pull_request] jobs: test: - runs-on: ubuntu-latest + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 strategy: fail-fast: false matrix: - python-version: ["pypy-3.6", "pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10-dev"] - tox-extra-versions: [ - "pytest46-xdist127", - "pytest46-xdist133", - "pytest54-xdist133", - "pytest62-xdist202", - ] include: - # Add new helper variables to existing jobs - - {python-version: "pypy-3.6", tox-python-version: "pypy3"} - - {python-version: "pypy-3.7", tox-python-version: "pypy3"} - - {python-version: "3.6", tox-python-version: "py36"} - - {python-version: "3.7", tox-python-version: "py37"} - - {python-version: "3.8", tox-python-version: "py38"} - - {python-version: "3.9", tox-python-version: "py39"} - - {python-version: "3.10-dev", tox-python-version: "py310"} - exclude: - # Remove some jobs from the matrix - - {tox-extra-versions: "pytest46-xdist127", python-version: "3.8"} - - {tox-extra-versions: "pytest46-xdist127", python-version: "3.9"} - - {tox-extra-versions: "pytest46-xdist133", python-version: "3.9"} - - {tox-extra-versions: "pytest54-xdist133", python-version: "3.9"} - - {tox-extra-versions: "pytest46-xdist127", python-version: "3.10-dev"} - - {tox-extra-versions: "pytest46-xdist133", python-version: "3.10-dev"} - - {tox-extra-versions: "pytest54-xdist133", python-version: "3.10-dev"} - + - name: 'check' + python: '3.9' + toxpython: 'python3.9' + tox_env: 'check' + os: 'ubuntu-latest' + - name: 'docs' + python: '3.9' + toxpython: 'python3.9' + tox_env: 'docs' + os: 'ubuntu-latest' + - name: 'py36-pytest70-xdist250-coverage62 (ubuntu)' + python: '3.6' + toxpython: 'python3.6' + python_arch: 'x64' + tox_env: 'py36-pytest70-xdist250-coverage62' + os: 'ubuntu-latest' + - name: 'py36-pytest70-xdist250-coverage62 (windows)' + python: '3.6' + toxpython: 'python3.6' + python_arch: 'x64' + tox_env: 'py36-pytest70-xdist250-coverage62' + os: 'windows-latest' + - name: 'py36-pytest70-xdist250-coverage62 (macos)' + python: '3.6' + toxpython: 'python3.6' + python_arch: 'x64' + tox_env: 'py36-pytest70-xdist250-coverage62' + os: 'macos-latest' + - name: 'py37-pytest71-xdist250-coverage64 (ubuntu)' + python: '3.7' + toxpython: 'python3.7' + python_arch: 'x64' + tox_env: 'py37-pytest71-xdist250-coverage64' + os: 'ubuntu-latest' + - name: 'py37-pytest71-xdist250-coverage64 (windows)' + python: '3.7' + toxpython: 'python3.7' + python_arch: 'x64' + tox_env: 'py37-pytest71-xdist250-coverage64' + os: 'windows-latest' + - name: 'py37-pytest71-xdist250-coverage64 (macos)' + python: '3.7' + toxpython: 'python3.7' + python_arch: 'x64' + tox_env: 'py37-pytest71-xdist250-coverage64' + os: 'macos-latest' + - name: 'py38-pytest71-xdist250-coverage64 (ubuntu)' + python: '3.8' + toxpython: 'python3.8' + python_arch: 'x64' + tox_env: 'py38-pytest71-xdist250-coverage64' + os: 'ubuntu-latest' + - name: 'py38-pytest71-xdist250-coverage64 (windows)' + python: '3.8' + toxpython: 'python3.8' + python_arch: 'x64' + tox_env: 'py38-pytest71-xdist250-coverage64' + os: 'windows-latest' + - name: 'py38-pytest71-xdist250-coverage64 (macos)' + python: '3.8' + toxpython: 'python3.8' + python_arch: 'x64' + tox_env: 'py38-pytest71-xdist250-coverage64' + os: 'macos-latest' + - name: 'py39-pytest71-xdist250-coverage64 (ubuntu)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39-pytest71-xdist250-coverage64' + os: 'ubuntu-latest' + - name: 'py39-pytest71-xdist250-coverage64 (windows)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39-pytest71-xdist250-coverage64' + os: 'windows-latest' + - name: 'py39-pytest71-xdist250-coverage64 (macos)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39-pytest71-xdist250-coverage64' + os: 'macos-latest' + - name: 'py310-pytest71-xdist250-coverage64 (ubuntu)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest71-xdist250-coverage64' + os: 'ubuntu-latest' + - name: 'py310-pytest71-xdist250-coverage64 (windows)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest71-xdist250-coverage64' + os: 'windows-latest' + - name: 'py310-pytest71-xdist250-coverage64 (macos)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest71-xdist250-coverage64' + os: 'macos-latest' + - name: 'pypy37-pytest71-xdist250-coverage64 (ubuntu)' + python: 'pypy-3.7' + toxpython: 'pypy3.7' + python_arch: 'x64' + tox_env: 'pypy37-pytest71-xdist250-coverage64' + os: 'ubuntu-latest' + - name: 'pypy37-pytest71-xdist250-coverage64 (windows)' + python: 'pypy-3.7' + toxpython: 'pypy3.7' + python_arch: 'x64' + tox_env: 'pypy37-pytest71-xdist250-coverage64' + os: 'windows-latest' + - name: 'pypy37-pytest71-xdist250-coverage64 (macos)' + python: 'pypy-3.7' + toxpython: 'pypy3.7' + python_arch: 'x64' + tox_env: 'pypy37-pytest71-xdist250-coverage64' + os: 'macos-latest' + - name: 'pypy38-pytest71-xdist250-coverage64 (ubuntu)' + python: 'pypy-3.8' + toxpython: 'pypy3.8' + python_arch: 'x64' + tox_env: 'pypy38-pytest71-xdist250-coverage64' + os: 'ubuntu-latest' + - name: 'pypy38-pytest71-xdist250-coverage64 (windows)' + python: 'pypy-3.8' + toxpython: 'pypy3.8' + python_arch: 'x64' + tox_env: 'pypy38-pytest71-xdist250-coverage64' + os: 'windows-latest' + - name: 'pypy38-pytest71-xdist250-coverage64 (macos)' + python: 'pypy-3.8' + toxpython: 'pypy3.8' + python_arch: 'x64' + tox_env: 'pypy38-pytest71-xdist250-coverage64' + os: 'macos-latest' steps: - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" - - - name: Cache - uses: actions/cache@v2 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: - test-${{ matrix.python-version }}-v1-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - test-${{ matrix.python-version }}-v1- - - - name: Install dependencies - run: | - python -m pip install -U pip - python -m pip install -U wheel - python -m pip install --progress-bar=off tox -rci/requirements.txt - virtualenv --version - pip --version - tox --version - - - name: Tox tests - run: | - tox -v -e ${{ matrix.tox-python-version }}-${{ matrix.tox-extra-versions }}-coverage55 - - allgood: - needs: test - runs-on: ubuntu-latest - name: Test successful - steps: - - name: Success - run: echo Test successful + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + architecture: ${{ matrix.python_arch }} + - name: install dependencies + run: | + python -mpip install --progress-bar=off -r ci/requirements.txt + virtualenv --version + pip --version + tox --version + pip list --format=freeze + - name: test + env: + TOXPYTHON: '${{ matrix.toxpython }}' + run: > + tox -e ${{ matrix.tox_env }} -v diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3087426b..6dbf514b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,12 +2,25 @@ Changelog ========= -3.1.0 (future) +4.0.0 (future) ------------------- +**Note that this release drops support for multiprocessing.** + + * `--cov-fail-under` no longer causes `pytest --collect-only` to fail Contributed by Zac Hatfield-Dodds in `#511 `_. +* Dropped support for multiprocessing (mostly because `issue 82408 `_). This feature was + mostly working but made out test suite very flaky and slow. + + There is builtin multiprocessing support in coverage and you can switch to that if you feel lucky. All you need is this in your + ``.coveragerc``:: + + [run] + concurrency = multiprocessing + parallel = true + sigterm = true 3.0.0 (2021-10-04) @@ -36,17 +49,6 @@ Changelog Contributed by Thomas Grainger in `#477 `_. - -2.13.0 (2021-06-01) -------------------- - -* Changed the `toml` requirement to be always be directly required (instead of being required through a coverage extra). - This fixes issues with pip-compile (`pip-tools#1300 `_). - Contributed by Sorin Sbarnea in `#472 `_. -* Documented ``show_contexts``. - Contributed by Brian Rutledge in `#473 `_. - - 2.12.1 (2021-06-01) ------------------- @@ -196,8 +198,6 @@ Changelog `#272 `_, `#271 `_ and `#269 `_. -* Improved documentation regarding subprocess and multiprocessing. - Contributed in `#265 `_. * Improved ``pytest_cov.embed.cleanup_on_sigterm`` to be reentrant (signal deliveries while signal handling is running won't break stuff). * Added ``pytest_cov.embed.cleanup_on_signal`` for customized cleanup. diff --git a/MANIFEST.in b/MANIFEST.in index af1581be..cbb88f74 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -12,16 +12,17 @@ graft ci graft tests include .bumpversion.cfg -include .coveragerc include .cookiecutterrc +include .coveragerc include .editorconfig - +include tox.ini +include .readthedocs.yml +include .pre-commit-config.yaml include AUTHORS.rst include CHANGELOG.rst include CONTRIBUTING.rst include LICENSE include README.rst -include tox.ini .appveyor.yml .readthedocs.yml .pre-commit-config.yaml -global-exclude *.py[cod] __pycache__/* *.so *.dylib .coverage .coverage.* +global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/README.rst b/README.rst index 508aff84..2137c118 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,8 @@ Overview * - docs - |docs| * - tests - - | |github-actions| |appveyor| |requires| + - | |github-actions| |requires| + | * - package - | |version| |conda-forge| |wheel| |supported-versions| |supported-implementations| | |commits-since| diff --git a/ci/appveyor-with-compiler.cmd b/ci/appveyor-with-compiler.cmd deleted file mode 100644 index 289585fc..00000000 --- a/ci/appveyor-with-compiler.cmd +++ /dev/null @@ -1,23 +0,0 @@ -:: Very simple setup: -:: - if WINDOWS_SDK_VERSION is set then activate the SDK. -:: - disable the WDK if it's around. - -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows -SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" -ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% - -IF EXIST %WIN_WDK% ( - REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ - REN %WIN_WDK% 0wdf -) -IF "%WINDOWS_SDK_VERSION%"=="" GOTO main - -SET DISTUTILS_USE_SDK=1 -SET MSSdk=1 -"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% -CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - -:main -ECHO Executing: %COMMAND_TO_RUN% -CALL %COMMAND_TO_RUN% || EXIT 1 diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 77daad5b..b0977495 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -3,13 +3,14 @@ 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 +from os.path import relpath base_path = dirname(dirname(abspath(__file__))) +templates_path = join(base_path, "ci", "templates") def check_call(args): @@ -51,7 +52,7 @@ def main(): print(f"Project path: {base_path}") jinja = jinja2.Environment( - loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), + loader=jinja2.FileSystemLoader(templates_path), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True @@ -59,22 +60,21 @@ def main(): tox_environments = [ line.strip() - # WARNING: 'tox' must be installed globally or in the project's virtualenv - for line in subprocess.check_output(['tox', '--listenvs'], universal_newlines=True).splitlines() + # 'tox' need not be installed globally, but must be importable + # by the Python that is running this script. + # This uses sys.executable the same way that the call in + # cookiecutter-pylibrary/hooks/post_gen_project.py + # invokes this bootstrap.py itself. + for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], 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(f"Wrote {name}") + tox_environments = [line for line in tox_environments if line.startswith('py')] + + for root, _, files in os.walk(templates_path): + for name in files: + relative = relpath(root, templates_path) + with open(join(base_path, relative, name), "w") as fh: + fh.write(jinja.get_template(join(relative, name)).render(tox_environments=tox_environments)) + print(f"Wrote {name}") print("DONE.") diff --git a/ci/requirements.txt b/ci/requirements.txt index d7f5177e..a0ef106f 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -2,3 +2,4 @@ virtualenv>=16.6.0 pip>=19.1.1 setuptools>=18.0.1 six>=1.14.0 +tox diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml deleted file mode 100644 index 2b7e611c..00000000 --- a/ci/templates/.appveyor.yml +++ /dev/null @@ -1,53 +0,0 @@ -version: '{branch}-{build}' -build: off -image: - - Visual Studio 2015 - - Visual Studio 2019 -environment: - matrix: - - TOXENV: check - - TOXENV: '{{ py27_environments|join(",") }}' - - TOXENV: '{{ py35_environments|join(",") }}' - - TOXENV: '{{ py36_environments|join(",") }}' - - TOXENV: '{{ py37_environments|join(",") }}' - - TOXENV: '{{ py38_environments|join(",") }}' - - TOXENV: '{{ py39_environments|join(",") }}' - - TOXENV: '{{ pypy_environments|join(",") }}' - - TOXENV: '{{ pypy3_environments|join(",") }}' -matrix: - exclude: - - image: Visual Studio 2019 - TOXENV: '{{ py27_environments|join(",") }}' - - image: Visual Studio 2015 - TOXENV: '{{ py36_environments|join(",") }}' - - image: Visual Studio 2015 - TOXENV: '{{ py37_environments|join(",") }}' - - image: Visual Studio 2015 - TOXENV: '{{ py38_environments|join(",") }}' - - image: Visual Studio 2015 - TOXENV: '{{ py39_environments|join(",") }}' - - image: Visual Studio 2015 - TOXENV: '{{ pypy_environments|join(",") }}' - - image: Visual Studio 2015 - TOXENV: '{{ pypy3_environments|join(",") }}' -init: - - ps: echo $env:TOXENV - - ps: ls C:\Python* -install: - - IF "%TOXENV:~0,5%" == "pypy-" choco install --no-progress python.pypy - - IF "%TOXENV:~0,6%" == "pypy3-" choco install --no-progress pypy3 - - SET PATH=C:\tools\pypy\pypy;%PATH% - - C:\Python37\python -m pip install --progress-bar=off tox -rci/requirements.txt - -test_script: - - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd C:\Python37\python -m tox - -on_failure: - - ps: dir "env:" - - ps: get-content .tox\*\log\* -artifacts: - - path: dist\* - -### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): -# on_finish: -# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/ci/templates/.github/workflows/test.yml b/ci/templates/.github/workflows/test.yml new file mode 100644 index 00000000..fda6886a --- /dev/null +++ b/ci/templates/.github/workflows/test.yml @@ -0,0 +1,65 @@ +name: Tests +on: [push, pull_request] +jobs: + test: + name: {{ '${{ matrix.name }}' }} + runs-on: {{ '${{ matrix.os }}' }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: 'check' + python: '3.9' + toxpython: 'python3.9' + tox_env: 'check' + os: 'ubuntu-latest' + - name: 'docs' + python: '3.9' + toxpython: 'python3.9' + tox_env: 'docs' + os: 'ubuntu-latest' +{% for env in tox_environments %} +{% set prefix = env.split('-')[0] -%} +{% if prefix.startswith('pypy') %} +{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% set cpython %}pp{{ prefix[4:5] }}{% endset %} +{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% else %} +{% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} +{% set cpython %}cp{{ prefix[2:] }}{% endset %} +{% set toxpython %}python{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} +{% endif %} +{% for os, python_arch in [ + ['ubuntu', 'x64'], + ['windows', 'x64'], + ['macos', 'x64'], +] %} + - name: '{{ env }} ({{ os }})' + python: '{{ python }}' + toxpython: '{{ toxpython }}' + python_arch: '{{ python_arch }}' + tox_env: '{{ env }}' + os: '{{ os }}-latest' +{% endfor %} +{% endfor %} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-python@v2 + with: + python-version: {{ '${{ matrix.python }}' }} + architecture: {{ '${{ matrix.python_arch }}' }} + - name: install dependencies + run: | + python -mpip install --progress-bar=off -r ci/requirements.txt + virtualenv --version + pip --version + tox --version + pip list --format=freeze + - name: test + env: + TOXPYTHON: '{{ '${{ matrix.toxpython }}' }}' + run: > + tox -e {{ '${{ matrix.tox_env }}' }} -v diff --git a/docs/requirements.txt b/docs/requirements.txt index ccec79fd..6fdf26f9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ sphinx==3.0.3 sphinx-py3doc-enhanced-theme==2.4.0 docutils==0.16 +jinja2<3.1 -e . diff --git a/setup.py b/setup.py index 0748d12e..21681887 100755 --- a/setup.py +++ b/setup.py @@ -115,6 +115,11 @@ def run(self): 'Topic :: Software Development :: Testing', 'Topic :: Utilities', ], + project_urls={ + 'Documentation': 'https://pytest-cov.readthedocs.io/', + 'Changelog': 'https://pytest-cov.readthedocs.io/en/latest/changelog.html', + 'Issue Tracker': 'https://github.com/pytest-dev/pytest-cov/issues', + }, keywords=[ 'cover', 'coverage', 'pytest', 'py.test', 'distributed', 'parallel', ], diff --git a/src/pytest-cov.pth b/src/pytest-cov.pth index 91f2b7c7..8ed1a516 100644 --- a/src/pytest-cov.pth +++ b/src/pytest-cov.pth @@ -1 +1 @@ -import os, sys;exec('if \'COV_CORE_SOURCE\' in os.environ:\n try:\n from pytest_cov.embed import init\n init()\n except Exception as exc:\n sys.stderr.write(\n "pytest-cov: Failed to setup subprocess coverage. "\n "Environ: {0!r} "\n "Exception: {1!r}\\n".format(\n dict((k, v) for k, v in os.environ.items() if k.startswith(\'COV_CORE\')),\n exc\n )\n )\n') \ No newline at end of file +import os, sys;exec('if \'COV_CORE_SOURCE\' in os.environ:\n try:\n from pytest_cov.embed import init\n init()\n except Exception as exc:\n sys.stderr.write(\n "pytest-cov: Failed to setup subprocess coverage. "\n "Environ: {0!r} "\n "Exception: {1!r}\\n".format(\n dict((k, v) for k, v in os.environ.items() if k.startswith(\'COV_CORE\')),\n exc\n )\n )\n') diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index fe0575e9..f8a2749f 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -20,22 +20,6 @@ _active_cov = None -def multiprocessing_start(_): - global _active_cov - cov = init() - if cov: - _active_cov = cov - multiprocessing.util.Finalize(None, cleanup, exitpriority=1000) - - -try: - import multiprocessing.util -except ImportError: - pass -else: - multiprocessing.util.register_after_fork(multiprocessing_start, multiprocessing_start) - - def init(): # Only continue if ancestor process has set everything needed in # the env. @@ -105,8 +89,6 @@ def cleanup(): _signal_cleanup_handler(*pending_signal) -multiprocessing_finish = cleanup # in case someone dared to use this internal - _previous_handlers = {} _pending_signal = None _cleanup_in_progress = False diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 6d979495..4402fc7b 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -194,7 +194,7 @@ def prop(request): ) -def test_central(testdir, prop): +def test_central(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) @@ -461,7 +461,7 @@ def test_cov_min_no_report(testdir): ]) -def test_central_nonspecific(testdir, prop): +def test_central_nonspecific(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', @@ -496,7 +496,7 @@ def test_cov_min_from_coveragerc(testdir): assert result.ret != 0 -def test_central_coveragerc(testdir, prop): +def test_central_coveragerc(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(COVERAGERC_SOURCE + prop.conf) @@ -514,7 +514,7 @@ def test_central_coveragerc(testdir, prop): @xdist_params -def test_central_with_path_aliasing(testdir, monkeypatch, opts, prop): +def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): mod1 = testdir.mkdir('src').join('mod.py') mod1.write(SCRIPT) mod2 = testdir.mkdir('aliased').join('mod.py') @@ -548,7 +548,7 @@ def test_central_with_path_aliasing(testdir, monkeypatch, opts, prop): @xdist_params -def test_borken_cwd(testdir, monkeypatch, opts): +def test_borken_cwd(pytester, testdir, monkeypatch, opts): testdir.makepyfile(mod=''' def foobar(a, b): return a + b @@ -587,7 +587,7 @@ def test_foobar(bad): assert result.ret == 0 -def test_subprocess_with_path_aliasing(testdir, monkeypatch): +def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): src = testdir.mkdir('src') src.join('parent_script.py').write(SCRIPT_PARENT) src.join('child_script.py').write(SCRIPT_CHILD) @@ -623,7 +623,7 @@ def test_subprocess_with_path_aliasing(testdir, monkeypatch): assert result.ret == 0 -def test_show_missing_coveragerc(testdir, prop): +def test_show_missing_coveragerc(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(""" [run] @@ -666,7 +666,7 @@ def test_fail(): result.stdout.fnmatch_lines(['*1 failed*']) -def test_no_cov(testdir, monkeypatch): +def test_no_cov(pytester, testdir, monkeypatch): script = testdir.makepyfile(SCRIPT) testdir.makeini(""" [pytest] @@ -733,7 +733,7 @@ def test_foo(foo): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -def test_dist_collocated(testdir, prop): +def test_dist_collocated(pytester, testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', @@ -753,7 +753,7 @@ def test_dist_collocated(testdir, prop): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -def test_dist_not_collocated(testdir, prop): +def test_dist_not_collocated(pytester, testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') dir2 = testdir.mkdir('dir2') @@ -786,7 +786,7 @@ def test_dist_not_collocated(testdir, prop): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -def test_dist_not_collocated_coveragerc_source(testdir, prop): +def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') dir2 = testdir.mkdir('dir2') @@ -861,7 +861,7 @@ def test_central_subprocess_change_cwd(testdir): assert result.ret == 0 -def test_central_subprocess_change_cwd_with_pythonpath(testdir, monkeypatch): +def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkeypatch): stuff = testdir.mkdir('stuff') parent_script = stuff.join('parent_script.py') parent_script.write(SCRIPT_PARENT_CHANGE_CWD_IMPORT_CHILD) @@ -932,7 +932,7 @@ def test_dist_subprocess_collocated(testdir): @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') -def test_dist_subprocess_not_collocated(testdir, tmpdir): +def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) parent_script = scripts.dirpath().join('parent_script.py') @@ -994,6 +994,8 @@ def test_invalid_coverage_source(testdir): @pytest.mark.skipif("'dev' in pytest.__version__") @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +@pytest.mark.skipif('tuple(map(int, xdist.__version__.split("."))) >= (2, 3, 0)', + reason="Since pytest-xdist 2.3.0 the parent sys.path is copied in the child process") def test_dist_missing_data(testdir): """Test failure when using a worker without pytest-cov installed.""" venv_path = os.path.join(str(testdir.tmpdir), 'venv') @@ -1023,7 +1025,7 @@ def test_dist_missing_data(testdir): '--dist=load', '--tx=popen//python=%s' % exe, max_worker_restart_0, - script) + str(script)) result.stdout.fnmatch_lines([ 'The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.' ]) @@ -1057,231 +1059,8 @@ def test_funcarg_not_active(testdir): 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") +@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason="Interpreter seems buggy") def test_cleanup_on_sigterm(testdir): script = testdir.makepyfile(''' import os, signal, subprocess, sys, time @@ -1332,7 +1111,7 @@ def test_run(): ('cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), ('cleanup()', '73% 19-22'), ]) -def test_cleanup_on_sigterm_sig_break(testdir, setup): +def test_cleanup_on_sigterm_sig_break(pytester, testdir, setup): # worth a read: https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/ script = testdir.makepyfile(''' import os, signal, subprocess, sys, time @@ -1373,12 +1152,14 @@ def test_run(): @pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") +@pytest.mark.xfail('sys.platform == "darwin"', reason="Something weird going on Macs...") +@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason="Interpreter seems buggy") @pytest.mark.parametrize('setup', [ ('signal.signal(signal.SIGTERM, signal.SIG_DFL); cleanup_on_sigterm()', '88% 18-19'), ('cleanup_on_sigterm()', '88% 18-19'), ('cleanup()', '75% 16-19'), ]) -def test_cleanup_on_sigterm_sig_dfl(testdir, setup): +def test_cleanup_on_sigterm_sig_dfl(pytester, testdir, setup): script = testdir.makepyfile(''' import os, signal, subprocess, sys, time @@ -1416,6 +1197,8 @@ def test_run(): @pytest.mark.skipif('sys.platform == "win32"', reason="SIGINT is subtly broken on Windows") +@pytest.mark.xfail('sys.platform == "darwin"', reason="Something weird going on Macs...") +@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason="Interpreter seems buggy") def test_cleanup_on_sigterm_sig_dfl_sigint(testdir): script = testdir.makepyfile(''' import os, signal, subprocess, sys, time @@ -1455,6 +1238,7 @@ def test_run(): @pytest.mark.skipif('sys.platform == "win32"', reason="fork not available on Windows") +@pytest.mark.xfail('platform.python_implementation() == "PyPy"', reason="Interpreter seems buggy") def test_cleanup_on_sigterm_sig_ign(testdir): script = testdir.makepyfile(''' import os, signal, subprocess, sys, time @@ -1620,7 +1404,6 @@ def test_basic(no_cover): # Regexes for lines to exclude from consideration exclude_lines = raise NotImplementedError - ''' EXCLUDED_TEST = ''' @@ -1687,7 +1470,7 @@ def test_basic(): @pytest.mark.parametrize('report_option', [ 'term-missing:skip-covered', 'term:skip-covered']) -def test_skip_covered_cli(testdir, report_option): +def test_skip_covered_cli(pytester, testdir, report_option): testdir.makefile('', coveragerc=SKIP_COVERED_COVERAGERC) script = testdir.makepyfile(SKIP_COVERED_TEST) result = testdir.runpytest('-v', @@ -1781,6 +1564,7 @@ def test_not_started_plugin_does_not_fail(testdir): class ns: cov_source = [True] cov_report = '' + plugin = pytest_cov.plugin.CovPlugin(ns, None, start=False) plugin.pytest_runtestloop(None) plugin.pytest_terminal_summary(None) @@ -1892,13 +1676,13 @@ def test_external_data_file_negative(testdir): @xdist_params -def test_append_coverage(testdir, opts, prop): +def test_append_coverage(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), script, - *opts.split()+prop.args) + *opts.split() + prop.args) result.stdout.fnmatch_lines([ 'test_1* %s*' % prop.result, ]) @@ -1907,7 +1691,7 @@ def test_append_coverage(testdir, opts, prop): '--cov-append', '--cov=%s' % script2.dirpath(), script2, - *opts.split()+prop.args) + *opts.split() + prop.args) result.stdout.fnmatch_lines([ 'test_1* %s*' % prop.result, 'test_2* %s*' % prop.result2, @@ -1915,7 +1699,7 @@ def test_append_coverage(testdir, opts, prop): @xdist_params -def test_do_not_append_coverage(testdir, opts, prop): +def test_do_not_append_coverage(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', @@ -2097,7 +1881,7 @@ def find_labels(text, pattern): @pytest.mark.skipif("coverage.version_info < (5, 0)") @xdist_params -def test_contexts(testdir, opts): +def test_contexts(pytester, testdir, opts): with open(os.path.join(os.path.dirname(__file__), "contextful.py")) as f: contextful_tests = f.read() script = testdir.makepyfile(contextful_tests) diff --git a/tox.ini b/tox.ini index 20a1561b..282cd244 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [testenv:bootstrap] deps = jinja2 - matrix tox skip_install = true commands = @@ -13,9 +12,8 @@ passenv = [tox] envlist = check - py{36,37,py,py3}-pytest46-xdist127-coverage{55} - py{36,37,38,py3}-pytest{46,54}-xdist133-coverage{55} - py{36,37,38,39,310,py3}-pytest{62}-xdist202-coverage{55} + py{36}-pytest{70}-xdist250-coverage{62} + py{37,38,39,310,py37,py38}-pytest{71}-xdist250-coverage{64} docs [testenv] @@ -30,6 +28,8 @@ setenv = pytest60: _DEP_PYTEST=pytest==6.0.2 pytest61: _DEP_PYTEST=pytest==6.1.2 pytest62: _DEP_PYTEST=pytest==6.2.5 + pytest70: _DEP_PYTEST=pytest==7.0.1 + pytest71: _DEP_PYTEST=pytest==7.1.2 xdist127: _DEP_PYTESTXDIST=pytest-xdist==1.27.0 xdist129: _DEP_PYTESTXDIST=pytest-xdist==1.29.0 @@ -40,6 +40,7 @@ setenv = xdist200: _DEP_PYTESTXDIST=pytest-xdist==2.0.0 xdist201: _DEP_PYTESTXDIST=pytest-xdist==2.1.0 xdist202: _DEP_PYTESTXDIST=pytest-xdist==2.2.0 + xdist250: _DEP_PYTESTXDIST=pytest-xdist==2.5.0 xdistdev: _DEP_PYTESTXDIST=git+https://github.com/pytest-dev/pytest-xdist.git#egg=pytest-xdist coverage45: _DEP_COVERAGE=coverage==4.5.4 @@ -49,6 +50,11 @@ setenv = coverage53: _DEP_COVERAGE=coverage==5.3.1 coverage54: _DEP_COVERAGE=coverage==5.4 coverage55: _DEP_COVERAGE=coverage==5.5 + coverage60: _DEP_COVERAGE=coverage==6.0.2 + coverage61: _DEP_COVERAGE=coverage==6.1.2 + coverage62: _DEP_COVERAGE=coverage==6.2 + coverage63: _DEP_COVERAGE=coverage==6.3.3 + coverage64: _DEP_COVERAGE=coverage==6.4.2 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv = @@ -59,7 +65,7 @@ deps = {env:_DEP_COVERAGE:coverage} pip_pre = true commands = - pytest {posargs:-vv} + {posargs:pytest -vv} [testenv:spell] setenv =