首页
找靠谱产品
找解决方案
找靠谱公司
找案例
找对的人
专家智库
悬赏任务
SAAS
ToB门户
了解全球最新的ToB事件
论坛
潜水/灌水快乐,沉淀知识,认识更多同行。
ToB圈子
加入IT圈,遇到更多同好之人。
微博
Follow
记录
Doing
博客
Blog
文库
业界最专业的IT文库,上传资料也可以赚钱
下载
分享
Share
排行榜
Ranklist
相册
Album
应用中心
qidao123.com ToB IT社区-企服评测·应用市场
»
论坛
›
软件与程序人生
›
移动端开发
›
IOS
›
iOS 覆盖率检测原理与增量代码测试覆盖率工具实现12 ...
返回列表
发新帖
iOS 覆盖率检测原理与增量代码测试覆盖率工具实现12
[复制链接]
发表于 2025-9-22 08:31:05
|
显示全部楼层
|
阅读模式
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要
登录
才可以下载或查看,没有账号?
立即注册
×
配景
对苹果开发者而言,由于平台稽核周期较长,客户端
代码
导致的线上题目影响时间每每比力久。如果在开发、测试阶段可以大概提前袒露题目,就有助于制止线上事故的发生。
代码
覆盖率检测正是资助开发、测试同砚提前发现题目,包管
代码
质量的好帮手。
对于开发者而言,代码覆盖率可以反馈两方面信息:
自测的充实程度。
代码操持的冗余程度。
只管代码覆盖率对代码质量有着上述利益,但在 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所示(猜数字游戏 )。
- (void)guessNumberGame:(NSInteger)guessNumber
{
NSLog(@"Welcome to the game");
if (guessNumber == 10) {
NSLog(@"Bingo!");
} else {
NSLog(@"You guess is wrong!");
}
}
复制代码
这段代码如果开启了覆盖率检测,会天生一个长度为 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函数范例)。
void __gcov_flush() {
struct flush_fn_node *curr = flush_fn_head;
while (curr) {
curr->fn();
curr = curr->next;
}
}
复制代码
详细的分发逻辑
观察__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 中的游戏增长一些提示,当输入比预设数字大时,我们就提示出来,反之亦然。
- (void)guessNumberGame:(NSInteger)guessNumber
{
NSInteger targetNumber = 10;
NSLog(@"Welcome to the game");
if (guessNumber == targetNumber) {
NSLog(@"Bingo!");
} else if (guessNumber > targetNumber) {
NSLog(@"Input number is larger than the given target!");
} else {
NSLog(@"Input number is smaller than the given target!");
}
}
复制代码
这个题目困扰了我们很久,也推动了对覆盖率检测原理的调研。联合前面覆盖率检测的原理可以知道,
不能归并的原因是天生的控制流程图比原来多了两条边( .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博客
回复
使用道具
举报
返回列表
浏览过的版块
Oracle
开源技术
数据仓库与分析
SQL-Server
.Net
分布式数据库
移动端开发
Java
公有云
物联网
郭卫东
+ 我要发帖
登录后关闭弹窗
登录参与点评抽奖 加入IT实名职场社区
去登录
微信订阅号
微信服务号
微信客服(加群)
H5
小程序
快速回复
返回顶部
返回列表