本文简要介绍代码覆盖率的概述,以及如何使用gcov和lcov覆盖率工具来统计C程序代码的覆盖率。
1.代码覆盖率概述
代码覆盖率是软件测试中的一种度量指标,通常指在测试过程中已运行的代码占总的可执行代码的比例。
测试代码覆盖率的主要目的是评估测试用例的质量和全面性,以发现潜在的错误和漏洞,可以帮助开发团队判断测试用例是否足够全面地检查了代码的各个部分。
测试代码覆盖率的好处包括:
-
发现代码中潜在的错误和漏洞:通过提高覆盖率,可以更全面地测试代码,发现潜在的错误和边界情况。
-
提高代码质量:覆盖率指标可以作为代码质量的度量标准,帮助开发团队评估测试的全面性和效果。
-
指导测试用例的编写:通过分析覆盖率报告,测试团队可以了解哪些部分的代码没有被覆盖到,进而有针对性地编写更全面的测试用例。
-
促进团队合作和交流:覆盖率指标可以作为评估测试工作的一个参考指标,并促进开发和测试团队之间的合作和交流。
代码覆盖率通常可以通过以下几个指标来评估:
-
函数覆盖率(Function coverage):衡量测试用例是否执行了代码中的所有函数。
-
行覆盖率(Line coverage):衡量测试用例是否执行了代码中的每一行。
-
分支覆盖率(Branch coverage):衡量测试用例是否执行了代码中分支语句的所有分支路径。
-
条件覆盖率(Condition coverage):衡量测试用例是否覆盖了代码中条件语句的所有可能情况。条件覆盖率要求每个条件的各种取值至少被执行一次。
-
路径覆盖率(Path coverage):衡量测试用例是否覆盖了程序的所有可能执行路径。
2. gcov和lcov覆盖率工具使用:
2.1 入门介绍
gcov和lcov是一组用于代码覆盖率分析的工具,统计的覆盖率指标是函数覆盖率和行覆盖率。
其中,gcov是GNU工具链中的一个代码覆盖率分析工具,它可以统计源代码中每个语句的执行次数。在编译时,需要加上-fprofile-arcs
和-ftest-coverage
两个编译选项来告诉编译器生成相应的统计文件。运行程序后,执行gcov
命令可以生成覆盖率报告文件.gcov
,其中包含了每个源代码文件的执行情况以及每个函数和代码块的执行次数等信息。
而lcov则是基于gcov的一个图形化工具,提供了更方便的报告生成和查看方式。使用lcov需要先运行程序并收集gcov数据。然后,使用lcov --capture --directory <source_dir> --output-file <output_file>
命令将收集到的gcov数据转换为lcov格式。最后,使用genhtml
命令将lcov数据转换为HTML格式的报告,以便开发人员可视化地查看代码覆盖率统计结果。
在使用这些工具时,应注意几点:
-
确保程序已经被充分测试,并且测试用例覆盖了尽可能多的代码路径。
-
在编译时加上
-fprofile-arcs
和-ftest-coverage
选项,以便生成相应的gcov数据。 -
使用lcov时,要将源代码目录和输出文件指定为正确的路径,并在需要分析的所有源文件上运行gcov。
-
对于大型项目,可能需要自行使用和编写覆盖率测试套件来自动运行测试并生成报告,以减少手动操作的时间和错误。
2.2 简单的具体使用案例:
#include <stdio.h>
int main()
{
int a = 0;
if (a == 0) {
printf("a == 0\n");
} else {
printf("a != 0\n");
}
printf("Hello, World!\n");
return 0;
}
上面是一个待测试覆盖率的C语言文件。
-
在仅使用gcov工具时,可以按照如下所示的操作进行编译,执行,以及统计覆盖率,可以看到,代码的行覆盖率为85.71%,打开.gcov文件可以看到具体的每一行的覆盖率情况。具体执行步骤如下:
-
生成C语言文件:test1.c
-
添加编译选项编译:gcc -fprofile-arcs -ftest-coverage test1.c -o test1
-
运行可执行文件:./test1
-
执行gcov命令生成结果文件test1.c.gcov:gcov test1.c
-
查看覆盖率结果: cat test1.c.gcov
在一套流程结束后文件中与覆盖率相关的文件有.gcno, .gcda, .gcov文件,三种文件类型的含义如下:
-
-
gcno文件:是在编译源代码时由编译器生成的文件,它包含了源代码中每个函数和代码块的控制流图信息,以及用于计算代码覆盖率的相关数据结构。gcno文件记录了每个代码块被执行的次数以及与其他代码块之间的关系。
-
gcda文件:是在程序运行时由应用程序生成的文件,它记录了程序运行期间每个代码块的执行情况。当程序执行时,gcda文件会被更新,其中包含了每个代码块被执行的次数等信息。
-
gcov文件:是通过对gcno和gcda文件的分析生成的覆盖率分析报告文件,它提供了详细的代码覆盖率信息。gcov文件可以告诉开发人员哪些代码行被执行了多少次,哪些代码行没有被执行到。开发人员可以通过查看gcov文件来了解他们的代码被测试到了哪些程度,以及哪些代码需要更多的测试覆盖。
-
在lcov结合gcov一起使用时,可以提供图形化界面数据。具体的命令如下,首先需要创建保存结果的文件夹 ,然后运行lcov命令生成中间结果文件,再执行genhtml生成html网页文件。前3步操作同上,然后分别运行
-
创建输出文件夹:mkdir lcov_output
-
生成中间结果文件:lcov -c -o "lcov_output/coverage.info" -d .
-
生成html相关网页文件:genhtml lcov_output/coverage.info -o lcov_output
-
浏览器打开输出文件夹位置的test1.heml查看结果
-
2.3 可能遇到的问题及解决方法:
问题1:需要指定收集或不收集某些目录下的覆盖率信息
解决方法:可以利用lcov工具的--extract选项或--remove选项,例如,将
lcov -c -o "coverage.info" -d .得到的中间结果文件进一步筛选
-
lcov --extract "coverage.info" "{file_path}" -o "result.info"
genhtml "result.info" -o "./output" 将{file_path}替换为具体的目录,即可进一步过滤,仅统计{file_path}下的覆盖率
-
lcov --remove"coverage.info" "{file_path}" -o "result.info"
genhtml "result.info" -o "./output" 将{file_path}替换为具体的目录,即可进一步过滤,不统计{file_path}下的覆盖率
问题2:当程序在关闭后,可能无法获取覆盖率信息
原因分析:可能是运行完测试代码后,程序未主动退出调用exit()
函数,也就不会调用gcov_exit
函数得到调用,其继续调用 __gcov_flush
函数输出统计数据到 *.gcda 文件中 解决方法:主动捕获退出信号,并调用exit()函数进行程序的退出。 例如下面的示例代码,通过显式捕获程序终止信号,可以自定义程序终止后的操作,并手动调用exit()函数,修改后
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// 函数其他代码···
int finish_state = -1;
static void signal_handler(int signal)
{
if (signal == SIGTERM || signal == SIGINT || signal == SIGQUIT || signal == SIGHUP) {
finish_state = 0;
}
}
int signal_init(void)
{
if (signal(SIGTERM, signal_handler) == SIG_ERR){
printf("Failed to register SIGTERM signal handler function");
return -1;
}
if (signal(SIGINT, signal_handler) == SIG_ERR) {
printf("Failed to register SIGINT signal handler function");
return -1;
}
if (signal(SIGQUIT, signal_handler) == SIG_ERR) {
printf("Failed to register SIGQUIT signal handler function");
return -1;
}
if (signal(SIGHUP, signal_handler) == SIG_ERR) {
printf("Failed to register SIGHUP signal handler function");
return -1;
}
return 0;
}
static int signal_loop(void) {
if (finish_state == 0) {
printf("\nThis code has been stopped!\n");
exit(0);
}
return 1;
}
int main(int argc, char *argv[]){
if (signal_init() != 0) {
printf("Failed to initialize signal handler.\n");
return -1;
}
while(1) {
printf("running\n");
sleep(1);
signal_loop();
}
}
运行编译后的文件,并用ctrl+c终止后的结果如下: