Skip to content

Code Coverage with CMake and CTest

In this article, I’ll walk you through the steps for adding code coverage testing using CMake and CTest. This is an addition to my previous article on code coverage testing for C++. Integrating basic coverage testing with CMake has become a lot easier since then.

The pre-requisites are the same: I’m assuming CMake as a build system and gcc or clang as compiler.

A Minimal Setup

Let’s start with some minimal example code:

// main.cpp

#include <cassert>

int max(int a, int b) {
    if (a > b) {
        return a;
    }
    return b;
}

int main() {
    assert(max(1, 0) == 1);
}

Next, add a basic CMakeLists.txt file for compiling the code:

cmake_minimum_required(VERSION 3.20)
project(coverage_test)

# make sure to use a Debug build
set(CMAKE_BUILD_TYPE "Debug")

# define executable target
add_executable(coverage_test main.cpp)

Use the standard steps to build using CMake:

mkdir build && cd build && cmake .. && cmake --build .

This should give you something like this:

[ 50%] Building CXX object CMakeFiles/coverage_test.dir/main.cpp.o
[100%] Linking CXX executable coverage_test
[100%] Built target coverage_test

Congratulations!

Adding Coverage Support

There are three steps required to add code coverage testing to the setup:

  1. Add the required compiler and linker flags
  2. Enable CTest
  3. Add your executable as a test

Both clang and gcc only need a single option to enable generation of coverage information: -coverage. However, you need to add this to both your compiler and linker flags:

target_compile_options(coverage_test PRIVATE -coverage)
target_link_options(coverage_test PRIVATE -coverage)

I’d recommend using the more modern variant of specifying options per target instead of using global options using add_compile_options or add_link_options. The PRIVATE scope limits the options only for compiling this target.

Enabling CTest just requires

include(CTest)

Finally, in order to register your executable as a test you need to add

add_test(NAME coverage COMMAND coverage_test)

This needs to come after defining the executable target. The full CMakeLists.txt now looks like this:

cmake_minimum_required(VERSION 3.20)
project(coverage)

# include CTest support
include(CTest)

# define executable target
add_executable(coverage_test main.cpp)

# make sure to use a Debug build
set(CMAKE_BUILD_TYPE "Debug")

# compile with coverage options
target_compile_options(coverage_test PRIVATE -coverage)
target_link_options(coverage_test PRIVATE -coverage)

add_test(NAME coverage COMMAND coverage_test)

Generating Coverage Information

Re-build the project:

cmake .. && cmake --build .

Run your test with ctest:

ctest -T Test

Generate coverage information:

ctest -T Coverage

This should give you something like this as output:

Performing coverage
   Processing coverage (each . represents one file):
    .
   Accumulating results (each . represents one file):
    .
        Covered LOC:         7
        Not covered LOC:     1
        Total LOC:           8
        Percentage Coverage: 87.50%

You can also combine the two steps into a single call to ctest:

ctest -T Test -T Coverage

You can also verify that additional tests increase your coverage by adding another assertion to your main() function:

assert(max(0, 1) == 1);

Wipe out any outdated coverage information:

find . -name '*.gcda' -delete

Re-run the whole thing:

cmake --build . && ctest -T Test -T Coverage

Congratulations! You achieved 100% code coverage:

Performing coverage
   Processing coverage (each . represents one file):
    .
   Accumulating results (each . represents one file):
    .
        Covered LOC:         9
        Not covered LOC:     0
        Total LOC:           9
        Percentage Coverage: 100.00%

What’s Next?

This setup doesn’t provide you with a nice way to visualize which parts of the code are covered. However, this can be achieved either by using lcov as described in my previous article, or by using an extension for your IDE to directly visualize the coverage information in your editor. The Gcov Viewer for VS Code seems to be a popular choice, but I did not test it yet.

Hope this helps!