#!/usr/bin/env bash # #region Coverage.Summary [C:3] [TYPE Module] [SEMANTICS coverage,testing,reporting,ci] # @BRIEF Run backend + frontend tests with coverage, generate unified HTML summary report. # @LAYER DevOps # @RELATION DEPENDS_ON -> [pytest] # @RELATION DEPENDS_ON -> [vitest] # @RELATION DEPENDS_ON -> [coverage.py] # @POST Unified HTML report written to $OUTPUT_DIR/index.html # @POST Backend HTML coverage at $OUTPUT_DIR/backend/ # @POST Frontend HTML coverage at $OUTPUT_DIR/frontend/ # @SIDE_EFFECT Runs tests; writes coverage artifacts to $OUTPUT_DIR/ # # Usage: ./scripts/coverage-summary.sh [options] # # Options: # --backend-only Run only backend tests # --frontend-only Run only frontend tests # --unit Run backend unit tests instead of integration (skips Docker) # --skip-integration Skip backend integration tests (requires --unit to run backend) # --output-dir Output directory (default: coverage-summary) # --help Show this help # # Examples: # ./scripts/coverage-summary.sh # Full run (backend integration + frontend) # ./scripts/coverage-summary.sh --unit # Backend unit + frontend # ./scripts/coverage-summary.sh --frontend-only # Frontend only # ./scripts/coverage-summary.sh --backend-only --unit # Backend unit tests only # ./scripts/coverage-summary.sh --skip-integration # Frontend only (backend skipped entirely) # #endregion Coverage.Summary # Do NOT use -e: test failures must not abort the script before report generation set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # ── Defaults ───────────────────────────────────────────────────────────── RUN_BACKEND=true RUN_FRONTEND=true BACKEND_MODE="integration" # integration | unit OUTPUT_DIR="${PROJECT_DIR}/coverage-summary" TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" # ── Parse args ─────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --backend-only) RUN_FRONTEND=false; shift ;; --frontend-only) RUN_BACKEND=false; shift ;; --unit) BACKEND_MODE="unit"; shift ;; --skip-integration) RUN_BACKEND=false; shift ;; --output-dir) OUTPUT_DIR="$2"; shift 2 ;; --help|-h) echo "Usage: $(basename "$0") [options]" echo "" echo "Options:" echo " --backend-only Run only backend tests" echo " --frontend-only Run only frontend tests" echo " --unit Run backend unit tests (SQLite, no Docker)" echo " --skip-integration Skip backend integration tests entirely" echo " --output-dir Output directory (default: coverage-summary)" echo " --help Show this help" echo "" echo "Examples:" echo " $0 # Full run (integration + frontend)" echo " $0 --unit # Backend unit + frontend" echo " $0 --frontend-only # Frontend only" echo " $0 --backend-only --unit # Backend unit only" echo " $0 --skip-integration # Frontend only" exit 0 ;; *) echo "Error: unknown option $1"; exit 1 ;; esac done mkdir -p "$OUTPUT_DIR/backend" "$OUTPUT_DIR/frontend" # ── Helpers ────────────────────────────────────────────────────────────── green() { echo -e "\033[0;32m$1\033[0m"; } blue() { echo -e "\033[0;34m$1\033[0m"; } red() { echo -e "\033[0;31m$1\033[0m"; } bold() { echo -e "\033[1m$1\033[0m"; } # ── Default coverage values ────────────────────────────────────────────── BACKEND_COVERAGE_PCT="N/A" BACKEND_STMTS="N/A" BACKEND_MISSED="N/A" BACKEND_TESTS_PASSED="?" BACKEND_TESTS_FAILED="?" BACKEND_TESTS_TOTAL="?" FRONTEND_COVERAGE_STMTS="N/A" FRONTEND_COVERAGE_BRANCH="N/A" FRONTEND_COVERAGE_FUNCS="N/A" FRONTEND_COVERAGE_LINES="N/A" FRONTEND_TESTS_PASSED="?" FRONTEND_TESTS_FAILED="?" FRONTEND_TESTS_TOTAL="?" # ── Backend coverage ───────────────────────────────────────────────────── run_backend() { echo "" bold "━━━ Backend ($BACKEND_MODE) ━━━" cd "$PROJECT_DIR/backend" if [ ! -f ".venv/bin/activate" ]; then red "Error: backend/.venv not found. Run: cd backend && python3 -m venv .venv" exit 1 fi source .venv/bin/activate local pytest_args=() local cov_args=(--cov=src --cov-report=term --cov-report=html:"$OUTPUT_DIR/backend") if [ "$BACKEND_MODE" = "integration" ]; then if ! docker ps >/dev/null 2>&1; then red "Docker is not running. Integration tests require Testcontainers." red " Use --unit for SQLite-based unit tests or start Docker." return 1 fi pytest_args=(tests/integration/ --run-integration --ignore=tests/integration/test_superset_tls_custom_ca.py) else pytest_args=(tests/ --ignore=tests/integration/) fi blue "Running: python -m pytest ${pytest_args[*]} ${cov_args[*]}" local out set +e out=$(python -m pytest "${pytest_args[@]}" "${cov_args[@]}" -q 2>&1) set -e echo "$out" > "$OUTPUT_DIR/backend/pytest_output.txt" # Parse summary: "X passed" or "X passed, Y failed, Z warnings" if echo "$out" | grep -qP '^\d+ passed'; then BACKEND_TESTS_PASSED=$(echo "$out" | grep -oP '^\d+(?= passed)') if echo "$out" | grep -qP '\d+ failed'; then BACKEND_TESTS_FAILED=$(echo "$out" | grep -oP '\d+(?= failed)' | tail -1) else BACKEND_TESTS_FAILED=0 fi BACKEND_TESTS_TOTAL=$((BACKEND_TESTS_PASSED + BACKEND_TESTS_FAILED)) else local summary summary=$(echo "$out" | grep -E "(passed|failed)" | tail -1) if echo "$summary" | grep -qP '\d+ passed'; then BACKEND_TESTS_PASSED=$(echo "$summary" | grep -oP '\d+(?= passed)') BACKEND_TESTS_FAILED=$(echo "$summary" | grep -oP '\d+(?= failed)' || echo "0") BACKEND_TESTS_TOTAL=$((BACKEND_TESTS_PASSED + BACKEND_TESTS_FAILED)) fi fi # Parse TOTAL from coverage: "TOTAL 33686 29679 12%" local total_line total_line=$(echo "$out" | grep "^TOTAL" | tail -1) if [ -n "$total_line" ]; then BACKEND_STMTS=$(echo "$total_line" | awk '{print $2}') BACKEND_MISSED=$(echo "$total_line" | awk '{print $3}') BACKEND_COVERAGE_PCT=$(echo "$total_line" | awk '{print $4}' | tr -d '%') fi green " Tests: $BACKEND_TESTS_PASSED passed / ${BACKEND_TESTS_FAILED:-0} failed" green " Coverage: ${BACKEND_COVERAGE_PCT}% ($BACKEND_STMTS stmts, $BACKEND_MISSED missed)" cd "$PROJECT_DIR" } # ── Frontend coverage ──────────────────────────────────────────────────── run_frontend() { echo "" bold "━━━ Frontend ━━━" cd "$PROJECT_DIR/frontend" if [ ! -d "node_modules" ]; then red "Error: frontend/node_modules not found. Run: cd frontend && npm install" exit 1 fi blue "Running: npx vitest run --coverage --coverage.reportOnFailure" local out set +e out=$(npx vitest run --coverage --coverage.reportOnFailure \ --coverage.reporter=text \ --coverage.reporter=html \ --coverage.reportsDirectory="$OUTPUT_DIR/frontend" 2>&1) set -e echo "$out" > "$OUTPUT_DIR/frontend/vitest_output.txt" # Parse test results local summary_line summary_line=$(echo "$out" | grep -E "^\s*Tests\s+" | tail -1) if [ -n "$summary_line" ]; then FRONTEND_TESTS_PASSED=$(echo "$summary_line" | grep -oP '\d+(?= passed)') FRONTEND_TESTS_FAILED=$(echo "$summary_line" | grep -oP '\d+(?= failed)') FRONTEND_TESTS_TOTAL=$((FRONTEND_TESTS_PASSED + FRONTEND_TESTS_FAILED)) fi # Parse coverage: "All files | 99.25 | 87.39 | 98.91 | 99.48" local cov_line cov_line=$(echo "$out" | grep "All files" | head -1) if [ -n "$cov_line" ]; then FRONTEND_COVERAGE_STMTS=$(echo "$cov_line" | awk -F'|' '{print $2}' | xargs) FRONTEND_COVERAGE_BRANCH=$(echo "$cov_line" | awk -F'|' '{print $3}' | xargs) FRONTEND_COVERAGE_FUNCS=$(echo "$cov_line" | awk -F'|' '{print $4}' | xargs) FRONTEND_COVERAGE_LINES=$(echo "$cov_line" | awk -F'|' '{print $5}' | xargs) fi green " Tests: $FRONTEND_TESTS_PASSED passed / ${FRONTEND_TESTS_FAILED:-0} failed" green " Coverage: Stmts ${FRONTEND_COVERAGE_STMTS}% | Branch ${FRONTEND_COVERAGE_BRANCH}% | Funcs ${FRONTEND_COVERAGE_FUNCS}% | Lines ${FRONTEND_COVERAGE_LINES}%" cd "$PROJECT_DIR" } # ── Generate HTML Report ───────────────────────────────────────────────── generate_report() { echo "" bold "━━━ Generating HTML Report ━━━" # Determine status local backend_status="success" local frontend_status="success" local overall_status="success" if [ "${BACKEND_TESTS_FAILED:-0}" != "?" ] && [ "${BACKEND_TESTS_FAILED:-0}" -gt 0 ]; then backend_status="failed" overall_status="failed" fi if [ "${FRONTEND_TESTS_FAILED:-0}" != "?" ] && [ "${FRONTEND_TESTS_FAILED:-0}" -gt 0 ]; then frontend_status="failed" overall_status="failed" fi # Coverage color helper cov_color() { local pct=$1 if [ "$pct" = "N/A" ]; then echo "gray"; return; fi local int_pct int_pct=$(printf "%.0f" "$pct" 2>/dev/null || echo 0) if [ "$int_pct" -ge 80 ]; then echo "green" elif [ "$int_pct" -ge 50 ]; then echo "orange" else echo "red"; fi } # Collect which stacks local subtitle_parts=() [ "$RUN_BACKEND" = true ] && subtitle_parts+=("Backend (${BACKEND_MODE})") [ "$RUN_FRONTEND" = true ] && subtitle_parts+=("Frontend") local subtitle subtitle=$(IFS=" + "; echo "${subtitle_parts[*]}") local backend_cov_color backend_cov_color=$(cov_color "$BACKEND_COVERAGE_PCT") # Build status pills local status_pills="" status_pills+="Overall: ${overall_status}" if [ "$RUN_BACKEND" = true ]; then status_pills+="Backend: ${backend_status}" fi if [ "$RUN_FRONTEND" = true ]; then status_pills+="Frontend: ${frontend_status}" fi # Build cards local cards_html="" if [ "$RUN_BACKEND" = true ]; then cards_html+="

