diff --git a/scripts/check-coverage.sh b/scripts/check-coverage.sh index b21ea9f4..25b5df5d 100755 --- a/scripts/check-coverage.sh +++ b/scripts/check-coverage.sh @@ -25,10 +25,59 @@ max_reported=25 go test ./... -timeout 90s -covermode=atomic -coverprofile="$cover_file" # Profile body, one block per line: "/.go: ". +# +# Blocks are merged by identity — the ".go:" field — before anything is +# counted, because the same block can arrive several times over. go test builds the +# -coverprofile output by concatenating each package's fragment, and cmd/go appends a +# cached fragment before the checks that decide whether that cached result is usable, +# once for each of the two keys it tries. Immediately after `go clean -testcache` every +# block therefore lands three times, and a count over the raw lines reports how warm +# the test cache is rather than how large the tree is. Merging is what go tool cover +# does when it reads a profile; an explicit -coverpkg would not help, since the repeats +# are re-emissions of a package's own fragment. +# +# Counts merge by max rather than the sum go tool cover uses in atomic mode, because +# the repeats are one run's data re-emitted: summing would multiply real execution +# counts by however many times a fragment happened to be appended. The verdict is the +# same under either rule, since the gate only asks whether a block ever ran. +# +# One consequence is deliberate: a block that ran in one fragment and not in another +# now counts as covered, the plain meaning of "some test executed this statement". It +# retires a false failure the raw count could produce, where such a block added 2 to +# the total and 1 to the hits. A block no fragment ran still merges to 0, and is still +# reported and still fails. +# # Sorted so the same failure reads the same way on every run. -uncovered="$(tail -n +2 "$cover_file" | awk '$3 == 0' | sort)" +merged="$(awk 'NR > 1 { + if (!($1 in stmts)) { + stmts[$1] = $2 + 0 + count[$1] = $3 + 0 + next + } + # A span identifies one block of one file, so its statement count cannot + # differ between copies. If it does, the profile is not one build and the + # total would depend on line order. + if (stmts[$1] != $2 + 0) { + printf "COVERAGE FAIL: %s reports %d and %d statements\n", $1, stmts[$1], $2 > "/dev/stderr" + conflict = 1 + exit 1 + } + if ($3 + 0 > count[$1]) { + count[$1] = $3 + 0 + } +} +END { + if (conflict) { + exit 1 + } + for (block in stmts) { + print block, stmts[block], count[block] + } +}' "$cover_file" | sort)" + +uncovered="$(printf '%s\n' "$merged" | awk '$3 == 0')" -read -r hit total <<<"$(awk 'NR > 1 { total += $2; if ($3 > 0) hit += $2 } END { print hit + 0, total + 0 }' "$cover_file")" +read -r hit total <<<"$(printf '%s\n' "$merged" | awk '{ total += $2; if ($3 > 0) hit += $2 } END { print hit + 0, total + 0 }')" if [ "$total" -eq 0 ]; then echo "COVERAGE FAIL: the profile records no statements"