diff --git a/scripts/frontend-test-coverage.sh b/scripts/frontend-test-coverage.sh index 3444605d..434bbb39 100755 --- a/scripts/frontend-test-coverage.sh +++ b/scripts/frontend-test-coverage.sh @@ -37,25 +37,68 @@ fi # Extract and print total coverage summary using python LINES_PERCENT=$(python3 - <<'PY' import json -summary = json.load(open('coverage/coverage-summary.json'))['total'] +import sys + +try: + with open('coverage/coverage-summary.json') as f: + summary = json.load(f) +except json.JSONDecodeError as e: + print(f"Error: Invalid JSON in coverage-summary.json: {e}", file=sys.stderr) + sys.exit(1) +except KeyError as e: + print(f"Error: Missing key in coverage-summary.json: {e}", file=sys.stderr) + sys.exit(1) + +# Validate structure +if 'total' not in summary: + print("Error: 'total' key not found in coverage-summary.json", file=sys.stderr) + sys.exit(1) + +total = summary['total'] +metrics = ['statements', 'branches', 'functions', 'lines'] +for metric in metrics: + if metric not in total: + print(f"Error: '{metric}' metric missing from coverage summary", file=sys.stderr) + sys.exit(1) + if not isinstance(total[metric], dict) or 'pct' not in total[metric]: + print(f"Error: '{metric}' metric missing 'pct' field", file=sys.stderr) + sys.exit(1) + def fmt(metric): return f"{metric['pct']}% ({metric['covered']}/{metric['total']})" print("Frontend coverage summary:") -print(f" Statements: {fmt(summary['statements'])}") -print(f" Branches: {fmt(summary['branches'])}") -print(f" Functions: {fmt(summary['functions'])}") -print(f" Lines: {fmt(summary['lines'])}") +print(f" Statements: {fmt(total['statements'])}") +print(f" Branches: {fmt(total['branches'])}") +print(f" Functions: {fmt(total['functions'])}") +print(f" Lines: {fmt(total['lines'])}") -print(summary['lines']['pct']) +lines_pct = total['lines']['pct'] + +# Validate that lines pct is numeric +if not isinstance(lines_pct, (int, float)): + print(f"Error: Coverage percentage is not numeric: {lines_pct} ({type(lines_pct).__name__})", file=sys.stderr) + sys.exit(1) + +# Print just the numeric value +print(lines_pct) PY ) python3 - <= minimum else "FAIL" print(f"Coverage gate: {status} (lines {total}% vs minimum {minimum}%)") diff --git a/scripts/go-test-coverage.sh b/scripts/go-test-coverage.sh index de93b6fb..f9f72bbb 100755 --- a/scripts/go-test-coverage.sh +++ b/scripts/go-test-coverage.sh @@ -101,10 +101,46 @@ COVERAGE_OUTPUT=$(timeout 180 go tool cover -func="$COVERAGE_FILE" 2>&1) || { # Extract and display the summary line (total coverage) TOTAL_LINE=$(echo "$COVERAGE_OUTPUT" | awk '/^total:/ {line=$0} END {print line}') + +if [ -z "$TOTAL_LINE" ]; then + echo "Error: Coverage report missing 'total:' line" + echo "Coverage output:" + echo "$COVERAGE_OUTPUT" + exit 1 +fi + echo "$TOTAL_LINE" -# Extract total coverage percentage -TOTAL_PERCENT=$(echo "$TOTAL_LINE" | awk '{print substr($3, 1, length($3)-1)}') +# Extract total coverage percentage (format: "total: (statements)% (branches)% (functions)% (lines)% XX.X%") +# Field $3 is the third space-separated token (line count %) +# We need to remove trailing '%' character +TOTAL_PERCENT=$(echo "$TOTAL_LINE" | awk '{ + if (NF < 3) { + print "ERROR: Invalid coverage line format" > "/dev/stderr" + exit 1 + } + # Extract last field and remove trailing % + last_field = $NF + if (last_field !~ /^[0-9]+(\.[0-9]+)?%$/) { + printf "ERROR: Last field is not a valid percentage: %s\n", last_field > "/dev/stderr" + exit 1 + } + # Remove trailing % + gsub(/%$/, "", last_field) + print last_field +}') + +if [ -z "$TOTAL_PERCENT" ] || [ "$TOTAL_PERCENT" = "ERROR" ]; then + echo "Error: Could not extract coverage percentage from: $TOTAL_LINE" + exit 1 +fi + +# Validate that extracted value is numeric (allows decimals and integers) +if ! echo "$TOTAL_PERCENT" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then + echo "Error: Extracted coverage value is not numeric: '$TOTAL_PERCENT'" + echo "Source line: $TOTAL_LINE" + exit 1 +fi echo "Computed coverage: ${TOTAL_PERCENT}% (minimum required ${MIN_COVERAGE}%)" @@ -112,11 +148,28 @@ export TOTAL_PERCENT export MIN_COVERAGE python3 - <<'PY' -import os, sys -from decimal import Decimal +import os +import sys +from decimal import Decimal, InvalidOperation + +try: + total = Decimal(os.environ['TOTAL_PERCENT']) +except InvalidOperation as e: + print(f"Error: TOTAL_PERCENT is not numeric: '{os.environ['TOTAL_PERCENT']}' ({e})", file=sys.stderr) + sys.exit(1) +except KeyError: + print("Error: TOTAL_PERCENT environment variable not set", file=sys.stderr) + sys.exit(1) + +try: + minimum = Decimal(os.environ['MIN_COVERAGE']) +except InvalidOperation as e: + print(f"Error: MIN_COVERAGE is not numeric: '{os.environ['MIN_COVERAGE']}' ({e})", file=sys.stderr) + sys.exit(1) +except KeyError: + print("Error: MIN_COVERAGE environment variable not set", file=sys.stderr) + sys.exit(1) -total = Decimal(os.environ['TOTAL_PERCENT']) -minimum = Decimal(os.environ['MIN_COVERAGE']) if total < minimum: print(f"Coverage {total}% is below required {minimum}% (set CHARON_MIN_COVERAGE or CPM_MIN_COVERAGE to override)", file=sys.stderr) sys.exit(1)