🐍 Backend

${BACKEND_MODE}
${BACKEND_COVERAGE_PCT}%
Coverage
${BACKEND_TESTS_PASSED}/${BACKEND_TESTS_TOTAL}
Tests (pass/total)
${BACKEND_STMTS}
Statements
${BACKEND_MISSED}
Missed
" fi if [ "$RUN_FRONTEND" = true ]; then local f_stmts="${FRONTEND_COVERAGE_STMTS:-0}" local f_branch="${FRONTEND_COVERAGE_BRANCH:-0}" local f_funcs="${FRONTEND_COVERAGE_FUNCS:-0}" local f_lines="${FRONTEND_COVERAGE_LINES:-0}" local f_stmts_c f_stmts_c=$(cov_color "$f_stmts") local f_branch_c f_branch_c=$(cov_color "$f_branch") local f_funcs_c f_funcs_c=$(cov_color "$f_funcs") local f_lines_c f_lines_c=$(cov_color "$f_lines") cards_html+="

🎨 Frontend

Svelte
${f_stmts}%
Statements
${f_branch}%
Branch
${f_funcs}%
Functions
${f_lines}%
Lines
${FRONTEND_TESTS_PASSED}/${FRONTEND_TESTS_TOTAL} tests passed
" fi # Build links local links_html="" [ "$RUN_BACKEND" = true ] && links_html+="Backend Coverage Details" [ "$RUN_FRONTEND" = true ] && links_html+="Frontend Coverage Details" # Build combined table rows local table_rows="" if [ "$RUN_BACKEND" = true ]; then table_rows+=" Backend (Python) ${BACKEND_MODE} ${BACKEND_TESTS_PASSED} ${BACKEND_TESTS_FAILED:-0} ${BACKEND_COVERAGE_PCT}% — " fi if [ "$RUN_FRONTEND" = true ]; then table_rows+=" Frontend (Svelte) unit ${FRONTEND_TESTS_PASSED} ${FRONTEND_TESTS_FAILED:-0} ${FRONTEND_COVERAGE_STMTS}% ${FRONTEND_COVERAGE_BRANCH}% " fi # ── Write HTML (using quoted heredoc to prevent bash expansion) ───── cat > "$OUTPUT_DIR/index.html" << HTML_EOF Coverage Summary — ss-tools

Coverage Report — ss-tools

${subtitle}
Generated: ${TIMESTAMP}
${status_pills}
${cards_html}

Combined Summary

${table_rows}
Stack Mode Tests Passed Tests Failed Coverage % Branch %

Generated by scripts/coverage-summary.sh

HTML_EOF green " Report saved: $OUTPUT_DIR/index.html" } # ── Main ───────────────────────────────────────────────────────────────── echo "" bold "╔══════════════════════════════════════════════╗" bold "║ ss-tools Coverage Summary Generator ║" bold "╚══════════════════════════════════════════════╝" if [ "$RUN_BACKEND" = false ] && [ "$RUN_FRONTEND" = false ]; then red "Error: nothing to run. Specify at least one of --backend-only, --frontend-only." exit 1 fi [ "$RUN_BACKEND" = true ] && run_backend [ "$RUN_FRONTEND" = true ] && run_frontend generate_report echo "" green "Done! Open: file://${OUTPUT_DIR}/index.html" echo ""