Skip to content

Commit 12a3f7c

Browse files
author
Nathan Gillett
committed
Adopt tiered coverage gates for SDK package
Move threshold enforcement from pytest fail-under to tiered check-coverage.sh (90% total, 95% on intentproof/). Signed-off-by: Nathan Gillett <nathan@intentproof.io>
1 parent f7fcbca commit 12a3f7c

8 files changed

Lines changed: 123 additions & 36 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,4 @@ jobs:
2222
pip install -e ".[dev]"
2323
2424
- name: Run tests with coverage
25-
run: pytest -q
26-
27-
- name: Enforce coverage threshold
28-
run: bash ./scripts/check-coverage.sh 95
25+
run: bash ./scripts/run-coverage-gate.sh

.github/workflows/release-pypi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
run: pytest -q
4747

4848
- name: Enforce coverage threshold
49-
run: bash ./scripts/check-coverage.sh 95
49+
run: bash ./scripts/run-coverage-gate.sh
5050

5151
build:
5252
name: "IntentProof Release: Build Python Distributions"

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ vectors. Run `pytest` locally before publishing.
3737

3838
```bash
3939
pytest
40-
bash ./scripts/check-coverage.sh 95
40+
bash ./scripts/run-coverage-gate.sh
4141
```
4242

43-
CI enforces at least 95% line coverage on `src/intentproof/` (see
44-
`pyproject.toml` and `scripts/check-coverage.sh`).
43+
Tiered coverage: **90%** total and **95%** on `src/intentproof/` (see
44+
`scripts/README-coverage-tiers.md`).
4545

4646
## Release
4747

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ source = ["intentproof"]
3636
omit = []
3737

3838
[tool.coverage.report]
39-
fail_under = 95
4039
show_missing = true
4140
skip_empty = true
4241

@@ -46,5 +45,4 @@ pythonpath = ["src"]
4645
addopts = [
4746
"--cov=intentproof",
4847
"--cov-report=term-missing",
49-
"--cov-fail-under=95",
5048
]

scripts/README-coverage-tiers.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Tiered coverage policy (intentproof-sdk-python)
2+
3+
| Tier | Minimum | Scope |
4+
|------|---------|--------|
5+
| **Total** | 90% | `src/intentproof/` |
6+
| **Critical** | 95% | Same — public SDK surface |
7+
8+
Configuration: `scripts/coverage-tiers.conf`.

scripts/check-coverage.sh

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,107 @@
11
#!/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)
26

37
set -euo pipefail
48

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
1724
fi
1825

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
2128
exit 2
2229
fi
2330

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
2780
exit 2
2881
fi
2982

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
3185

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"
34100

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
38104
fi
39105

40-
echo "FAIL: coverage threshold not met" >&2
41-
exit 1
106+
echo "PASS: coverage thresholds met"
107+
exit 0

scripts/coverage-tiers.conf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# IntentProof tiered coverage (intentproof-sdk-python)
2+
3+
TOTAL_MIN=90
4+
5+
CRITICAL_RULES=(
6+
"95:/intentproof/"
7+
)

scripts/run-coverage-gate.sh

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,16 @@ else
1212
PYTHON=python3
1313
fi
1414

15+
COVERAGE=()
16+
if command -v coverage >/dev/null 2>&1; then
17+
COVERAGE=(coverage)
18+
elif "$PYTHON" -m coverage --version >/dev/null 2>&1; then
19+
COVERAGE=("$PYTHON" -m coverage)
20+
else
21+
echo "coverage CLI not found; install dev deps with: pip install -e \".[dev]\"" >&2
22+
exit 2
23+
fi
24+
1525
"$PYTHON" -m pytest -q
16-
exec bash ./scripts/check-coverage.sh 95
26+
"${COVERAGE[@]}" json -o coverage.json
27+
exec bash ./scripts/check-coverage.sh coverage.json

0 commit comments

Comments
 (0)