Code Coverage
Code coverage is a metric used to calculate the percentage of source code executed during the Testing phase. In high-reliability C++ systems, coverage analysis is essential for identifying Untested logical branches and dead code.
Unlike dynamic analysis (Sanitizers) which checks for correctness, coverage checking checks for Completeness.
Instrumentation Mechanics
Coverage tools work by instrumenting the binary. During compilation, the compiler inserts hidden Counter instructions at the entry points of every Basic Block (a linear sequence of code with One entry and one exit).
When the program runs, these counters increment. Upon process termination, the runtime writes the Counter data to raw profile files on the disk, which are then mapped back to the source code by Analysis tools.
The Two Ecosystems
There are two primary coverage implementations in the modern C++ landscape:
- Gcov (GCC): The traditional approach. Generates
.gcno(notes) files during build and.gcda(data) files during execution. - Source-Based Coverage (Clang/LLVM): The modern approach. Uses the LLVM profile runtime. It is generally faster and handles C++ templates and inlined functions with greater precision.
1. GCC Workflow: gcov & lcov
This workflow applies to GCC on Linux and MinGW-w64 on Windows.
Prerequisites
- gcov: Included with GCC.
- lcov: A graphical frontend for gcov.
- genhtml: Generates HTML reports from lcov data.
Step 1: Compilation Flags
To instrument the binary, pass --coverage to both the compiler and the linker. This is a shorthand For -fprofile-arcs -ftest-coverage and linking libgcov.
g++ -std=c++23 --coverage -g -O0 main.cpp -o app
Step 2: Execution
Run the application (or test suite). This generates .gcda files alongside the object files.
./app
Step 3: Report Generation
Use lcov to capture the data and genhtml to visualize it.
## 1. Capture coverage data
lcov --capture --directory . --output-file coverage.info
## 2. Filter out system headers and external libraries (optional but recommended)
lcov --remove coverage.info '/usr/*' '*/vcpkg/*' --output-file coverage_filtered.info
# 3. Generate HTML report
genhtml coverage_filtered.info --output-directory out
Open out/index.html in a browser to view line-by-line coverage.
2. Clang Workflow: Source-Based Coverage
This workflow applies to Clang on Linux, macOS, and MSYS2.
Prerequisites
- llvm-profdata: Merges raw profile data.
- llvm-cov: Maps profiles to source code.
Step 1: Compilation Flags
Clang requires specific flags to enable the source-based profiling runtime.
clang++ -std=c++23 -fprofile-instr-generate -fcoverage-mapping -g -O0 main.cpp -o app
Step 2: Execution
Run the application. By default, LLVM writes to default.profraw.
LLVM_PROFILE_FILE="app-%p.profraw" ./app
Note: %p expands to the process ID, useful for multi-process applications.
Step 3: Indexing
Merge the raw profiles into an indexed format optimized for viewing.
llvm-profdata merge -sparse app-*.profraw -o app.profdata
Step 4: Visualization
Use llvm-cov to display the results. Note that llvm-cov requires the executable to map the Counters back to the source.
# Console Summary
llvm-cov report ./app -instr-profile=app.profdata
# HTML Export
llvm-cov show ./app -instr-profile=app.profdata -format=html -output-dir=coverage_report
CMake Integration Strategy
To maintain a clean build architecture, coverage logic should be encapsulated within the build System but guarded by a configuration option. We do not want coverage flags leaking into Release or Standard Debug builds.
Implementation
Add the following logic to your root CMakeLists.txt or a dedicated module.
option(ENABLE_COVERAGE "Enable code coverage instrumentation" OFF)
if(ENABLE_COVERAGE)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
# GCC Configuration
add_compile_options(--coverage -g -O0)
add_link_options(--coverage)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
# Clang Configuration
add_compile_options(-fprofile-instr-generate -fcoverage-mapping -g -O0)
add_link_options(-fprofile-instr-generate -fcoverage-mapping)
else()
message(FATAL_ERROR "Code coverage not supported for this compiler")
endif()
endif()
Defining a Coverage Target
Instead of manually typing lcov or llvm-cov commands, define a custom CMake target to automate The generation.
if(ENABLE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
find_program(LLVM_PROFDATA llvm-profdata REQUIRED)
find_program(LLVM_COV llvm-cov REQUIRED)
add_custom_target(coverage-report
COMMAND $\{LLVM_PROFDATA\} merge -sparse default.profraw -o coverage.profdata
COMMAND ${LLVM_COV} show $<TARGET_FILE:UnitTests>
-instr-profile=coverage.profdata
-format=html
-output-dir=coverage_html
COMMAND ${LLVM_COV} report $<TARGET_FILE:UnitTests>
-instr-profile=coverage.profdata
WORKING_DIRECTORY $\{CMAKE_BINARY_DIR\}
COMMENT "Generating HTML coverage report..."
)
endif()
Execution
- Configure:
cmake -S . -B build -DENABLE_COVERAGE=ON - Build:
cmake --build build - Run Tests:
cd build && ctest - Generate Report:
cmake --build build --target coverage-report
Continuous Integration (CI) Best Practices
In a CI environment (GitHub Actions, GitLab CI), the goal is to fail the build if coverage drops Below a threshold or to upload reports to visualization services.
1. Artifact Upload
Do not commit coverage artifacts. Instead, configure the CI pipeline to upload the coverage_html Directory as a build artifact for manual inspection.
2. Services (Codecov / Coveralls)
Services like Codecov ingest the raw reports (XML or LCOV format) and provide pull-request Integration (e.g., “This PR decreases coverage by 2%”).
To support these services, llvm-cov can export to LCOV format:
llvm-cov export ./app -instr-profile=app.profdata -format=lcov > coverage.info
Coverage Metrics: Line vs. Branch vs. Function
Coverage tools report multiple metrics, each measuring a different aspect of test completeness.
Line Coverage
The simplest metric: the percentage of source lines that execute at least once during testing.
Lines: 847/1200 (70.6%)
Limitation: A line containing a complex ternary expression counts as “covered” if any branch Executes. Line coverage alone can give a false sense of completeness.
Branch Coverage
Measures the percentage of conditional branches (if/else, switch, ternary) where both the true And false paths have been taken.
Branches: 234/300 (78.0%)
Example:
int classify(int x) \{
if (x > 0) \{ // Branch 1: true
return 1;
\} else if (x < 0) \{ // Branch 2: true (only if Branch 1 was false)
return -1;
\} else \{
return 0; // Branch 1: false AND Branch 2: false
\}
\}
If tests only call classify(1) and classify(-1)Line coverage is 100% (all lines execute), but Branch coverage is 83% (the return 0 path is never taken).
Function Coverage
Measures the percentage of functions that have been called at least once.
Functions: 45/50 (90.0%)
Limitation: Calling a function once only proves the entry point is reached. It does not verify Internal logic.
Metric Comparison
| Metric | Granularity | False Confidence Risk | Cost to Achieve High % |
|---|---|---|---|
| Line | Coarse | High | Low |
| Branch | Medium | Low | Medium |
| Function | Coarse (per-function) | High | Low |
| Condition | Fine (per-subexpression) | Very Low | High |
Industry standard for critical systems: Branch coverage > 80%, line coverage > 90%. For Safety-critical (automotive, medical), branch coverage > 95% is often required (per ISO 26262 / IEC 62304).
gcov File Format: .gcno and .gcda
Understanding the gcov file format helps diagnose coverage issues.
.gcno (GCN Notes)
Generated at compile time by -ftest-coverage. Contains:
- The source file name and line number mapping.
- Basic block structure (the CFG - control flow graph).
- Edge counts between basic blocks (initialized to zero).
- Location of counters in the generated object file.
.gcda (GCN Data)
Generated at runtime when the instrumented program exits. Contains:
- The actual execution counts for each basic block.
- Arc transitions (how many times each branch was taken).
- Summary statistics (total runs, total counts).
File Generation Timeline
Compile: main.cpp → main.o + main.gcno
Link: main.o → app
Run: ./app → main.gcda (written at exit)
Analyze: gcov main.gcno main.gcda → main.cpp.gcov
Common File Issues
- Stale
.gcdafiles: If you recompile without running the program, old.gcdafiles from a previous run remain. Always run the program after recompilation, or delete.gcdafiles before analysis. - Missing
.gcdafiles: If the program crashes (segfault) without callingatexit()handlers,.gcdafiles may not be written. Use__gcov_flush()to force-write coverage data at specific checkpoints.
#include <gcov.h> // GCC-specific
void critical_section() \{
// Force flush coverage data before risky operation
__gcov_flush();
perform_risky_operation();
\}
Code Coverage Exclude Patterns
Not all code should contribute to coverage metrics. Test harnesses, generated code, and third-party Templates inflate coverage numbers without providing value.
Excluding Files with lcov
lcov --capture --directory . --output-file coverage.info \
--exclude '*/test/*' \
--exclude '*/generated/*' \
--exclude '*/third_party/*' \
--exclude '/usr/*'
Excluding Code with Compiler Attributes
GCC and Clang support the __attribute__((no_sanitize("coverage"))) attribute (Clang) or pragmas to Exclude specific functions:
// GCC: exclude a function from coverage
void debug_only_function() __attribute__((no_instrument_function));
void debug_only_function() \{
// This function will not appear in coverage reports
\}
// Clang: exclude using -fprofile-exclude
// clang++ -fprofile-instr-generate -fcoverage-mapping \
// -fprofile-exclude='*/test/*.cpp'
Excluding Lines with Annotations
Lcov respects exclusion markers in source comments:
// LCOV_EXCL_START
void internal_debug_dump() \{
// This entire function is excluded from coverage
std::cerr << "Debug dump...\n";
\}
// LCOV_EXCL_STOP
int main() \{
// LCOV_EXCL_LINE — only this line is excluded
return 0;
\}
HTML Report Generation
lcov/genhtml Report
# Full pipeline
lcov --capture --directory . --output-file coverage.info
lcov --remove coverage.info '/usr/*' '*/test/*' --output-file coverage_filtered.info
genhtml coverage_filtered.info \
--output-directory coverage_html \
--legend \
--show-details \
--branch-coverage \
--title "MyProject Coverage Report"
Output structure:
coverage_html/
├── index.html # Summary page with overall metrics
├── index.html.sort.html # Sorted by coverage percentage
├── main.cpp.gcov.html # Per-file detailed report
└── ...
llvm-cov HTML Report
llvm-cov show ./app \
-instr-profile=app.profdata \
-format=html \
-output-dir=coverage_html \
-title "MyProject Coverage" \
-show-line-counts-or-regions \
-show-branches=percent
Clang’s HTML report is generally considered more readable than lcov’s, with color-coded regions and Branch coverage percentages inline.
Coverage Report Integration (Codecov Example)
For CI integration with Codecov:
# GitHub Actions
- name: Generate coverage report
run: |
cmake --build build
cd build && ctest
llvm-profdata merge -sparse default.profraw -o coverage.profdata
llvm-cov export ./UnitTests \
-instr-profile=coverage.profdata \
-format=lcov > coverage.info
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: build/coverage.info
fail_ci_if_error: true
flags: unittests
Coverage Thresholds in CI
Enforcing minimum coverage thresholds prevents regressions.
CMake/CTest Integration
CTest supports coverage thresholds since CMake 3.28:
if(ENABLE_COVERAGE)
# GCC coverage setup
add_compile_options(--coverage -g -O0)
add_link_options(--coverage)
# Define coverage target
add_custom_target(coverage
COMMAND lcov --capture --directory . --output-file coverage.info
COMMAND lcov --remove coverage.info '/usr/*' '*/test/*' --output-file coverage_filtered.info
COMMAND genhtml coverage_filtered.info --output-directory coverage_html
WORKING_DIRECTORY $\{CMAKE_BINARY_DIR\}
)
# Enforce minimum coverage (CMake 3.28+)
add_custom_target(coverage-check
COMMAND $\{CMAKE_COMMAND\}
-DCOVERAGE_FILE=$\{CMAKE_BINARY_DIR\}/coverage_filtered.info
-DMIN_LINE_COVERAGE=80
-DMIN_BRANCH_COVERAGE=70
-P $\{CMAKE_SOURCE_DIR\}/cmake/check_coverage.cmake
DEPENDS coverage
)
endif()
cmake/check_coverage.cmake:
file(READ "$\{COVERAGE_FILE\}" COVERAGE_DATA)
string(REGEX MATCH "lines...: ([0-9.]+)%" _line_match "$\{COVERAGE_DATA\}")
string(REGEX MATCH "functions...: ([0-9.]+)%" _func_match "$\{COVERAGE_DATA\}")
set(LINE_COV $\{CMAKE_MATCH_1\})
set(FUNC_COV $\{CMAKE_MATCH_1\})
message(STATUS "Line coverage: ${LINE_COV}% (minimum: $\{MIN_LINE_COVERAGE\}%)")
message(STATUS "Function coverage: $\{FUNC_COV\}%")
if(LINE_COV LESS MIN_LINE_COVERAGE)
message(FATAL_ERROR "Line coverage ${LINE_COV}% is below threshold $\{MIN_LINE_COVERAGE\}%")
endif()
CI Pipeline Example (GitHub Actions)
- name: Build with coverage
run: cmake -S . -B build -DENABLE_COVERAGE=ON && cmake --build build
- name: Run tests
run: cd build && ctest --output-on-failure
- name: Check coverage threshold
run: cmake --build build --target coverage-check
Common Pitfalls
- Running coverage builds in production: Coverage instrumentation adds 20-50% overhead. Never deploy instrumented binaries to production. Use separate build configurations for coverage and release.
- Stale
.gcdafiles from previous runs: Always clean the build directory or delete.gcdafiles before running tests. Otherwise, coverage data from a previous run may be merged with the current run, producing inaccurate results. - Optimized builds hiding dead code: Compiling with
-O2or higher may eliminate unreachable code, causing it to appear as “not covered” rather than “dead code.” Always compile with-O0for coverage builds. - Coverage of third-party code: Including system headers or vcpkg libraries in coverage reports inflates numbers. Always filter them out with
lcov --removeorllvm-covexclude paths. - Multi-process coverage merge: When running tests in parallel (e.g.,
ctest -j8), each process writes to the same.gcdafile. UseLLVM_PROFILE_FILE="app-%p.profraw"(Clang) or setGCOV_PREFIX(GCC) to separate output per process.
See Also
- Unit Tests — Writing tests that drive coverage analysis
- Build Caching — Avoiding cache poisoning with instrumented builds
- CMake Targets and Properties — Custom targets for coverage automation
Summary
This topic covers the essential concepts and techniques related to code coverage, including key principles and practical applications.
Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis
A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.
Worked Examples
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Intuition
Code coverage measures which lines of code are executed during testing. gcov and llvm-cov collect coverage data, while lcov and gcovr generate reports. Coverage thresholds enforce minimum testing standards. Integration with CI pipelines ensures coverage does not regress. Combined with mutation testing, coverage metrics provide insight into test quality beyond simple line execution.
Cross-References
- [[enviroment_and_toolchain/2_build_system/5_unit_tests]] - Test execution for coverage
- [[enviroment_and_toolchain/2_build_system/4_build_caching.mdx]] - Coverage and build caching
- [[enviroment_and_toolchain/2_build_system/2_ninja_and_parallelism]] - Parallel coverage collection
- [[enviroment_and_toolchain/4_development_enviroment_analysis/3_static_analysis.mdx]] - Static analysis complement