12、代码覆盖率入门:gcov与lcov工具介绍、生成覆盖率报告
各位同学,今天我们来聊聊代码覆盖率。说实话,我刚开始做嵌入式开发那会儿,对覆盖率这玩意儿是有点不屑的。觉得“我代码都跑通了,还要看什么覆盖率?”直到有一次,一个看似稳定的模块,在客户现场出了个极其隐蔽的bug——某个异常分支从来没被触发过,而那个分支恰好有逻辑错误。嗯,从那以后,我再也不敢忽视覆盖率了。
说白了,代码覆盖率就是一把尺子。它告诉你:你的测试用例到底跑了多少代码?哪些代码是“被遗忘的角落”?今天我们就来学习两个最经典的工具——gcov 和 lcov。
12.1 什么是代码覆盖率?
代码覆盖率,衡量的是测试执行过程中,源代码被覆盖到的比例。常见的覆盖率指标有:
- 语句覆盖率:每行代码是否被执行过。
- 分支覆盖率:每个if-else、switch-case的分支是否都走到过。
- 函数覆盖率:每个函数是否被调用过。
- 条件覆盖率:每个布尔子表达式是否取过true和false。
我个人习惯,在单元测试阶段至少盯住语句覆盖率和分支覆盖率。这两个指标最直观,也最容易发现问题。
12.2 gcov:GCC自带的覆盖率利器
gcov是GCC编译器自带的工具,不需要额外安装。你只要在编译时加上几个特殊选项,它就能帮你记录代码的执行情况。
12.2.1 基本用法
假设我们有一个简单的C文件 calc.c:
// calc.c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
int main() {
int x = add(3, 5);
int y = max(10, 7);
printf("x=%d, y=%d\n", x, y);
return 0;
}
编译时加上 -fprofile-arcs -ftest-coverage 选项:
gcc -fprofile-arcs -ftest-coverage calc.c -o calc
运行生成的可执行文件:
./calc
这时目录下会生成 calc.gcda 和 calc.gcno 文件。然后执行:
gcov calc.c
你会看到输出:
File 'calc.c'
Lines executed:85.71% of 7
Creating 'calc.c.gcov'
打开 calc.c.gcov 文件,每一行前面会显示该行被执行了多少次:
-: 0:Source:calc.c
-: 1:#include <stdio.h>
-: 2:
-: 3:int add(int a, int b) {
1: 4: return a + b;
-: 5:}
-: 6:
-: 7:int max(int a, int b) {
1: 8: if (a > b) {
1: 9: return a;
-: 10: } else {
#####: 11: return b;
-: 12: }
-: 13:}
-: 14:
-: 15:int main() {
1: 16: int x = add(3, 5);
1: 17: int y = max(10, 7);
1: 18: printf("x=%d, y=%d\n", x, y);
1: 19: return 0;
-: 20:}
看到第11行前面的 ##### 了吗?这表示该行从未被执行过。因为我们的测试用例中 max(10, 7) 走的是 a > b 分支,else 分支被漏掉了。
##### 标记。如果有,说明有代码没测到,赶紧补用例。
12.2.2 常用选项
| 选项 | 说明 |
|---|---|
-b |
显示分支覆盖率信息 |
-f |
显示每个函数的覆盖率 |
-o |
指定目标文件目录 |
例如:
gcov -b -f calc.c
会额外输出每个函数的分支覆盖情况。
12.3 lcov:让覆盖率报告更直观
gcov的输出是文本文件,看多了眼睛疼。lcov就是来解决这个问题的——它能把gcov的数据转换成漂亮的HTML报告。
12.3.1 安装lcov
在Ubuntu/Debian上:
sudo apt-get install lcov
在macOS上:
brew install lcov
12.3.2 生成HTML报告
还是用上面的 calc.c 例子。先确保已经生成了 .gcda 和 .gcno 文件,然后执行:
# 第一步:收集覆盖率数据
lcov --capture --directory . --output-file coverage.info
# 第二步:过滤掉系统头文件(可选,但强烈建议)
lcov --remove coverage.info '/usr/*' --output-file coverage_filtered.info
# 第三步:生成HTML报告
genhtml coverage_filtered.info --output-directory coverage_report
打开 coverage_report/index.html,你会看到一个漂亮的网页:
- 顶部显示总体覆盖率百分比。
- 左侧是文件列表,每个文件旁边有覆盖率进度条。
- 点击文件名,可以看到每一行代码的覆盖情况——绿色表示已覆盖,红色表示未覆盖。
make clean 或者手动删除所有 .gcda 文件。
12.4 实战:一个完整的覆盖率流程
我们用一个稍微复杂点的例子,演示完整的流程。假设项目结构如下:
project/
├── src/
│ ├── math.c
│ └── math.h
└── test/
└── test_math.c
math.c 内容:
// math.c
#include "math.h"
int divide(int a, int b) {
if (b == 0) {
return -1; // 错误处理
}
return a / b;
}
int is_even(int n) {
return (n % 2 == 0);
}
test_math.c 内容:
// test_math.c
#include "math.h"
#include <assert.h>
int main() {
assert(divide(10, 2) == 5);
assert(divide(10, 0) == -1);
assert(is_even(4) == 1);
assert(is_even(7) == 0);
return 0;
}
编译并运行测试:
gcc -fprofile-arcs -ftest-coverage src/math.c test/test_math.c -o test_runner
./test_runner
生成覆盖率报告:
lcov --capture --directory . --output-file coverage.info
lcov --remove coverage.info '/usr/*' --output-file coverage_filtered.info
genhtml coverage_filtered.info --output-directory coverage_report
打开报告,你会看到 math.c 的语句覆盖率是100%,分支覆盖率也是100%。因为我们的测试用例覆盖了 b == 0 和 b != 0 两个分支,以及 n % 2 == 0 的true和false两种情况。
12.5 知识体系图
下面这张图总结了本章的核心逻辑:
12.6 常见问题与避坑
- gcda文件没生成? 检查程序是否正常退出。如果程序被
kill -9或者异常崩溃,gcda文件可能写不完整。 - 覆盖率数据不准? 确保每次重新编译前清理了旧的
.gcda文件。我习惯在Makefile里加一个clean_coverage目标。 - 嵌入式环境怎么用? 交叉编译时,在宿主机上编译带覆盖率选项的代码,然后在目标板上运行,把生成的
.gcda文件拷回宿主机,再用lcov处理。 - 报告里全是红色? 别慌,先检查是不是过滤掉了系统头文件。如果不过滤,
/usr/include下的代码也会被统计,那覆盖率肯定惨不忍睹。
好了,关于gcov和lcov的基础用法就讲到这里。这两个工具组合起来,能让你对代码的测试情况一目了然。记住,覆盖率不是目的,而是手段——它的真正价值,是帮你找到那些你没想到要测的代码路径。