使用C++编写的LCOV/GCOV分支覆盖产生了各种各样的分支。

29
我们正在使用 LCOV/GCOV 生成项目的测试覆盖率。最近我们尝试额外开启分支覆盖率,但是从高级开发者的角度来看,这似乎并没有产生我们期望的结果。
在 C++ 中使用分支覆盖率会让报告中到处都是分支,我们怀疑(因为搜索问题时表明)大部分异常处理代码造成了这些“隐藏分支”。而 GCOV/LCOV 似乎无法跳过这些分支。
我创建了一个小的测试项目来展示这个问题:https://github.com/ghandmann/lcov-branch-coverage-weirdness 目前我们使用带有以下组件的 Ubuntu 16.04:
- gcc v5.4 - lcov & genhtml v1.12
我们的生产代码使用启用了 c++11 的编译器。最小化的示例未使用启用 c++11,但是我们尝试了所有不同选项(c++ 标准,优化,-fno-exceptions),却没有得到可接受的结果。
有人有什么想法吗?提示?我们是否错误地使用了任何东西?这是否是 - 如其他地方所述 - 真正的预期行为?
更新:
正如在 gcc-help 邮件列表 中指出的那样,这些“隐藏分支”是由于异常处理引起的。因此,在 gcc 中添加 -fno-exceptions 开关可以为“简单”的程序产生 100% 的分支覆盖率。但是当禁用异常时,gcc 拒绝编译实际使用异常的代码(例如 try-catch、throw)。因此对于真正的生产代码来说,这不是一个选择。看起来在这种情况下,您必须将 ~50% 的覆盖率声明为新的 100%。 ;)

正如 maxschlepzig 所解释的那样,您实际上需要“部分分支覆盖”,因为您明确希望排除一些异常部分(这通常很有用)。 要实现这一点,您可能希望过滤结果,请参见 maxschlepzig 的答案。 - Simon Sobisch
6个回答

18
事实上,GCC还为每一行记录分支信息,以便在某个抛出异常导致作用域退出的位置(例如在Fedora 25上使用GCC 6.3.1和lcov 1.12)。

这些信息的价值有限。分支覆盖数据的主要用例是具有多句逻辑表达式的复杂if语句,如下所示:

if (foo < 1 && (bar > x || y == 0))

假如你想要确认你的测试套件是否也涵盖了bar > x这种情况,或者你只有包含y == 0的测试用例。

为此,分支覆盖率数据收集和使用lcov的genhtml进行可视化是很有用的。对于像下面这样简单的if语句

if (p == nullptr) {
  return false;
}
return true;

你不需要分支覆盖率数据,因为通过查看以下行的覆盖范围,你可以看到该分支是否被执行。

lcov生成的genhtml的输入采用了相对简单的文本格式(参见geninfo(1))。因此,您可以进行后处理,删除所有以BRDA:开头且不属于if语句的行。例如,可以参考实现了这种方法的filterbr.py。还可以查看gen-coverage.py,了解其他的lcov/genhtml处理步骤,并查看一个示例项目,其中生成的跟踪文件上传到codecov(codecov不使用genhtml,但可以导入lcov跟踪文件并显示分支覆盖数据)

