diff --git a/.coverage-threshold b/.coverage-threshold new file mode 100644 index 00000000..844aa571 --- /dev/null +++ b/.coverage-threshold @@ -0,0 +1 @@ +64.2 diff --git a/.github/workflows/unit-test-and-coverage-gate.yml b/.github/workflows/unit-test-and-coverage-gate.yml index efee3198..9e09247c 100644 --- a/.github/workflows/unit-test-and-coverage-gate.yml +++ b/.github/workflows/unit-test-and-coverage-gate.yml @@ -1,227 +1,200 @@ -name: Unit and Coverage +name: Unit Tests and Coverage on: pull_request: - branches: [ main ] # Gate PRs into main + branches: [main] push: - branches: [ main ] # also run on direct pushes - workflow_dispatch: # Manual runs + branches: [main] # Only on main to avoid duplicate runs with pull_request + workflow_dispatch: inputs: ref: - description: "Branch or SHA to test (e.g. feature/x or a1b2c3)" + description: "Branch or SHA to test" required: false cov_threshold: - description: "Override threshold (percent) for this manual run only" + description: "Override coverage threshold (%)" required: false concurrency: - group: earthly-tests-${{ github.ref }} + group: unit-tests-${{ github.ref }} cancel-in-progress: true permissions: - contents: read + contents: write # Needed to push threshold updates jobs: - run-earthly-tests: - name: Run earthly +test (coverage gate) + test: + name: Unit Tests & Coverage Gate runs-on: ubuntu-latest timeout-minutes: 30 steps: - - name: Checkout repository + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - # Use input ref if provided; else PR head SHA; else current SHA - ref: ${{ inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }} + ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.sha }} - name: Setup Earthly uses: earthly/actions/setup-earthly@v1 with: version: "latest" - - name: Show Earthly version - run: earthly --version - - - name: Resolve coverage threshold - id: threshold + - name: Configure test parameters + id: config env: - MANUAL_COV: ${{ inputs.cov_threshold }} + INPUT_COV_THRESHOLD: ${{ inputs.cov_threshold }} run: | - set -euo pipefail - - # Default threshold (matches script default) - DEFAULT="12.3" - - # Use manual override if provided, otherwise use default - if [[ -n "${MANUAL_COV:-}" ]]; then - HEAD_VAL="${MANUAL_COV}" - echo "Using manual threshold override: ${HEAD_VAL}%" + # Read threshold from file, allow manual override + FILE_THRESHOLD=$(cat .coverage-threshold 2>/dev/null || echo "65.0") + COV_THRESHOLD="${INPUT_COV_THRESHOLD:-$FILE_THRESHOLD}" + echo "cov_threshold=${COV_THRESHOLD}" >> "$GITHUB_OUTPUT" + echo "build_id=${GITHUB_RUN_ID}" >> "$GITHUB_OUTPUT" + if [[ -n "${INPUT_COV_THRESHOLD}" ]]; then + echo "::notice::Coverage threshold: ${COV_THRESHOLD}% (from manual override)" else - HEAD_VAL="$DEFAULT" - echo "Using default threshold: ${HEAD_VAL}%" + echo "::notice::Coverage threshold: ${COV_THRESHOLD}% (from .coverage-threshold file)" fi - # Set environment variables for subsequent steps - echo "COV_THRESHOLD=${HEAD_VAL}" >> "$GITHUB_ENV" - echo "PRINT_TS=${GITHUB_RUN_ID}" >> "$GITHUB_ENV" - echo "FAIL_ON_NO_TESTS=false" >> "$GITHUB_ENV" - - echo "Resolved threshold: ${HEAD_VAL}%" - echo "Build ID: ${GITHUB_RUN_ID}" - - # Run standard tests with coverage - - name: Run Earthly +test with coverage threshold + - name: Run tests with coverage + id: test + env: + COV_THRESHOLD: ${{ steps.config.outputs.cov_threshold }} + BUILD_ID: ${{ steps.config.outputs.build_id }} run: | - earthly +test --COV_THRESHOLD="${COV_THRESHOLD}" --PRINT_TS="${PRINT_TS}" --FAIL_ON_NO_TESTS="${FAIL_ON_NO_TESTS}" + earthly +test \ + --COV_THRESHOLD="${COV_THRESHOLD}" \ + --PRINT_TS="${BUILD_ID}" \ + --FAIL_ON_NO_TESTS="false" - # Upload main coverage artifacts (always generated by script) - - name: Upload main coverage artifacts + - name: Upload coverage artifacts if: always() uses: actions/upload-artifact@v4 with: - name: coverage-reports + name: coverage-${{ github.run_id }} path: | coverage.out - coverage_total.txt - coverage_packages.txt - test_raw.log - - # Upload detailed per-directory artifacts for debugging - - name: Upload per-directory coverage artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: per-directory-coverage - path: | - *_coverage.out - *_test.log - overall_coverage.out - combined_coverage.out - overall_test.log - overall_test_with_failures.log - if-no-files-found: ignore - - # Legacy individual uploads for backward compatibility - - name: Upload coverage.out (legacy) - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage.out - path: coverage.out - if-no-files-found: warn - - - name: Upload coverage_total.txt (legacy) - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage_total.txt - path: coverage_total.txt - if-no-files-found: warn + coverage_report.txt + retention-days: 30 - - name: Upload coverage_packages.txt (legacy) - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage_packages.txt - path: coverage_packages.txt - if-no-files-found: warn - - - name: Upload test_raw.log (legacy) - if: always() - uses: actions/upload-artifact@v4 - with: - name: test_raw.log - path: test_raw.log - if-no-files-found: warn - - - name: Publish coverage summary + - name: Generate coverage summary if: always() run: | { - echo "## Coverage Summary" - if [[ -f coverage_total.txt ]]; then - echo "" - echo '```' - cat coverage_total.txt - echo '```' - fi - if [[ -f coverage_packages.txt ]]; then - echo "" - echo "Packages by coverage (lowest first):" - echo '```' - head -n 50 coverage_packages.txt || true - echo '```' - fi + echo "## 📊 Test Coverage Report" echo "" - echo "**Threshold used:** ${COV_THRESHOLD}%" - echo "**Build ID:** ${PRINT_TS}" - echo "**Test method:** Per-directory coverage with script-based execution" - # Add directory-level summary if available - if [[ -f coverage_total.txt ]] && grep -q "| Directory" coverage_total.txt; then + # Extract overall coverage + if [[ -f coverage_report.txt ]]; then + # Extract numeric values using simpler patterns + OVERALL=$(grep "Overall Coverage:" coverage_report.txt | sed 's/[^0-9.]*\([0-9.]\+\)%.*/\1/')% + THRESHOLD=$(grep "Threshold:" coverage_report.txt | sed 's/[^0-9.]*\([0-9.]\+\)%.*/\1/')% + STATUS=$(grep "Status:" coverage_report.txt | sed 's/[^A-Z]*\([A-Z]\+\).*/\1/') + + # Status badge + if [[ "$STATUS" == "PASSED" ]]; then + echo "| Metric | Value | Status |" + echo "|--------|-------|--------|" + echo "| **Overall Coverage** | $OVERALL | ✅ PASSED |" + echo "| **Threshold** | $THRESHOLD | - |" + echo "| **Build** | #${{ github.run_id }} | - |" + else + echo "| Metric | Value | Status |" + echo "|--------|-------|--------|" + echo "| **Overall Coverage** | $OVERALL | ❌ FAILED |" + echo "| **Threshold** | $THRESHOLD | - |" + echo "| **Build** | #${{ github.run_id }} | - |" + fi echo "" - echo "### Directory Test Results" - echo '```' - grep -A 100 "| Directory" coverage_total.txt | head -n 50 || true - echo '```' + + # Failed tests section + if grep -q "Failed Tests:" coverage_report.txt; then + echo "### ❌ Failed Tests" + echo "" + sed -n '/\*\*Failed Tests:\*\*/,/^\*\*Note/p' coverage_report.txt | grep "•" | head -20 + echo "" + fi + + # Directory results table + if grep -q "| Directory" coverage_report.txt; then + echo "### 📁 Directory Results" + echo "" + sed -n '/| Directory/,/^$/p' coverage_report.txt | head -50 + echo "" + fi + else + echo "⚠️ Coverage report not generated" + echo "" + echo "Check the workflow logs for details." fi } >> "$GITHUB_STEP_SUMMARY" - # Debug job for troubleshooting (runs on manual trigger with failures) - run-earthly-tests-debug: - name: Run earthly +test-debug (enhanced debugging) - runs-on: ubuntu-latest - timeout-minutes: 45 - if: failure() && github.event_name == 'workflow_dispatch' - needs: run-earthly-tests - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - ref: ${{ inputs.ref != '' && inputs.ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }} - - - name: Setup Earthly - uses: earthly/actions/setup-earthly@v1 - with: - version: "latest" - - - name: Run Earthly +test-debug with enhanced output + - name: Auto-update coverage threshold (ratchet) + if: success() && github.ref != 'refs/heads/main' env: - COV_THRESHOLD: ${{ inputs.cov_threshold || '12.3' }} - PRINT_TS: ${{ github.run_id }} - FAIL_ON_NO_TESTS: "false" - run: | - earthly +test-debug --COV_THRESHOLD="${COV_THRESHOLD}" --PRINT_TS="${PRINT_TS}" --FAIL_ON_NO_TESTS="${FAIL_ON_NO_TESTS}" - - - name: Upload all debug artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: debug-coverage-artifacts - path: | - coverage.out - coverage_total.txt - coverage_packages.txt - test_raw.log - *_coverage.out - *_test.log - overall_coverage.out - combined_coverage.out - overall_test.log - overall_test_with_failures.log - if-no-files-found: ignore - - - name: Show debug summary - if: always() + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + HEAD_REF: ${{ github.head_ref }} + REF_NAME: ${{ github.ref_name }} run: | - echo "=== Debug Coverage Analysis ===" - echo "Files generated:" - ls -la *.out *.txt *.log || true - echo "" - echo "Directory structure:" - find . -name "*coverage*" -o -name "*test*.log" | head -20 || true + set -x # Debug: show commands + + # Determine branch name based on event type + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + BRANCH="${HEAD_REF}" + else + # For workflow_dispatch or other events, use ref_name + BRANCH="${REF_NAME}" + fi + + echo "Target branch: ${BRANCH}" + + # Skip if on main branch (shouldn't happen due to condition, but safety check) + if [[ "$BRANCH" == "main" ]]; then + echo "Skipping ratchet on main branch" + exit 0 + fi + + # Get current coverage from report using simpler pattern + CURRENT=$(grep "Overall Coverage:" coverage_report.txt | sed 's/[^0-9.]*\([0-9.]\+\)%.*/\1/') + OLD_THRESHOLD=$(cat .coverage-threshold 2>/dev/null || echo "0") + + # Validate CURRENT is a valid number before proceeding + if [[ -z "$CURRENT" ]] || ! [[ "$CURRENT" =~ ^[0-9]+\.?[0-9]*$ ]]; then + echo "::error::Failed to parse coverage value from report (got: '$CURRENT')" + exit 1 + fi + + echo "Current coverage: ${CURRENT}%" + echo "Current threshold: ${OLD_THRESHOLD}%" + + # Calculate new threshold (0.5% buffer below actual coverage) + # Use printf to ensure consistent one-decimal-place formatting + NEW_THRESHOLD=$(printf '%.1f' "$(echo "$CURRENT - 0.5" | bc -l)") + + echo "Proposed new threshold: ${NEW_THRESHOLD}%" + + # Only update if new threshold is higher than old threshold + if (( $(echo "$NEW_THRESHOLD > $OLD_THRESHOLD" | bc -l) )); then + echo "Updating threshold: ${OLD_THRESHOLD}% -> ${NEW_THRESHOLD}%" + echo "${NEW_THRESHOLD}" > .coverage-threshold + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Fetch and checkout the branch + git fetch origin "${BRANCH}" + git checkout "${BRANCH}" + # Ensure branch is up to date in case it changed after the workflow started + git pull --rebase origin "${BRANCH}" + + # Stage and commit + git add .coverage-threshold + git commit -m "chore: auto-update coverage threshold to ${NEW_THRESHOLD}% (was ${OLD_THRESHOLD}%)" + git push origin "${BRANCH}" + + echo "::notice::Coverage threshold updated to ${NEW_THRESHOLD}% on branch ${BRANCH}" + else + echo "New threshold (${NEW_THRESHOLD}%) is not higher than current (${OLD_THRESHOLD}%), no update needed" + fi diff --git a/Earthfile b/Earthfile index ef0a1a90..6412ea5e 100644 --- a/Earthfile +++ b/Earthfile @@ -141,6 +141,7 @@ lint: test: FROM +golang-base + ARG COV_THRESHOLD="" ARG PRINT_TS="" ARG FAIL_ON_NO_TESTS=false @@ -151,16 +152,19 @@ test: RUN chmod +x /work/scripts/run_coverage_tests.sh # Run the comprehensive coverage tests using our script - RUN cd /work && ./scripts/run_coverage_tests.sh "${PRINT_TS}" "${FAIL_ON_NO_TESTS}" + # Args: COV_THRESHOLD PRINT_TS FAIL_ON_NO_TESTS DEBUG + # If COV_THRESHOLD not provided, read from .coverage-threshold file + RUN cd /work && \ + THRESHOLD="${COV_THRESHOLD:-$(cat .coverage-threshold 2>/dev/null || echo 65.0)}" && \ + ./scripts/run_coverage_tests.sh "${THRESHOLD}" "${PRINT_TS}" "${FAIL_ON_NO_TESTS}" - # Save all generated artifacts locally + # Save coverage artifacts locally SAVE ARTIFACT coverage.out AS LOCAL ./coverage.out - SAVE ARTIFACT coverage_total.txt AS LOCAL ./coverage_total.txt - SAVE ARTIFACT coverage_packages.txt AS LOCAL ./coverage_packages.txt - SAVE ARTIFACT test_raw.log AS LOCAL ./test_raw.log + SAVE ARTIFACT coverage_report.txt AS LOCAL ./coverage_report.txt test-debug: FROM +golang-base + ARG COV_THRESHOLD="" ARG PRINT_TS="" ARG FAIL_ON_NO_TESTS=false @@ -170,14 +174,16 @@ test-debug: # Make the coverage script executable RUN chmod +x /work/scripts/run_coverage_tests.sh - # Run the coverage tests with debug output - RUN cd /work && ./scripts/run_coverage_tests.sh "${PRINT_TS}" "${FAIL_ON_NO_TESTS}" "true" + # Run the coverage tests with debug output (keeps temp files for inspection) + # Args: COV_THRESHOLD PRINT_TS FAIL_ON_NO_TESTS DEBUG + # If COV_THRESHOLD not provided, read from .coverage-threshold file + RUN cd /work && \ + THRESHOLD="${COV_THRESHOLD:-$(cat .coverage-threshold 2>/dev/null || echo 65.0)}" && \ + ./scripts/run_coverage_tests.sh "${THRESHOLD}" "${PRINT_TS}" "${FAIL_ON_NO_TESTS}" "true" - # Save all generated artifacts locally + # Save coverage artifacts locally SAVE ARTIFACT coverage.out AS LOCAL ./coverage.out - SAVE ARTIFACT coverage_total.txt AS LOCAL ./coverage_total.txt - SAVE ARTIFACT coverage_packages.txt AS LOCAL ./coverage_packages.txt - SAVE ARTIFACT test_raw.log AS LOCAL ./test_raw.log + SAVE ARTIFACT coverage_report.txt AS LOCAL ./coverage_report.txt test-quick: FROM +golang-base diff --git a/scripts/run_coverage_tests.sh b/scripts/run_coverage_tests.sh index a8dcfe50..e3180a35 100755 --- a/scripts/run_coverage_tests.sh +++ b/scripts/run_coverage_tests.sh @@ -2,21 +2,44 @@ set -euo pipefail # Script to run Go tests with per-directory coverage reporting -# Usage: ./run_coverage_tests.sh [COVERAGE_THRESHOLD] [PRINT_TS] [FAIL_ON_NO_TESTS] [DEBUG] +# Usage: ./run_coverage_tests.sh [COV_THRESHOLD] [PRINT_TS] [FAIL_ON_NO_TESTS] [DEBUG] +# +# Arguments: +# COV_THRESHOLD - Minimum coverage percentage required (default: 64.2) +# PRINT_TS - Build ID/timestamp for reports (default: empty) +# FAIL_ON_NO_TESTS - Fail if directories have no tests (default: false) +# DEBUG - Keep temp files for debugging (default: false) # # Behavior: # - Runs all tests in all directories, even if some fail (for complete visibility) # - Tracks all test failures and reports them at the end # - Exits with code 1 if ANY tests fail OR overall coverage is below threshold # - Generates coverage reports for all directories where tests ran successfully - -COV_THRESHOLD=64.2 -PRINT_TS=${1:-""} -FAIL_ON_NO_TESTS=${2:-false} # Set to true if directories without tests should fail the build -DEBUG=${3:-false} # Set to true for verbose debugging +# +# Output files (in current directory): +# - coverage.out: Combined coverage profile for use with go tool cover +# - coverage_report.txt: Human-readable coverage summary + +COV_THRESHOLD=${1:-64.2} +PRINT_TS=${2:-""} +FAIL_ON_NO_TESTS=${3:-false} # Set to true if directories without tests should fail the build +DEBUG=${4:-false} # Set to true for verbose debugging OVERALL_EXIT_CODE=0 FAILED_DIRS="" +# Create temporary directory for intermediate files +WORK_DIR=$(mktemp -d -t coverage-XXXXXX) + +# Cleanup function to remove temporary files on exit +cleanup() { + if [[ "$DEBUG" != "true" ]]; then + rm -rf "$WORK_DIR" + else + echo "DEBUG: Keeping work directory: $WORK_DIR" >&2 + fi +} +trap cleanup EXIT + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -131,8 +154,8 @@ declare -a FAILED_TEST_DETAILS=() # Initialize empty array # Run tests for each directory with Go files for GO_DIR in ${ALL_GO_DIRS}; do DIR_NAME=$(echo ${GO_DIR} | sed 's|^\./||') - COVERAGE_FILE="${DIR_NAME//\//_}_coverage.out" - TEST_LOG="${DIR_NAME//\//_}_test.log" + COVERAGE_FILE="${WORK_DIR}/${DIR_NAME//\//_}_coverage.out" + TEST_LOG="${WORK_DIR}/${DIR_NAME//\//_}_test.log" if [[ "$DEBUG" == "true" ]]; then echo "DEBUG: Processing directory: ${GO_DIR}" >&2 @@ -141,15 +164,6 @@ for GO_DIR in ${ALL_GO_DIRS}; do echo "DEBUG: TEST_LOG: ${TEST_LOG}" >&2 fi - # Create directory structure for coverage file; fail with error if creation fails - if ! mkdir -p "$(dirname "${COVERAGE_FILE}")" 2>/dev/null; then - echo -e "${RED}ERROR: Failed to create directory for coverage file: $(dirname "${COVERAGE_FILE}")${NC}" >&2 - exit 1 - fi - if ! mkdir -p "$(dirname "${TEST_LOG}")" 2>/dev/null; then - echo -e "${RED}ERROR: Failed to create directory for test log: $(dirname "${TEST_LOG}")${NC}" >&2 - exit 1 - fi # Check if this directory has test files if echo "${TEST_DIRS}" | grep -q "^${GO_DIR}$"; then # Directory has tests - run them @@ -295,13 +309,13 @@ echo "" echo -e "${BLUE}=== Generating Overall Coverage Report ===${NC}" # Calculate overall coverage by running tests on all packages at once -OVERALL_COVERAGE_FILE="overall_coverage.out" +OVERALL_COVERAGE_FILE="${WORK_DIR}/overall_coverage.out" echo "Calculating overall repository coverage..." # Method 1: Try to get overall coverage even if some tests fail echo "Attempting overall coverage calculation (Method 1)..." -if $GO_BIN test -coverprofile="${OVERALL_COVERAGE_FILE}" ./... > overall_test.log 2>&1; then +if $GO_BIN test -coverprofile="${OVERALL_COVERAGE_FILE}" ./... > "${WORK_DIR}/overall_test.log" 2>&1; then # All tests passed if [[ -f "${OVERALL_COVERAGE_FILE}" ]] && [[ -s "${OVERALL_COVERAGE_FILE}" ]]; then OVERALL_COVERAGE=$($GO_BIN tool cover -func="${OVERALL_COVERAGE_FILE}" | grep "total:" | awk '{print $3}' | sed 's/%//') @@ -326,7 +340,7 @@ else echo "Some tests failed, attempting coverage with failures ignored..." # Method 1b: Try to get coverage despite test failures by continuing on failure - if $GO_BIN test -coverprofile="${OVERALL_COVERAGE_FILE}" -failfast=false ./... > overall_test_with_failures.log 2>&1 || true; then + if $GO_BIN test -coverprofile="${OVERALL_COVERAGE_FILE}" -failfast=false ./... > "${WORK_DIR}/overall_test_with_failures.log" 2>&1 || true; then if [[ -f "${OVERALL_COVERAGE_FILE}" ]] && [[ -s "${OVERALL_COVERAGE_FILE}" ]]; then OVERALL_COVERAGE=$($GO_BIN tool cover -func="${OVERALL_COVERAGE_FILE}" | grep "total:" | awk '{print $3}' | sed 's/%//') @@ -344,7 +358,7 @@ else COVERAGE_METHOD="fallback to combined files" # Method 2: Combine individual coverage files from successful tests - COMBINED_COVERAGE="combined_coverage.out" + COMBINED_COVERAGE="${WORK_DIR}/combined_coverage.out" echo "mode: set" > "${COMBINED_COVERAGE}" SUCCESSFUL_DIRS=0 @@ -353,7 +367,7 @@ else for TEST_DIR in ${TEST_DIRS}; do DIR_NAME=$(echo ${TEST_DIR} | sed 's|^\./||') - COVERAGE_FILE="${DIR_NAME//\//_}_coverage.out" + COVERAGE_FILE="${WORK_DIR}/${DIR_NAME//\//_}_coverage.out" if [[ -f "${COVERAGE_FILE}" ]] && [[ -s "${COVERAGE_FILE}" ]]; then # Skip the mode line and append @@ -414,7 +428,12 @@ fi echo "Coverage threshold: ${COV_THRESHOLD}%" echo "Calculation method: ${COVERAGE_METHOD}" -if (( $(echo "${OVERALL_COVERAGE} >= ${COV_THRESHOLD}" | bc -l) )); then +# Normalize both values to 1 decimal place to avoid precision issues +# (e.g., 64.29 displays as 64.3 but fails >= 64.3) +COVERAGE_NORMALIZED=$(printf '%.1f' "${OVERALL_COVERAGE}") +THRESHOLD_NORMALIZED=$(printf '%.1f' "${COV_THRESHOLD}") + +if (( $(echo "${COVERAGE_NORMALIZED} >= ${THRESHOLD_NORMALIZED}" | bc -l) )); then echo -e "${GREEN}✓ Overall coverage PASSED threshold${NC}" else echo -e "${RED}✗ Overall coverage FAILED threshold${NC}" @@ -422,34 +441,34 @@ else fi # Generate coverage reports for saving -echo "## Test Coverage Report" > coverage_total.txt -echo "" >> coverage_total.txt -echo "**Overall Coverage:** ${OVERALL_COVERAGE}%" >> coverage_total.txt -echo "**Threshold:** ${COV_THRESHOLD}% (applies to overall coverage only)" >> coverage_total.txt -echo "**Status:** $(if [[ ${OVERALL_EXIT_CODE} -eq 0 ]]; then echo "PASSED"; else echo "FAILED"; fi)" >> coverage_total.txt -echo "" >> coverage_total.txt +echo "## Test Coverage Report" > coverage_report.txt +echo "" >> coverage_report.txt +echo "**Overall Coverage:** ${COVERAGE_NORMALIZED}%" >> coverage_report.txt +echo "**Threshold:** ${THRESHOLD_NORMALIZED}% (applies to overall coverage only)" >> coverage_report.txt +echo "**Status:** $(if [[ ${OVERALL_EXIT_CODE} -eq 0 ]]; then echo "PASSED"; else echo "FAILED"; fi)" >> coverage_report.txt +echo "" >> coverage_report.txt # FIXED: Use safer array check that works with set -u if [[ "${#FAILED_TEST_DETAILS[@]}" -gt 0 ]] 2>/dev/null; then - echo "**Failed Tests:**" >> coverage_total.txt + echo "**Failed Tests:**" >> coverage_report.txt for detail in "${FAILED_TEST_DETAILS[@]}"; do - echo " • ${detail}" >> coverage_total.txt + echo " • ${detail}" >> coverage_report.txt done - echo "" >> coverage_total.txt + echo "" >> coverage_report.txt fi -echo "**Note:** Directory PASS/FAIL indicates test results only, not coverage." >> coverage_total.txt -echo "**Note:** Coverage threshold applies to overall repository coverage only." >> coverage_total.txt -echo "" >> coverage_total.txt +echo "**Note:** Directory PASS/FAIL indicates test results only, not coverage." >> coverage_report.txt +echo "**Note:** Coverage threshold applies to overall repository coverage only." >> coverage_report.txt +echo "" >> coverage_report.txt -echo "| Directory | Coverage | Result |" >> coverage_total.txt -echo "|-------------------------------------|----------|----------|" >> coverage_total.txt +echo "| Directory | Coverage | Result |" >> coverage_report.txt +echo "|-------------------------------------|----------|----------|" >> coverage_report.txt # Recreate the table for the report for GO_DIR in ${ALL_GO_DIRS}; do DIR_NAME=$(echo ${GO_DIR} | sed 's|^\./||') - COVERAGE_FILE="${DIR_NAME//\//_}_coverage.out" - TEST_LOG="${DIR_NAME//\//_}_test.log" + COVERAGE_FILE="${WORK_DIR}/${DIR_NAME//\//_}_coverage.out" + TEST_LOG="${WORK_DIR}/${DIR_NAME//\//_}_test.log" # Check if this directory has test files if echo "${TEST_DIRS}" | grep -q "^${GO_DIR}$"; then @@ -468,7 +487,7 @@ for GO_DIR in ${ALL_GO_DIRS}; do fi printf "| %-35s | %8s%% | %-8s |\n" \ - "${DIR_NAME}" "${COVERAGE_PCT}" "${STATUS}" >> coverage_total.txt + "${DIR_NAME}" "${COVERAGE_PCT}" "${STATUS}" >> coverage_report.txt else # Tests failed or no coverage generated - check if any coverage was generated COVERAGE_DISPLAY="N/A" @@ -480,60 +499,20 @@ for GO_DIR in ${ALL_GO_DIRS}; do fi printf "| %-35s | %8s | %-8s |\n" \ - "${DIR_NAME}" "${COVERAGE_DISPLAY}" "FAIL" >> coverage_total.txt + "${DIR_NAME}" "${COVERAGE_DISPLAY}" "FAIL" >> coverage_report.txt fi else # Directory has no tests if [[ "${FAIL_ON_NO_TESTS}" == "true" ]]; then printf "| %-35s | %8s | %-8s |\n" \ - "${DIR_NAME}" "N/A" "FAIL" >> coverage_total.txt + "${DIR_NAME}" "N/A" "FAIL" >> coverage_report.txt else printf "| %-35s | %8s | %-8s |\n" \ - "${DIR_NAME}" "N/A" "NO-TESTS" >> coverage_total.txt + "${DIR_NAME}" "N/A" "NO-TESTS" >> coverage_report.txt fi fi done -# Generate package-level coverage breakdown -if [[ -f "overall_coverage.out" ]] && [[ -s "overall_coverage.out" ]]; then - echo "" > coverage_packages.txt - echo "Package Coverage Breakdown (sorted by coverage ascending):" >> coverage_packages.txt - echo "================================================================" >> coverage_packages.txt - $GO_BIN tool cover -func="overall_coverage.out" | grep -v "total:" | sort -k3 -n >> coverage_packages.txt -elif [[ -f "coverage.out" ]] && [[ -s "coverage.out" ]]; then - echo "" > coverage_packages.txt - echo "Package Coverage Breakdown (sorted by coverage ascending):" >> coverage_packages.txt - echo "================================================================" >> coverage_packages.txt - $GO_BIN tool cover -func="coverage.out" | grep -v "total:" | sort -k3 -n >> coverage_packages.txt -fi - -# Generate detailed test log -echo "=== Detailed Test Results ===" > test_raw.log -echo "Threshold: ${COV_THRESHOLD}%" >> test_raw.log -echo "Overall Coverage: ${OVERALL_COVERAGE}%" >> test_raw.log -echo "Status: $(if [[ ${OVERALL_EXIT_CODE} -eq 0 ]]; then echo "PASSED"; else echo "FAILED"; fi)" >> test_raw.log -echo "" >> test_raw.log - -for GO_DIR in ${ALL_GO_DIRS}; do - DIR_NAME=$(echo ${GO_DIR} | sed 's|^\./||') - TEST_LOG="${DIR_NAME//\//_}_test.log" - echo "=== ${DIR_NAME} ===" >> test_raw.log - - # Check if this directory has test files - if echo "${TEST_DIRS}" | grep -q "^${GO_DIR}$"; then - # Directory has tests - include test log - if [[ -f "${TEST_LOG}" ]]; then - cat "${TEST_LOG}" >> test_raw.log - else - echo "No test log found" >> test_raw.log - fi - else - # Directory has no tests - echo "No test files found in this directory" >> test_raw.log - fi - echo "" >> test_raw.log -done - echo "" if [[ ${OVERALL_EXIT_CODE} -eq 0 ]]; then echo -e "${GREEN}🎉 All tests passed!${NC}" @@ -561,5 +540,9 @@ echo "Note: All tests are executed even if some fail (for complete visibility)" echo "Note: Directory PASS/FAIL is based on test results only, not coverage." echo "Note: Overall coverage threshold (${COV_THRESHOLD}%) applies to repository total." echo "Note: Directories without tests are marked as NO-TESTS and $(if [[ "${FAIL_ON_NO_TESTS}" == "true" ]]; then echo "DO"; else echo "DO NOT"; fi) cause build failure" +echo "" +echo "Generated files:" +echo " - coverage.out: Combined coverage profile (use with 'go tool cover')" +echo " - coverage_report.txt: Human-readable coverage summary" exit ${OVERALL_EXIT_CODE}