diff --git a/.bumpversion.cfg b/.bumpversion.cfg index cca1163f..d2649188 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 5.0.0 +current_version = 6.2.1 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 18487fef..324a99de 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -40,7 +40,7 @@ default_context: sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme test_matrix_separate_coverage: 'no' - version: 5.0.0 + version: 6.2.1 version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2010' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84753edb..0a81ed49 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,149 +60,509 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py38-pytest81-xdist350-coverage74 (ubuntu)' - python: '3.8' - toxpython: 'python3.8' + - name: 'py39-pytest83-xdist36-coverage78 (ubuntu)' + python: '3.9' + toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py38-pytest81-xdist350-coverage74' + tox_env: 'py39-pytest83-xdist36-coverage78' os: 'ubuntu-latest' - - name: 'py38-pytest81-xdist350-coverage74 (windows)' - python: '3.8' - toxpython: 'python3.8' + - name: 'py39-pytest83-xdist36-coverage78 (windows)' + python: '3.9' + toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py38-pytest81-xdist350-coverage74' + tox_env: 'py39-pytest83-xdist36-coverage78' os: 'windows-latest' - - name: 'py38-pytest81-xdist350-coverage74 (macos)' - python: '3.8' - toxpython: 'python3.8' + - name: 'py39-pytest83-xdist36-coverage78 (macos)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'arm64' + tox_env: 'py39-pytest83-xdist36-coverage78' + os: 'macos-latest' + - name: 'py39-pytest83-xdist37-coverage78 (ubuntu)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39-pytest83-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py39-pytest83-xdist37-coverage78 (windows)' + python: '3.9' + toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py38-pytest81-xdist350-coverage74' + tox_env: 'py39-pytest83-xdist37-coverage78' + os: 'windows-latest' + - name: 'py39-pytest83-xdist37-coverage78 (macos)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'arm64' + tox_env: 'py39-pytest83-xdist37-coverage78' + os: 'macos-latest' + - name: 'py39-pytest84-xdist36-coverage78 (ubuntu)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39-pytest84-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'py39-pytest84-xdist36-coverage78 (windows)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39-pytest84-xdist36-coverage78' + os: 'windows-latest' + - name: 'py39-pytest84-xdist36-coverage78 (macos)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'arm64' + tox_env: 'py39-pytest84-xdist36-coverage78' os: 'macos-latest' - - name: 'py39-pytest81-xdist350-coverage74 (ubuntu)' + - name: 'py39-pytest84-xdist37-coverage78 (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest81-xdist350-coverage74' + tox_env: 'py39-pytest84-xdist37-coverage78' os: 'ubuntu-latest' - - name: 'py39-pytest81-xdist350-coverage74 (windows)' + - name: 'py39-pytest84-xdist37-coverage78 (windows)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' - tox_env: 'py39-pytest81-xdist350-coverage74' + tox_env: 'py39-pytest84-xdist37-coverage78' os: 'windows-latest' - - name: 'py39-pytest81-xdist350-coverage74 (macos)' + - name: 'py39-pytest84-xdist37-coverage78 (macos)' python: '3.9' toxpython: 'python3.9' + python_arch: 'arm64' + tox_env: 'py39-pytest84-xdist37-coverage78' + os: 'macos-latest' + - name: 'py310-pytest83-xdist36-coverage78 (ubuntu)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest83-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'py310-pytest83-xdist36-coverage78 (windows)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest83-xdist36-coverage78' + os: 'windows-latest' + - name: 'py310-pytest83-xdist36-coverage78 (macos)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'arm64' + tox_env: 'py310-pytest83-xdist36-coverage78' + os: 'macos-latest' + - name: 'py310-pytest83-xdist37-coverage78 (ubuntu)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest83-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py310-pytest83-xdist37-coverage78 (windows)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest83-xdist37-coverage78' + os: 'windows-latest' + - name: 'py310-pytest83-xdist37-coverage78 (macos)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'arm64' + tox_env: 'py310-pytest83-xdist37-coverage78' + os: 'macos-latest' + - name: 'py310-pytest84-xdist36-coverage78 (ubuntu)' + python: '3.10' + toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py39-pytest81-xdist350-coverage74' + tox_env: 'py310-pytest84-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'py310-pytest84-xdist36-coverage78 (windows)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310-pytest84-xdist36-coverage78' + os: 'windows-latest' + - name: 'py310-pytest84-xdist36-coverage78 (macos)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'arm64' + tox_env: 'py310-pytest84-xdist36-coverage78' os: 'macos-latest' - - name: 'py310-pytest81-xdist350-coverage74 (ubuntu)' + - name: 'py310-pytest84-xdist37-coverage78 (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest81-xdist350-coverage74' + tox_env: 'py310-pytest84-xdist37-coverage78' os: 'ubuntu-latest' - - name: 'py310-pytest81-xdist350-coverage74 (windows)' + - name: 'py310-pytest84-xdist37-coverage78 (windows)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' - tox_env: 'py310-pytest81-xdist350-coverage74' + tox_env: 'py310-pytest84-xdist37-coverage78' os: 'windows-latest' - - name: 'py310-pytest81-xdist350-coverage74 (macos)' + - name: 'py310-pytest84-xdist37-coverage78 (macos)' python: '3.10' toxpython: 'python3.10' + python_arch: 'arm64' + tox_env: 'py310-pytest84-xdist37-coverage78' + os: 'macos-latest' + - name: 'py311-pytest83-xdist36-coverage78 (ubuntu)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest83-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'py311-pytest83-xdist36-coverage78 (windows)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest83-xdist36-coverage78' + os: 'windows-latest' + - name: 'py311-pytest83-xdist36-coverage78 (macos)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'arm64' + tox_env: 'py311-pytest83-xdist36-coverage78' + os: 'macos-latest' + - name: 'py311-pytest83-xdist37-coverage78 (ubuntu)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest83-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py311-pytest83-xdist37-coverage78 (windows)' + python: '3.11' + toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py310-pytest81-xdist350-coverage74' + tox_env: 'py311-pytest83-xdist37-coverage78' + os: 'windows-latest' + - name: 'py311-pytest83-xdist37-coverage78 (macos)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'arm64' + tox_env: 'py311-pytest83-xdist37-coverage78' os: 'macos-latest' - - name: 'py311-pytest81-xdist350-coverage74 (ubuntu)' + - name: 'py311-pytest84-xdist36-coverage78 (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest81-xdist350-coverage74' + tox_env: 'py311-pytest84-xdist36-coverage78' os: 'ubuntu-latest' - - name: 'py311-pytest81-xdist350-coverage74 (windows)' + - name: 'py311-pytest84-xdist36-coverage78 (windows)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' - tox_env: 'py311-pytest81-xdist350-coverage74' + tox_env: 'py311-pytest84-xdist36-coverage78' os: 'windows-latest' - - name: 'py311-pytest81-xdist350-coverage74 (macos)' + - name: 'py311-pytest84-xdist36-coverage78 (macos)' python: '3.11' toxpython: 'python3.11' + python_arch: 'arm64' + tox_env: 'py311-pytest84-xdist36-coverage78' + os: 'macos-latest' + - name: 'py311-pytest84-xdist37-coverage78 (ubuntu)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest84-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py311-pytest84-xdist37-coverage78 (windows)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311-pytest84-xdist37-coverage78' + os: 'windows-latest' + - name: 'py311-pytest84-xdist37-coverage78 (macos)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'arm64' + tox_env: 'py311-pytest84-xdist37-coverage78' + os: 'macos-latest' + - name: 'py312-pytest83-xdist36-coverage78 (ubuntu)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'x64' + tox_env: 'py312-pytest83-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'py312-pytest83-xdist36-coverage78 (windows)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'x64' + tox_env: 'py312-pytest83-xdist36-coverage78' + os: 'windows-latest' + - name: 'py312-pytest83-xdist36-coverage78 (macos)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'arm64' + tox_env: 'py312-pytest83-xdist36-coverage78' + os: 'macos-latest' + - name: 'py312-pytest83-xdist37-coverage78 (ubuntu)' + python: '3.12' + toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py311-pytest81-xdist350-coverage74' + tox_env: 'py312-pytest83-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py312-pytest83-xdist37-coverage78 (windows)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'x64' + tox_env: 'py312-pytest83-xdist37-coverage78' + os: 'windows-latest' + - name: 'py312-pytest83-xdist37-coverage78 (macos)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'arm64' + tox_env: 'py312-pytest83-xdist37-coverage78' os: 'macos-latest' - - name: 'py312-pytest81-xdist350-coverage74 (ubuntu)' + - name: 'py312-pytest84-xdist36-coverage78 (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest81-xdist350-coverage74' + tox_env: 'py312-pytest84-xdist36-coverage78' os: 'ubuntu-latest' - - name: 'py312-pytest81-xdist350-coverage74 (windows)' + - name: 'py312-pytest84-xdist36-coverage78 (windows)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'py312-pytest81-xdist350-coverage74' + tox_env: 'py312-pytest84-xdist36-coverage78' os: 'windows-latest' - - name: 'py312-pytest81-xdist350-coverage74 (macos)' + - name: 'py312-pytest84-xdist36-coverage78 (macos)' python: '3.12' toxpython: 'python3.12' + python_arch: 'arm64' + tox_env: 'py312-pytest84-xdist36-coverage78' + os: 'macos-latest' + - name: 'py312-pytest84-xdist37-coverage78 (ubuntu)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'x64' + tox_env: 'py312-pytest84-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py312-pytest84-xdist37-coverage78 (windows)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'x64' + tox_env: 'py312-pytest84-xdist37-coverage78' + os: 'windows-latest' + - name: 'py312-pytest84-xdist37-coverage78 (macos)' + python: '3.12' + toxpython: 'python3.12' + python_arch: 'arm64' + tox_env: 'py312-pytest84-xdist37-coverage78' + os: 'macos-latest' + - name: 'py313-pytest83-xdist36-coverage78 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest83-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'py313-pytest83-xdist36-coverage78 (windows)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest83-xdist36-coverage78' + os: 'windows-latest' + - name: 'py313-pytest83-xdist36-coverage78 (macos)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'arm64' + tox_env: 'py313-pytest83-xdist36-coverage78' + os: 'macos-latest' + - name: 'py313-pytest83-xdist37-coverage78 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest83-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py313-pytest83-xdist37-coverage78 (windows)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest83-xdist37-coverage78' + os: 'windows-latest' + - name: 'py313-pytest83-xdist37-coverage78 (macos)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'arm64' + tox_env: 'py313-pytest83-xdist37-coverage78' + os: 'macos-latest' + - name: 'py313-pytest84-xdist36-coverage78 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest84-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'py313-pytest84-xdist36-coverage78 (windows)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest84-xdist36-coverage78' + os: 'windows-latest' + - name: 'py313-pytest84-xdist36-coverage78 (macos)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'arm64' + tox_env: 'py313-pytest84-xdist36-coverage78' + os: 'macos-latest' + - name: 'py313-pytest84-xdist37-coverage78 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest84-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'py313-pytest84-xdist37-coverage78 (windows)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'x64' + tox_env: 'py313-pytest84-xdist37-coverage78' + os: 'windows-latest' + - name: 'py313-pytest84-xdist37-coverage78 (macos)' + python: '3.13' + toxpython: 'python3.13' + python_arch: 'arm64' + tox_env: 'py313-pytest84-xdist37-coverage78' + os: 'macos-latest' + - name: 'pypy39-pytest83-xdist36-coverage78 (ubuntu)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39-pytest83-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'pypy39-pytest83-xdist36-coverage78 (windows)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'py312-pytest81-xdist350-coverage74' + tox_env: 'pypy39-pytest83-xdist36-coverage78' + os: 'windows-latest' + - name: 'pypy39-pytest83-xdist36-coverage78 (macos)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'arm64' + tox_env: 'pypy39-pytest83-xdist36-coverage78' os: 'macos-latest' - - name: 'pypy38-pytest81-xdist350-coverage74 (ubuntu)' - python: 'pypy-3.8' - toxpython: 'pypy3.8' + - name: 'pypy39-pytest83-xdist37-coverage78 (ubuntu)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy38-pytest81-xdist350-coverage74' + tox_env: 'pypy39-pytest83-xdist37-coverage78' os: 'ubuntu-latest' - - name: 'pypy38-pytest81-xdist350-coverage74 (windows)' - python: 'pypy-3.8' - toxpython: 'pypy3.8' + - name: 'pypy39-pytest83-xdist37-coverage78 (windows)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy38-pytest81-xdist350-coverage74' + tox_env: 'pypy39-pytest83-xdist37-coverage78' os: 'windows-latest' - - name: 'pypy38-pytest81-xdist350-coverage74 (macos)' - python: 'pypy-3.8' - toxpython: 'pypy3.8' + - name: 'pypy39-pytest83-xdist37-coverage78 (macos)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'arm64' + tox_env: 'pypy39-pytest83-xdist37-coverage78' + os: 'macos-latest' + - name: 'pypy39-pytest84-xdist36-coverage78 (ubuntu)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy38-pytest81-xdist350-coverage74' + tox_env: 'pypy39-pytest84-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'pypy39-pytest84-xdist36-coverage78 (windows)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39-pytest84-xdist36-coverage78' + os: 'windows-latest' + - name: 'pypy39-pytest84-xdist36-coverage78 (macos)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'arm64' + tox_env: 'pypy39-pytest84-xdist36-coverage78' os: 'macos-latest' - - name: 'pypy39-pytest81-xdist350-coverage74 (ubuntu)' + - name: 'pypy39-pytest84-xdist37-coverage78 (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest81-xdist350-coverage74' + tox_env: 'pypy39-pytest84-xdist37-coverage78' os: 'ubuntu-latest' - - name: 'pypy39-pytest81-xdist350-coverage74 (windows)' + - name: 'pypy39-pytest84-xdist37-coverage78 (windows)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' - tox_env: 'pypy39-pytest81-xdist350-coverage74' + tox_env: 'pypy39-pytest84-xdist37-coverage78' os: 'windows-latest' - - name: 'pypy39-pytest81-xdist350-coverage74 (macos)' + - name: 'pypy39-pytest84-xdist37-coverage78 (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' + python_arch: 'arm64' + tox_env: 'pypy39-pytest84-xdist37-coverage78' + os: 'macos-latest' + - name: 'pypy310-pytest83-xdist36-coverage78 (ubuntu)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest83-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'pypy310-pytest83-xdist36-coverage78 (windows)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy39-pytest81-xdist350-coverage74' + tox_env: 'pypy310-pytest83-xdist36-coverage78' + os: 'windows-latest' + - name: 'pypy310-pytest83-xdist36-coverage78 (macos)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'arm64' + tox_env: 'pypy310-pytest83-xdist36-coverage78' os: 'macos-latest' - - name: 'pypy310-pytest81-xdist350-coverage74 (ubuntu)' + - name: 'pypy310-pytest83-xdist37-coverage78 (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest81-xdist350-coverage74' + tox_env: 'pypy310-pytest83-xdist37-coverage78' os: 'ubuntu-latest' - - name: 'pypy310-pytest81-xdist350-coverage74 (windows)' + - name: 'pypy310-pytest83-xdist37-coverage78 (windows)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest81-xdist350-coverage74' + tox_env: 'pypy310-pytest83-xdist37-coverage78' os: 'windows-latest' - - name: 'pypy310-pytest81-xdist350-coverage74 (macos)' + - name: 'pypy310-pytest83-xdist37-coverage78 (macos)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'arm64' + tox_env: 'pypy310-pytest83-xdist37-coverage78' + os: 'macos-latest' + - name: 'pypy310-pytest84-xdist36-coverage78 (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' - tox_env: 'pypy310-pytest81-xdist350-coverage74' + tox_env: 'pypy310-pytest84-xdist36-coverage78' + os: 'ubuntu-latest' + - name: 'pypy310-pytest84-xdist36-coverage78 (windows)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest84-xdist36-coverage78' + os: 'windows-latest' + - name: 'pypy310-pytest84-xdist36-coverage78 (macos)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'arm64' + tox_env: 'pypy310-pytest84-xdist36-coverage78' + os: 'macos-latest' + - name: 'pypy310-pytest84-xdist37-coverage78 (ubuntu)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest84-xdist37-coverage78' + os: 'ubuntu-latest' + - name: 'pypy310-pytest84-xdist37-coverage78 (windows)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'x64' + tox_env: 'pypy310-pytest84-xdist37-coverage78' + os: 'windows-latest' + - name: 'pypy310-pytest84-xdist37-coverage78 (macos)' + python: 'pypy-3.10' + toxpython: 'pypy3.10' + python_arch: 'arm64' + tox_env: 'pypy310-pytest84-xdist37-coverage78' os: 'macos-latest' steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a9d1217..1d2f108a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,16 +6,13 @@ exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' # Note the order is intentional to avoid multiple passes of the hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.3 + rev: v0.11.13 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] - - repo: https://github.com/psf/black - rev: 24.3.0 - hooks: - - id: black + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/AUTHORS.rst b/AUTHORS.rst index 44e92874..5a2dc63d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,14 +2,14 @@ Authors ======= -* Marc Schlaich - \http://www.schlamar.org +* Marc Schlaich - https://github.com/schlamar (\http://www.schlamar.org) * Rick van Hattem - http://wol.ph * Buck Evan - https://github.com/bukzor * Eric Larson - http://larsoner.com * Marc Abramowitz - \http://marc-abramowitz.com * Thomas Kluyver - https://github.com/takluyver * Guillaume Ayoub - http://www.yabz.fr -* Federico Ceratto - http://firelet.net +* 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 @@ -19,7 +19,7 @@ Authors * Michael Elovskikh - https://github.com/wronglink * Saurabh Kumar - https://github.com/theskumar * Michael Elovskikh - https://github.com/wronglink -* Daniel Hahler - https://daniel.hahler.de +* Daniel Hahler - https://github.com/blueyed (\https://daniel.hahler.de) * Florian Bruhin - http://www.the-compiler.org * Zoltan Kozma - https://github.com/kozmaz87 * Francis Niu - https://flniu.github.io @@ -62,3 +62,4 @@ Authors * Matthew Gamble - https://github.com/mwgamble * Christian Clauss - https://github.com/cclauss * Dawn James - https://github.com/dawngerpony +* Tsvika Shapira - https://github.com/tsvikas diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5dd46aa3..beac9eea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,65 @@ Changelog ========= + +6.2.1 (2025-06-12) +------------------ + +* Added a version requirement for pytest's pluggy dependency (1.2.0, released 2023-06-21) that has the required new-style hookwrapper API. +* Removed deprecated license classifier (packaging). +* Disabled coverage warnings in two more situations where they have no value: + + * "module-not-measured" in workers + * "already-imported" in subprocesses + +6.2.0 (2025-06-11) +------------------ + +* The plugin now adds 3 rules in the filter warnings configuration to prevent common coverage warnings being raised as obscure errors:: + + default:unclosed database in `_. +* Removed unnecessary CovFailUnderWarning. Fixes `#675 `_. +* Fixed the term report not using the precision specified via ``--cov-precision``. + + +6.0.0 (2024-10-29) +------------------ + +* Updated various documentation inaccuracies, especially on subprocess handling. +* Changed fail under checks to use the precision set in the coverage configuration. + Now it will perform the check just like ``coverage report`` would. +* Added a ``--cov-precision`` cli option that can override the value set in your coverage configuration. +* Dropped support for now EOL Python 3.8. + 5.0.0 (2024-03-24) ------------------ @@ -18,6 +77,7 @@ Changelog Contributed by Dawn James in `#626 `_. * Modernized project's pre-commit hooks to use ruff. Initial POC contributed by Christian Clauss in `#584 `_. +* Dropped support for Python 3.7. 4.1.0 (2023-05-24) ------------------ @@ -34,6 +94,7 @@ Changelog Contributed by Mark Mayo in `#572 `_. * Fixed a skip in the test suite for some old xdist. Contributed by a bunch of people in `#565 `_. +* Dropped support for Python 3.6. 4.0.0 (2022-09-28) diff --git a/MANIFEST.in b/MANIFEST.in index 22042a59..529ba8f4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -25,5 +25,6 @@ include CHANGELOG.rst include CONTRIBUTING.rst include LICENSE include README.rst +include SECURITY.md global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/README.rst b/README.rst index 19b24e9f..61b0b4aa 100644 --- a/README.rst +++ b/README.rst @@ -39,9 +39,9 @@ Overview :alt: Supported implementations :target: https://pypi.org/project/pytest-cov -.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v5.0.0.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/pytest-dev/pytest-cov/v6.2.1.svg :alt: Commits since latest release - :target: https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...master + :target: https://github.com/pytest-dev/pytest-cov/compare/v6.2.1...master .. end-badges @@ -128,15 +128,21 @@ examine it. Limitations =========== -For distributed testing the workers must have the pytest-cov package installed. This is needed since +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 +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. +started. See the `subprocess support docs `_ +for more details of how this works. +Security +======== + +To report a security vulnerability please use the `Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. Acknowledgements ================ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..da9c516d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/ci/templates/.github/workflows/test.yml b/ci/templates/.github/workflows/test.yml index 39e64078..22fec036 100644 --- a/ci/templates/.github/workflows/test.yml +++ b/ci/templates/.github/workflows/test.yml @@ -17,15 +17,15 @@ jobs: - {python-version: "pypy-3.9", tox-python-version: "pypy3"} - {python-version: "3.11", tox-python-version: "py311"} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: @@ -77,7 +77,7 @@ jobs: {% for os, python_arch in [ ['ubuntu', 'x64'], ['windows', 'x64'], - ['macos', 'x64'], + ['macos', 'arm64'], ] %} - name: '{{ env }} ({{ os }})' python: '{{ python }}' diff --git a/docs/conf.py b/docs/conf.py index f15a262c..9ec7494d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ year = '2010-2024' author = 'pytest-cov contributors' copyright = f'{year}, {author}' -version = release = '5.0.0' +version = release = '6.2.1' pygments_style = 'trac' templates_path = ['.'] @@ -31,7 +31,9 @@ } html_theme = 'furo' html_theme_options = { - 'githuburl': 'https://github.com/pytest-dev/pytest-cov/', + 'source_repository': 'https://github.com/pytest-dev/pytest-cov/', + 'source_branch': 'master', + 'source_directory': 'docs/', } html_use_smartypants = True @@ -39,6 +41,10 @@ html_split_index = False html_short_title = f'{project}-{version}' +linkcheck_anchors_ignore_for_url = [ + r'^https?://(www\.)?github\.com/.*', +] + napoleon_use_ivar = True napoleon_use_rtype = False napoleon_use_param = False diff --git a/docs/config.rst b/docs/config.rst index 852c014b..0d3bfacf 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -51,10 +51,11 @@ 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``. +In practical terms this means that if you have multiple configuration files around (``tox.ini``, ``pyproject.toml`` or ``setup.cfg``) you +might need to use ``--cov-config`` to make coverage use the correct configuration file. -You might not be affected but it's unlikely that you won't ever use ``chdir`` in a test. +Also, if you change the working directory and also use subprocesses in a test you might also need to use ``--cov-config`` to make pytest-cov +will use the expected configuration file in the subprocess. Reference ========= diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 331db7d4..56044392 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -6,43 +6,72 @@ Normally coverage writes the data via a pretty standard atexit handler. However, 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. +pytest-cov supports subprocesses, and works around these atexit limitations. However, there are a few pitfalls that need to be explained. -If you use ``multiprocessing.Pool`` -=================================== +But first, how does pytest-cov's subprocess support works? -**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: +pytest-cov packaging injects a pytest-cov.pth into the installation. This file effectively runs this at *every* python startup: .. code-block:: python - from multiprocessing import Pool + 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 + ) + ) - def f(x): - return x*x +The pytest plugin will set this ``COV_CORE_SOURCE`` environment variable thus any subprocess that inherits the environment variables +(the default behavior) will run ``pytest_cov.embed.init`` which in turn sets up coverage according to these variables: - 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. +* ``COV_CORE_SOURCE`` +* ``COV_CORE_CONFIG`` +* ``COV_CORE_DATAFILE`` +* ``COV_CORE_BRANCH`` +* ``COV_CORE_CONTEXT`` + +Why does it have the ``COV_CORE`` you wonder? Well, it's mostly historical reasons: long time ago pytest-cov depended on a cov-core package +that implemented common functionality for pytest-cov, nose-cov and nose2-cov. The dependency is gone but the convention is kept. It could +be changed but it would break all projects that manually set these intended-to-be-internal-but-sadly-not-in-reality environment variables. + +Coverage's subprocess support +============================= + +Now that you understand how pytest-cov works you can easily figure out that using +`coverage's recommended `_ way of dealing with subprocesses, +by either having this in a ``.pth`` file or ``sitecustomize.py`` will break everything: + +.. code-block:: + + import coverage; coverage.process_startup() # this will break pytest-cov +Do not do that as that will restart coverage with the wrong options. -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: +If you use ``multiprocessing`` +============================== -.. warning:: +Builtin support for multiprocessing was dropped in pytest-cov 4.0. +This support was mostly working but very broken in certain scenarios (see `issue 82408 `_) +and made the test suite very flaky and slow. - **This technique cannot be used on Python 3.8** (registering signal handlers will cause deadlocks in the pool, - see: https://bugs.python.org/issue38227). +However, there is `builtin multiprocessing support in coverage `_ +and you can migrate to that. All you need is this in your preferred configuration file (example: ``.coveragerc``): + +.. code-block:: ini + + [run] + concurrency = multiprocessing + parallel = true + sigterm = true + +Now as a side-note, it's a good idea in general to properly close your Pool by using ``Pool.join()``: .. code-block:: python @@ -52,46 +81,33 @@ register a cleaning SIGTERM handler like so: return x*x if __name__ == '__main__': + p = Pool(5) 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])) + finally: + p.close() # Marks the pool as closed. + p.join() # Waits for workers to exit. -If you use ``multiprocessing.Process`` -====================================== - -There's similar issue when using the ``Process`` objects. Don't forget to use ``.join()``: -.. code-block:: python +.. _cleanup_on_sigterm: - from multiprocessing import Process +Signal handlers +=============== - def f(name): - print('hello', name) +pytest-cov provides a signal handling routines, mostly for special situations where you'd have custom signal handling that doesn't +allow atexit to properly run and the now-gone multiprocessing support: - if __name__ == '__main__': - try: - from pytest_cov.embed import cleanup_on_sigterm - except ImportError: - pass - else: - cleanup_on_sigterm() +* ``pytest_cov.embed.cleanup_on_sigterm()`` +* ``pytest_cov.embed.cleanup_on_signal(signum)`` (e.g.: ``cleanup_on_signal(signal.SIGHUP)``) - 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) +If you use multiprocessing +-------------------------- -.. _cleanup_on_sigterm: +It is not recommanded to use these signal handlers with multiprocessing as registering signal handlers will cause deadlocks in the pool, +see: https://bugs.python.org/issue38227). 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. @@ -140,7 +156,7 @@ Alternatively you can do this: 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 diff --git a/examples/src-layout/.coveragerc b/examples/src-layout/.coveragerc index b4c80de2..7ecf0087 100644 --- a/examples/src-layout/.coveragerc +++ b/examples/src-layout/.coveragerc @@ -7,7 +7,7 @@ source = branch = true parallel = true source = - example + src/example tests [report] diff --git a/examples/src-layout/tox.ini b/examples/src-layout/tox.ini index 94b72730..953955c9 100644 --- a/examples/src-layout/tox.ini +++ b/examples/src-layout/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pypy3,py39,report +envlist = clean,pypy310,py310,report [tool:pytest] testpaths = tests @@ -7,9 +7,7 @@ addopts = --cov-report=term-missing [testenv] -setenv = - py{py3,39}: COVERAGE_FILE = .coverage.{envname} -commands = pytest --cov {posargs:-vv} +commands = pytest --cov --cov-append {posargs:-vv} deps = pytest coverage @@ -20,12 +18,17 @@ deps = ../.. depends = - report: pypy3,py39 + report: pypy310,py310 + {pypy310,py310}: clean + +[testenv:clean] +skip_install = true +deps = coverage +commands = + coverage erase [testenv:report] skip_install = true deps = coverage commands = - coverage combine - coverage html coverage report --fail-under=100 diff --git a/pyproject.toml b/pyproject.toml index 61be7748..e795c6de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires = [ extend-exclude = ["static", "ci/templates"] line-length = 140 src = ["src", "tests"] -target-version = "py38" +target-version = "py39" [tool.ruff.lint.per-file-ignores] "ci/*" = ["S"] @@ -33,7 +33,6 @@ select = [ "PLE", # pylint errors "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib - "Q", # flake8-quotes "RSE", # flake8-raise "RUF", # ruff-specific rules "S", # flake8-bandit @@ -45,14 +44,9 @@ select = [ fixture-parentheses = false mark-parentheses = false -[tool.ruff.lint.flake8-quotes] -inline-quotes = "single" - [tool.ruff.lint.isort] forced-separate = ["conftest"] force-single-line = true -[tool.black] -line-length = 140 -target-version = ["py38"] -skip-string-normalization = true +[tool.ruff.format] +quote-style = "single" diff --git a/setup.py b/setup.py index 221111e8..f95279d1 100755 --- a/setup.py +++ b/setup.py @@ -72,12 +72,12 @@ def finalize_options(self): def run(self): with Path(__file__).parent.joinpath('src', 'pytest-cov.pth').open('w') as fh: with Path(__file__).parent.joinpath('src', 'pytest-cov.embed').open() as sh: - fh.write(f"import os, sys;exec({sh.read().replace(' ', ' ')!r})") + fh.write(f'import os, sys;exec({sh.read().replace(" ", " ")!r})') setup( name='pytest-cov', - version='5.0.0', + version='6.2.1', license='MIT', description='Pytest plugin for measuring coverage.', long_description='{}\n{}'.format(read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), @@ -94,14 +94,12 @@ def run(self): 'Development Status :: 5 - Production/Stable', 'Framework :: Pytest', 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Operating System :: Unix', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', @@ -124,10 +122,11 @@ def run(self): 'distributed', 'parallel', ], - python_requires='>=3.8', + python_requires='>=3.9', install_requires=[ - 'pytest>=4.6', - 'coverage[toml]>=5.2.1', + 'pytest>=6.2.5', + 'coverage[toml]>=7.5', + 'pluggy>=1.2', ], extras_require={ 'testing': [ diff --git a/src/pytest_cov/__init__.py b/src/pytest_cov/__init__.py index 82ce08bb..0487a25b 100644 --- a/src/pytest_cov/__init__.py +++ b/src/pytest_cov/__init__.py @@ -1,3 +1,41 @@ """pytest-cov: avoid already-imported warning: PYTEST_DONT_REWRITE.""" -__version__ = '5.0.0' +__version__ = '6.2.1' + +import pytest + + +class CoverageError(Exception): + """Indicates that our coverage is too low""" + + +class PytestCovWarning(pytest.PytestWarning): + """ + The base for all pytest-cov warnings, never raised directly. + """ + + +class CovDisabledWarning(PytestCovWarning): + """ + Indicates that Coverage was manually disabled. + """ + + +class CovReportWarning(PytestCovWarning): + """ + Indicates that we failed to generate a report. + """ + + +class CentralCovContextWarning(PytestCovWarning): + """ + Indicates that dynamic_context was set to test_function instead of using the builtin --cov-context. + """ + + +class DistCovError(Exception): + """ + Raised when dynamic_context is set to test_function and xdist is also used. + + See: https://github.com/pytest-dev/pytest-cov/issues/604 + """ diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index 57c2f423..153cb83d 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -61,6 +61,7 @@ def init(): cov.switch_context(cov_context) cov._warn_no_data = False cov._warn_unimported_source = False + cov._warn_preimported_source = False return cov diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 0dab6c03..4d53c2fc 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -1,21 +1,32 @@ """Coverage controllers for use by pytest-cov and nose-cov.""" +import argparse import contextlib import copy import functools import os import random +import shutil import socket import sys +import warnings from io import StringIO from pathlib import Path +from typing import Union import coverage from coverage.data import CoverageData +from coverage.sqldata import filename_suffix +from . import CentralCovContextWarning +from . import DistCovError from .embed import cleanup +class BrokenCovConfigError(Exception): + pass + + class _NullFile: @staticmethod def write(v): @@ -51,16 +62,21 @@ def ensure_topdir_wrapper(self, *args, **kwargs): return ensure_topdir_wrapper +def _data_suffix(name): + return f'{filename_suffix(True)}.{name}' + + class CovController: """Base class for different plugin implementations.""" - def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, config=None, nodeid=None): + def __init__(self, options: argparse.Namespace, config: Union[None, object], nodeid: Union[None, str]): """Get some common config used by multiple derived classes.""" - self.cov_source = cov_source - self.cov_report = cov_report - self.cov_config = cov_config - self.cov_append = cov_append - self.cov_branch = cov_branch + self.cov_source = options.cov_source + self.cov_report = options.cov_report + self.cov_config = options.cov_config + self.cov_append = options.cov_append + self.cov_branch = options.cov_branch + self.cov_precision = options.cov_precision self.config = config self.nodeid = nodeid @@ -71,6 +87,7 @@ def __init__(self, cov_source, cov_report, cov_config, cov_append, cov_branch, c self.failed_workers = [] self.topdir = os.fspath(Path.cwd()) self.is_collocated = None + self.started = False @contextlib.contextmanager def ensure_topdir(self): @@ -81,6 +98,7 @@ def ensure_topdir(self): @_ensure_topdir def pause(self): + self.started = False self.cov.stop() self.unset_env() @@ -88,6 +106,13 @@ def pause(self): def resume(self): self.cov.start() self.set_env() + self.started = True + + def start(self): + self.started = True + + def finish(self): + self.started = False @_ensure_topdir def set_env(self): @@ -122,15 +147,39 @@ def get_node_desc(platform, version_info): return 'platform {}, python {}'.format(platform, '{}.{}.{}-{}-{}'.format(*version_info[:5])) @staticmethod - def sep(stream, s, txt): + def get_width(): + # taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L26 + width, _ = shutil.get_terminal_size(fallback=(80, 24)) + # The Windows get_terminal_size may be bogus, let's sanify a bit. + if width < 40: + width = 80 + return width + + def sep(self, stream, s, txt): if hasattr(stream, 'sep'): stream.sep(s, txt) else: - sep_total = max((70 - 2 - len(txt)), 2) - sep_len = sep_total // 2 - sep_extra = sep_total % 2 - out = f'{s * sep_len} {txt} {s * (sep_len + sep_extra)}\n' - stream.write(out) + fullwidth = self.get_width() + # taken from https://github.com/pytest-dev/pytest/blob/33c7b05a/src/_pytest/_io/terminalwriter.py#L126 + # The goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth. + if sys.platform == 'win32': + # If we print in the last column on windows we are on a + # new line but there is no way to verify/neutralize this + # (we may not know the exact line width). + # So let's be defensive to avoid empty lines in the output. + fullwidth -= 1 + N = max((fullwidth - len(txt) - 2) // (2 * len(s)), 1) + fill = s * N + line = f'{fill} {txt} {fill}' + # In some situations there is room for an extra sepchar at the right, + # in particular if we consider that with a sepchar like "_ " the + # trailing space is not important at the end of the line. + if len(line) + len(s.rstrip()) <= fullwidth: + line += s.rstrip() + # (end of terminalwriter borrowed code) + line += '\n\n' + stream.write(line) @_ensure_topdir def summary(self, stream): @@ -143,15 +192,15 @@ def summary(self, stream): # Output coverage section header. if len(self.node_descs) == 1: - self.sep(stream, '-', f"coverage: {''.join(self.node_descs)}") + self.sep(stream, '_', f'coverage: {"".join(self.node_descs)}') else: - self.sep(stream, '-', 'coverage') + self.sep(stream, '_', 'coverage') for node_desc in sorted(self.node_descs): self.sep(stream, ' ', f'{node_desc}') # Report on any failed workers. if self.failed_workers: - self.sep(stream, '-', 'coverage: failed workers') + self.sep(stream, '_', 'coverage: failed workers') stream.write('The following workers failed to return coverage data, ensure that pytest-cov is installed on these workers.\n') for node in self.failed_workers: stream.write(f'{node.gateway.id}\n') @@ -162,6 +211,7 @@ def summary(self, stream): 'show_missing': ('term-missing' in self.cov_report) or None, 'ignore_errors': True, 'file': stream, + 'precision': self.cov_precision, } skip_covered = isinstance(self.cov_report, dict) and 'skip-covered' in self.cov_report.values() options.update({'skip_covered': skip_covered or None}) @@ -230,13 +280,20 @@ def start(self): self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=True, + data_suffix=_data_suffix('c'), config_file=self.cov_config, ) + if self.cov.config.dynamic_context == 'test_function': + message = ( + 'Detected dynamic_context=test_function in coverage configuration. ' + 'This is unnecessary as this plugin provides the more complete --cov-context option.' + ) + warnings.warn(CentralCovContextWarning(message), stacklevel=1) + self.combining_cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=True, + data_suffix=_data_suffix('cc'), data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 config_file=self.cov_config, ) @@ -247,9 +304,12 @@ def start(self): self.cov.start() self.set_env() + super().start() + @_ensure_topdir def finish(self): """Stop coverage, save data to file and set the list of coverage objects to report on.""" + super().finish() self.unset_env() self.cov.stop() @@ -274,16 +334,22 @@ def start(self): self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=True, + data_suffix=_data_suffix('m'), config_file=self.cov_config, ) + if self.cov.config.dynamic_context == 'test_function': + raise DistCovError( + 'Detected dynamic_context=test_function in coverage configuration. ' + 'This is known to cause issues when using xdist, see: https://github.com/pytest-dev/pytest-cov/issues/604\n' + 'It is recommended to use --cov-context instead.' + ) self.cov._warn_no_data = False self.cov._warn_unimported_source = False self.cov._warn_preimported_source = False self.combining_cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=True, + data_suffix=_data_suffix('mc'), data_file=os.path.abspath(self.cov.config.data_file), # noqa: PTH100 config_file=self.cov_config, ) @@ -316,7 +382,7 @@ def testnodedown(self, node, error): # 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' % ( + data_suffix = '%s.%s.%06d.%s' % ( # noqa: UP031 socket.gethostname(), os.getpid(), random.randint(0, 999999), # noqa: S311 @@ -330,7 +396,7 @@ def testnodedown(self, node, error): data.read_fileobj(StringIO(output['cov_worker_data'])) cov.data.update(data) else: - data = CoverageData(no_disk=True) + data = CoverageData(no_disk=True, suffix='should-not-exist') data.loads(output['cov_worker_data']) cov.get_data().update(data) cov.stop() @@ -361,7 +427,6 @@ class DistWorker(CovController): @_ensure_topdir def start(self): - cleanup() # Determine whether we are collocated with master. @@ -382,15 +447,20 @@ def start(self): self.cov = coverage.Coverage( source=self.cov_source, branch=self.cov_branch, - data_suffix=True, + data_suffix=_data_suffix(f'w{self.nodeid}'), config_file=self.cov_config, ) + # Prevent workers from issuing module-not-measured type of warnings (expected for a workers to not have coverage in all the files). + self.cov._warn_unimported_source = False self.cov.start() self.set_env() + super().start() @_ensure_topdir def finish(self): """Stop coverage and send relevant info back to the master.""" + super().finish() + self.unset_env() self.cov.stop() diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 49413d46..6efc4694 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -2,33 +2,28 @@ import argparse import os +import re import warnings from io import StringIO from pathlib import Path +from typing import TYPE_CHECKING import coverage import pytest +from coverage.exceptions import CoverageWarning +from coverage.results import display_covered +from coverage.results import should_fail_under +from . import CovDisabledWarning +from . import CovReportWarning +from . import PytestCovWarning from . import compat from . import embed +if TYPE_CHECKING: + from .engine import CovController -class CoverageError(Exception): - """Indicates that our coverage is too low""" - - -class PytestCovWarning(pytest.PytestWarning): - """ - The base for all pytest-cov warnings, never raised directly - """ - - -class CovDisabledWarning(PytestCovWarning): - """Indicates that Coverage was manually disabled""" - - -class CovReportWarning(PytestCovWarning): - """Indicates that we failed to generate a report""" +COVERAGE_SQLITE_WARNING_RE = re.compile('unclosed database in 100: raise argparse.ArgumentTypeError( - 'Your desire for over-achievement is admirable but misplaced. ' - 'The maximum value is 100. Perhaps write more integration tests?' + 'Your desire for over-achievement is admirable but misplaced. The maximum value is 100. Perhaps write more integration tests?' ) return value @@ -162,6 +156,12 @@ def pytest_addoption(parser): default=None, help='Enable branch coverage.', ) + group.addoption( + '--cov-precision', + type=int, + default=None, + help='Override the reporting precision.', + ) group.addoption( '--cov-context', action='store', @@ -206,7 +206,7 @@ class CovPlugin: distributed worker. """ - def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False): + def __init__(self, options: argparse.Namespace, pluginmanager, start=True, no_cov_should_warn=False): """Creates a coverage pytest plugin. We read the rc file that coverage uses to get the data file @@ -224,6 +224,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) self._start_path = None self._disabled = False self.options = options + self._wrote_heading = False is_dist = getattr(options, 'numprocesses', False) or getattr(options, 'distload', False) or getattr(options, 'dist', 'no') != 'no' if getattr(options, 'no_cov', False): @@ -247,8 +248,7 @@ def __init__(self, options, pluginmanager, start=True, no_cov_should_warn=False) # worker is started in pytest hook - def start(self, controller_cls, config=None, nodeid=None): - + def start(self, controller_cls: type['CovController'], config=None, nodeid=None): if config is None: # fake config option for engine class Config: @@ -256,21 +256,15 @@ class Config: 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 = controller_cls(self.options, config, nodeid) self.cov_controller.start() self._started = True self._start_path = Path.cwd() cov_config = self.cov_controller.cov.config if self.options.cov_fail_under is None and hasattr(cov_config, 'fail_under'): self.options.cov_fail_under = cov_config.fail_under + if self.options.cov_precision is None: + self.options.cov_precision = getattr(cov_config, 'precision', 0) def _is_worker(self, session): return getattr(session.config, 'workerinput', None) is not None @@ -295,7 +289,7 @@ def pytest_sessionstart(self, session): self.start(engine.Central) if self.options.cov_context == 'test': - session.config.pluginmanager.register(TestContextPlugin(self.cov_controller.cov), '_cov_contexts') + session.config.pluginmanager.register(TestContextPlugin(self.cov_controller), '_cov_contexts') @pytest.hookimpl(optionalhook=True) def pytest_configure_node(self, node): @@ -319,18 +313,31 @@ def _should_report(self): needed = self.options.cov_report or self.options.cov_fail_under return needed and not (self.failed and self.options.no_cov_on_fail) - 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 - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtestloop(self, session): - yield - if self._disabled: - return + return (yield) + + # we add default warning configuration to prevent certain warnings to bubble up as errors due to rigid filterwarnings configuration + for _, message, category, _, _ in warnings.filters: + if category is ResourceWarning and message == COVERAGE_SQLITE_WARNING_RE: + break + else: + warnings.filterwarnings('default', 'unclosed database in = (7, 5): + expected.insert(0, dest_dir.join('function_index.html')) + expected.insert(0, dest_dir.join('class_index.html')) + assert sorted(dest_dir.visit('**/*.html')) == expected assert dest_dir.join('index.html').check() assert result.ret == 0 @@ -309,7 +317,7 @@ def test_html_configured_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage HTML written to dir somewhere', '*10 passed*', ] @@ -327,7 +335,7 @@ def test_xml_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage XML written to file ' + XML_REPORT_NAME, '*10 passed*', ] @@ -343,7 +351,7 @@ def test_json_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage JSON written to file ' + JSON_REPORT_NAME, '*10 passed*', ] @@ -360,7 +368,7 @@ def test_lcov_output_dir(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Coverage LCOV written to file ' + LCOV_REPORT_NAME, '*10 passed*', ] @@ -406,7 +414,7 @@ def test_term_missing_output_dir(testdir): result.stderr.fnmatch_lines( [ - '*argument --cov-report: output specifier not supported for: ' '"term-missing:%s"*' % DEST_DIR, + '*argument --cov-report: output specifier not supported for: "term-missing:%s"*' % DEST_DIR, ] ) assert result.ret != 0 @@ -450,12 +458,57 @@ def test_cov_min_float_value(testdir): def test_cov_min_float_value_not_reached(testdir): script = testdir.makepyfile(SCRIPT) - + testdir.tmpdir.join('.coveragerc').write(""" +[report] +precision = 3 +""") result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=88.89', script) assert result.ret == 1 result.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%']) +def test_cov_min_float_value_not_reached_cli(testdir): + script = testdir.makepyfile(SCRIPT) + result = testdir.runpytest( + '-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-precision=3', '--cov-fail-under=88.89', script + ) + assert result.ret == 1 + result.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%']) + + +def test_cov_precision(testdir): + script = testdir.makepyfile(SCRIPT) + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-precision=6', script) + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + 'Name Stmts Miss Cover Missing', + '----------------------------------------------------------', + 'test_cov_precision.py 9 1 88.888889% 11', + '----------------------------------------------------------', + 'TOTAL 9 1 88.888889%', + ] + ) + + +def test_cov_precision_from_config(testdir): + script = testdir.makepyfile(SCRIPT) + testdir.tmpdir.join('pyproject.toml').write(""" +[tool.coverage.report] +precision = 6""") + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + 'Name Stmts Miss Cover Missing', + '----------------------------------------------------------------------', + 'test_cov_precision_from_config.py 9 1 88.888889% 11', + '----------------------------------------------------------------------', + 'TOTAL 9 1 88.888889%', + ] + ) + + def test_cov_min_no_report(testdir): script = testdir.makepyfile(SCRIPT) @@ -470,7 +523,7 @@ def test_central_nonspecific(pytester, testdir, prop): testdir.tmpdir.join('.coveragerc').write(prop.fullconf) result = testdir.runpytest('-v', '--cov', '--cov-report=term-missing', script, *prop.args) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_central_nonspecific* {prop.result} *', '*10 passed*']) # multi-module coverage report assert any(line.startswith('TOTAL ') for line in result.stdout.lines) @@ -500,7 +553,7 @@ def test_central_coveragerc(pytester, testdir, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'test_central_coveragerc* {prop.result} *', '*10 passed*', ] @@ -537,7 +590,7 @@ def test_central_with_path_aliasing(pytester, testdir, monkeypatch, opts, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'src[\\/]mod* {prop.result} *', '*10 passed*', ] @@ -579,7 +632,7 @@ def test_foobar(bad): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', '*mod* 100%', '*1 passed*', ] @@ -616,7 +669,7 @@ def test_subprocess_with_path_aliasing(pytester, testdir, monkeypatch): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'src[\\/]child_script* {CHILD_SCRIPT_RESULT}*', f'src[\\/]parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -641,7 +694,7 @@ def test_show_missing_coveragerc(pytester, testdir, prop): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'Name * Stmts * Miss * Cover * Missing', f'test_show_missing_coveragerc* {prop.result} * 11*', '*10 passed*', @@ -747,7 +800,7 @@ def test_dist_collocated(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -782,7 +835,7 @@ def test_dist_not_collocated(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -818,7 +871,7 @@ def test_dist_not_collocated_coveragerc_source(pytester, testdir, prop): *prop.args, ) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_not_collocated* {prop.result} *', '*10 passed*']) assert result.ret == 0 @@ -830,7 +883,7 @@ def test_central_subprocess(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -856,7 +909,7 @@ def test_central_subprocess_change_cwd(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'*child_script* {CHILD_SCRIPT_RESULT}*', '*parent_script* 100%*', ] @@ -884,7 +937,7 @@ def test_central_subprocess_change_cwd_with_pythonpath(pytester, testdir, monkey result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'*child_script* {CHILD_SCRIPT_RESULT}*', ] ) @@ -910,7 +963,7 @@ def test_foo(): result = testdir.runpytest('-v', '--cov-config=coveragerc', f'--cov={script.dirpath()}', '--cov-branch', script) result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', 'test_central_subprocess_no_subscript* * 3 * 0 * 100%*', ] ) @@ -928,7 +981,7 @@ def test_dist_subprocess_collocated(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -968,7 +1021,7 @@ def test_dist_subprocess_not_collocated(pytester, testdir, tmpdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -1041,7 +1094,7 @@ def test_funcarg(testdir): result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_funcarg* 3 * 100%*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_funcarg* 3 * 100%*', '*1 passed*']) assert result.ret == 0 @@ -1091,7 +1144,7 @@ def test_run(): result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 26-27', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 26-27', '*1 passed*']) assert result.ret == 0 @@ -1137,7 +1190,7 @@ def test_run(): result = testdir.runpytest('-vv', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) assert result.ret == 0 @@ -1181,7 +1234,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cleanup_on_sigterm* {setup[1]}', '*1 passed*']) assert result.ret == 0 @@ -1216,7 +1269,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 88% 19-20', '*1 passed*']) assert result.ret == 0 @@ -1253,7 +1306,7 @@ def test_run(): result = testdir.runpytest('-vv', '--assert=plain', f'--cov={script.dirpath()}', '--cov-report=term-missing', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', 'test_cleanup_on_sigterm* 89% 22-23', '*1 passed*']) assert result.ret == 0 @@ -1290,6 +1343,27 @@ def test_cover_conftest(testdir): result.stdout.fnmatch_lines([CONF_RESULT]) +def test_filterwarnings_error(testdir): + testdir.makeini(r""" + [pytest] + filterwarnings = + error + """) + testdir.makepyfile(mod=MODULE) + testdir.makepyfile(plug=CONFTEST) + script = testdir.makepyfile(BASIC_TEST) + result = testdir.runpytest('-v', '--cov=mod', '--cov-report=xml', '--cov-report=term-missing', '-p', 'plug', script) + assert result.ret == 0 + result.stdout.fnmatch_lines(['* 1 passed *']) + result.stderr.fnmatch_lines( + [ + '* (module-not-measured)', + '* (no-data-collected)', + '* CovReportWarning: Failed to generate report: No data to report.', + ] + ) + + @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_cover_looponfail(testdir, monkeypatch): testdir.makepyfile(mod=MODULE) @@ -1485,7 +1559,7 @@ def test_dist_boxed(testdir): result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--boxed', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_boxed* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1496,7 +1570,7 @@ def test_dist_bare_cov(testdir): result = testdir.runpytest('-v', '--cov', '-n', '1', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_dist_bare_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1624,6 +1698,71 @@ def test_append_coverage(pytester, testdir, opts, prop): ) +@xdist_params +def test_coverage_plugin(pytester, testdir, opts, prop): + script = testdir.makepyfile(test_1=prop.code) + testdir.makepyfile( + coverageplugin=""" +import coverage + +class ExamplePlugin(coverage.CoveragePlugin): + pass + +def coverage_init(reg, options): + reg.add_file_tracer(ExamplePlugin()) +""" + ) + testdir.makepyprojecttoml(f""" +[tool.coverage.run] +plugins = ["coverageplugin"] +concurrency = ["thread", "multiprocessing"] +{prop.conf} +""") + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + ] + ) + + +@xdist_params +def test_dynamic_context(pytester, testdir, opts, prop): + script = testdir.makepyfile(test_1=prop.code) + testdir.makepyprojecttoml(f""" +[tool.coverage.run] +dynamic_context = "test_function" +parallel = true +{prop.conf} +""") + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) + if opts: + result.stderr.fnmatch_lines(['pytest_cov.DistCovError: Detected dynamic_context=test_function*']) + else: + result.stdout.fnmatch_lines( + [ + '* CentralCovContextWarning: Detected dynamic_context=test_function*', + f'test_1* {prop.result}*', + ] + ) + + +@xdist_params +def test_simple(pytester, testdir, opts, prop): + script = testdir.makepyfile(test_1=prop.code) + testdir.makepyprojecttoml(f""" +[tool.coverage.run] +parallel = true +{prop.conf} +""") + result = testdir.runpytest('-v', f'--cov={script.dirpath()}', script, *opts.split() + prop.args) + result.stdout.fnmatch_lines( + [ + f'test_1* {prop.result}*', + ] + ) + + @xdist_params def test_do_not_append_coverage(pytester, testdir, opts, prop): script = testdir.makepyfile(test_1=prop.code) @@ -1662,7 +1801,7 @@ def test_append_coverage_subprocess(testdir): result.stdout.fnmatch_lines( [ - '*- coverage: platform *, python * -*', + '*_ coverage: platform *, python * _*', f'child_script* {CHILD_SCRIPT_RESULT}*', f'parent_script* {PARENT_SCRIPT_RESULT}*', ] @@ -1688,7 +1827,7 @@ def bad_init(): monkeypatch.setattr(sys, 'stderr', buff) monkeypatch.setitem(os.environ, 'COV_CORE_SOURCE', 'foobar') exec(payload) - expected = "pytest-cov: Failed to setup subprocess coverage. " "Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" + expected = "pytest-cov: Failed to setup subprocess coverage. Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n" assert buff.getvalue() == expected @@ -1696,7 +1835,7 @@ def test_double_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1704,7 +1843,7 @@ def test_double_cov2(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', '--cov', '--cov', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_double_cov2* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) assert result.ret == 0 @@ -1719,7 +1858,7 @@ def test_cov_reset_then_set(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', '--assert=plain', f'--cov={script.dirpath()}', '--cov-reset', f'--cov={script.dirpath()}', script) - result.stdout.fnmatch_lines(['*- coverage: platform *, python * -*', f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) + result.stdout.fnmatch_lines(['*_ coverage: platform *, python * _*', f'test_cov_reset_then_set* {SCRIPT_SIMPLE_RESULT}*', '*1 passed*']) @pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') @@ -1824,6 +1963,36 @@ def test_contexts_not_supported(testdir): assert result.ret != 0 +def test_contexts_no_cover(testdir): + script = testdir.makepyfile(""" +import pytest + +def foobar(): + return 1 + +def test_with_coverage(): + foobar() + +@pytest.mark.no_cover() +def test_without_coverage(): + foobar() +""") + result = testdir.runpytest( + '-v', + '--cov-context=test', + '--cov=test_contexts_no_cover', + script, + ) + result.stdout.fnmatch_lines( + [ + 'test_contexts_no_cover.py 8 1 88%', + 'TOTAL 8 1 88%', + ] + ) + assert result.stderr.lines == [] + assert result.ret == 0 + + def test_issue_417(testdir): # https://github.com/pytest-dev/pytest-cov/issues/417 whatever = testdir.maketxtfile(whatever='') diff --git a/tox.ini b/tox.ini index 4cbe6c8a..a4465b5a 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ envlist = clean, check, docs, - {py38,py39,py310,py311,py312,pypy38,pypy39,pypy310}-pytest{81}-xdist{350}-coverage{74}, + {py39,py310,py311,py312,py313,pypy39,pypy310}-{pytest83,pytest84}-{xdist36,xdist37}-{coverage78}, report ignore_basepython_conflict = true @@ -28,6 +28,7 @@ basepython = py310: {env:TOXPYTHON:python3.10} py311: {env:TOXPYTHON:python3.11} py312: {env:TOXPYTHON:python3.12} + py313: {env:TOXPYTHON:python3.13} {bootstrap,clean,check,report,docs}: {env:TOXPYTHON:python3} extras = testing setenv = @@ -35,52 +36,28 @@ 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.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 - pytest72: _DEP_PYTEST=pytest==7.2.0 - pytest73: _DEP_PYTEST=pytest==7.3.3 - pytest74: _DEP_PYTEST=pytest==7.4.4 pytest80: _DEP_PYTEST=pytest==8.0.2 pytest81: _DEP_PYTEST=pytest==8.1.1 + pytest82: _DEP_PYTEST=pytest==8.2.2 + pytest83: _DEP_PYTEST=pytest==8.3.5 + pytest84: _DEP_PYTEST=pytest==8.4.0 - xdist127: _DEP_PYTESTXDIST=pytest-xdist==1.27.0 - xdist129: _DEP_PYTESTXDIST=pytest-xdist==1.29.0 - xdist131: _DEP_PYTESTXDIST=pytest-xdist==1.31.0 - xdist132: _DEP_PYTESTXDIST=pytest-xdist==1.32.0 - xdist133: _DEP_PYTESTXDIST=pytest-xdist==1.33.0 - xdist134: _DEP_PYTESTXDIST=pytest-xdist==1.34.0 - 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 - xdist320: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 - xdist330: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 - xdist340: _DEP_PYTESTXDIST=pytest-xdist==3.4.0 - xdist350: _DEP_PYTESTXDIST=pytest-xdist==3.5.0 + xdist32: _DEP_PYTESTXDIST=pytest-xdist==3.2.0 + xdist33: _DEP_PYTESTXDIST=pytest-xdist==3.3.1 + xdist34: _DEP_PYTESTXDIST=pytest-xdist==3.4.0 + xdist35: _DEP_PYTESTXDIST=pytest-xdist==3.5.0 + xdist36: _DEP_PYTESTXDIST=pytest-xdist==3.6.1 + xdist37: _DEP_PYTESTXDIST=pytest-xdist==3.7.0 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 - 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 - coverage65: _DEP_COVERAGE=coverage==6.5.0 coverage72: _DEP_COVERAGE=coverage==7.2.7 coverage73: _DEP_COVERAGE=coverage==7.3.4 coverage74: _DEP_COVERAGE=coverage==7.4.4 + coverage75: _DEP_COVERAGE=coverage==7.5.4 + coverage76: _DEP_COVERAGE=coverage==7.6.12 + coverage77: _DEP_COVERAGE=coverage==7.7.1 + coverage77: _DEP_COVERAGE=coverage==7.8.2 # For testing against a coverage.py working tree. coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME} passenv =