本项目使用RT-Thread的QEMU VExpress A9板级支持包,演示如何在RT-Thread项目中进行单元测试和覆盖率测试。
本项目应用到的代码、程序、工具,包含:
在此十分感谢海南大学刘伟同学的热心帮助。
工程主目录
├───applications
│ ├───code_be_test # 即将被测试的代码
│ └───utest_tc_cases # 编写的测试用例,本项目采用RTT的UTEST框架
│ └───main.c # 必要的时候调用__gcov_call_constructors初始化GCOV
├───packages
│ └───embedded-gcov # 用于进行覆盖率测试的工具包,需要在rtconfig.h中#define PKG_USING_GCOV才能启用
│ ├───code # 核心代码,用户只需要#include "gcov_public.h"即可,或者开启某些宏
│ └───scripts # 后处理脚本
├───rtconfig.py # gcc编译选项,在链接开启--covrage
├───rtconfig.h # 需要的定义#define PKG_USING_GCOV和#define RT_USING_UTEST开启单元测试和覆盖率测试
1、先安装好Git for windows、VS Code和7zip。建议将7zip安装目录C:\Program Files\7-Zip
添加到系统PATH环境变量。
2、下载安装env和lcov两个工具,前者是RTT开发工具链,后者用于覆盖率报告生成。
下载env-v1.3.5,解压到不包含中文路径(比如C:\env-windows-v1.3.5),并建议将C:\env-windows-v1.3.5\tools\qemu\qemu32
和C:\env-windows-v1.3.5\tools\gnu_gcc\arm_gcc\mingw\bin
添加到系统PATH环境变量。
下载lcov lcov-1.15(不要用v1.16哈,用v1.15就好了),解压到不包含中文的路径(比如C:\lcov-1.16),并将C:\lcov-1.15\bin
添加到系统PATH环境变量。
3、下载本项目代码,比如clone到D:\nextpilot\rtt-gcov。
git clone https://gitee.com/latercomer/rtt-gcov.git
4、打开env终端C:\env-windows-v1.3.5\env.exe,并在终端运行:
# 切换到工程目录
cd /d D:\nextpilot\rtt-gcov
# 编译项目
scons -j10
# 将qemu添加到path,如果之前添加了PATH环境变量就不需要
set PATH=C:\env-windows-v1.3.5\tools\qemu\qemu32;%PATH%
# 运行qemu模拟器
qemu.bat
此时rtt-gcov工程就会在qemu模拟器中运行起来,终端显示如下
5、执行utest中的测试用例
# 查看包含了哪些用例
utest_list
# 运行所有测试用例
utest_run
5、Ctr+C停止env终端,使用7zip打开压缩包D:\nextpilot\rtt-gcov\sd.bin,并将其中gcov_output.bin文件提取出来。
# 切换到工程目录
cd /d D:\nextpilot\rtt-gcov
set PATH=C:\Program Files\7-Zip;%PATH%
# 从sd.bin中提取gcov_output.bin文件
7z e sd.bin gcov_output.bin
6、调用parse_gcov_output.py脚本将gcov_output.bin文件拆分成独立的.gcda文件。
cd D:\nextpilot\rtt-gcov
python3 packages/embedded-gcov/scripts/parse_gcov_output.py
运行结果,命令行会显示以下内容:
b'D:\nextpilot\rtt-gcov\build\applications\utest_tc_cases\test_bubble_sort.gcda' b'D:\nextpilot\rtt-gcov\build\applications\code_be_test\bubble_sort.gcda'
7、调用lcov生成覆盖率测试报告,在工程主目录右键,选择Git Bash Here
,输入:
# 切换到工程目录
cd D:\nextpilot\rtt-gcov
# 运行lcov
lcov -c -d . --rc lcov_branch_coverage=1 -o build/test.coverage --gcov-tool arm-none-eabi-gcov && genhtml --branch-coverage build/test.coverage -o build/lcov
注意:运行上述命令需要将
C:\lcov-1.15\bin
和C:\env-windows-v1.3.5\tools\gnu_gcc\arm_gcc\mingw\bin
路径添加到系统PATH环境变量,否则lcov和arm-none-eabi-gcov之前需要添加完整路径名。
8、用浏览器打开build/lcov/index.html
,查看被测试代码的覆盖结果:
1、将packages\embedded-gcov拷贝到您的工程,并设置gcov_public.h中部分宏,意义如下:
// 使用为覆盖率信息存储动态分配内存
// 否则使用预定义gcov_GcovInfo和gcov_buf存储空间
// 对于与嵌入式系统不建议开启
// #define GCOV_OPT_USE_MALLOC
// 是否使用stdlib.h库,我们使用RTT的rt_kprintf,因此不需要开启
// #define GCOV_OPT_USE_STDLIB
// 是否打印过程信息,调试阶段建议开启
#define GCOV_OPT_PRINT_STATUS
// 这个我没有使用,也没有开启
// #define GCOV_OPT_RESET_WATCHDOG
// 是否定义覆盖率初始化构造函数,建议开启
// 如果RTT启用了RT_USING_CPLUSPLUS就不需要
#define GCOV_OPT_PROVIDE_CALL_CONSTRUCTORS
// 是否使用原作者提供了gcov_printf函数
// 我们使用了rtt的rt_kprintf,因此不需要开启
// #define GCOV_OPT_PROVIDE_PRINTF_IMITATION
// 是否将覆盖率信息写入到文件,如果有SD卡和文件系统,建议开启
#define GCOV_OPT_OUTPUT_BINARY_FILE
// 覆盖率信息存储文件名
#define GCOV_OUTPUT_BINARY_FILENAME "/gcov_output.bin"
// 是否在内存里面保存覆盖率信息,不需要开启
// #define GCOV_OPT_OUTPUT_BINARY_MEMORY
// 是否通过打印串口输入出覆盖率信息
// 如果没有文件系统,这是将覆盖率信息输出一种手段,平时也可以作为调试使用
// 如果需要统计覆盖率信息的文件很多,建议关闭
#define GCOV_OPT_OUTPUT_SERIAL_HEXDUMP
如果关闭了GCOV_OPT_USE_MALLOC宏,则一定注意调整gcov_pulbic.c中gcov_GcovInfo和gcov_buf两个数组的大小,防止开辟的空间不够。
#ifndef GCOV_OPT_USE_MALLOC
/* Declare space. Need one entry per file compiled for coverage. */
static GcovInfo gcov_GcovInfo[1000];
static gcov_unsigned_t gcov_GcovIndex = 0;
/* Declare space. Needs to be enough for the largest single file coverage data. */
/* Size used will depend on size and complexity of source code
* that you have compiled for coverage. */
/* Need buffer to be 32-bit-aligned for type-safe internal usage */
gcov_unsigned_t gcov_buf[81920];
#endif // not GCOV_OPT_USE_MALLOC
2、在rtconfig.py中开启覆盖率链接选项,也就是LFLAGS中增加--coverage。
# 使链接时将相关的代码也使用覆盖率测试 --coverage
LFLAGS = DEVICE + ' -nostartfiles -Wl,--gc-sections,-Map=rtthread.map,-cref,-u,system_vectors --coverage' + ' -T %s' % LINK_SCRIPT
3、修改rtconfig.h文件,定义以下几个宏:
/* Utilities */
// 启用utest测试框架,非必须
#define RT_USING_UTEST
#define UTEST_THR_STACK_SIZE 4096
#define UTEST_THR_PRIORITY 20
// 启用packages\embedded-gcov
#define PKG_USING_GCOV
4、在合适的地方调用__gcov_call_constructors函数,启动覆盖率测试初始化函数。比如我是放在main.c文件中,当然其它任意位置都可以。
__gcov_call_constructors()只能被调用一次,且越早调用越好,如果rtt开启了RT_USING_CPLUSPLUS,则rtt会在cplusplus_system_init()将.init.array区的函数调用一遍,因此不需要pkg_gcov_init()再次调用了。
#include <rtthread.h>
#ifdef PKG_USING_GCOV
#include "gcov_public.h"
static int pkg_gcov_init(void){
// 如果开启了c++,在cplusplus_system_init()会自动调用的.init.array,因此这里不需要再次调用
#ifndef RT_USING_CPLUSPLUS
__gcov_call_constructors();
#endif
return 0;
}
// 在INIT_COMPONENT_EXPORT阶段初始化GCOV
INIT_COMPONENT_EXPORT(pkg_gcov_init);
#endif
5、在合适的地方调用__gcov_exit函数,将覆盖率信息输入到终端或文件,本项目一般写入到utest_tc_cleanup()中,详细见后面的utest用例编写。
__gcov_exit()函数可以被不同的地方多次调用哈。一般是在测试结束之后调用,比如通过上位机、MSH命令行,或者utest运行结束等方式触发。
// utest测试的结束程序,一般是用来释放资源
static rt_err_t utest_tc_cleanup(void)
{
// 在测试用例结束之后,将覆盖率结果输出来
#ifdef PKG_USING_GCOV
__gcov_exit();
#endif
return RT_EOK;
}
6、添加待测试代码,并在DefineGroup中增加LOCAL_CFLAGS=' --coverage'
参数。假如待测试代码在application/code_be_test目录下,则编译脚本SConscript该如下修改:
import os
from building import *
cwd = GetCurrentDir()
src = Glob('*.c') + Glob('*.cpp')
CPPPATH = [cwd]
if GetDepend(["PKG_USING_GCOV"]):
# 如果定义了PKG_USING_GCOV,则开启覆盖率编译选项
group = DefineGroup('src', src, depend = [''], CPPPATH = CPPPATH, LOCAL_CFLAGS=' --coverage')
else:
group = DefineGroup('src', src, depend = [''], CPPPATH = CPPPATH)
Return('group')
其中
--coverage
等价于-fprofile-arcs -ftest-coverage
表示将这个文件夹的源码添加覆盖率测试(插桩)。
7、编写utest测试用例,需要注意的是在utest_tc_cleanup()
函数中调用了__gcov_exit
也就是用例跑完之后输出覆盖率信息,utest测试框架代码如下:
RTT UTEST测试框架UTEST_UNIT_RUN宏展开发现,当任意测试套件中,有用例断言失败的时候,将会终止当前套件后续用例的执行。
#include "utest.h"
// 如果定义了PKG_USING_GCOV宏,引用gcov_public.h头文件
#ifdef PKG_USING_GCOV
#include "gcov_public.h"
#endif
#include "bubble_sort.h"
// 这是冒泡算法的测试用例1
static void test_bubble_sort1(void){
// 定义等待被排序的数组
int arr [] ={5,3,1,4,2};
// 调用冒泡排序算法
bubble_sort(arr, 5);
// 使用utest断言
for (int i = 0; i< 5; i++){
uassert_int_equal(arr[i], i+1);
}
}
// 这是冒泡算法的测试用例2
static void test_bubble_sort2(void){
// 定义等待被排序的数组
int arr [] = {3,1,2};
// 调用冒泡排序算法
bubble_sort(arr, 3);
// 使用utest断言
for (int i = 0; i< 3; i++){
uassert_int_equal(arr[i], i+1);
}
}
// utest测试的初始化程序,一般是用来准备资源
static rt_err_t utest_tc_init(void)
{
return RT_EOK;
}
// utest测试的结束程序,一般是用来释放资源
static rt_err_t utest_tc_cleanup(void)
{
// 在测试用例运行结束后,将覆盖率结果输出来
#ifdef PKG_USING_GCOV
__gcov_exit();
#endif
return RT_EOK;
}
// 将所有测试用例放在一个套件里面调用
static void utest_tc_cases(void)
{
UTEST_UNIT_RUN(test_bubble_sort1);
UTEST_UNIT_RUN(test_bubble_sort2);
}
// 导出测试用例套件,之后可以通过MSH命令行运行
UTEST_TC_EXPORT(utest_tc_cases, "bubble_sort", utest_tc_init, utest_tc_cleanup, 10);
Scons脚本SConscript,增加了RT_USING_UTEST的依赖项:
import os
from building import *
cwd = GetCurrentDir()
src = Glob('*.c') + Glob('*.cpp')
CPPPATH = [cwd]
# 增加RT_USING_UTEST依赖项,如果定义RT_USING_UTEST则编译测试用例
# 不需要做覆盖率测试的代码,千万不要添加LOCAL_CFLAGS=' --coverage'
group = DefineGroup('src', src, depend = ['RT_USING_UTEST'], CPPPATH = CPPPATH)
Return('group')
1、gcov_output.bin文件存储格式
// 前面是N个gcda文件的信息
for i = 0 : N
第i个gcda文件路径\0,用\0分割字符串
第i个gcda信息长度,4个字节(大端在前)
第i个gcda信息内容
end
// 文件最后输出
Gcov End\0
编写了一个python脚本来解析该文件,并生成分离的gcda文件
import struct
gcov_output_file = r"gcov_output.bin"
with open(gcov_output_file, "rb") as fd:
content = fd.read()
while len(content) > 0:
# 读取gcda文件名
is_file_find = False
for i in range(len(content)):
if content[i] == 0:
gcda_file_name = struct.unpack(str(i)+'s', content[:i])[0]
content = content[i+1:]
is_file_find = True
break
if not is_file_find:
break
if gcda_file_name == b"Gcov End":
break
# 读取gcda数据长度
gcda_data_size = struct.unpack('>I', content[:4])[0]
content = content[4:]
# 读取gcada数据内容
gcda_data_buff = struct.unpack(str(gcda_data_size)+'s', content[:gcda_data_size])[0]
content = content[gcda_data_size:]
# 将数据写入到文件
with open(gcda_file_name, "wb") as gcda:
gcda.write(gcda_data_buff)
1、目前使用过程需要输入较多命令,后续会将相关命令写成一个python脚本,然后通过scons coverage来一次性执行。
2、计划支持gcovr工具,相比lcov,gcovr对跨平台更友好,命令行也更加简介。
1、不需要做覆盖率测试的代码,DefineGroup千万不要添加LOCAL_CFLAGS=' --coverage'参数
2、lcov版本就用v1.15就好,不要升级到v1.16,否则在git bash中无法正确运行
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。