Skip to content

Commit b1ddc16

Browse files
committed
add LLVM-based coverage generation support and integrate with CI pipeline
1 parent 1f9bb76 commit b1ddc16

File tree

4 files changed

+191
-20
lines changed

4 files changed

+191
-20
lines changed

.github/workflows/ubuntu.yml

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ jobs:
322322
env:
323323
PPC_NUM_PROC: 1
324324
PPC_ASAN_RUN: 1
325-
gcc-build-codecov:
325+
clang-build-codecov:
326326
needs:
327327
- gcc-test-extended
328328
- clang-test-extended
@@ -339,12 +339,13 @@ jobs:
339339
- name: ccache
340340
uses: hendrikmuhs/[email protected]
341341
with:
342-
key: ${{ runner.os }}-gcc
342+
key: ${{ runner.os }}-clang-coverage
343343
create-symlink: true
344344
max-size: 1G
345345
- name: CMake configure
346346
run: >
347347
cmake -S . -B build -G Ninja
348+
-D CMAKE_C_COMPILER=clang-20 -D CMAKE_CXX_COMPILER=clang++-20
348349
-D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache
349350
-D CMAKE_BUILD_TYPE=RELEASE
350351
-D CMAKE_VERBOSE_MAKEFILE=ON -D USE_COVERAGE=ON
@@ -358,31 +359,38 @@ jobs:
358359
PPC_NUM_THREADS: 2
359360
OMPI_ALLOW_RUN_AS_ROOT: 1
360361
OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1
362+
LLVM_PROFILE_FILE: "coverage-%p-%m.profraw"
361363
- name: Run tests (threads)
362364
run: scripts/run_tests.py --running-type="threads" --counts 1 2 3 4
363365
env:
364366
PPC_NUM_PROC: 1
365-
- name: Generate gcovr Coverage Data
367+
LLVM_PROFILE_FILE: "coverage-%p-%m.profraw"
368+
- name: Generate LLVM Coverage Data
366369
run: |
367370
mkdir cov-report
368371
cd build
369-
gcovr --gcov-executable `which gcov-14` \
370-
-r ../ \
371-
--exclude '.*3rdparty/.*' \
372-
--exclude '/usr/.*' \
373-
--exclude '.*tasks/.*/tests/.*' \
374-
--exclude '.*modules/.*/tests/.*' \
375-
--exclude '.*tasks/common/runners/.*' \
376-
--exclude '.*modules/runners/.*' \
377-
--exclude '.*modules/util/include/perf_test_util.hpp' \
378-
--exclude '.*modules/util/include/func_test_util.hpp' \
379-
--exclude '.*modules/util/src/func_test_util.cpp' \
380-
--xml --output ../coverage.xml \
381-
--html=../cov-report/index.html --html-details
372+
# Merge all raw profiles into a single indexed profile
373+
llvm-profdata-20 merge -sparse $(find . -name "*.profraw") -o coverage.profdata
374+
# Find all test executables
375+
BINARIES=$(find bin -type f -executable | tr '\n' ' ')
376+
# Generate coverage report in LCOV format for Codecov
377+
llvm-cov-20 export \
378+
$BINARIES \
379+
--format=lcov \
380+
--ignore-filename-regex='.*3rdparty/.*|/usr/.*|.*tests/.*|.*tasks/.*|.*modules/runners/.*|.*modules/util/include/perf_test_util.hpp|.*modules/util/include/func_test_util.hpp|.*modules/util/src/func_test_util.cpp' \
381+
--instr-profile=coverage.profdata \
382+
> ../coverage.lcov
383+
# Generate HTML report
384+
llvm-cov-20 show \
385+
$BINARIES \
386+
--format=html \
387+
--output-dir=../cov-report \
388+
--ignore-filename-regex='.*3rdparty/.*|/usr/.*|.*tests/.*|.*tasks/.*|.*modules/runners/.*|.*modules/util/include/perf_test_util.hpp|.*modules/util/include/func_test_util.hpp|.*modules/util/src/func_test_util.cpp' \
389+
--instr-profile=coverage.profdata
382390
- name: Upload coverage reports to Codecov
383391
uses: codecov/[email protected]
384392
with:
385-
files: coverage.xml
393+
files: coverage.lcov
386394
- name: Upload coverage report artifact
387395
id: upload-cov
388396
uses: actions/upload-artifact@v4

