gcov#
gcov 是 GCC 工具链中的代码覆盖率分析工具,能够精确展示单元测试对源代码的覆盖情况。
使用方法#
在编译阶段,由 .c 源文件生成 .o 目标文件时,需添加 --coverage 编译选项:
$(OBJS): %.o: %.c
$(CC) $(CFLAGS) --coverage -c $< -o $@
在链接生成可执行文件时,同样需要添加 --coverage 选项:
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) --coverage -o $@ $^
Note
--coverage 是 GCC 提供的宏选项,等价于同时使用以下三个选项:
-fprofile-arcs:编译时插桩,记录代码执行路径-ftest-coverage:生成程序结构信息文件-lgcov:链接覆盖率运行时支持库
生成覆盖率报告#
运行程序生成数据文件:执行编译后的程序:
./your_program
程序运行后会生成 .gcda(运行时数据)和 .gcno(程序结构信息)文件。
生成原始覆盖率数据:对每个需要分析的源文件执行 gcov 命令:
gcov main.c
此命令会生成对应的 .gcov 文本格式报告文件。
生成可视化报告:使用 lcov + genhtml 工具链:
# 1. 收集覆盖率数据
lcov -c -d . -o coverage.info
# 2. 可选:过滤系统头文件
lcov -r coverage.info '/usr/include/*' -o coverage_filtered.info
# 3. 生成 HTML 报告
genhtml coverage_filtered.info -o coverage_report
# 4. 查看报告
firefox coverage_report/index.html
lcov 常用参数
# 仅收集指定目录的覆盖率数据
lcov -c -d src/ -o coverage.info
# 合并多个覆盖率数据文件
lcov -a coverage1.info -a coverage2.info -o total.info
覆盖率报告解读
双击 lcov 生成的 index.html 文件,即可在浏览器中查看覆盖率报告。报告主要包含以下三列数据:
列名 |
说明 |
|---|---|
Branch data |
分支覆盖率,显示代码中每个分支(如 |
Line Data |
行覆盖率,显示每行代码是否被执行过 |
Source code |
对应的源代码,通过颜色高亮显示覆盖情况 |
桩函数使用示例#
#include <cstdlib>
#include <cstring>
#include <uci.h>
// 分配上下文失败桩函数
struct uci_context* uci_alloc_context_stub_fail() {
return nullptr;
}
// 查找指针成功桩函数
int uci_lookup_ptr_stub_success(struct uci_context* ctx, struct uci_ptr* ptr, char* str,
bool extended) {
ptr->last = (struct uci_element*)malloc(sizeof(struct uci_element));
if (ptr->last) { ptr->last->type = UCI_TYPE_OPTION; }
ptr->p = (struct uci_package*)malloc(sizeof(struct uci_package));
ptr->value = NULL;
ptr->flags = uci_ptr::UCI_LOOKUP_COMPLETE;
ptr->o = (struct uci_option*)malloc(sizeof(struct uci_option));
if (ptr->o) {
ptr->o->type = UCI_TYPE_STRING;
ptr->o->v.string = strdup("test_value");
}
return UCI_OK;
}
// 设置操作成功桩函数
int uci_set_stub_success(struct uci_context* ctx, struct uci_ptr* ptr) {
return UCI_OK;
}
// 保存操作成功桩函数
int uci_save_stub_success(struct uci_context* ctx, struct uci_package* p) {
return UCI_OK;
}
// 提交操作成功桩函数
int uci_commit_stub_success(struct uci_context* ctx, struct uci_package** p, bool overwrite) {
return UCI_OK;
}
// 提交操作失败桩函数
int uci_commit_stub_fail(struct uci_context* ctx, struct uci_package** p, bool overwrite) {
return UCI_FRR_UNKNOWN;
}
// 卸载操作成功桩函数
int uci_unload_stub_success(struct uci_context* ctx, struct uci_package* p) {
return UCI_OK;
}
// 释放上下文成功桩函数
void uci_free_context_stub_success(struct uci_context* ctx) {
// 桩函数实现为空
}
如何对 static 函数或变量进行打桩?
对于 static 函数,由于作用域限制无法直接打桩,但可以通过间接方式模拟。考虑到 static 函数必然在同一文件的其他位置被调用,可以针对调用点进行测试。对于 static 变量,通常会在非 static 函数中被赋值,因此只需对这些函数进行打桩,即可控制 static 变量的值,使其符合测试预期。
桩函数不生效的解决方案
当编译器将函数优化为内联函数时,会导致桩函数失效。解决方法是在需要打桩的函数声明前添加 __attribute__((noinline)) 属性,例如:
__attribute__((noinline)) bool stop_while_1_stub() {
return true;
}
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
当被测试函数需要指针类型的参数时,必须确保传入有效的内存地址。正确做法是:
声明变量:先定义一个具体类型的变量
获取地址:使用取地址运算符
&获取该变量的内存地址传入函数:将地址传递给需要指针参数的函数
// 正确示例
int actual_variable; // 1. 声明实际变量
test_func(&actual_variable); // 2. 获取地址并传入函数
// 错误示例:传入未初始化的野指针
int* wild_pointer; // 未初始化的指针
test_func(wild_pointer); // 可能导致段错误或未定义行为
核心原则:永远不要将未初始化的指针传递给函数,这会导致程序崩溃或不可预测的行为。
屏蔽相关代码的统计
如需从覆盖率统计中排除特定代码,请在源代码中添加对应的屏蔽注释并重新编译。重新生成的库文件在运行时将忽略被标记的代码,使其不参与覆盖率统计。
支持以下屏蔽方式:
多行屏蔽:在目标代码段起始处添加
// LCOV_EXCL_START,在结束处添加// LCOV_EXCL_STOP单行屏蔽:在代码行末尾添加
// LCOV_EXCL_LINE分支屏蔽:仅排除分支统计,保留代码执行统计,在分支判断语句后添加
// LCOV_EXCL_BR_LINE
示例:
// LCOV_EXCL_START
void unused_function() {
// 此函数不会被统计
}
// LCOV_EXCL_STOP
void active_function() {
log("running"); // LCOV_EXCL_LINE
if (condition) { // LCOV_EXCL_BR_LINE
// 此分支不纳入分支统计
}
}