Jeff Bell
I recently tried using CMake to add gcov to the testing script. As I learned, this is much more difficult than using a Makefile. This post covers what I came up with.

What is Gcov?

gcov is a tool to check test coverage. gcov records a run of your program and will measure the lines of code that are executed. This allows you to see how well your tests cover the code you have written. For a more detailed description on gcov, checkout this introduction on the GNU website.

What is CMake?

If you haven’t seen my previous post on an introduction to CMake, check it out. In a nutshell, CMake is a tool/language for cross-platform software builds in various languages. CMake attempts to remove some of the uncertainties that come with using Makefiles. For example, if you wanted to locate a library on the system for linking, CMake has a single-line command to do it. Read more about CMake here.

Rationale

When using a coverage tool alongside a testing framework, it is very easy to see how much of your code is executed when you run your tests. This allows you to see if there are holes in your tests and, to a further extent, where the holes are.

The goal of this project was to use CMake to build a simple program and run a few tests. Then, create a target where we can say make gcov to run gcov on our program and output the coverage data.

Putting It All Together

Let’s take a look at how this example project works. If you’d like to follow along, you can check out the project source here.

To show that gcov is working, I created the simple Adder class. An Adder contains a value that you can add numbers to using the add() method. You can also print the current value of the Adder using the print_value() method and reset the value to zero using the clear() method. Once again, to see the full project source, check it out on GitHub.

I created the following test program for this class:

// RunAdder.cpp
#include <iostream>
#include "Adder.h"

int main() {
    Adder adder;
    adder.print_value(std::cout);
    adder.add(5);
    adder.print_value(std::cout);
    adder.add(5);
    adder.print_value(std::cout);

    return 0;
}

When run, this program outputs the following:

Current value: 0
Current value: 5
Current value: 10

Lastly, my CMakeLists.txt file looks like this:

cmake_minimum_required(VERSION 3.5)
project(CMake_GCov CXX)

# Set the compiler options
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "-g -O0 -Wall -fprofile-arcs -ftest-coverage")
set(CMAKE_CXX_OUTPUT_EXTENSION_REPLACE ON)

# Create OBJECT_DIR variable
set(OBJECT_DIR ${CMAKE_BINARY_DIR}/CMakeFiles/RunAdder.dir)
message("-- Object files will be output to: ${OBJECT_DIR}")

# Set the sources
set(SOURCES
    RunAdder.cpp
    Adder.cpp
   )

# Create the executable
add_executable(RunAdder ${SOURCES})

# Create the gcov target. Run coverage tests with 'make gcov'
add_custom_target(gcov
    COMMAND mkdir -p coverage
    COMMAND ${CMAKE_MAKE_PROGRAM} test
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
    )
add_custom_command(TARGET gcov
    COMMAND echo "=================== GCOV ===================="
    COMMAND gcov -b ${CMAKE_SOURCE_DIR}/*.cpp -o ${OBJECT_DIR}
    | grep -A 5 "Adder.cpp" > CoverageSummary.tmp
    COMMAND cat CoverageSummary.tmp
    COMMAND echo "-- Coverage files have been output to ${CMAKE_BINARY_DIR}/coverage"
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/coverage
    )
add_dependencies(gcov RunAdder)
# Make sure to clean up the coverage folder
set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES coverage)

# Create the gcov-clean target. This cleans the build as well as generated 
# .gcda and .gcno files.
add_custom_target(scrub
COMMAND ${CMAKE_MAKE_PROGRAM} clean
COMMAND rm -f ${OBJECT_DIR}/*.gcno
COMMAND rm -f ${OBJECT_DIR}/*.gcda
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)

# Testing
enable_testing()

add_test(output_test ${CMAKE_CURRENT_BINARY_DIR}/RunAdder)
set_tests_properties(output_test PROPERTIES PASS_REGULAR_EXPRESSION "0;5;10")

Here are some things to note about the above CMakeLists.txt. The first is that the gcov target is what will do everything we need to show us results on our code coverage. The COMMAND to run gcov is possible thanks to the -fprofile-arcs -ftest-coverage compile flags. Also note that the scrub target will clean up the generated .gcno and .gcda files. Unfortunately, I couldn’t find a much better way to clean up these files since they are buried in the CMakeFiles directory.

All .gcov files and results are output into a coverage directory located in the project binary directory. I decided to go with this solution because it allowed for easy cleanup by adding just the coverage directory to the list of additional make clean files.

Lastly, a call to make gcov will automatically build the project, and run any tests added using the add_test command. For this project, I created a very simple test that checked to see the correct three numbers are output: 0, 5 and 10. As long as these three numbers are output, the test will pass.

set_tests_properties(output_test PROPERTIES PASS_REGULAR_EXPRESSION "0;5;10")

Building and Testing

As usual, I can perform an out-of-source build using the following commands

$ mkdir build
$ cd build
$ cmake ..

Now that the Makefile has been generated, we can run coverage tests by simply saying make gcov. When I run make gcov, I get something like the following as output:

[ 33%] Building CXX object CMakeFiles/RunAdder.dir/RunAdder.o
[ 66%] Building CXX object CMakeFiles/RunAdder.dir/Adder.o
[100%] Linking CXX executable RunAdder
[100%] Built target RunAdder
Running tests...
Test project cmake-gcov/build
Start 1: output_test
1/1 Test #1: output_test ......................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.01 sec
=================== GCOV ====================
File 'cmake-gcov/Adder.cpp'
Lines executed:71.43% of 7
No branches
No calls
cmake-gcov/Adder.cpp:creating 'Adder.cpp.gcov'

File 'cmake-gcov/RunAdder.cpp'
Lines executed:100.00% of 7
No branches
No calls
cmake-gcov/RunAdder.cpp:creating 'RunAdder.cpp.gcov'

-- Coverage files have been output to cmake-gcov/build/coverage
[100%] Built target gcov

Notice that only about 71% of the lines of Adder.cpp are executed. This is because the clear() method is never used in our test. If we were to add a call to clear() in RunAdder.cpp, this value goes back up to 100%. If you don’t trust me on that, try it yourself!

If I want to run a new coverage test, I can run:

$ make scrub
$ make gcov

make scrub MUST be run before running make gcov again. If make scrub is not run, the coverage results will be incorrect due to the .gcno and .gcda files being updated, rather than regenerated. This can be seen easily by running make gcov twice and looking at coverage/Adder.cpp.gcov after each run. The total number of runs will continue to increase for each line of code. These numbers will not be reset until you run make scrub to clean the .gcda and .gcno files.

Closing Remarks

And there you have it, folks! They said it couldn’t be done, but here you are using CMake and gcov at the same time. This project took way longer than I was expecting, especially considering how few lines I ended up with.

The largest problem I was unable to find a solution for was how to delete the .gcno and .gcda files automatically when running the gcov target. I couldn’t find a way to clean up the old ones automatically before building the project. Just about any way I tried it, the project would be built first, and then the newly generated profiling files would be deleted causing an error. I suspect part of this comes from using the add_dependencies() CMake command to build the project before analyzing the coverage.

The amount of time I had to spend on tiny details like this makes me want to run away from CMake and never look back. However, I am overall happy with how this turned out, but I wish that I could make it just a touch more elegant. Integrating tools like this really shows off how much more clean and straight-forward GNU Makefiles can be.

In the future, I intend to integrate this with googletest to create a CMake project template. This would make it super easy to start a new C++ project that already has testing and coverage capabilites. Until then, happy hacking!