Skip to content

About

Minimal reproduction of coverage.py path collapsing bug with pytest-cov

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Coverage.py Path Collapsing Bug with pytest-cov

Minimal reproduction showing pytest --cov with relative_files=true incorrectly collapses files when a directory name appears at both root and nested levels, resulting in lost coverage data.

The Bug

When a directory name exists at both root and nested levels (e.g., features/ and pkg/features/), pytest-cov's auto-generated path mapping rules collapse both locations into a single entry:

  • features/templates.py → features/templates.py
  • pkg/features/templates.py → features/templates.py ❌ COLLAPSED

Result: Coverage data from pkg/features/templates.py is lost.

Note: The bug does NOT occur with coverage run -m pytest - only with pytest --cov.

Repository Structure

coverage-path-bug/
├── features/templates.py          # Root level
├── pkg/features/templates.py      # Nested level - TRIGGERS BUG
├── utils/helpers.py               # Root level (working comparison)
├── app1/utils/helpers.py          # Nested level (working comparison)
├── test_base_features.py
├── test_pkg_features.py
└── pyproject.toml

The utils/ pattern works correctly because the */app1 rule preserves app1/utils/helpers.py as separate. The features/ pattern fails because the */features rule matches both locations and collapses them.

Reproduction

  1. Install dependencies:

    uv sync
  2. Run with pytest --cov:

    COVERAGE_DEBUG=pathmap uv run pytest --cov=. --cov-report=term
  3. Observe bug: pkg/features/templates.py is missing from the report:

    features/templates.py        4      0      0      0   100%
    utils/helpers.py             4      0      0      0   100%
    app1/utils/helpers.py        4      0      0      0   100%
    TOTAL                       44 (should be 48!)
    
  4. Debug output shows path collapsing:

    Matched path 'features/templates.py' to rule '*/features' -> 'features/', producing 'features/templates.py'
    Matched path 'pkg/features/templates.py' to rule '*/features' -> 'features/', producing 'features/templates.py'
    
  5. Compare with coverage run (no bug):

    rm .coverage
    COVERAGE_DEBUG=pathmap uv run coverage run -m pytest
    COVERAGE_DEBUG=pathmap uv run coverage report

    Output correctly shows both files:

    features/templates.py         4      0      0      0   100%
    pkg/features/templates.py     4      0      0      0   100%
    TOTAL                        48
    

Expected vs Actual

Expected: All four files tracked separately (48 total statements) Actual with pytest --cov: Only three files appear - pkg/features/templates.py is missing (44 total statements)

Investigation

When relative_files=true, coverage.py auto-generates path mapping rules. The rule */features matches both:

  • features/templates.py → features/templates.py
  • pkg/features/templates.py → features/templates.py

Both collapse to the same destination.

The utils/ files work because */app1 catches app1/utils/helpers.py first, preserving the app1/ prefix before */utils can match it.

Rule Generation Order

The bug is triggered by the order in which pathmap rules are generated:

  1. features/ discovered first → generates rule */features -> features/
  2. features/templates.py matched → produces features/templates.py ✓
  3. pkg/ discovered → generates rule */pkg -> pkg/
  4. pkg/__init__.py matched → produces pkg/__init__.py ✓
  5. pkg/features/templates.py encountered → matches existing */features rule ❌
  6. Result: pkg/features/templates.py → features/templates.py (collapsed!)

The */features rule is too broad - it matches any path ending with /features/, regardless of what comes before it.

Debug output showing the issue:

Generating rule: '*/features' -> 'features/' using regex '^(.*[\\\\/])?features[\\\\/]'
Matched path 'features/__init__.py' to rule '*/features' -> 'features/', producing 'features/__init__.py'
Matched path 'features/templates.py' to rule '*/features' -> 'features/', producing 'features/templates.py'
Generating rule: '*/pkg' -> 'pkg/' using regex '^(.*[\\\\/])?pkg[\\\\/]'
Matched path 'pkg/__init__.py' to rule '*/pkg' -> 'pkg/', producing 'pkg/__init__.py'
Matched path 'pkg/features/__init__.py' to rule '*/features' -> 'features/', producing 'features/__init__.py'
Matched path 'pkg/features/templates.py' to rule '*/features' -> 'features/', producing 'features/templates.py'

