From d725371d552dac64dd0985637fb53e9670d28a74 Mon Sep 17 00:00:00 2001 From: Logan Lindquist Land Date: Sat, 17 Oct 2026 17:03:22 -0500 Subject: [PATCH] build: fail the gate when a package's coverage drops `just check` now enforces a per-package coverage floor from coverage-floors.txt via scripts/coverage-floor.sh. It fails on a drop, a package with no floor, a stale floor, a test failure, or unparseable output; `go list ./...` is the authoritative package set. --- coverage-floors.txt | 13 ++++++ docs/development.md | 16 +++++-- mise.toml | 8 +++- scripts/coverage-floor.sh | 89 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 coverage-floors.txt create mode 100755 scripts/coverage-floor.sh diff --git a/coverage-floors.txt b/coverage-floors.txt new file mode 100644 index 0000000..c659ed7 --- /dev/null +++ b/coverage-floors.txt @@ -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 diff --git a/docs/development.md b/docs/development.md index 7ef7da0..9afc079 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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: diff --git a/mise.toml b/mise.toml index 5344304..fa9ab27 100644 --- a/mise.toml +++ b/mise.toml @@ -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 = ''' @@ -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" ''' diff --git a/scripts/coverage-floor.sh b/scripts/coverage-floor.sh new file mode 100755 index 0000000..38a4a54 --- /dev/null +++ b/scripts/coverage-floor.sh @@ -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 : " $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"