iOS 覆盖率检测原理与增量代码测试覆盖率工具实现15

打印 上一主题 下一主题

主题 883|帖子 883|积分 2649

 背景

对苹果开发者而言,由于平台审核周期较长,客户端代码导致的线上题目影响时间往往比力久。如果在开发、测试阶段能够提前袒露题目,就有助于避免线上变乱的发生。代码覆盖率检测正是帮助开发、测试同学提前发现题目,保证代码质量的好帮手。
对于开发者而言,代码覆盖率可以反馈两方面信息:

  • 自测的充实程度。
  • 代码设计的冗余程度。
只管代码覆盖率对代码质量有着上述利益,但在 iOS 开发中却利用的不多。我们调研了市场上常用的 iOS 覆盖率检测工具,这些工具主要存在以下四个题目:

  • 第三方工具有时生成的检测陈诉文件会出错甚至会失败,开发者对覆盖率生成原理不了解,遇到这类题目容易弃用工具。
  • 第三方工具每次展示全量的覆盖率陈诉,会分散开发者的很多精力在未修改部分。而在绝大多数情况下,开发者的关注意点在本次新增和修改的部分。
  • Xcode 自带的覆盖率检测只适用于单元测试场景,由于需求变更频仍,业务团队开发单元测试的本钱很高
  • 已有工具很难和现有开发流程联合起来,须要额外举行测试,运行覆盖率脚本才能获取陈诉文件。
为了解决上述题目,我们深入调研了覆盖率陈诉的生成逻辑,并联合团队的开发流程,开发了一套嵌入在代码提交流程中、基于单次代码提交(git commit)生成陈诉、对开发者透明的增量代码测试覆盖率工具。开发者只须要正常开发,通过模拟器测试开发代码,commit 本次代码(commit 和测试顺序可交换),推送(git push)到远端,就可以在当地看到这次提交代码的具体覆盖率陈诉了。
本文分为两部分,先从介绍通用覆盖率检测的原理出发,让读者对覆盖率的网络、解析有直观的认识。之后介绍我们增量代码测试覆盖率工具的实现。
覆盖率检测原理

生成覆盖率陈诉,首先须要在 Xcode 中设置编译选项,编译后会为每个可执行文件生成对应的 .gcno 文件;之后在代码中调用覆盖率分发函数,会生成对应的 .gcda 文件。
其中,.gcno 包含了代码计数器和源码的映射关系, .gcda 记录了每段代码具体的执行次数。覆盖率解析工具须要联合这两个文件给出末了的检测报表。接下来先看看 .gcno 的生成逻辑。
.gcno

利用 Clang 分别生成源文件的 AST 和 IR 文件,对比发现,AST 中不存在计数指令,而 IR 中存在用来记录执行次数的代码。搜索 LLVM 源码可以找到覆盖率映射关系生成源码。覆盖率映射关系生成源码是 LLVM 的一个 Pass,(下文简称 GCOVPass)用来向 IR 中插入计数代码并生成 .gcno 文件(关联计数指令和源文件)。
下面分别介绍IR插桩逻辑和 .gcno 文件布局。
IR 插桩逻辑

代码行是否执行到,须要在运行中统计,这就须要对代码自己做一些修改,LLVM 通过修改 IR 插入了计数代码,因此我们不须要改动任何源文件,仅需在编译阶段增加编译器选项,就能实现覆盖率检测了。
从编译器角度看,基本块(Basic Block,下文简称 BB)是代码执行的基本单元,LLVM 基于 BB 举行覆盖率计数指令的插入,BB 的特点是:

  • 只有一个入口。
  • 只有一个出口。
  • 只要基本块中第一条指令被执行,那么基本块内全部指令都会顺序执行一次
覆盖率计数指令的插入会举行两次循环,外层循环遍历编译单元中的函数,内层循环遍历函数的基本块。函数遍历仅用来向 .gcno 中写入函数位置信息,这里不再赘述。
一个函数中基本块的插桩方法如下:

  • 统计全部 BB 的后继数 n,创建和后继数大小雷同的数组 ctr[n]。
  • 以后继数编号为序号将执行次数依次记录在 ctr 位置,对于多后继情况根据条件判断插入。
举个例子,下面是一段猜数字的游戏代码,当玩家猜中了我们预设的数字10的时间会输出Bingo,否则输出You guessed wrong!。这段代码的控制流程图如图1所示(猜数字游戏 )。
  1. - (void)guessNumberGame:(NSInteger)guessNumber
  2. {
  3.     NSLog(@"Welcome to the game");
  4.     if (guessNumber == 10) {
  5.         NSLog(@"Bingo!");
  6.     } else {
  7.         NSLog(@"You guess is wrong!");
  8.     }
  9. }
复制代码
这段代码如果开启了覆盖率检测,会生成一个长度为 6 的 64 位数组,对照插桩位置,方括号中标记了桩点序号,图 1 中代码前数字为所在行数。


图 1 桩点位置
.gcno计数符号和文件位置关联

.gcno 是用来保存计数插桩位置和源文件之间关系的文件。GCOVPass 在通过两层循环插入计数指令的同时,会将文件及 BB 的信息写入 .gcno 文件。写入步骤如下:

  • 创建 .gcno 文件,写入 Magic number(oncg+version)。
  • 随着函数遍历写入文件地点、函数名和函数在源文件中的起止行数(标记文件名,函数在源文件对应行数)。
  • 随着 BB 遍历,写入 BB 编号、BB 起止范围、BB 的后继节点编号(标记基本块跳转关系)。
  • 写入函数中BB对应行号信息(标注基本块与源码行数关系)。
从上面的写入步骤可以看出,.gcno 文件布局由四部分组成:


  • 文件布局
  • 函数布局
  • BB 布局
  • BB 行布局
通过这四部分布局可以完全还原插桩代码和源码的关联,我们以 BB 布局 / BB 行布局为例,给出布局图 2 (a) BB 布局,(b) BB 行信息布局,在本章末尾覆盖率解析部分,我们利用这个布局图还原代码执行次数(每行等高格代表 64bit):


图2 BB 布局和 BB 行信息布局
.gcda

入口函数

关于 .gcda 的生成逻辑,可参考覆盖率数据分发源码。这个文件中包含了 __gcov_flush() 函数,这个函数正是分发逻辑的入口。接下来看看 __gcov_flush() 怎样生成 .gcda 文件。
通过阅读代码和调试,我们发现在二进制代码加载时,调用了 llvm_gcov_init(writeout_fn wfn, flush_fn ffn) 函数,传入了 _llvm_gcov_writeout(写 gcov 文件),_llvm_gcov_flush(gcov 节点分发)两个函数,而且根据调用顺序,分别建立了以文件为节点的链表布局。(flush_fn_node * ,writeout_fn_node *)
__gcov_flush() 代码如下所示,当我们手动调用 __gcov_flush()举行覆盖率分发时,会遍历flush_fn_node *这个链表(即遍历全部文件节点),并调用分发函数_llvm_gcov_flush(curr->fn 正是__llvm_gcov_flush函数范例)。
  1. void __gcov_flush() {
  2.     struct flush_fn_node *curr = flush_fn_head;
  3.    
  4.     while (curr) {
  5.         curr->fn();
  6.         curr = curr->next;
  7.     }
  8. }
复制代码
具体的分发逻辑

观察__llvm_gcov_flush的 IR 代码,可以看到:


图3 __llvm_gcov_flush 代码示例

  • __llvm_gcov_flush先调用了__llvm_gcov_writeout,来向 .gcda 写入覆盖率信息。
  • 末了将计数数组清零__llvm_gcov_ctr.xx。
而 __llvm_gcov_writeout 逻辑为:

  • 生成对应源文件的 .gcda 文件,写入 Magic number。
  • 循环执行

    • llvm_gcda_emit_function: 向 .gcda 文件写入函数信息。
    • llvm_gcda_emit_arcs: 向 .gcda 文件写入BB执行信息,如果已经存在 .gcda 文件,会和之前的执行次数举行归并

  • 调用llvm_gcda_summary_info,写入校验信息。
  • 调用llvm_gcda_end_file,写竣事符。
感兴趣的同学可以自己生成 IR 文件查看更多细节,这里不再赘述。
.gcda 的文件/函数布局和 .gcno 基本同等,这里不再赘述,统计插桩信息布局如图 4 所示。定制化的输出也可以通过修改上述函数完成。我们的增量代码测试覆盖率工具解决代码 BB 布局变动后归并到已有 .gcda 文件不兼容的题目,也是修改上述函数实现的。


图4 计数桩输出布局
覆盖率解析

