在chromium项目中支持gcov单测覆盖率

背景

gcov是gcc中的一个附带工具,配合GCC编译进行覆盖测试使用

在gcc编译时,使用-fprofile-arcs -ftest-coverage两个编译参数,即可使用gcov的功能
在编译成功后,会生成.gcno文件,文件来自于编译过程中对目标文件打的桩生成的。
对可执行文件进行一次运行,即可得到.gcda文件
最终,用gcov工具对源代码进行处理,得到.gcov文件,即为我们需要的覆盖信息的初试状态

因为gcov生成的是纯文字,不好直观展现,lcov工具就是在gcov的基础上,生成html做了界面化展现。

我们准备在chromium项目(Cronet)中新增gcov单测覆盖率
chromium使用的是Google自研的gn & ninja,前者gn是用于生成类似Makefile的编译文件,后者进行真正的编译,产出对象文件
关于chromium的编译可以参考官方文档:chromium/build-instructions

实现

准备材料:

  • lcov 1.10版本
  • gcc 4.8.1及以上版本

实现步骤:

在build.gn中添加全局编译参数

1
2
3
4
5
6
7
//build/config/BUILDCONFIG.gn
if (is_linux && enable_coverage) {
_native_compiler_configs += [ "//build/config:gcov" ]
use_sysroot = false
assert(!use_sysroot, "sysroot images require clang, so disable it.")
assert(is_debug, "Target should be compiled in debug mode.")
}
1
2
3
4
5
//build/config/BUILD.gn
config("gcov") {
cflags = [ "-fprofile-arcs", "-ftest-coverage" ]
libs = [ "gcov" ]
}

enable_coverage即为全局gcov开关
编译的时候,
gn args out/default

1
2
3
4
5
use_sysroot = false
is_debug = true //打开debug开关
is_clang = false //不适应clang,即用gcc
symbol_level = 0
enable_coverage = true //打开gcov开关

执行gcov.sh,本地化lcov工具脚本

1
2
3
4
5
6
7
8
9
10
11
#/bin/bash
test_model="bdns_unittests" #需要编译的模块名
out_dir="./" #编译输出目录
COVERAGE_DIR="./out_html" #lcov输出文件夹
ninja -C $out_dir -j 11 $test_model #使用ninja进行编译
lcov -b ./ -c -i -d ./ -o coverage.init #使用lcov先跑一遍初始数据(处理gcno,信息输出到coverage.init)
$out_dir$test_model --test-launcher-jobs=11 --test-launcher-retry-limit=0 #运行可执行文件
lcov -b ./ -c -d ./ -o coverage.run #再跑一遍lcov(处理gcda,信息输出到coverage.run)
lcov -a coverage.init -a coverage.run -o coverage.total #两个文件合并成coverage.total
genhtml coverage.total --output-directory $COVERAGE_DIR --title "TurboNet Test Coverage" --show-details
--legend #产出html

最终,在out_html下就有我们需要的产出,以html形式。

踩坑点

1.pkg-config could not run

  • chromium为了更好支持跨机器,使用了pkg-config这个编译辅助工具,目的是为了快速找到编译依赖包。
  • 在配置完gn后,可能会出现xxx.pc无法找到,pkg-config cound not run的错误
    solution:
  • 去了解了一下pkg-config的作用后,明白这个错误其实就是环境变量没有添加好
  • 在/usr下找到所有xxx.pc的鬼东西,将其所在目录路径(可能是多个)添加到 export PKG_CONFIG_PATH中去

2.gcc/g++版本请务必使用4.8.1及以上的,为了支持C++11(gcc/g++ 4.6只认识-std=c++0x,这是C++11的早期标识,正式的是-std=gnu++11).

  • 如果不升级,会报一堆gcc的运行参数错误
  • ps.最开始不知道为啥我机器上4.6版本不支持c++11,后来找了一些资料才知道4.8.1之前的版本只认识早期c++11或者c++0x这种标识,官方推荐的应该是gnu++11,只在gcc 4.8.1开始

3.升级gcc后,可能你的gcov版本还是老版本(比如4.6)通过gcc/gcov -v可以查看版本。但其实你机器上已经存在了gcov-4.8了,可以去/usr/bin下查看

  • 如果gcc和gcov版本不一致,在gcov运行阶段,会报版本错误,可能最终能生成html,但覆盖率数据会有问题
    solution:
  • 替换/usr/bin下的gcov,这个gcov仅仅是一个软连接,删除它,新建一个gcov的软连接指向gcov-4.8即可

4.lcov版本问题,1.9版本的会出错

  • lcov1.9版本有代码bug(网上可以搜到,包括已经提交到了lcov官方),使用它可能会出现
  • geninfo: ERROR: xxx.gcno:reached unexpected end of file
    solution:
  • 使用lcov1.10版本,不会出现这个问题

5.gcov编译参数一定要加全局
cflags = [ “-fprofile-arcs”, “-ftest-coverage” ]这个编译参数一定要加到全局
当时为了节省效率,只对需要产出覆盖率的那部分source代码加上了gcov的编译参数
problem:

  • 这样做确实会节省很多时间,主要是在gcov处理方面会很快(只有你加了编译参数的source才会产出.gcno/.gcda文件)
  • 但是在真正运行时,会导致所有的ut全部crash
    solution:
  • 直接把编译参数加到全局(黄龑已经写好了,我们只需要打开enable_coverage开关即可)
  • 全局后,即便只是编译你的ut模块,但会对所有依赖的source产出gcno和gcda,而且都产出覆盖率
  • 编译和gcov处理时间会超过10+分钟(我只是对bdns作为目标产出),如果整体全局,估计会更长
  • 这个问题跟gcov本身原理有关,因为它会对代码进行特殊处理,可能是只对一部分代码加编译参数导致整体不一致的情况,ut执行的时候调用了gcov处理过的和没处理过的代码,触发了crash

6.修改代码,重新跑覆盖率时,gcno/gcda文件又出错

solution:

  • 最好gn clean一下,重新跑一遍全过程。
  • 或者至少清除产出目录下的 .gcno/.gcda文件。
  • 还是lcov的bug,如果连续跑覆盖率,不清除上一次产生的gcno/gcda文件,可能又会产生这些文件无法打开或读取的问题。