Repository navigation
check-coverage passes when there is no coverage data to check #610
Description
- Version: 12.0.0, read from
node_modules/c8/package.json(c8 --versionprints1.0.0, which is v12.0.0 seems to have the wrong binary version #607) - Platform: Windows 11 (10.0.26200), 64-bit, Node v24.15.0
check-coverage exits 0 when there is no coverage data to check. The report for the same run says Unknown% ( 0/0 ), so the fact that nothing was measured is known at report time and does not reach the threshold check.
Reproduction
Fresh directory, npm init -y, npm i c8@12.0.0.
- A run that produces no coverage data:
$ npx c8 --check-coverage --lines 100 node -e "1"
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------
exit code: 0
- The same run with
--reporter=text-summary:
Statements : Unknown% ( 0/0 )
Branches : Unknown% ( 0/0 )
Functions : Unknown% ( 0/0 )
Lines : Unknown% ( 0/0 )
- Control, so the threshold is visibly working when there is something to measure.
lib.jsholds one function,t.jsisrequire("./lib.js"):
$ npx c8 --check-coverage --lines 100 node t.js
All files | 66.66 | 100 | 0 | 66.66 |
ERROR: Coverage for lines (66.66%) does not meet global threshold (100%)
exit code: 1
Three more shapes of the same thing, all exit 0:
npx c8 --check-coverage --lines 100 --include 'src/**' node t.js, with the same real test as the control and a glob that matches none of it. The table prints 0 in every column and the gate passes.npx c8 --check-coverage --per-file --lines 100 node -e "1".npx c8 check-coverage --temp-directory ./empty-dir --lines 100, which prints nothing at all and exits 0.
Where it comes from
Two independent paths reach the same result, in case that is useful:
checkCoverage()inlib/commands/check-coverage.jsdoessummary[key].pct < thresholds[key]. For an empty map, istanbul'sCoverageSummarysetspct: 'Unknown'(istanbul-lib-coverage/lib/coverage-summary.js), and'Unknown' < 100isfalse, so nothing is ever below a threshold.- With
--per-file,map.files()is empty, so theforEachbody never runs and there is nothing to fail.
What I am not proposing
I am not saying istanbul's Unknown is wrong. A file with no branches should not fail a branch threshold, and that is the same mechanism doing something useful. I also have no view I would defend on which way c8 should go: fail on an empty coverage map by default, warn, put it behind a flag, or document that check-coverage means "no measured metric is below the threshold". Each of those changes what existing CI does, including for people who run c8 over a process that legitimately produces no JS coverage, so the choice looks like yours rather than mine. No PR from me. If you land on a direction and want it written up, I am glad to do that.
Of the shapes above, the --include one seems most likely to happen by accident. A glob that stops matching after a directory is renamed leaves a coverage gate that always passes, and nothing in the output says it stopped measuring.
Activity
mahirhir commentedon Sep 12, 2026
Re-ran this on
c8@latest, which is still12.0.0, so nothing has moved under it. Same result, and this time with the control in the same table so the zeros mean something:Every one of those is
--lines 100. The control shows the threshold is alive in that exact invocation; the four below it are the same flag against nothing.Two reporters describe the same empty run differently, which is the part I find most useful for locating it:
text-summaryis right. The table prints0, which reads as a real measurement of zero, and the threshold check sees the'Unknown'thattext-summaryshows:So
summary[key].pct < thresholds[key]isfalsefor an empty map, and false means "not below threshold", which means pass. It is the same shape as aNaNcomparison: the value that means we could not measure this is ordered against the threshold rather than excluded from the comparison.I am not asking for a particular fix and I know the ecosystem-compatibility question here is real (istanbul's
pct: 'Unknown'is longstanding, and people do runcheck-coveragein matrix jobs where some shards legitimately produce nothing). If it helps, the options as I see them, cheapest first:pctas a distinct third outcome and fail the check with its own message, rather than routing it through the same<that numeric percentages use.pct: 'Unknown'alone for anyone parsing reports.0for an unmeasured run, so the two reporters at least agree.Happy to send a patch for whichever of those you would take, or none if this is intended. Mostly I wanted the reproduction on record with its control attached, since "exit 0" and "exit 0 because there was nothing to check" are indistinguishable in CI.