#!/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_gateway" ) # 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.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  ### 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
Generated: $(date)
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 "$@"