Files
foxhunt/scripts/enforce_coverage.sh
jgrusewski 57e22c01a8 refactor: update K8s, CI, Docker, Prometheus, scripts, and FXT CLI for api rename
- K8s: rename api-gateway → api manifests, delete web-gateway, update network policies
- CI: rename compile/deploy jobs, delete web-gateway jobs
- Docker: rename service in compose files
- Prometheus: update scrape targets and alert rules
- Scripts: update binary references in build/test/cert scripts
- FXT CLI: rename api_gateway_url → api_url (with serde alias for compat)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:46:36 +01:00

433 lines
14 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Coverage Enforcement Script for Foxhunt HFT System
# Enforces 60% minimum coverage, 75% target for production modules
# Generates JSON and HTML reports, calculates per-module coverage
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Coverage thresholds
MIN_COVERAGE=60
TARGET_COVERAGE=75
PRODUCTION_COVERAGE=75
# Output files
COVERAGE_JSON="coverage_report.json"
COVERAGE_HTML="coverage_html"
MODULE_REPORT="module_coverage.json"
# Production modules (higher threshold)
PRODUCTION_MODULES=(
"trading_engine"
"risk"
"config"
"common"
"services/trading_service"
"services/api"
)
# Print colored message
print_message() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Print section header
print_header() {
echo ""
print_message "$BLUE" "================================"
print_message "$BLUE" "$1"
print_message "$BLUE" "================================"
}
# Check if cargo-llvm-cov is installed
check_dependencies() {
print_header "Checking Dependencies"
if ! command -v cargo-llvm-cov &> /dev/null; then
print_message "$RED" "Error: cargo-llvm-cov not found"
print_message "$YELLOW" "Install with: cargo install cargo-llvm-cov"
exit 1
fi
if ! command -v jq &> /dev/null; then
print_message "$RED" "Error: jq not found"
print_message "$YELLOW" "Install with: sudo apt-get install jq (Ubuntu/Debian) or brew install jq (macOS)"
exit 1
fi
if ! command -v bc &> /dev/null; then
print_message "$RED" "Error: bc not found"
print_message "$YELLOW" "Install with: sudo apt-get install bc (Ubuntu/Debian) or brew install bc (macOS)"
exit 1
fi
print_message "$GREEN" "✓ cargo-llvm-cov found: $(cargo-llvm-cov --version)"
print_message "$GREEN" "✓ jq found: $(jq --version)"
print_message "$GREEN" "✓ bc found: $(bc --version | head -1)"
}
# Clean previous coverage data
clean_coverage() {
print_header "Cleaning Previous Coverage Data"
find . -name "*.profraw" -delete 2>/dev/null || true
rm -rf "$COVERAGE_HTML" 2>/dev/null || true
rm -f "$COVERAGE_JSON" 2>/dev/null || true
rm -f "$MODULE_REPORT" 2>/dev/null || true
rm -f lcov.info 2>/dev/null || true
print_message "$GREEN" "✓ Coverage data cleaned"
}
# Run comprehensive coverage analysis
run_coverage() {
print_header "Running Coverage Analysis"
print_message "$YELLOW" "Running tests with coverage instrumentation..."
print_message "$YELLOW" "This may take several minutes..."
# Run coverage with HTML output (primary run with tests)
print_message "$YELLOW" "Generating HTML report (running tests)..."
if ! timeout 600 cargo llvm-cov --workspace \
--all-features \
--html \
--exclude examples \
--exclude benchmarks \
--output-dir "$COVERAGE_HTML" 2>&1 | tee coverage_output.log; then
print_message "$RED" "Error: Coverage analysis failed"
print_message "$YELLOW" "Check coverage_output.log for details"
exit 1
fi
# Generate LCOV report (reuses cached coverage data)
print_message "$YELLOW" "Generating LCOV report..."
if ! timeout 600 cargo llvm-cov --workspace \
--all-features \
--no-run \
--lcov \
--exclude examples \
--exclude benchmarks \
--output-path lcov.info 2>&1 | tee -a coverage_output.log; then
print_message "$YELLOW" "Warning: LCOV generation failed"
fi
# Generate JSON report (reuses cached coverage data)
print_message "$YELLOW" "Generating JSON report..."
if ! timeout 600 cargo llvm-cov --workspace \
--all-features \
--no-run \
--json \
--exclude examples \
--exclude benchmarks \
--output-path "$COVERAGE_JSON" 2>&1 | tee -a coverage_output.log; then
print_message "$YELLOW" "Warning: JSON generation failed, coverage will be extracted from HTML"
fi
print_message "$GREEN" "✓ Coverage analysis complete"
}
# Extract overall coverage percentage
extract_coverage() {
print_header "Extracting Coverage Metrics"
# Try to extract from JSON first (most reliable)
if [ -f "$COVERAGE_JSON" ]; then
local lines_covered=$(jq '.data[0].totals.lines.covered' "$COVERAGE_JSON" 2>/dev/null || echo "null")
local lines_total=$(jq '.data[0].totals.lines.count' "$COVERAGE_JSON" 2>/dev/null || echo "null")
# Handle null values from empty or malformed JSON
if [ "$lines_covered" == "null" ] || [ "$lines_total" == "null" ]; then
print_message "$YELLOW" "Warning: JSON coverage data is empty or malformed, trying alternative methods..."
elif [ "$lines_total" != "0" ] && [ "$lines_total" != "1" ]; then
# Protect against division by zero
if [ "$lines_total" -gt 0 ] 2>/dev/null; then
COVERAGE_PERCENT=$(echo "scale=2; ($lines_covered * 100) / $lines_total" | bc -l)
print_message "$GREEN" "✓ Coverage extracted from JSON: ${COVERAGE_PERCENT}%"
return 0
fi
fi
fi
# Fallback to lcov parsing
if [ -f lcov.info ]; then
local lines_found=$(grep -o 'LF:[0-9]*' lcov.info 2>/dev/null | cut -d: -f2 | paste -sd+ | bc 2>/dev/null || echo "0")
local lines_hit=$(grep -o 'LH:[0-9]*' lcov.info 2>/dev/null | cut -d: -f2 | paste -sd+ | bc 2>/dev/null || echo "0")
# Protect against division by zero
if [ "$lines_found" != "0" ] && [ "$lines_found" -gt 0 ] 2>/dev/null; then
COVERAGE_PERCENT=$(echo "scale=2; ($lines_hit * 100) / $lines_found" | bc -l)
print_message "$GREEN" "✓ Coverage extracted from LCOV: ${COVERAGE_PERCENT}%"
return 0
fi
fi
# Fallback to summary output
COVERAGE_PERCENT=$(cargo llvm-cov --workspace --all-features --summary-only 2>/dev/null | grep -o '[0-9.]*%' | head -1 | tr -d '%' || echo "0")
if [ "$COVERAGE_PERCENT" == "0" ] || [ -z "$COVERAGE_PERCENT" ]; then
print_message "$RED" "Error: Could not extract coverage percentage from any source"
print_message "$YELLOW" "Checked: JSON report, LCOV file, and summary output"
exit 1
fi
print_message "$GREEN" "✓ Coverage extracted: ${COVERAGE_PERCENT}%"
}
# Calculate per-module coverage
calculate_module_coverage() {
print_header "Calculating Per-Module Coverage"
# Initialize JSON array
echo "[" > "$MODULE_REPORT"
local first=true
# Get list of workspace members
local members=$(cargo metadata --no-deps --format-version 1 | jq -r '.workspace_members[]' | cut -d' ' -f1)
for package in $members; do
print_message "$YELLOW" "Analyzing package: $package"
# Run coverage for single package
local pkg_coverage=$(cargo llvm-cov --package "$package" --all-features --summary-only 2>/dev/null | grep -o '[0-9.]*%' | head -1 | tr -d '%' || echo "0.0")
# Handle empty or invalid coverage values
if [ -z "$pkg_coverage" ] || ! [[ "$pkg_coverage" =~ ^[0-9.]+$ ]]; then
print_message "$YELLOW" " Warning: Could not extract coverage for $package, defaulting to 0.0%"
pkg_coverage="0.0"
fi
# Determine if this is a production module
local is_production=false
for prod_module in "${PRODUCTION_MODULES[@]}"; do
if [[ "$package" == *"$prod_module"* ]]; then
is_production=true
break
fi
done
# Determine threshold
local threshold=$MIN_COVERAGE
if [ "$is_production" = true ]; then
threshold=$PRODUCTION_COVERAGE
fi
# Determine status (using bc -l for floating-point comparison)
local status="PASS"
local color="$GREEN"
if (( $(echo "$pkg_coverage < $threshold" | bc -l) )); then
status="FAIL"
color="$RED"
elif (( $(echo "$pkg_coverage < $TARGET_COVERAGE" | bc -l) )); then
status="WARN"
color="$YELLOW"
fi
# Add comma separator
if [ "$first" = false ]; then
echo "," >> "$MODULE_REPORT"
fi
first=false
# Write JSON entry
cat >> "$MODULE_REPORT" << EOF
{
"package": "$package",
"coverage": $pkg_coverage,
"threshold": $threshold,
"is_production": $is_production,
"status": "$status"
}
EOF
print_message "$color" " $package: ${pkg_coverage}% (threshold: ${threshold}%) - $status"
done
echo "]" >> "$MODULE_REPORT"
print_message "$GREEN" "✓ Module coverage report saved to $MODULE_REPORT"
}
# Generate coverage summary
generate_summary() {
print_header "Coverage Summary"
# Determine overall status
local status="PASS"
local color="$GREEN"
local badge_color="brightgreen"
if (( $(echo "$COVERAGE_PERCENT < $MIN_COVERAGE" | bc -l) )); then
status="FAIL"
color="$RED"
badge_color="red"
elif (( $(echo "$COVERAGE_PERCENT < $TARGET_COVERAGE" | bc -l) )); then
status="WARN"
color="$YELLOW"
badge_color="yellow"
fi
# Print summary
print_message "$color" "Overall Coverage: ${COVERAGE_PERCENT}%"
print_message "$BLUE" "Minimum Required: ${MIN_COVERAGE}%"
print_message "$BLUE" "Target Coverage: ${TARGET_COVERAGE}%"
print_message "$color" "Status: $status"
# Generate badge markdown
local badge_url="https://img.shields.io/badge/coverage-${COVERAGE_PERCENT}%25-${badge_color}"
echo "![Coverage Badge]($badge_url)" > coverage_badge.md
# Generate detailed summary for PR comments
cat > coverage_summary.md << EOF
## 📊 Code Coverage Report
**Overall Coverage**: ${COVERAGE_PERCENT}%
**Minimum Required**: ${MIN_COVERAGE}%
**Target**: ${TARGET_COVERAGE}%
**Status**: $status
![Coverage Badge]($badge_url)
### Module Coverage Breakdown
| Module | Coverage | Threshold | Status |
|--------|----------|-----------|--------|
EOF
# Add module rows
jq -r '.[] | "| \(.package) | \(.coverage)% | \(.threshold)% | \(.status) |"' "$MODULE_REPORT" >> coverage_summary.md
cat >> coverage_summary.md << EOF
### Coverage Targets
- **Production Modules** (Trading Engine, Risk, API Gateway): ${PRODUCTION_COVERAGE}%
- **Core Modules** (Config, Common, Data): ${TARGET_COVERAGE}%
- **Supporting Modules** (Tests, Utilities): ${MIN_COVERAGE}%
[📈 View Detailed HTML Report](./coverage_html/index.html)
EOF
print_message "$GREEN" "✓ Coverage summary generated"
}
# Check coverage thresholds
check_thresholds() {
print_header "Checking Coverage Thresholds"
local failed_modules=()
# Check each module
while IFS= read -r module; do
local package=$(echo "$module" | jq -r '.package')
local coverage=$(echo "$module" | jq -r '.coverage')
local threshold=$(echo "$module" | jq -r '.threshold')
local status=$(echo "$module" | jq -r '.status')
if [ "$status" = "FAIL" ]; then
failed_modules+=("$package (${coverage}% < ${threshold}%)")
fi
done < <(jq -c '.[]' "$MODULE_REPORT")
# Check overall coverage
if (( $(echo "$COVERAGE_PERCENT < $MIN_COVERAGE" | bc -l) )); then
print_message "$RED" "✗ Overall coverage ${COVERAGE_PERCENT}% is below minimum ${MIN_COVERAGE}%"
if [ ${#failed_modules[@]} -gt 0 ]; then
print_message "$RED" "Failed modules:"
for module in "${failed_modules[@]}"; do
print_message "$RED" " - $module"
done
fi
exit 1
fi
if [ ${#failed_modules[@]} -gt 0 ]; then
print_message "$YELLOW" "⚠ Some modules below their thresholds (non-blocking):"
for module in "${failed_modules[@]}"; do
print_message "$YELLOW" " - $module"
done
fi
print_message "$GREEN" "✓ Coverage thresholds met"
}
# Generate coverage artifacts
generate_artifacts() {
print_header "Generating Coverage Artifacts"
# Create artifacts directory
mkdir -p coverage_artifacts
# Copy reports
cp -r "$COVERAGE_HTML" coverage_artifacts/ 2>/dev/null || true
cp lcov.info coverage_artifacts/ 2>/dev/null || true
cp "$COVERAGE_JSON" coverage_artifacts/ 2>/dev/null || true
cp "$MODULE_REPORT" coverage_artifacts/ 2>/dev/null || true
cp coverage_summary.md coverage_artifacts/ 2>/dev/null || true
cp coverage_badge.md coverage_artifacts/ 2>/dev/null || true
# Generate index page
cat > coverage_artifacts/index.html << EOF
<!DOCTYPE html>
<html>
<head>
<title>Foxhunt Coverage Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; }
.metric { font-size: 24px; font-weight: bold; }
.pass { color: green; }
.warn { color: orange; }
.fail { color: red; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
</style>
</head>
<body>
<h1>Foxhunt Code Coverage Report</h1>
<div class="metric">Overall Coverage: <span class="$([ "$status" = "PASS" ] && echo "pass" || echo "warn")">${COVERAGE_PERCENT}%</span></div>
<p>Generated: $(date)</p>
<ul>
<li><a href="coverage_html/index.html">Detailed HTML Report</a></li>
<li><a href="module_coverage.json">Module Coverage JSON</a></li>
<li><a href="coverage_summary.md">Coverage Summary</a></li>
</ul>
</body>
</html>
EOF
print_message "$GREEN" "✓ Coverage artifacts generated in coverage_artifacts/"
}
# Main execution
main() {
print_header "Foxhunt Coverage Enforcement"
check_dependencies
clean_coverage
run_coverage
extract_coverage
calculate_module_coverage
generate_summary
generate_artifacts
check_thresholds
print_header "Coverage Analysis Complete"
print_message "$GREEN" "✓ All checks passed!"
}
# Run main function
main "$@"