Code Coverage
Measure test coverage with LLVM and GCC toolchains
Code Coverage
Code coverage measures which parts of your code are executed during testing. This guide covers generating coverage reports using LLVM source-based coverage (clang) and gcov (GCC).
Note
Coverage instrumentation is mutually exclusive with sanitizers. If you enable both, sanitizers take precedence and coverage is disabled.
Quick start
Build with coverage
# Build with coverage enabled (debug mode)
qbs build config:debug project.enableCoverage:true
# Or in profiling mode
qbs build config:profiling project.enableCoverage:true
Run tests
# Run your test executables
./debug/testFoo.*/testFoo
./debug/testBar.*/testBar
Generate report
Clang (LLVM):
# Merge profile data
llvm-profdata merge -sparse *.profraw -o merged.profdata
# Generate HTML report
llvm-cov show ./debug/testFoo.*/testFoo -instr-profile=merged.profdata \
-format=html -output-dir=coverage
GCC (gcov/lcov):
# Generate coverage info
lcov --capture --directory . --output-file coverage.info
# Generate HTML report
genhtml coverage.info --output-directory coverage
View report
xdg-open coverage/index.html
How it works
LLVM source-based coverage (clang)
LLVM source-based coverage is the primary method for clang. It uses:
- Compile flags:
-fprofile-instr-generate -fcoverage-mapping - Output:
.profrawfiles generated at runtime - Tools:
llvm-profdata(merge),llvm-cov(report)
Advantages:
- More accurate than gcov
- Faster instrumentation
- Native clang integration
- Region-based coverage (not just line-based)
GCC coverage (gcov)
GCC coverage uses the traditional gcov approach:
- Compile flags:
--coverage - Output:
.gcno(compile time) +.gcda(runtime) - Tools:
gcov,lcov,genhtml
Advantages:
- Wide ecosystem support
- Works with any GCC version
- Mature tooling (lcov, genhtml)
LLVM coverage workflow
Step 1: Build
qbs build config:debug project.enableCoverage:true
The build system automatically adds:
-fprofile-instr-generate- generate instrumentation-fcoverage-mapping- generate coverage map
Step 2: Run tests
# Set output location (optional, defaults to current directory)
export LLVM_PROFILE_FILE="coverage-%p.profraw"
# Run tests
./debug/testFoo.*/testFoo
./debug/testBar.*/testBar
Each executable generates a .profraw file with coverage data.
Step 3: Merge profiles
# Find all profile data
find . -name "*.profraw" -print
# Merge into single file
llvm-profdata merge -sparse *.profraw -o merged.profdata
Step 4: Generate reports
Summary report:
llvm-cov report ./debug/testFoo.*/testFoo -instr-profile=merged.profdata
Detailed HTML report:
llvm-cov show ./debug/testFoo.*/testFoo -instr-profile=merged.profdata \
-format=html -output-dir=coverage
Text report:
llvm-cov show ./debug/testFoo.*/testFoo -instr-profile=merged.profdata \
-format=text > coverage.txt
LCOV export (for tooling integration):
llvm-cov export ./debug/testFoo.*/testFoo -instr-profile=merged.profdata \
-format=lcov > coverage.lcov
Multiple executables
To combine coverage from multiple test executables:
# Run all tests
./debug/testFoo.*/testFoo
./debug/testBar.*/testBar
./debug/testBaz.*/testBaz
# Merge all profiles
llvm-profdata merge -sparse *.profraw -o merged.profdata
# Report with multiple objects
llvm-cov report \
-instr-profile=merged.profdata \
-object=./debug/testFoo.*/testFoo \
-object=./debug/testBar.*/testBar \
-object=./debug/testBaz.*/testBaz
Filtering output
# Only show specific directories
llvm-cov show ./debug/testFoo.*/testFoo -instr-profile=merged.profdata \
-format=html -output-dir=coverage \
-ignore-filename-regex='third_party/.*' \
-ignore-filename-regex='test/.*'
# Show only specific files
llvm-cov show ./debug/testFoo.*/testFoo -instr-profile=merged.profdata \
src/lib/foo/*.cpp
GCC coverage workflow
Step 1: Build
qbs build config:debug project.enableCoverage:true
With GCC, the build system adds --coverage to both compile and link flags.
Step 2: Run tests
./debug/testFoo.*/testFoo
./debug/testBar.*/testBar
This generates .gcda files alongside the .gcno files created during compilation.
Step 3: Generate coverage info
# Capture coverage data
lcov --capture --directory . --output-file coverage.info
# Remove system headers and test code
lcov --remove coverage.info '/usr/*' 'test/*' -o coverage.info
Step 4: Generate HTML report
genhtml coverage.info --output-directory coverage
Advanced lcov options
# Set base directory
lcov --capture --directory . --base-directory /home/user/project \
--output-file coverage.info
# Include branch coverage
lcov --capture --directory . --output-file coverage.info --rc lcov_branch_coverage=1
genhtml coverage.info --output-directory coverage --branch-coverage
# Combine multiple coverage files
lcov -a coverage1.info -a coverage2.info -o combined.info
Preprocessor detection
When coverage is enabled, the build defines COVERAGE_ENABLED=1. Use this to conditionally compile code:
#ifdef COVERAGE_ENABLED
// Coverage-specific code (e.g., skip slow tests)
#endif
Troubleshooting
No .profraw files generated (LLVM)
-
Check the executable was built with coverage:
objdump -t ./debug/testFoo.*/testFoo | grep __llvm_profile -
Check
LLVM_PROFILE_FILEis writable:export LLVM_PROFILE_FILE="/tmp/coverage-%p.profraw" -
Ensure program exits normally (coverage is written at exit)
No .gcda files generated (GCC)
-
Check
.gcnofiles exist (created at compile time):find . -name "*.gcno" -
Ensure program exits normally
-
Check directory permissions
Coverage seems low
- Make sure tests actually exercise the code
- Check filtered paths don't exclude your source
- Verify coverage data wasn't reset between runs
llvm-profdata: unsupported instrumentation profile format
LLVM tools must match the compiler version:
# Check clang version
clang --version
# Use matching llvm-profdata
llvm-profdata-17 merge ... # if using clang-17
lcov: unable to open .gcda
The .gcda files must be accessible from the same paths used during compilation. If you moved the build directory, use --base-directory:
lcov --capture --directory . --base-directory /original/build/path \
--output-file coverage.info
Best practices
- Don't mix sanitizers and coverage - they're mutually exclusive
- Use debug or profiling builds - release builds disable coverage
- Filter third-party code - exclude vendor directories from reports
- Run all tests - partial test runs give misleading coverage
- Reset coverage data - delete old
.profraw/.gcdabefore fresh runs