- 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>
433 lines
14 KiB
Bash
Executable File
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.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
|
|
<!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 "$@"
|