Repository navigation
Description
Split out of #2290 as requested.
Describe the bug
coverage xml reports line-rate="1" for files that still have uncovered lines. Any
ratio at or above 0.99996 renders as exactly 1, because rate() formats with %.4g:
# coverage/xmlreport.py:33-37
def rate(hit: int, num: int) -> str:
"""Return the fraction of `hit`/`num`, as a string."""
if num == 0:
return "1"
else:
return f"{hit / num:.4g}"%.4g keeps four significant figures, so the fourth nine rounds the value up to 1. The
resulting XML is internally inconsistent — lines-valid and lines-covered disagree
with line-rate in the same element — and any consumer that gates on line-rate
(Cobertura-compatible CI plugins, Codecov/Jenkins-style thresholds) reads 100% coverage
for a file that has uncovered lines.
This contradicts coverage.py's own rule elsewhere: display_covered() in
coverage/results.py goes out of its way to guarantee that "100" is only shown when the
value is truly 100. The text report gets this right. The XML path has no such guard.
To Reproduce
- Python: CPython 3.14.6 (macOS 26.5.2, arm64)
- coverage.py: 7.16.1,
pip install coverageinto a fresh venv - Packages: clean venv containing only
coverage==7.16.1plus pip/setuptools - Code: no repo needed, the snippet below generates its own input file
- Commands:
$ python3 -m venv /tmp/cov && /tmp/cov/bin/pip install coverage
$ cd "$(mktemp -d)"
$ /tmp/cov/bin/python -c "
lines = [f'x{i} = {i}' for i in range(24999)] + ['def never_called():', ' y = 1']
open('big.py', 'w').write('\n'.join(lines) + '\n')
open('main.py', 'w').write('import big\n')
"
$ /tmp/cov/bin/coverage run main.py
$ /tmp/cov/bin/coverage report --include=big.py --precision=4
Name Stmts Miss Cover
-------------------------------
big.py 25001 1 99.9960%
-------------------------------
TOTAL 25001 1 99.9960%
$ /tmp/cov/bin/coverage xml --include=big.py -o -
<coverage version="7.16.1" ... lines-valid="25001" lines-covered="25000" line-rate="1" ...>
<package name="." line-rate="1" ...>
<class name="big.py" filename="big.py" ... line-rate="1" ...>25001 statements, exactly 1 uncovered. The text report says 99.9960%; the XML says
line-rate="1" while simultaneously carrying lines-valid="25001" lines-covered="25000".
Expected behavior
line-rate should never be 1 unless hit == num (and never 0 unless hit == 0) —
the same clamping display_covered already applies.
Additional context
This looks like the same bug class as #2256, where the HTML index footer can show 100%
when coverage isn't 100%. PR #2259 there is titled "html: clamp JS-recomputed footer
percentage to mirror display_covered", which is the same fix shape this would need. The
XML line-rate path appears to be a third instance of it.
See also the sibling bug in the same function, filed separately: rate() can also emit
scientific notation.
Happy to send a PR if you'd take one.
Activity
fix: clamp rate in XML report to prevent false 1.0/0.0 rounding and s…
nedbat commentedon Oct 21, 2026
Fixed in commit 99024b5.
nedbat commentedon Oct 22, 2026
This is now released as part of coverage 7.17.0.