CMake and coverage with gcov
TL;DR: To generate coverage data in CMake: enable CTest, enable debug, build and link all relevant targets with --coverage
, then run ctest --test-dir build -T Coverage
. Use gcov
or lcov
to generate a CLI or HTML coverage report respectively.
Generate coverage data with CMake §
At a high-level, to generate coverage with CMake:
- Enable CTest1 with
include(CTest)
. - (Optional) Enable debug builds.
- Specify
--coverage
options on the test executable targets, for compile and link stages. - Run
ctest --test-dir build -T Coverage
.
The following headings run through these steps in detail.
Enable CTest §
Make sure include(CTest)
is specified. This typically comes in the top-level CMakeLists.txt
:
project("Example project" C)
cmake_minimum_required(VERSION 3.15.0)
include(CTest)
Enable debug builds §
It is not necessary to enable debug builds in order for coverage to work, but it does help get more meaningful reports.
I do this by setting CMAKE_BUILD_TYPE
via the command line:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
Another option is to set it within the CMakeLists.txt
by calling set(CMAKE_BUILD_TYPE "Debug")
. However, it is generally preferable to set debug builds via the command line to make it possible to switch between debug/release builds without having to modify the project files.
Specify coverage option §
For each test target, add the --coverage
option to the compile and link options. Using the:
add_executable(DumbExample_app
TestDumbExample.c
)
target_compile_options(DumbExample_app PRIVATE --coverage)
target_link_options(DumbExample_app PRIVATE --coverage)
target_link_libraries(DumbExample_app
DumbExample_lib
Unity
)
add_test(NAME DumbExample_test COMMAND DumbExample_app)
See the gcc docs for more details. In short:
- When compiling,
--coverage
is a synonym for-fprofile-arcs
(which generates*.gcda
files) and-ftest-coverage
(which generates*.gcno
files).2 - When linking,
--coverage
is a synonym for-lgcov
(which links thegcov
runtime library).
Make sure to add the --coverage
option to both the test code and the code under test, e.g. your library and/or executable. Otherwise you may find you have “100% coverage” because only your test code is generating coverage data. This will be more obvious once you have lcov set up for HTML reports.
Generate coverage data §
Run ctest
with the option -T Coverage
to generate coverage data. This can be done in conjunction with -T Test
to also run the tests:
$ ctest -T Test -T Coverage
Site: 3cfb2c57d360
Build name: Linux-cc
Create new tag: 20241125-1345 - Experimental
Test project /workspaces/mount/build
Start 1: DumbExample
1/2 Test #1: DumbExample ...................... Passed 0.01 sec
Start 2: OneMoreExample
2/2 Test #2: OneMoreExample ................... Passed 0.01 sec
100% tests passed, 0 tests failed out of 2
Total Test time (real) = 0.04 sec
Performing coverage
Processing coverage (each . represents one file):
..
Accumulating results (each . represents one file):
..
Covered LOC: 54
Not covered LOC: 0
Total LOC: 54
Percentage Coverage: 100.00%
Process coverage data §
The coverage data is written to the aforementioned *.gcno
and *.gcda
files. These may be buried deep within the CMake build directory.
Find the coverage data §
To find the coverage data, use find | grep gcno$
to search for all files ending in gcno
:
$ find | grep gcno$
./test/dumb_example/CMakeFiles/DumbExample.dir/TestDumbExample.c.gcno
./test/one_more_example/CMakeFiles/OneMoreExample.dir/TestOneMoreExample.c.gcno
Likewise for gcda
:
$ find | grep gcda$
./test/dumb_example/CMakeFiles/DumbExample.dir/TestDumbExample.c.gcda
./test/one_more_example/CMakeFiles/OneMoreExample.dir/TestOneMoreExample.c.gcda
This confirms that the coverage data was generated.
Generate CLI report with gcov §
The coverage data can be used by gcov to generate a report in the CLI.
We have to do a little file wrangling to feed the data into gcov, since it expects to be fed a list of source file names, such as foo.c
, and expects that the corresponding object file foo.o
is in the same directory. However, CMake keeps the full filename and appends .o
, generating files such as foo.c.o
. Fortunately, we can just feed gcov the .gcda
files, and then gcov will perform the same steps as if it were handling a .c
file: it will strip the extension and add .gcno
and .gcna
to get the relevant coverage data. So gcov will find all the right files if we just feed it the gcna
files:3
$ find build -name '*.gcda' | xargs gcov
File '/workspaces/mount/test/dumb_example/TestDumbExample.c'
Lines executed:100.00% of 19
Creating 'TestDumbExample.c.gcov'
File '/workspaces/mount/test/one_more_example/TestOneMoreExample.c'
Lines executed:100.00% of 35
Creating 'TestOneMoreExample.c.gcov'
Lines executed:100.00% of 54
Generate HTML report with lcov §
By installing lcov (e.g. apt-get install lcov
), we can generate an HTML coverage report. This shows the source code with the untested lines highlighted in red.
This is easier to use than gcov. Just pass in --directory build
and write to coverage.info
, then pass that into genhtml:4
$ lcov --directory build --capture --output-file coverage.info
$ genhtml -o coverage coverage.info
This results in coverage/index.html
and a bunch of supporting files in that directory.
Bonus: report coverage to GitLab §
Simply add the following to your .gitlab-ci.yml
file:
coverage: '/Percentage Coverage: \d+(?:\.\d+)?/'
This will match a coverage percentage generated by gcov such as the following:
Percentage Coverage: 100.00%
CTest is “an executable that comes with CMake; it handles running the tests for the project”. ↩︎
Largely based off this comment, which seems to match up with my observations. ↩︎
Thanks to this section of this blog post for providing a demonstration. I decided to pass in
build
instead of.
to--directory
, since that is the build directory where our coverage data will be. ↩︎