mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
ci: enforce code coverage
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+167
@@ -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"
|
||||
Reference in New Issue
Block a user