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:
compile your software to enable coverage profiling
execute your software to collect coverage profiles
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
23
int foo(int param)
4{
5if (param)
6{
7return 1;
8}
9else
10{
11return 0;
12}
13}
1415
int main(int argc, char* argv[])
16{
17foo(0);
1819
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
Compiling the object code creates the
a.o
andb.o
object files, but also correspondinga.gcno
andb.gcno
notes files, one for each compilation unit. The-c
option is used to only compile but to not link the code.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.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
andb.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.