smhk

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:

  1. Enable CTest1 with include(CTest).
  2. (Optional) Enable debug builds.
  3. Specify --coverage options on the test executable targets, for compile and link stages.
  4. 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:

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:

CMakeLists.txt
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 the gcov 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.


  1. CTest is “an executable that comes with CMake; it handles running the tests for the project”↩︎

  2. See here for more details on .gcno and .gcda files. ↩︎

  3. Largely based off this comment, which seems to match up with my observations. ↩︎

  4. 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. ↩︎