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:
- Add the required compiler and linker flags
- Enable CTest
- 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!