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.
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.pypkg/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.
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.
-
Install dependencies:
uv sync
-
Run with
pytest --cov:COVERAGE_DEBUG=pathmap uv run pytest --cov=. --cov-report=term
-
Observe bug:
pkg/features/templates.pyis 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!) -
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' -
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: 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)
When relative_files=true, coverage.py auto-generates path mapping rules. The rule */features matches both:
features/templates.py→features/templates.pypkg/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.
The bug is triggered by the order in which pathmap rules are generated:
features/discovered first → generates rule*/features -> features/features/templates.pymatched → producesfeatures/templates.py✓pkg/discovered → generates rule*/pkg -> pkg/pkg/__init__.pymatched → producespkg/__init__.py✓pkg/features/templates.pyencountered → matches existing*/featuresrule ❌- 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.
pyproject.toml:
[tool.coverage.run]
relative_files = true
branch = true
source = ["."]Tested with:
- Python: 3.11.10
- coverage.py: 7.11.0
- pytest: 8.4.2
- pytest-cov: 7.0.0
- uv: for dependency management
$ COVERAGE_DEBUG=pathmap uv run pytest --cov=. --cov-report=termOutput:
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)
$ COVERAGE_DEBUG=pathmap coverage run -m pytest
$ COVERAGE_DEBUG=pathmap coverage reportOutput:
============================= 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)
With COVERAGE_DEBUG=pathmap:
pytest --cov: Generates pathmap rules, applies them, causes file collapsingcoverage run: Does NOT generate or apply pathmap rules at all, even with debug flag enabled