本文讲述了如何在C++代码中使用单元测试覆盖率工具lcov,以及gcov命令的使用。版本是lcov 2.0gcov 11.4.0

写在前面:lcov是我在实习期间初次接触到的工具,当时在配置的时候就遇到了大量中文互联网没有任何记录的问题。绝大部分博客对lcov工具的介绍仅停留在安装,并没有对它的使用和报告分析做出更进一步的详解,这也是慕雪撰写本文的原因。希望这篇文章能对需要使用lcov工具却又苦于没有引导教程的老哥提供一丝丝帮助。

1. 安装

安装lcov的方式比较简单,去github上下载官方的安装包就可以了。

1
2
3
4
5
6
7
8
9
10
11
# ubuntu安装依赖项
sudo apt-get install -y wget perl \
libcapture-tiny-perl libdatetime-perl \
libdatetime-format-dateparse-perl
# 下载
wget https://github.com/linux-test-project/lcov/releases/download/v2.0/lcov-2.0.tar.gz
# 解压
tar -zxvf lcov-2.0.tar.gz
cd lcov-2.0
# 安装
sudo make install

安装完毕后查看版本号,成功出现版本号则代表安装成功。

1
2
❯ lcov --version
lcov: LCOV version 2.0-1

更详细的lcov安装教程详见本站【Linux】lcov2.0安装和perl修改镜像源一文。另外,本文演示所用的单元测试框架Gtest也建议安装一下。需要说明的是,lcov的报告并不依赖于Gtest或任何测试框架,只要函数被调用、代码被运行了,它就可以生成覆盖率报告。

2. 基本命令

2.1. 手工执行

lcov的基本使用方式如下:

首先我们需要用g++命令编译gtest写出来的单元测试代码,使用-lgtest -lgtest_main -pthread链接gtest库和pthread库。选项-ftest-coverage可以让g++编译器在代码中插入额外的指令,来确认某部分的代码是否执行了,一般要和-fprofile-arcs连用才能产生完整的覆盖率报告。

程序运行后会产生.gcda.gcov.gcno文件,记录了覆盖率信息,lcov依赖于这些文件产生最终的html覆盖率报告。

1
2
3
g++ -std=c++17 test.cpp -o test \
-lgtest -lgtest_main -pthread \
-fprofile-arcs -ftest-coverage -fprofile-update=atomic

g++命令最后的-fprofile-update=atomic是lcov 2.0中需要新增的一个编译选项,否则运行lcov的时候会有告警(具体记不清了,最初的记录里面忘记写这一块的内容了)。

使用如上方式编译了单元测试的代码了之后,就可以执行lcov命令来生成报告了

1
2
3
4
5
lcov --capture \
--rc branch_coverage=1 \
--directory . \
--output-file coverage.info \
--ignore-errors mismatch

这个命令最终会生成一个coverage.info信息文件。其中--rc branch_coverage=1是用于开启分支检测的,不指定这个选项,输出的文件中将不包含分支覆盖率信息,只会有行覆盖率信息。选项--ignore-errors mismatch是因为lcov 2.0版本出现了一些问题,经常会找不到某些函数的符号表(不知道啥情况,lcov 1.6没有此告警),会有mismatch错误,需要将其忽略。

生成了coverage.info文件之后,再使用genhtml命令将其转化为最终的html报告,输出到coverage_report目录中。

1
2
3
genhtml coverage.info \
--rc branch_coverage=1 \
--output-directory coverage_report

一切顺利的话,执行了这些命令,你就可以在当前目录下的coverage_report子目录中找到lcov的html报告了。

2.2. makefile

我们可以把上述命令写入一个makefile中,这样可以方便我们执行命令。更新了测试源码之后,使用make locv就可以生成最新的覆盖率报告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
test:test.cpp
g++ -std=c++17 test.cpp -o test -lgtest -lgtest_main -pthread -fprofile-arcs -ftest-coverage -fprofile-update=atomic

lcov:test.cpp
g++ -std=c++17 test.cpp -o test -lgtest -lgtest_main -pthread -fprofile-arcs -ftest-coverage -fprofile-update=atomic && \
./test && \
gcov -b -c -o . test.cpp && \
lcov --capture \
--rc branch_coverage=1 \
--directory . \
--output-file coverage_all.info \
--ignore-errors mismatch && \
genhtml coverage.info \
--rc branch_coverage=1 \
--ignore-errors mismatch \
--output-directory coverage_report && \
rm *.info