在了解了如上所述 .gcno ,.gcda 生成逻辑与文件布局之后,我们以例 1 中的代码为例,来阐述解析算法的实现。
例 1 中基本块 B0,B1 对应的 .gcno 文件布局如下图所示,从图中可以看出,BB 的主布局完全记录了基本块之间的跳转关系。


图5 B0,B1 对应跳转信息
B0,B1 的行信息在 .gcno 中表示如下图所示,B0 块因为是入口块,只有一行,对应行号可以从 B1 布局中获取,而 B1 有两行代码,会依次把行号写入 .gcno 文件。


图6 B0,B1 对应行信息
在输入数字 100 的情况下,生成的 .gcda 文件如下:


图7 输入 100 得到的 .gcda 文件
通过控制流程图中节点出边的执行次数可以盘算出 BB 的执行次数,核心算法为盘算这个 BB 的全部出边的执行次数,不存在出边的情况下盘算全部入边的执行次数(具体实现可以参考 gcov 工具源码),对于 B0 来说,即看 index=0 的执行次数。而 B1 的执行次数即 index=1,2 的执行次数的和,对照上图中 .gcda 文件可以推断出,B0 的执行次数为 ctr[0]=1,B1 的执行次数是 ctr[1]+ctr[2]=1, B2 的执行次数是 ctr[3]=0,B4 的执行次数为 ctr[4]=1,B5 的执行次数为 ctr[5]=1。
颠末上述解析,终极生成的 HTML 如下图所示(利用 lcov):


图8 覆盖率检测陈诉
以上是 Clang 生成覆盖率信息息争析的过程,下面介绍美团到店餐饮 iOS 团队基于以上原理做的增量代码测试覆盖率工具。
增量代码覆盖率检测原理

方案衡量

由于 gcov 工具(和前面的 .gcov 文件区分,gcov 是覆盖率陈诉生成工具)生成的覆盖率检测陈诉可读性不佳,如图 9 所示。我们做的增量代码测试覆盖率工具是基于 lcov 的扩展,陈诉展示如上节末尾图 8 所示。


图9 gcov 输出,行前数字代表执行次数,##### 代表没执行
比 gcov 直接生成陈诉多了一步,lcov 的处置惩罚流程是将 .gcno 和 .gcda 文件解析成一个以 .info 结尾的中间文件(这个文件已经包含全部覆盖率信息了),之后通过覆盖率陈诉生成工具生成可读性比力好的 HTML 陈诉。
联合前两章内容和覆盖率陈诉生成步骤,覆盖率生成流程如下图所示。考虑到增量代码覆盖率检测中代码增量部分须要通过 Git 获取,比力自然的想法是用 git diff 的信息去过滤覆盖率的内容。根据过滤点的不同,存在以下两套方案:

  • 通过 GCOVPass 过滤,只对修改的代码举行插桩,每次修改后需重新插桩。
  • 通过 .info 过滤,一次性为全部代码插桩,获取全部覆盖率信息,过滤覆盖率信息。


图10 覆盖率生成流程
分析这两个方案,第一个方案须要自定义 LLVM 的 Pass,进而会引入以下两个题目:


  • 只能利用开源 Clang 举行编译,不利于接入正常的开发流程。
  • 每次重新插桩会丢失之前的覆盖率信息,多次运行只能得到末了一次的效果。
而第二个方案相对更加轻量,只须要过滤中间格式文件,不但可以解决我们在文章开头提到的题目,也可以避免上述题目:


  • 可以很方便地加入到寻常代码的开发流程中,甚至对开发者透明。
  • 未修改文件的覆盖率可以叠加(有修改的那些控制流程图布局可能变革,无法叠加)。
因此我们现实开发选定的过滤点是在 .info 。在选定了方案 2 之后,我们对中间文件 .info 举行了一系列调研,确定了文件基本格式(函数/代码行覆盖率对应的文件的表示),这里不再赘述,具体可以参考 .info 生成文档。
增量代码测试覆盖率工具的实现

前一节是实现增量代码覆盖率检测的基本方案选择,为了更好地接入现有开发流程,我们做了以下几方面的优化。
降低利用本钱

