iOS增量代码覆盖率实现 (上)

iOS增量代码覆盖率可以用来评估开发的自测质量,以及多轮测试后的测试质量。这里会解析iOS代码覆盖率的收集,上报,合并这些环节当中涉及到的技术点。

GCDAProfiling

clang编译参数 GCC_GENERATE_TEST_COVERAGE_FILES 以及 GCC_INSTRUMENT_PROGRAM_FLOW_ARCS 产生的相关函数是可以由上层进行替换的,这里可以在项目当中接入 GCDAProfiling 源码进行相关修正。

https://github.com/llvm-mirror/compiler-rt/tree/master/lib/profile

  • 修正 cannot merge previous GCDA file 等错误

每次调用 __gcov_flush 的时候,都会重新check一下对应的 gcda文件(比如: main.m -> main.gcda,AppDelegate.m -> AppDelegate.gcda) 是否有结构变动。默认情况下如果 gcda文件 产生了变动,那么会直接报错,无法进行 代码覆盖率信息 的写入,因此如果出现这种错误,这里可以选择将这个 gcda文件 标记删除,详见 _bs_mark_need_remove

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
void llvm_gcda_emit_arcs(uint32_t num_counters, uint64_t *counters) {
...
val = read_le_32bit_value();

if (val != (uint32_t)-1) {
/* There are counters present in the file. Merge them. */
if (val != 0x01a10000) {
fprintf(stderr, "profiling: %s: cannot merge previous GCDA file: "
"corrupt arc tag (0x%08x)\n",
filename, val);
_bs_mark_need_remove();
return;
}

val = read_32bit_value();
if (val == (uint32_t)-1 || val / 2 != num_counters) {
fprintf(stderr, "profiling: %s: cannot merge previous GCDA file: "
"mismatched number of counters (%d)\n",
filename, val);
_bs_mark_need_remove();
return;
}
}
...
}

void _bs_mark_need_remove(void) {
struct stat filenameStat = {0};
if (filename && stat(filename, &filenameStat) == 0) {
//mark filename to remove later
need_remove = 1;
}
}

在 llvm_gcda_end_file 当中处理需要删除的 gcda文件,逻辑如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
void llvm_gcda_end_file() {
...
//remove invalid file such as : mismatched .gcda
if (need_remove) {
fprintf(stderr, "remove : %s\n", filename);
need_remove = 0;
int retRemove = remove(filename);
if (retRemove != 0) {
fprintf(stderr, "remove error : %s\n", filename);
}
}
...
}

gcda文件

可以通过 GCOV_PREFIX 以及 GCOV_PREFIX_STRIP 指定 gcda文件 的生成路径,比如放到 Caches目录。

1
2
3
NSString *codeCoverageDataDir = [BSCodeCoverage codeCoverageDataDir];
setenv("GCOV_PREFIX", [codeCoverageDataDir cStringUsingEncoding:NSUTF8StringEncoding], 1);
setenv("GCOV_PREFIX_STRIP", "13", 1);

如果需要在真机上收集gcda文件,可以考虑搭建一个简易的文件服务器去存储客户端上报的gcda文件。

gcno文件

编译器生成的描述 源文件 结构的文件,需要根据项目target,arch信息在DerivedData目录下面去查找。

1
2
3
4
5
6
如: Demo这个target下面的 main.m 文件对应的 真机 gcno路径

Demo.build/Debug-iphoneos/Demo.build/Objects-normal/arm64/main.gcno

arm64, armv7s 等对应真机的cpu架构
x86_64 对应模拟器的cpu架构

Coverage.info

lcov可以通过gcda以及gcno文件生成Coverage.info这个中间产物,Coverage.info易于阅读以及解析,可以用来做后续的问题排查以及html报告生成。

1
2
3
4
5
6
7
8
9
10
11
12
Coverage.info文件内容:

TN:
SF:Demo/main.m #SF: source file,源文件路径
FN:12,main #FN: function, 函数行号+函数名
FNDA:2,main #FNDA: function data
FNF:1 #FNF: function found
FNH:1 #FNH: function hit
DA:13,2 #DA: data, 行号+执行次数
LF:4 #LF: line found
LH:4 #LH: line hit
end_of_record