gcov produces different results on Clang and GCC
Asked Answered
H

1

13

I'm trying to understand how to properly structure a C++ project by using CMake, googletest, and gcov for test coverage. I would like to build a general CMakeLists.txt that would work for any platform/compiler.

This is my first attempt. However, if I try to build the project and then run lcov (to generate the report), I see that I have different results if I use CLang (right result) or GCC (wrong result). Note that I'm on MacOs and I installed gcc through brew (brew install gcc).

Moreover I used the following flags in my main CMakeLists.txt:

if(CODE_COVERAGE)
    SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage" )
endif()

Note: If you find something wrong/weird in my CMakeLists.txt files or lcov usage, I'm open to any kind of feedback!

My library

#include "library.h"

#include <iostream>

void foo(){
    std::cout << "Foo!" << std::endl;
}

void bar(int n){
    if (n > 0){
        std::cout << "n is grater than 0!" << std::endl;
    }
    else if (n < 0){
        std::cout << "n is less than 0!" << std::endl;
    }
    else{
        std::cout << "n is exactly 0!" << std::endl;
    }
}

void baz(){  // LCOV_EXCL_START
    std::cout << "Baz!" << std::endl;
}
// LCOV_EXCL_STOP

My tests


#ifndef GCOV_TUTORIAL_TEST_LIBRARY_H
#define GCOV_TUTORIAL_TEST_LIBRARY_H

#include "../src/library.h"

#include <gtest/gtest.h>


namespace gcov_tutorial::tests {
    TEST(TestFooSuite,TestFoo){
        foo();
    }
    TEST(TestBarSuite,TestBarGreaterThanZero){
        bar(100);
    }
    TEST(TestBarSuite,TestBarEqualToZero){
        //bar(0);
    }
    TEST(TestBarSuite,TestBarLessThanZero){
        bar(-100);
    }
}

#endif //GCOV_TUTORIAL_TEST_LIBRARY_H

CLang Compilation

#!/bin/bash

# Rationale: https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -euxo pipefail

# BASE_DIR is the project's directory, containing the src/ and tests/ folders.
BASE_DIR=$PWD
COVERAGE_FILE=coverage.info

GCOV_PATH=/usr/bin/gcov
CLANG_PATH=/usr/bin/clang
CLANGPP_PATH=/usr/bin/clang++

rm -rf build
mkdir build && cd build

# Configure
cmake -DCMAKE_C_COMPILER=$CLANG_PATH -DCMAKE_CXX_COMPILER=$CLANGPP_PATH -DCODE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Release ..

# Build (for Make on Unix equivalent to `make -j $(nproc)`)
cmake --build . --config Release

# Clean-up for any previous run.
rm -f $COVERAGE_FILE
lcov --zerocounters --directory .
# Run tests
./tests/RunTests
# Create coverage report by taking into account only the files contained in src/
lcov --capture --directory tests/ -o $COVERAGE_FILE --include "$BASE_DIR/src/*" --gcov-tool $GCOV_PATH
# Create HTML report in the out/ directory
genhtml $COVERAGE_FILE --output-directory out
# Show coverage report to the terminal
lcov --list $COVERAGE_FILE
# Open HTML
open out/index.html

Coverage report when compiling with CLang

GCC Compilation

#!/bin/bash

# Rationale: https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -euxo pipefail

# BASE_DIR is the project's directory, containing the src/ and tests/ folders.
BASE_DIR=$PWD
COVERAGE_FILE=coverage.info

GCOV_PATH=/usr/local/bin/gcov-11
GCC_PATH=/usr/local/bin/gcc-11
GPP_PATH=/usr/local/bin/g++-11

rm -rf build
mkdir build && cd build

# Configure
cmake -DCMAKE_C_COMPILER=$GCC_PATH -DCMAKE_CXX_COMPILER=$GPP_PATH -DCODE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Release ..

# Build (for Make on Unix equivalent to `make -j $(nproc)`)
cmake --build . --config Release

# Clean-up for any previous run.
rm -f $COVERAGE_FILE
lcov --zerocounters --directory .
# Run tests
./tests/RunTests
# Create coverage report by taking into account only the files contained in src/
lcov --capture --directory tests/ -o $COVERAGE_FILE --include "$BASE_DIR/src/*" --gcov-tool $GCOV_PATH
# Create HTML report in the out/ directory
genhtml $COVERAGE_FILE --output-directory out
# Show coverage report to the terminal
lcov --list $COVERAGE_FILE
# Open HTML
open out/index.html

Coverage report with GCC