在接入方面,接入增量代码测试覆盖率工具只需一次接入设置,同步到代码堆栈后,团队中成员无需设置即可利用,降低了接入本钱。
在利用方面,考虑到插桩在编译时举行,对全部代码举行插桩会很大程度降低编译速率,我们通过解析 Podfile(iOS 开发中较为常用的包管理工具 CocoaPods 的依赖形貌文件),只对 Podfile 中利用当地代码的堆栈举行插桩(可设置指定堆栈),降低了团队的开发本钱。
对开发者透明

接入增量代码测试覆盖率工具后,开发者无需特殊操作,也不须要对工程做任何其他修改,正常的 git commit 代码,git push 到远端就会主动生成并上传这次 commit 的覆盖率信息了。
为了做到这一点,我们在接入 Pod 的过程中,主动摆设了 Git 的 pre-push 脚本。熟悉 Git 的同学知道,Git 的 hooks 是开发者的当地脚本,不会被纳入版本控制,怎样通过一次设置就让这个堆栈的全部利用成员都能开启,是做好这件事的一个难点。
我们考虑到 Pod 自己会被纳入版本控制,因此利用了 CocoaPods 的一个属性 script_phase,增加了 Pod 编译后脚本,来帮助我们把 pre-push 插入到当地堆栈。利用 script_phase 插入还带来了别的一个利益,我们可以直接获取到工程的缓存文件,也避免了 .gcno / .gcda 文件获取的不确定性。整个流程如下:


图11 pre-push 分发流程
覆盖率累计

在实现了覆盖率的过滤后,我们在现实开发中遇到了别的一个题目:修改分支/循环布局后生成的 .gcda 文件无法和之前的归并。 在这种情况下,__gcov_flush会直接返回,不再写入 .gcda 文件了导致覆盖率检测失败,这也是市面上已有工具的通用题目
而这个题目在开发过程中很常见,比如我们给例 1 中的游戏增加一些提示,当输入比预设数字大时,我们就提示出来,反之亦然。
  1. - (void)guessNumberGame:(NSInteger)guessNumber
  2. {
  3.     NSInteger targetNumber = 10;
  4.     NSLog(@"Welcome to the game");
  5.     if (guessNumber == targetNumber) {
  6.         NSLog(@"Bingo!");
  7.     } else if (guessNumber > targetNumber) {
  8.         NSLog(@"Input number is larger than the given target!");
  9.     } else {
  10.         NSLog(@"Input number is smaller than the given target!");
  11.     }
  12. }
复制代码
这个题目困扰了我们好久,也推动了对覆盖率检测原理的调研。联合前面覆盖率检测的原理可以知道,不能归并的缘故原由是生成的控制流程图比原来多了两条边( .gcno 和旧的 .gcda 也不能匹配了),反映在 .gcda 上就是数组多了两个数据。考虑到代码变动后,原有的覆盖率信息已经没故意义了,当发生边数不同等的时间,我们会删除掉旧的 .gcda 文件,只保留最新 .gcda 文件(有变动情况下 .gcno 会重新生成)。如下图所示:


图12 覆盖率冲突解决算法
整体流程图

联合上述流程,我们的增量代码测试覆盖率工具的整体流程如图 13 所示。
开发者只需举行接入设置,再次运行时,工程中那些作为当地堆栈举行开发的代码库会被主动插桩,并在 .git 目次插入 hooks 信息;当开发者利用模拟器举行需求自测时,插桩统计效果会被主动分发出去;在代码被推到远端前,会根据插桩统计效果,生成仅包含本次代码修改的具体增量代码测试覆盖率陈诉,以及向远端推送覆盖率信息;同时如果测试覆盖率小于 80% 会强制拒绝提交(可设置关闭,百分比可自定义),保证只有颠末充实自测的代码才能提交到远端。


图13 增量代码测试覆盖率生成流程图
总结

以上是我们在代码开发质量方面做的一些积累和探索。通过对覆盖率生成、解析逻辑的探究,我们揭开了覆盖率检测的神秘面纱。开发阶段的增量代码覆盖率检测,可以帮助开发者聚焦变动代码的逻辑缺陷,从而更好地避免线上题目。


from :
iOS 覆盖率检测原理与增量代码测试覆盖率工具实现 - 美团技术团队
一样平常开发Guava提效工具库核心实用指南梳理_guava string转list-CSDN博客
从ES的JVM设置起步思考JVM常见参数优化_es jvm设置-CSDN博客
异步处置惩罚优化:多线程线程池与消息队列的选择与应用_模版模式利用-CSDN博客



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

钜形不锈钢水箱

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表