diff --git a/.github/workflows/hive.yml b/.github/workflows/hive.yml index 41ff0a19d..407f0a2cb 100644 --- a/.github/workflows/hive.yml +++ b/.github/workflows/hive.yml @@ -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 diff --git a/Makefile b/Makefile index 99e550751..a5d86a9de 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/scripts/diff-coverage.sh b/scripts/diff-coverage.sh new file mode 100755 index 000000000..909bdb15a --- /dev/null +++ b/scripts/diff-coverage.sh @@ -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 [--base ] [--threshold ] +# +# 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 [--base ] [--threshold ]" >&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 " >&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"