Overview of workflow
The basic workflow is as follows:
- Build tests with code coverage enabled.
- Run tests.
- Use
gcovrto create a code coverage report from that execution in a JSON file. - Use
doxideto create a line data report from the original source files in a JSON file. - Use
gcovragain to create a final code coverage report in HTML, merging the two JSON files created in steps 3 and 4.
As we shall see, the lines of function templates that have not been instantiated will be reported as uncovered in the final report.
Preliminaries
You will need to install Gcovr and Doxide for this to work. The former is readily available from Linux distributions and as a Python package installable via pip; see the installation instructions. The latter you are unlikely to find in your Linux distribution just yet, but there are Linux, Mac and Windows packages available; see the installation instructions—you will need the absolute latest version at time of writing, which is 0.7.0.
Working example
We will consider the following simple program:
#include <iostream>
/**
* Overload of f() with concrete type.
*/
double twice(double x) {
return 2*x;
}
/**
* Function template.
*/
template<class T>
T twice(T x) {
return 3*x; // deliberate error
}
int main() {
double x = 5.0;
double y = twice(x);
std::cout << y << std::endl;
return 0;
}This has a function twice() that multiplies its argument by two. There is an overload of this function for arguments of type double, as well as a function template for generic types. The main() program, which we can think of as a test program for twice(), calls that function with argument x and prints out the result. Because x is of type double, the first overload of the function is called and the test looks successful. The function template is not instantiated during the compile, and not executed during the run—for good measure, we’ve snuck a silly error in there that would not be caught if this was our test suite!
Unfortunately, as we shall see, code coverage is reported at 100%, and the busy developer may overlook this problem.
Building and running with coverage enabled
To build a program with gcov coverage enabled, using either g++ or clang++, simply add the --coverage command-line option. It is a good idea to disable optimization and inlining as well. Using g++, if the above code is in a file called test.cpp, we can build with:
g++ --coverage -O0 -fno-inline -o test test.cppAs well as the usual compile, this creates a file test.gcno in the same directory, with some basic block and source line mapping information necessary for collecting coverage data.
In addition to gcov-compatible coverage data, clang++ can provide alternative Source-Based Code Coverage data, enabled with different command-line options. However, gcovr and doxide require gcov-compatible data.
Now run the program as normal:
./testThis runs the program and outputs the result as normal, but also creates a second code coverage file test.gcda in the same directory, with data on how many times each line, branch, and function of the program was executed.
For more information on the test.gcno and test.gcda files, see Brief Description of gcov Data Files in the GCC documentation.
Demonstrating the problem
To see the problem with function templates, we can produce an HTML report using gcovr:
gcovr --html-details gcovr.html \
--exclude-function-lines \
--exclude-noncode-linesThis command already enables a few options to make the line count more accurate, excluding some lines that do not really reflect executable code: function signatures (--exclude-function-lines) and end-of-function closing braces (--exclude-noncode-lines).
Now open gcovr.html in your browser (created by the last command). It reports 100% code coverage. Clicking on test.cpp for further details, we see that, as expected, all of the main() function is executed along with the first definition of twice(), but that the generic version of twice() is unmarked. This means that it is simply not included in the coverage report at all; if it was included but uncovered it would be marked in red instead, and total coverage would be less than 100%. We may consider this a problem.
A fix is to mix line data from the source code with the count data from the execution. We can get that from Doxide. But first, we need to produce JSON output from Gcovr to mix it into.
Generating a JSON report with Gcovr
We previously ran gcovr to produce an HTML report. A simple change to the command allows us to produce a JSON report instead:
gcovr --json gcovr.json \
--exclude-function-lines \
--exclude-noncode-linesAn alternative to gcovr is lcov. We do not use it here as it does not have the JSON support that makes merging reports from gcovr and doxide straightforward. But for completeness, to use lcov, first collate coverage information in an lcov.info file:
lcov --directory . --no-external --capture --output lcov.infothen produce an HTML report with the accompanying tool genhtml:
genhtml lcov.info -o lcovand open lcov/index.html in your browser. The report looks similar to gcovr.
Generating a JSON report with Doxide
Doxide must be first set up for documentation, although that is easy. Just run, in your souce directory:
doxide initInspect the doxide.yaml configuration file that is created and make sure that the files section includes your source files. By default all *.hpp and *.h header files are included, recursively. If you have function templates in *.cpp source files then add those source files here as well. For the working example we have the test.cpp source file to add at least. A minimal example of a working doxide.yaml file for code coverage is something like:
title: Untitled
path: /blog/cplusplus-code-coverage-with-gcov-gcovr-doxide/
category: dev
description:
files:
- "*.hpp"
- "**/*.hpp"
- "*.cpp"
- "**/*.cpp"
- "*.h"
- "**/*.h"Once Doxide is configured, produce a line data report by running, from the same directory as the doxide.yaml file:
doxide cover > doxide.jsonThe doxide.json file now contains the report. It lists files and line numbers for all functions and operators found in the source code, with zero execution counts.
The remainder of the doxide.yaml configuration file and the other files generated by doxide init are not important, unless you are also interested in using Doxide for documentation—they can be deleted if not of interest. The only exception is if Doxide fails to parse your source files correctly, often due to macro use, in which case further tweaking of the configuration file may be necessary. See the Doxide website on Parsing Considerations if that seems to be affecting you.
Merging the JSON reports of Gcovr and Doxide
In the previous two sections we produced a gcovr.json file with coverage data from executing the program, and a doxide.json file with line data from the source code. The next step is to combine them into a single HTML report with gcovr:
gcovr --html-details gcovr.html \
--exclude-function-lines \
--exclude-noncode-lines \
--json-add-tracefile gcovr.json \
--json-add-tracefile doxide.jsonThe two --json-add-tracefile options at the end of the command add the two JSON files that we have produced.
We can now inspect gcovr.html again. We see that coverage is now reported at 83.3%, with the function template marked as uncovered when the program was run. We may consider this a more accurate reflection of the code coverage of a test suite, which will be particularly useful for large header-only libraries, where a lack of data on uninstantiated function templates is easily missed.
Summary and further work
This post has proposed an approach to including uninstantiated function templates in C++ code coverage reports by mixing line data from source code with execution data from gcov. It uses a new feature in Doxide—the cover command—to provide the supplementary line data from source code.
This is still early work. While use of line data from source code can provide better recall of all executable lines, it may lack precision and include lines that are not, in fact, executable code, or that are executed at compile time rather than runtime (e.g. if constexpr lines). Refinements are needed in this regard.
Related links
- Force-cover, which uses Clang tooling to preprocess C++ source code with comments to identify templates, then a Python program to postprocess code coverage output to ensure that those templates are counted as uncovered code.