ci: enforce code coverage

This commit is contained in:
Julian Sommer
2026-02-20 08:58:44 +10:00
parent eba702b1f2
commit 3dc3d41d79
3 changed files with 191 additions and 7 deletions
+10 -6
View File
@@ -23,19 +23,20 @@ jobs:
uses: actions/checkout@v6
with:
path: hive
fetch-depth: 2
- name: Checkout Viperblock
uses: actions/checkout@v6
with:
repository: mulgadc/viperblock
ref: dev
ref: ${{ github.ref_name }}
path: viperblock
- name: Checkout Predastore
uses: actions/checkout@v6
with:
repository: mulgadc/predastore
ref: dev
ref: ${{ github.ref_name }}
path: predastore
- name: Setup Go Workspace
@@ -50,8 +51,11 @@ jobs:
- name: Build Hive
run: make -C hive build
- name: Run Tests
run: make -C hive test
- name: Run Tests with Coverage
run: make -C hive test-cover
- name: Check Diff Coverage
run: cd hive && scripts/diff-coverage.sh coverage.out --base HEAD~1
lint_and_security:
name: Hive Lint and Security
@@ -67,14 +71,14 @@ jobs:
uses: actions/checkout@v6
with:
repository: mulgadc/viperblock
ref: dev
ref: ${{ github.ref_name }}
path: viperblock
- name: Checkout Predastore
uses: actions/checkout@v6
with:
repository: mulgadc/predastore
ref: dev
ref: ${{ github.ref_name }}
path: predastore
- name: Setup Go Workspace
+14 -1
View File
@@ -60,6 +60,19 @@ test:
@echo -e "\n....Running tests for $(GO_PROJECT_NAME)...."
LOG_IGNORE=1 go test -v -timeout 120s ./...
# Run unit tests with coverage profile
COVERPROFILE ?= coverage.out
test-cover:
@echo -e "\n....Running tests with coverage for $(GO_PROJECT_NAME)...."
LOG_IGNORE=1 go test -v -timeout 120s -coverprofile=$(COVERPROFILE) -covermode=atomic ./hive/...
@echo ""
@echo "=== Total Coverage ==="
@go tool cover -func=$(COVERPROFILE) | tail -1
# Check that new/changed code meets coverage threshold (runs tests first)
diff-coverage: test-cover
@scripts/diff-coverage.sh $(COVERPROFILE)
bench:
@echo -e "\n....Running benchmarks for $(GO_PROJECT_NAME)...."
$(MAKE) easyjson
@@ -192,7 +205,7 @@ test-docker: test-docker-build
@echo -e "\n....Running Multi-Node E2E Tests...."
docker run --privileged --rm -v /dev/kvm:/dev/kvm --cap-add=NET_ADMIN $(E2E_IMAGE) ./tests/e2e/run-multinode-e2e.sh
.PHONY: build build-ui go_build go_run preflight test bench run clean \
.PHONY: build build-ui go_build go_run preflight test test-cover diff-coverage bench run clean \
install-system install-go install-aws quickinstall \
format check-format modernize check-modernize vet security-check \
test-docker-build test-docker-single test-docker-multi test-docker
+167
View File
@@ -0,0 +1,167 @@
#!/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:
# dev branch → origin/main
# main branch → HEAD~1
# other branch → origin/dev
PROFILE=""
BASE_REF=""
THRESHOLD=70
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
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
git diff "${BASE_REF}" HEAD --unified=0 --diff-filter=AM -- '*.go' ':!*_test.go' | 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
# --- 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 {
uncov[uncov_n++] = $1
}
} else {
skipped++
}
}
END {
if (total == 0) {
printf "No instrumentable lines in diff (%d non-instrumentable skipped) — skipping.\n", skipped+0
exit 0
}
uncovered = total - covered
pct = (covered / total) * 100
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 (uncovered > 0) {
printf "\nUncovered lines:\n"
for (i = 0; i < uncov_n; i++) {
printf " %s\n", uncov[i]
}
}
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"