gcov在Clang和GCC上产生不同的结果

13

我正在尝试通过使用CMake、googletest和gcov进行测试覆盖,以正确地构建C++项目的结构。我想要构建一个通用的CMakeLists.txt,可以适用于任何平台/编译器。

这里是我的第一次尝试。然而,如果我尝试构建项目,然后运行lcov(生成报告),我会发现如果我使用CLang(正确结果)或GCC(错误结果),我会得到不同的结果。 请注意,我在MacOS上安装了gcc(brew install gcc)。

此外,我在我的主要CMakeLists.txt中使用了以下标志:

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

注意:如果你在我的CMakeLists.txt文件或lcov使用中发现任何错误或奇怪的地方,我乐于接受任何形式的反馈!

我的库

#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

我的测试


#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编译

#!/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

使用 CLang 编译时的代码覆盖率报告

GCC编译

#!/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

使用GCC生成的测试覆盖率报告


你能否将你的示例最小化,使其仅依赖于gcc、clang和gcov,而不需要googletest、cmake和lcov? - Joseph Sible-Reinstate Monica
1个回答

16

实际上你在这里问了两个问题。

  1. 为什么这两个编译器的代码覆盖率结果会不同?
  2. 如何为一个CMake项目配置代码覆盖率?

回答1: 覆盖率差异

简单地说,问题在于你是在Release模式下构建程序,而不是RelWithDebInfo模式。GCC默认情况下没有像Clang那样放入那么多调试信息。在我的系统上,将-DCMAKE_CXX_FLAGS="-g"添加到你的build-and-run-cov-gcc.sh脚本中将得到与Clang相同的结果,即在RelWithDebInfo模式下构建也可以得到相同的结果。

出于某种原因,Clang似乎默认跟踪更多的调试信息或者在启用覆盖率时跟踪更多的调试信息。GCC则没有这些保护措施。需要记住的教训是:收集代码覆盖率信息是一种调试形式;如果想要准确的结果,则必须使用一个支持调试的编译器配置。

回答2: 构建系统结构

在你的构建中设置CMAKE_CXX_FLAGS通常是一个不好的做法。该变量旨在为用户注入其自己的标志而提供的钩子。正如我在这个问题的另一个回答中详细介绍的那样,存储这些设置的现代方法是使用预设

我建议删除你顶层CMakeLists.txt中的if (CODE_COVERAGE)部分,并创建以下CMakePresets.json文件:

{
  "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"
    }
  ]
}

那么您的构建脚本可以大大简化。

#!/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

关键在于以下这几行代码:

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

现在,这个脚本允许您通过环境变量来改变编译器和代码覆盖工具,而且 CMakeLists.txt 不需要做任何关于正在使用什么编译器的假设。

在我的(Linux)系统上,我可以成功地运行以下命令:

$ 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

llvm-cov-13.sh是一个包装器,用于与--gcov-tool标志兼容的llvm-cov-13。有关详细信息,请参见此答案

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

results-clang

现在可以看到,使用正确的标志后结果是无法区分的。


3
我上周看到了您关于使用CMake预设的答案,所以首先要感谢您。这比使用工具链简单多了!您不使用configurePresets.environment来设置CCCXX有什么原因吗?您甚至可以使用继承来创建gcc和clang的覆盖率配置。将binaryDir设置为${sourceDir}/build/${presetName}也可能很有价值,以方便使用多个预设。 - Wes Toleman
@WesToleman - 不用谢!我恰好安装了五个版本的Clang(10-14)和三个版本的GCC(9-11),上述预设适用于所有八个版本。我认为为每个版本都继承gcc-coverage并没有什么问题,只是对于一个SO答案来说可能有点过了。现在也许有点“YAGNI”了。将${presetName}放入binaryDir路径中的建议很好,特别是当您开始添加多个预设时。同样,出于简洁起见而省略。 - Alex Reinking
2
请注意,您可以创建一个指向“llvm-cov”的符号链接gcov,而不是一个包装脚本。 - Johan Boulé

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接