Visitar URL original
XML report: line-rate="1" when coverage is not 100% · Issue #2293 · coveragepy/coveragepy · GitHub
Skip to content

XML report: line-rate="1" when coverage is not 100% #2293

Closed

Description

@KRRT7

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

  1. Python: CPython 3.14.6 (macOS 26.5.2, arm64)
  2. coverage.py: 7.16.1, pip install coverage into a fresh venv
  3. Packages: clean venv containing only coverage==7.16.1 plus pip/setuptools
  4. Code: no repo needed, the snippet below generates its own input file
  5. 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

  1. nedbat commented on Oct 21, 2026

    @nedbat
    Member

    Fixed in commit 99024b5.

  2. nedbat commented on Oct 22, 2026

    @nedbat
    Member

    This is now released as part of coverage 7.17.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfixedxml

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions