Search code examples
c++g++code-coveragegoogletestgcov

gcov coverage limited to test files in minimal g++ project


After failing to get coverage with-cmake I set up a minimalistic project to see if I can get coverage working that way. It's derived from using-gtest-without-cmake

It has a src folder with a header and source file in it. QuickMaths.hpp :

#include <cstdint>

using size_t = std::size_t;

size_t
multiply(size_t a, size_t b);

inline size_t
add(size_t a, size_t b)
{
  return a + b;
}

class QuickMaths
{
public:
  size_t operator*() const;
  friend QuickMaths operator+(QuickMaths const&, QuickMaths const&);

  QuickMaths(size_t x);

private:
  size_t x;
};

QuickMaths.cpp:

#include "QuickMaths.hpp"

size_t
multiply(size_t a, size_t b)
{
  return a * b;
}

size_t
QuickMaths::operator*() const
{
  return x;
}

QuickMaths::QuickMaths(size_t x)
  : x(x)
{}

QuickMaths
operator+(QuickMaths const& a, QuickMaths const& b)
{
  return a.x + b.x;
}

And a test folder with QuickMaths.cpp :

#include <gtest/gtest.h>

#include <QuickMaths.hpp>

TEST(AddTest, shouldAdd)
{
  EXPECT_EQ(add(1UL, 1UL), 2UL);
}

TEST(MultiplyTest, shouldMultiply)
{
  EXPECT_EQ(multiply(2UL, 4UL), 8UL);
}

TEST(QuickMathTest, haveValue)
{
  auto v = QuickMaths{ 4UL };
  EXPECT_EQ(*v, 4UL);
}

and main.cpp :

#include <gtest/gtest.h>

int
main(int argc, char** argv)
{
  testing::InitGoogleTest(&argc, argv);

  return RUN_ALL_TESTS();
}
  • I create a build folder and cd into it, then compile using g++ --coverage -O0 ../src/QuickMaths.cpp ../test/*.cpp -I../src/ -pthread -lgtest -lgtest_main -lgcov
  • I run ./a.out => output shows tests being run and passing

Lastly I run gcovr -r ../ . and get the following output:

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: ../
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
src/QuickMaths.hpp                             2       0     0%   9,11
test/QuickMaths.cpp                            7       0     0%   5,7,10,12,15,17-18
test/main.cpp                                  3       3   100%   
------------------------------------------------------------------------------
TOTAL                                         12       3    25%
------------------------------------------------------------------------------

So it's visible that the gtest setup situated in main is being picked up, but the test cases themselves as well as the code from the src directory is not picked up as executed.


Solution

  • This error occurs because you are linking multiple files with the same name.

    There are two clues to the problem:

    1. When running the test, you will see a warning such as the following:

      libgcov profiling error:REDACTED/a-QuickMaths.gcda:overwriting an existing profile data with a different timestamp
      
    2. The coverage report lists three files:

      • src/QuickMaths.hpp
      • test/QuickMaths.cpp
      • test/main.cpp

      But one file is missing entirely:

      • src/QuickMaths.cpp

    What has happened?

    • When your tests shut down, raw coverage data is written by the test process into .gcda files. The name of these files depends on the compilation unit.

    • Looking into your build dir, we see the following data (.gcda) and notes (.gcno) files:

      build
      |-- a-QuickMaths.gcda
      |-- a-QuickMaths.gcno
      |-- a-main.gcda
      |-- a-main.gcno
      `-- a.out
      
    • You have provided a total of three files to your compilation command

      • Your command is g++ ... ../src/QuickMaths.cpp ../test/*.cpp ....
      • The three files are src/QuickMaths.cpp, test/QuickMaths.cpp, test/main.cpp
    • The autogenerated names for the compilation units seems to only consider the input file's basename, ignoring the directory. Since two files have the same basename, the same name for the compilation unit a-QuickMaths is used.

    • Since there's a name clash for the compilation unit, there is also a conflict for the coverage data file names.

    • The result is corrupted coverage data.

    The solution is to compile each compilation unit separately and linking them afterwards. You must give each compilation unit a distinct name, possibly using multiple subdirectories. For example:

    set -euo pipefail
    
    # compile foo/bar.cpp -> build/foo/bar.o
    for source in src/*.cpp test/*.cpp; do
      mkdir -p build/"$(dirname "$source")"
      cd build
      g++ --coverage -O0 -pthread -I../src -c -o "${source%.cpp}".o ../"${source}"
      cd -
    done
    
    # link the tests
    cd build;
    g++ --coverage -pthread -o testcase src/*.o test/*.o -lgtest
    cd -
    
    # run the test
    cd build
    ./testcase
    cd -
    
    tree build  # show directory structure
    
    # run gcovr
    cd build
    gcovr -r ..
    cd -
    

    In this example, the build directory would look like:

    build
    |-- src
    |   |-- QuickMaths.gcda
    |   |-- QuickMaths.gcno
    |   `-- QuickMaths.o
    |-- test
    |   |-- QuickMaths.gcda
    |   |-- QuickMaths.gcno
    |   |-- QuickMaths.o
    |   |-- main.gcda
    |   |-- main.gcno
    |   `-- main.o
    `-- testcase
    

    And the coverage report is as expected:

    ------------------------------------------------------------------------------
                               GCC Code Coverage Report
    Directory: ..
    ------------------------------------------------------------------------------
    File                                       Lines    Exec  Cover   Missing
    ------------------------------------------------------------------------------
    src/QuickMaths.cpp                             9       7    77%   20,22
    src/QuickMaths.hpp                             2       2   100%   
    test/QuickMaths.cpp                           10      10   100%   
    test/main.cpp                                  3       3   100%   
    ------------------------------------------------------------------------------
    TOTAL                                         24      22    91%
    ------------------------------------------------------------------------------
    

    Additional notes:

    • You're providing a main() but are also linking -lgtest_main. This is unnecessary.
    • The --coverage flag already includes -lgcov.