# Automated Code Coverage Enforcement for Foxhunt HFT System # TDD-compliant coverage tracking with 60% minimum, 75% target name: Code Coverage on: push: branches: [ main, master, develop ] pull_request: branches: [ main, master, develop ] schedule: # Run coverage analysis daily at 3 AM UTC - cron: '0 3 * * *' env: CARGO_TERM_COLOR: always RUSTFLAGS: "-C instrument-coverage" LLVM_PROFILE_FILE: "foxhunt-%p-%m.profraw" MIN_COVERAGE: 60 TARGET_COVERAGE: 75 jobs: coverage: name: Coverage Enforcement runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target/ key: ${{ runner.os }}-cargo-coverage-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-coverage- ${{ runner.os }}-cargo- - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y \ build-essential \ pkg-config \ libssl-dev \ postgresql-client \ bc \ jq - name: Run coverage enforcement script id: coverage run: | ./scripts/enforce_coverage.sh || echo "COVERAGE_FAILED=true" >> $GITHUB_ENV # Extract coverage percentage for outputs if [ -f coverage_report.json ]; then COVERAGE_PERCENT=$(jq -r '.data[0].totals.lines.percent' coverage_report.json 2>/dev/null || echo "0") else COVERAGE_PERCENT=$(grep -o 'Overall Coverage: [0-9.]*%' coverage_summary.md | grep -o '[0-9.]*' || echo "0") fi echo "COVERAGE_PERCENT=$COVERAGE_PERCENT" >> $GITHUB_ENV echo "coverage=$COVERAGE_PERCENT" >> $GITHUB_OUTPUT - name: Upload HTML coverage report uses: actions/upload-artifact@v4 if: always() with: name: html-coverage-report path: coverage_artifacts/coverage_html/ retention-days: 30 - name: Upload LCOV report uses: actions/upload-artifact@v4 if: always() with: name: lcov-report path: coverage_artifacts/lcov.info retention-days: 30 - name: Upload JSON reports uses: actions/upload-artifact@v4 if: always() with: name: json-reports path: | coverage_artifacts/coverage_report.json coverage_artifacts/module_coverage.json retention-days: 30 - name: Upload coverage summary uses: actions/upload-artifact@v4 if: always() with: name: coverage-summary path: coverage_artifacts/coverage_summary.md retention-days: 7 - name: Generate coverage badge if: always() run: | COVERAGE_PERCENT="${{ env.COVERAGE_PERCENT }}" if (( $(echo "$COVERAGE_PERCENT >= 75" | bc -l) )); then BADGE_COLOR="brightgreen" elif (( $(echo "$COVERAGE_PERCENT >= 60" | bc -l) )); then BADGE_COLOR="yellow" else BADGE_COLOR="red" fi echo "BADGE_COLOR=$BADGE_COLOR" >> $GITHUB_ENV # Update coverage badge in README BADGE_URL="https://img.shields.io/badge/coverage-${COVERAGE_PERCENT}%25-${BADGE_COLOR}" echo "![Coverage]($BADGE_URL)" > coverage_badge.md - name: Comment PR with coverage if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); try { const summary = fs.readFileSync('coverage_artifacts/coverage_summary.md', 'utf8'); // Add comparison with base branch if available let comment = summary + '\n\n---\n'; comment += `\n**Coverage Delta**: Compare with base branch to see coverage changes\n`; comment += `\n[📊 View Full HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})\n`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: comment }); } catch (error) { console.error('Failed to post coverage comment:', error); } - name: Post coverage to job summary if: always() run: | if [ -f coverage_artifacts/coverage_summary.md ]; then cat coverage_artifacts/coverage_summary.md >> $GITHUB_STEP_SUMMARY fi - name: Check coverage threshold if: always() run: | COVERAGE_PERCENT="${{ env.COVERAGE_PERCENT }}" MIN_COVERAGE="${{ env.MIN_COVERAGE }}" if (( $(echo "$COVERAGE_PERCENT < $MIN_COVERAGE" | bc -l) )); then echo "❌ Coverage $COVERAGE_PERCENT% is below the required $MIN_COVERAGE% threshold" exit 1 else echo "✅ Coverage $COVERAGE_PERCENT% meets the required $MIN_COVERAGE% threshold" fi # Per-module coverage analysis module-coverage: name: Module Coverage Analysis runs-on: ubuntu-latest continue-on-error: true strategy: matrix: component: - name: "trading_engine" packages: "trading_engine" target: 75 - name: "risk" packages: "risk" target: 75 - name: "api_gateway" packages: "services/api_gateway" target: 75 - name: "trading_service" packages: "services/trading_service" target: 75 - name: "config" packages: "config" target: 75 - name: "common" packages: "common" target: 75 - name: "backtesting" packages: "backtesting" target: 60 - name: "ml" packages: "ml" target: 60 - name: "data" packages: "data" target: 60 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target/ key: ${{ runner.os }}-module-${{ matrix.component.name }}-${{ hashFiles('**/Cargo.lock') }} - name: Run component coverage continue-on-error: true run: | # Handle path-based packages if [[ "${{ matrix.component.packages }}" == *"/"* ]]; then # For services, navigate to directory PKG_PATH="${{ matrix.component.packages }}" cargo llvm-cov \ --manifest-path "$PKG_PATH/Cargo.toml" \ --all-features \ --lcov --output-path ${{ matrix.component.name }}.lcov \ --html --output-dir coverage_${{ matrix.component.name }} || true else # For workspace members cargo llvm-cov \ --package ${{ matrix.component.packages }} \ --all-features \ --lcov --output-path ${{ matrix.component.name }}.lcov \ --html --output-dir coverage_${{ matrix.component.name }} || true fi - name: Upload component coverage uses: actions/upload-artifact@v4 if: always() with: name: coverage-${{ matrix.component.name }} path: | ${{ matrix.component.name }}.lcov coverage_${{ matrix.component.name }}/ retention-days: 14 # Coverage trend analysis for main branch coverage-trends: name: Coverage Trend Analysis runs-on: ubuntu-latest needs: [coverage] if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download coverage reports uses: actions/download-artifact@v4 with: name: lcov-report - name: Store coverage history run: | # Create coverage history directory mkdir -p .coverage-history # Extract coverage percentage from LCOV if [ -f lcov.info ]; then LINES_FOUND=$(grep -o 'LF:[0-9]*' lcov.info | cut -d: -f2 | paste -sd+ | bc) LINES_HIT=$(grep -o 'LH:[0-9]*' lcov.info | cut -d: -f2 | paste -sd+ | bc) COVERAGE_PERCENT=$(echo "scale=2; ($LINES_HIT * 100) / $LINES_FOUND" | bc) # Store in history file echo "$(date -Iseconds),$COVERAGE_PERCENT,${{ github.sha }}" >> .coverage-history/coverage.csv # Keep only last 100 entries if [ -f .coverage-history/coverage.csv ]; then tail -100 .coverage-history/coverage.csv > .coverage-history/coverage.csv.tmp mv .coverage-history/coverage.csv.tmp .coverage-history/coverage.csv fi fi - name: Generate trend chart run: | # Generate simple ASCII chart of coverage trends if [ -f .coverage-history/coverage.csv ]; then echo "## Coverage Trend (Last 10 commits)" > coverage_trend.md echo "\`\`\`" >> coverage_trend.md tail -10 .coverage-history/coverage.csv | while IFS=, read -r date coverage commit; do echo "$date: $coverage% (${commit:0:7})" >> coverage_trend.md done echo "\`\`\`" >> coverage_trend.md cat coverage_trend.md >> $GITHUB_STEP_SUMMARY fi - name: Commit coverage history run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add .coverage-history/ git diff --staged --quiet || git commit -m "Update coverage history [skip ci]" git push || echo "No changes to push"