Describe the bug
On the HTML report index, the <tfoot> Total percentage is recomputed in
JavaScript with toFixed(), which does not implement the 0/100 clamp that
display_covered() guarantees. The <h1> heading on the same page is rendered
server-side from pc_covered_str and is never touched by JS, so the two can
disagree — including showing 100% when coverage is not complete.
coverage/htmlfiles/coverage_html.js (in filter_handler):
const { numer, denom } = totals[column];
cell.dataset.ratio = `${numer} ${denom}`;
cell.textContent = denom
? `${(numer * 100 / denom).toFixed(places)}%`
: `${(100).toFixed(places)}%`;
This is not limited to filtered views. A few lines below, a synthetic input
event is dispatched on load:
// Trigger change event on setup, to force filter on page refresh
// (filter value may still be present).
document.getElementById("filter").dispatchEvent(new Event("input"));
document.getElementById("hide100").dispatchEvent(new Event("input"));
so the server-rendered footer is overwritten on every page view, with no filter
typed and no checkbox ticked.
Expected behaviour
display_covered() in coverage/results.py documents the invariant:
Note that "0" is only returned when the value is truly zero, and "100" is only
returned when the value is truly 100. Rounding can never result in either "0" or
"100".
The footer should honour the same rule as the heading and the text report.
Actual behaviour
I ran both implementations directly — display_covered() as written in
coverage/results.py, and the JS expression above in node — at the default
precision = 0:
| statements covered |
true % |
<h1> (Python) |
Total row (JS) |
| 249 / 250 |
99.60 |
99% |
100% |
| 199 / 200 |
99.50 |
99% |
100% |
| 999 / 1000 |
99.90 |
99% |
100% |
| 173 / 200 |
86.50 |
86% |
87% |
| 4 / 1000 |
0.40 |
1% |
0% |
The first three are the invariant being broken: the footer reads 100% while one
statement was never executed. The last is the mirror case — 0% while four
statements did run. The 86 / 87 row is an independent divergence: Python's
round() is banker's rounding, toFixed() rounds ties away from zero, so ordinary
values can differ by one without any clamp being involved.
For a single-file project the disagreement is between a row and the Total directly
beneath it.
Additionally
With --skip-covered, should_report() adds a file's numbers to
index_page.totals before the skip returns, but fully-covered files produce no
<tr>. The JS sums only the surviving rows, so the heading and the footer diverge
further — e.g. a.py 10/10 and b.py 10/20 gives a heading of 67% over a footer
of 50%. (I traced this in the source but did not run it, so treat it as secondary
to the rounding issue above.)
Note
grep -E "filter_handler|totals|hide100" tests/js/tests.js returns no matches, so
this path appears to be untested.
Versions
- coverage 7.16.0a0,
master @ e14c88af87a7d071cc3920b9948f0b4794b44d2c
- Found by reading the source and by running the two rounding implementations
side by side; I have not generated a live HTML report to confirm on screen.
Happy to open a PR routing the footer through the same clamp if that's welcome.
Describe the bug
On the HTML report index, the
<tfoot>Total percentage is recomputed inJavaScript with
toFixed(), which does not implement the 0/100 clamp thatdisplay_covered()guarantees. The<h1>heading on the same page is renderedserver-side from
pc_covered_strand is never touched by JS, so the two candisagree — including showing
100%when coverage is not complete.coverage/htmlfiles/coverage_html.js(infilter_handler):This is not limited to filtered views. A few lines below, a synthetic
inputevent is dispatched on load:
so the server-rendered footer is overwritten on every page view, with no filter
typed and no checkbox ticked.
Expected behaviour
display_covered()incoverage/results.pydocuments the invariant:The footer should honour the same rule as the heading and the text report.
Actual behaviour
I ran both implementations directly —
display_covered()as written incoverage/results.py, and the JS expression above in node — at the defaultprecision = 0:<h1>(Python)The first three are the invariant being broken: the footer reads
100%while onestatement was never executed. The last is the mirror case —
0%while fourstatements did run. The
86 / 87row is an independent divergence: Python'sround()is banker's rounding,toFixed()rounds ties away from zero, so ordinaryvalues can differ by one without any clamp being involved.
For a single-file project the disagreement is between a row and the Total directly
beneath it.
Additionally
With
--skip-covered,should_report()adds a file's numbers toindex_page.totalsbefore the skip returns, but fully-covered files produce no<tr>. The JS sums only the surviving rows, so the heading and the footer divergefurther — e.g.
a.py10/10 andb.py10/20 gives a heading of 67% over a footerof 50%. (I traced this in the source but did not run it, so treat it as secondary
to the rounding issue above.)
Note
grep -E "filter_handler|totals|hide100" tests/js/tests.jsreturns no matches, sothis path appears to be untested.
Versions
master@e14c88af87a7d071cc3920b9948f0b4794b44d2cside by side; I have not generated a live HTML report to confirm on screen.
Happy to open a PR routing the footer through the same clamp if that's welcome.