.PHONY:cl
cl:
sudo rm -rf test *.gcno *.gcda *.gcov out

3. Demo演示

3.1. 基本demo

下面是一个最简单的C++代码,以及对应的测试处理,首先在main.hpp里面定义了一个最基础的相减函数

1
2
3
4
5
6
7
8
9
// 相减函数
int Sub(int a, int b)
{
if (a > b)
{
return a - b;
}
return b - a;
}

随后,在test.cpp中引用这个头文件并调用Sub函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <gtest/gtest.h>
#include "main.hpp"

TEST(SubTest, SubTest1)
{
EXPECT_EQ(Sub(3, 2), 1); // 期望 result 等于 1
}

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

使用上文提到的命令,编译和创建lcov报告。在lcov命令的最后输出中,会包含如下覆盖率信息

1
2
3
4
Overall coverage rate:
lines......: 50.3% (83 of 165 lines)
functions......: 46.3% (44 of 95 functions)
branches......: 42.9% (24 of 56 branches)

因为我用的是WSL2,可以方便的直接打开报告生成目录,查看index.html文件。

image.png

可以看到,报告中列出了所有涉及到的文件,以及这些文件的行覆盖率,分支覆盖率,执行次数。

image.png

但是!这里面有大量C++库函数以及gtest库的代码,我们自己的代码反而被掩盖过去了,这肯定不是我们想要的结果。毕竟不是自己写的代码都不需要测试覆盖率。所以我们需要做点操作,屏蔽掉所有库函数的报告。

将上文的lcov命令的最终输出文件改成coverage_all.info

1
2
3
4
5
lcov --capture \
--rc branch_coverage=1 \
--directory . \
--output-file coverage_all.info \
--ignore-errors mismatch

然后在genhtml命令之前,执行如下命令。这个命令会处理原本生成的全量数据,把里面我们不想要的东西全都删除掉,再生成一个coverage.info文件。

1
2
3
4
5
6
7
lcov --remove coverage_all.info \
'*/usr/include/*' '*/usr/lib/*' '*/usr/lib64/*' \
'*/usr/local/include/*' '*/usr/local/lib/*' '*/usr/local/lib64/*' \
--rc branch_coverage=1 \
--output-file coverage.info \
--ignore-errors unused \
--ignore-errors mismatch

随后再执行genhtml命令,这一次生成的报告文件就只有我们自己的代码了。

1
2
3
genhtml coverage.info \
--rc branch_coverage=1 \
--output-directory coverage_report

image.png

进入报告中看,其实这里还是有一个需要排除的项目的,即test.cpp是单元测试的文件,我们也不需要关注单元测试这个文件本身的覆盖率正常不,当前我们只需要关注main.hpp这个功能源码文件的覆盖率。

可以将test.cpp也写入上文的--remove选项之后,这样它也会被过滤掉。实际项目中,直接过滤单元测试代码文件的目录即可。

另外,使用这个命令,lcov会在输出中报告没有被匹配上的地址,可以用--ignore-errors unused来屏蔽这个告警。

1
2
3
4
5
6
7
lcov: WARNING: ('unused') 'exclude' pattern '*/usr/lib/*' is unused.
lcov: WARNING: ('unused') 'exclude' pattern '*/usr/lib64/*' is unused.
(use "lcov --ignore-errors unused,unused ..." to suppress this warning)
lcov: WARNING: ('unused') 'exclude' pattern '*/usr/local/lib/*' is unused.
(use "lcov --ignore-errors unused,unused ..." to suppress this warning)
lcov: WARNING: ('unused') 'exclude' pattern '*/usr/local/lib64/*' is unused.
(use "lcov --ignore-errors unused,unused ..." to suppress this warning)

3.2. 报告基本分析

image.png

点开main.hpp文件,可以看到如下报告。其中右上角是当前文件的覆盖率信息,然后会展示文件的源码:

  • 源码每一行之前的数字是这一行被运行了几次;
  • 底色为橙色标注的,就是没有被覆盖的行;
  • 蓝色标注的,则是被覆盖了的行;

在每一个if语句的分支点,也会产生一个分支覆盖率报告,这里显示的[+,-]代表if条件为true的分支被命中了,为false的分支没有命中。在测试代码中我使用的是Sub(3, 2)来调用该函数,参数a是大于b的(命中true分支),也和这里的分支覆盖报告相符。

image.png

