PowerDNS uses coveralls to generate code coverage reports from our Continuous Integration tests. The resulting analysis can then be consulted online, and gives insight into which parts of the code are automatically tested.
Code coverage is generated during our Continuous Integration tests, for every pull request. In addition to the dashboard on Coveralls' website, a summary is posted on pull requests.
There are two main ways of generating code coverage: GCOV and source-based.
The GCOV approach, supported by both g++ and clang++, is enabled by passing the --coverage flag (equivalent to -ftest-coverage -fprofile-arcs) to the compiler and linker. It operates on debugging information (DebugInfo), usually DWARF, generated by the compiler, and also used by debuggers.
This approach generates .gcno files during the compilation, which are stored along the object files, and .gcda files at runtime when the final program is executed.
- There are as many
.gcnoand.gcdafiles as object files, which may be a lot. - Every invocation of a program updates the
.gcdafiles corresponding to the code that has been executed. It will append to existing.gcdafiles, but only process can update a given file so parallel execution will result in corrupted data. - Writing to each
.gcdamight take a while for large programs, and has been known to slow down execution quite a lot. - Accurate reporting of lines and branches may be problematic when optimizations are enabled, so it is advised to disable optimizations to get useful analysis.
- Note that the
.gcdafiles produced byclang++are not fully compatible with theg++ones, and with the existing tools, butllvm-cov gcovcan produce.gcovfiles that should be compatible. A symptom of this incompatibility looks like this:
Processing pdns/ednssubnet.gcda
__w/pdns/pdns/pdns/ednssubnet.gcno:version '408', prefer 'B02'
clang++ supports source-based coverage, which operates on AST and preprocessor information directly. This is enabled by passing -fprofile-instr-generate -fcoverage-mapping to the compiler and leads to .profraw files being produced when the binary is executed.
The .profraw file(s) can be merged by llvm-profdata merge into a .profdata file which can then be used by llvm-cov show to generate HTML and text reports, or by llvm-cov export to export LCOV data that is compatible with other tools.
- Source-based coverage can generate accurate data with optimizations enabled, and has a much lower overhead that
GCOV. - The path and exact name of the
.profrawfiles generated when a program is executed can be controlled via theLLVM_PROFILE_FILEenvironment variable, which supports patterns like%p, which expands to the process ID. That allows running several programs in parallel, each program generating its own file at the end.
We use clang++'s source-based coverage method in our CI, as it allows running our regression tests in parallel with several workers. It is enabled by passing the --enable-coverage=clang flag during configure for all products.
The code coverage generation is done as part of the build-and-test-all.yml workflow.
Since we have a monorepo for three products which share the same code-base, the process is a bit tricky:
- We use coveralls's
parallelfeature, which allows us to generate partial reports from several steps of our CI process, then merge them during thecollectphase and upload the resultingLCOVfile to coveralls. - After executing our tests, the
generate_coverage_infomethod intasks.pymerges the.profrawfiles that have been generated every time a binary has been executed into a single.profdatafile viallvm-profdata merge. We enable thesparsemode to get a smaller.profdatafile, since we do not do Profile-Guided Optimization (PGO). - It then generates a
.lcovfile from the.profdataviallvm-cov export, telling it to ignore reports for files under/usrin the process (via the-ignore-filename-regexparameter). - We then normalize the paths of the source files to prevent duplicates for files that are used by more than one product, and to account for the fact that our CI actually compiles from a
distdir. This is handled by a Python script, .github/scripts/normalize_paths_in_coverage.py that parses theLCOVdata and updates the paths. - We call Coveralls's github action to upload the resulting
LCOVdata for this step. - After all steps have completed, we call that action again to let it know that our workflow is finished and the data can be consolidated.
One important thing to remember is that the content is only written into a .profraw file is the program terminates correctly, calling exit handlers, and if the __llvm_profile_write_file() function is called. Our code base has a wrapper around that, pdns::coverage::dumpCoverageData().
This is especially important for us because our products often terminates by calling _exit(), bypassing the exit handlers, to avoid issues with the destruction order of global objects.
It is possible to generate a code coverage report without going through the CI, for example to test the coverage of a new feature in a given product.
- Run the
configurescript with the--enable-coverage=clangoption, setting theCCandCXXenvironment variables to use theclangcompiler:CC=clang CXX=clang++ ./configure --enable-coverage=clang - Compile the product as usual with:
make - Run the test(s) that are expected to cover the new feature, via
./testrunnerormake checkfor the unit tests, and the instructions of the correspondingregression-tests*directory for the regression tests. It is advised to set theLLVM_PROFILE_FILEenvironment variable in such a way that an invocation of the product do not override the results from the previous invocation. For example settingLLVM_PROFILE_FILE="/tmp/code-%p.profraw"will result in each invocation writing a new file into the/tmpdirectory, replacing%pwith the process ID. - Merge the resulting
*.profrawfile into a singlecode.profdatafile by runningllvm-profdata merge -sparse -o /tmp/code.profdata /tmp/code-*.profraw - Generate a HTML report into the
/tmp/html-reportdirectory by runningllvm-cov show --instr-profile /tmp/code.profdata -format html -output-dir /tmp/html-report -object </path/to/product/binary>
- Run the
configurescript with the--enable-coverageoption, using eitherg++orclang++:./configure --enable-coverage - Compile as usual with:
make. This will generate.gcnofiles along with the usual.oobject files and the final binaries. - Run the test(s) that are expected to cover the new feature, via
./testrunnerormake checkfor the unit tests, and the instructions of the correspondingregression-tests*directory for the regression tests. Note that the regression should not be run in parallel, as it would corrupt the.gcnafiles that will be generated in the process. For dnsdist, that means runningpytestwithout the--dist=loadfile -n autooptions. - Generate a HTML report using
gcovr, orgcovthenlcov
The way our code coverage report is generated does not currently handle the different authoritative server tools (that end up in the pdns-tools package) very well. Consequently the coverage report for these tools, and the related code parts, is not accurate.
It is likely possible to pass several --object </path/to/binary> options to llvm-cov when processing the .profdata file.