Highchair answered 21/5, 2022 at 16:16 Comment(1)
Can you minimize your example so that it only relies on gcc, clang, and gcov, rather than also needing googletest, cmake, and lcov?Adey
B
16

You are actually asking two questions, here.

  1. Why do the coverage results differ between these two compilers?
  2. How do I structure a CMake project for code coverage?

Answer 1: Coverage differences

The simple answer here is that you are building in Release mode, rather than RelWithDebInfo mode. GCC does not put as much debugging information in by default as Clang does. On my system, adding -DCMAKE_CXX_FLAGS="-g" to your build-and-run-cov-gcc.sh script yields the same results as Clang, as does building in RelWithDebInfo.

For whatever reason, it appears that Clang tracks more debug information either by default or when coverage is enabled. GCC does not have these same guardrails. The lesson to take away is this: collecting coverage information is a form of debugging; you must use a debugging-aware configuration for your compiler if you want accurate results.

Answer 2: Build system structure

It is generally a terrible idea to set CMAKE_CXX_FLAGS inside your build. That variable is intended to be a hook for your build's users to inject their own flags. As I detail in another answer on this site, the modern approach to storing such settings is in the presets

I would get rid of the if (CODE_COVERAGE) section of your top-level CMakeLists.txt and then create the following CMakePresets.json file:

{
  "version": 4,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 23,
    "patch": 0
  },
  "configurePresets": [
    {
      "name": "gcc-coverage",
      "displayName": "Code coverage (GCC)",
      "description": "Enable code coverage on GCC-compatible compilers",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "RelWithDebInfo",
        "CMAKE_CXX_FLAGS": "-fprofile-arcs -ftest-coverage"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "gcc-coverage",
      "configurePreset": "gcc-coverage",
      "configuration": "RelWithDebInfo"
    }
  ]
}

Then your build script can be simplified considerably.

#!/bin/bash

# Rationale: https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -euxo pipefail

# Set up defaults for CC, CXX, GCOV_PATH
export CC="${CC:-gcc-11}"
export CXX="${CXX:-g++-11}"
: "${GCOV_PATH:=gcov-11}"

# Record the base directory
BASE_DIR=$PWD

# Clean up old build
rm -rf build

# Configure
cmake --preset gcc-coverage

# Build
cmake --build --preset gcc-coverage

# Enter build directory
cd build

# Clean-up counters for any previous run.
lcov --zerocounters --directory .

# Run tests
./tests/RunTests

# Create coverage report by taking into account only the files contained in src/
lcov --capture --directory tests/ -o coverage.info --include "$BASE_DIR/src/*" --gcov-tool $GCOV_PATH

# Create HTML report in the out/ directory
genhtml coverage.info --output-directory out

# Show coverage report to the terminal
lcov --list coverage.info

# Open HTML
open out/index.html

The key here is the following lines:

# Configure
cmake --preset gcc-coverage
# Build
cmake --build --preset gcc-coverage

This script now lets you vary the compiler and coverage tool via environment variables and the CMakeLists.txt doesn't have to make any assumptions about what compiler is being used.

On my (Linux) system, I can run the following commands successfully:

$ CC=gcc-12 CXX=g++-12 GCOV=gcov-12 ./build-and-run-cov.sh

results-gcc

$ CC=clang-13 CXX=clang++-13 GCOV=$PWD/llvm-cov-13.sh ./build-and-run-cov.sh

Where llvm-cov-13.sh is a wrapper for llvm-cov-13 for compatibility with the --gcov-tool flag. See this answer for more detail.

#!/bin/bash
exec llvm-cov-13 gcov "$@"

results-clang

As you can see, the results are indistinguishable now that the correct flags are used.

Blennioid answered 21/5, 2022 at 19:24 Comment(3)
I ran across your answer on using CMake presets the other week so I want to start by saying thanks. It's so much simpler than using toolchains! Is there any reason why you're not using configurePresets.environment to set CC and CXX? You could even use inheritance to create coverage configurations for gcc and clang. It might also be worth setting binaryDir to ${sourceDir}/build/${presetName} to facilitate having multiple presets.Krystynakshatriya
@WesToleman - You're welcome! So, I happen to have five versions of Clang (10-14) and three versions of GCC (9-11) installed and the above preset worked for all eight. I don't think there's anything wrong with inheriting gcc-coverage for each one, it just seems like a bit much for an SO answer. Maybe a little "YAGNI" for now, too. The suggestion to put the ${presetName} in the binaryDir path is a good one, especially when you start adding multiple presets. Again, omitted for brevity.Blennioid
Note you can create a gcov symlink pointing to llvm-cov instead of a wrapper script.Loree

© 2022 - 2024 — McMap. All rights reserved.