diff --git a/.github/workflows/libc-full-coverage.yml b/.github/workflows/libc-full-coverage.yml
new file mode 100644
index 0000000000000..7604907b75d69
--- /dev/null
+++ b/.github/workflows/libc-full-coverage.yml
@@ -0,0 +1,152 @@
+name: Libc Full Codebase Coverage
+
+permissions:
+ contents: read
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
+
+on:
+ # Daily overnight run at 02:00 UTC
+ schedule:
+ - cron: '0 2 * * *'
+
+ # Allow manual on-demand execution from GitHub Actions UI
+ workflow_dispatch:
+
+ # Trigger on pushes to main
+ push:
+ branches:
+ - main
+ paths:
+ - 'libc/**'
+ - '.github/workflows/libc-full-coverage.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ full-coverage:
+ timeout-minutes: 60
+ name: libc-full-coverage
+ runs-on: ubuntu-24.04
+ container:
+ image: ghcr.io/llvm/libc-ubuntu-24.04:latest@sha256:8fee4c9ce5a1fd095686593cd36e032c48a31b6ae575378c82accd1d86a08d59
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ fetch-depth: 1
+ persist-credentials: false
+
+ - name: Setup Compiler Cache (sccache)
+ uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23
+ with:
+ max-size: 1G
+ key: libc_coverage_unified_v2_x86_64
+ variant: sccache
+
+ - name: Configure CMake
+ run: |
+ export CMAKE_FLAGS="
+ -G Ninja
+ -S runtimes
+ -B build-cov
+ -DCMAKE_C_COMPILER=clang-23
+ -DCMAKE_CXX_COMPILER=clang++-23
+ -DCMAKE_BUILD_TYPE=Debug
+ -DCMAKE_C_COMPILER_LAUNCHER=sccache
+ -DCMAKE_CXX_COMPILER_LAUNCHER=sccache
+ -DLLVM_USE_LINKER=lld-23
+ -DLLVM_ENABLE_RUNTIMES=libc
+ -DLLVM_LIBC_FULL_BUILD=ON
+ -DLIBC_ENABLE_COVERAGE=ON
+ -DLIBC_TEST_UNIT_TEST_ONLY=ON
+ -DLIBC_TEST_SKIP_DEATH_TESTS=ON
+ -DLIBC_TEST_SKIP_SHARED_TESTS=ON
+ "
+ cmake $CMAKE_FLAGS
+
+ - name: Run Full Codebase Unit Tests
+ run: |
+ export LLVM_PROFILE_FILE="libc_cov_%p.profraw"
+ ninja -k 0 -C build-cov libc-unit-tests || true
+
+ - name: Merge Profiles
+ run: |
+ find . -name "libc_cov_*.profraw" > profraw_list.txt
+ NUM_PROFS=$(wc -l < profraw_list.txt || echo 0)
+ echo "[LOG] Discovered $NUM_PROFS raw profile data files."
+ if [ -s profraw_list.txt ]; then
+ llvm-profdata-23 merge -sparse --input-files=profraw_list.txt -o libc_full.profdata
+ echo "[LOG] Merged full codebase profile into libc_full.profdata."
+ else
+ echo "[LOG] Warning: No profraw files found."
+ fi
+
+ - name: Generate Reports and Summary
+ env:
+ COMMIT_SHA: ${{ github.sha }}
+ BRANCH_REF: ${{ github.ref_name }}
+ run: |
+ EXECUTABLES=($(find build-cov -type f -executable -name "*__build__"))
+ OBJECTS=("${EXECUTABLES[@]:1}")
+ OBJECTS=("${OBJECTS[@]/#/-object=}")
+
+ echo "[LOG] Exporting coverage data across ${#EXECUTABLES[@]} test binaries."
+
+ if [ ! -f libc_full.profdata ] || [ ${#EXECUTABLES[@]} -eq 0 ]; then
+ echo "[LOG] Warning: Profile data or test executables missing. Skipping report generation."
+ echo "### LLVM-libc Coverage Report" >> $GITHUB_STEP_SUMMARY
+ echo "No coverage data was generated for this run." >> $GITHUB_STEP_SUMMARY
+ exit 0
+ fi
+
+ # 1. Generate JSON export for Full Coverage Analyzer
+ llvm-cov-23 export -format=text -instr-profile=libc_full.profdata "${EXECUTABLES[0]}" "${OBJECTS[@]}" > coverage.json
+
+ # 2. Generate HTML Coverage Report with directory hierarchy and branch tracking
+ llvm-cov-23 show -format=html \
+ -output-dir=coverage_html \
+ -instr-profile=libc_full.profdata \
+ "${EXECUTABLES[0]}" "${OBJECTS[@]}" \
+ --show-directory-coverage \
+ --show-branches=count \
+ --compilation-dir=. \
+ --path-equivalence="$GITHUB_WORKSPACE,." \
+ -ignore-filename-regex=".*(test|utils).*"
+ # 3. Run Codebase Coverage Analyzer
+ PYTHONPATH="libc/utils/coverage" python3 libc/utils/coverage/codebase_coverage.py coverage.json "$COMMIT_SHA" "$BRANCH_REF" > coverage_summary.md
+ cat coverage_summary.md >> $GITHUB_STEP_SUMMARY
+ echo "[LOG] Post-commit summary report written to GITHUB_STEP_SUMMARY."
+
+ - name: Fallback Failure Summary
+ if: failure() && !hashFiles('coverage.json')
+ run: |
+ echo "### LLVM-libc Coverage Report" >> $GITHUB_STEP_SUMMARY
+ echo "Workflow run encountered an error before coverage reports could be generated. Inspect job logs for details." >> $GITHUB_STEP_SUMMARY
+
+ - name: Upload Coverage Summary Artifact
+ if: always() && hashFiles('coverage_summary.md')
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: libc-coverage-summary
+ path: |
+ coverage_summary.md
+ coverage.json
+ if-no-files-found: ignore
+ retention-days: 14
+ overwrite: true
+
+ - name: Upload HTML Coverage Artifact
+ if: always() && hashFiles('coverage_html/**')
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: libc-coverage-html
+ path: coverage_html/
+ retention-days: 14
+ overwrite: true
diff --git a/.github/workflows/libc-full-mcdc.yml b/.github/workflows/libc-full-mcdc.yml
new file mode 100644
index 0000000000000..9aa6d96943891
--- /dev/null
+++ b/.github/workflows/libc-full-mcdc.yml
@@ -0,0 +1,157 @@
+name: Libc Full Codebase MC/DC Coverage
+
+permissions:
+ contents: read
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
+
+on:
+ # Daily overnight run at 02:30 UTC (staggered by 30 mins from baseline sweep)
+ schedule:
+ - cron: '30 2 * * *'
+
+ # Allow manual on-demand execution from GitHub Actions UI ("Run workflow" button)
+ workflow_dispatch:
+
+ # Trigger on pushes to main
+ push:
+ branches:
+ - main
+ paths:
+ - 'libc/**'
+ - '.github/workflows/libc-full-mcdc.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ full-mcdc-coverage:
+ timeout-minutes: 60
+ name: libc-full-mcdc-coverage
+ runs-on: ubuntu-24.04
+ container:
+ image: ghcr.io/llvm/libc-ubuntu-24.04:latest@sha256:8fee4c9ce5a1fd095686593cd36e032c48a31b6ae575378c82accd1d86a08d59
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ fetch-depth: 1
+ persist-credentials: false
+
+ - name: Setup Compiler Cache (sccache)
+ uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23
+ with:
+ max-size: 1G
+ key: libc_mcdc_coverage_unified_x86_64
+ variant: sccache
+
+ - name: Configure CMake with MC/DC
+ run: |
+ export CMAKE_FLAGS="
+ -G Ninja
+ -S runtimes
+ -B build-cov
+ -DCMAKE_C_COMPILER=clang-23
+ -DCMAKE_CXX_COMPILER=clang++-23
+ -DCMAKE_BUILD_TYPE=Debug
+ -DCMAKE_C_COMPILER_LAUNCHER=sccache
+ -DCMAKE_CXX_COMPILER_LAUNCHER=sccache
+ -DLLVM_USE_LINKER=lld-23
+ -DLLVM_ENABLE_RUNTIMES=libc
+ -DLLVM_LIBC_FULL_BUILD=ON
+ -DLIBC_ENABLE_COVERAGE=ON
+ -DLIBC_ENABLE_MCDC=ON
+ -DLIBC_TEST_UNIT_TEST_ONLY=ON
+ -DLIBC_TEST_SKIP_DEATH_TESTS=ON
+ -DLIBC_TEST_SKIP_SHARED_TESTS=ON
+ "
+ cmake $CMAKE_FLAGS
+
+ - name: Run Full Codebase Unit Tests
+ run: |
+ export LLVM_PROFILE_FILE="libc_cov_%p.profraw"
+ ninja -k 0 -C build-cov libc-unit-tests || true
+
+ - name: Merge Profiles
+ run: |
+ find . -name "libc_cov_*.profraw" > profraw_list.txt
+ NUM_PROFS=$(wc -l < profraw_list.txt || echo 0)
+ echo "[LOG] Discovered $NUM_PROFS raw profile data files."
+ if [ -s profraw_list.txt ]; then
+ llvm-profdata-23 merge -sparse --input-files=profraw_list.txt -o libc_full.profdata
+ echo "[LOG] Merged full codebase profile into libc_full.profdata."
+ else
+ echo "[LOG] Warning: No profraw files found."
+ fi
+
+ - name: Generate Reports and Summary
+ env:
+ COMMIT_SHA: ${{ github.sha }}
+ BRANCH_REF: ${{ github.ref_name }}
+ run: |
+ EXECUTABLES=($(find build-cov -type f -executable -name "*__build__"))
+ OBJECTS=("${EXECUTABLES[@]:1}")
+ OBJECTS=("${OBJECTS[@]/#/-object=}")
+
+ echo "[LOG] Exporting coverage data across ${#EXECUTABLES[@]} test binaries."
+
+ if [ ! -f libc_full.profdata ] || [ ${#EXECUTABLES[@]} -eq 0 ]; then
+ echo "[LOG] Warning: Profile data or test executables missing. Skipping report generation."
+ echo "### LLVM-libc MC/DC Coverage Report" >> $GITHUB_STEP_SUMMARY
+ echo "No coverage data was generated for this run." >> $GITHUB_STEP_SUMMARY
+ exit 0
+ fi
+
+ # 1. Generate JSON export for Full Coverage Analyzer (contains mcdc_records)
+ llvm-cov-23 export -format=text -instr-profile=libc_full.profdata "${EXECUTABLES[0]}" "${OBJECTS[@]}" > coverage.json
+
+ # 2. Generate HTML Coverage Report with directory hierarchy, branch tracking, and MC/DC truth tables
+ llvm-cov-23 show -format=html \
+ -output-dir=coverage_mcdc_html \
+ -instr-profile=libc_full.profdata \
+ "${EXECUTABLES[0]}" "${OBJECTS[@]}" \
+ --show-directory-coverage \
+ --show-branches=count \
+ --show-mcdc \
+ --show-mcdc-summary \
+ --compilation-dir=. \
+ --path-equivalence="$GITHUB_WORKSPACE,." \
+ -ignore-filename-regex=".*(test|utils).*"
+ # 3. Run Codebase Coverage Analyzer
+ PYTHONPATH="libc/utils/coverage" python3 libc/utils/coverage/codebase_coverage.py coverage.json "$COMMIT_SHA" "$BRANCH_REF" > coverage_summary.md
+ cat coverage_summary.md >> $GITHUB_STEP_SUMMARY
+ echo "[LOG] Post-commit summary report written to GITHUB_STEP_SUMMARY."
+
+ - name: Fallback Failure Summary
+ if: failure() && !hashFiles('coverage.json')
+ run: |
+ echo "### LLVM-libc MC/DC Coverage Report" >> $GITHUB_STEP_SUMMARY
+ echo "Workflow run encountered an error before coverage reports could be generated. Inspect job logs for details." >> $GITHUB_STEP_SUMMARY
+
+ - name: Upload MC/DC Coverage Summary Artifact
+ if: always() && hashFiles('coverage_summary.md')
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: libc-mcdc-coverage-summary
+ path: |
+ coverage_summary.md
+ coverage.json
+ if-no-files-found: ignore
+ retention-days: 14
+ overwrite: true
+
+ - name: Upload MC/DC Coverage Artifact
+ if: always() && hashFiles('coverage_mcdc_html/**')
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: libc-mcdc-coverage-html
+ path: coverage_mcdc_html/
+ retention-days: 14
+ overwrite: true
+
+
diff --git a/.github/workflows/libc-patch-coverage.yml b/.github/workflows/libc-patch-coverage.yml
new file mode 100644
index 0000000000000..c5637301575ea
--- /dev/null
+++ b/.github/workflows/libc-patch-coverage.yml
@@ -0,0 +1,298 @@
+name: Libc Patch Code Coverage
+
+permissions:
+ contents: read
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ paths:
+ - 'libc/src/**'
+ - 'libc/include/**'
+ - 'libc/test/**'
+ - 'libc/CMakeLists.txt'
+ - '.github/workflows/libc-patch-coverage.yml'
+ - '!libc/docs/**'
+ - '!libc/benchmarks/**'
+ - '!libc/fuzzing/**'
+ - '!libc/utils/**'
+ - '!**.md'
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'libc/src/**'
+ - 'libc/include/**'
+ - 'libc/test/**'
+ - 'libc/CMakeLists.txt'
+ - '.github/workflows/libc-patch-coverage.yml'
+ - '!libc/docs/**'
+ - '!libc/benchmarks/**'
+ - '!libc/fuzzing/**'
+ - '!libc/utils/**'
+ - '!**.md'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ pre-commit-coverage:
+ timeout-minutes: 60
+ name: libc-pre-commit-coverage
+ runs-on: ubuntu-24.04
+ container:
+ image: ghcr.io/llvm/libc-ubuntu-24.04:latest@sha256:8fee4c9ce5a1fd095686593cd36e032c48a31b6ae575378c82accd1d86a08d59
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 100
+ persist-credentials: false
+
+ - name: Setup Compiler Cache (sccache)
+ uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23
+ with:
+ max-size: 1G
+ key: libc_coverage_unified_v2_x86_64
+ variant: sccache
+
+ - name: Configure CMake
+ run: |
+ export CMAKE_FLAGS="
+ -G Ninja
+ -S runtimes
+ -B build-cov
+ -DCMAKE_C_COMPILER=clang-23
+ -DCMAKE_CXX_COMPILER=clang++-23
+ -DCMAKE_BUILD_TYPE=Debug
+ -DCMAKE_C_COMPILER_LAUNCHER=sccache
+ -DCMAKE_CXX_COMPILER_LAUNCHER=sccache
+ -DLLVM_USE_LINKER=lld-23
+ -DLLVM_ENABLE_RUNTIMES=libc
+ -DLLVM_LIBC_FULL_BUILD=ON
+ -DLIBC_ENABLE_COVERAGE=ON
+ -DLIBC_TEST_UNIT_TEST_ONLY=ON
+ -DLIBC_TEST_SKIP_DEATH_TESTS=ON
+ -DLIBC_TEST_SKIP_SHARED_TESTS=ON
+ "
+ cmake $CMAKE_FLAGS
+
+ - name: Build and Run Targeted Tests
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
+ PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
+ PR_BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }}
+ PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
+ COMMIT_SHA: ${{ github.sha }}
+ BRANCH_REF: ${{ github.ref_name }}
+ REPO_NAME: ${{ github.repository }}
+ run: |
+ # Ensure upstream llvm-project remote is configured
+ git remote add upstream https://github.com/llvm/llvm-project.git 2>/dev/null || true
+
+ # 1. Deterministically resolve Base and Head references against upstream llvm-project
+ if [ "$EVENT_NAME" == "pull_request" ]; then
+ BASE_SHA="$PR_BASE_SHA"
+ HEAD_SHA="$PR_HEAD_SHA"
+ BASE_REF="$PR_BASE_REF"
+ HEAD_REF="$PR_HEAD_REF"
+ BASE_REPO="${PR_BASE_REPO:-llvm/llvm-project}"
+ HEAD_REPO="${PR_HEAD_REPO:-$REPO_NAME}"
+ # Fetch base branch history to resolve merge base
+ git fetch upstream "$BASE_REF" --depth=100 2>/dev/null || git fetch origin "$BASE_REF" --depth=100 2>/dev/null || git fetch upstream "$BASE_SHA" --depth=1 2>/dev/null || git fetch origin "$BASE_SHA" --depth=1 2>/dev/null || true
+ DIFF_BASE=$(git merge-base "$BASE_SHA" HEAD 2>/dev/null || true)
+ if [ -z "$DIFF_BASE" ]; then
+ git fetch --no-tags --deepen=200 2>/dev/null || true
+ DIFF_BASE=$(git merge-base "$BASE_SHA" HEAD 2>/dev/null || echo "$BASE_SHA")
+ fi
+ elif [ "$BRANCH_REF" != "main" ]; then
+ # For feature branches, compare against main merge-base
+ git fetch origin main --depth=100 2>/dev/null || true
+ BASE_SHA=$(git merge-base origin/main HEAD 2>/dev/null || git rev-parse HEAD~1 2>/dev/null || echo "HEAD~1")
+ HEAD_SHA="$COMMIT_SHA"
+ BASE_REF="main"
+ HEAD_REF="$BRANCH_REF"
+ BASE_REPO="llvm/llvm-project"
+ HEAD_REPO="$REPO_NAME"
+ DIFF_BASE="$BASE_SHA"
+ else
+ # For direct pushes, compare the pushed commit against its parent (HEAD~1)
+ BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || echo "HEAD~1")
+ HEAD_SHA="$COMMIT_SHA"
+ BASE_REF="main"
+ HEAD_REF="$BRANCH_REF"
+ BASE_REPO="llvm/llvm-project"
+ HEAD_REPO="$REPO_NAME"
+ DIFF_BASE="HEAD~1"
+ fi
+
+ # Persist resolved commit references for all downstream steps
+ echo "DIFF_BASE=$DIFF_BASE" >> $GITHUB_ENV
+ echo "BASE_SHA=$BASE_SHA" >> $GITHUB_ENV
+ echo "HEAD_SHA=$HEAD_SHA" >> $GITHUB_ENV
+ echo "BASE_REF=$BASE_REF" >> $GITHUB_ENV
+ echo "HEAD_REF=$HEAD_REF" >> $GITHUB_ENV
+ echo "BASE_REPO=$BASE_REPO" >> $GITHUB_ENV
+ echo "HEAD_REPO=$HEAD_REPO" >> $GITHUB_ENV
+
+ echo "[LOG] Resolved Base: ${BASE_REF} (${BASE_SHA:0:7}) in ${BASE_REPO}"
+ echo "[LOG] Resolved Head: ${HEAD_REF} (${HEAD_SHA:0:7}) in ${HEAD_REPO}"
+
+ MODIFIED_FILES=$(git diff --name-only "$DIFF_BASE" HEAD -- libc/src/ || true)
+ echo "[LOG] Modified files in libc/src/:"
+ echo "$MODIFIED_FILES"
+
+ if [ -z "$MODIFIED_FILES" ]; then
+ echo "[LOG] No source files modified in libc/src/. Exiting successfully."
+ echo "TARGETS=" >> $GITHUB_ENV
+
+ echo "## LLVM-libc Patch Coverage Report" > coverage_report.md
+ echo "" >> coverage_report.md
+ echo "- **Base Branch:** [\`${BASE_REF}\` (${BASE_SHA:0:7})](https://github.com/${BASE_REPO}/commit/${BASE_SHA})" >> coverage_report.md
+ echo "- **Head Commit:** [\`${HEAD_REF}\` (${HEAD_SHA:0:7})](https://github.com/${HEAD_REPO}/commit/${HEAD_SHA})" >> coverage_report.md
+ echo "---" >> coverage_report.md
+ echo "" >> coverage_report.md
+ echo "**Coverage Validated**" >> coverage_report.md
+ echo "No \`.cpp\` source files in \`libc/src/\` were modified in this patch." >> coverage_report.md
+
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+ exit 0
+ fi
+
+ # Get all valid ninja targets
+ ninja -C build-cov -t targets > all_targets.txt
+
+ TARGETS=""
+
+ for FILE in $MODIFIED_FILES; do
+ # Skip core support files from direct target mapping (tested transitively)
+ if [[ "$FILE" == *"__support/"* ]]; then
+ echo "[LOG] -> Support file ($FILE) will be tested transitively."
+ continue
+ fi
+
+ if [[ "$FILE" =~ ^libc/src/([^/]+)/(.*/)?([^/]+)\.cpp$ ]]; then
+ DIR="${BASH_REMATCH[1]}"
+ FUNC="${BASH_REMATCH[3]}"
+ ALL_MATCHES=$(grep -oE "^libc\.test\.src\.${DIR}\.([a-zA-Z0-9_]+\.)*${FUNC}_test\.__unit__" all_targets.txt || true)
+ FOUND_TARGET=$(echo "$ALL_MATCHES" | head -n 1 | tr -d '\r\n ' || true)
+ if [ -n "$FOUND_TARGET" ]; then
+ echo "[LOG] -> Matched $FILE -> $FOUND_TARGET"
+ TARGETS="$TARGETS $FOUND_TARGET"
+ else
+ echo "[LOG] -> Notice: No direct unit test target found for $FILE."
+ fi
+ fi
+ done
+
+ # Remove duplicate targets
+ TARGETS=$(echo "$TARGETS" | xargs -n1 | sort -u | xargs || true)
+ echo "[LOG] Executing Ninja targets: $TARGETS"
+
+ if [ -n "$TARGETS" ]; then
+ export LLVM_PROFILE_FILE="libc_cov_%p.profraw"
+ ninja -C build-cov $TARGETS
+ echo "TARGETS=$TARGETS" >> $GITHUB_ENV
+ else
+ echo "[LOG] No standalone unit test targets to run."
+ echo "TARGETS=" >> $GITHUB_ENV
+
+ echo "## LLVM-libc Patch Coverage Report" > coverage_report.md
+ echo "" >> coverage_report.md
+ echo "- **Base Branch:** [\`${BASE_REF}\` (${BASE_SHA:0:7})](https://github.com/${BASE_REPO}/commit/${BASE_SHA})" >> coverage_report.md
+ echo "- **Head Commit:** [\`${HEAD_REF}\` (${HEAD_SHA:0:7})](https://github.com/${HEAD_REPO}/commit/${HEAD_SHA})" >> coverage_report.md
+ echo "---" >> coverage_report.md
+ echo "" >> coverage_report.md
+ echo "**Coverage Validated**" >> coverage_report.md
+ echo "Modified files do not have standalone unit test targets." >> coverage_report.md
+
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+ fi
+
+ - name: Merge Profiles
+ if: env.TARGETS != ''
+ run: |
+ find . -name "libc_cov_*.profraw" > profraw_list.txt
+ NUM_PROFS=$(wc -l < profraw_list.txt || echo 0)
+ echo "[LOG] Discovered $NUM_PROFS raw profile data files."
+ if [ -s profraw_list.txt ]; then
+ llvm-profdata-23 merge -sparse --input-files=profraw_list.txt -o libc_full.profdata
+ echo "[LOG] Successfully merged profile data into libc_full.profdata."
+ else
+ echo "[LOG] Warning: No profraw files found."
+ fi
+
+ - name: Extract Executables and Generate Summary
+ if: env.TARGETS != ''
+ run: |
+ EXECUTABLES=($(find build-cov -type f -executable -name "*__build__"))
+ OBJECTS=("${EXECUTABLES[@]:1}")
+ OBJECTS=("${OBJECTS[@]/#/-object=}")
+
+ echo "[LOG] Discovered ${#EXECUTABLES[@]} instrumented binaries for llvm-cov export."
+
+ if [ ! -f libc_full.profdata ] || [ ${#EXECUTABLES[@]} -eq 0 ]; then
+ echo "[LOG] Notice: Profile data or instrumented executables not found. Generating non-coverage summary."
+ echo "## LLVM-libc Patch Coverage Report" > coverage_report.md
+ echo "" >> coverage_report.md
+ echo "- **Base Branch:** [\`${BASE_REF}\` (${BASE_SHA:0:7})](https://github.com/${BASE_REPO}/commit/${BASE_SHA})" >> coverage_report.md
+ echo "- **Head Commit:** [\`${HEAD_REF}\` (${HEAD_SHA:0:7})](https://github.com/${HEAD_REPO}/commit/${HEAD_SHA})" >> coverage_report.md
+ echo "---" >> coverage_report.md
+ echo "" >> coverage_report.md
+ echo "**Coverage Validated**" >> coverage_report.md
+ echo "No executable statement profile data was collected for the modified lines in this patch." >> coverage_report.md
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+ exit 0
+ fi
+
+ # 1. Generate JSON report for Python script
+ llvm-cov-23 export -format=text -instr-profile=libc_full.profdata "${EXECUTABLES[0]}" "${OBJECTS[@]}" > coverage.json
+
+ # 2. Generate git diff for modified libc source files
+ git diff "$DIFF_BASE" HEAD -- libc/src/ > patch.diff
+
+ # 3. Run Diff Coverage Python Script using persisted commit metadata
+ PYTHONPATH="libc/utils/coverage" python3 libc/utils/coverage/diff_coverage.py patch.diff coverage.json "$BASE_SHA" "$HEAD_SHA" "$BASE_REF" "$HEAD_REF" "$TARGETS" "$BASE_REPO" "$HEAD_REPO" > coverage_report.md
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+ echo "[LOG] Coverage report successfully written to GITHUB_STEP_SUMMARY."
+
+ - name: Fallback Failure Summary
+ if: failure() && !hashFiles('coverage_report.md')
+ run: |
+ echo "## LLVM-libc Patch Coverage Report" > coverage_report.md
+ echo "" >> coverage_report.md
+ echo "- **Base Branch:** [\`${BASE_REF:-main}\` (${BASE_SHA:0:7})](https://github.com/${BASE_REPO:-llvm/llvm-project}/commit/${BASE_SHA:-main})" >> coverage_report.md
+ echo "- **Head Commit:** [\`${HEAD_REF:-HEAD}\` (${HEAD_SHA:0:7})](https://github.com/${HEAD_REPO:-llvm/llvm-project}/commit/${HEAD_SHA:-HEAD})" >> coverage_report.md
+ echo "---" >> coverage_report.md
+ echo "" >> coverage_report.md
+ echo "**Coverage Validated**" >> coverage_report.md
+ echo "The targeted build or test execution encountered an error before coverage data could be finalized. Inspect the job logs for details." >> coverage_report.md
+
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+
+
+ - name: Upload Patch Coverage Artifacts
+ if: always() && hashFiles('coverage_report.md')
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: libc-patch-coverage-report
+ path: |
+ coverage_report.md
+ patch.diff
+ coverage.json
+ if-no-files-found: ignore
+ retention-days: 14
+ overwrite: true
diff --git a/.github/workflows/libc-patch-mcdc.yml b/.github/workflows/libc-patch-mcdc.yml
new file mode 100644
index 0000000000000..c4e6c238e4b8d
--- /dev/null
+++ b/.github/workflows/libc-patch-mcdc.yml
@@ -0,0 +1,298 @@
+name: Libc Patch MC/DC Coverage
+
+permissions:
+ contents: read
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ paths:
+ - 'libc/src/**'
+ - 'libc/include/**'
+ - 'libc/test/**'
+ - 'libc/CMakeLists.txt'
+ - '.github/workflows/libc-patch-mcdc.yml'
+ - '!libc/docs/**'
+ - '!libc/benchmarks/**'
+ - '!libc/fuzzing/**'
+ - '!libc/utils/**'
+ - '!**.md'
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'libc/src/**'
+ - 'libc/include/**'
+ - 'libc/test/**'
+ - 'libc/CMakeLists.txt'
+ - '.github/workflows/libc-patch-mcdc.yml'
+ - '!libc/docs/**'
+ - '!libc/benchmarks/**'
+ - '!libc/fuzzing/**'
+ - '!libc/utils/**'
+ - '!**.md'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ pre-commit-mcdc-coverage:
+ timeout-minutes: 60
+ name: libc-pre-commit-mcdc-coverage
+ runs-on: ubuntu-24.04
+ container:
+ image: ghcr.io/llvm/libc-ubuntu-24.04:latest@sha256:8fee4c9ce5a1fd095686593cd36e032c48a31b6ae575378c82accd1d86a08d59
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 100
+ persist-credentials: false
+
+ - name: Setup Compiler Cache (sccache)
+ uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23
+ with:
+ max-size: 1G
+ key: libc_coverage_unified_v2_x86_64
+ variant: sccache
+
+ - name: Configure CMake
+ run: |
+ export CMAKE_FLAGS="
+ -G Ninja
+ -S runtimes
+ -B build-cov
+ -DCMAKE_C_COMPILER=clang-23
+ -DCMAKE_CXX_COMPILER=clang++-23
+ -DCMAKE_BUILD_TYPE=Debug
+ -DCMAKE_C_COMPILER_LAUNCHER=sccache
+ -DCMAKE_CXX_COMPILER_LAUNCHER=sccache
+ -DLLVM_USE_LINKER=lld-23
+ -DLLVM_ENABLE_RUNTIMES=libc
+ -DLLVM_LIBC_FULL_BUILD=ON
+ -DLIBC_ENABLE_COVERAGE=ON
+ -DLIBC_ENABLE_MCDC=ON
+ -DLIBC_TEST_UNIT_TEST_ONLY=ON
+ -DLIBC_TEST_SKIP_DEATH_TESTS=ON
+ -DLIBC_TEST_SKIP_SHARED_TESTS=ON
+ "
+ cmake $CMAKE_FLAGS
+
+ - name: Build and Run Targeted Tests
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
+ PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
+ PR_BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }}
+ PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
+ COMMIT_SHA: ${{ github.sha }}
+ BRANCH_REF: ${{ github.ref_name }}
+ REPO_NAME: ${{ github.repository }}
+ run: |
+ # Ensure upstream llvm-project remote is configured
+ git remote add upstream https://github.com/llvm/llvm-project.git 2>/dev/null || true
+
+ # 1. Deterministically resolve Base and Head references against upstream llvm-project
+ if [ "$EVENT_NAME" == "pull_request" ]; then
+ BASE_SHA="$PR_BASE_SHA"
+ HEAD_SHA="$PR_HEAD_SHA"
+ BASE_REF="$PR_BASE_REF"
+ HEAD_REF="$PR_HEAD_REF"
+ BASE_REPO="${PR_BASE_REPO:-llvm/llvm-project}"
+ HEAD_REPO="${PR_HEAD_REPO:-$REPO_NAME}"
+ # Fetch base branch history to resolve merge base
+ git fetch upstream "$BASE_REF" --depth=100 2>/dev/null || git fetch origin "$BASE_REF" --depth=100 2>/dev/null || git fetch upstream "$BASE_SHA" --depth=1 2>/dev/null || git fetch origin "$BASE_SHA" --depth=1 2>/dev/null || true
+ DIFF_BASE=$(git merge-base "$BASE_SHA" HEAD 2>/dev/null || true)
+ if [ -z "$DIFF_BASE" ]; then
+ git fetch --no-tags --deepen=200 2>/dev/null || true
+ DIFF_BASE=$(git merge-base "$BASE_SHA" HEAD 2>/dev/null || echo "$BASE_SHA")
+ fi
+ elif [ "$BRANCH_REF" != "main" ]; then
+ # For feature branches, compare against main merge-base
+ git fetch origin main --depth=100 2>/dev/null || true
+ BASE_SHA=$(git merge-base origin/main HEAD 2>/dev/null || git rev-parse HEAD~1 2>/dev/null || echo "HEAD~1")
+ HEAD_SHA="$COMMIT_SHA"
+ BASE_REF="main"
+ HEAD_REF="$BRANCH_REF"
+ BASE_REPO="llvm/llvm-project"
+ HEAD_REPO="$REPO_NAME"
+ DIFF_BASE="$BASE_SHA"
+ else
+ BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || echo "HEAD~1")
+ HEAD_SHA="$COMMIT_SHA"
+ BASE_REF="main"
+ HEAD_REF="$BRANCH_REF"
+ BASE_REPO="llvm/llvm-project"
+ HEAD_REPO="$REPO_NAME"
+ DIFF_BASE="HEAD~1"
+ fi
+
+ # Persist resolved commit references for all downstream steps
+ echo "DIFF_BASE=$DIFF_BASE" >> $GITHUB_ENV
+ echo "BASE_SHA=$BASE_SHA" >> $GITHUB_ENV
+ echo "HEAD_SHA=$HEAD_SHA" >> $GITHUB_ENV
+ echo "BASE_REF=$BASE_REF" >> $GITHUB_ENV
+ echo "HEAD_REF=$HEAD_REF" >> $GITHUB_ENV
+ echo "BASE_REPO=$BASE_REPO" >> $GITHUB_ENV
+ echo "HEAD_REPO=$HEAD_REPO" >> $GITHUB_ENV
+
+ echo "[LOG] Resolved Base: ${BASE_REF} (${BASE_SHA:0:7}) in ${BASE_REPO}"
+ echo "[LOG] Resolved Head: ${HEAD_REF} (${HEAD_SHA:0:7}) in ${HEAD_REPO}"
+
+ MODIFIED_FILES=$(git diff --name-only "$DIFF_BASE" HEAD -- libc/src/ || true)
+ echo "[LOG] Modified files in libc/src/:"
+ echo "$MODIFIED_FILES"
+
+ if [ -z "$MODIFIED_FILES" ]; then
+ echo "[LOG] No source files modified in libc/src/. Exiting successfully."
+ echo "TARGETS=" >> $GITHUB_ENV
+
+ echo "## LLVM-libc Patch Coverage Report" > coverage_report.md
+ echo "" >> coverage_report.md
+ echo "- **Base Branch:** [\`${BASE_REF}\` (${BASE_SHA:0:7})](https://github.com/${BASE_REPO}/commit/${BASE_SHA})" >> coverage_report.md
+ echo "- **Head Commit:** [\`${HEAD_REF}\` (${HEAD_SHA:0:7})](https://github.com/${HEAD_REPO}/commit/${HEAD_SHA})" >> coverage_report.md
+ echo "---" >> coverage_report.md
+ echo "" >> coverage_report.md
+ echo "**Coverage Validated**" >> coverage_report.md
+ echo "No \`.cpp\` source files in \`libc/src/\` were modified in this patch." >> coverage_report.md
+
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+ exit 0
+ fi
+
+ # Get all valid ninja targets
+ ninja -C build-cov -t targets > all_targets.txt
+
+ TARGETS=""
+
+ for FILE in $MODIFIED_FILES; do
+ # Skip core support files from direct target mapping (tested transitively)
+ if [[ "$FILE" == *"__support/"* ]]; then
+ echo "[LOG] -> Support file ($FILE) will be tested transitively."
+ continue
+ fi
+
+ if [[ "$FILE" =~ ^libc/src/([^/]+)/(.*/)?([^/]+)\.cpp$ ]]; then
+ DIR="${BASH_REMATCH[1]}"
+ FUNC="${BASH_REMATCH[3]}"
+ ALL_MATCHES=$(grep -oE "^libc\.test\.src\.${DIR}\.([a-zA-Z0-9_]+\.)*${FUNC}_test\.__unit__" all_targets.txt || true)
+ FOUND_TARGET=$(echo "$ALL_MATCHES" | head -n 1 | tr -d '\r\n ' || true)
+ if [ -n "$FOUND_TARGET" ]; then
+ echo "[LOG] -> Matched $FILE -> $FOUND_TARGET"
+ TARGETS="$TARGETS $FOUND_TARGET"
+ else
+ echo "[LOG] -> Notice: No direct unit test target found for $FILE."
+ fi
+ fi
+ done
+
+ # Remove duplicate targets
+ TARGETS=$(echo "$TARGETS" | xargs -n1 | sort -u | xargs || true)
+ echo "[LOG] Executing Ninja targets: $TARGETS"
+
+ if [ -n "$TARGETS" ]; then
+ export LLVM_PROFILE_FILE="libc_cov_%p.profraw"
+ ninja -C build-cov $TARGETS
+ echo "TARGETS=$TARGETS" >> $GITHUB_ENV
+ else
+ echo "[LOG] No standalone unit test targets to run."
+ echo "TARGETS=" >> $GITHUB_ENV
+
+ echo "## LLVM-libc Patch Coverage Report" > coverage_report.md
+ echo "" >> coverage_report.md
+ echo "- **Base Branch:** [\`${BASE_REF}\` (${BASE_SHA:0:7})](https://github.com/${BASE_REPO}/commit/${BASE_SHA})" >> coverage_report.md
+ echo "- **Head Commit:** [\`${HEAD_REF}\` (${HEAD_SHA:0:7})](https://github.com/${HEAD_REPO}/commit/${HEAD_SHA})" >> coverage_report.md
+ echo "---" >> coverage_report.md
+ echo "" >> coverage_report.md
+ echo "**Coverage Validated**" >> coverage_report.md
+ echo "Modified files do not have standalone unit test targets." >> coverage_report.md
+
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+ fi
+
+ - name: Merge Profiles
+ if: env.TARGETS != ''
+ run: |
+ find . -name "libc_cov_*.profraw" > profraw_list.txt
+ NUM_PROFS=$(wc -l < profraw_list.txt || echo 0)
+ echo "[LOG] Discovered $NUM_PROFS raw profile data files."
+ if [ -s profraw_list.txt ]; then
+ llvm-profdata-23 merge -sparse --input-files=profraw_list.txt -o libc_full.profdata
+ echo "[LOG] Successfully merged profile data into libc_full.profdata."
+ else
+ echo "[LOG] Warning: No profraw files found."
+ fi
+
+ - name: Extract Executables and Generate Summary
+ if: env.TARGETS != ''
+ run: |
+ EXECUTABLES=($(find build-cov -type f -executable -name "*__build__"))
+ OBJECTS=("${EXECUTABLES[@]:1}")
+ OBJECTS=("${OBJECTS[@]/#/-object=}")
+
+ echo "[LOG] Discovered ${#EXECUTABLES[@]} instrumented binaries for llvm-cov export."
+
+ if [ ! -f libc_full.profdata ] || [ ${#EXECUTABLES[@]} -eq 0 ]; then
+ echo "[LOG] Notice: Profile data or instrumented executables not found. Generating non-coverage summary."
+ echo "## LLVM-libc Patch Coverage Report" > coverage_report.md
+ echo "" >> coverage_report.md
+ echo "- **Base Branch:** [\`${BASE_REF}\` (${BASE_SHA:0:7})](https://github.com/${BASE_REPO}/commit/${BASE_SHA})" >> coverage_report.md
+ echo "- **Head Commit:** [\`${HEAD_REF}\` (${HEAD_SHA:0:7})](https://github.com/${HEAD_REPO}/commit/${HEAD_SHA})" >> coverage_report.md
+ echo "---" >> coverage_report.md
+ echo "" >> coverage_report.md
+ echo "**Coverage Validated**" >> coverage_report.md
+ echo "No executable statement profile data was collected for the modified lines in this patch." >> coverage_report.md
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+ exit 0
+ fi
+
+ # 1. Generate JSON report for Python script
+ llvm-cov-23 export -format=text -instr-profile=libc_full.profdata "${EXECUTABLES[0]}" "${OBJECTS[@]}" > coverage.json
+
+ # 2. Generate git diff for modified libc source files
+ git diff "$DIFF_BASE" HEAD -- libc/src/ > patch.diff
+
+ # 3. Run Diff Coverage Python Script using persisted commit metadata
+ PYTHONPATH="libc/utils/coverage" python3 libc/utils/coverage/diff_coverage.py patch.diff coverage.json "$BASE_SHA" "$HEAD_SHA" "$BASE_REF" "$HEAD_REF" "$TARGETS" "$BASE_REPO" "$HEAD_REPO" > coverage_report.md
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+ echo "[LOG] Coverage report successfully written to GITHUB_STEP_SUMMARY."
+
+ - name: Fallback Failure Summary
+ if: failure() && !hashFiles('coverage_report.md')
+ run: |
+ echo "## LLVM-libc Patch Coverage Report" > coverage_report.md
+ echo "" >> coverage_report.md
+ echo "- **Base Branch:** [\`${BASE_REF:-main}\` (${BASE_SHA:0:7})](https://github.com/${BASE_REPO:-llvm/llvm-project}/commit/${BASE_SHA:-main})" >> coverage_report.md
+ echo "- **Head Commit:** [\`${HEAD_REF:-HEAD}\` (${HEAD_SHA:0:7})](https://github.com/${HEAD_REPO:-llvm/llvm-project}/commit/${HEAD_SHA:-HEAD})" >> coverage_report.md
+ echo "---" >> coverage_report.md
+ echo "" >> coverage_report.md
+ echo "**Coverage Validated**" >> coverage_report.md
+ echo "The targeted build or test execution encountered an error before coverage data could be finalized. Inspect the job logs for details." >> coverage_report.md
+
+ cat coverage_report.md >> $GITHUB_STEP_SUMMARY
+
+
+ - name: Upload Patch MC/DC Coverage Artifacts
+ if: always() && hashFiles('coverage_report.md')
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: libc-patch-mcdc-coverage-report
+ path: |
+ coverage_report.md
+ patch.diff
+ coverage.json
+ if-no-files-found: ignore
+ retention-days: 14
+ overwrite: true
diff --git a/libc/utils/coverage/codebase_coverage.py b/libc/utils/coverage/codebase_coverage.py
new file mode 100644
index 0000000000000..03b35e84ace70
--- /dev/null
+++ b/libc/utils/coverage/codebase_coverage.py
@@ -0,0 +1,325 @@
+#!/usr/bin/env python3
+#
+# ===- Generate codebase coverage reports --------------------*- python -*--==#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+# ==------------------------------------------------------------------------==#
+
+"""
+Standalone file for generating whole-codebase statement, branch, and MC/DC
+coverage reports.
+
+This script parses full-codebase `llvm-cov export` JSON files, aggregates
+metrics across all top-level LLVM-libc directories (e.g. `src/ctype`, `src/math`,
+`src/string`), and outputs Markdown summary tables for CI step summaries.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+DEFAULT_REPOSITORY = "llvm/llvm-project"
+
+
+@dataclass
+class DirectoryCoverageMetrics:
+ """Encapsulates coverage metrics and boolean decision counts for a directory."""
+
+ name: str = ""
+ lines_cov: int = 0
+ lines_tot: int = 0
+ func_cov: int = 0
+ func_tot: int = 0
+ mcdc_cov: int = 0
+ mcdc_tot: int = 0
+ decisions_tot: int = 0
+ decisions_full: int = 0
+
+ @property
+ def line_pct(self) -> float:
+ """Percentage of executed statements."""
+ return (self.lines_cov / self.lines_tot * 100.0) if self.lines_tot > 0 else 0.0
+
+ @property
+ def func_pct(self) -> float:
+ """Percentage of executed functions."""
+ return (self.func_cov / self.func_tot * 100.0) if self.func_tot > 0 else 0.0
+
+ @property
+ def mcdc_pct(self) -> float:
+ """Percentage of evaluated independent boolean conditions."""
+ return (self.mcdc_cov / self.mcdc_tot * 100.0) if self.mcdc_tot > 0 else 0.0
+
+ @property
+ def decisions_pct(self) -> float:
+ """Percentage of fully verified boolean decisions."""
+ if self.decisions_tot == 0:
+ return 0.0
+ return self.decisions_full / self.decisions_tot * 100.0
+
+ @property
+ def missed_lines(self) -> int:
+ """Count of unexecuted lines."""
+ return max(0, self.lines_tot - self.lines_cov)
+
+
+@dataclass
+class FullCoverageSummary:
+ """Encapsulates global and directory-level coverage statistics across LLVM-libc."""
+
+ global_stats: DirectoryCoverageMetrics = field(
+ default_factory=DirectoryCoverageMetrics
+ )
+ directories: Dict[str, DirectoryCoverageMetrics] = field(default_factory=dict)
+
+ @property
+ def has_mcdc(self) -> bool:
+ """Returns True if any MC/DC condition data exists in the summary."""
+ return self.global_stats.mcdc_tot > 0
+
+
+def extract_full_coverage_statistics(
+ cov_data: dict,
+) -> Optional[FullCoverageSummary]:
+ """Extracts global and per-directory metrics from llvm-cov export JSON data."""
+ if "data" not in cov_data or not cov_data["data"]:
+ return None
+
+ global_metrics = DirectoryCoverageMetrics(name="global")
+ directories: Dict[str, DirectoryCoverageMetrics] = {}
+
+ for item in cov_data["data"][0].get("files", []):
+ file_path = item.get("filename", "")
+ if "src/" not in file_path or "/test/" in file_path or "/utils/" in file_path:
+ continue
+
+ idx = file_path.find("src/")
+ rel_path = file_path[idx:]
+
+ summary = item.get("summary", {})
+ lines_summary = summary.get("lines", {})
+ func_summary = summary.get("functions", {})
+ mcdc_summary = summary.get("mcdc", {})
+
+ line_tot = lines_summary.get("count", 0)
+ line_cov = lines_summary.get("covered", 0)
+ func_tot = func_summary.get("count", 0)
+ func_cov = func_summary.get("covered", 0)
+ mcdc_tot = mcdc_summary.get("count", 0)
+ mcdc_cov = mcdc_summary.get("covered", 0)
+
+ if line_tot == 0:
+ continue
+
+ mcdc_records = item.get("mcdc_records", [])
+ valid_mcdc_records = [
+ rec
+ for rec in mcdc_records
+ if len(rec) >= 10 and isinstance(rec[9], list) and len(rec[9]) > 0
+ ]
+ file_decisions_tot = len(valid_mcdc_records)
+ file_decisions_full = sum(1 for rec in valid_mcdc_records if all(rec[9]))
+
+ global_metrics.lines_cov += line_cov
+ global_metrics.lines_tot += line_tot
+ global_metrics.func_cov += func_cov
+ global_metrics.func_tot += func_tot
+ global_metrics.mcdc_cov += mcdc_cov
+ global_metrics.mcdc_tot += mcdc_tot
+ global_metrics.decisions_tot += file_decisions_tot
+ global_metrics.decisions_full += file_decisions_full
+
+ parts = rel_path.split("/")
+ directory_name = "/".join(parts[:2]) if len(parts) >= 2 else parts[0]
+
+ if directory_name not in directories:
+ directories[directory_name] = DirectoryCoverageMetrics(name=directory_name)
+
+ dir_metrics = directories[directory_name]
+ dir_metrics.lines_cov += line_cov
+ dir_metrics.lines_tot += line_tot
+ dir_metrics.func_cov += func_cov
+ dir_metrics.func_tot += func_tot
+ dir_metrics.mcdc_cov += mcdc_cov
+ dir_metrics.mcdc_tot += mcdc_tot
+ dir_metrics.decisions_tot += file_decisions_tot
+ dir_metrics.decisions_full += file_decisions_full
+
+ if global_metrics.lines_tot == 0:
+ return None
+
+ return FullCoverageSummary(
+ global_stats=global_metrics,
+ directories=directories,
+ )
+
+
+def format_overview_callout(summary: FullCoverageSummary) -> str:
+ """Generates the executive summary banner."""
+ g = summary.global_stats
+ lines: List[str] = []
+
+ if summary.has_mcdc:
+ lines.append(
+ f"### Overall Codebase Coverage: **{g.line_pct:.2f}% Line**"
+ f" | **{g.mcdc_pct:.2f}% MC/DC**"
+ )
+ lines.append(
+ f"Tested **{g.lines_cov:,} / {g.lines_tot:,}** executable lines "
+ f"and **{g.mcdc_cov:,} / {g.mcdc_tot:,}** boolean conditions "
+ f"across **{g.decisions_tot:,}** decisions."
+ )
+ else:
+ lines.append(f"### Overall Codebase Coverage: **{g.line_pct:.2f}%**")
+ lines.append(
+ f"Tested **{g.lines_cov:,} / {g.lines_tot:,}** executable lines "
+ "across all LLVM-libc directories."
+ )
+
+ lines.append("")
+ lines.append(
+ "- **HTML Coverage Report:** Available for download under the "
+ "**Artifacts** section of this workflow run."
+ )
+ return "\n".join(lines)
+
+
+def format_global_summary_table(summary: FullCoverageSummary) -> str:
+ """Generates the top-level metric summary table."""
+ g = summary.global_stats
+ lines: List[str] = [
+ "### Overall",
+ "| Metric | Covered | Total | Coverage % |",
+ "| :--- | :---: | :---: | :---: |",
+ ]
+
+ if summary.has_mcdc:
+ lines.append(
+ f"| **MC/DC Condition Independence** | {g.mcdc_cov:,} | "
+ f"{g.mcdc_tot:,} | **{g.mcdc_pct:.2f}%** |"
+ )
+ lines.append(
+ f"| **Fully Verified Decisions** | {g.decisions_full:,} | "
+ f"{g.decisions_tot:,} | **{g.decisions_pct:.2f}%** |"
+ )
+
+ lines.append(
+ f"| **Executable Lines** | {g.lines_cov:,} | {g.lines_tot:,} | "
+ f"**{g.line_pct:.2f}%** |"
+ )
+ lines.append(
+ f"| **Functions** | {g.func_cov:,} | {g.func_tot:,} | "
+ f"**{g.func_pct:.2f}%** |"
+ )
+ return "\n".join(lines)
+
+
+def format_directory_breakdown_table(summary: FullCoverageSummary) -> str:
+ """Generates the directory breakdown table."""
+ lines: List[str] = ["### Coverage Breakdown"]
+ has_mcdc = summary.has_mcdc
+
+ if has_mcdc:
+ lines.append(
+ "| Directory | MC/DC Conditions | Decisions (Verified / Total) | "
+ "Line Coverage | Function Coverage | Executable Lines | Missed Lines |"
+ )
+ lines.append("| :--- | :---: | :---: | :---: | :---: | :---: | :---: |")
+ else:
+ lines.append(
+ "| Directory | Line Coverage | Function Coverage | "
+ "Executable Lines | Missed Lines |"
+ )
+ lines.append("| :--- | :---: | :---: | :---: | :---: |")
+
+ for dir_name in sorted(summary.directories.keys()):
+ data = summary.directories[dir_name]
+ if has_mcdc:
+ mc_cell = (
+ f"**{data.mcdc_pct:.1f}%** ({data.mcdc_cov}/{data.mcdc_tot})"
+ if data.mcdc_tot > 0
+ else "N/A"
+ )
+ dec_cell = (
+ f"{data.decisions_full} / {data.decisions_tot}"
+ if data.decisions_tot > 0
+ else "N/A"
+ )
+ lines.append(
+ f"| `libc/{dir_name}` | {mc_cell} | {dec_cell} | "
+ f"**{data.line_pct:.2f}%** | {data.func_pct:.2f}% | "
+ f"{data.lines_tot:,} | {data.missed_lines:,} |"
+ )
+ else:
+ lines.append(
+ f"| `libc/{dir_name}` | **{data.line_pct:.2f}%** | "
+ f"{data.func_pct:.2f}% | {data.lines_tot:,} | "
+ f"{data.missed_lines:,} |"
+ )
+
+ return "\n".join(lines)
+
+
+def render_full_report(cov_data: dict) -> None:
+ """Orchestrates extraction and renders the full Markdown report to stdout."""
+ summary = extract_full_coverage_statistics(cov_data)
+
+ print("## LLVM-libc Full Codebase Coverage Report\n")
+
+ if not summary:
+ print("### No Coverage Data Detected")
+ print("The test execution completed but no coverage profiles were exported.")
+ return
+
+ # 1. Executive Callout Banner
+ print(format_overview_callout(summary))
+ print("\n---\n")
+
+ # 2. Global Metric Summary Table
+ print(format_global_summary_table(summary))
+ print("")
+
+ # 3. Directory Breakdown Table
+ print(format_directory_breakdown_table(summary))
+
+
+def main() -> None:
+ """Parses command-line arguments and triggers report generation."""
+ parser = argparse.ArgumentParser(description="LLVM-libc Codebase Coverage Analyzer")
+ parser.add_argument("json_file", help="Path to llvm-cov export JSON file")
+ parser.add_argument(
+ "commit_sha",
+ nargs="?",
+ default="",
+ help="Commit SHA under evaluation",
+ )
+ parser.add_argument(
+ "branch_ref",
+ nargs="?",
+ default="",
+ help="Branch reference under evaluation",
+ )
+
+ args, _ = parser.parse_known_args()
+
+ try:
+ with open(args.json_file, "r", encoding="utf-8") as f:
+ cov_data = json.load(f)
+ except Exception as err:
+ sys.stderr.write(
+ f"Error: Failed to parse coverage JSON from '{args.json_file}': {err}\n"
+ )
+ sys.exit(1)
+
+ render_full_report(cov_data)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/libc/utils/coverage/diff_coverage.py b/libc/utils/coverage/diff_coverage.py
new file mode 100644
index 0000000000000..8bfbe78e1cda5
--- /dev/null
+++ b/libc/utils/coverage/diff_coverage.py
@@ -0,0 +1,800 @@
+#!/usr/bin/env python3
+#
+# ===- Generate diff coverage reports ------------------------*- python -*--==#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+# ==------------------------------------------------------------------------==#
+
+"""
+Standalone analyzer for evaluating diff-level statement, branch, and MC/DC
+coverage.
+
+This script parses unified git diffs alongside `llvm-cov export` JSON summaries,
+correlates added/modified lines with execution counts and boolean decision
+records, and outputs formatted Markdown reports for CI job summaries and PR
+comments.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import re
+import sys
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional, Set, Tuple
+
+DEFAULT_BASE_REPOSITORY = "llvm/llvm-project"
+DEFAULT_HEAD_REPOSITORY = "llvm/llvm-project"
+
+COMMENT_PREFIXES = ("//", "/*", "*", "*/")
+STRUCTURAL_TOKENS = ("{", "}", "};", "{};")
+DECLARATION_PREFIXES = (
+ "namespace ",
+ "extern ",
+ "using ",
+ "__attribute__",
+ "template",
+ "typedef ",
+ "friend ",
+ "static_assert",
+)
+ACCESS_SPECIFIERS = ("public:", "private:", "protected:")
+
+
+@dataclass
+class DiffHunk:
+ """Represents a unified diff hunk with its header and line tokens."""
+
+ header: str
+ lines: List[Tuple[str, str, int]] = field(
+ default_factory=list
+ ) # (prefix, text, line_number)
+
+
+@dataclass
+class FilePatchMetrics:
+ """Encapsulates coverage metrics and decisions for a single modified file."""
+
+ file_path: str
+ covered_lines: Set[int] = field(default_factory=set)
+ missed_lines: Set[int] = field(default_factory=set)
+ added_lines: Set[int] = field(default_factory=set)
+ mcdc_covered_conditions: int = 0
+ mcdc_total_conditions: int = 0
+ decisions_verified: int = 0
+ decisions_total: int = 0
+ condition_diagnostics: List[str] = field(default_factory=list)
+ unverified_decision_lines: Dict[int, List[str]] = field(default_factory=dict)
+
+ @property
+ def total_lines(self) -> int:
+ """Total executable lines evaluated in this file."""
+ return len(self.covered_lines) + len(self.missed_lines)
+
+ @property
+ def line_coverage_percentage(self) -> float:
+ """Percentage of executed patch lines."""
+ return (
+ (len(self.covered_lines) / self.total_lines * 100.0)
+ if self.total_lines > 0
+ else 0.0
+ )
+
+ @property
+ def mcdc_coverage_percentage(self) -> float:
+ """Percentage of independent boolean conditions evaluated."""
+ return (
+ (self.mcdc_covered_conditions / self.mcdc_total_conditions * 100.0)
+ if self.mcdc_total_conditions > 0
+ else 0.0
+ )
+
+
+@dataclass
+class PatchCoverageSummary:
+ """Aggregated coverage statistics across all modified files in the patch."""
+
+ files: Dict[str, FilePatchMetrics] = field(default_factory=dict)
+ total_covered_lines: int = 0
+ total_missed_lines: int = 0
+ total_mcdc_covered_conditions: int = 0
+ total_mcdc_total_conditions: int = 0
+ total_decisions_count: int = 0
+ fully_verified_decisions: int = 0
+
+ @property
+ def total_lines(self) -> int:
+ """Total executable lines across all modified files in the patch."""
+ return self.total_covered_lines + self.total_missed_lines
+
+ @property
+ def line_coverage_percentage(self) -> float:
+ """Aggregated patch line coverage percentage."""
+ return (
+ (self.total_covered_lines / self.total_lines * 100.0)
+ if self.total_lines > 0
+ else 0.0
+ )
+
+ @property
+ def mcdc_coverage_percentage(self) -> float:
+ """Aggregated patch MC/DC condition coverage percentage."""
+ if self.total_mcdc_total_conditions == 0:
+ return 0.0
+ return (
+ self.total_mcdc_covered_conditions
+ / self.total_mcdc_total_conditions
+ * 100.0
+ )
+
+ @property
+ def has_mcdc(self) -> bool:
+ """Returns True if any MC/DC decision records intersect the patch."""
+ return self.total_mcdc_total_conditions > 0
+
+
+class DiffParser:
+ """Parses unified diff outputs into structured file hunks with line numbers."""
+
+ @staticmethod
+ def parse(diff_source: str) -> Dict[str, List[DiffHunk]]:
+ """Parses a diff file into a mapping of file path to hunks."""
+ files: Dict[str, List[DiffHunk]] = {}
+ current_file: Optional[str] = None
+ current_hunk: Optional[DiffHunk] = None
+ current_line_number: int = 0
+
+ if os.path.isfile(diff_source):
+ with open(
+ diff_source, "r", encoding="utf-8", errors="replace"
+ ) as file_handle:
+ lines = file_handle.readlines()
+ else:
+ lines = diff_source.splitlines(keepends=True)
+
+ for raw_line in lines:
+ line = raw_line.rstrip("\n")
+
+ if line.startswith("+++ b/"):
+ current_file = line[6:]
+ files[current_file] = []
+ current_hunk = None
+ continue
+
+ if line.startswith("+++ /dev/null"):
+ current_file = None
+ current_hunk = None
+ continue
+
+ if current_file is None:
+ continue
+
+ if line.startswith("@@"):
+ hunk_match = re.search(r"\+([0-9]+)", line)
+ if hunk_match:
+ current_line_number = int(hunk_match.group(1))
+ current_hunk = DiffHunk(header=line)
+ files[current_file].append(current_hunk)
+ continue
+
+ if current_hunk is None:
+ continue
+
+ if line.startswith("-"):
+ continue
+ elif line.startswith("+"):
+ current_hunk.lines.append(("+", line[1:], current_line_number))
+ current_line_number += 1
+ elif line.startswith(" "):
+ current_hunk.lines.append((" ", line[1:], current_line_number))
+ current_line_number += 1
+
+ return files
+
+
+class CoverageJSONParser:
+ """Parses statement segments and MC/DC records from llvm-cov JSON export."""
+
+ @staticmethod
+ def load(json_path: str) -> dict:
+ """Loads JSON file from disk with error reporting."""
+ try:
+ with open(json_path, "r", encoding="utf-8") as file_handle:
+ return json.load(file_handle)
+ except Exception as error:
+ sys.stderr.write(
+ f"Error: Failed to parse coverage JSON from '{json_path}': {error}\n"
+ )
+ sys.exit(1)
+
+ @staticmethod
+ def extract_patch_matrix(
+ coverage_data: dict, diff_files: Dict[str, List[DiffHunk]]
+ ) -> Dict[str, Dict[str, Any]]:
+ """Maps coverage segments and MC/DC decision records to modified files."""
+ coverage_matrix: Dict[str, Dict[str, Any]] = {
+ file_path: {
+ "covered": set(),
+ "missed": set(),
+ "mcdc_decisions": [],
+ }
+ for file_path in diff_files.keys()
+ }
+
+ if "data" not in coverage_data or not coverage_data["data"]:
+ return coverage_matrix
+
+ for item in coverage_data["data"][0].get("files", []):
+ file_name = item.get("filename", "")
+ relative_file_path = next(
+ (
+ target_path
+ for target_path in diff_files.keys()
+ if file_name == target_path
+ or file_name.endswith("/" + target_path)
+ or target_path.endswith("/" + file_name)
+ ),
+ None,
+ )
+ if not relative_file_path:
+ continue
+
+ # 1. Process statement coverage segments
+ segments = item.get("segments", [])
+ for index, current_segment in enumerate(segments):
+ line_start = current_segment[0]
+ execution_count = current_segment[2]
+ has_execution_count = current_segment[3]
+
+ if not has_execution_count:
+ continue
+
+ if index < len(segments) - 1:
+ next_segment = segments[index + 1]
+ next_line = next_segment[0]
+ end_range = next_line if next_line > line_start else line_start + 1
+ else:
+ end_range = line_start + 1
+
+ for line_number in range(line_start, end_range):
+ if execution_count > 0:
+ coverage_matrix[relative_file_path]["covered"].add(line_number)
+ else:
+ coverage_matrix[relative_file_path]["missed"].add(line_number)
+
+ # 2. Process MC/DC decision records
+ mcdc_records = item.get("mcdc_records", [])
+ for record in mcdc_records:
+ if (
+ len(record) >= 10
+ and isinstance(record[9], list)
+ and len(record[9]) > 0
+ ):
+ decision_start_line = record[0]
+ decision_end_line = record[2]
+ boolean_conditions = record[9]
+ covered_conditions_count = sum(
+ 1 for condition in boolean_conditions if condition
+ )
+ coverage_matrix[relative_file_path]["mcdc_decisions"].append(
+ {
+ "line_start": decision_start_line,
+ "line_end": decision_end_line,
+ "conditions": boolean_conditions,
+ "covered": covered_conditions_count,
+ "total": len(boolean_conditions),
+ }
+ )
+
+ return coverage_matrix
+
+
+def is_executable_line(line_text: str) -> bool:
+ """Filters out non-executable code lines (comments, braces, pure declarations)."""
+ stripped_line = line_text.strip()
+ if not stripped_line:
+ return False
+ # Strip block comments on the same line and trailing line comments
+ clean_line = re.sub(r"/\*.*?\*/", "", stripped_line)
+ clean_line = re.sub(r"//.*$", "", clean_line).strip()
+ if not clean_line:
+ return False
+ if (
+ clean_line.startswith("*")
+ or clean_line.startswith("/*")
+ or clean_line.startswith("*/")
+ ):
+ return False
+ if clean_line in STRUCTURAL_TOKENS or clean_line.startswith(":"):
+ return False
+ if clean_line in ACCESS_SPECIFIERS:
+ return False
+ if clean_line.startswith("#"):
+ return False
+ if any(clean_line.startswith(prefix) for prefix in DECLARATION_PREFIXES):
+ return False
+ if (
+ clean_line.startswith("struct ")
+ or clean_line.startswith("class ")
+ or clean_line.startswith("enum ")
+ ):
+ if ("{" in clean_line and "=" not in clean_line) or (
+ clean_line.endswith(";") and "=" not in clean_line and "(" not in clean_line
+ ):
+ return False
+ return True
+
+
+def format_line_ranges(line_numbers: Set[int]) -> str:
+ """Formats an integer set of line numbers into concise span representations."""
+ if not line_numbers:
+ return "None"
+ sorted_line_numbers = sorted(line_numbers)
+ formatted_ranges: List[str] = []
+ start_line = sorted_line_numbers[0]
+ end_line = sorted_line_numbers[0]
+ for current_number in sorted_line_numbers[1:]:
+ if current_number == end_line + 1:
+ end_line = current_number
+ else:
+ range_label = (
+ f"`L{start_line}-L{end_line}`"
+ if start_line != end_line
+ else f"`L{start_line}`"
+ )
+ formatted_ranges.append(range_label)
+ start_line = end_line = current_number
+ range_label = (
+ f"`L{start_line}-L{end_line}`" if start_line != end_line else f"`L{start_line}`"
+ )
+ formatted_ranges.append(range_label)
+ return ", ".join(formatted_ranges)
+
+
+def calculate_patch_statistics(
+ diff_files: Dict[str, List[DiffHunk]],
+ coverage_matrix: Dict[str, Dict[str, Any]],
+) -> PatchCoverageSummary:
+ """Calculates line, branch, and MC/DC statistics for modified patch files."""
+ summary = PatchCoverageSummary()
+
+ for file_path, file_data in coverage_matrix.items():
+ if (
+ not any(file_path.endswith(ext) for ext in (".cpp", ".c", ".h", ".inc"))
+ or "/test/" in file_path
+ or file_path.startswith("test/")
+ or "/utils/" in file_path
+ or file_path.startswith("utils/")
+ ):
+ continue
+
+ added_lines: Set[int] = set()
+ for hunk in diff_files.get(file_path, []):
+ for line_type, text, line_number in hunk.lines:
+ if line_type == "+" and is_executable_line(text):
+ added_lines.add(line_number)
+
+ if not added_lines:
+ continue
+
+ file_covered_lines = added_lines.intersection(file_data["covered"])
+ file_missed_lines = (
+ added_lines.intersection(file_data["missed"])
+ ) - file_covered_lines
+
+ file_metrics = FilePatchMetrics(
+ file_path=file_path,
+ added_lines=added_lines,
+ )
+
+ if len(file_data["covered"]) > 0 or len(file_data["missed"]) > 0:
+ file_metrics.covered_lines = file_covered_lines
+ file_metrics.missed_lines = file_missed_lines
+ summary.total_covered_lines += len(file_covered_lines)
+ summary.total_missed_lines += len(file_missed_lines)
+ else:
+ file_metrics.missed_lines = added_lines
+ summary.total_missed_lines += len(added_lines)
+
+ # Evaluate MC/DC decision records intersecting modified lines
+ for decision in file_data.get("mcdc_decisions", []):
+ decision_start_line = decision["line_start"]
+ decision_end_line = decision["line_end"]
+ if any(
+ decision_start_line <= line_number <= decision_end_line
+ for line_number in added_lines
+ ):
+ summary.total_decisions_count += 1
+ file_metrics.decisions_total += 1
+ file_metrics.mcdc_covered_conditions += decision["covered"]
+ file_metrics.mcdc_total_conditions += decision["total"]
+ summary.total_mcdc_covered_conditions += decision["covered"]
+ summary.total_mcdc_total_conditions += decision["total"]
+
+ if decision["covered"] == decision["total"]:
+ summary.fully_verified_decisions += 1
+ file_metrics.decisions_verified += 1
+ file_metrics.condition_diagnostics.append(
+ f"`L{decision_start_line}`: "
+ f"{decision['covered']}/{decision['total']} verified"
+ )
+ else:
+ uncovered_indices = [
+ f"C{condition_index + 1}"
+ for condition_index, is_covered in enumerate(
+ decision["conditions"]
+ )
+ if not is_covered
+ ]
+ unverified_conditions_string = ", ".join(uncovered_indices)
+ file_metrics.condition_diagnostics.append(
+ f"`L{decision_start_line}`: "
+ f"{decision['covered']}/{decision['total']} verified "
+ f"({unverified_conditions_string} unverified)"
+ )
+ for decision_line in range(
+ decision_start_line, decision_end_line + 1
+ ):
+ if decision_line in added_lines:
+ file_metrics.unverified_decision_lines[
+ decision_line
+ ] = uncovered_indices
+
+ summary.files[file_path] = file_metrics
+
+ return summary
+
+
+def format_status_banner(summary: PatchCoverageSummary) -> str:
+ """Generates the executive summary block."""
+ lines: List[str] = []
+ if summary.total_missed_lines == 0:
+ if not summary.has_mcdc:
+ lines.append(
+ f"### Patch Coverage: **{summary.line_coverage_percentage:.2f}%**"
+ )
+ lines.append(
+ f"All **{summary.total_lines}** newly added or modified "
+ "executable lines are covered."
+ )
+ elif (
+ summary.total_mcdc_covered_conditions == summary.total_mcdc_total_conditions
+ ):
+ lines.append(
+ f"### Patch Coverage: "
+ f"**{summary.line_coverage_percentage:.2f}% Line** | "
+ "**100.00% MC/DC**"
+ )
+ lines.append(
+ f"All **{summary.total_lines}** executable lines and "
+ f"**{summary.total_mcdc_total_conditions}** boolean conditions "
+ f"across **{summary.total_decisions_count}** decisions are covered."
+ )
+ else:
+ lines.append(
+ f"### Patch Coverage: "
+ f"**{summary.line_coverage_percentage:.2f}% Line** | "
+ f"**{summary.mcdc_coverage_percentage:.1f}% MC/DC**"
+ )
+ lines.append(
+ f"Executed **{summary.total_covered_lines} / {summary.total_lines}** "
+ f"lines. **{summary.total_mcdc_covered_conditions} / "
+ f"{summary.total_mcdc_total_conditions}** boolean conditions "
+ f"achieved independence across **{summary.fully_verified_decisions} / "
+ f"{summary.total_decisions_count}** decisions."
+ )
+ else:
+ if not summary.has_mcdc:
+ lines.append(
+ f"### Patch Coverage: "
+ f"**{summary.line_coverage_percentage:.2f}%** "
+ f"({summary.total_missed_lines} Missed Lines)"
+ )
+ lines.append(
+ f"Executed **{summary.total_covered_lines} / {summary.total_lines}** "
+ f"lines (**{summary.total_missed_lines}** unexecuted lines "
+ "detected in patch)."
+ )
+ else:
+ lines.append(
+ f"### Patch Coverage: "
+ f"**{summary.line_coverage_percentage:.2f}% Line** | "
+ f"**{summary.mcdc_coverage_percentage:.1f}% MC/DC** "
+ f"({summary.total_missed_lines} Missed Lines)"
+ )
+ lines.append(
+ f"Executed **{summary.total_covered_lines} / {summary.total_lines}** "
+ f"lines. **{summary.total_mcdc_covered_conditions} / "
+ f"{summary.total_mcdc_total_conditions}** boolean conditions "
+ f"achieved independence across **{summary.fully_verified_decisions} / "
+ f"{summary.total_decisions_count}** decisions "
+ f"(**{summary.total_missed_lines}** unexecuted lines "
+ "detected in patch)."
+ )
+ return "\n".join(lines)
+
+
+def format_metadata_section(
+ base_commit_sha: Optional[str],
+ head_commit_sha: Optional[str],
+ base_branch_name: Optional[str],
+ head_branch_name: Optional[str],
+ targeted_tests_string: Optional[str] = None,
+ base_repository: str = DEFAULT_BASE_REPOSITORY,
+ head_repository: str = DEFAULT_HEAD_REPOSITORY,
+) -> str:
+ """Formats Git commit and target test metadata."""
+ lines: List[str] = []
+ if base_commit_sha and head_commit_sha and base_branch_name and head_branch_name:
+ lines.append(
+ f"- **Base Branch:** [`{base_branch_name}` ({base_commit_sha[:7]})]"
+ f"(https://github.com/{base_repository}/commit/{base_commit_sha})"
+ )
+ lines.append(
+ f"- **Head Commit:** [`{head_branch_name}` ({head_commit_sha[:7]})]"
+ f"(https://github.com/{head_repository}/commit/{head_commit_sha})"
+ )
+ if targeted_tests_string:
+ formatted_targets = ", ".join(
+ f"`{target.strip()}`"
+ for target in targeted_tests_string.split()
+ if target.strip()
+ )
+ lines.append(f"- **Targeted Tests Executed:** {formatted_targets}")
+ return "\n".join(lines)
+
+
+def format_breakdown_table(
+ summary: PatchCoverageSummary,
+ head_repository: str = DEFAULT_HEAD_REPOSITORY,
+ head_commit_sha: Optional[str] = None,
+) -> str:
+ """Generates the Markdown table breaking down coverage per source file."""
+ lines: List[str] = ["### Coverage Breakdown"]
+ if summary.has_mcdc:
+ lines.append(
+ "| Modified Source File | Line Coverage | MC/DC Conditions | "
+ "Decisions (Verified / Total) | Missed Lines | Unverified Conditions |"
+ )
+ lines.append("| :--- | :---: | :---: | :---: | :---: | :--- |")
+ else:
+ lines.append(
+ "| Modified Source File | Patch Coverage | Covered / Total | "
+ "Missed Lines | Unexecuted Line Spans |"
+ )
+ lines.append("| :--- | :---: | :---: | :---: | :---: |")
+
+ for file_path, file_metric in summary.files.items():
+ commit_ref = head_commit_sha or "main"
+ repo_file_path = (
+ file_path if file_path.startswith("libc/") else f"libc/{file_path}"
+ )
+ file_link = (
+ f"[`{file_path}`]"
+ f"(https://github.com/{head_repository}/blob/{commit_ref}/{repo_file_path})"
+ )
+ missed_lines = file_metric.missed_lines
+ covered_count = len(file_metric.covered_lines)
+ total_file_lines = file_metric.total_lines
+
+ if summary.has_mcdc:
+ mc_pct = file_metric.mcdc_coverage_percentage
+ mc_cov = file_metric.mcdc_covered_conditions
+ mc_tot = file_metric.mcdc_total_conditions
+ mcdc_cell = (
+ f"**{mc_pct:.1f}%** ({mc_cov}/{mc_tot})" if mc_tot > 0 else "N/A"
+ )
+ dec_cell = (
+ f"**{file_metric.decisions_verified} / "
+ f"{file_metric.decisions_total}**"
+ if file_metric.decisions_total > 0
+ else "N/A"
+ )
+ diagnostic_cell = (
+ "
".join(file_metric.condition_diagnostics)
+ if file_metric.condition_diagnostics
+ else "None"
+ )
+ lines.append(
+ f"| {file_link} | "
+ f"**{file_metric.line_coverage_percentage:.2f}%** "
+ f"({covered_count}/{total_file_lines}) | "
+ f"{mcdc_cell} | {dec_cell} | "
+ f"{len(missed_lines)} | {diagnostic_cell} |"
+ )
+ else:
+ line_spans = format_line_ranges(missed_lines)
+ lines.append(
+ f"| {file_link} | "
+ f"**{file_metric.line_coverage_percentage:.2f}%** | "
+ f"{covered_count} / {total_file_lines} | "
+ f"{len(missed_lines)} | {line_spans} |"
+ )
+
+ # Summary Row
+ if summary.has_mcdc:
+ total_decision_cell = (
+ f"**{summary.fully_verified_decisions} / "
+ f"{summary.total_decisions_count}**"
+ )
+ lines.append(
+ f"| **Total (Patch)** | "
+ f"**{summary.line_coverage_percentage:.2f}%** "
+ f"({summary.total_covered_lines}/{summary.total_lines}) | "
+ f"**{summary.mcdc_coverage_percentage:.1f}%** "
+ f"({summary.total_mcdc_covered_conditions}/"
+ f"{summary.total_mcdc_total_conditions}) | "
+ f"{total_decision_cell} | **{summary.total_missed_lines}** | - |"
+ )
+ else:
+ lines.append(
+ f"| **Total (Patch)** | "
+ f"**{summary.line_coverage_percentage:.2f}%** | "
+ f"{summary.total_covered_lines} / {summary.total_lines} | "
+ f"**{summary.total_missed_lines}** | - |"
+ )
+
+ return "\n".join(lines)
+
+
+def format_annotated_diff(
+ summary: PatchCoverageSummary,
+ diff_files: Dict[str, List[DiffHunk]],
+) -> str:
+ """Renders the collapsible source map diff with execution indicators."""
+ lines: List[str] = [
+ "",
+ "View Annotated Patch Diff (Source Map)
\n",
+ ]
+
+ for file_path, file_metric in summary.files.items():
+ hunks = diff_files.get(file_path, [])
+ unverified_decision_lines = file_metric.unverified_decision_lines
+
+ lines.append(f"#### `{file_path}`")
+ lines.append("```diff")
+ for hunk in hunks:
+ lines.append(hunk.header)
+ for line_type, line_text, line_number in hunk.lines:
+ if line_type == "+":
+ if line_number in file_metric.missed_lines:
+ lines.append(f"- {line_text} // [MISSED]")
+ elif line_number in unverified_decision_lines:
+ unverified_conditions = ", ".join(
+ unverified_decision_lines[line_number]
+ )
+ lines.append(
+ f"! {line_text} // [PARTIAL MC/DC: "
+ f"{unverified_conditions} unverified]"
+ )
+ elif line_number in file_metric.covered_lines:
+ lines.append(f"+ {line_text}")
+ else:
+ lines.append(f" {line_text}")
+ elif line_type == " ":
+ lines.append(f" {line_text}")
+ lines.append("```\n")
+
+ lines.append(" ")
+ return "\n".join(lines)
+
+
+def render_patch_report(
+ diff_files: Dict[str, List[DiffHunk]],
+ coverage_matrix: Dict[str, Dict[str, Any]],
+ base_commit_sha: Optional[str],
+ head_commit_sha: Optional[str],
+ base_branch_name: Optional[str],
+ head_branch_name: Optional[str],
+ targeted_tests_string: Optional[str] = None,
+ base_repository: str = DEFAULT_BASE_REPOSITORY,
+ head_repository: str = DEFAULT_HEAD_REPOSITORY,
+) -> None:
+ """Composes and outputs the full Markdown report."""
+ summary = calculate_patch_statistics(diff_files, coverage_matrix)
+
+ if summary.has_mcdc:
+ print("## LLVM-libc MC/DC Patch Coverage Report\n")
+ else:
+ print("## LLVM-libc Patch Coverage Report\n")
+
+ if summary.total_lines == 0 or not summary.files:
+ metadata_section_string = format_metadata_section(
+ base_commit_sha,
+ head_commit_sha,
+ base_branch_name,
+ head_branch_name,
+ targeted_tests_string,
+ base_repository,
+ head_repository,
+ )
+ if metadata_section_string:
+ print(metadata_section_string)
+ print("\n---\n")
+ print("### Coverage Summary")
+ print("No executable lines were added or modified in this patch.")
+ return
+
+ # 1. Status Banner
+ print(format_status_banner(summary))
+ print("")
+
+ # 2. Metadata Section
+ metadata_section_string = format_metadata_section(
+ base_commit_sha,
+ head_commit_sha,
+ base_branch_name,
+ head_branch_name,
+ targeted_tests_string,
+ base_repository,
+ head_repository,
+ )
+ if metadata_section_string:
+ print(metadata_section_string)
+ print("\n---\n")
+
+ # 3. Breakdown Table
+ print(format_breakdown_table(summary, head_repository, head_commit_sha))
+ print("")
+
+ # 4. Source Map Diff
+ print(format_annotated_diff(summary, diff_files))
+
+
+def main() -> None:
+ """Parses command-line arguments and triggers report generation."""
+ parser = argparse.ArgumentParser(description="LLVM-libc Diff Coverage Analyzer")
+ parser.add_argument("diff_file", help="Path to unified diff file")
+ parser.add_argument("json_file", help="Path to llvm-cov export JSON file")
+ parser.add_argument("base_sha", nargs="?", help="Base commit SHA")
+ parser.add_argument("head_sha", nargs="?", help="Head commit SHA")
+ parser.add_argument("base_branch", nargs="?", help="Base branch name")
+ parser.add_argument("head_branch", nargs="?", help="Head branch name")
+ parser.add_argument(
+ "targets",
+ nargs="?",
+ help="Space-separated list of executed test targets",
+ )
+ parser.add_argument(
+ "base_repo",
+ nargs="?",
+ default=DEFAULT_BASE_REPOSITORY,
+ help=f"Base repository (default: {DEFAULT_BASE_REPOSITORY})",
+ )
+ parser.add_argument(
+ "head_repo",
+ nargs="?",
+ default=DEFAULT_HEAD_REPOSITORY,
+ help=f"Head repository (default: {DEFAULT_HEAD_REPOSITORY})",
+ )
+
+ arguments = parser.parse_args()
+
+ if not os.path.isfile(arguments.diff_file):
+ sys.stderr.write(f"Error: Diff file not found: '{arguments.diff_file}'\n")
+ sys.exit(1)
+
+ diff_files = DiffParser.parse(arguments.diff_file)
+ coverage_data = CoverageJSONParser.load(arguments.json_file)
+ coverage_matrix = CoverageJSONParser.extract_patch_matrix(coverage_data, diff_files)
+
+ render_patch_report(
+ diff_files,
+ coverage_matrix,
+ arguments.base_sha,
+ arguments.head_sha,
+ arguments.base_branch,
+ arguments.head_branch,
+ arguments.targets,
+ arguments.base_repo,
+ arguments.head_repo,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/libc/utils/coverage/test_codebase_coverage.py b/libc/utils/coverage/test_codebase_coverage.py
new file mode 100644
index 0000000000000..9d9d19ee3fad0
--- /dev/null
+++ b/libc/utils/coverage/test_codebase_coverage.py
@@ -0,0 +1,705 @@
+# ====- Unit tests for codebase_coverage.py ------------------*- python -*--==#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+# ==-------------------------------------------------------------------------==#
+
+"""Unit tests for codebase_coverage.py."""
+
+import io
+import json
+import os
+import sys
+import tempfile
+import unittest
+from contextlib import redirect_stdout
+from unittest.mock import patch
+
+# Ensure libc/utils/coverage is in sys.path when running from any working directory
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from codebase_coverage import (
+ DirectoryCoverageMetrics,
+ FullCoverageSummary,
+ extract_full_coverage_statistics,
+ format_directory_breakdown_table,
+ format_global_summary_table,
+ format_overview_callout,
+ main,
+ render_full_report,
+)
+
+
+class TestDirectoryCoverageMetrics(unittest.TestCase):
+ """Tests DirectoryCoverageMetrics mathematical operations and property safeguards."""
+
+ def test_zero_totals_return_zero_percentages(self):
+ """Zero totals must safely evaluate to 0.0 without ZeroDivisionError."""
+ metrics = DirectoryCoverageMetrics(name="empty")
+ self.assertEqual(metrics.line_pct, 0.0)
+ self.assertEqual(metrics.func_pct, 0.0)
+ self.assertEqual(metrics.mcdc_pct, 0.0)
+ self.assertEqual(metrics.decisions_pct, 0.0)
+ self.assertEqual(metrics.missed_lines, 0)
+
+ def test_percentage_calculations(self):
+ """Percentages must correctly compute ratios across lines, functions, and MC/DC."""
+ metrics = DirectoryCoverageMetrics(
+ name="src/math",
+ lines_cov=75,
+ lines_tot=100,
+ func_cov=3,
+ func_tot=4,
+ mcdc_cov=7,
+ mcdc_tot=10,
+ decisions_tot=5,
+ decisions_full=4,
+ )
+ self.assertAlmostEqual(metrics.line_pct, 75.0, places=2)
+ self.assertAlmostEqual(metrics.func_pct, 75.0, places=2)
+ self.assertAlmostEqual(metrics.mcdc_pct, 70.0, places=2)
+ self.assertAlmostEqual(metrics.decisions_pct, 80.0, places=2)
+ self.assertEqual(metrics.missed_lines, 25)
+
+ def test_missed_lines_clamping(self):
+ """Missed lines must clamp to 0 if covered lines exceed total lines."""
+ metrics = DirectoryCoverageMetrics(
+ name="src/clamped", lines_cov=120, lines_tot=100
+ )
+ self.assertEqual(metrics.missed_lines, 0)
+
+ def test_all_zero_metrics(self):
+ """Default initialized metrics object must have zero values and empty name."""
+ metrics = DirectoryCoverageMetrics()
+ self.assertEqual(metrics.name, "")
+ self.assertEqual(metrics.lines_cov, 0)
+ self.assertEqual(metrics.lines_tot, 0)
+ self.assertEqual(metrics.decisions_pct, 0.0)
+
+
+class TestFullCoverageSummary(unittest.TestCase):
+ """Tests FullCoverageSummary attributes and condition indicators."""
+
+ def test_has_mcdc_property(self):
+ """has_mcdc must reflect whether global MC/DC conditions exist."""
+ without_mcdc = FullCoverageSummary(
+ global_stats=DirectoryCoverageMetrics(mcdc_tot=0)
+ )
+ self.assertFalse(without_mcdc.has_mcdc)
+
+ with_mcdc = FullCoverageSummary(
+ global_stats=DirectoryCoverageMetrics(mcdc_tot=12)
+ )
+ self.assertTrue(with_mcdc.has_mcdc)
+
+ def test_default_initialization(self):
+ """Default FullCoverageSummary must initialize empty directories dictionary."""
+ summary = FullCoverageSummary()
+ self.assertFalse(summary.has_mcdc)
+ self.assertEqual(len(summary.directories), 0)
+
+
+class TestExtractFullCoverageStatistics(unittest.TestCase):
+ """Tests JSON extraction, file path filtering, directory grouping, and MC/DC aggregation."""
+
+ def test_empty_or_invalid_payloads_return_none(self):
+ """Missing or malformed payload structures must return None."""
+ test_cases = [
+ ({}, "Empty dictionary"),
+ ({"data": []}, "Empty data array"),
+ ({"data": [{}]}, "Data array without files key"),
+ ({"data": [{"files": []}]}, "Empty files array"),
+ ]
+ for payload, description in test_cases:
+ with self.subTest(msg=description):
+ self.assertIsNone(extract_full_coverage_statistics(payload))
+
+ def test_all_files_zero_lines_returns_none(self):
+ """Payload containing only files with zero total lines must return None."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/src/empty.cpp",
+ "summary": {
+ "lines": {"count": 0, "covered": 0},
+ "functions": {"count": 0, "covered": 0},
+ },
+ }
+ ]
+ }
+ ]
+ }
+ self.assertIsNone(extract_full_coverage_statistics(payload))
+
+ def test_all_files_excluded_returns_none(self):
+ """Payload containing only test or utility files must return None."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/test/src/math/sin_test.cpp",
+ "summary": {
+ "lines": {"count": 100, "covered": 100},
+ "functions": {"count": 1, "covered": 1},
+ },
+ },
+ {
+ "filename": "/workspace/libc/utils/MPFRWrapper/MPFRUtils.cpp",
+ "summary": {
+ "lines": {"count": 200, "covered": 200},
+ "functions": {"count": 2, "covered": 2},
+ },
+ },
+ ]
+ }
+ ]
+ }
+ self.assertIsNone(extract_full_coverage_statistics(payload))
+
+ def test_file_path_filtering(self):
+ """Test and utility directories must be excluded from codebase coverage."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/src/math/sin.cpp",
+ "summary": {
+ "lines": {"count": 100, "covered": 80},
+ "functions": {"count": 2, "covered": 2},
+ },
+ },
+ {
+ # Test file: must be excluded
+ "filename": "/workspace/libc/test/src/math/sin_test.cpp",
+ "summary": {
+ "lines": {"count": 500, "covered": 500},
+ "functions": {"count": 5, "covered": 5},
+ },
+ },
+ {
+ # Utility file: must be excluded
+ "filename": "/workspace/libc/utils/MPFRWrapper/MPFRUtils.cpp",
+ "summary": {
+ "lines": {"count": 300, "covered": 300},
+ "functions": {"count": 4, "covered": 4},
+ },
+ },
+ {
+ # Non-src file: must be excluded
+ "filename": "/workspace/libc/include/llvm-libc-types/size_t.h",
+ "summary": {
+ "lines": {"count": 50, "covered": 50},
+ "functions": {"count": 1, "covered": 1},
+ },
+ },
+ {
+ # File with zero total lines: must be excluded
+ "filename": "/workspace/libc/src/empty.cpp",
+ "summary": {
+ "lines": {"count": 0, "covered": 0},
+ "functions": {"count": 0, "covered": 0},
+ },
+ },
+ ]
+ }
+ ]
+ }
+
+ summary = extract_full_coverage_statistics(payload)
+ self.assertIsNotNone(summary)
+ self.assertEqual(summary.global_stats.lines_tot, 100)
+ self.assertEqual(summary.global_stats.lines_cov, 80)
+ self.assertEqual(summary.global_stats.func_tot, 2)
+ self.assertEqual(summary.global_stats.func_cov, 2)
+ self.assertIn("src/math", summary.directories)
+ self.assertEqual(len(summary.directories), 1)
+
+ def test_directory_bucketing_and_nested_paths(self):
+ """Files within identical top-level directories or deep subpaths must aggregate properly."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/src/math/sin.cpp",
+ "summary": {
+ "lines": {"count": 100, "covered": 60},
+ "functions": {"count": 2, "covered": 1},
+ },
+ },
+ {
+ "filename": "/workspace/libc/src/math/cos.cpp",
+ "summary": {
+ "lines": {"count": 80, "covered": 80},
+ "functions": {"count": 2, "covered": 2},
+ },
+ },
+ {
+ # Nested subpath under src/string/
+ "filename": "/workspace/libc/src/string/memory_utils/op_builtin.cpp",
+ "summary": {
+ "lines": {"count": 120, "covered": 100},
+ "functions": {"count": 4, "covered": 3},
+ },
+ },
+ ]
+ }
+ ]
+ }
+
+ summary = extract_full_coverage_statistics(payload)
+ self.assertIsNotNone(summary)
+ self.assertEqual(summary.global_stats.lines_tot, 300)
+ self.assertEqual(summary.global_stats.lines_cov, 240)
+
+ self.assertIn("src/math", summary.directories)
+ math_dir = summary.directories["src/math"]
+ self.assertEqual(math_dir.lines_tot, 180)
+ self.assertEqual(math_dir.lines_cov, 140)
+ self.assertEqual(math_dir.func_tot, 4)
+ self.assertEqual(math_dir.func_cov, 3)
+
+ self.assertIn("src/string", summary.directories)
+ str_dir = summary.directories["src/string"]
+ self.assertEqual(str_dir.lines_tot, 120)
+ self.assertEqual(str_dir.lines_cov, 100)
+
+ def test_mcdc_records_aggregation_and_decision_tracking(self):
+ """MC/DC records must be parsed for total conditions and full decision verification."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/src/math/fma.cpp",
+ "summary": {
+ "lines": {"count": 50, "covered": 50},
+ "functions": {"count": 1, "covered": 1},
+ "mcdc": {"count": 4, "covered": 3},
+ },
+ "mcdc_records": [
+ # Fully verified decision: [True, True]
+ [10, 5, 10, 20, 0, 0, 0, 0, 0, [True, True]],
+ # Partially verified decision: [True, False]
+ [25, 5, 25, 25, 0, 0, 0, 0, 0, [True, False]],
+ # Malformed record: ignored
+ [30, 5, 30, 20],
+ ],
+ }
+ ]
+ }
+ ]
+ }
+
+ summary = extract_full_coverage_statistics(payload)
+ self.assertIsNotNone(summary)
+ self.assertTrue(summary.has_mcdc)
+ self.assertEqual(summary.global_stats.mcdc_tot, 4)
+ self.assertEqual(summary.global_stats.mcdc_cov, 3)
+ self.assertEqual(summary.global_stats.decisions_tot, 2)
+ self.assertEqual(summary.global_stats.decisions_full, 1)
+
+ def test_malformed_and_empty_mcdc_records_ignored(self):
+ """Malformed, non-list, or empty condition vectors must not count as valid decisions."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/src/math/exp.cpp",
+ "summary": {
+ "lines": {"count": 50, "covered": 50},
+ "functions": {"count": 1, "covered": 1},
+ "mcdc": {"count": 2, "covered": 2},
+ },
+ "mcdc_records": [
+ # Valid fully verified decision
+ [10, 5, 10, 20, 0, 0, 0, 0, 0, [True]],
+ # Empty list: not a valid decision
+ [20, 5, 20, 20, 0, 0, 0, 0, 0, []],
+ # Non-list 10th element: not a valid decision
+ [30, 5, 30, 20, 0, 0, 0, 0, 0, None],
+ # Record length < 10: not a valid decision
+ [40, 5, 40, 20],
+ ],
+ }
+ ]
+ }
+ ]
+ }
+
+ summary = extract_full_coverage_statistics(payload)
+ self.assertIsNotNone(summary)
+ self.assertEqual(summary.global_stats.decisions_tot, 1)
+ self.assertEqual(summary.global_stats.decisions_full, 1)
+
+
+class TestCodebaseReportFormatting(unittest.TestCase):
+ """Tests Markdown rendering of callouts, summary tables, and directory breakdowns."""
+
+ def test_format_overview_callout_without_mcdc(self):
+ """Callout without MC/DC must render line coverage and artifacts instructions."""
+ metrics = DirectoryCoverageMetrics(name="global", lines_tot=1000, lines_cov=850)
+ summary = FullCoverageSummary(global_stats=metrics)
+ callout = format_overview_callout(summary)
+
+ self.assertIn("### Overall Codebase Coverage: **85.00%**", callout)
+ self.assertIn("Tested **850 / 1,000** executable lines", callout)
+ self.assertIn("Artifacts", callout)
+ self.assertIn("HTML Coverage Report", callout)
+ self.assertNotIn("MC/DC", callout)
+
+ def test_format_overview_callout_with_mcdc(self):
+ """Callout with MC/DC must render both line and condition coverage metrics."""
+ metrics = DirectoryCoverageMetrics(
+ name="global",
+ lines_tot=2000,
+ lines_cov=1800,
+ mcdc_tot=50,
+ mcdc_cov=45,
+ decisions_tot=20,
+ decisions_full=18,
+ )
+ summary = FullCoverageSummary(global_stats=metrics)
+ callout = format_overview_callout(summary)
+
+ self.assertIn(
+ "### Overall Codebase Coverage: **90.00% Line** | **90.00% MC/DC**",
+ callout,
+ )
+ self.assertIn("Tested **1,800 / 2,000** executable lines", callout)
+ self.assertIn("and **45 / 50** boolean conditions", callout)
+ self.assertIn("across **20** decisions.", callout)
+ self.assertIn("Artifacts", callout)
+
+ def test_format_global_summary_table_without_mcdc(self):
+ """Global table without MC/DC must display lines and functions only."""
+ metrics = DirectoryCoverageMetrics(
+ name="global",
+ lines_tot=500,
+ lines_cov=400,
+ func_tot=50,
+ func_cov=40,
+ )
+ summary = FullCoverageSummary(global_stats=metrics)
+ table = format_global_summary_table(summary)
+
+ self.assertIn("| **Executable Lines** | 400 | 500 | **80.00%** |", table)
+ self.assertIn("| **Functions** | 40 | 50 | **80.00%** |", table)
+ self.assertNotIn("MC/DC", table)
+
+ def test_format_global_summary_table_with_mcdc(self):
+ """Global table with MC/DC must include condition independence and decision verification."""
+ metrics = DirectoryCoverageMetrics(
+ name="global",
+ lines_tot=1000,
+ lines_cov=900,
+ func_tot=100,
+ func_cov=95,
+ mcdc_tot=80,
+ mcdc_cov=60,
+ decisions_tot=30,
+ decisions_full=25,
+ )
+ summary = FullCoverageSummary(global_stats=metrics)
+ table = format_global_summary_table(summary)
+
+ self.assertIn(
+ "| **MC/DC Condition Independence** | 60 | 80 | **75.00%** |", table
+ )
+ self.assertIn("| **Fully Verified Decisions** | 25 | 30 | **83.33%** |", table)
+
+ def test_format_directory_breakdown_table_alphabetical_sorting(self):
+ """Directory breakdown table must sort directory names alphabetically."""
+ dir_string = DirectoryCoverageMetrics(
+ name="src/string", lines_tot=100, lines_cov=100
+ )
+ dir_math = DirectoryCoverageMetrics(
+ name="src/math", lines_tot=100, lines_cov=80
+ )
+ dir_ctype = DirectoryCoverageMetrics(
+ name="src/ctype", lines_tot=100, lines_cov=90
+ )
+
+ summary = FullCoverageSummary(
+ global_stats=DirectoryCoverageMetrics(lines_tot=300, lines_cov=270),
+ directories={
+ "src/string": dir_string,
+ "src/math": dir_math,
+ "src/ctype": dir_ctype,
+ },
+ )
+ table = format_directory_breakdown_table(summary)
+
+ # Check alphabetical order in markdown output
+ idx_ctype = table.find("`libc/src/ctype`")
+ idx_math = table.find("`libc/src/math`")
+ idx_string = table.find("`libc/src/string`")
+
+ self.assertTrue(0 <= idx_ctype < idx_math < idx_string)
+
+ def test_format_directory_breakdown_table_without_mcdc(self):
+ """Directory table without MC/DC must show line and function coverage columns."""
+ dir_math = DirectoryCoverageMetrics(
+ name="src/math", lines_tot=100, lines_cov=80, func_tot=2, func_cov=2
+ )
+ summary = FullCoverageSummary(
+ global_stats=DirectoryCoverageMetrics(lines_tot=100, lines_cov=80),
+ directories={"src/math": dir_math},
+ )
+ table = format_directory_breakdown_table(summary)
+ self.assertNotIn("MC/DC Conditions", table)
+ self.assertIn("`libc/src/math` | **80.00%** | 100.00% | 100 | 20 |", table)
+
+ def test_format_directory_breakdown_table_with_mcdc(self):
+ """Directory table with MC/DC must show MC/DC condition columns."""
+ dir_math = DirectoryCoverageMetrics(
+ name="src/math",
+ lines_tot=100,
+ lines_cov=80,
+ mcdc_tot=10,
+ mcdc_cov=8,
+ decisions_tot=4,
+ decisions_full=3,
+ )
+ summary = FullCoverageSummary(
+ global_stats=DirectoryCoverageMetrics(
+ lines_tot=100, lines_cov=80, mcdc_tot=10, mcdc_cov=8
+ ),
+ directories={"src/math": dir_math},
+ )
+ table = format_directory_breakdown_table(summary)
+
+ self.assertIn("MC/DC Conditions", table)
+ self.assertIn("**80.0%** (8/10)", table)
+
+ def test_format_directory_breakdown_table_mixed_mcdc(self):
+ """Directories without MC/DC records in an MC/DC run must display N/A."""
+ dir_math = DirectoryCoverageMetrics(
+ name="src/math",
+ lines_tot=100,
+ lines_cov=80,
+ mcdc_tot=10,
+ mcdc_cov=8,
+ decisions_tot=4,
+ decisions_full=3,
+ )
+ dir_ctype = DirectoryCoverageMetrics(
+ name="src/ctype",
+ lines_tot=50,
+ lines_cov=50,
+ mcdc_tot=0,
+ mcdc_cov=0,
+ decisions_tot=0,
+ decisions_full=0,
+ )
+ summary = FullCoverageSummary(
+ global_stats=DirectoryCoverageMetrics(
+ lines_tot=150, lines_cov=130, mcdc_tot=10, mcdc_cov=8
+ ),
+ directories={"src/math": dir_math, "src/ctype": dir_ctype},
+ )
+ table = format_directory_breakdown_table(summary)
+ self.assertIn("`libc/src/math` | **80.0%** (8/10) | 3 / 4", table)
+ self.assertIn("`libc/src/ctype` | N/A | N/A", table)
+
+
+class TestRenderFullReportEndToEnd(unittest.TestCase):
+ """Tests full Markdown report composition from JSON payload to stdout."""
+
+ def test_render_empty_payload_fallback(self):
+ """Empty payload must output fallback message without crashing."""
+ buf = io.StringIO()
+ with redirect_stdout(buf):
+ render_full_report({})
+ output = buf.getvalue()
+ self.assertIn("## LLVM-libc Full Codebase Coverage Report", output)
+ self.assertIn("### No Coverage Data Detected", output)
+ self.assertIn(
+ "The test execution completed but no coverage profiles were exported.",
+ output,
+ )
+
+ def test_render_complete_report_without_mcdc(self):
+ """Valid report without MC/DC must render line coverage and summary tables."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/src/math/sin.cpp",
+ "summary": {
+ "lines": {"count": 100, "covered": 90},
+ "functions": {"count": 2, "covered": 2},
+ },
+ }
+ ]
+ }
+ ]
+ }
+ buf = io.StringIO()
+ with redirect_stdout(buf):
+ render_full_report(payload)
+ output = buf.getvalue()
+
+ self.assertIn("## LLVM-libc Full Codebase Coverage Report", output)
+ self.assertIn("### Overall Codebase Coverage: **90.00%**", output)
+ self.assertIn("`libc/src/math`", output)
+ self.assertNotIn("MC/DC", output)
+
+ def test_render_complete_report_with_mcdc(self):
+ """Valid report with MC/DC must render full callouts, global table, and directory table."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/src/math/sin.cpp",
+ "summary": {
+ "lines": {"count": 100, "covered": 90},
+ "functions": {"count": 2, "covered": 2},
+ "mcdc": {"count": 6, "covered": 6},
+ },
+ "mcdc_records": [
+ [10, 5, 10, 20, 0, 0, 0, 0, 0, [True, True]]
+ ],
+ }
+ ]
+ }
+ ]
+ }
+
+ buf = io.StringIO()
+ with redirect_stdout(buf):
+ render_full_report(payload)
+ output = buf.getvalue()
+
+ self.assertIn("## LLVM-libc Full Codebase Coverage Report", output)
+ self.assertIn("### Overall Codebase Coverage:", output)
+ self.assertIn("### Overall", output)
+ self.assertIn("### Coverage Breakdown", output)
+ self.assertIn("`libc/src/math`", output)
+
+
+class TestCommandLineInterface(unittest.TestCase):
+ """Tests CLI invocation, arguments parsing, and file handling."""
+
+ def test_cli_execution_with_file(self):
+ """CLI must read JSON coverage file from disk and write report to stdout."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/src/math/sin.cpp",
+ "summary": {
+ "lines": {"count": 50, "covered": 40},
+ "functions": {"count": 1, "covered": 1},
+ },
+ }
+ ]
+ }
+ ]
+ }
+
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".json", delete=False
+ ) as tmp_file:
+ json.dump(payload, tmp_file)
+ tmp_path = tmp_file.name
+
+ try:
+ buf = io.StringIO()
+ with patch.object(
+ sys,
+ "argv",
+ ["codebase_coverage.py", tmp_path, "aabbccdd1122", "main"],
+ ):
+ with redirect_stdout(buf):
+ main()
+ output = buf.getvalue()
+ self.assertIn("## LLVM-libc Full Codebase Coverage Report", output)
+ self.assertIn("`libc/src/math`", output)
+ finally:
+ if os.path.exists(tmp_path):
+ os.remove(tmp_path)
+
+ def test_cli_execution_with_minimal_arguments(self):
+ """CLI must execute successfully when commit SHA and branch ref are omitted."""
+ payload = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/libc/src/ctype/isalnum.cpp",
+ "summary": {
+ "lines": {"count": 20, "covered": 20},
+ "functions": {"count": 1, "covered": 1},
+ },
+ }
+ ]
+ }
+ ]
+ }
+
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".json", delete=False
+ ) as tmp_file:
+ json.dump(payload, tmp_file)
+ tmp_path = tmp_file.name
+
+ try:
+ buf = io.StringIO()
+ with patch.object(sys, "argv", ["codebase_coverage.py", tmp_path]):
+ with redirect_stdout(buf):
+ main()
+ output = buf.getvalue()
+ self.assertIn("## LLVM-libc Full Codebase Coverage Report", output)
+ self.assertIn("`libc/src/ctype`", output)
+ finally:
+ if os.path.exists(tmp_path):
+ os.remove(tmp_path)
+
+ def test_cli_nonexistent_file_exits_with_error(self):
+ """CLI must exit with code 1 when targeted file does not exist."""
+ stderr_buf = io.StringIO()
+ with patch.object(
+ sys,
+ "argv",
+ ["codebase_coverage.py", "/nonexistent/path/coverage.json"],
+ ):
+ with patch("sys.stderr", stderr_buf):
+ with self.assertRaises(SystemExit) as cm:
+ main()
+ self.assertEqual(cm.exception.code, 1)
+ self.assertIn("Error: Failed to parse coverage JSON", stderr_buf.getvalue())
+
+ def test_cli_invalid_json_exits_with_error(self):
+ """CLI must exit with code 1 when targeted file contains invalid JSON syntax."""
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".json", delete=False
+ ) as tmp_file:
+ tmp_file.write("INVALID JSON CONTENT")
+ tmp_path = tmp_file.name
+
+ stderr_buf = io.StringIO()
+ try:
+ with patch.object(sys, "argv", ["codebase_coverage.py", tmp_path]):
+ with patch("sys.stderr", stderr_buf):
+ with self.assertRaises(SystemExit) as cm:
+ main()
+ self.assertEqual(cm.exception.code, 1)
+ self.assertIn("Error: Failed to parse coverage JSON", stderr_buf.getvalue())
+ finally:
+ if os.path.exists(tmp_path):
+ os.remove(tmp_path)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/libc/utils/coverage/test_diff_coverage.py b/libc/utils/coverage/test_diff_coverage.py
new file mode 100644
index 0000000000000..e369a6a5f7004
--- /dev/null
+++ b/libc/utils/coverage/test_diff_coverage.py
@@ -0,0 +1,624 @@
+# ====- Unit tests for diff_coverage.py ----------------------*- python -*--==#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+# ==-------------------------------------------------------------------------==#
+
+"""Unit tests for diff_coverage.py."""
+
+import io
+import json
+import os
+import sys
+import tempfile
+import unittest
+from contextlib import redirect_stdout
+from unittest.mock import patch
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from diff_coverage import (
+ DEFAULT_BASE_REPOSITORY,
+ DEFAULT_HEAD_REPOSITORY,
+ CoverageJSONParser,
+ DiffHunk,
+ DiffParser,
+ FilePatchMetrics,
+ PatchCoverageSummary,
+ calculate_patch_statistics,
+ format_annotated_diff,
+ format_breakdown_table,
+ format_line_ranges,
+ format_metadata_section,
+ format_status_banner,
+ is_executable_line,
+ main,
+ render_patch_report,
+)
+
+
+def _make_cov_json(filename, segments, mcdc_records=None):
+ """Constructs a minimal llvm-cov JSON export payload."""
+ return {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": filename,
+ "segments": segments,
+ "mcdc_records": mcdc_records or [],
+ }
+ ]
+ }
+ ]
+ }
+
+
+class TestCoverageDataStructures(unittest.TestCase):
+ """Tests FilePatchMetrics and PatchCoverageSummary ratio logic and zero-division guards."""
+
+ def test_file_patch_metrics_calculations(self):
+ """FilePatchMetrics must correctly compute line and MC/DC ratios or 0.0 on zero totals."""
+ empty = FilePatchMetrics(file_path="src/math/sin.cpp")
+ self.assertEqual(empty.total_lines, 0)
+ self.assertEqual(empty.line_coverage_percentage, 0.0)
+ self.assertEqual(empty.mcdc_coverage_percentage, 0.0)
+
+ metrics = FilePatchMetrics(
+ file_path="src/math/sin.cpp",
+ covered_lines={10, 11, 12},
+ missed_lines={13},
+ mcdc_covered_conditions=3,
+ mcdc_total_conditions=4,
+ )
+ self.assertEqual(metrics.total_lines, 4)
+ self.assertAlmostEqual(metrics.line_coverage_percentage, 75.0, places=2)
+ self.assertAlmostEqual(metrics.mcdc_coverage_percentage, 75.0, places=2)
+
+ def test_patch_coverage_summary_aggregation(self):
+ """PatchCoverageSummary must correctly aggregate metrics and condition indicators."""
+ empty = PatchCoverageSummary()
+ self.assertEqual(empty.total_lines, 0)
+ self.assertEqual(empty.line_coverage_percentage, 0.0)
+ self.assertEqual(empty.mcdc_coverage_percentage, 0.0)
+ self.assertFalse(empty.has_mcdc)
+
+ summary = PatchCoverageSummary(
+ total_covered_lines=6,
+ total_missed_lines=2,
+ total_mcdc_covered_conditions=1,
+ total_mcdc_total_conditions=2,
+ )
+ self.assertEqual(summary.total_lines, 8)
+ self.assertAlmostEqual(summary.line_coverage_percentage, 75.0, places=2)
+ self.assertAlmostEqual(summary.mcdc_coverage_percentage, 50.0, places=2)
+ self.assertTrue(summary.has_mcdc)
+
+
+class TestExecutableLineFiltering(unittest.TestCase):
+ """Tests statement heuristics distinguishing executable C++ statements from non-code."""
+
+ def test_statement_heuristics(self):
+ """Verifies statement classification across distinct C/C++ syntactic forms."""
+ test_cases = [
+ ("int x = 42;", True, "Variable assignment"),
+ ("x += y;", True, "Compound arithmetic assignment"),
+ ("return result;", True, "Return statement"),
+ ("if (x > 0) {", True, "Branch condition header"),
+ ("for (size_t i = 0; i < count; ++i) {", True, "For loop header"),
+ ("do_work(a, b);", True, "Function call"),
+ ("struct Point p = {1, 2};", True, "Struct variable assignment"),
+ ("int x = 42; // assignment", True, "Statement with line comment"),
+ ("int x = 42; /* inline comment */", True, "Statement with block comment"),
+ ("// Single line comment", False, "Line comment"),
+ ("/* Block comment start", False, "Block comment start"),
+ (" * Continuation line", False, "Block comment middle"),
+ (" */", False, "Block comment end"),
+ (
+ "} // namespace LIBC_NAMESPACE_DECL",
+ False,
+ "Brace with namespace comment",
+ ),
+ ("}; // struct Point", False, "Scope end with comment"),
+ ("{ // begin loop", False, "Opening brace with comment"),
+ ("} /* namespace */", False, "Brace with block comment"),
+ ("public:", False, "Access specifier"),
+ ("private: // methods", False, "Access specifier with comment"),
+ (
+ 'static_assert(sizeof(long) == 8, "msg");',
+ False,
+ "Compile-time assertion",
+ ),
+ ("friend class Peer;", False, "Friend declaration"),
+ ("{", False, "Opening brace"),
+ ("}", False, "Closing brace"),
+ ("};", False, "Scope terminator"),
+ (": value_(0) {", False, "Constructor initializer header"),
+ ("#include ", False, "Preprocessor include"),
+ ("namespace LIBC_NAMESPACE {", False, "Namespace definition"),
+ ("using size_t = unsigned long;", False, "Type alias"),
+ ("struct ListNode;", False, "Forward struct declaration"),
+ ("enum class Status : uint8_t {", False, "Enum definition header"),
+ ("", False, "Empty line"),
+ (" ", False, "Whitespace line"),
+ ]
+ for line, expected, desc in test_cases:
+ with self.subTest(msg=desc, line=line):
+ self.assertEqual(is_executable_line(line), expected)
+
+
+class TestFormatLineRanges(unittest.TestCase):
+ """Tests line number set formatting into concise span representations."""
+
+ def test_formatting_spans(self):
+ """Line number sets must format as empty, single, contiguous, or disjoint spans."""
+ self.assertEqual(format_line_ranges(set()), "None")
+ self.assertEqual(format_line_ranges({42}), "`L42`")
+ self.assertEqual(format_line_ranges({10, 11, 12}), "`L10-L12`")
+ disjoint = {1, 2, 5, 8, 9, 100}
+ self.assertEqual(format_line_ranges(disjoint), "`L1-L2`, `L5`, `L8-L9`, `L100`")
+
+
+class TestDiffParser(unittest.TestCase):
+ """Tests Unified Diff parsing across single/multiple hunks, creations, and deletions."""
+
+ def test_parse_diff_hunks(self):
+ """DiffParser must extract added and context lines across hunks while ignoring deletions."""
+ diff_text = (
+ "diff --git a/src/math/sin.cpp b/src/math/sin.cpp\n"
+ "--- a/src/math/sin.cpp\n"
+ "+++ b/src/math/sin.cpp\n"
+ "@@ -10,3 +10,4 @@\n"
+ " ctx1();\n"
+ "-deleted();\n"
+ "+added1();\n"
+ "+added2();\n"
+ "@@ -50,1 +51,2 @@\n"
+ " ctx2();\n"
+ "+added3();\n"
+ )
+ parsed = DiffParser.parse(diff_text)
+ self.assertIn("src/math/sin.cpp", parsed)
+ hunks = parsed["src/math/sin.cpp"]
+ self.assertEqual(len(hunks), 2)
+ added_hunk1 = [line for line in hunks[0].lines if line[0] == "+"]
+ added_hunk2 = [line for line in hunks[1].lines if line[0] == "+"]
+ self.assertEqual(added_hunk1, [("+", "added1();", 11), ("+", "added2();", 12)])
+ self.assertEqual(added_hunk2, [("+", "added3();", 52)])
+
+ def test_parse_special_files(self):
+ """Newly created files must start at line 1, deleted files and headers must be skipped."""
+ diff_text = (
+ "diff --git a/src/math/new.cpp b/src/math/new.cpp\n"
+ "new file mode 100644\n"
+ "--- /dev/null\n"
+ "+++ b/src/math/new.cpp\n"
+ "index 0000..1111\n"
+ "@@ -0,0 +1,1 @@\n"
+ "+int new_func();\n"
+ "diff --git a/src/math/old.cpp b/src/math/old.cpp\n"
+ "--- a/src/math/old.cpp\n"
+ "+++ /dev/null\n"
+ "@@ -1,1 +0,0 @@\n"
+ "-deleted();\n"
+ )
+ parsed = DiffParser.parse(diff_text)
+ self.assertIn("src/math/new.cpp", parsed)
+ self.assertNotIn("src/math/old.cpp", parsed)
+ self.assertEqual(
+ parsed["src/math/new.cpp"][0].lines[0], ("+", "int new_func();", 1)
+ )
+ self.assertEqual(DiffParser.parse(""), {})
+
+ def test_parse_from_disk_file(self):
+ """DiffParser must successfully read and parse diff files from disk."""
+ diff_text = "diff --git a/a b/b\n+++ b/src/math/f.cpp\n@@ -1,1 +1,2 @@\n ctx();\n+line();\n"
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".diff", delete=False) as tmp:
+ tmp.write(diff_text)
+ tmp_path = tmp.name
+ try:
+ parsed = DiffParser.parse(tmp_path)
+ self.assertIn("src/math/f.cpp", parsed)
+ finally:
+ if os.path.exists(tmp_path):
+ os.remove(tmp_path)
+
+
+class TestCoverageJSONParser(unittest.TestCase):
+ """Tests JSON parsing, segment expansion, MC/DC extraction, and loader safeguards."""
+
+ def test_segment_expansion_and_path_normalization(self):
+ """Segments spanning multiple lines must mark each line covered, uncounted must skip."""
+ diff_files = {"libc/src/math/sin.cpp": [], "src/math/cos.cpp": []}
+ json_data = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/runner/work/llvm-project/libc/src/math/sin.cpp",
+ "segments": [
+ [10, 0, 5, 1, 1],
+ [13, 0, 0, 1, 1],
+ [20, 0, 0, 0, 1],
+ ],
+ "mcdc_records": [],
+ },
+ {
+ # Tests target_path.endswith("/" + file_name)
+ "filename": "cos.cpp",
+ "segments": [[5, 0, 1, 1, 1]],
+ "mcdc_records": [],
+ },
+ {
+ # Unmatched file: tests continue branch
+ "filename": "/runner/work/llvm-project/libc/src/math/other.cpp",
+ "segments": [[1, 0, 1, 1, 1]],
+ "mcdc_records": [],
+ },
+ ]
+ }
+ ]
+ }
+ matrix = CoverageJSONParser.extract_patch_matrix(json_data, diff_files)
+ covered = matrix["libc/src/math/sin.cpp"]["covered"]
+ missed = matrix["libc/src/math/sin.cpp"]["missed"]
+ self.assertEqual(covered, {10, 11, 12})
+ self.assertIn(13, missed)
+ self.assertIn(19, missed)
+ self.assertNotIn(20, missed)
+
+ def test_mcdc_records_extraction(self):
+ """Valid MC/DC records must be extracted; truncated or empty records must be ignored."""
+ diff_files = {"src/math/sin.cpp": []}
+ json_data = _make_cov_json(
+ filename="/workspace/src/math/sin.cpp",
+ segments=[[10, 0, 1, 1, 1]],
+ mcdc_records=[
+ [10, 4, 10, 14, 0, 0, 0, 0, 0, [True, False]], # Valid
+ [11, 4, 11, 14], # Truncated (< 10)
+ [12, 4, 12, 14, 0, 0, 0, 0, 0, []], # Empty condition vector
+ ],
+ )
+ matrix = CoverageJSONParser.extract_patch_matrix(json_data, diff_files)
+ decisions = matrix["src/math/sin.cpp"]["mcdc_decisions"]
+ self.assertEqual(len(decisions), 1)
+ self.assertEqual(decisions[0]["line_start"], 10)
+ self.assertEqual(decisions[0]["covered"], 1)
+ self.assertEqual(decisions[0]["total"], 2)
+
+ def test_load_and_empty_payload_safeguards(self):
+ """CoverageJSONParser must load valid JSON, fallback on empty data, and exit on error."""
+ diff_files = {"src/math/sin.cpp": []}
+ empty_matrix = CoverageJSONParser.extract_patch_matrix({}, diff_files)
+ self.assertEqual(len(empty_matrix["src/math/sin.cpp"]["covered"]), 0)
+
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
+ json.dump({"key": "val"}, tmp)
+ tmp_path = tmp.name
+ try:
+ self.assertEqual(CoverageJSONParser.load(tmp_path), {"key": "val"})
+ finally:
+ if os.path.exists(tmp_path):
+ os.remove(tmp_path)
+
+ stderr_buf = io.StringIO()
+ with patch("sys.stderr", stderr_buf):
+ with self.assertRaises(SystemExit):
+ CoverageJSONParser.load("/nonexistent/cov.json")
+ self.assertIn("Error: Failed to parse coverage JSON", stderr_buf.getvalue())
+
+
+class TestCalculatePatchStatistics(unittest.TestCase):
+ """Tests correlating patch lines against coverage segments and MC/DC truth tables."""
+
+ def test_calculate_patch_statistics(self):
+ """Covered lines take precedence, uninstrumented files miss, and MC/DC diagnoses unverified."""
+ diff_text = (
+ "diff --git a/src/math/sin.cpp b/src/math/sin.cpp\n"
+ "--- a/src/math/sin.cpp\n"
+ "+++ b/src/math/sin.cpp\n"
+ "@@ -10,2 +10,3 @@\n"
+ " ctx();\n"
+ "+int covered_and_missed = 1;\n"
+ "+if (a && b) return 1;\n"
+ "diff --git a/src/math/untested.cpp b/src/math/untested.cpp\n"
+ "--- a/src/math/untested.cpp\n"
+ "+++ b/src/math/untested.cpp\n"
+ "@@ -1,1 +1,2 @@\n"
+ " ctx();\n"
+ "+int untested = 1;\n"
+ )
+ diff_files = DiffParser.parse(diff_text)
+ coverage_matrix = {
+ "src/math/sin.cpp": {
+ "covered": {11, 12},
+ "missed": {11}, # Covered takes precedence
+ "mcdc_decisions": [
+ {
+ "line_start": 12,
+ "line_end": 12,
+ "conditions": [True, False],
+ "covered": 1,
+ "total": 2,
+ }
+ ],
+ },
+ "src/math/untested.cpp": {
+ "covered": set(),
+ "missed": set(),
+ "mcdc_decisions": [],
+ },
+ }
+ summary = calculate_patch_statistics(diff_files, coverage_matrix)
+ self.assertEqual(summary.total_lines, 3)
+ self.assertEqual(summary.total_covered_lines, 2)
+ self.assertEqual(summary.total_missed_lines, 1)
+ self.assertEqual(summary.total_mcdc_covered_conditions, 1)
+ self.assertEqual(summary.total_mcdc_total_conditions, 2)
+
+ sin_metric = summary.files["src/math/sin.cpp"]
+ self.assertIn(
+ "1/2 verified (C2 unverified)", sin_metric.condition_diagnostics[0]
+ )
+ self.assertEqual(sin_metric.unverified_decision_lines[12], ["C2"])
+
+ def test_non_source_and_comment_files_skipped(self):
+ """Test files, documentation, and files with only comment additions must be skipped."""
+ diff_text = (
+ "diff --git a/libc/test/src/math/sin_test.cpp b/libc/test/src/math/sin_test.cpp\n"
+ "+++ b/libc/test/src/math/sin_test.cpp\n"
+ "@@ -1,1 +1,2 @@\n"
+ "+TEST(Foo, Bar) {}\n"
+ "diff --git a/src/math/comment_only.cpp b/src/math/comment_only.cpp\n"
+ "+++ b/src/math/comment_only.cpp\n"
+ "@@ -1,1 +1,2 @@\n"
+ "+// comment only\n"
+ )
+ diff_files = DiffParser.parse(diff_text)
+ coverage_matrix = {
+ "libc/test/src/math/sin_test.cpp": {
+ "covered": set(),
+ "missed": set(),
+ "mcdc_decisions": [],
+ },
+ "src/math/comment_only.cpp": {
+ "covered": set(),
+ "missed": set(),
+ "mcdc_decisions": [],
+ },
+ }
+ summary = calculate_patch_statistics(diff_files, coverage_matrix)
+ self.assertEqual(summary.total_lines, 0)
+ self.assertEqual(len(summary.files), 0)
+
+
+class TestPatchReportFormatting(unittest.TestCase):
+ """Tests Markdown formatting across status banners, metadata, tables, and annotated diffs."""
+
+ def test_format_status_banner_variants(self):
+ """Verifies phrasing across all 5 status banner operational conditions."""
+ cases = [
+ (
+ PatchCoverageSummary(total_covered_lines=5, total_missed_lines=0),
+ "All **5** newly added",
+ ),
+ (
+ PatchCoverageSummary(
+ total_covered_lines=5,
+ total_missed_lines=0,
+ total_mcdc_covered_conditions=2,
+ total_mcdc_total_conditions=2,
+ total_decisions_count=1,
+ fully_verified_decisions=1,
+ ),
+ "All **5** executable lines and **2** boolean conditions",
+ ),
+ (
+ PatchCoverageSummary(
+ total_covered_lines=5,
+ total_missed_lines=0,
+ total_mcdc_covered_conditions=1,
+ total_mcdc_total_conditions=2,
+ total_decisions_count=1,
+ fully_verified_decisions=0,
+ ),
+ "Executed **5 / 5** lines. **1 / 2** boolean conditions",
+ ),
+ (
+ PatchCoverageSummary(total_covered_lines=4, total_missed_lines=1),
+ "Executed **4 / 5** lines (**1** unexecuted",
+ ),
+ (
+ PatchCoverageSummary(
+ total_covered_lines=4,
+ total_missed_lines=1,
+ total_mcdc_covered_conditions=1,
+ total_mcdc_total_conditions=2,
+ total_decisions_count=1,
+ fully_verified_decisions=0,
+ ),
+ "(**1** unexecuted lines detected in patch).",
+ ),
+ ]
+ for summary, expected in cases:
+ with self.subTest(expected=expected):
+ self.assertIn(expected, format_status_banner(summary))
+
+ def test_format_metadata_section(self):
+ """Metadata section must format commits, tests, or return empty on missing arguments."""
+ metadata = format_metadata_section(
+ "1111111", "2222222", "main", "patch", "test_target"
+ )
+ self.assertIn("Base Branch", metadata)
+ self.assertIn("`test_target`", metadata)
+ self.assertEqual(format_metadata_section(None, None, None, None), "")
+
+ def test_format_breakdown_table(self):
+ """Breakdown tables must render line/MCDC stats and normalize paths to libc/ on GitHub."""
+ file_mcdc = FilePatchMetrics(
+ file_path="src/math/sin.cpp",
+ covered_lines={10},
+ missed_lines=set(),
+ added_lines={10},
+ mcdc_covered_conditions=2,
+ mcdc_total_conditions=2,
+ decisions_verified=1,
+ decisions_total=1,
+ condition_diagnostics=["`L10`: 2/2 verified"],
+ )
+ file_no_mcdc = FilePatchMetrics(
+ file_path="src/string/strlen.cpp",
+ covered_lines={20},
+ missed_lines={21},
+ added_lines={20, 21},
+ )
+ summary = PatchCoverageSummary(
+ total_covered_lines=2,
+ total_missed_lines=1,
+ total_mcdc_covered_conditions=2,
+ total_mcdc_total_conditions=2,
+ fully_verified_decisions=1,
+ total_decisions_count=1,
+ files={
+ "src/math/sin.cpp": file_mcdc,
+ "src/string/strlen.cpp": file_no_mcdc,
+ },
+ )
+ table = format_breakdown_table(summary, head_commit_sha="abcd123")
+ self.assertIn("blob/abcd123/libc/src/math/sin.cpp", table)
+ self.assertIn("MC/DC Conditions", table)
+ self.assertIn("N/A | N/A", table) # strlen has no MC/DC
+
+ def test_format_annotated_diff(self):
+ """Annotated diff must output covered, missed, partial MC/DC, non-executable, and context lines."""
+ diff_text = (
+ "diff --git a/src/math/sin.cpp b/src/math/sin.cpp\n"
+ "+++ b/src/math/sin.cpp\n"
+ "@@ -10,4 +10,5 @@\n"
+ " ctx();\n"
+ "+covered();\n"
+ "+missed();\n"
+ "+if (a && b) {}\n"
+ "+{\n"
+ )
+ diff_files = DiffParser.parse(diff_text)
+ file_metrics = FilePatchMetrics(
+ file_path="src/math/sin.cpp",
+ covered_lines={11, 13},
+ missed_lines={12},
+ unverified_decision_lines={13: ["C2"]},
+ )
+ summary = PatchCoverageSummary(files={"src/math/sin.cpp": file_metrics})
+ annotated = format_annotated_diff(summary, diff_files)
+ self.assertIn(" ctx();", annotated)
+ self.assertIn("+ covered();", annotated)
+ self.assertIn("- missed(); // [MISSED]", annotated)
+ self.assertIn("! if (a && b) {} // [PARTIAL MC/DC: C2 unverified]", annotated)
+ self.assertIn(" {", annotated)
+
+
+class TestRenderPatchReportEndToEnd(unittest.TestCase):
+ """Tests full Markdown report composition from inputs to stdout."""
+
+ def test_render_empty_diff(self):
+ """Empty diff must render coverage notice without failing."""
+ buf = io.StringIO()
+ with redirect_stdout(buf):
+ render_patch_report({}, {}, "1111", "2222", "main", "feature")
+ self.assertIn(
+ "No executable lines were added or modified in this patch.", buf.getvalue()
+ )
+
+ def test_render_full_report_with_mcdc(self):
+ """Patch report with MC/DC must display the MC/DC report title and full tables."""
+ diff_text = "diff --git a/src/math/f.cpp b/src/math/f.cpp\n+++ b/src/math/f.cpp\n@@ -1,1 +1,2 @@\n ctx();\n+return a && b;\n"
+ diff_files = DiffParser.parse(diff_text)
+ coverage_matrix = {
+ "src/math/f.cpp": {
+ "covered": {2},
+ "missed": set(),
+ "mcdc_decisions": [
+ {
+ "line_start": 2,
+ "line_end": 2,
+ "conditions": [True, True],
+ "covered": 2,
+ "total": 2,
+ }
+ ],
+ }
+ }
+ buf = io.StringIO()
+ with redirect_stdout(buf):
+ render_patch_report(
+ diff_files, coverage_matrix, "1111", "2222", "main", "feature"
+ )
+ output = buf.getvalue()
+ self.assertIn("## LLVM-libc MC/DC Patch Coverage Report", output)
+ self.assertIn(
+ "### Patch Coverage: **100.00% Line** | **100.00% MC/DC**", output
+ )
+ self.assertIn("View Annotated Patch Diff", output)
+
+
+class TestCommandLineInterfaceDiff(unittest.TestCase):
+ """Tests CLI execution and file validation safeguards."""
+
+ def test_cli_execution(self):
+ """CLI must read diff and JSON files from disk and print the report to stdout."""
+ diff_text = "diff --git a/src/math/s.cpp b/src/math/s.cpp\n+++ b/src/math/s.cpp\n@@ -1,1 +1,2 @@\n ctx();\n+int x = 1;\n"
+ json_data = _make_cov_json("/workspace/src/math/s.cpp", [[2, 0, 1, 1, 1]])
+
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".diff", delete=False
+ ) as f_diff:
+ f_diff.write(diff_text)
+ path_diff = f_diff.name
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".json", delete=False
+ ) as f_json:
+ json.dump(json_data, f_json)
+ path_json = f_json.name
+
+ try:
+ buf = io.StringIO()
+ with patch.object(
+ sys,
+ "argv",
+ [
+ "diff_coverage.py",
+ path_diff,
+ path_json,
+ "111",
+ "222",
+ "m",
+ "f",
+ "target",
+ ],
+ ):
+ with redirect_stdout(buf):
+ main()
+ self.assertIn("## LLVM-libc Patch Coverage Report", buf.getvalue())
+ self.assertIn("[`src/math/s.cpp`]", buf.getvalue())
+ finally:
+ if os.path.exists(path_diff):
+ os.remove(path_diff)
+ if os.path.exists(path_json):
+ os.remove(path_json)
+
+ def test_cli_missing_files_exit(self):
+ """CLI must exit with code 1 when diff file or JSON file is missing."""
+ stderr_buf = io.StringIO()
+ with patch.object(
+ sys, "argv", ["diff_coverage.py", "/missing.diff", "/missing.json"]
+ ):
+ with patch("sys.stderr", stderr_buf):
+ with self.assertRaises(SystemExit):
+ main()
+ self.assertIn("Error: Diff file not found", stderr_buf.getvalue())
+
+
+if __name__ == "__main__":
+ unittest.main()