这样我们就可以知道,当前需要怎么补充测试用例了。我们需要补充一个b比a大或者相等的测试用例,追加如下测试调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <gtest/gtest.h>
#include "main.hpp"

TEST(SubTest, SubTest1)
{
EXPECT_EQ(Sub(3, 2), 1); // 期望 result 等于 1
EXPECT_EQ(Sub(2, 4), 2); // 期望 result 等于 2
EXPECT_EQ(Sub(3, 3), 0); // 期望 result 等于 0
}

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

再次编译执行,重新查看报告。此时可以看到,我们的main.hpp已经实现了100%的覆盖。

image.png

删除其他测试,留一个b比a大的测试用例。此时可以看到,分支覆盖显示为[-,+],代表我们当前分支的false条件被命中了,但是true条件没有。

image.png

简单总结,单个分支覆盖率中[]的逗号左侧是true,右侧是false;+代表覆盖,-代表没有覆盖,#代表这个分支没有被执行。

3.3. 多条件判断

上面的if语句中我们只写了一个判断条件,实际场景中判断条件不止一个的情况还是经常出现的,给Sub函数新增一个参数,再来进行测试。

1
2
3
4
5
6
7
8
9
10
#include <cstdbool>
// 相减函数,默认是A-B,第三个参数为是否要返回绝对值
int Sub(int a, int b, bool isAbs)
{
if (b < a && isAbs)
{
return b - a;
}
return a - b;
}

测试用例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <gtest/gtest.h>
#include "main.hpp"

TEST(SubTest, SubTest1)
{
// 传入false代表我们想a-b,不需要绝对值
EXPECT_EQ(Sub(2, 4, false), -2); // 期望 result 等于 -2
}

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

这里可以看到,在if语句左侧的分支覆盖率条件的[]里面多了一对加减,这里多的就是isAbs这个判断条件,分支覆盖率中的每一个判断条件都有一个true/false分支,两两一对,从左到右的顺序和我们的判断条件中的条件顺序是一致的。

在上面的测试用例中,我们传入了b大于a的值,同时isAbs是false,命中了b > a为true的分支,和isAbs为false的分支。

image.png

新增一个测试用例,这一次命中的是isAbs为true的分支。

1
EXPECT_EQ(Sub(2, 4, true), 2);

报告中,isAbs为true的分支也变成了+代表已命中,符合预期。

image.png

再添加一个a比b大的测试用例,即可将该函数的所有分支覆盖完毕。

1
EXPECT_EQ(Sub(6, 4, true), 2);

image.png

4. 引入gcov命令

4.1. 基本使用

接下来给大家引入gcov命令的使用,gcov命令可以生成更加详细的关于某个分支为什么没有被覆盖的说明。比如未覆盖的异常分支在生成的源文件.gcov文件中就会显示出来。

gcov命令和gcc/g++是同源的,只要你的系统上安装了gcc,那就会有gcov命令。二者的版本号输出都是一致的。

1
2
3
4
5
6
7
8
9
10
❯ gcov --version          
gcov (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
❯ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

执行过lcov命令后,在构建目录下会产生很多的.gcov文件,和我们自己的代码的.gcda文件

1
2
3
4
5
6
ls
alloc_traits.h.gcov gtest-assertion-result.h.gcov main.hpp stl_iterator_base_funcs.h.gcov test.gcda
basic_string.h.gcov gtest.h.gcov main.hpp.gcov stl_iterator_base_types.h.gcov test.gcno
basic_string.tcc.gcov gtest-internal.h.gcov makefile test tuple.gcov
char_traits.h.gcov gtest-port.h.gcov move.h.gcov test.cpp type_traits.h.gcov
coverage_report gtest-printers.h.gcov new_allocator.h.gcov test.cpp.gcov unique_ptr.h.gcov

我们可以使用gcov命令,把这些文件文件转换成可读报告。

1
gcov -b -c -o .gcda文件所在路径 cpp源文件路径

比如这里我们要处理的是test.gcda,命令如下。

1
gcov -b -c -o . test.cpp

这个命令会生成一个源文件名.gcov文件,文件中就会有详细的文字说明了。比如main.hpp.gcov文件中的描述如下,有每一个分支被命中的次数,后面有个括号是这个分支的说明。

文件中第一列的-:代表这一行不统计命中次数,第一列数字:代表这一行被执行的次数(注意要和第二列的代码行号区分开)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        -:    0:Source:main.hpp
-: 0:Graph:./test.gcno
-: 0:Data:./test.gcda
-: 0:Runs:1
-: 1:#include <cstdbool>
-: 2:// 相减函数,默认是A-B,第三个参数为是否要返回绝对值
function _Z3Subiib called 3 returned 100% blocks executed 100%
3: 3:int Sub(int a, int b, bool isAbs)
-: 4:{
3: 5: if (b > a && isAbs)
branch 0 taken 2 (fallthrough)
branch 1 taken 1
branch 2 taken 1 (fallthrough)
branch 3 taken 1
-: 6: {
1: 7: return b - a;
-: 8: }
2: 9: return a - b;
-: 10:}

文件中的(fallthrough)代表当前if分支被跳过。比如下面的代码中,fallthrough的意思就是当a不等于b的时候,分支A会被跳过,走到分支B中。

1
2
3
4
5
if (a == b){
// A
} else {
// B
}

在上面的例子中,当b小于等于a或者isAbs为假的时候,return b - a;就会被跳过,落到return a - b;分支中。

这里将isAbs改成一个函数调用,函数本身参数不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cstdbool>
#include <exception>

bool isAbsFunc(int a, int b, bool isAbs)
{
// 当a为100的时候抛出异常
if (a == 100)
{
throw std::invalid_argument("a should not be 100.");
}
return isAbs;
}

// 相减函数,默认是A-B,第三个参数为是否要返回绝对值
int Sub(int a, int b, bool isAbs)
{
if (b > a && isAbsFunc(a, b, isAbs))
{
return b - a;
}
return a - b;
}

此时分支覆盖率报告如下。因为我们当前的a并不等于100,所以一直命中的都是a == 100为false的分支,符合预期。但是这里会有一个额外的分支未覆盖情况,即throw这一行也出现了一个分支,且[]里面的两个符号都是#代表这一行没有被运行

image.png

我们可以用gcov命令来看看throw这一行的分支覆盖情况。可以看到这里提示never executed,没有运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
        -:    0:Source:main.hpp
-: 0:Graph:./test.gcno
-: 0:Data:./test.gcda
-: 0:Runs:1
-: 1:#include <cstdbool>
-: 2:#include <exception>
-: 3:
function _Z9isAbsFunciib called 2 returned 100% blocks executed 50%
2: 4:bool isAbsFunc(int a, int b, bool isAbs)
-: 5:{
-: 6: // 当a为100的时候抛出异常
2: 7: if (a == 100)
branch 0 taken 0 (fallthrough)
branch 1 taken 2
-: 8: {
#####: 9: throw std::invalid_argument("a should not be 100.");
call 0 never executed
call 1 never executed
branch 2 never executed
branch 3 never executed
call 4 never executed
call 5 never executed
-: 10: }
2: 11: return isAbs;
-: 12:}
-: 13:
-: 14:// 相减函数,默认是A-B,第三个参数为是否要返回绝对值
function _Z3Subiib called 3 returned 100% blocks executed 100%
3: 15:int Sub(int a, int b, bool isAbs)
-: 16:{
3: 17: if (b > a && isAbsFunc(a, b, isAbs))
branch 0 taken 2 (fallthrough)
branch 1 taken 1
call 2 returned 2
branch 3 taken 1 (fallthrough)
branch 4 taken 1
branch 5 taken 1 (fallthrough)
branch 6 taken 2
-: 18: {
1: 19: return b - a;
-: 20: }
2: 21: return a - b;
-: 22:}

那我们加一个a等于100的测试用例呢?

1
2
// 期望抛出异常
EXPECT_ANY_THROW(Sub(100, 400, true));

此时gcov文件会是如下模样,我们a == 100的两个分支都命中了,但是你会发现,它有一个branch 3 taken 0 (throw)为0次命中,没有被覆盖上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function _Z9isAbsFunciib called 3 returned 67% blocks executed 88%
3: 4:bool isAbsFunc(int a, int b, bool isAbs)
-: 5:{
-: 6: // 当a为100的时候抛出异常
3: 7: if (a == 100)
branch 0 taken 1 (fallthrough)
branch 1 taken 2
-: 8: {
1: 9: throw std::invalid_argument("a should not be 100.");
call 0 returned 1
call 1 returned 1
branch 2 taken 1 (fallthrough)
branch 3 taken 0 (throw)
call 4 returned 0
call 5 never executed
-: 10: }
2: 11: return isAbs;
-: 12:}

在lcov报告中也是如此,会显示有一个没有覆盖的(throw)抛异常分支。

image.png

4.2. lcov过滤std库函数造成的分支

这就涉及到lcov的一个不那么容易找到的设置了,当时百度了老久,最后还是去Github翻issue才得到的答案。下面贴出几个相关的issue

简而言之,lcov支持过滤掉这类由std库造成的无法覆盖的异常分支。只需要在lcov命令和genhtml命令中加上--filter branch选项即可。添加了这个命令后,可以看到throw这一行的分支被过滤不显示了。

image.png

即便我们没有命中a == 100的情况,throw这一行也不会出现[##]的未命中分支。

image.png

再举个map的emplace的例子,代码如下

1
2
3
4
5
void EmplaceMap(int key, int value)
{
static std::map<int, std::set<int>> mapValue;
mapValue[key].emplace(value);
}

测试代码

1
2
3
4
5
TEST(EmplaceMapTest,EmplaceMapTest1)
{
EXPECT_NO_THROW(EmplaceMap(1,2));
EXPECT_NO_THROW(EmplaceMap(1,3));
}

可以看到,如果不加上--filter branch过滤选项,在lcov报告中,即便这一行是完全不存在任何分支的,也会出现一个未覆盖的情况。

image.png

生成的gcov文件如下,这里会有一个不知道什么由来的branch 4没有被覆盖到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function _Z10EmplaceMapii called 2 returned 100% blocks executed 100%
2: 6:void EmplaceMap(int key, int value)
-: 7:{
2: 8: static std::map<int, std::set<int>> mapValue;
branch 0 taken 1 (fallthrough)
branch 1 taken 1
call 2 returned 1
branch 3 taken 1 (fallthrough)
branch 4 taken 0
call 5 returned 1
call 6 returned 1
call 7 returned 1
2: 9: mapValue[key].emplace(value);
call 0 returned 2
call 1 returned 2
2: 10:}
-: 11:

加上了--filter branch过滤选项之后,这一行则完全不会有分支覆盖率信息。这才是我们预期的输出,因为我们不应该关注不是我们自己写的代码(比如std库和第三方库)中的分支。

image.png

另外,过滤选项默认只对常见的cpp头文件起效。对诸如.inl这种头文件是不起效果的。可以使用如下命令,修改默认的c_file_extensions后缀名配置,添加你需要的文件后缀。

1
--rc c_file_extensions=c,cpp,hpp,h,inl

关联issue:https://github.com/linux-test-project/lcov/issues/250

5. 一些lcov报告问题的记录

经过上面的步骤,想必你已经知道怎么去使用lcov了。下面是我在使用lcov过程中遇到的一些报告的共性问题,记录于此,经供参考。

5.1. lcov屏蔽语法

lcov本身也支持通过在代码中添加注释的方式来屏蔽一些代码的覆盖率检测。屏蔽的语法分为单行代码和多行屏蔽。

1
2
3
4
5
// LCOV_EXCL_BR_START
多行代码
// LCOV_EXCL_BR_STOP

单行代码 // LCOV_EXCL_BR_LINE

在实际代码中,可能会有一些linux库函数调用这类难以复现失败场景的函数调用,又没有办法被过滤掉的分支。这种情况就可以在注明原因以后,使用lcov的屏蔽注释将其屏蔽掉,让最终生成的报告里面没有这些难以覆盖的错误情况。

5.2. assert假分支无法覆盖

如下图所示,lcov的assert始终只会覆盖假的分支,因为分支为真的时候就直接程序终止了。

image.png

Gtest中有一个EXPECT_DEATH可以用来测试assert为真的情况,但即便使用了这个宏,lcov和gcov依旧无法生成命中的报告。所以,推荐的做法是在编译单元测试代码的时候使用-DNDEBUG宏直接禁用所有assert,这样就不会有关于assert的分支覆盖率报告了。

5.3. trylock分支覆盖

一般情况下,在我们的测试场景中不太好复现try_lock()函数调用失败的分支,这需要有一个多线程的场景,但多线程操作共享资源的运行顺序本身就是不可预知的,不太好在单元测试中构建出一个一定冲突的场景来。

image.png

这时候可以用一种黑魔法,在单元测试中,取出类的私有成员变量mutex,将其lock了之后,再去调用包含try_lock()调用和判断的函数。函数调用完毕后,再unlock解锁。

如下是这个黑魔法的源码和使用示例,注意只有g++使用-std=c++17之后才支持编译这个代码。在windows的vs2019下这个特性是编译不过的,即便设置了C++17也不行,可能是我的配置不对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once
// C++17才支持,通过友元和元组,取出任意成员变量
template <class T, auto... Member>
struct StealMember
{
friend auto StealClass(T &t)
{
return std::make_tuple(Member...);
}
};

// 使用示例:
// // 1.友元函数声明,TestClass是我们需要操作的目标类。
auto StealClass(TestClass &t);
// // 2.在下面的模板中添加需要的私有函数或成员。
template class StealMember<TestClass,
&TestClass::GetA, &TestClass::_a, &TestClass::_b>;
// // 3.在需要的函数中使用如下方式取成员变量。
void TestFunc() {
TestClass t1(20, 300.23);
auto tp = StealClass(t1); // 构建元组
cout << "GetA: " << (t1.*(std::get<0>(tp)))() << endl;
cout << "_a: " << (t1.*(std::get<1>(tp))) << endl;
}

这个代码首先声明了友元函数,StealClass会被声明成TestClass的友元函数,从而可以读取到该类的私有成员。随后在实例化StealMember模板的时候,指定了目标类和其私有成员,这样StealClass函数就可以在调用的时候,给我们返回一个包含私有成员指针的元组

重点来了:C++在实例化模板的时候,不会去检查成员的访问限定符。

有了元组,就可以用std::get<元组内元素下标>(元组对象)的方式取出元组的某一个成员,即私有成员的指针。有了私有成员的指针之后,我们就可以使用对象.(*私有成员指针)的方式访问到一个私有成员变量或者成员函数了。

关于这个特性的更多介绍,可以参考下面的资料

咋样,是不是很“黑魔法”呢?

5.4. string相加的时候会有大量无法覆盖的异常分支

如下图所示,这个函数中调用了string的相加操作,造成了大量的没有覆盖的异常分支。

image.png

在gcov报告中可以更详细的看到没有被覆盖的分支都是什么,大多都是和throw有关的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function _Z16test_string_plusRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES6_ called 2 returned 100% blocks executed 69%
2: 48:void test_string_plus(const string& local ,const string& remote)
-: 49:{
2: 50: static string recv_msg;
branch 0 taken 1 (fallthrough)
branch 1 taken 1
call 2 returned 1
branch 3 taken 1 (fallthrough)
branch 4 taken 0
call 5 returned 1
call 6 returned 1
call 7 returned 1
2: 51: static_cast<void>(recv_msg.assign("/" + (local < remote ? local + "_" + remote : remote + "_" + local)));
call 0 returned 2
branch 1 taken 1 (fallthrough)
branch 2 taken 1
call 3 returned 1
branch 4 taken 1 (fallthrough)
branch 5 taken 0 (throw)
call 6 returned 1
branch 7 taken 1 (fallthrough)
branch 8 taken 0 (throw)
call 9 returned 1
branch 10 taken 1 (fallthrough)
branch 11 taken 0 (throw)
call 12 returned 1
branch 13 taken 1 (fallthrough)
branch 14 taken 0 (throw)
call 15 returned 2
branch 16 taken 2 (fallthrough)
branch 17 taken 0 (throw)
call 18 returned 2
call 19 returned 2
call 20 returned 2
branch 21 taken 1 (fallthrough)
branch 22 taken 1
call 23 returned 1
branch 24 taken 1 (fallthrough)
branch 25 taken 1
call 26 returned 1
call 27 never executed
branch 28 never executed
branch 29 never executed
call 30 never executed
branch 31 never executed
branch 32 never executed
call 33 never executed
2: 52:}

可当前我已经添加了过滤命令了,为什么没有生效呢?

实际上,将上面的代码改成下面的if/else逻辑,就不会有这么多的异常分支了。

image.png

这是我的猜想:lcov的过滤命令在检测到某一行中有用户定义的判断条件local < remote的时候就会失效,因为可能会错误过滤掉用户自己的分支。与其错报一万不可少报一个,于是就把所有的异常分支都展现出来了。

三目运算符改成if/else了之后,用户定义的判断条件和string的相加操作隔离开了,就能正常进行过滤了。所以,在编写优化分支覆盖率的代码的时候,可以考虑将

6. The end

其实在最开始的时候我记录了更多lcov相关的错误,但大部分错误都可以使用--filter branch选项过滤掉,且有一部分错误在我当前的环境中并没有被复现出来,故此不记录于本文中。

如果你遇到了本文没有记录的问题,欢迎在评论区留言交流。