cmake/configure.cmake

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ if(UNIX)
6363
endif()
6464

6565
if(USE_COVERAGE)
66-
add_compile_options(--coverage)
67-
add_link_options(--coverage)
66+
# Use LLVM source-based code coverage
67+
add_compile_options(-fprofile-instr-generate -fcoverage-mapping)
68+
add_link_options(-fprofile-instr-generate -fcoverage-mapping)
6869
endif(USE_COVERAGE)
6970
endif()
7071

docker/ubuntu.Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN set -e \
1616
openmpi-bin openmpi-common libopenmpi-dev \
1717
libomp-dev \
1818
gcc-14 g++-14 \
19-
gcovr zip \
19+
zip \
2020
&& wget -q https://apt.llvm.org/llvm.sh \
2121
&& chmod +x llvm.sh \
2222
&& ./llvm.sh 20 all \

scripts/generate_llvm_coverage.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env python3
2+
"""Generate LLVM coverage report for the project."""
3+
4+
import os
5+
import subprocess
6+
import sys
7+
import glob
8+
import argparse
9+
10+
11+
def run_command(cmd, cwd=None):
12+
"""Run a command and return its output."""
13+
print(f"Running: {' '.join(cmd)}")
14+
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
15+
if result.returncode != 0:
16+
print(f"Error: {result.stderr}")
17+
sys.exit(1)
18+
return result.stdout
19+
20+
21+
def main():
22+
parser = argparse.ArgumentParser(description="Generate LLVM coverage report")
23+
parser.add_argument("--build-dir", default="build", help="Build directory")
24+
parser.add_argument("--output-dir", default="coverage", help="Output directory for coverage report")
25+
parser.add_argument("--llvm-version", default="20", help="LLVM version (default: 20)")
26+
args = parser.parse_args()
27+
28+
build_dir = os.path.abspath(args.build_dir)
29+
output_dir = os.path.abspath(args.output_dir)
30+
31+
# Try to find LLVM tools in various locations
32+
llvm_profdata = None
33+
llvm_cov = None
34+
35+
# List of possible LLVM tool names
36+
if args.llvm_version:
37+
profdata_names = [f"llvm-profdata-{args.llvm_version}", "llvm-profdata"]
38+
cov_names = [f"llvm-cov-{args.llvm_version}", "llvm-cov"]
39+
else:
40+
profdata_names = ["llvm-profdata"]
41+
cov_names = ["llvm-cov"]
42+
43+
# Try to find the tools
44+
for name in profdata_names:
45+
result = subprocess.run(["which", name], capture_output=True, text=True)
46+
if result.returncode == 0:
47+
llvm_profdata = name
48+
break
49+
50+
for name in cov_names:
51+
result = subprocess.run(["which", name], capture_output=True, text=True)
52+
if result.returncode == 0:
53+
llvm_cov = name
54+
break
55+
56+
if not llvm_profdata or not llvm_cov:
57+
print("Error: Could not find llvm-profdata or llvm-cov in PATH")
58+
print("Make sure LLVM tools are installed and in your PATH")
59+
sys.exit(1)
60+
61+
if not os.path.exists(build_dir):
62+
print(f"Error: Build directory {build_dir} does not exist")
63+
sys.exit(1)
64+
65+
# Create output directory
66+
os.makedirs(output_dir, exist_ok=True)
67+
68+
# Find all .profraw files
69+
profraw_files = glob.glob(os.path.join(build_dir, "**", "*.profraw"), recursive=True)
70+
if not profraw_files:
71+
print("No .profraw files found. Make sure to run tests with LLVM_PROFILE_FILE set.")
72+
print("Example: LLVM_PROFILE_FILE='coverage-%p-%m.profraw' ./your_test")
73+
sys.exit(1)
74+
75+
print(f"Found {len(profraw_files)} .profraw files")
76+
77+
# Merge profiles
78+
profdata_file = os.path.join(output_dir, "coverage.profdata")
79+
run_command([llvm_profdata, "merge", "-sparse"] + profraw_files + ["-o", profdata_file])
80+
print(f"Created merged profile: {profdata_file}")
81+
82+
# Find all executables in bin directory
83+
bin_dir = os.path.join(build_dir, "bin")
84+
if not os.path.exists(bin_dir):
85+
print(f"Error: Bin directory {bin_dir} does not exist")
86+
sys.exit(1)
87+
88+
executables = []
89+
for root, dirs, files in os.walk(bin_dir):
90+
for file in files:
91+
filepath = os.path.join(root, file)
92+
if os.access(filepath, os.X_OK) and not file.endswith('.txt'):
93+
executables.append(filepath)
94+
95+
if not executables:
96+
print("No executables found in bin directory")
97+
sys.exit(1)
98+
99+
print(f"Found {len(executables)} executables")
100+
101+
# Get the project root directory (parent of build dir)
102+
project_root = os.path.dirname(build_dir)
103+
104+
# Generate LCOV report
105+
lcov_file = os.path.join(output_dir, "coverage.lcov")
106+
cmd = [llvm_cov, "export"] + executables + [
107+
"--format=lcov",
108+
"--ignore-filename-regex=.*3rdparty/.*|/usr/.*|.*tests/.*|"
109+
".*tasks/.*|.*modules/runners/.*|.*modules/util/include/perf_test_util.hpp|"
110+
".*modules/util/include/func_test_util.hpp|.*modules/util/src/func_test_util.cpp",
111+
f"--instr-profile={profdata_file}"
112+
]
113+
114+
with open(lcov_file, "w") as f:
115+
result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, text=True)
116+
if result.returncode != 0:
117+
print(f"Error generating LCOV report: {result.stderr}")
118+
sys.exit(1)
119+
120+
print(f"Generated LCOV report: {lcov_file}")
121+
122+
# Post-process LCOV file to use relative paths
123+
with open(lcov_file, 'r') as f:
124+
lcov_content = f.read()
125+
126+
# Replace absolute paths with relative paths
127+
lcov_content = lcov_content.replace(project_root + '/', '')
128+
129+
with open(lcov_file, 'w') as f:
130+
f.write(lcov_content)
131+
132+
print("Post-processed LCOV file to use relative paths")
133+
134+
# Generate HTML report
135+
html_dir = os.path.join(output_dir, "html")
136+
cmd = [llvm_cov, "show"] + executables + [
137+
"--format=html",
138+
f"--output-dir={html_dir}",
139+
"--ignore-filename-regex=.*3rdparty/.*|/usr/.*|.*tests/.*|"
140+
".*tasks/.*|.*modules/runners/.*|.*modules/util/include/perf_test_util.hpp|"
141+
".*modules/util/include/func_test_util.hpp|.*modules/util/src/func_test_util.cpp",
142+
f"--instr-profile={profdata_file}"
143+
]
144+
145+
run_command(cmd)
146+
print(f"Generated HTML report: {html_dir}/index.html")
147+
148+
# Generate summary
149+
cmd = [llvm_cov, "report"] + executables + [
150+
f"--instr-profile={profdata_file}",
151+
"--ignore-filename-regex=.*3rdparty/.*|/usr/.*|.*tasks/.*/tests/.*|.*modules/.*/tests/.*|"
152+
".*tasks/common/runners/.*|.*modules/runners/.*|.*modules/util/include/perf_test_util.hpp|"
153+
".*modules/util/include/func_test_util.hpp|.*modules/util/src/func_test_util.cpp"
154+
]
155+
156+
summary = run_command(cmd)
157+
print("\nCoverage Summary:")
158+
print(summary)
159+
160+
161+
if __name__ == "__main__":
162+
main()

0 commit comments

Comments
 (0)