(非)替代方案

  • 当你的C++代码不使用异常时,禁用异常只是一个选项
  • 使用类似-O1 -fno-omit-frame-pointer -fno-optimize-sibling-calls编译可以减少记录的分支覆盖数据数量,但并不多
  • Clang支持GCOV风格的覆盖率收集,但也实现了一种不同的方法,称为'基于源码的代码覆盖率'(使用-fprofile-instr-generate -fcoverage-mapping编译,并使用llvm-profdatallvm-cov进行后处理)。然而,该工具链不支持分支覆盖率数据(截至2017-05-01)。
  • 默认情况下,lcov+genhtml不生成分支覆盖率数据——有时您确实不需要它(参见上文),因此,禁用它是一个选项(参见--rc lcov_branch_coverage=0--no-branch-coverage

当我使用GNAT Community 2018编译Ada以进行覆盖测试时,我遇到了同样的问题。这个版本使用GCC 7.3.1,在使用--coverage和没有优化的情况下,分支覆盖被扩大了。添加-O1 -fno-omit-frame-pointer -fno-optimize-sibling-calls选项可以减少记录的Ada分支覆盖量。 - Chester Gillon

7

GCC将增加一些异常处理的内容,特别是在函数调用时。

您可以通过在构建中添加-fno-exceptions -fno-inline来解决此问题。

我应该补充说明,您可能只想在测试时开启这些标志。因此,可以像这样设置:

g++ -O0 --coverage -fno-exceptions -fno-inline main.cpp -o test-coverage 

1
感谢您的发现。但是,由于我们正在使用异常处理,因此“-fno-exceptions”不是一个选项。如果您正在使用异常处理并禁用它们,GCC甚至会拒绝首先编译代码。看起来,这种行为没有真正的解决方案。 - Sven Eppler
好的。但这就是问题所在。您可以将异常分解到不同的单元中,并仅链接到测试文件。 - user2976957
lcov/gcov库在汇编级别检查分支。您可以查看汇编输出以查看它缺少的分支(即由异常处理引起的分支)。要查看输出汇编,请使用以下命令:objdump -S --disassemble [your_exe] > asm_output - user2976957

5

1
我们很久以前就离开了,放弃了整个项目(好吧,是客户放弃了它:D),但仍然很高兴看到还有一些进展! - Sven Eppler

1
您可以尝试使用g++ -O3 --coverage main.cpp -o testcov。我已经在您的文件上使用g++-5.4尝试过,它可以正常工作,这意味着使用标准printf和string调用时会丢弃异常。
实际上,除了O0之外的任何优化标志都会导致gcov忽略在CPP文件中生成的普通标准库调用引发的异常。我不确定普通异常是否也会被优化掉(我认为不会,但还没有尝试过)。
但是,我不确定您的项目是否有任何要求只能使用O0与您的代码配合,而不能使用O1O2O3甚至Os

1
优化器可能会省略异常处理代码,如果它能看到不会抛出异常,但是在使用大型项目或使用在堆耗尽时抛出异常的STL容器时,您是否能够获得令人满意的结果? - John McFarlane

1

我做了一些工作,将过滤器添加到geninfo/lcov/genhtml中,以删除与源代码行不包含任何条件语句相关联的C/C++代码分支-使用一些相对简单的正则表达式。这些过滤器似乎在我们的代码库/使用修改后的lcov工具的产品中有效。这些过滤器并不完美,并且很容易被顽固的用户打败。

最近我得到批准将我的lcov更新上游。您可以在https://github.com/henry2cox/lcov找到它们。

此版本增加了差异覆盖率、日期和所有者分组的支持。

一个附加的外围变化是添加“过滤”,如上所述-主要是因为分支覆盖似乎对于没有它的C++代码不可用。
您可以在方法lcovutil :: ReadCurrentSource :: containsConditional中找到(无论如何都很粗糙且很容易被规避的)正则表达式-靠近... /bin/lcovutil.pm:1122行

虽然不完美:但这个黑客似乎适用于我们的代码。您的情况可能会有所不同。

这是使用perl/5.12.5和gcc/8.3.0以及9.2.0进行测试的。它也可能适用于其他版本(如果您发现可移植性问题,请告诉我,以便我可以修复它们)。


1
嗨,亨利;欢迎来到Stack Overflow。你之前的回答(不是评论)被删除了,因为它没有任何有用的信息。这个答案至少包括了一个指向GitHub仓库的链接,所以更加有用。查看帮助中心获取更多关于在这里回答问题的信息和建议。 - TylerH
谢谢你的回答和你对 lcov 的分支,Henry。我会评估你的项目分支。不过,如果你能给其他人一些使用提示就更好了。从上游拉取请求中我了解到,你复制了一些工具并用了不同的名称,所以在使用你的分支时唯一的区别应该就是这个了吧? - Alejandro Exojo
看起来HenryCox的修改已经合并到lcov主分支中,但尚未打标签(直到2023-03-10左右,如果我错了请纠正)。对于不知道如何使用的人,请在lcov github主分支的genhtml中检查--filter(即man/genhtml.1)。与旧版分支覆盖的使用相比,没有其他变化。(假设您已经知道设置lcov_branch_coverage=1)我已经测试过了,虽然在没有if/switch的行上仍然存在一些分支缺失警告,但是95%可用,我想。感谢你的工作! - Lhfcws

-1

我刚遇到了同样的问题,我想要摆脱由于异常而导致的这些未覆盖的分支。我为自己找到了一个合适的解决方案:

我只是避免在我想要覆盖的代码中直接使用“throw exception”。我设计了一个类,它提供了一些方法来抛出异常。由于异常类并不是那么复杂,所以我并不真的关心覆盖率,所以我只是用 LCOV_EXCL_START 和 LCOV_EXCL_STOP 排除了所有内容。或者,我也可以仅关闭该异常类的分支覆盖。

我承认,这不是一个直截了当的解决方案,但对于我的目的来说,它是完美的,因为还有其他原因(我需要该异常类具有灵活性,以便我可以提供不同的实现:一次抛出异常,另一次执行其他操作)。


1
这根本不是解决方案,而是鼓励要么删除异常使用,要么完全排除对它们的覆盖。这可能适用于您的个人项目,但在许多情况下并不适用。 - kevr
我认为你没有理解这个解决方案。它并不是阻止使用异常 - 它只是将其封装起来。这使得你在处理异常时更加灵活。 - tangoal

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