Notice how pkg/features/templates.py matches the */features rule (generated earlier) instead of being preserved with its full path.

Configuration

pyproject.toml:

[tool.coverage.run]
relative_files = true
branch = true
source = ["."]

Environment

Tested with:

  • Python: 3.11.10
  • coverage.py: 7.11.0
  • pytest: 8.4.2
  • pytest-cov: 7.0.0
  • uv: for dependency management

Detailed Output Comparison

With pytest --cov (BUG - Missing File)

$ COVERAGE_DEBUG=pathmap uv run pytest --cov=. --cov-report=term

Output:

test_app1_helpers.py ..                                                  [ 25%]
test_base_features.py ..                                                 [ 50%]
test_base_helpers.py ..                                                  [ 75%]
test_pkg_features.py ..                                                  [100%]

================================ tests coverage ================================
______________ coverage: platform darwin, python 3.11.10-final-0 _______________

Name                     Stmts   Miss Branch BrPart  Cover   Missing
--------------------------------------------------------------------
app1/__init__.py             0      0      0      0   100%
app1/utils/__init__.py       0      0      0      0   100%
app1/utils/helpers.py        4      0      0      0   100%
features/__init__.py         0      0      0      0   100%
features/templates.py        4      0      0      0   100%
pkg/__init__.py              0      0      0      0   100%
test_app1_helpers.py         8      0      0      0   100%
test_base_features.py        8      0      0      0   100%
test_base_helpers.py         8      0      0      0   100%
test_pkg_features.py         8      0      0      0   100%
utils/__init__.py            0      0      0      0   100%
utils/helpers.py             4      0      0      0   100%
--------------------------------------------------------------------
TOTAL                       44      0      0      0   100%
============================== 8 passed in 0.03s ===============================

❌ Notice: pkg/features/__init__.py and pkg/features/templates.py are MISSING! Total statements: 44 (should be 48)

With coverage run (WORKS - All Files Present)

$ COVERAGE_DEBUG=pathmap coverage run -m pytest
$ COVERAGE_DEBUG=pathmap coverage report

Output:

============================= test session starts ==============================
platform darwin -- Python 3.11.10, pytest-8.4.2, pluggy-1.6.0
rootdir: /private/tmp/coverage-path-bug
configfile: pyproject.toml
plugins: cov-7.0.0
collected 8 items

test_app1_helpers.py ..                                                  [ 25%]
test_base_features.py ..                                                 [ 50%]
test_base_helpers.py ..                                                  [ 75%]
test_pkg_features.py ..                                                  [100%]

============================== 8 passed in 0.02s ===============================

Name                        Stmts   Miss Branch BrPart  Cover   Missing
-----------------------------------------------------------------------
app1/__init__.py                0      0      0      0   100%
app1/utils/__init__.py          0      0      0      0   100%
app1/utils/helpers.py           4      0      0      0   100%
features/__init__.py            0      0      0      0   100%
features/templates.py           4      0      0      0   100%
pkg/__init__.py                 0      0      0      0   100%
pkg/features/__init__.py        0      0      0      0   100%
pkg/features/templates.py       4      0      0      0   100%
test_app1_helpers.py            8      0      0      0   100%
test_base_features.py           8      0      0      0   100%
test_base_helpers.py            8      0      0      0   100%
test_pkg_features.py            8      0      0      0   100%
utils/__init__.py               0      0      0      0   100%
utils/helpers.py                4      0      0      0   100%
-----------------------------------------------------------------------
TOTAL                          48      0      0      0   100%

✓ Notice: ALL files including pkg/features/__init__.py and pkg/features/templates.py are present! Total statements: 48 (correct)

Key Difference

With COVERAGE_DEBUG=pathmap:

  • pytest --cov: Generates pathmap rules, applies them, causes file collapsing
  • coverage run: Does NOT generate or apply pathmap rules at all, even with debug flag enabled

About

Minimal reproduction of coverage.py path collapsing bug with pytest-cov

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages