Use Gcov + LCOV / gcovr to show C/C++ projects code coverage results.
This repo shows how to use Gcov to create lcov/gcovr coverage reports for C/C++ projects.
๐ Read the write-up: English ยท ไธญๆ
Code coverage reports online
Note: The source code is under the master branch, and code coverage report under branch coverage.
The problem I encountered: A C/C++ project from decades ago has no unit tests, only regression tests. But I want to know:
Can code coverage be measured without unit tests? The answer is Yes.
Commercial tools such as Squish Coco and Bullseye can measure the coverage of black-box tests the same way: they instrument the code at build time. See Other C/C++ coverage tools for how they compare.
Gcov workflow diagram

There are three main steps:
*.gcno.*.gcda data file.*.gcno and *.gcda, generate the report. lcov and gcovr run gcov internally and turn its output into HTML. Running gcov yourself only writes annotated *.c.gcov text files for reading in a terminal; lcov and gcovr do not read those files.Hereโs how each of these steps is done exactly.
You can clone this repository and run make help to see how to use it.
$ git clone https://github.com/shenxianpeng/gcov-example.git
$ cd gcov-example
$ make help
help Makefile help
build Make build
coverage Run code coverage
lcov-report Generate lcov report
gcovr-report Generate gcovr report
deps Install dependencies (requires Debian/Ubuntu for apt-get)
build-config1 Build with -DTRIGGER_ON into build/config1 (multi-config demo)
build-config2 Build without TRIGGER_ON into build/config2 (multi-config demo)
coverage-merged Merge coverage of both configurations into one report
coverage-filter Show coverage for trigger.c only (file-specific filtering)
clean Clean all generate files
lint Lint code with clang-format
Steps to generate code coverage reports
# 1. compile
make build
# 2. run executable
./main
# 3. run code coverage
make coverage
# 4. generate HTML report
# install lcov and gcovr first
make deps
# then
make lcov-report
# or
make gcovr-report
Real-world projects often compile the same source with different macros (feature flags, platform switches, etc.). You can combine the coverage from multiple configurations into a single report.
trigger.c demonstrates this with an #ifdef TRIGGER_ON / #else block, and main.c calls trigger_action(). A single build can only ever execute one of the two branches, because the other one is not even compiled:
# Build and run both configurations, then merge into one report
make coverage-merged
# => merged-report/index.html
| Build | trigger.c lines hit |
|---|---|
build/config1 (-DTRIGGER_ON) |
3 of 3 |
build/config2 (default) |
3 of 3 |
| merged | 4 of 4 |
Each configuration alone reports 100% and hides the untested branch. Only the merged report sees both.
Each configuration is built into its own directory (build/config1, build/config2). This is not optional: GCC stamps every .gcno with the compile time, so when two configurations share a directory the second build overwrites the .gcno files, and the .gcda written by the first binary no longer matches them (stamp mismatch with notes file).
Under the hood, this uses lcov --add-tracefile to merge two separate .info files:
lcov --capture --directory build/config1 --output-file config1.info
lcov --capture --directory build/config2 --output-file config2.info
lcov --add-tracefile config1.info --add-tracefile config2.info --output-file merged.info
genhtml merged.info --output-directory merged-report
To see coverage for a single file (e.g. trigger.c) rather than the whole project:
# First generate the full lcov report, then filter to trigger.c only
make coverage-filter
# => trigger-report/index.html
Using lcov --extract directly:
lcov --extract lcov-report/coverage.info "$(pwd)/trigger.c" --output-file trigger_only.info
genhtml trigger_only.info --output-directory trigger-report
With gcovr, use --filter:
gcovr --filter trigger.c --html --html-details -o trigger-report/coverage.html
This repository stays focused on Gcov with LCOV and gcovr. For context, these are the other tools you will run into:
| Tool | License | Compilers / platforms | Metrics | Notes |
|---|---|---|---|---|
| Gcov + LCOV / gcovr | free (GPL / BSD) | GCC; Clang through llvm-cov gcov |
line, function, branch; condition (MC/DC) with GCC 14 -fcondition-coverage |
what this repository shows |
| llvm-cov source-based coverage | free (Apache 2.0) | Clang only | line, region, branch, MC/DC (-fcoverage-mcdc) |
-fprofile-instr-generate -fcoverage-mapping; gcovr 8.4+ and LCOVโs llvm2lcov can import its output |
| OpenCppCoverage | free (GPL) | MSVC on Windows | line | works from PDB debug info, no instrumentation at build time; Cobertura XML and HTML output |
| Squish Coco | commercial | GCC, Clang, MSVC, many embedded toolchains | line, branch, condition, MC/DC, function | instruments at build time; aimed at safety-critical projects |
| Bullseye Coverage | commercial | GCC, Clang, MSVC, embedded | function, condition/decision | instruments at build time; the oldest of the commercial tools |
Other tools I maintain for C/C++ CI: