[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 01:26:35 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/3] [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/3] [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/3] [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
More information about the libc-commits
mailing list