Compiling for Coverage

In order to collect coverage data, your software must be “instrumented” by the compiler. That means, you must re-compile your software with special compiler options.

The general workflow is:

  1. compile your software to enable coverage profiling
  2. execute your software to collect coverage profiles
  3. run gcovr to create reports from the collected coverage profiling data

This document explains how you can use GCC or Clang to compile with coverage instrumentation.

If you cannot compile your software with coverage flags, you cannot use gcovr. However, other tools like kcov might help.

Example Code

The following example.cpp program is used to illustrate the compilation process:

 1 // example.cpp
 2 
 3 int foo(int param)
 4 {
 5     if (param)
 6     {
 7         return 1;
 8     }
 9     else
10     {
11         return 0;
12     }
13 }
14 
15 int main(int argc, char* argv[])
16 {
17     foo(0);
18 
19     return 0;
20 }

This code executes several subroutines in this program, but some lines in the program are not executed.

Compiler Options

We compile example.cpp with the GCC compiler as follows:

g++ -fprofile-arcs -ftest-coverage -fPIC -O0 example.cpp -o program

What do these compiler flags mean?

  • We compile without optimization (-O0), because optimizations may merge lines of code or otherwise change the flow of execution in the program. This can change the measured coverage.

    On the other hand, enabling basic optimizations with -O1 can sometimes produce “better” coverage reports, especially for C++. This is a matter of personal preference, just make sure to avoid comparing coverage metrics across optimization levels.

    If you are having problems with lots of uncovered branches, see: Why does C++ code have so many uncovered branches?

  • Either --coverage or -fprofile-arcs -ftest-coverage are needed so that the compiler produces the information necessary to gather coverage data.

    With these options, the compiler adds logic to the output program that counts how often which part of the code was executed. The compiler will also create a example.gcno file with metadata. The name of the gcno file matches the compilation unit (see below).

Optional compiler flags:

  • You can use other flags like -g or -fPIC as required by your tests. These don’t affect the coverage results.
  • Using -fprofile-abs-path (available since GCC 8) can avoid some problems with interpreting the coverage data correctly. By default, the additional coverage files generated by GCC contain relative paths from the working directory to the source files. If there are multiple potential working directories from which you might have run the compiler, gcovr can get confused. Adding this option is more robust.

This examples uses the g++ compiler for C++ code, but any GCC or Clang-based compiler should work.

If you are using CMake, see Out-of-Source Builds with CMake for information on configuring that build system to compile your software with coverage enabled.

Running the Program

The above compiler invocation generated a program executable. Now, we have to execute this command:

./program

This will run whatever you designed this program to do. Often, such a program would contain unit tests to exercise your code.

As a side effect, this will create an example.gcda file with the coverage data for our compilation unit. This is a binary file so it needs to be processed first. Together, the .gcda and .gcno files can be used to create coverage reports.

Processing Coverage

Your compiler ships with tools to analyze the coverage data files. For GCC, this is gcov. For Clang, this is llvm-cov. You don’t have to call these programs yourself – gcovr will do that for you.

So let’s invoke gcovr:

gcovr

This will search for all your .gcno and .gcda files, run the compiler’s gcov tool, and summarize the code coverage statistics into a report. By default, we get a text summary on the command line that shows aggregate statistics for each line:

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: .
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
example.cpp                                    7       6    85%   7
------------------------------------------------------------------------------
TOTAL                                          7       6    85%
------------------------------------------------------------------------------

Gcovr supports many different Output Formats that you can generate instead.

Choosing the Right Gcov Executable

If you have multiple compilers installed or if you are using Clang, you will likely need to tell gcovr which gcov executable to use. By default, gcovr just uses the program named gcov. This is fine for the default GCC compiler, e.g. gcc or g++. Otherwise, you must use the --gcov-executable to tell gcovr what to use.

If you have used a specific GCC version (e.g. gcc-8 or g++-8), then you must name the gcov tool with the corresponding version. For example:

gcovr --gcov-executable gcov-8

If you have used Clang, then you can use its gcov emulation mode. For example:

gcovr --gcov-executable "llvm-cov gcov"

Again, the llvm-cov name may have to include your compiler version.

Working with Multiple Object Files

Code coverage instrumentation works on a per object file basis, which means you have to re-compile your entire project to collect coverage data.

The C/C++ model has a concept of “compilation units”. A large project is typically not compiled in one go, but in separate steps. The result of compiling a compilation unit is a .o object file with the machine code. The object code from multiple compilation units is later linked into the final executable or library. The previous example only had a single compilation unit, so no explicit linking step was necessary.

Because each compilation unit is compiled independently, every one has to be instrumented with coverage counters separately. A common mistake is to add the compiler flags for coverage (e.g. in the CFLAGS or CXXFLAGS variables) but then forgetting to force a re-compile. Depending on the build system, it may be necessary to clear out the old object files that weren’t compiled with coverage, e.g. with a make clean command. Other build systems use a separate build directory when compiling with coverage so that incremental compilation works as expected.

Each object file will have an associated .gcno and .gcda file in the same directory as the .o object file. For example, consider the following compilation process:

# (1) compile to object code
g++ --coverage -c -o a.o a.cpp
g++ --coverage -c -o b.o b.cpp

# (2) link the object files in the program
g++ --coverage -o the-program a.o b.o

# (3) run the program
./the-program
  1. Compiling the object code creates the a.o and b.o object files, but also corresponding a.gcno and b.gcno notes files, one for each compilation unit. The -c option is used to only compile but to not link the code.
  2. Linking the object code produces the final program. This has no effect on coverage processing, except that the --coverage flag makes sure that a compiler-internal gcov support library is linked.
  3. Running the program will increment the in-memory coverage counters for all executed lines. At the end, the counters are written into gcov data files, one for each compilation unit. Here, we would get a.gcda and b.gcda files.

If you only want coverage data for certain source files, it is sufficient to only compile those compilation units with coverage enabled that contain these source files. But this can be tricky to do correctly. For example, header files are often part of multiple compilation units.