|
1 | 1 | #!/usr/bin/env bash |
| 2 | +# Tiered statement coverage using coverage.py JSON output. |
| 3 | +# |
| 4 | +# Usage: check-coverage.sh [coverage_json] |
| 5 | +# Default: coverage.json (run pytest with --cov first) |
2 | 6 |
|
3 | 7 | set -euo pipefail |
4 | 8 |
|
5 | | -MIN_COVERAGE="${1:-95}" |
6 | | - |
7 | | -COVERAGE=() |
8 | | -if command -v coverage >/dev/null 2>&1; then |
9 | | - COVERAGE=(coverage) |
10 | | -else |
11 | | - for py in python python3; do |
12 | | - if command -v "$py" >/dev/null 2>&1 && "$py" -m coverage --version >/dev/null 2>&1; then |
13 | | - COVERAGE=("$py" -m coverage) |
14 | | - break |
15 | | - fi |
16 | | - done |
| 9 | +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 10 | +CONF="${ROOT}/scripts/coverage-tiers.conf" |
| 11 | +COVERAGE_JSON="${1:-coverage.json}" |
| 12 | + |
| 13 | +if [[ ! -f "$CONF" ]]; then |
| 14 | + echo "coverage tiers config not found: $CONF" >&2 |
| 15 | + exit 2 |
| 16 | +fi |
| 17 | + |
| 18 | +# shellcheck disable=SC1090 |
| 19 | +source "$CONF" |
| 20 | + |
| 21 | +if [[ -z "${TOTAL_MIN:-}" ]]; then |
| 22 | + echo "coverage-tiers.conf must set TOTAL_MIN" >&2 |
| 23 | + exit 2 |
17 | 24 | fi |
18 | 25 |
|
19 | | -if [[ ${#COVERAGE[@]} -eq 0 ]]; then |
20 | | - echo "coverage CLI not found; install dev deps with: pip install -e \".[dev]\"" >&2 |
| 26 | +if [[ ! -f "$COVERAGE_JSON" ]]; then |
| 27 | + echo "coverage json not found: $COVERAGE_JSON (run pytest with --cov first)" >&2 |
21 | 28 | exit 2 |
22 | 29 | fi |
23 | 30 |
|
24 | | -TOTAL_LINE="$("${COVERAGE[@]}" report --include='src/intentproof/*' | awk '/^TOTAL/{print; exit}')" |
25 | | -if [[ -z "$TOTAL_LINE" ]]; then |
26 | | - echo "unable to read total coverage; run pytest with --cov first" >&2 |
| 31 | +rules_file="$(mktemp)" |
| 32 | +trap 'rm -f "$rules_file"' EXIT |
| 33 | +printf '%s\n' "${CRITICAL_RULES[@]}" >"$rules_file" |
| 34 | + |
| 35 | +report_threshold() { |
| 36 | + local label="$1" covered="$2" total="$3" min="$4" |
| 37 | + local pct |
| 38 | + pct="$(awk -v c="$covered" -v t="$total" 'BEGIN { printf "%.1f", 100 * c / t }')" |
| 39 | + echo "${label}: ${pct}% (${covered}/${total} statements), minimum ${min}%" |
| 40 | + if awk -v c="$covered" -v t="$total" -v min="$min" \ |
| 41 | + 'BEGIN { exit !(t > 0 && c * 100 >= t * min) }'; then |
| 42 | + echo " PASS" |
| 43 | + return 0 |
| 44 | + fi |
| 45 | + echo " FAIL" >&2 |
| 46 | + return 1 |
| 47 | +} |
| 48 | + |
| 49 | +stats_for_prefix() { |
| 50 | + local prefix="$1" |
| 51 | + python3 - "$ROOT/$COVERAGE_JSON" "$prefix" <<'PY' |
| 52 | +import json |
| 53 | +import sys |
| 54 | +
|
| 55 | +path = sys.argv[1] |
| 56 | +prefix = sys.argv[2] |
| 57 | +data = json.load(open(path)) |
| 58 | +covered = 0 |
| 59 | +total = 0 |
| 60 | +for file_path, entry in data.get("files", {}).items(): |
| 61 | + if prefix not in file_path.replace("\\", "/"): |
| 62 | + continue |
| 63 | + summary = entry.get("summary", {}) |
| 64 | + stmts = int(summary.get("num_statements", 0)) |
| 65 | + if stmts == 0: |
| 66 | + continue |
| 67 | + pct = float(summary.get("percent_covered", 0)) |
| 68 | + total += stmts |
| 69 | + covered += int(round(stmts * pct / 100.0)) |
| 70 | +print(covered, total) |
| 71 | +PY |
| 72 | +} |
| 73 | + |
| 74 | +read -r TOTAL_COVERED TOTAL_STMTS <<EOF |
| 75 | +$(stats_for_prefix "") |
| 76 | +EOF |
| 77 | + |
| 78 | +if [[ -z "$TOTAL_STMTS" || "$TOTAL_STMTS" -eq 0 ]]; then |
| 79 | + echo "unable to read total coverage from $COVERAGE_JSON" >&2 |
27 | 80 | exit 2 |
28 | 81 | fi |
29 | 82 |
|
30 | | -TOTAL_PERCENT="$(printf '%s' "$TOTAL_LINE" | awk '{print $NF}' | tr -d '%')" |
| 83 | +fail=0 |
| 84 | +report_threshold "Total coverage" "$TOTAL_COVERED" "$TOTAL_STMTS" "$TOTAL_MIN" || fail=1 |
31 | 85 |
|
32 | | -echo "Total coverage: ${TOTAL_PERCENT}%" |
33 | | -echo "Minimum required: ${MIN_COVERAGE}%" |
| 86 | +echo "Critical tiers:" |
| 87 | +while IFS= read -r rule; do |
| 88 | + [[ -n "$rule" ]] || continue |
| 89 | + min="${rule%%:*}" |
| 90 | + prefix="${rule#*:}" |
| 91 | + read -r c t <<EOF |
| 92 | +$(stats_for_prefix "$prefix") |
| 93 | +EOF |
| 94 | + if [[ "$t" -eq 0 ]]; then |
| 95 | + echo " ${prefix} (min ${min}%): no statements in profile, skipped" |
| 96 | + continue |
| 97 | + fi |
| 98 | + report_threshold " ${prefix}" "$c" "$t" "$min" || fail=1 |
| 99 | +done <"$rules_file" |
34 | 100 |
|
35 | | -if awk -v got="$TOTAL_PERCENT" -v min="$MIN_COVERAGE" 'BEGIN { exit !(got + 0 >= min + 0) }'; then |
36 | | - echo "PASS: coverage threshold met" |
37 | | - exit 0 |
| 101 | +if [[ "$fail" -ne 0 ]]; then |
| 102 | + echo "FAIL: coverage threshold not met" >&2 |
| 103 | + exit 1 |
38 | 104 | fi |
39 | 105 |
|
40 | | -echo "FAIL: coverage threshold not met" >&2 |
41 | | -exit 1 |
| 106 | +echo "PASS: coverage thresholds met" |
| 107 | +exit 0 |
0 commit comments