[libc-commits] [libc] [llvm] [libc][WIP] Implement native code coverage pipeline (PR #210046)
Tapiwa Gonga via libc-commits
libc-commits at lists.llvm.org
Thu Jul 16 05:30:23 PDT 2026
https://github.com/tapiwagonga created https://github.com/llvm/llvm-project/pull/210046
This Draft PR extracts code coverage metrics from LLVM libc without circular I/O dependencies. `LibcTestMain.cpp` is refactored to use direct Linux syscalls (`SYS_mmap`, `SYS_openat`, `SYS_write`) for writing coverage buffers, bypassing `compiler-rt` standard library I/O. Environment variable parsing is removed, so test binaries now dump `<pid>.profraw` files directly into their execution directories. The `coverage_report.py` script uses recursive globbing across the build tree to locate all profile data. Finally, the `--show-mcdc-summary` flag and path filters (`libc/src`, `libc/include`, `libc/hdr`) are added to isolate MC/DC metrics for core implementations.
>From ca7a1603b98f5599ef739426d8f600e81cf91378 Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Thu, 16 Jul 2026 11:18:48 +0000
Subject: [PATCH] [libc] Implement native coverage extraction and recursive
reporting pipeline
---
.github/workflows/libc-coverage.yml | 64 ++
coverage_pipeline.patch | 849 +++++++++++++++++++++
libc/cmake/modules/LLVMLibCTestRules.cmake | 27 +-
libc/test/UnitTest/LibcTestMain.cpp | 91 ++-
libc/utils/coverage_report.py | 103 +++
5 files changed, 1117 insertions(+), 17 deletions(-)
create mode 100644 .github/workflows/libc-coverage.yml
create mode 100644 coverage_pipeline.patch
create mode 100644 libc/utils/coverage_report.py
diff --git a/.github/workflows/libc-coverage.yml b/.github/workflows/libc-coverage.yml
new file mode 100644
index 0000000000000..2ba9e953a101f
--- /dev/null
+++ b/.github/workflows/libc-coverage.yml
@@ -0,0 +1,64 @@
+name: 'LLVM Libc Code Coverage Pipeline'
+
+on:
+ push:
+ branches:
+ - main
+ - test-libc-coverage-bot
+ paths:
+ - 'libc/**'
+ - '.github/workflows/libc-coverage.yml'
+ pull_request:
+ branches:
+ - main
+ - test-libc-coverage-bot
+ paths:
+ - 'libc/**'
+ - '.github/workflows/libc-coverage.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ BUILD_DIR: 'build-cov'
+ LLVM_VERSION: '19'
+ CLANG_CC: 'clang-19'
+ CLANG_CXX: 'clang++-19'
+
+jobs:
+ run-coverage:
+ name: 'Generate and Display Code Coverage'
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: 'Checkout Repository'
+ uses: actions/checkout at v4
+
+ - name: 'Install System Dependencies'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ clang-${{ env.LLVM_VERSION }} \
+ llvm-${{ env.LLVM_VERSION }} \
+ lld-${{ env.LLVM_VERSION }} \
+ ninja-build
+
+ - name: 'Configure CMake Build System'
+ run: |
+ cmake -S llvm -B ${{ env.BUILD_DIR }} -G Ninja \
+ -DLLVM_ENABLE_PROJECTS="libc" \
+ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
+ -DLLVM_USE_LINKER=lld \
+ -DCMAKE_C_COMPILER=${{ env.CLANG_CC }} \
+ -DCMAKE_CXX_COMPILER=${{ env.CLANG_CXX }} \
+ -DLLVM_LIBC_ENABLE_LINTING=OFF \
+ -DLLVM_LIBC_ENABLE_COVERAGE=ON
+
+ - name: 'Execute Instrumented Test Suite'
+ run: |
+ ninja -C ${{ env.BUILD_DIR }} check-libc || true
+
+ - name: 'Aggregate Profiles and Display Coverage Report'
+ run: |
+ ninja -C ${{ env.BUILD_DIR }} libc-coverage
diff --git a/coverage_pipeline.patch b/coverage_pipeline.patch
new file mode 100644
index 0000000000000..14718d14fcf67
--- /dev/null
+++ b/coverage_pipeline.patch
@@ -0,0 +1,849 @@
+diff --git a/.ci/check_line_coverage.py b/.ci/check_line_coverage.py
+deleted file mode 100644
+index f59024074..000000000
+--- a/.ci/check_line_coverage.py
++++ /dev/null
+@@ -1,207 +0,0 @@
+-#!/usr/bin/env python3
+-import os
+-import json
+-import sys
+-
+-def main():
+- # 1. Check if we are running in a GitHub Actions environment
+- event_path = os.environ.get('GITHUB_EVENT_PATH')
+- event_name = os.environ.get('GITHUB_EVENT_NAME', 'push')
+-
+- if not event_path or not os.path.exists(event_path):
+- print("Error: GITHUB_EVENT_PATH not set or file does not exist.")
+- sys.exit(1)
+-
+- print(f"[*] Detected GitHub Event: {event_name}")
+-
+- with open(event_path, 'r') as f:
+- event_payload = json.load(f)
+-
+- # 2. Determine Diff Boundaries and API Endpoints
+- base_sha = None
+- head_sha = None
+- api_endpoint = None
+-
+- repo_name = event_payload.get('repository', {}).get('full_name', 'unknown/repo')
+-
+- if event_name == 'push':
+- base_sha = event_payload.get('before')
+- head_sha = event_payload.get('after')
+-
+- # Handle new branch creation where base_sha is 000000...
+- if base_sha == '0000000000000000000000000000000000000000':
+- base_sha = f"{head_sha}^1"
+-
+- api_endpoint = f"/repos/{repo_name}/commits/{head_sha}/comments"
+- print(f"[*] Post-Commit Context Detected.")
+- print(f"[*] Diff Boundaries: {base_sha} ... {head_sha}")
+- print(f"[*] Target API Endpoint: {api_endpoint}")
+-
+- elif event_name == 'pull_request':
+- pr_number = event_payload.get('number')
+- base_sha = event_payload.get('pull_request', {}).get('base', {}).get('sha')
+- head_sha = event_payload.get('pull_request', {}).get('head', {}).get('sha')
+- api_endpoint = f"/repos/{repo_name}/issues/{pr_number}/comments"
+- print(f"[*] Pre-Commit (PR) Context Detected.")
+- print(f"[*] Diff Boundaries: {base_sha} ... {head_sha}")
+- print(f"[*] Target API Endpoint: {api_endpoint}")
+-
+- else:
+- print(f"Unsupported event type: {event_name}")
+- sys.exit(1)
+-
+- if not base_sha or not head_sha:
+- print("Error: Could not resolve base and head SHAs from the event payload.")
+- sys.exit(1)
+-
+- import subprocess
+- import argparse
+-
+- parser = argparse.ArgumentParser()
+- parser.add_argument('--coverage-json', required=True)
+- args = parser.parse_args()
+-
+- # 3. Formulate the Git Command
+- import re
+- git_diff_cmd = ['git', 'diff', f'{base_sha}...{head_sha}', '--unified=0']
+-
+- modified_lines = {}
+- try:
+- diff_output = subprocess.check_output(git_diff_cmd, text=True)
+- current_file = None
+- for line in diff_output.splitlines():
+- if line.startswith('+++ b/'):
+- current_file = line[6:]
+- modified_lines[current_file] = set()
+- elif line.startswith('@@') and current_file:
+- m = re.search(r'\+([0-9]+)(?:,([0-9]+))?', line)
+- if m:
+- start = int(m.group(1))
+- length = int(m.group(2)) if m.group(2) else 1
+- for i in range(start, start + length):
+- modified_lines[current_file].add(i)
+- except subprocess.CalledProcessError as e:
+- print(f"Error running git diff: {e}")
+- sys.exit(1)
+-
+- print(f"[*] Modified files in this commit: {list(modified_lines.keys())}")
+-
+- # 4. Parse Coverage JSON
+- print(f"[*] Loading coverage data from {args.coverage_json}...")
+- try:
+- with open(args.coverage_json, 'r') as f:
+- cov_data = json.load(f)
+- except Exception as e:
+- print(f"Error loading coverage JSON: {e}")
+- sys.exit(1)
+-
+- # 5. Evaluate Functions
+- print("\n" + "="*50)
+- print("[POST-COMMIT BOT] COVERAGE EVALUATION")
+- print("="*50)
+-
+- issues_dict = {}
+- functions = cov_data.get('data', [{}])[0].get('functions', [])
+-
+- for func in functions:
+- func_filenames = func.get('filenames', [])
+- if not func_filenames:
+- continue
+-
+- is_modified = False
+- modified_file_match = None
+- absolute_file_path = None
+-
+- for file_path in func_filenames:
+- for mf in modified_lines.keys():
+- if file_path.endswith(mf):
+- is_modified = True
+- modified_file_match = mf
+- absolute_file_path = file_path
+- break
+- if is_modified:
+- break
+-
+- if is_modified:
+- regions = func.get('regions', [])
+- if not regions:
+- continue
+-
+- func_start = regions[0][0]
+- func_end = regions[-1][2]
+-
+- # Scope Bleed Fix: Bounding box intersection check
+- intersect = any(line >= func_start and line <= func_end for line in modified_lines[modified_file_match])
+- if not intersect:
+- continue
+-
+- func_name = func.get('name', 'Unknown')
+- branches = func.get('branches', [])
+- mcdc_records = func.get('mcdc_records', [])
+-
+- uncovered_regions = [r for r in regions if r[4] == 0 and r[7] == 0]
+- uncovered_branches = [b for b in branches if b[4] == 0 or b[5] == 0]
+-
+- if uncovered_regions or uncovered_branches or mcdc_records:
+- # Template Deduplication Fix
+- issue_key = (modified_file_match, func_start, func_end)
+- if issue_key not in issues_dict:
+- issues_dict[issue_key] = {
+- 'file': absolute_file_path,
+- 'func_names': set([func_name]),
+- 'uncovered_blocks': len(uncovered_regions),
+- 'uncovered_branches': len(uncovered_branches)
+- }
+- else:
+- issues_dict[issue_key]['func_names'].add(func_name)
+-
+- # API Limit Fix: Output generation and truncation
+- output_lines = []
+- for (mf, start, end), data in issues_dict.items():
+- output_lines.append(f"\n[WARNING] Coverage Issue Detected in Modified File:")
+- output_lines.append(f" File: {data['file']}")
+- output_lines.append(f" Function(s):")
+- for fn in sorted(data['func_names']):
+- output_lines.append(f" - {fn}")
+- output_lines.append(f" Details: {data['uncovered_blocks']} uncovered blocks, {data['uncovered_branches']} uncovered branches.")
+-
+- final_output = "\n".join(output_lines)
+- if len(final_output) > 60000:
+- final_output = final_output[:60000] + "\n\n[WARNING: Output truncated due to GitHub API limits]"
+-
+- if not issues_dict:
+- print("\n[SUCCESS] All modified functions have 100% coverage. No action needed.")
+- sys.exit(0)
+- else:
+- print(final_output)
+- print(f"\n[*] Coverage evaluation complete. Detected {len(issues_dict)} function blocks with regressions.")
+-
+- # Post the comment directly using GitHub API
+- import urllib.request
+- github_token = os.environ.get('GITHUB_TOKEN')
+- if github_token and api_endpoint:
+- print("[*] Posting review comment to GitHub...")
+- url = f"https://api.github.com{api_endpoint}"
+- headers = {
+- "Authorization": f"Bearer {github_token}",
+- "Accept": "application/vnd.github.v3+json",
+- "Content-Type": "application/json"
+- }
+- # Wrap final_output in standard markdown formatting
+- markdown_body = f"### ⚠️ Code Coverage Regression Detected\n\n```text\n{final_output}\n```"
+- data = json.dumps({"body": markdown_body}).encode('utf-8')
+-
+- try:
+- req = urllib.request.Request(url, data=data, headers=headers, method='POST')
+- with urllib.request.urlopen(req) as response:
+- print(f"[*] Successfully posted comment. HTTP Status: {response.status}")
+- except Exception as e:
+- print(f"[ERROR] Failed to post comment to GitHub API: {e}")
+- else:
+- print("[*] GITHUB_TOKEN not found or api_endpoint missing. Skipping API POST.")
+-
+- print(f"\n[FAIL] Failing CI pipeline due to {len(issues_dict)} coverage regressions.")
+- sys.exit(1)
+-
+-if __name__ == '__main__':
+- main()
+diff --git a/.ci/coverage_post_process.sh b/.ci/coverage_post_process.sh
+deleted file mode 100755
+index 748148e7e..000000000
+--- a/.ci/coverage_post_process.sh
++++ /dev/null
+@@ -1,63 +0,0 @@
+-#!/usr/bin/env bash
+-#===-- coverage_post_process.sh -------------------------------------------===#
+-#
+-# 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
+-#
+-#===----------------------------------------------------------------------===#
+-
+-set -euo pipefail
+-
+-# Find profile data tools
+-BUILD_DIR="${BUILD_DIR:-build}"
+-PROFDATA=$(command -v ./${BUILD_DIR}/bin/llvm-profdata || command -v llvm-profdata-19 || command -v llvm-profdata)
+-COV=$(command -v ./${BUILD_DIR}/bin/llvm-cov || command -v llvm-cov-19 || command -v llvm-cov)
+-
+-COVERAGE_BINARY_PATH="${COVERAGE_BINARY_PATH:-${BUILD_DIR}/projects/libc/libllvm-libc.a}"
+-
+-mkdir -p ${BUILD_DIR}/coverage-results
+-shopt -s globstar nullglob
+-profraw_files=(${BUILD_DIR}/**/*.profraw)
+-
+-if [ ${#profraw_files[@]} -eq 0 ]; then
+- echo "WARNING: No .profraw files generated across build subdirectories! Skipping profile merge."
+- exit 0
+-fi
+-
+-# Merge the raw profiles
+-$PROFDATA merge -sparse "${profraw_files[@]}" -o ${BUILD_DIR}/coverage-results/merged.profdata
+-
+-# Clean up raw profiles to conserve disk space
+-rm -f "${profraw_files[@]}"
+-
+-if [ ! -f ${BUILD_DIR}/coverage-results/merged.profdata ]; then
+- exit 0
+-fi
+-
+-test_binaries=($(find ${BUILD_DIR}/libc/test -type f -executable -name "*.__build__" 2>/dev/null || true))
+-
+-if [ ${#test_binaries[@]} -eq 0 ]; then
+- test_binaries=(${COVERAGE_BINARY_PATH})
+-fi
+-
+-primary_binary=${test_binaries[0]}
+-object_args=""
+-for ((i=1; i<${#test_binaries[@]}; i++)); do
+- object_args+=" -object=${test_binaries[$i]}"
+-done
+-
+-# Write textual summary to GitHub Actions UI
+-if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
+- echo "### Coverage & MC/DC Summary" >> $GITHUB_STEP_SUMMARY
+- echo '```text' >> $GITHUB_STEP_SUMMARY
+- $COV report ${primary_binary} ${object_args} \
+- -instr-profile=${BUILD_DIR}/coverage-results/merged.profdata \
+- --show-mcdc >> $GITHUB_STEP_SUMMARY
+- echo '```' >> $GITHUB_STEP_SUMMARY
+-fi
+-
+-# Export JSON for the line coverage verification script
+-$COV export ${primary_binary} ${object_args} \
+- -instr-profile=${BUILD_DIR}/coverage-results/merged.profdata \
+- > ${BUILD_DIR}/coverage-results/coverage.json
+diff --git a/.ci/run_coverage_pipeline.sh b/.ci/run_coverage_pipeline.sh
+deleted file mode 100755
+index b667a895b..000000000
+--- a/.ci/run_coverage_pipeline.sh
++++ /dev/null
+@@ -1,54 +0,0 @@
+-#!/usr/bin/env bash
+-#===-- run_coverage_pipeline.sh -------------------------------------------===#
+-#
+-# 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
+-#
+-#===----------------------------------------------------------------------===#
+-
+-set -uo pipefail
+-
+-BUILD_DIR="${BUILD_DIR:-build-cov}"
+-echo "================================================================="
+-echo "Step 1: Executing Test Suite"
+-echo "================================================================="
+-
+-# Temporarily disable 'Exit on Error' so we can capture test failures
+-# but still continue to run the coverage extraction.
+-set +e
+-ninja -C "${BUILD_DIR}" check-libc
+-TEST_EXIT_CODE=$?
+-set -e
+-
+-echo ""
+-echo "================================================================="
+-echo "Step 2: Aggregating Profile Data"
+-echo "================================================================="
+-BUILD_DIR="${BUILD_DIR}" bash .ci/coverage_post_process.sh
+-
+-echo ""
+-echo "================================================================="
+-echo "Step 3: Verifying Deterministic Line Coverage"
+-echo "================================================================="
+-GITHUB_EVENT_PATH="${GITHUB_EVENT_PATH:-push_event.json}" \
+-GITHUB_EVENT_NAME="${GITHUB_EVENT_NAME:-push}" \
+-python3 .ci/check_line_coverage.py --coverage-json "${BUILD_DIR}/coverage-results/coverage.json"
+-REVIEW_EXIT_CODE=$?
+-
+-echo ""
+-echo "================================================================="
+-echo "Pipeline Final Status"
+-echo "================================================================="
+-if [ $TEST_EXIT_CODE -ne 0 ]; then
+- echo "FATAL: Pipeline failed. C++ Unit Tests exited with code ${TEST_EXIT_CODE}."
+- exit $TEST_EXIT_CODE
+-fi
+-
+-if [ $REVIEW_EXIT_CODE -ne 0 ]; then
+- echo "FATAL: Pipeline failed. Coverage regressions were detected."
+- exit $REVIEW_EXIT_CODE
+-fi
+-
+-echo "SUCCESS: All tests passed and no coverage regressions found."
+-exit 0
+diff --git a/.ci/show_coverage_report.sh b/.ci/show_coverage_report.sh
+deleted file mode 100755
+index 79fbbe10b..000000000
+--- a/.ci/show_coverage_report.sh
++++ /dev/null
+@@ -1,62 +0,0 @@
+-#!/usr/bin/env bash
+-#===-- show_coverage_report.sh --------------------------------------------===#
+-#
+-# 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
+-#
+-#===----------------------------------------------------------------------===#
+-
+-set -euo pipefail
+-
+-BUILD_DIR="${BUILD_DIR:-build-cov}"
+-COV=$(command -v ./${BUILD_DIR}/bin/llvm-cov || command -v llvm-cov-19 || command -v llvm-cov)
+-
+-if [ ! -f "${BUILD_DIR}/coverage-results/merged.profdata" ]; then
+- echo "Error: Profile data not found at ${BUILD_DIR}/coverage-results/merged.profdata"
+- echo "Please run the coverage pipeline first."
+- exit 1
+-fi
+-
+-test_binaries=($(find "${BUILD_DIR}/libc/test" -type f -executable -name "*.__build__" 2>/dev/null || true))
+-if [ ${#test_binaries[@]} -eq 0 ]; then
+- echo "Error: No test binaries found in ${BUILD_DIR}/libc/test"
+- exit 1
+-fi
+-
+-primary_binary=${test_binaries[0]}
+-object_args=""
+-for ((i=1; i<${#test_binaries[@]}; i++)); do
+- object_args+=" -object=${test_binaries[$i]}"
+-done
+-
+-# We support multiple modular view formats
+-FORMAT="${1:-text}"
+-
+-if [ "$FORMAT" == "html" ]; then
+- echo "[*] Generating HTML report across ${#test_binaries[@]} test binaries..."
+- mkdir -p "${BUILD_DIR}/coverage-html"
+- $COV show ${primary_binary} ${object_args} \
+- -instr-profile="${BUILD_DIR}/coverage-results/merged.profdata" \
+- -format=html \
+- -output-dir="${BUILD_DIR}/coverage-html"
+- echo "[*] HTML report generated at: ${BUILD_DIR}/coverage-html/index.html"
+-
+-elif [ "$FORMAT" == "summary" ]; then
+- echo "[*] Compiling Summary report..."
+- # By ignoring all filenames, we force the table to only print the headers and the TOTAL row
+- $COV report ${primary_binary} ${object_args} \
+- -instr-profile="${BUILD_DIR}/coverage-results/merged.profdata" \
+- --ignore-filename-regex=".*"
+-
+-elif [ "$FORMAT" == "less" ]; then
+- echo "[*] Opening full table in interactive pager. Use arrow keys to scroll, press 'q' to quit."
+- $COV report ${primary_binary} ${object_args} \
+- -instr-profile="${BUILD_DIR}/coverage-results/merged.profdata" | less -S
+-
+-else
+- # Textual Terminal Output
+- echo "[*] Dumping full raw table to stdout..."
+- $COV report ${primary_binary} ${object_args} \
+- -instr-profile="${BUILD_DIR}/coverage-results/merged.profdata"
+-fi
+diff --git a/.github/workflows/libc-coverage.yml b/.github/workflows/libc-coverage.yml
+index dbd42ea4d..2ba9e953a 100644
+--- a/.github/workflows/libc-coverage.yml
++++ b/.github/workflows/libc-coverage.yml
+@@ -5,10 +5,20 @@ on:
+ branches:
+ - main
+ - test-libc-coverage-bot
++ paths:
++ - 'libc/**'
++ - '.github/workflows/libc-coverage.yml'
+ pull_request:
+ branches:
+ - main
+ - test-libc-coverage-bot
++ paths:
++ - 'libc/**'
++ - '.github/workflows/libc-coverage.yml'
++
++concurrency:
++ group: ${{ github.workflow }}-${{ github.ref }}
++ cancel-in-progress: true
+
+ env:
+ BUILD_DIR: 'build-cov'
+@@ -31,13 +41,15 @@ jobs:
+ sudo apt-get install -y \
+ clang-${{ env.LLVM_VERSION }} \
+ llvm-${{ env.LLVM_VERSION }} \
++ lld-${{ env.LLVM_VERSION }} \
+ ninja-build
+
+ - name: 'Configure CMake Build System'
+ run: |
+ cmake -S llvm -B ${{ env.BUILD_DIR }} -G Ninja \
+ -DLLVM_ENABLE_PROJECTS="libc" \
+- -DCMAKE_BUILD_TYPE=Debug \
++ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
++ -DLLVM_USE_LINKER=lld \
+ -DCMAKE_C_COMPILER=${{ env.CLANG_CC }} \
+ -DCMAKE_CXX_COMPILER=${{ env.CLANG_CXX }} \
+ -DLLVM_LIBC_ENABLE_LINTING=OFF \
+diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
+index 53481a4b3..2153bdedd 100644
+--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
++++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
+@@ -364,13 +364,7 @@ function(create_libc_unittest fq_target_name)
+ target_link_libraries(${fq_build_target_name} PRIVATE ${link_libraries})
+
+ if(NOT LIBC_UNITTEST_NO_RUN_POSTBUILD)
+- if(LLVM_LIBC_ENABLE_COVERAGE)
+- set(test_cmd ${Python3_EXECUTABLE} ${LIBC_SOURCE_DIR}/utils/extract_coverage.py
+- --out ${CMAKE_CURRENT_BINARY_DIR}/${fq_target_name}.profraw
+- ${LIBC_UNITTEST_ENV} ${CMAKE_CROSSCOMPILING_EMULATOR} ${CMAKE_CURRENT_BINARY_DIR}/${fq_build_target_name})
+- else()
+- set(test_cmd ${LIBC_UNITTEST_ENV} ${CMAKE_CROSSCOMPILING_EMULATOR} ${CMAKE_CURRENT_BINARY_DIR}/${fq_build_target_name})
+- endif()
++ set(test_cmd ${LIBC_UNITTEST_ENV} ${CMAKE_CROSSCOMPILING_EMULATOR} ${CMAKE_CURRENT_BINARY_DIR}/${fq_build_target_name})
+ add_custom_target(
+ ${fq_target_name}
+ COMMAND ${test_cmd}
+@@ -675,23 +669,12 @@ function(add_integration_test test_name)
+ # command also will not run. So, we use this list and tell `add_custom_target`
+ # to expand the list (by including the option COMMAND_EXPAND_LISTS). This
+ # makes `add_custom_target` construct the correct command and execute it.
+- if(LLVM_LIBC_ENABLE_COVERAGE)
+- set(test_cmd
+- ${Python3_EXECUTABLE} ${LIBC_SOURCE_DIR}/utils/extract_coverage.py
+- --out ${CMAKE_CURRENT_BINARY_DIR}/${fq_target_name}.profraw
+- ${INTEGRATION_TEST_ENV}
+- $<$<BOOL:${LIBC_TARGET_ARCHITECTURE_IS_NVPTX}>:LIBOMPTARGET_STACK_SIZE=3072>
+- ${CMAKE_CROSSCOMPILING_EMULATOR}
+- ${INTEGRATION_TEST_LOADER_ARGS}
+- $<TARGET_FILE:${fq_build_target_name}> ${INTEGRATION_TEST_ARGS})
+- else()
+- set(test_cmd
++ set(test_cmd
+ ${INTEGRATION_TEST_ENV}
+ $<$<BOOL:${LIBC_TARGET_ARCHITECTURE_IS_NVPTX}>:LIBOMPTARGET_STACK_SIZE=3072>
+ ${CMAKE_CROSSCOMPILING_EMULATOR}
+ ${INTEGRATION_TEST_LOADER_ARGS}
+ $<TARGET_FILE:${fq_build_target_name}> ${INTEGRATION_TEST_ARGS})
+- endif()
+ # Generate a sidecar .params file alongside the executable for any test that
+ # requires specific command-line arguments or environment variables. The
+ # LibcTest lit format reads this file at test time. Format: one arg per line,
+@@ -936,22 +919,12 @@ function(add_libc_hermetic test_name)
+ # In the form of "<command> binary=@BINARY@", e.g. "qemu-system-arm -loader$<COMMA>file=@BINARY@"
+ string(REPLACE "@BINARY@" "$<TARGET_FILE:${fq_build_target_name}>" test_cmd_parsed ${LIBC_TEST_CMD})
+ string(REPLACE " " ";" test_cmd "${test_cmd_parsed}")
+- if(LLVM_LIBC_ENABLE_COVERAGE)
+- set(test_cmd ${Python3_EXECUTABLE} ${LIBC_SOURCE_DIR}/utils/extract_coverage.py --out ${CMAKE_CURRENT_BINARY_DIR}/${fq_target_name}.profraw ${test_cmd})
+- endif()
++
+ else()
+- if(LLVM_LIBC_ENABLE_COVERAGE)
+- set(test_cmd ${Python3_EXECUTABLE} ${LIBC_SOURCE_DIR}/utils/extract_coverage.py --out ${CMAKE_CURRENT_BINARY_DIR}/${fq_target_name}.profraw
+- ${HERMETIC_TEST_ENV}
+- $<$<BOOL:${LIBC_TARGET_ARCHITECTURE_IS_NVPTX}>:LIBOMPTARGET_STACK_SIZE=3072>
+- ${CMAKE_CROSSCOMPILING_EMULATOR} ${HERMETIC_TEST_LOADER_ARGS}
+- $<TARGET_FILE:${fq_build_target_name}> ${HERMETIC_TEST_ARGS})
+- else()
+ set(test_cmd ${HERMETIC_TEST_ENV}
+ $<$<BOOL:${LIBC_TARGET_ARCHITECTURE_IS_NVPTX}>:LIBOMPTARGET_STACK_SIZE=3072>
+ ${CMAKE_CROSSCOMPILING_EMULATOR} ${HERMETIC_TEST_LOADER_ARGS}
+ $<TARGET_FILE:${fq_build_target_name}> ${HERMETIC_TEST_ARGS})
+- endif()
+ endif()
+
+ set(_params_content "")
+diff --git a/libc/test/UnitTest/LibcTestMain.cpp b/libc/test/UnitTest/LibcTestMain.cpp
+index 1c9e69e73..00f747d1f 100644
+--- a/libc/test/UnitTest/LibcTestMain.cpp
++++ b/libc/test/UnitTest/LibcTestMain.cpp
+@@ -45,6 +45,7 @@ TestOptions parseOptions(int argc, char **argv) {
+
+ #if defined(__linux__)
+ #include "src/__support/OSUtil/syscall.h"
++#include <errno.h>
+ #include <fcntl.h>
+ #include <sys/mman.h>
+ #include <sys/syscall.h>
+@@ -69,131 +70,50 @@ void write_raw_profile() {
+ if (required_size == 0)
+ return;
+
+- // Allocate buffer via mmap to avoid depending on libc malloc.
+-#ifdef SYS_mmap
+- long mmap_syscall = SYS_mmap;
+-#elif defined(SYS_mmap2)
+- long mmap_syscall = SYS_mmap2;
+-#else
+-#error "System does not support SYS_mmap or SYS_mmap2."
+-#endif
+ long mmap_ret = LIBC_NAMESPACE::syscall_impl<long>(
+- mmap_syscall, nullptr, required_size, PROT_READ | PROT_WRITE,
++ SYS_mmap, nullptr, required_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+-
+ if (mmap_ret < 0 && mmap_ret > -4096)
+ return;
+ char *profile_buffer = reinterpret_cast<char *>(mmap_ret);
+
+ if (__llvm_profile_write_buffer(profile_buffer) != 0) {
+- LIBC_NAMESPACE::syscall_impl<long>(SYS_munmap, profile_buffer,
+- required_size);
++ LIBC_NAMESPACE::syscall_impl<long>(SYS_munmap, profile_buffer, required_size);
+ return;
+ }
+
+- char filename[256];
+- int idx = 0;
+- bool has_env_file = false;
+-
+- long pid = LIBC_NAMESPACE::syscall_impl<long>(SYS_getpid);
+- if (pid <= 0)
+- pid = 1;
+-
+- char pid_str[32];
+- int pid_len = 0;
+- long temp_pid = pid;
+- while (temp_pid > 0) {
+- pid_str[pid_len++] = (char)('0' + (temp_pid % 10));
+- temp_pid /= 10;
+- }
+- if (pid_len == 0)
+- pid_str[pid_len++] = '0';
+-
+- // Parse LLVM_PROFILE_FILE environment variable manually.
+- if (LIBC_NAMESPACE::testing::envp) {
+- for (char **env = LIBC_NAMESPACE::testing::envp; *env != nullptr; ++env) {
+- const char *str = *env;
+- const char *prefix = "LLVM_PROFILE_FILE=";
+- int i = 0;
+- while (prefix[i] != '\0' && str[i] == prefix[i])
+- i++;
+- if (prefix[i] == '\0') {
+- const char *val = &str[i];
+- int val_idx = 0;
+- while (val[val_idx] != '\0' && idx < 200) {
+- if (val[val_idx] == '%' && val[val_idx + 1] == 'm') {
+- for (int j = pid_len - 1; j >= 0; --j)
+- filename[idx++] = pid_str[j];
+- val_idx += 2;
+- } else {
+- filename[idx++] = val[val_idx++];
+- }
+- }
+- filename[idx] = '\0';
+- has_env_file = true;
+- break;
+- }
+- }
+- }
+-
+- // Fallback to a unique filename if no environment variable is set.
+- if (!has_env_file) {
+- const char *default_prefix = "default_";
+- for (int i = 0; default_prefix[i] != '\0'; ++i)
+- filename[idx++] = default_prefix[i];
+-
+- for (int i = pid_len - 1; i >= 0; --i)
+- filename[idx++] = pid_str[i];
++ // Create a minimal filename: libc_cov_<pid>.profraw
++ long pid = LIBC_NAMESPACE::syscall_impl<long>(SYS_getpid);
++ if (pid <= 0) pid = 1;
++
++ char filename[64] = "libc_cov_";
++ int idx = 9;
++
++ long temp = pid;
++ char digits[32];
++ int d_len = 0;
++ while (temp > 0) { digits[d_len++] = static_cast<char>('0' + (temp % 10)); temp /= 10; }
++ if (d_len == 0) digits[d_len++] = '0';
++ for (int i = d_len - 1; i >= 0; --i) filename[idx++] = digits[i];
++
++ const char *suffix = ".profraw";
++ for (int i = 0; suffix[i] != '\0'; ++i) filename[idx++] = suffix[i];
++ filename[idx] = '\0';
+
+- filename[idx++] = '_';
+-
+- struct timespec ts;
+- LIBC_NAMESPACE::syscall_impl<long>(SYS_clock_gettime, CLOCK_MONOTONIC, &ts);
+- long temp_nsec = ts.tv_nsec;
+- if (temp_nsec < 0)
+- temp_nsec = -temp_nsec;
+-
+- char nsec_str[32];
+- int nsec_len = 0;
+- while (temp_nsec > 0) {
+- nsec_str[nsec_len++] = (char)('0' + (temp_nsec % 10));
+- temp_nsec /= 10;
+- }
+- if (nsec_len == 0)
+- nsec_str[nsec_len++] = '0';
+- for (int i = nsec_len - 1; i >= 0; --i)
+- filename[idx++] = nsec_str[i];
+-
+- const char *suffix = ".profraw";
+- for (int i = 0; suffix[i] != '\0'; ++i)
+- filename[idx++] = suffix[i];
+- filename[idx] = '\0';
+- }
+-
+- // Write profile data using raw OS syscalls to bypass libc I/O functions.
+ long fd = LIBC_NAMESPACE::syscall_impl<long>(
+ SYS_openat, AT_FDCWD, filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+- if (fd < 0) {
+- LIBC_NAMESPACE::syscall_impl<long>(SYS_munmap, profile_buffer, required_size);
+- return;
+- }
+-
+- uint64_t bytes_written = 0;
+- while (bytes_written < required_size) {
+- long ret = LIBC_NAMESPACE::syscall_impl<long>(
+- SYS_write, fd, profile_buffer + bytes_written,
+- required_size - bytes_written);
+- if (ret < 0) {
+- if (ret == -4) // EINTR retry
+- continue;
+- break;
++
++ if (fd >= 0) {
++ uint64_t bytes_written = 0;
++ while (bytes_written < required_size) {
++ long ret = LIBC_NAMESPACE::syscall_impl<long>(
++ SYS_write, fd, profile_buffer + bytes_written, required_size - bytes_written);
++ if (ret <= 0 && ret != -EINTR) break;
++ if (ret > 0) bytes_written += ret;
+ }
+- if (ret == 0)
+- break;
+- bytes_written += ret;
++ LIBC_NAMESPACE::syscall_impl<long>(SYS_close, fd);
+ }
+
+- LIBC_NAMESPACE::syscall_impl<long>(SYS_close, fd);
+ LIBC_NAMESPACE::syscall_impl<long>(SYS_munmap, profile_buffer, required_size);
+
+ // Clear the filename pattern to prevent compiler-rt from writing at exit.
+@@ -223,4 +143,3 @@ TEST_MAIN(int argc, char **argv, char **envp) {
+ write_raw_profile();
+ return result;
+ }
+-
+diff --git a/libc/utils/coverage_report.py b/libc/utils/coverage_report.py
+old mode 100755
+new mode 100644
+index 741f761ca..6f79f1e9a
+--- a/libc/utils/coverage_report.py
++++ b/libc/utils/coverage_report.py
+@@ -6,75 +6,98 @@
+ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+ #
+ #===----------------------------------------------------------------------===#
+-#
+-# Aggregates LLVM libc code coverage profiles and generates reports.
+-#
+-#===----------------------------------------------------------------------===#
+
+ import argparse
+ import glob
+ import os
++import shutil
+ import subprocess
+ import sys
+
+ def main():
+- parser = argparse.ArgumentParser(description="Generate libc coverage report")
+- parser.add_argument("--build-dir", required=True, help="Path to the build directory")
+- parser.add_argument("--llvm-tools-dir", default="", help="Path to LLVM tools")
++ parser = argparse.ArgumentParser(description="Generate libc code coverage report")
++ parser.add_argument("--build-dir", required=True, help="Path to the libc build directory")
++ parser.add_argument("--llvm-tools-dir", required=True, help="Path to the LLVM tools directory (llvm-profdata, llvm-cov)")
+ args = parser.parse_args()
+
+ build_dir = args.build_dir
+ tools_dir = args.llvm_tools_dir
+- profiles_dir = os.path.join(build_dir, "profiles")
++
++ profdata_tool = os.path.join(tools_dir, "llvm-profdata")
++ cov_tool = os.path.join(tools_dir, "llvm-cov")
++
++ if not os.path.isfile(profdata_tool):
++ profdata_tool = shutil.which("llvm-profdata-19") or shutil.which("llvm-profdata")
+
+- # 1. Find all profraw files
+- profraw_files = glob.glob(os.path.join(profiles_dir, "*.profraw"))
++ if not os.path.isfile(cov_tool):
++ cov_tool = shutil.which("llvm-cov-19") or shutil.which("llvm-cov")
++
++ if not profdata_tool or not cov_tool:
++ print(f"Error: Could not find llvm-profdata or llvm-cov in {tools_dir} or in PATH")
++ sys.exit(1)
++
++ # Find all .profraw files recursively throughout the build tree
++ # This ensures we catch profiles dumped in subdirectories if LLVM_PROFILE_FILE isn't strictly adhered to.
++ profraw_files = glob.glob(os.path.join(build_dir, "**", "*.profraw"), recursive=True)
++
+ if not profraw_files:
+- print(f"Error: No .profraw files found in {profiles_dir}. Did tests run with coverage enabled?", file=sys.stderr)
++ print(f"Error: No .profraw files found in {build_dir}. Did tests run with coverage enabled?")
+ sys.exit(1)
+-
+- # 2. Merge profraw files
+- # Find llvm-profdata
+- llvm_profdata = "llvm-profdata-19"
+- if tools_dir:
+- local_profdata = os.path.join(tools_dir, "llvm-profdata")
+- if os.path.exists(local_profdata):
+- llvm_profdata = local_profdata
+-
+- merged_profdata = os.path.join(build_dir, "merged.profdata")
+-
+- # Write paths to a list file to avoid ARG_MAX limits
++
++ # Prevent command line too long error by writing paths to a file
+ list_file = os.path.join(build_dir, "profraw_list.txt")
+ with open(list_file, "w") as f:
+- for pf in profraw_files:
+- f.write(f"{pf}\n")
+-
+- print(f"Merging {len(profraw_files)} profile files...")
+- subprocess.check_call([llvm_profdata, "merge", "-sparse", "-input-files=" + list_file, "-o", merged_profdata])
++ for p in profraw_files:
++ f.write(p + "\n")
++
++ merged_profdata = os.path.join(build_dir, "merged.profdata")
++
++ print(f"Merging {len(profraw_files)} profiles into {merged_profdata}...")
++ subprocess.check_call(
++ [profdata_tool, "merge", "-sparse", f"-input-files={list_file}", "-o", merged_profdata]
++ )
+
+- # 3. Find all test binaries
+- # We look for *.__build__ in the test directory
++ # Remove raw profiles after merge to save space and prevent inflation on next run
++ for p in profraw_files:
++ try:
++ os.remove(p)
++ except OSError:
++ pass
++ if os.path.exists(list_file):
++ os.remove(list_file)
++
+ test_dir = os.path.join(build_dir, "test")
+- # Python 3.5+ recursive glob
+- test_binaries = glob.glob(os.path.join(test_dir, "**", "*.__build__"), recursive=True)
++ test_binaries = []
++ for root, dirs, files in os.walk(test_dir):
++ for f in files:
++ if f.endswith(".__build__"):
++ test_binaries.append(os.path.join(root, f))
++
+ if not test_binaries:
+- print(f"Error: No test binaries found in {test_dir}.", file=sys.stderr)
++ print(f"Error: No test binaries found in {test_dir}")
+ sys.exit(1)
+-
+- # 4. Generate report
+- llvm_cov = "llvm-cov-19"
+- if tools_dir:
+- local_cov = os.path.join(tools_dir, "llvm-cov")
+- if os.path.exists(local_cov):
+- llvm_cov = local_cov
+-
+- cov_cmd = [llvm_cov, "report", test_binaries[0]]
++
++ # Use the first binary as the main object, the rest as -object arguments
++ cov_cmd = [
++ cov_tool, "report", test_binaries[0],
++ f"-instr-profile={merged_profdata}",
++ "--show-mcdc-summary"
++ ]
++
+ for tb in test_binaries[1:]:
+- cov_cmd.extend(["-object", tb])
+- cov_cmd.extend(["-instr-profile", merged_profdata])
+-
++ cov_cmd.append(f"-object={tb}")
++
++ # Add source path filtering so we only get coverage for core library implementation,
++ # rather than inflating coverage with test suite files.
++ workspace_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
++ cov_cmd.extend([
++ os.path.join(workspace_root, "src"),
++ os.path.join(workspace_root, "include"),
++ os.path.join(workspace_root, "hdr")
++ ])
++
+ print("Generating coverage report...")
+ subprocess.check_call(cov_cmd)
+-
++
+ if __name__ == "__main__":
+- sys.exit(main())
++ main()
diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
index b7d3a98059723..2153bdedd9f1d 100644
--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
+++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
@@ -28,10 +28,6 @@ function(_get_common_test_compile_options output_var c_test flags)
list(APPEND compile_options "-DLIBC_TEST_SKIP_DEATH_TESTS")
endif()
- if(CMAKE_CROSSCOMPILING_EMULATOR)
- list(APPEND compile_options "-DLIBC_TEST_UNDER_EMULATOR")
- endif()
-
if(LLVM_LIBC_COMPILER_IS_GCC_COMPATIBLE)
list(APPEND compile_options "-fpie")
@@ -368,9 +364,11 @@ function(create_libc_unittest fq_target_name)
target_link_libraries(${fq_build_target_name} PRIVATE ${link_libraries})
if(NOT LIBC_UNITTEST_NO_RUN_POSTBUILD)
+ set(test_cmd ${LIBC_UNITTEST_ENV} ${CMAKE_CROSSCOMPILING_EMULATOR} ${CMAKE_CURRENT_BINARY_DIR}/${fq_build_target_name})
add_custom_target(
${fq_target_name}
- COMMAND ${LIBC_UNITTEST_ENV} ${CMAKE_CROSSCOMPILING_EMULATOR} ${CMAKE_CURRENT_BINARY_DIR}/${fq_build_target_name}
+ COMMAND ${test_cmd}
+ COMMAND_EXPAND_LISTS
COMMENT "Running unit test ${fq_target_name}"
DEPENDS ${fq_build_target_name}
)
@@ -672,11 +670,11 @@ function(add_integration_test test_name)
# to expand the list (by including the option COMMAND_EXPAND_LISTS). This
# makes `add_custom_target` construct the correct command and execute it.
set(test_cmd
- ${INTEGRATION_TEST_ENV}
- $<$<BOOL:${LIBC_TARGET_ARCHITECTURE_IS_NVPTX}>:LIBOMPTARGET_STACK_SIZE=3072>
- ${CMAKE_CROSSCOMPILING_EMULATOR}
- ${INTEGRATION_TEST_LOADER_ARGS}
- $<TARGET_FILE:${fq_build_target_name}> ${INTEGRATION_TEST_ARGS})
+ ${INTEGRATION_TEST_ENV}
+ $<$<BOOL:${LIBC_TARGET_ARCHITECTURE_IS_NVPTX}>:LIBOMPTARGET_STACK_SIZE=3072>
+ ${CMAKE_CROSSCOMPILING_EMULATOR}
+ ${INTEGRATION_TEST_LOADER_ARGS}
+ $<TARGET_FILE:${fq_build_target_name}> ${INTEGRATION_TEST_ARGS})
# Generate a sidecar .params file alongside the executable for any test that
# requires specific command-line arguments or environment variables. The
# LibcTest lit format reads this file at test time. Format: one arg per line,
@@ -921,11 +919,12 @@ function(add_libc_hermetic test_name)
# In the form of "<command> binary=@BINARY@", e.g. "qemu-system-arm -loader$<COMMA>file=@BINARY@"
string(REPLACE "@BINARY@" "$<TARGET_FILE:${fq_build_target_name}>" test_cmd_parsed ${LIBC_TEST_CMD})
string(REPLACE " " ";" test_cmd "${test_cmd_parsed}")
+
else()
- set(test_cmd ${HERMETIC_TEST_ENV}
- $<$<BOOL:${LIBC_TARGET_ARCHITECTURE_IS_NVPTX}>:LIBOMPTARGET_STACK_SIZE=3072>
- ${CMAKE_CROSSCOMPILING_EMULATOR} ${HERMETIC_TEST_LOADER_ARGS}
- $<TARGET_FILE:${fq_build_target_name}> ${HERMETIC_TEST_ARGS})
+ set(test_cmd ${HERMETIC_TEST_ENV}
+ $<$<BOOL:${LIBC_TARGET_ARCHITECTURE_IS_NVPTX}>:LIBOMPTARGET_STACK_SIZE=3072>
+ ${CMAKE_CROSSCOMPILING_EMULATOR} ${HERMETIC_TEST_LOADER_ARGS}
+ $<TARGET_FILE:${fq_build_target_name}> ${HERMETIC_TEST_ARGS})
endif()
set(_params_content "")
diff --git a/libc/test/UnitTest/LibcTestMain.cpp b/libc/test/UnitTest/LibcTestMain.cpp
index c348d5ef1aa1b..00f747d1f6f1e 100644
--- a/libc/test/UnitTest/LibcTestMain.cpp
+++ b/libc/test/UnitTest/LibcTestMain.cpp
@@ -43,8 +43,90 @@ TestOptions parseOptions(int argc, char **argv) {
} // anonymous namespace
-// The C++ standard forbids declaring the main function with a linkage specifier
-// outisde of 'freestanding' mode, only define the linkage for hermetic tests.
+#if defined(__linux__)
+#include "src/__support/OSUtil/syscall.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <time.h>
+
+extern "C" {
+__attribute__((weak)) uint64_t __llvm_profile_get_size_for_buffer(void);
+__attribute__((weak)) int __llvm_profile_write_buffer(char *Buffer);
+__attribute__((weak)) void __llvm_profile_set_filename(const char *FilenamePat);
+
+// Override compiler-rt's weak filename symbol. This redirects the default
+// filename to /dev/null to silence the default dumper by default.
+char __llvm_profile_filename[] = "/dev/null";
+}
+
+namespace {
+void write_raw_profile() {
+ if (!__llvm_profile_get_size_for_buffer || !__llvm_profile_write_buffer)
+ return;
+
+ uint64_t required_size = __llvm_profile_get_size_for_buffer();
+ if (required_size == 0)
+ return;
+
+ long mmap_ret = LIBC_NAMESPACE::syscall_impl<long>(
+ SYS_mmap, nullptr, required_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (mmap_ret < 0 && mmap_ret > -4096)
+ return;
+ char *profile_buffer = reinterpret_cast<char *>(mmap_ret);
+
+ if (__llvm_profile_write_buffer(profile_buffer) != 0) {
+ LIBC_NAMESPACE::syscall_impl<long>(SYS_munmap, profile_buffer, required_size);
+ return;
+ }
+
+ // Create a minimal filename: libc_cov_<pid>.profraw
+ long pid = LIBC_NAMESPACE::syscall_impl<long>(SYS_getpid);
+ if (pid <= 0) pid = 1;
+
+ char filename[64] = "libc_cov_";
+ int idx = 9;
+
+ long temp = pid;
+ char digits[32];
+ int d_len = 0;
+ while (temp > 0) { digits[d_len++] = static_cast<char>('0' + (temp % 10)); temp /= 10; }
+ if (d_len == 0) digits[d_len++] = '0';
+ for (int i = d_len - 1; i >= 0; --i) filename[idx++] = digits[i];
+
+ const char *suffix = ".profraw";
+ for (int i = 0; suffix[i] != '\0'; ++i) filename[idx++] = suffix[i];
+ filename[idx] = '\0';
+
+ long fd = LIBC_NAMESPACE::syscall_impl<long>(
+ SYS_openat, AT_FDCWD, filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+
+ if (fd >= 0) {
+ uint64_t bytes_written = 0;
+ while (bytes_written < required_size) {
+ long ret = LIBC_NAMESPACE::syscall_impl<long>(
+ SYS_write, fd, profile_buffer + bytes_written, required_size - bytes_written);
+ if (ret <= 0 && ret != -EINTR) break;
+ if (ret > 0) bytes_written += ret;
+ }
+ LIBC_NAMESPACE::syscall_impl<long>(SYS_close, fd);
+ }
+
+ LIBC_NAMESPACE::syscall_impl<long>(SYS_munmap, profile_buffer, required_size);
+
+ // Clear the filename pattern to prevent compiler-rt from writing at exit.
+ if (__llvm_profile_set_filename)
+ __llvm_profile_set_filename("/dev/null");
+}
+} // anonymous namespace
+#else
+namespace {
+void write_raw_profile() {}
+} // anonymous namespace
+#endif
+
#if __STDC_HOSTED__
#define TEST_MAIN int main
#else
@@ -56,5 +138,8 @@ TEST_MAIN(int argc, char **argv, char **envp) {
LIBC_NAMESPACE::testing::argv = argv;
LIBC_NAMESPACE::testing::envp = envp;
- return LIBC_NAMESPACE::testing::Test::runTests(parseOptions(argc, argv));
+ int result =
+ LIBC_NAMESPACE::testing::Test::runTests(parseOptions(argc, argv));
+ write_raw_profile();
+ return result;
}
diff --git a/libc/utils/coverage_report.py b/libc/utils/coverage_report.py
new file mode 100644
index 0000000000000..6f79f1e9a9aab
--- /dev/null
+++ b/libc/utils/coverage_report.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+#===-- coverage_report.py ------------------------------------------------===#
+#
+# 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 glob
+import os
+import shutil
+import subprocess
+import sys
+
+def main():
+ parser = argparse.ArgumentParser(description="Generate libc code coverage report")
+ parser.add_argument("--build-dir", required=True, help="Path to the libc build directory")
+ parser.add_argument("--llvm-tools-dir", required=True, help="Path to the LLVM tools directory (llvm-profdata, llvm-cov)")
+ args = parser.parse_args()
+
+ build_dir = args.build_dir
+ tools_dir = args.llvm_tools_dir
+
+ profdata_tool = os.path.join(tools_dir, "llvm-profdata")
+ cov_tool = os.path.join(tools_dir, "llvm-cov")
+
+ if not os.path.isfile(profdata_tool):
+ profdata_tool = shutil.which("llvm-profdata-19") or shutil.which("llvm-profdata")
+
+ if not os.path.isfile(cov_tool):
+ cov_tool = shutil.which("llvm-cov-19") or shutil.which("llvm-cov")
+
+ if not profdata_tool or not cov_tool:
+ print(f"Error: Could not find llvm-profdata or llvm-cov in {tools_dir} or in PATH")
+ sys.exit(1)
+
+ # Find all .profraw files recursively throughout the build tree
+ # This ensures we catch profiles dumped in subdirectories if LLVM_PROFILE_FILE isn't strictly adhered to.
+ profraw_files = glob.glob(os.path.join(build_dir, "**", "*.profraw"), recursive=True)
+
+ if not profraw_files:
+ print(f"Error: No .profraw files found in {build_dir}. Did tests run with coverage enabled?")
+ sys.exit(1)
+
+ # Prevent command line too long error by writing paths to a file
+ list_file = os.path.join(build_dir, "profraw_list.txt")
+ with open(list_file, "w") as f:
+ for p in profraw_files:
+ f.write(p + "\n")
+
+ merged_profdata = os.path.join(build_dir, "merged.profdata")
+
+ print(f"Merging {len(profraw_files)} profiles into {merged_profdata}...")
+ subprocess.check_call(
+ [profdata_tool, "merge", "-sparse", f"-input-files={list_file}", "-o", merged_profdata]
+ )
+
+ # Remove raw profiles after merge to save space and prevent inflation on next run
+ for p in profraw_files:
+ try:
+ os.remove(p)
+ except OSError:
+ pass
+ if os.path.exists(list_file):
+ os.remove(list_file)
+
+ test_dir = os.path.join(build_dir, "test")
+ test_binaries = []
+ for root, dirs, files in os.walk(test_dir):
+ for f in files:
+ if f.endswith(".__build__"):
+ test_binaries.append(os.path.join(root, f))
+
+ if not test_binaries:
+ print(f"Error: No test binaries found in {test_dir}")
+ sys.exit(1)
+
+ # Use the first binary as the main object, the rest as -object arguments
+ cov_cmd = [
+ cov_tool, "report", test_binaries[0],
+ f"-instr-profile={merged_profdata}",
+ "--show-mcdc-summary"
+ ]
+
+ for tb in test_binaries[1:]:
+ cov_cmd.append(f"-object={tb}")
+
+ # Add source path filtering so we only get coverage for core library implementation,
+ # rather than inflating coverage with test suite files.
+ workspace_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+ cov_cmd.extend([
+ os.path.join(workspace_root, "src"),
+ os.path.join(workspace_root, "include"),
+ os.path.join(workspace_root, "hdr")
+ ])
+
+ print("Generating coverage report...")
+ subprocess.check_call(cov_cmd)
+
+if __name__ == "__main__":
+ main()
More information about the libc-commits
mailing list