mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
4c948512d1
* fix: follow ListObjectsV2 continuation tokens on every production caller Six ListObjectsV2 call sites (DeleteSnapshot, DescribeSnapshots, EKS etcd-snapshot selection, DescribeTags, and both key-pair listings) issued a single request and never read IsTruncated or NextContinuationToken. This was latent while predastore ignored max-keys and always answered IsTruncated: false; predastore#117 pages listings at 1000 keys, turning a truncated listing beyond that into a silently wrong answer rather than a short one. DeleteSnapshot in particular could delete 1000 objects, report success, and orphan the rest, and the EKS restore path could resolve to a stale snapshot if the newest keys fell past the first page. Add objectstore.ListAll, a shared helper that drains a listing to exhaustion, accumulating Contents and CommonPrefixes across pages and erroring instead of returning a partial result if the server claims truncation with no token to resume from or never finishes within a bounded page budget. All six call sites, plus the ebsfake test double with the same shape, now use it. Bump the predastore pin to dev head ff6a1dc4 so a real cluster picks up the server-side pagination fix this depends on. * fix: use external test packages and exclude test fakes from diff coverage test-package-check requires new test files to bind to the exported API so internals can change without rewriting tests. Move list_test.go and delete_snapshot_pagination_test.go to their _test packages, adding a small export_test.go for the one unexported constant a test needs to assert against. Simplify list_test.go's pagingStore to embed MemoryObjectStore instead of hand-stubbing unused methods. Exclude spinifex/testutil from diff-coverage.sh: those files are fakes and harnesses driven only from other packages' _test.go files, so go test records them as zero-covered under the package they live in however hard another package's tests exercise them, which is not a real coverage gap.
183 lines
5.5 KiB
Bash
Executable File
183 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Diff-based coverage checker for Go projects.
|
|
# Ensures new/changed lines meet a minimum test coverage threshold.
|
|
#
|
|
# Usage: scripts/diff-coverage.sh <coverprofile> [--base <ref>] [--threshold <pct>]
|
|
#
|
|
# Base ref auto-detection:
|
|
# main branch → HEAD~1
|
|
# dev branch → origin/main
|
|
# other branch → origin/dev
|
|
|
|
PROFILE=""
|
|
BASE_REF=""
|
|
THRESHOLD=60
|
|
QUIET="${QUIET:-}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--base) BASE_REF="$2"; shift 2 ;;
|
|
--threshold) THRESHOLD="$2"; shift 2 ;;
|
|
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
*) PROFILE="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$PROFILE" ]]; then
|
|
echo "Usage: scripts/diff-coverage.sh <coverprofile> [--base <ref>] [--threshold <pct>]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$PROFILE" ]]; then
|
|
echo "Error: coverprofile not found: $PROFILE" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -s "$PROFILE" ]]; then
|
|
echo "Error: coverprofile is empty: $PROFILE" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Auto-detect base ref from branch name
|
|
if [[ -z "$BASE_REF" ]]; then
|
|
BRANCH="${GITHUB_REF_NAME:-$(git rev-parse --abbrev-ref HEAD)}"
|
|
case "$BRANCH" in
|
|
main) BASE_REF="HEAD~1" ;;
|
|
dev) BASE_REF="origin/main" ;;
|
|
*) BASE_REF="origin/dev" ;;
|
|
esac
|
|
[[ -z "$QUIET" ]] && echo "Base ref: $BASE_REF (branch: $BRANCH)"
|
|
fi
|
|
|
|
# Verify base ref exists
|
|
if ! git rev-parse --verify "$BASE_REF" &>/dev/null; then
|
|
echo "Error: base ref '$BASE_REF' not reachable." >&2
|
|
echo "Fetch it first: git fetch origin <branch>" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Get Go module path to strip from coverprofile paths
|
|
MODULE_PATH=$(awk '/^module / {print $2; exit}' go.mod)/
|
|
|
|
TMPDIR=$(mktemp -d)
|
|
trap 'rm -rf "$TMPDIR"' EXIT
|
|
|
|
# --- Step 1: Extract added line numbers from diff ---
|
|
# Only non-test .go files that were added or modified.
|
|
# -M enables rename detection so file splits/moves only count truly changed lines.
|
|
# -C enables copy detection for the same reason (file split = partial copy).
|
|
# cmd/installer/ui is excluded: interactive TUI rendering has no meaningful unit
|
|
# test, and the installer is validated by the ISO hardware run instead.
|
|
# spinifex/testutil is excluded because it is fakes and harnesses used only from
|
|
# _test.go files. Coverage is recorded per package under test, so a fake another
|
|
# package's tests drive reads as zero-covered however hard it is exercised.
|
|
git diff "${BASE_REF}" HEAD --unified=0 -M -C --diff-filter=AM -- '*.go' ':!*_test.go' ':!spinifex/migrate/*' ':!cmd/installer/ui/*' ':!spinifex/testutil/*' | awk '
|
|
/^\+\+\+ / {
|
|
file = substr($2, 3)
|
|
next
|
|
}
|
|
/^@@ / {
|
|
plus = $3
|
|
sub(/^\+/, "", plus)
|
|
split(plus, nums, ",")
|
|
start = nums[1] + 0
|
|
count = (nums[2] != "") ? nums[2] + 0 : 1
|
|
if (count == 0) next
|
|
for (i = 0; i < count; i++) {
|
|
print file ":" (start + i)
|
|
}
|
|
}
|
|
' > "$TMPDIR/diff_lines"
|
|
|
|
DIFF_COUNT=$(wc -l < "$TMPDIR/diff_lines" | tr -d ' ')
|
|
if [[ "$DIFF_COUNT" -eq 0 ]]; then
|
|
echo "No new/modified Go source lines — skipping diff coverage."
|
|
exit 0
|
|
fi
|
|
|
|
MIN_LOC=100
|
|
if [[ "$DIFF_COUNT" -lt "$MIN_LOC" ]]; then
|
|
echo "Only $DIFF_COUNT changed lines (< $MIN_LOC) — skipping diff coverage."
|
|
exit 0
|
|
fi
|
|
|
|
# --- Step 2: Build coverage map from coverprofile ---
|
|
# Expand line ranges into file:line → covered (0 or 1)
|
|
awk -v module="$MODULE_PATH" '
|
|
NR == 1 { next }
|
|
{
|
|
split($1, parts, ":")
|
|
file = parts[1]
|
|
sub(module, "", file)
|
|
|
|
split(parts[2], rng, ",")
|
|
split(rng[1], s, ".")
|
|
split(rng[2], e, ".")
|
|
|
|
start = s[1] + 0
|
|
end = e[1] + 0
|
|
hit = ($3 + 0 > 0) ? 1 : 0
|
|
|
|
for (l = start; l <= end; l++) {
|
|
key = file ":" l
|
|
if (!(key in cov) || hit) cov[key] = hit
|
|
}
|
|
}
|
|
END { for (k in cov) print k, cov[k] }
|
|
' "$PROFILE" > "$TMPDIR/cover_map"
|
|
|
|
# --- Step 3: Cross-reference diff lines with coverage map ---
|
|
# Lines in the coverage map are instrumentable code.
|
|
# Lines NOT in the map are non-instrumentable (comments, blank, declarations) — skipped.
|
|
awk -v threshold="$THRESHOLD" '
|
|
NR == FNR {
|
|
cov[$1] = $2
|
|
next
|
|
}
|
|
{
|
|
if ($1 in cov) {
|
|
total++
|
|
if (cov[$1]) {
|
|
covered++
|
|
}
|
|
} else {
|
|
skipped++
|
|
}
|
|
}
|
|
END {
|
|
if (total == 0) {
|
|
printf "No instrumentable lines in diff (%d non-instrumentable skipped) — skipping.\n", skipped+0
|
|
exit 0
|
|
}
|
|
|
|
min_instrumentable = 5
|
|
if (total < min_instrumentable) {
|
|
printf "Only %d instrumentable lines (< %d minimum) — skipping diff coverage.\n", total, min_instrumentable
|
|
exit 0
|
|
}
|
|
|
|
uncovered = total - covered
|
|
pct = (covered / total) * 100
|
|
|
|
quiet = ENVIRON["QUIET"]
|
|
|
|
if (!quiet) {
|
|
printf "\n=== Diff Coverage ===\n"
|
|
printf "Instrumentable lines: %d\n", total
|
|
printf "Covered: %d\n", covered+0
|
|
printf "Uncovered: %d\n", uncovered
|
|
printf "Non-instrumentable: %d (skipped)\n", skipped+0
|
|
printf "Diff coverage: %.1f%%\n", pct
|
|
printf "Threshold: %d%%\n", threshold
|
|
}
|
|
|
|
if (pct + 0.05 < threshold) {
|
|
printf "\nFAIL: diff coverage %.1f%% is below %d%% threshold\n", pct, threshold
|
|
exit 1
|
|
}
|
|
printf "\nPASS: diff coverage %.1f%% meets %d%% threshold\n", pct, threshold
|
|
}
|
|
' "$TMPDIR/cover_map" "$TMPDIR/diff_lines"
|