[libc-commits] [libc] [llvm] [libc][ci] Add automated pre-commit and post-commit coverage bots (PR #215269)
Tapiwa Gonga via libc-commits
libc-commits at lists.llvm.org
Thu Aug 20 04:26:56 PDT 2026
https://github.com/tapiwagonga updated https://github.com/llvm/llvm-project/pull/215269
>From 08778902ac2fbe6a6f6279fe90b21c8fe5a90d93 Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Mon, 10 Aug 2026 11:13:29 +0000
Subject: [PATCH 1/6] [libc][ci] Add automated pre-commit and post-commit
coverage bots
---
.../workflows/libc-post-commit-coverage.yml | 134 +++++++
.../workflows/libc-pre-commit-coverage.yml | 362 ++++++++++++++++++
libc/utils/coverage/full_report.py | 138 +++++++
libc/utils/coverage/patch_report.py | 245 ++++++++++++
4 files changed, 879 insertions(+)
create mode 100644 .github/workflows/libc-post-commit-coverage.yml
create mode 100644 .github/workflows/libc-pre-commit-coverage.yml
create mode 100644 libc/utils/coverage/full_report.py
create mode 100644 libc/utils/coverage/patch_report.py
diff --git a/.github/workflows/libc-post-commit-coverage.yml b/.github/workflows/libc-post-commit-coverage.yml
new file mode 100644
index 0000000000000..6a7749b5dc181
--- /dev/null
+++ b/.github/workflows/libc-post-commit-coverage.yml
@@ -0,0 +1,134 @@
+name: libc post-commit code coverage
+
+permissions:
+ contents: write # Required to publish live HTML dashboard to gh-pages branch
+
+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 / post-commit-bot
+ push:
+ branches:
+ - main
+ - post-commit-bot
+ paths:
+ - 'libc/**'
+ - '.github/workflows/libc-post-commit-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 at sha256:8fee4c9ce5a1fd095686593cd36e032c48a31b6ae575378c82accd1d86a08d59
+ options: --privileged
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout at v4
+ with:
+ fetch-depth: 1
+ persist-credentials: false
+
+ - name: Setup Compiler Cache (sccache)
+ uses: hendrikmuhs/ccache-action at v1.2.23
+ with:
+ max-size: 1G
+ key: libc_coverage_unified_v2_x86_64
+ variant: sccache
+
+ - name: Configure CMake
+ run: |
+ echo "=== [STAGE 1/4] Starting CMake Configuration at $(date -u '+%Y-%m-%d %H:%M:%S UTC') ==="
+ START_TIME=$(date +%s)
+
+ 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
+ -DLLVM_LIBC_ENABLE_COVERAGE=ON
+ -DLIBC_TEST_UNIT_TEST_ONLY=ON
+ -DLIBC_TEST_SKIP_DEATH_TESTS=ON
+ -DLIBC_TEST_SKIP_SHARED_TESTS=ON
+ "
+ cmake $CMAKE_FLAGS
+
+ END_TIME=$(date +%s)
+ echo "=== [STAGE 1/4] CMake Configuration Completed in $((END_TIME - START_TIME))s ==="
+
+ - name: Run Full Codebase Unit Tests
+ run: |
+ echo "=== [STAGE 2/4] Running Full Unit Test Suite at $(date -u '+%Y-%m-%d %H:%M:%S UTC') ==="
+ START_TIME=$(date +%s)
+
+ export LLVM_PROFILE_FILE="libc_cov_%p.profraw"
+ ninja -C build-cov libc-unit-tests
+
+ END_TIME=$(date +%s)
+ echo "=== [STAGE 2/4] libc-unit-tests Completed in $((END_TIME - START_TIME))s ==="
+
+ - name: Merge Profiles
+ run: |
+ echo "=== [STAGE 3/4] Merging Raw Profiles at $(date -u '+%Y-%m-%d %H:%M:%S UTC') ==="
+ find build-cov -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."
+ llvm-profdata-23 merge -sparse --input-files=profraw_list.txt -o libc_full.profdata
+ echo "[LOG] Merged full codebase profile into libc_full.profdata."
+
+ - name: Generate Reports and Summary
+ env:
+ COMMIT_SHA: ${{ github.sha }}
+ BRANCH_REF: ${{ github.ref_name }}
+ run: |
+ echo "=== [STAGE 4/4] Generating Full Codebase Coverage Reports at $(date -u '+%Y-%m-%d %H:%M:%S UTC') ==="
+ EXECUTABLES=($(find build-cov -type f -executable -name "*__build__"))
+ OBJECTS=("${EXECUTABLES[@]:1}")
+ OBJECTS=("${OBJECTS[@]/#/-object=}")
+
+ echo "[LOG] Exporting coverage data across ${#EXECUTABLES[@]} test binaries."
+
+ # 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 \
+ -ignore-filename-regex=".*(test|utils).*"
+ touch coverage_html/.nojekyll
+
+ # 3. Run Full Coverage Analyzer
+ python3 libc/utils/coverage/full_report.py coverage.json "$COMMIT_SHA" "$BRANCH_REF" >> $GITHUB_STEP_SUMMARY
+ echo "[LOG] Post-commit summary report written to GITHUB_STEP_SUMMARY."
+
+ - name: Deploy Coverage Dashboard to GitHub Pages
+ uses: peaceiris/actions-gh-pages at v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./coverage_html
+ enable_jekyll: false
+ force_orphan: true
diff --git a/.github/workflows/libc-pre-commit-coverage.yml b/.github/workflows/libc-pre-commit-coverage.yml
new file mode 100644
index 0000000000000..ae4eb224cfc21
--- /dev/null
+++ b/.github/workflows/libc-pre-commit-coverage.yml
@@ -0,0 +1,362 @@
+name: libc pre-commit code coverage
+
+permissions:
+ contents: read
+ pull-requests: write # Required to post/update pre-commit coverage comments
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - pre-commit-bot
+ - main
+ paths:
+ - 'libc/src/**'
+ - 'libc/include/**'
+ - 'libc/test/**'
+ - 'libc/CMakeLists.txt'
+ - '.github/workflows/libc-pre-commit-coverage.yml'
+ - '!libc/docs/**'
+ - '!libc/benchmarks/**'
+ - '!libc/fuzzing/**'
+ - '!libc/utils/**'
+ - '!**.md'
+ pull_request:
+ paths:
+ - 'libc/src/**'
+ - 'libc/include/**'
+ - 'libc/test/**'
+ - 'libc/CMakeLists.txt'
+ - '.github/workflows/libc-pre-commit-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 at sha256:8fee4c9ce5a1fd095686593cd36e032c48a31b6ae575378c82accd1d86a08d59
+ options: >-
+ --privileged
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout at v4
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ fetch-depth: 2
+ persist-credentials: false
+
+ - name: Setup Compiler Cache (sccache)
+ uses: hendrikmuhs/ccache-action at v1.2.23
+ with:
+ max-size: 1G
+ key: libc_coverage_unified_v2_x86_64
+ variant: sccache
+
+ - name: Configure CMake
+ run: |
+ echo "=== [STAGE 1/4] Starting CMake Configuration at $(date -u '+%Y-%m-%d %H:%M:%S UTC') ==="
+ START_TIME=$(date +%s)
+
+ 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
+ -DLLVM_LIBC_ENABLE_COVERAGE=ON
+ -DLIBC_TEST_UNIT_TEST_ONLY=ON
+ -DLIBC_TEST_SKIP_DEATH_TESTS=ON
+ -DLIBC_TEST_SKIP_SHARED_TESTS=ON
+ "
+ cmake $CMAKE_FLAGS
+
+ END_TIME=$(date +%s)
+ echo "=== [STAGE 1/4] CMake Configuration Completed in $((END_TIME - START_TIME))s ==="
+
+ - 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: |
+ echo "=== [STAGE 2/4] Starting Targeted Test Discovery at $(date -u '+%Y-%m-%d %H:%M:%S UTC') ==="
+ START_TIME=$(date +%s)
+
+ # 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}"
+ git fetch upstream "$BASE_SHA" --depth=1 2>/dev/null || git fetch origin "$BASE_SHA" --depth=1 2>/dev/null || git fetch origin "$BASE_REF" --depth=1 2>/dev/null || true
+ 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
+
+ END_TIME=$(date +%s)
+ echo "=== [STAGE 2/4] Targeted Tests Completed in $((END_TIME - START_TIME))s ==="
+
+ - name: Merge Profiles
+ if: env.TARGETS != ''
+ run: |
+ echo "=== [STAGE 3/4] Merging Raw Profiles at $(date -u '+%Y-%m-%d %H:%M:%S UTC') ==="
+ find build-cov -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: |
+ echo "=== [STAGE 4/4] Generating Coverage Report at $(date -u '+%Y-%m-%d %H:%M:%S UTC') ==="
+ 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 ]; then
+ echo "[LOG] Notice: libc_full.profdata 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 Patch Report Python Script using persisted commit metadata
+ python3 libc/utils/coverage/patch_report.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: Post or Update Sticky PR Comment
+ if: always() && github.event_name == 'pull_request'
+ uses: actions/github-script at v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const fs = require('fs');
+ if (!fs.existsSync('coverage_report.md')) return;
+
+ const reportContent = fs.readFileSync('coverage_report.md', 'utf8');
+ const identifier = '<!-- LLVM-LIBC-PRE-COMMIT-COVERAGE-COMMENT -->';
+ const issue_number = context.payload.pull_request.number;
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+
+ try {
+ // Fetch existing PR comments
+ const comments = await github.rest.issues.listComments({
+ owner,
+ repo,
+ issue_number,
+ per_page: 100
+ });
+
+ const botComment = comments.data.find(c => c.body && c.body.includes(identifier));
+
+ // Parse metrics from reportContent for history table entry
+ const headSha = context.payload.pull_request.head.sha.substring(0, 7);
+ const dateStr = new Date().toISOString().replace('T', ' ').substring(0, 16);
+
+ const pctMatch = reportContent.match(/\|\s*\*\*Patch Line Coverage\*\*\s*\|\s*\*\*([0-9.]+)%\*\*\s*\|/);
+ const pct = pctMatch ? pctMatch[1] + '%' : '100.00%';
+
+ const lineMatch = reportContent.match(/\|\s*\*\*Executable Lines Evaluated\*\*\s*\|\s*\*\*([0-9]+)\*\*\s*\|/);
+ const totalLines = lineMatch ? lineMatch[1] : 'N/A';
+
+ const missMatch = reportContent.match(/\|\s*\*\*Unexecuted Lines\*\*\s*\|\s*\*\*([0-9]+)\*\*\s*\|/);
+ const missedLines = missMatch ? missMatch[1] : '0';
+
+ const newHistoryRow = `| [\`${headSha}\`](https://github.com/${owner}/${repo}/commit/${context.payload.pull_request.head.sha}) | ${dateStr} | ${totalLines} | **${pct}** | ${missedLines} |`;
+
+ let existingHistory = '';
+ if (botComment && botComment.body.includes('<!-- HISTORY_START -->')) {
+ const historyStart = botComment.body.indexOf('<!-- HISTORY_START -->') + '<!-- HISTORY_START -->'.length;
+ const historyEnd = botComment.body.indexOf('<!-- HISTORY_END -->');
+ if (historyStart !== -1 && historyEnd !== -1) {
+ existingHistory = botComment.body.substring(historyStart, historyEnd).trim();
+ }
+ }
+
+ let historyTable = existingHistory;
+ if (!historyTable) {
+ historyTable = `| Commit | Date (UTC) | Executable Lines | Patch Coverage | Missed Lines |\n| :---: | :---: | :---: | :---: | :---: |\n${newHistoryRow}`;
+ } else {
+ if (!historyTable.includes(headSha)) {
+ historyTable += `\n${newHistoryRow}`;
+ }
+ }
+
+ const fullBody = `${identifier}\n${reportContent}\n\n<details>\n<summary><b>View Coverage History for this PR</b></summary>\n\n<!-- HISTORY_START -->\n${historyTable}\n<!-- HISTORY_END -->\n\n</details>`;
+
+ if (botComment) {
+ await github.rest.issues.updateComment({
+ owner,
+ repo,
+ comment_id: botComment.id,
+ body: fullBody
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number,
+ body: fullBody
+ });
+ }
+ } catch (err) {
+ core.warning(`Could not post/update sticky PR comment: ${err.message}`);
+ }
diff --git a/libc/utils/coverage/full_report.py b/libc/utils/coverage/full_report.py
new file mode 100644
index 0000000000000..7c2f65551fb3b
--- /dev/null
+++ b/libc/utils/coverage/full_report.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+#
+# ====- Generate full 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
+#
+# ==-------------------------------------------------------------------------==#
+
+import argparse
+import os
+import sys
+from pathlib import Path
+from typing import Dict, List, Tuple
+
+# Ensure local module import works regardless of CWD
+sys.path.insert(0, str(Path(__file__).parent.resolve()))
+from parser import CoverageJSONParser
+
+
+def render_full_report(cov_data: dict) -> None:
+ if "data" not in cov_data or not cov_data["data"]:
+ print("## LLVM-libc Full Codebase Coverage Report\n")
+ print("> [!WARNING]")
+ print("> ### No Coverage Data Detected")
+ print("> The test execution completed but no coverage profiles were exported.")
+ return
+
+ subsystems: Dict[str, Dict[str, int]] = {}
+ file_stats: List[Tuple[str, int, int, int, int]] = []
+
+ total_lines_cov = 0
+ total_lines_tot = 0
+ total_func_cov = 0
+ total_func_tot = 0
+
+ for item in cov_data["data"][0].get("files", []):
+ fpath = item["filename"]
+ if "src/" not in fpath or "/test/" in fpath or "/utils/" in fpath:
+ continue
+
+ idx = fpath.find("src/")
+ if idx == -1:
+ continue
+ rel_path = fpath[idx:]
+
+ summary = item.get("summary", {})
+ lines_summary = summary.get("lines", {})
+ func_summary = summary.get("functions", {})
+
+ 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)
+
+ if line_tot == 0:
+ continue
+
+ total_lines_cov += line_cov
+ total_lines_tot += line_tot
+ total_func_cov += func_cov
+ total_func_tot += func_tot
+
+ parts = rel_path.split("/")
+ subsystem = "/".join(parts[:2]) if len(parts) >= 2 else parts[0]
+
+ if subsystem not in subsystems:
+ subsystems[subsystem] = {
+ "lines_cov": 0,
+ "lines_tot": 0,
+ "func_cov": 0,
+ "func_tot": 0,
+ }
+
+ subsystems[subsystem]["lines_cov"] += line_cov
+ subsystems[subsystem]["lines_tot"] += line_tot
+ subsystems[subsystem]["func_cov"] += func_cov
+ subsystems[subsystem]["func_tot"] += func_tot
+
+ file_stats.append((rel_path, line_cov, line_tot, func_cov, func_tot))
+
+ line_pct = (total_lines_cov / total_lines_tot * 100) if total_lines_tot > 0 else 0
+ func_pct = (total_func_cov / total_func_tot * 100) if total_func_tot > 0 else 0
+
+ repo = os.environ.get("GITHUB_REPOSITORY", "tapiwagonga/llvm-project")
+ if "/" in repo:
+ owner, repo_name = repo.split("/", 1)
+ pages_url = f"https://{owner}.github.io/{repo_name}/"
+ else:
+ pages_url = f"https://{repo}.github.io/"
+
+ print("## LLVM-libc Full Codebase Coverage Report\n")
+
+ print("> [!NOTE]")
+ print(f"> ### Overall Codebase Coverage: **{line_pct:.2f}%**")
+ print(
+ f"> Successfully tested **{total_lines_cov:,} / {total_lines_tot:,}** executable lines across all LLVM-libc subsystems."
+ )
+ print("")
+
+ print(f"- **Coverage Dashboard:** [{pages_url}]({pages_url})")
+ print("\n---\n")
+
+ print("### Codebase Health Metrics")
+ print("| Metric | Covered | Total | Coverage % |")
+ print("| :--- | :---: | :---: | :---: |")
+ print(
+ f"| **Executable Line Coverage** | {total_lines_cov:,} | {total_lines_tot:,} | **{line_pct:.2f}%** |"
+ )
+ print(
+ f"| **Function Coverage** | {total_func_cov:,} | {total_func_tot:,} | **{func_pct:.2f}%** |\n"
+ )
+
+ print("### Subsystem Coverage Breakdown")
+ print("| Subsystem | Line Coverage | Function Coverage | Executable Lines | Missed Lines |")
+ print("| :--- | :---: | :---: | :---: | :---: |")
+ for sub, data in sorted(subsystems.items()):
+ s_line_pct = (data["lines_cov"] / data["lines_tot"] * 100) if data["lines_tot"] > 0 else 0
+ s_func_pct = (data["func_cov"] / data["func_tot"] * 100) if data["func_tot"] > 0 else 0
+ missed = data["lines_tot"] - data["lines_cov"]
+ print(
+ f"| `libc/{sub}` | **{s_line_pct:.2f}%** | {s_func_pct:.2f}% | {data['lines_tot']:,} | {missed:,} |"
+ )
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="LLVM-libc Full Coverage Analyzer")
+ parser.add_argument("json_file", help="Path to llvm-cov export JSON file")
+
+ args, _ = parser.parse_known_args()
+
+ cov_data = CoverageJSONParser.load(args.json_file)
+ render_full_report(cov_data)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/libc/utils/coverage/patch_report.py b/libc/utils/coverage/patch_report.py
new file mode 100644
index 0000000000000..48f9891d4fe4b
--- /dev/null
+++ b/libc/utils/coverage/patch_report.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+#
+# ====- Generate patch 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
+#
+# ==-------------------------------------------------------------------------==#
+
+import argparse
+import os
+import sys
+from pathlib import Path
+from typing import Dict, List, Optional, Set
+
+# Ensure local module import works regardless of CWD
+sys.path.insert(0, str(Path(__file__).parent.resolve()))
+from parser import CoverageJSONParser, DiffHunk, DiffParser
+
+def is_executable_line(text: str) -> bool:
+ s = text.strip()
+ if not s:
+ return False
+ # Comments
+ if (
+ s.startswith("//")
+ or s.startswith("/*")
+ or s.startswith("*")
+ or s.startswith("*/")
+ ):
+ return False
+ # Structural braces and colons
+ if s in ("{", "}", "};", "{};") or s.startswith(":"):
+ return False
+ # Preprocessor directives
+ if s.startswith("#"):
+ return False
+ # Declarations / keywords / attributes
+ if (
+ s.startswith("namespace ")
+ or s.startswith("extern ")
+ or s.startswith("using ")
+ or s.startswith("__attribute__")
+ or s.startswith("template")
+ or s.startswith("typedef ")
+ or s.startswith("struct ")
+ or s.startswith("class ")
+ or s.startswith("enum ")
+ ):
+ return False
+ return True
+
+
+def format_line_ranges(lines: Set[int]) -> str:
+ if not lines:
+ return "None"
+ sorted_lines = sorted(lines)
+ ranges = []
+ start = sorted_lines[0]
+ end = sorted_lines[0]
+ for n in sorted_lines[1:]:
+ if n == end + 1:
+ end = n
+ else:
+ ranges.append(f"`L{start}-L{end}`" if start != end else f"`L{start}`")
+ start = end = n
+ ranges.append(f"`L{start}-L{end}`" if start != end else f"`L{start}`")
+ return ", ".join(ranges)
+
+
+def render_patch_report(
+ diff_files: Dict[str, List[DiffHunk]],
+ coverage_matrix: Dict[str, Dict[str, Set[int]]],
+ base_sha: Optional[str],
+ head_sha: Optional[str],
+ base_branch: Optional[str],
+ head_branch: Optional[str],
+ targets_str: Optional[str] = None,
+ base_repo: str = "llvm/llvm-project",
+ head_repo: str = "llvm/llvm-project",
+) -> None:
+ total_covered = 0
+ total_missed = 0
+ active_files = {}
+
+ for fpath, data in coverage_matrix.items():
+ added_lines: Set[int] = set()
+ for hunk in diff_files.get(fpath, []):
+ for l_type, text, l_num in hunk.lines:
+ if l_type == "+":
+ if not is_executable_line(text):
+ continue
+ added_lines.add(l_num)
+
+ if not added_lines:
+ continue
+
+ f_covered = added_lines.intersection(data["covered"])
+ f_missed = (added_lines.intersection(data["missed"])) - f_covered
+
+ if len(data["covered"]) > 0 or len(data["missed"]) > 0:
+ total_covered += len(f_covered)
+ total_missed += len(f_missed)
+ active_files[fpath] = (f_covered, f_missed, added_lines)
+ else:
+ total_missed += len(added_lines)
+ active_files[fpath] = (set(), added_lines, added_lines)
+
+ total_lines = total_covered + total_missed
+
+ if total_lines == 0 or not active_files:
+ print("## LLVM-libc Patch Coverage Report\n")
+ if base_sha and head_sha and base_branch and head_branch:
+ print(
+ f"- **Base Branch:** [`{base_branch}` ({base_sha[:7]})](https://github.com/{base_repo}/commit/{base_sha})"
+ )
+ print(
+ f"- **Head Commit:** [`{head_branch}` ({head_sha[:7]})](https://github.com/{head_repo}/commit/{head_sha})\n"
+ )
+ print("---\n")
+ print("> [!NOTE]")
+ print("> ### Coverage Validated")
+ print("> No `.cpp` source files in `libc/src/` were modified in this patch.")
+ sys.exit(0)
+
+ print("## LLVM-libc Patch Coverage Report\n")
+
+ coverage_percent = (total_covered / total_lines) * 100
+
+ # Modern GitHub UI Alert Card
+ if total_missed == 0:
+ print("> [!TIP]")
+ print(f"> ### Patch Coverage: **{coverage_percent:.2f}%** (PASSED)")
+ print(
+ f"> All **{total_lines}** newly added or modified executable lines are covered by targeted unit tests."
+ )
+ else:
+ print("> [!WARNING]")
+ print(f"> ### Patch Coverage: **{coverage_percent:.2f}%** ({total_missed} Missed Lines)")
+ print(
+ f"> **{total_missed}** unexecuted line(s) detected in your patch. Please review the missing lines below."
+ )
+ print("")
+
+ # Commit metadata and targets executed with exact upstream and fork repositories
+ if base_sha and head_sha and base_branch and head_branch:
+ print(
+ f"- **Base Branch:** [`{base_branch}` ({base_sha[:7]})](https://github.com/{base_repo}/commit/{base_sha})"
+ )
+ print(
+ f"- **Head Commit:** [`{head_branch}` ({head_sha[:7]})](https://github.com/{head_repo}/commit/{head_sha})"
+ )
+ if targets_str:
+ targets_formatted = ", ".join(f"`{t.strip()}`" for t in targets_str.split() if t.strip())
+ print(f"- **Targeted Tests Executed:** {targets_formatted}")
+ print("\n---\n")
+
+ # Executive Summary Table
+ status_label = "**PASSED**" if total_missed == 0 else "**ACTION REQUIRED**"
+ commit_link = f"[`{head_sha[:7]}`](https://github.com/{head_repo}/commit/{head_sha})" if head_sha else "HEAD"
+
+ print("### Executive Summary")
+ print(f"The code coverage on the recent commit {commit_link} is **{coverage_percent:.2f}%**.")
+ print("")
+ print("| Metric | Value | Status |")
+ print("| :--- | :---: | :---: |")
+ print(f"| **Patch Line Coverage** | **{coverage_percent:.2f}%** | {status_label} |")
+ print(f"| **Executable Lines Evaluated** | **{total_lines}** | — |")
+ print(f"| **Covered Lines** | **{total_covered}** | {coverage_percent:.1f}% |")
+ print(f"| **Unexecuted Lines** | **{total_missed}** | {'0' if total_missed == 0 else str(total_missed)} |")
+ print("")
+
+ # Modified Files Impact Table
+ print("### Modified Files Impact")
+ print("| Modified Source File | Patch Coverage | Covered / Total | Missed Lines | Unexecuted Line Spans |")
+ print("| :--- | :---: | :---: | :---: | :---: |")
+
+ for fpath, (f_covered, f_missed, added_lines) in active_files.items():
+ f_total = len(f_covered) + len(f_missed)
+ f_pct = (len(f_covered) / f_total * 100) if f_total > 0 else 0.0
+ line_spans = format_line_ranges(f_missed)
+ file_link = f"[`{fpath}`](https://github.com/{head_repo}/blob/{head_sha or 'main'}/{fpath})"
+ print(
+ f"| {file_link} | **{f_pct:.2f}%** | {len(f_covered)} / {f_total} | {len(f_missed)} | {line_spans} |"
+ )
+ print("")
+
+ # Collapsible Source Map Diff
+ print("<details>")
+ print("<summary><b>View Annotated Patch Diff (Source Map)</b></summary>\n")
+
+ for fpath, (f_covered, f_missed, added_lines) in active_files.items():
+ hunks = diff_files.get(fpath, [])
+ print(f"#### `{fpath}`")
+ print("```diff")
+ for hunk in hunks:
+ print(hunk.header)
+ for l_type, text, l_num in hunk.lines:
+ if l_type == "+":
+ if l_num in f_missed:
+ print(f"- {text}")
+ elif l_num in f_covered:
+ print(f"+ {text}")
+ else:
+ print(f" {text}")
+ elif l_type == " ":
+ print(f" {text}")
+ print("```\n")
+ print("</details>")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="LLVM-libc Patch 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="llvm/llvm-project", help="Base repository (e.g. llvm/llvm-project)")
+ parser.add_argument("head_repo", nargs="?", default="llvm/llvm-project", help="Head repository (e.g. tapiwagonga/llvm-project)")
+
+ args = parser.parse_args()
+
+ diff_files = DiffParser.parse(args.diff_file)
+ cov_data = CoverageJSONParser.load(args.json_file)
+ coverage_matrix = CoverageJSONParser.extract_patch_matrix(cov_data, diff_files)
+
+ render_patch_report(
+ diff_files,
+ coverage_matrix,
+ args.base_sha,
+ args.head_sha,
+ args.base_branch,
+ args.head_branch,
+ args.targets,
+ args.base_repo,
+ args.head_repo,
+ )
+
+
+if __name__ == "__main__":
+ main()
>From 559718d2fc381b5a2b8345516e0617eefa4cbe03 Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Wed, 19 Aug 2026 16:20:00 +0000
Subject: [PATCH 2/6] [libc][ci] Enable coverage instrumentation and harden
post-commit workflow
---
.../workflows/libc-post-commit-coverage.yml | 6 +++--
libc/CMakeLists.txt | 26 +++++++++++++++++++
2 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/libc-post-commit-coverage.yml b/.github/workflows/libc-post-commit-coverage.yml
index 6a7749b5dc181..5c47ecfbeb163 100644
--- a/.github/workflows/libc-post-commit-coverage.yml
+++ b/.github/workflows/libc-post-commit-coverage.yml
@@ -82,7 +82,7 @@ jobs:
START_TIME=$(date +%s)
export LLVM_PROFILE_FILE="libc_cov_%p.profraw"
- ninja -C build-cov libc-unit-tests
+ ninja -k 0 -C build-cov libc-unit-tests || true
END_TIME=$(date +%s)
echo "=== [STAGE 2/4] libc-unit-tests Completed in $((END_TIME - START_TIME))s ==="
@@ -90,7 +90,7 @@ jobs:
- name: Merge Profiles
run: |
echo "=== [STAGE 3/4] Merging Raw Profiles at $(date -u '+%Y-%m-%d %H:%M:%S UTC') ==="
- find build-cov -name "libc_cov_*.profraw" > profraw_list.txt
+ find . build-cov -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."
llvm-profdata-23 merge -sparse --input-files=profraw_list.txt -o libc_full.profdata
@@ -118,6 +118,8 @@ jobs:
"${EXECUTABLES[0]}" "${OBJECTS[@]}" \
--show-directory-coverage \
--show-branches=count \
+ --compilation-dir=. \
+ --path-equivalence="$GITHUB_WORKSPACE,." \
-ignore-filename-regex=".*(test|utils).*"
touch coverage_html/.nojekyll
diff --git a/libc/CMakeLists.txt b/libc/CMakeLists.txt
index e37d1a5414516..b7733a01ed464 100644
--- a/libc/CMakeLists.txt
+++ b/libc/CMakeLists.txt
@@ -91,6 +91,32 @@ endif()
set(LIBC_LINK_OPTIONS_DEFAULT "" CACHE STRING "Arguments used when linking.")
set(LIBC_TEST_LINK_OPTIONS_DEFAULT "" CACHE STRING "Common link options for all the tests.")
+option(LLVM_LIBC_ENABLE_COVERAGE "Build libc with coverage instrumentation" OFF)
+if(LLVM_LIBC_ENABLE_COVERAGE)
+ list(APPEND LIBC_COMPILE_OPTIONS_DEFAULT "-fprofile-instr-generate" "-fcoverage-mapping")
+ list(APPEND LIBC_TEST_COMPILE_OPTIONS_DEFAULT "-fprofile-instr-generate" "-fcoverage-mapping")
+ list(APPEND LIBC_TEST_LINK_OPTIONS_DEFAULT "-fprofile-instr-generate" "-fcoverage-mapping")
+
+ # When building with -nostdlib, the compiler does not automatically link the profiling runtime.
+ # We must explicitly query the compiler for the exact architecture-specific profile library path.
+ execute_process(
+ COMMAND ${CMAKE_CXX_COMPILER} --print-libgcc-file-name --rtlib=compiler-rt
+ OUTPUT_VARIABLE COMPILER_RT_BUILTINS
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ RESULT_VARIABLE COMPILER_RT_RETURN_CODE
+ )
+ if(COMPILER_RT_RETURN_CODE EQUAL 0)
+ string(REPLACE "builtins" "profile" COMPILER_RT_PROFILE "${COMPILER_RT_BUILTINS}")
+ if(EXISTS "${COMPILER_RT_PROFILE}")
+ list(APPEND LIBC_TEST_LINK_OPTIONS_DEFAULT "${COMPILER_RT_PROFILE}")
+ else()
+ message(WARNING "Coverage profiling runtime not found at ${COMPILER_RT_PROFILE}")
+ endif()
+ else()
+ message(WARNING "Failed to locate compiler-rt builtins library for coverage")
+ endif()
+endif()
+
set(LIBC_TEST_CMD "" CACHE STRING
"The full test command in the form <command> binary=@BINARY@, if using another program to test (e.g. QEMU)")
set(LIBC_TEST_HERMETIC_ONLY "" OFF CACHE BOOL "Only enable hermetic tests.")
>From fdb2a731843e3ba3aab759347da9b5a2af0b0acd Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Thu, 20 Aug 2026 08:26:17 +0000
Subject: [PATCH 3/6] [libc][string] Refactor strlen return value to test patch
coverage
---
libc/src/string/strlen.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/libc/src/string/strlen.cpp b/libc/src/string/strlen.cpp
index 234edb81d4c8c..afb9725638e4f 100644
--- a/libc/src/string/strlen.cpp
+++ b/libc/src/string/strlen.cpp
@@ -19,7 +19,8 @@ namespace LIBC_NAMESPACE_DECL {
// There might be potential for compiler optimization.
LLVM_LIBC_FUNCTION(size_t, strlen, (const char *src)) {
LIBC_CRASH_ON_NULLPTR(src);
- return internal::string_length(src);
+ const size_t length = internal::string_length(src);
+ return length;
}
} // namespace LIBC_NAMESPACE_DECL
>From 60ad5c22f793de97be6207d05ff157aea5a37231 Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Thu, 20 Aug 2026 08:59:28 +0000
Subject: [PATCH 4/6] [libc][ci] Use self-contained patch and full coverage
analyzers
---
libc/utils/coverage/full_report.py | 183 ++++++++---
libc/utils/coverage/patch_report.py | 341 ++++++++++++++++++---
libc/utils/coverage/test_coverage_tools.py | 310 +++++++++++++++++++
3 files changed, 755 insertions(+), 79 deletions(-)
create mode 100644 libc/utils/coverage/test_coverage_tools.py
diff --git a/libc/utils/coverage/full_report.py b/libc/utils/coverage/full_report.py
index 7c2f65551fb3b..8977b51e85e9e 100644
--- a/libc/utils/coverage/full_report.py
+++ b/libc/utils/coverage/full_report.py
@@ -9,14 +9,11 @@
# ==-------------------------------------------------------------------------==#
import argparse
+import json
import os
import sys
from pathlib import Path
-from typing import Dict, List, Tuple
-
-# Ensure local module import works regardless of CWD
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-from parser import CoverageJSONParser
+from typing import Any, Dict, List, Tuple
def render_full_report(cov_data: dict) -> None:
@@ -24,19 +21,24 @@ def render_full_report(cov_data: dict) -> None:
print("## LLVM-libc Full Codebase Coverage Report\n")
print("> [!WARNING]")
print("> ### No Coverage Data Detected")
- print("> The test execution completed but no coverage profiles were exported.")
+ print(
+ "> The test execution completed but no coverage profiles were exported."
+ )
return
- subsystems: Dict[str, Dict[str, int]] = {}
- file_stats: List[Tuple[str, int, int, int, int]] = []
+ subsystems: Dict[str, Dict[str, Any]] = {}
total_lines_cov = 0
total_lines_tot = 0
total_func_cov = 0
total_func_tot = 0
+ total_mcdc_cov = 0
+ total_mcdc_tot = 0
+ total_decisions_count = 0
+ total_decisions_full = 0
for item in cov_data["data"][0].get("files", []):
- fpath = item["filename"]
+ fpath = item.get("filename", "")
if "src/" not in fpath or "/test/" in fpath or "/utils/" in fpath:
continue
@@ -48,11 +50,23 @@ def render_full_report(cov_data: dict) -> None:
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)
+
+ mcdc_records = item.get("mcdc_records", [])
+ file_decisions_tot = len(mcdc_records)
+ file_decisions_full = 0
+ for rec in mcdc_records:
+ if len(rec) >= 10 and isinstance(rec[9], list):
+ conds = rec[9]
+ if all(conds):
+ file_decisions_full += 1
if line_tot == 0:
continue
@@ -61,6 +75,10 @@ def render_full_report(cov_data: dict) -> None:
total_lines_tot += line_tot
total_func_cov += func_cov
total_func_tot += func_tot
+ total_mcdc_cov += mcdc_cov
+ total_mcdc_tot += mcdc_tot
+ total_decisions_count += file_decisions_tot
+ total_decisions_full += file_decisions_full
parts = rel_path.split("/")
subsystem = "/".join(parts[:2]) if len(parts) >= 2 else parts[0]
@@ -71,57 +89,145 @@ def render_full_report(cov_data: dict) -> None:
"lines_tot": 0,
"func_cov": 0,
"func_tot": 0,
+ "mcdc_cov": 0,
+ "mcdc_tot": 0,
+ "decisions_tot": 0,
+ "decisions_full": 0,
}
subsystems[subsystem]["lines_cov"] += line_cov
subsystems[subsystem]["lines_tot"] += line_tot
subsystems[subsystem]["func_cov"] += func_cov
subsystems[subsystem]["func_tot"] += func_tot
+ subsystems[subsystem]["mcdc_cov"] += mcdc_cov
+ subsystems[subsystem]["mcdc_tot"] += mcdc_tot
+ subsystems[subsystem]["decisions_tot"] += file_decisions_tot
+ subsystems[subsystem]["decisions_full"] += file_decisions_full
- file_stats.append((rel_path, line_cov, line_tot, func_cov, func_tot))
-
- line_pct = (total_lines_cov / total_lines_tot * 100) if total_lines_tot > 0 else 0
- func_pct = (total_func_cov / total_func_tot * 100) if total_func_tot > 0 else 0
+ line_pct = (
+ (total_lines_cov / total_lines_tot * 100) if total_lines_tot > 0 else 0
+ )
+ func_pct = (
+ (total_func_cov / total_func_tot * 100) if total_func_tot > 0 else 0
+ )
+ has_mcdc = total_mcdc_tot > 0
+ mcdc_pct = (
+ (total_mcdc_cov / total_mcdc_tot * 100) if total_mcdc_tot > 0 else 0
+ )
+ decisions_pct = (
+ (total_decisions_full / total_decisions_count * 100)
+ if total_decisions_count > 0
+ else 0
+ )
- repo = os.environ.get("GITHUB_REPOSITORY", "tapiwagonga/llvm-project")
- if "/" in repo:
- owner, repo_name = repo.split("/", 1)
- pages_url = f"https://{owner}.github.io/{repo_name}/"
+ pages_url = os.environ.get("COVERAGE_DASHBOARD_URL")
+ if not pages_url:
+ repo = os.environ.get("GITHUB_REPOSITORY", "llvm/llvm-project")
+ if "/" in repo:
+ owner, repo_name = repo.split("/", 1)
+ pages_url = f"https://{owner}.github.io/{repo_name}/"
+ else:
+ pages_url = f"https://{repo}.github.io/"
+
+ if has_mcdc and not pages_url.endswith("/mcdc/"):
+ mcdc_pages_url = pages_url.rstrip("/") + "/mcdc/"
else:
- pages_url = f"https://{repo}.github.io/"
+ mcdc_pages_url = pages_url
print("## LLVM-libc Full Codebase Coverage Report\n")
print("> [!NOTE]")
- print(f"> ### Overall Codebase Coverage: **{line_pct:.2f}%**")
- print(
- f"> Successfully tested **{total_lines_cov:,} / {total_lines_tot:,}** executable lines across all LLVM-libc subsystems."
- )
- print("")
+ if has_mcdc:
+ print(
+ f"> ### Overall Codebase Coverage: **{line_pct:.2f}% Line** | **{mcdc_pct:.2f}% MC/DC**"
+ )
+ print(
+ f"> Tested **{total_lines_cov:,} / {total_lines_tot:,}** executable lines and **{total_mcdc_cov:,} / {total_mcdc_tot:,}** boolean conditions across **{total_decisions_count:,}** decisions."
+ )
+ print("")
+ print(
+ f"- **Coverage Dashboard:** [{mcdc_pages_url}]({mcdc_pages_url})"
+ )
+ else:
+ print(f"> ### Overall Codebase Coverage: **{line_pct:.2f}%**")
+ print(
+ f"> Tested **{total_lines_cov:,} / {total_lines_tot:,}** executable lines across all LLVM-libc subsystems."
+ )
+ print("")
+ print(f"- **Coverage Dashboard:** [{pages_url}]({pages_url})")
- print(f"- **Coverage Dashboard:** [{pages_url}]({pages_url})")
print("\n---\n")
- print("### Codebase Health Metrics")
+ print("### Overall")
print("| Metric | Covered | Total | Coverage % |")
print("| :--- | :---: | :---: | :---: |")
+ if has_mcdc:
+ print(
+ f"| **MC/DC Condition Independence** | {total_mcdc_cov:,} | {total_mcdc_tot:,} | **{mcdc_pct:.2f}%** |"
+ )
+ print(
+ f"| **Fully Verified Decisions** | {total_decisions_full:,} | {total_decisions_count:,} | **{decisions_pct:.2f}%** |"
+ )
print(
- f"| **Executable Line Coverage** | {total_lines_cov:,} | {total_lines_tot:,} | **{line_pct:.2f}%** |"
+ f"| **Executable Lines** | {total_lines_cov:,} | {total_lines_tot:,} | **{line_pct:.2f}%** |"
)
print(
- f"| **Function Coverage** | {total_func_cov:,} | {total_func_tot:,} | **{func_pct:.2f}%** |\n"
+ f"| **Functions** | {total_func_cov:,} | {total_func_tot:,} | **{func_pct:.2f}%** |"
)
+ print("")
- print("### Subsystem Coverage Breakdown")
- print("| Subsystem | Line Coverage | Function Coverage | Executable Lines | Missed Lines |")
- print("| :--- | :---: | :---: | :---: | :---: |")
- for sub, data in sorted(subsystems.items()):
- s_line_pct = (data["lines_cov"] / data["lines_tot"] * 100) if data["lines_tot"] > 0 else 0
- s_func_pct = (data["func_cov"] / data["func_tot"] * 100) if data["func_tot"] > 0 else 0
- missed = data["lines_tot"] - data["lines_cov"]
+ print("### Coverage Breakdown")
+ if has_mcdc:
print(
- f"| `libc/{sub}` | **{s_line_pct:.2f}%** | {s_func_pct:.2f}% | {data['lines_tot']:,} | {missed:,} |"
+ "| Subsystem | MC/DC Conditions | Decisions (Verified / Total) | Line Coverage | Function Coverage | Executable Lines | Missed Lines |"
+ )
+ print("| :--- | :---: | :---: | :---: | :---: | :---: | :---: |")
+
+ sorted_subsystems = sorted(subsystems.keys())
+ else:
+ print(
+ "| Subsystem | Line Coverage | Function Coverage | Executable Lines | Missed Lines |"
+ )
+ print("| :--- | :---: | :---: | :---: | :---: |")
+ sorted_subsystems = sorted(subsystems.keys())
+
+ for sub in sorted_subsystems:
+ data = subsystems[sub]
+ s_line_pct = (
+ (data["lines_cov"] / data["lines_tot"] * 100)
+ if data["lines_tot"] > 0
+ else 0
+ )
+ s_func_pct = (
+ (data["func_cov"] / data["func_tot"] * 100)
+ if data["func_tot"] > 0
+ else 0
)
+ missed_lines = data["lines_tot"] - data["lines_cov"]
+
+ if has_mcdc:
+ s_mc_pct = (
+ (data["mcdc_cov"] / data["mcdc_tot"] * 100)
+ if data["mcdc_tot"] > 0
+ else 0
+ )
+ mc_cell = (
+ f"**{s_mc_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"
+ )
+ print(
+ f"| `libc/{sub}` | {mc_cell} | {dec_cell} | **{s_line_pct:.2f}%** | {s_func_pct:.2f}% | {data['lines_tot']:,} | {missed_lines:,} |"
+ )
+ else:
+ print(
+ f"| `libc/{sub}` | **{s_line_pct:.2f}%** | {s_func_pct:.2f}% | {data['lines_tot']:,} | {missed_lines:,} |"
+ )
def main() -> None:
@@ -130,7 +236,12 @@ def main() -> None:
args, _ = parser.parse_known_args()
- cov_data = CoverageJSONParser.load(args.json_file)
+ try:
+ with open(args.json_file, "r", encoding="utf-8") as f:
+ cov_data = json.load(f)
+ except Exception as e:
+ sys.stderr.write(f"Error: Failed to parse coverage JSON: {e}\n")
+ sys.exit(1)
render_full_report(cov_data)
diff --git a/libc/utils/coverage/patch_report.py b/libc/utils/coverage/patch_report.py
index 48f9891d4fe4b..d2da34a469405 100644
--- a/libc/utils/coverage/patch_report.py
+++ b/libc/utils/coverage/patch_report.py
@@ -9,14 +9,143 @@
# ==-------------------------------------------------------------------------==#
import argparse
+import json
import os
+import re
import sys
from pathlib import Path
-from typing import Dict, List, Optional, Set
+from typing import Any, Dict, List, Optional, Set, Tuple
+
+
+class DiffHunk:
+ def __init__(self, header: str):
+ self.header: str = header
+ self.lines: List[Tuple[str, str, int]] = [] # (prefix, text, line_number)
+
+
+class DiffParser:
+ @staticmethod
+ def parse(diff_source: str) -> Dict[str, List[DiffHunk]]:
+ files: Dict[str, List[DiffHunk]] = {}
+ current_file: Optional[str] = None
+ current_hunk: Optional[DiffHunk] = None
+ current_line_num: int = 0
+
+ # Support both file path and raw diff string
+ if os.path.isfile(diff_source):
+ with open(diff_source, "r", encoding="utf-8") as f:
+ lines = f.readlines()
+ else:
+ lines = diff_source.splitlines(keepends=True)
+
+ for line in lines:
+ line = 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("@@"):
+ match = re.search(r"\+([0-9]+)", line)
+ if match:
+ current_line_num = int(match.group(1))
+ current_hunk = DiffHunk(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_num))
+ current_line_num += 1
+ elif line.startswith(" "):
+ current_hunk.lines.append((" ", line[1:], current_line_num))
+ current_line_num += 1
+
+ return files
+
+
+class CoverageJSONParser:
+ @staticmethod
+ def load(json_path: str) -> dict:
+ try:
+ with open(json_path, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception as e:
+ sys.stderr.write(f"Error: Failed to parse coverage JSON: {e}\n")
+ sys.exit(1)
+
+ @staticmethod
+ def extract_patch_matrix(
+ cov_data: dict, diff_files: Dict[str, List[DiffHunk]]
+ ) -> Dict[str, Dict[str, Any]]:
+ coverage_matrix: Dict[str, Dict[str, Any]] = {
+ fpath: {"covered": set(), "missed": set(), "mcdc_decisions": []}
+ for fpath in diff_files.keys()
+ }
+
+ if "data" not in cov_data or not cov_data["data"]:
+ return coverage_matrix
+
+ for item in cov_data["data"][0].get("files", []):
+ fpath = item.get("filename", "")
+ rel_path = next(
+ (rp for rp in diff_files.keys() if fpath.endswith(rp)), None
+ )
+ if not rel_path:
+ continue
+
+ # 1. Statement segments
+ segments = item.get("segments", [])
+ for i in range(len(segments) - 1):
+ current = segments[i]
+ nxt = segments[i + 1]
+
+ line_start = current[0]
+ line_end = nxt[0]
+ count = current[2]
+ has_count = current[3]
+
+ if has_count:
+ for line_num in range(line_start, line_end + 1):
+ if count > 0:
+ coverage_matrix[rel_path]["covered"].add(line_num)
+ else:
+ coverage_matrix[rel_path]["missed"].add(line_num)
+
+ # 2. MC/DC decision records (Clang 18+)
+ mcdc_records = item.get("mcdc_records", [])
+ for rec in mcdc_records:
+ if len(rec) >= 10 and isinstance(rec[9], list):
+ l_start = rec[0]
+ l_end = rec[2]
+ conds = rec[9]
+ cov_conds = sum(1 for c in conds if c)
+ coverage_matrix[rel_path]["mcdc_decisions"].append(
+ {
+ "line_start": l_start,
+ "line_end": l_end,
+ "conditions": conds,
+ "covered": cov_conds,
+ "total": len(conds),
+ }
+ )
+
+ return coverage_matrix
-# Ensure local module import works regardless of CWD
-sys.path.insert(0, str(Path(__file__).parent.resolve()))
-from parser import CoverageJSONParser, DiffHunk, DiffParser
def is_executable_line(text: str) -> bool:
s = text.strip()
@@ -71,7 +200,7 @@ def format_line_ranges(lines: Set[int]) -> str:
def render_patch_report(
diff_files: Dict[str, List[DiffHunk]],
- coverage_matrix: Dict[str, Dict[str, Set[int]]],
+ coverage_matrix: Dict[str, Dict[str, Any]],
base_sha: Optional[str],
head_sha: Optional[str],
base_branch: Optional[str],
@@ -84,6 +213,12 @@ def render_patch_report(
total_missed = 0
active_files = {}
+ total_mcdc_cov = 0
+ total_mcdc_tot = 0
+ total_decisions_count = 0
+ fully_verified_decisions = 0
+ file_mcdc_data: Dict[str, Tuple[int, int, List[str]]] = {}
+
for fpath, data in coverage_matrix.items():
added_lines: Set[int] = set()
for hunk in diff_files.get(fpath, []):
@@ -107,10 +242,64 @@ def render_patch_report(
total_missed += len(added_lines)
active_files[fpath] = (set(), added_lines, added_lines)
+ # Evaluate MC/DC decisions on modified lines
+ f_mcdc_cov = 0
+ f_mcdc_tot = 0
+ f_dec_ver = 0
+ f_dec_tot = 0
+ condition_diagnostics = []
+ unverified_decision_lines: Dict[int, List[str]] = {}
+
+ for decision in data.get("mcdc_decisions", []):
+ d_start = decision["line_start"]
+ d_end = decision["line_end"]
+ # Check if any modified line overlaps this decision range
+ if any(d_start <= l <= d_end for l in added_lines):
+ total_decisions_count += 1
+ f_dec_tot += 1
+ f_mcdc_cov += decision["covered"]
+ f_mcdc_tot += decision["total"]
+ if decision["covered"] == decision["total"]:
+ fully_verified_decisions += 1
+ f_dec_ver += 1
+ condition_diagnostics.append(
+ f"`L{d_start}`: {decision['covered']}/{decision['total']} verified"
+ )
+ else:
+ uncovered_idx = [
+ f"C{i+1}"
+ for i, is_cov in enumerate(decision["conditions"])
+ if not is_cov
+ ]
+ unverified_str = ", ".join(uncovered_idx)
+ condition_diagnostics.append(
+ f"`L{d_start}`: {decision['covered']}/{decision['total']} verified ({unverified_str} unverified)"
+ )
+ for l in range(d_start, d_end + 1):
+ if l in added_lines:
+ unverified_decision_lines[l] = uncovered_idx
+
+ if f_mcdc_tot > 0 or f_dec_tot > 0:
+ total_mcdc_cov += f_mcdc_cov
+ total_mcdc_tot += f_mcdc_tot
+ file_mcdc_data[fpath] = (
+ f_mcdc_cov,
+ f_mcdc_tot,
+ condition_diagnostics,
+ f_dec_ver,
+ f_dec_tot,
+ unverified_decision_lines,
+ )
+
total_lines = total_covered + total_missed
+ has_mcdc = total_mcdc_tot > 0
- if total_lines == 0 or not active_files:
+ if has_mcdc:
+ print("## LLVM-libc MC/DC Patch Coverage Report\n")
+ else:
print("## LLVM-libc Patch Coverage Report\n")
+
+ if total_lines == 0 or not active_files:
if base_sha and head_sha and base_branch and head_branch:
print(
f"- **Base Branch:** [`{base_branch}` ({base_sha[:7]})](https://github.com/{base_repo}/commit/{base_sha})"
@@ -122,28 +311,45 @@ def render_patch_report(
print("> [!NOTE]")
print("> ### Coverage Validated")
print("> No `.cpp` source files in `libc/src/` were modified in this patch.")
- sys.exit(0)
-
- print("## LLVM-libc Patch Coverage Report\n")
+ return
coverage_percent = (total_covered / total_lines) * 100
+ mcdc_percent = (total_mcdc_cov / total_mcdc_tot * 100) if has_mcdc else 0.0
- # Modern GitHub UI Alert Card
if total_missed == 0:
- print("> [!TIP]")
- print(f"> ### Patch Coverage: **{coverage_percent:.2f}%** (PASSED)")
- print(
- f"> All **{total_lines}** newly added or modified executable lines are covered by targeted unit tests."
- )
+ if not has_mcdc:
+ print("> [!TIP]")
+ print(f"> ### Patch Coverage: **{coverage_percent:.2f}%**")
+ print(
+ f"> All **{total_lines}** newly added or modified executable lines are covered."
+ )
+ elif total_mcdc_cov == total_mcdc_tot:
+ print("> [!TIP]")
+ print(
+ f"> ### Patch Coverage: **{coverage_percent:.2f}% Line** | **100.00% MC/DC**"
+ )
+ print(
+ f"> All **{total_lines}** executable lines and **{total_mcdc_tot}** boolean conditions across **{total_decisions_count}** decisions are covered."
+ )
+ else:
+ print("> [!NOTE]")
+ print(
+ f"> ### Patch Coverage: **{coverage_percent:.2f}% Line** | **{mcdc_percent:.1f}% MC/DC**"
+ )
+ print(
+ f"> Executed **{total_covered} / {total_lines}** lines. **{total_mcdc_cov} / {total_mcdc_tot}** boolean conditions achieved independence across **{fully_verified_decisions} / {total_decisions_count}** decisions."
+ )
else:
print("> [!WARNING]")
- print(f"> ### Patch Coverage: **{coverage_percent:.2f}%** ({total_missed} Missed Lines)")
print(
- f"> **{total_missed}** unexecuted line(s) detected in your patch. Please review the missing lines below."
+ f"> ### Patch Coverage: **{coverage_percent:.2f}%** ({total_missed} Missed Lines)"
+ )
+ print(
+ f"> **{total_missed}** unexecuted lines detected in patch."
)
print("")
- # Commit metadata and targets executed with exact upstream and fork repositories
+ # Commit metadata and targets executed
if base_sha and head_sha and base_branch and head_branch:
print(
f"- **Base Branch:** [`{base_branch}` ({base_sha[:7]})](https://github.com/{base_repo}/commit/{base_sha})"
@@ -152,37 +358,68 @@ def render_patch_report(
f"- **Head Commit:** [`{head_branch}` ({head_sha[:7]})](https://github.com/{head_repo}/commit/{head_sha})"
)
if targets_str:
- targets_formatted = ", ".join(f"`{t.strip()}`" for t in targets_str.split() if t.strip())
+ targets_formatted = ", ".join(
+ f"`{t.strip()}`" for t in targets_str.split() if t.strip()
+ )
print(f"- **Targeted Tests Executed:** {targets_formatted}")
print("\n---\n")
- # Executive Summary Table
- status_label = "**PASSED**" if total_missed == 0 else "**ACTION REQUIRED**"
- commit_link = f"[`{head_sha[:7]}`](https://github.com/{head_repo}/commit/{head_sha})" if head_sha else "HEAD"
-
- print("### Executive Summary")
- print(f"The code coverage on the recent commit {commit_link} is **{coverage_percent:.2f}%**.")
- print("")
- print("| Metric | Value | Status |")
- print("| :--- | :---: | :---: |")
- print(f"| **Patch Line Coverage** | **{coverage_percent:.2f}%** | {status_label} |")
- print(f"| **Executable Lines Evaluated** | **{total_lines}** | — |")
- print(f"| **Covered Lines** | **{total_covered}** | {coverage_percent:.1f}% |")
- print(f"| **Unexecuted Lines** | **{total_missed}** | {'0' if total_missed == 0 else str(total_missed)} |")
- print("")
-
- # Modified Files Impact Table
- print("### Modified Files Impact")
- print("| Modified Source File | Patch Coverage | Covered / Total | Missed Lines | Unexecuted Line Spans |")
- print("| :--- | :---: | :---: | :---: | :---: |")
+ # Unified Coverage Breakdown Table
+ print("### Coverage Breakdown")
+ if has_mcdc:
+ print(
+ "| Modified Source File | Line Coverage | MC/DC Conditions | Decisions (Verified / Total) | Missed Lines | Unverified Conditions |"
+ )
+ print("| :--- | :---: | :---: | :---: | :---: | :--- |")
+ else:
+ print(
+ "| Modified Source File | Patch Coverage | Covered / Total | Missed Lines | Unexecuted Line Spans |"
+ )
+ print("| :--- | :---: | :---: | :---: | :---: |")
for fpath, (f_covered, f_missed, added_lines) in active_files.items():
f_total = len(f_covered) + len(f_missed)
f_pct = (len(f_covered) / f_total * 100) if f_total > 0 else 0.0
line_spans = format_line_ranges(f_missed)
file_link = f"[`{fpath}`](https://github.com/{head_repo}/blob/{head_sha or 'main'}/{fpath})"
+
+ if has_mcdc:
+ f_mc_data = file_mcdc_data.get(fpath, (0, 0, [], 0, 0, {}))
+ f_mc_cov, f_mc_tot, diag_list, f_dec_ver, f_dec_tot = (
+ f_mc_data[0],
+ f_mc_data[1],
+ f_mc_data[2],
+ f_mc_data[3],
+ f_mc_data[4],
+ )
+ if f_mc_tot > 0:
+ f_mc_pct = f_mc_cov / f_mc_tot * 100
+ mcdc_cell = f"**{f_mc_pct:.1f}%** ({f_mc_cov}/{f_mc_tot})"
+ else:
+ mcdc_cell = "N/A"
+
+ dec_cell = (
+ f"**{f_dec_ver} / {f_dec_tot}**" if f_dec_tot > 0 else "N/A"
+ )
+ diag_cell = "<br>".join(diag_list) if diag_list else "None"
+
+ print(
+ f"| {file_link} | **{f_pct:.2f}%** ({len(f_covered)}/{f_total}) | {mcdc_cell} | {dec_cell} | {len(f_missed)} | {diag_cell} |"
+ )
+ else:
+ print(
+ f"| {file_link} | **{f_pct:.2f}%** | {len(f_covered)} / {f_total} | {len(f_missed)} | {line_spans} |"
+ )
+
+ # Summary Row
+ if has_mcdc:
+ total_dec_cell = f"**{fully_verified_decisions} / {total_decisions_count}**"
+ print(
+ f"| **Total (Patch)** | **{coverage_percent:.2f}%** ({total_covered}/{total_lines}) | **{mcdc_percent:.1f}%** ({total_mcdc_cov}/{total_mcdc_tot}) | {total_dec_cell} | **{total_missed}** | - |"
+ )
+ else:
print(
- f"| {file_link} | **{f_pct:.2f}%** | {len(f_covered)} / {f_total} | {len(f_missed)} | {line_spans} |"
+ f"| **Total (Patch)** | **{coverage_percent:.2f}%** | {total_covered} / {total_lines} | **{total_missed}** | - |"
)
print("")
@@ -192,6 +429,9 @@ def render_patch_report(
for fpath, (f_covered, f_missed, added_lines) in active_files.items():
hunks = diff_files.get(fpath, [])
+ f_mc_data = file_mcdc_data.get(fpath, (0, 0, [], 0, 0, {}))
+ unverified_lines = f_mc_data[5] if len(f_mc_data) > 5 else {}
+
print(f"#### `{fpath}`")
print("```diff")
for hunk in hunks:
@@ -199,7 +439,10 @@ def render_patch_report(
for l_type, text, l_num in hunk.lines:
if l_type == "+":
if l_num in f_missed:
- print(f"- {text}")
+ print(f"- {text} // [MISSED]")
+ elif l_num in unverified_lines:
+ unverified_conds = ", ".join(unverified_lines[l_num])
+ print(f"! {text} // [PARTIAL MC/DC: {unverified_conds} unverified]")
elif l_num in f_covered:
print(f"+ {text}")
else:
@@ -218,9 +461,21 @@ def main() -> None:
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="llvm/llvm-project", help="Base repository (e.g. llvm/llvm-project)")
- parser.add_argument("head_repo", nargs="?", default="llvm/llvm-project", help="Head repository (e.g. tapiwagonga/llvm-project)")
+ parser.add_argument(
+ "targets", nargs="?", help="Space-separated list of executed test targets"
+ )
+ parser.add_argument(
+ "base_repo",
+ nargs="?",
+ default="llvm/llvm-project",
+ help="Base repository (e.g. llvm/llvm-project)",
+ )
+ parser.add_argument(
+ "head_repo",
+ nargs="?",
+ default="llvm/llvm-project",
+ help="Head repository (e.g. contributor/llvm-project)",
+ )
args = parser.parse_args()
diff --git a/libc/utils/coverage/test_coverage_tools.py b/libc/utils/coverage/test_coverage_tools.py
new file mode 100644
index 0000000000000..a69be35cdd29e
--- /dev/null
+++ b/libc/utils/coverage/test_coverage_tools.py
@@ -0,0 +1,310 @@
+#!/usr/bin/env python3
+# ===-- test_coverage_tools.py - Unit tests for coverage utilities --------===#
+#
+# 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
+#
+# ===----------------------------------------------------------------------===#
+
+import io
+import json
+import os
+import sys
+import unittest
+from contextlib import redirect_stdout
+from unittest.mock import patch
+
+# Add parent directory to path to import coverage tools
+current_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, current_dir)
+
+from patch_report import (
+ DiffParser,
+ CoverageJSONParser,
+ is_executable_line,
+ format_line_ranges,
+ render_patch_report,
+)
+from full_report import render_full_report
+
+
+class TestDiffParser(unittest.TestCase):
+ def test_parse_unified_diff(self):
+ sample_diff = """diff --git a/libc/src/string/memchr.cpp b/libc/src/string/memchr.cpp
+index 1234567..89abcdef 100644
+--- a/libc/src/string/memchr.cpp
++++ b/libc/src/string/memchr.cpp
+@@ -10,6 +10,8 @@
+ void *memchr(const void *src, int c, size_t n) {
++ const unsigned char *p = (const unsigned char *)src;
++ // Check boundaries
++ if (n == 0) return nullptr;
+ return nullptr;
+ }
+"""
+ parsed = DiffParser.parse(sample_diff)
+ self.assertIn("libc/src/string/memchr.cpp", parsed)
+ hunks = parsed["libc/src/string/memchr.cpp"]
+ self.assertEqual(len(hunks), 1)
+
+ added_lines = [l_num for l_type, _, l_num in hunks[0].lines if l_type == "+"]
+ self.assertEqual(added_lines, [11, 12, 13])
+
+ def test_is_executable_line(self):
+ self.assertTrue(is_executable_line(" int x = 42;"))
+ self.assertTrue(is_executable_line(" return nullptr;"))
+ self.assertTrue(is_executable_line(" if (a > b) {"))
+
+ self.assertFalse(is_executable_line(" // Just a comment"))
+ self.assertFalse(is_executable_line(" /* Block comment */"))
+ self.assertFalse(is_executable_line(" #include <stddef.h>"))
+ self.assertFalse(is_executable_line(" {"))
+ self.assertFalse(is_executable_line(" }"))
+ self.assertFalse(is_executable_line(" "))
+
+
+class TestCoverageJSONParser(unittest.TestCase):
+ def test_extract_patch_matrix_standard(self):
+ sample_cov_data = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/llvm-project/libc/src/string/memchr.cpp",
+ "segments": [
+ [10, 1, 5, True, True, False],
+ [15, 1, 0, True, True, False],
+ [20, 1, 0, False, False, False],
+ ],
+ "summary": {
+ "lines": {"count": 20, "covered": 10, "percent": 50.0},
+ "functions": {"count": 1, "covered": 1, "percent": 100.0},
+ },
+ }
+ ]
+ }
+ ]
+ }
+
+ diff_files = {"libc/src/string/memchr.cpp": []}
+ matrix = CoverageJSONParser.extract_patch_matrix(sample_cov_data, diff_files)
+ self.assertIn("libc/src/string/memchr.cpp", matrix)
+ self.assertIn(10, matrix["libc/src/string/memchr.cpp"]["covered"])
+ self.assertIn(14, matrix["libc/src/string/memchr.cpp"]["covered"])
+ self.assertIn(15, matrix["libc/src/string/memchr.cpp"]["missed"])
+ self.assertIn(19, matrix["libc/src/string/memchr.cpp"]["missed"])
+
+ def test_extract_patch_matrix_with_mcdc(self):
+ sample_mcdc_data = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/workspace/llvm-project/libc/src/string/memchr.cpp",
+ "segments": [
+ [10, 1, 5, True, True, False],
+ [20, 1, 0, False, False, False],
+ ],
+ "mcdc_records": [
+ [12, 5, 12, 25, 1, 1, 0, 0, 5, [True, False, False]]
+ ],
+ "summary": {
+ "lines": {"count": 10, "covered": 10, "percent": 100.0},
+ "functions": {"count": 1, "covered": 1, "percent": 100.0},
+ "mcdc": {"count": 3, "covered": 1, "notcovered": 2, "percent": 33.33},
+ },
+ }
+ ]
+ }
+ ]
+ }
+
+ diff_files = {"libc/src/string/memchr.cpp": []}
+ matrix = CoverageJSONParser.extract_patch_matrix(sample_mcdc_data, diff_files)
+
+ decisions = matrix["libc/src/string/memchr.cpp"]["mcdc_decisions"]
+ self.assertEqual(len(decisions), 1)
+ self.assertEqual(decisions[0]["line_start"], 12)
+ self.assertEqual(decisions[0]["total"], 3)
+ self.assertEqual(decisions[0]["covered"], 1)
+
+
+class TestLineRangeFormatter(unittest.TestCase):
+ def test_format_contiguous_and_discrete_ranges(self):
+ self.assertEqual(format_line_ranges(set()), "None")
+ self.assertEqual(format_line_ranges({5}), "`L5`")
+ self.assertEqual(format_line_ranges({10, 11, 12, 13}), "`L10-L13`")
+ self.assertEqual(
+ format_line_ranges({1, 2, 3, 7, 10, 11, 15}),
+ "`L1-L3`, `L7`, `L10-L11`, `L15`",
+ )
+
+
+class TestPatchReportRendering(unittest.TestCase):
+ def test_render_linear_pass(self):
+ diff_text = """diff --git a/libc/src/string/memchr.cpp b/libc/src/string/memchr.cpp
+--- a/libc/src/string/memchr.cpp
++++ b/libc/src/string/memchr.cpp
+@@ -10,2 +10,2 @@
++ int a = 1;
++ int b = 2;
+"""
+ diff_files = DiffParser.parse(diff_text)
+ coverage_matrix = {
+ "libc/src/string/memchr.cpp": {
+ "covered": {10, 11},
+ "missed": set(),
+ "mcdc_decisions": [],
+ }
+ }
+
+ f = io.StringIO()
+ with redirect_stdout(f):
+ render_patch_report(
+ diff_files,
+ coverage_matrix,
+ base_sha="abc1234567",
+ head_sha="def8901234",
+ base_branch="main",
+ head_branch="patch-1",
+ targets_str="libc.test.src.string.memchr_test.__unit__",
+ )
+ output = f.getvalue()
+
+ self.assertIn("## LLVM-libc Patch Coverage Report", output)
+ self.assertIn("100.00%", output)
+
+ def test_render_full_mcdc(self):
+ diff_text = """diff --git a/libc/src/string/memchr.cpp b/libc/src/string/memchr.cpp
+--- a/libc/src/string/memchr.cpp
++++ b/libc/src/string/memchr.cpp
+@@ -10,2 +10,2 @@
++ if (a && b) {
++ return nullptr;
+"""
+ diff_files = DiffParser.parse(diff_text)
+ coverage_matrix = {
+ "libc/src/string/memchr.cpp": {
+ "covered": {10, 11},
+ "missed": set(),
+ "mcdc_decisions": [
+ {
+ "line_start": 10,
+ "line_end": 10,
+ "conditions": [True, True],
+ "covered": 2,
+ "total": 2,
+ }
+ ],
+ }
+ }
+
+ f = io.StringIO()
+ with redirect_stdout(f):
+ render_patch_report(
+ diff_files,
+ coverage_matrix,
+ base_sha="abc1234567",
+ head_sha="def8901234",
+ base_branch="main",
+ head_branch="patch-1",
+ targets_str="libc.test.src.string.memchr_test.__unit__",
+ )
+ output = f.getvalue()
+
+ self.assertIn("## LLVM-libc MC/DC Patch Coverage Report", output)
+ self.assertIn("100.00% MC/DC", output)
+ self.assertIn("Decisions (Verified / Total)", output)
+
+ def test_render_partial_mcdc(self):
+ diff_text = """diff --git a/libc/src/string/memchr.cpp b/libc/src/string/memchr.cpp
+--- a/libc/src/string/memchr.cpp
++++ b/libc/src/string/memchr.cpp
+@@ -10,2 +10,2 @@
++ if (a && (b || c)) {
++ return nullptr;
+"""
+ diff_files = DiffParser.parse(diff_text)
+ coverage_matrix = {
+ "libc/src/string/memchr.cpp": {
+ "covered": {10, 11},
+ "missed": set(),
+ "mcdc_decisions": [
+ {
+ "line_start": 10,
+ "line_end": 10,
+ "conditions": [True, False, False],
+ "covered": 1,
+ "total": 3,
+ }
+ ],
+ }
+ }
+
+ f = io.StringIO()
+ with redirect_stdout(f):
+ render_patch_report(
+ diff_files,
+ coverage_matrix,
+ base_sha="abc1234567",
+ head_sha="def8901234",
+ base_branch="main",
+ head_branch="patch-1",
+ targets_str="libc.test.src.string.memchr_test.__unit__",
+ )
+ output = f.getvalue()
+
+ self.assertIn("33.3% MC/DC", output)
+ self.assertIn("C2, C3 unverified", output)
+
+
+class TestFullReportRendering(unittest.TestCase):
+ @patch.dict(os.environ, {"GITHUB_REPOSITORY": "llvm/llvm-project"}, clear=True)
+ def test_render_full_report_streamlined(self):
+ cov_data = {
+ "data": [
+ {
+ "files": [
+ {
+ "filename": "/root/llvm-project/libc/src/string/memchr.cpp",
+ "mcdc_records": [
+ [10, 1, 10, 20, 1, 1, 0, 0, 5, [True, True]],
+ ],
+ "summary": {
+ "lines": {"count": 10, "covered": 10, "percent": 100.0},
+ "functions": {"count": 1, "covered": 1, "percent": 100.0},
+ "mcdc": {"count": 2, "covered": 2, "notcovered": 0, "percent": 100.0},
+ },
+ },
+ {
+ "filename": "/root/llvm-project/libc/src/math/sin.cpp",
+ "mcdc_records": [
+ [20, 1, 20, 30, 1, 1, 0, 0, 5, [False, False]],
+ ],
+ "summary": {
+ "lines": {"count": 20, "covered": 15, "percent": 75.0},
+ "functions": {"count": 1, "covered": 1, "percent": 100.0},
+ "mcdc": {"count": 2, "covered": 0, "notcovered": 2, "percent": 0.0},
+ },
+ }
+ ]
+ }
+ ]
+ }
+
+ f = io.StringIO()
+ with redirect_stdout(f):
+ render_full_report(cov_data)
+ output = f.getvalue()
+
+ self.assertIn("## LLVM-libc Full Codebase Coverage Report", output)
+ self.assertIn("Overall", output)
+ self.assertIn("Coverage Breakdown", output)
+ self.assertNotIn("Status", output)
+ self.assertNotIn("Health", output)
+ self.assertNotIn("Safety Priority", output)
+
+
+if __name__ == "__main__":
+ unittest.main()
>From 8258bbf46e89838a2bfd814d91075a2bf9d395ae Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Thu, 20 Aug 2026 09:06:25 +0000
Subject: [PATCH 5/6] [libc][ctype] Refactor isalnum boundary check to test
patch coverage
---
libc/src/ctype/isalnum.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/libc/src/ctype/isalnum.cpp b/libc/src/ctype/isalnum.cpp
index 102b5e79e4a18..d4958980472c6 100644
--- a/libc/src/ctype/isalnum.cpp
+++ b/libc/src/ctype/isalnum.cpp
@@ -16,7 +16,8 @@
namespace LIBC_NAMESPACE_DECL {
LLVM_LIBC_FUNCTION(int, isalnum, (int c)) {
- if (c < 0 || c > cpp::numeric_limits<unsigned char>::max())
+ const int max_uchar = static_cast<int>(cpp::numeric_limits<unsigned char>::max());
+ if (c < 0 || c > max_uchar)
return 0;
return static_cast<int>(internal::isalnum(static_cast<char>(c)));
}
>From aee95dcfaa4d6230260d04714a94ddd9d9007024 Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Thu, 20 Aug 2026 11:26:35 +0000
Subject: [PATCH 6/6] [libc][ci] Add artifact upload and repository guards to
post-commit coverage bot
---
.github/workflows/libc-post-commit-coverage.yml | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/libc-post-commit-coverage.yml b/.github/workflows/libc-post-commit-coverage.yml
index 5c47ecfbeb163..b59f014455e80 100644
--- a/.github/workflows/libc-post-commit-coverage.yml
+++ b/.github/workflows/libc-post-commit-coverage.yml
@@ -11,10 +11,11 @@ on:
# Allow manual on-demand execution from GitHub Actions UI
workflow_dispatch:
- # Trigger on pushes to main / post-commit-bot
+ # Trigger on pushes to main / libc-coverage-ci-bots / post-commit-bot
push:
branches:
- main
+ - libc-coverage-ci-bots
- post-commit-bot
paths:
- 'libc/**'
@@ -127,7 +128,16 @@ jobs:
python3 libc/utils/coverage/full_report.py coverage.json "$COMMIT_SHA" "$BRANCH_REF" >> $GITHUB_STEP_SUMMARY
echo "[LOG] Post-commit summary report written to GITHUB_STEP_SUMMARY."
+ - name: Upload HTML Coverage Artifact
+ uses: actions/upload-artifact at v4
+ with:
+ name: libc-coverage-html-report
+ path: ./coverage_html
+ retention-days: 14
+
- name: Deploy Coverage Dashboard to GitHub Pages
+ if: github.repository != 'llvm/llvm-project'
+ continue-on-error: true
uses: peaceiris/actions-gh-pages at v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
More information about the libc-commits
mailing list