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: .profraw files 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)

  1. Check the executable was built with coverage:

    objdump -t ./debug/testFoo.*/testFoo | grep __llvm_profile
    
  2. Check LLVM_PROFILE_FILE is writable:

    export LLVM_PROFILE_FILE="/tmp/coverage-%p.profraw"
    
  3. Ensure program exits normally (coverage is written at exit)

No .gcda files generated (GCC)

  1. Check .gcno files exist (created at compile time):

    find . -name "*.gcno"
    
  2. Ensure program exits normally

  3. Check directory permissions

Coverage seems low

  1. Make sure tests actually exercise the code
  2. Check filtered paths don't exclude your source
  3. 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

  1. Don't mix sanitizers and coverage - they're mutually exclusive
  2. Use debug or profiling builds - release builds disable coverage
  3. Filter third-party code - exclude vendor directories from reports
  4. Run all tests - partial test runs give misleading coverage
  5. Reset coverage data - delete old .profraw/.gcda before fresh runs

Further reading


In this section
Topics
howto development testing coverage llvm-cov gcov lcov
See also