Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions coverage-floors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Minimum per-package statement coverage, checked by `just check`.
# Raise a floor after a coverage pass; never lower one.
cmd/agentsync 89.7
internal/doctor 88.1
internal/importer 88.8
internal/inventory 91.2
internal/linker 79.0
internal/reconcile 84.3
internal/releasepolicy 96.4
internal/scan 91.6
internal/state 89.9
internal/testfs 77.1
scripts/release-policy 90.7
16 changes: 13 additions & 3 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,19 @@ just uninstall

`just check` runs, in order: `go vet` and `gofmt` verification (`lint` fails on
unformatted files), `go build`, `staticcheck`, `golangci-lint`, `go test`,
`go test -race`, and per-package coverage. It stops at the first failure. The
static-analysis steps run after the build because both need the packages to
type-check.
`go test -race`, and per-package coverage floors. It stops at the first
failure. The static-analysis steps run after the build because both need the
packages to type-check.

The coverage step (`scripts/coverage-floor.sh`) enforces a floor per package
from `coverage-floors.txt`. It fails when a package drops below its floor, when
a package has no floor, or when a floor names a package that no longer exists.
A package without tests needs an explicit floor of `0`. `just coverage` prints
the plain report.

Floors only go up. After a coverage pass, set the package's floor to the new
verified number. Coverage differs slightly between macOS and Linux, so use the
lower of the local and CI numbers.

Analysis work is split so no check runs twice:

Expand Down
8 changes: 6 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ run = "golangci-lint run"
description = "Report per-package test coverage"
run = "go test -cover ./..."

[tasks.coverage-floor]
description = "Fail if any package's coverage is below its floor in coverage-floors.txt"
run = "sh ./scripts/coverage-floor.sh"

[tasks.check]
description = "Full pre-review gate: lint, build, static analysis, test, race, coverage"
run = '''
Expand All @@ -120,8 +124,8 @@ echo "==> test (go test ./...)"
go test ./...
echo "==> race (go test -race ./...)"
go test -race ./...
echo "==> coverage (go test -cover ./...)"
go test -cover ./...
echo "==> coverage floors (scripts/coverage-floor.sh)"
mise run coverage-floor
echo "==> check passed"
'''

Expand Down
89 changes: 89 additions & 0 deletions scripts/coverage-floor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/bin/sh
# Fails when any package's coverage is below its floor in coverage-floors.txt,
# or when the floors and the packages go test reports disagree.
set -u

floors=${1:-coverage-floors.txt}
if [ ! -f "$floors" ]; then
echo "coverage-floor: floors file $floors not found" >&2
exit 2
fi
module=$(go list -m) || exit 2
out=$(mktemp) || exit 2
pkgs=$(mktemp) || { rm -f "$out"; exit 2; }
trap 'rm -f "$out" "$pkgs"' EXIT

# The authoritative package list, so a package missing from go test's output
# fails instead of passing unseen.
go list ./... >"$pkgs" || exit 2

# Captured to a file rather than piped so go test's exit status survives.
if ! go test -cover ./... >"$out" 2>&1; then
cat "$out" >&2
echo "coverage-floor: go test failed" >&2
exit 1
fi

awk -v floorsfile="$floors" -v pkgsfile="$pkgs" -v prefix="$module/" '
function short(p) {
return index(p, prefix) == 1 ? substr(p, length(prefix) + 1) : p
}
function fail(msg) {
print "coverage-floor: " msg > "/dev/stderr"
bad = 1
}
function check(p, actual) {
seen[p] = 1
parsed++
if (!(p in floor)) {
fail(p " " actual "% has no floor; add a line to " floorsfile)
} else if (actual + 0 < floor[p] + 0) {
fail(p " " actual "% is below its floor of " floor[p] "%")
} else {
print p " " actual " (floor " floor[p] ")"
}
}
FILENAME == floorsfile {
line = $0
sub(/#.*/, "", line)
n = split(line, f, " ")
if (n == 0) next
if (n != 2 || f[2] !~ /^[0-9]+(\.[0-9]+)?$/ || f[2] + 0 > 100) {
fail(floorsfile ":" FNR ": malformed line, want <package> <percent>: " $0)
next
}
k = short(f[1])
if (k in floor) fail(floorsfile ":" FNR ": duplicate floor for " k)
floor[k] = f[2]
next
}
FILENAME == pkgsfile {
if ($0 != "" && !(short($0) in seen)) fail("go test reported no coverage for " short($0))
next
}
{
# A no-test package with functions but no statements prints "\tpkg\t\t"
# with no newline, so the next package line is joined onto it.
while (match($0, /^\t[^\t]+\t\t/) && substr($0, RLENGTH + 1) !~ /^coverage:/) {
check(short(substr($0, 2, RLENGTH - 3)), "0.0")
$0 = substr($0, RLENGTH + 1)
}
if (split($0, f, "\t") < 3) next
# A package with no tests counts as 0 so only an explicit floor of 0 admits it.
if (match($0, /coverage: [0-9.]+%/)) {
check(short(f[2]), substr($0, RSTART + 10, RLENGTH - 11))
} else if ($0 ~ /\[no test files\]|\[no statements\]/) {
check(short(f[2]), "0.0")
}
}
END {
if (parsed == 0) {
fail("parsed no coverage lines from go test")
exit 1
}
for (k in floor)
if (!(k in seen)) fail(k " has a floor but go test reported no such package; remove the stale line")
if (bad) exit 1
print "==> coverage floors held"
}
' "$floors" "$out" "$pkgs"
Loading