# rtt-gcov **Repository Path**: latercomer/rtt-gcov ## Basic Information - **Project Name**: rtt-gcov - **Description**: 本项目使用RT-Thread的QEMU VExpress A9板级支持包,演示如何在RT-Thread项目中进行单元测试和覆盖率测试。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 11 - **Forks**: 6 - **Created**: 2023-03-26 - **Last Updated**: 2025-10-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RT-Thread 单元测试 & 代码覆盖率 本项目使用RT-Thread的[QEMU VExpress A9](https://github.com/RT-Thread/rt-thread/tree/lts-v4.1.x/bsp/qemu-vexpress-a9)板级支持包,演示如何在RT-Thread项目中进行[单元测试](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/utest/utest)和覆盖率测试。 本项目应用到的代码、程序、工具,包含: - [Rt-thread lst-v4.1.x](https://github.com/RT-Thread/rt-thread/tree/lts-v4.1.x),RT-Thread源代码,使用[utest框架](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/utest/utest)、[qemu-vexpress-a9 bsp](https://github.com/RT-Thread/rt-thread/tree/lts-v4.1.x/bsp/qemu-vexpress-a9) - [Env v1.3.5](https://download-sh-cmcc.rt-thread.org:9151/www/aozima/env_released_1.3.5.7z),RT-Thread开发工具链,详细使用方法[参见官方教程](https://www.rt-thread.org/document/site/#/development-tools/env/env) - [embedded-gcov](https://github.com/nasa-jpl/embedded-gcov),嵌入式gcov工具包 - [lcov](https://udomain.dl.sourceforge.net/project/ltp/Coverage%20Analysis/LCOV-1.15/lcov-1.15.tar.gz),用于生成覆盖测试报告的,需要linux环境,在windows可以在Git Bash中运行 - [7zip](https://7-zip.org/a/7z2201-x64.exe),用来从sd.bin(其实就是一个压缩包)中提出RTT运行时生成的覆盖率文件 - [Git for windows]() - [Vs code]() 在此十分感谢[海南大学刘伟同学](https://jswyll.com/note/embed/lcov/)的热心帮助。 ## 目录结构 ```sh 工程主目录 ├───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](tools/7z2201-x64.exe)。建议将7zip安装目录`C:\Program Files\7-Zip`添加到系统PATH环境变量。 2、下载安装env和lcov两个工具,前者是RTT开发工具链,后者用于覆盖率报告生成。 下载[env-v1.3.5](https://download-sh-cmcc.rt-thread.org:9151/www/aozima/env_released_1.3.5.7z),解压到不包含中文路径(比如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](https://udomain.dl.sourceforge.net/project/ltp/Coverage%20Analysis/LCOV-1.15/lcov-1.15.tar.gz)(**不要用v1.16哈,用v1.15就好了**),解压到不包含中文的路径(比如C:\lcov-1.16),并将`C:\lcov-1.15\bin`添加到系统PATH环境变量。 3、下载本项目代码,比如clone到D:\nextpilot\rtt-gcov。 ```sh git clone https://gitee.com/latercomer/rtt-gcov.git ``` 4、打开env终端C:\env-windows-v1.3.5\env.exe,并在终端运行: ```sh # 切换到工程目录 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模拟器中运行起来,终端显示如下 ![](./figures/start-qemu.png) 5、执行utest中的测试用例 ```sh # 查看包含了哪些用例 utest_list # 运行所有测试用例 utest_run ``` ![](./figures/utest-gcov.png) 5、Ctr+C停止env终端,使用7zip打开压缩包D:\nextpilot\rtt-gcov\sd.bin,并将其中gcov_output.bin文件提取出来。 ```sh # 切换到工程目录 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 ``` ![](./figures/7zip-open-sdbin.png) 6、调用[parse_gcov_output.py](packages/embedded-gcov/scripts/parse_gcov_output.py)脚本将gcov_output.bin文件拆分成独立的.gcda文件。 ```sh 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`,输入: ```sh # 切换到工程目录 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`,查看被测试代码的覆盖结果: ![示例代码测试结果.png](figures/lcov-gen-html.png "示例代码测试结果") ## 代码移植 1、将[packages\embedded-gcov](packages/embedded-gcov)拷贝到您的工程,并设置[gcov_public.h](packages/embedded-gcov/gcov/gcov_public.h)中部分宏,意义如下: ```c // 使用为覆盖率信息存储动态分配内存 // 否则使用预定义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](packages/embedded-gcov/gcov/gcov_public.c)中gcov_GcovInfo和gcov_buf两个数组的大小,防止开辟的空间不够。 ```c #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](rtconfig.py)中开启覆盖率链接选项,也就是LFLAGS中增加--coverage。 ```python # 使链接时将相关的代码也使用覆盖率测试 --coverage LFLAGS = DEVICE + ' -nostartfiles -Wl,--gc-sections,-Map=rtthread.map,-cref,-u,system_vectors --coverage' + ' -T %s' % LINK_SCRIPT ``` 3、修改[rtconfig.h](rtconfig.h)文件,定义以下几个宏: ```c /* 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](applications/main.c)文件中,当然其它任意位置都可以。 > __gcov_call_constructors()只能被调用一次,且越早调用越好,如果rtt开启了RT_USING_CPLUSPLUS,则rtt会在cplusplus_system_init()将.init.array区的函数调用一遍,因此不需要pkg_gcov_init()再次调用了。 ```c #include #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运行结束等方式触发。 ```c // 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](applications/code_be_test/SConscript)该如下修改: ```python 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测试用例](applications/utest_tc_cases/test_bubble_sort.c),需要注意的是在`utest_tc_cleanup()`函数中调用了`__gcov_exit`也就是用例跑完之后输出覆盖率信息,utest测试框架代码如下: > RTT UTEST测试框架UTEST_UNIT_RUN宏展开发现,当任意测试套件中,有用例断言失败的时候,将会终止当前套件后续用例的执行。 ```c #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](applications/utest_tc_cases/SConscript),增加了RT_USING_UTEST的依赖项: ```python 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文件存储格式 ```c // 前面是N个gcda文件的信息 for i = 0 : N 第i个gcda文件路径\0,用\0分割字符串 第i个gcda信息长度,4个字节(大端在前) 第i个gcda信息内容 end // 文件最后输出 Gcov End\0 ``` 编写了一个python脚本来解析该文件,并生成分离的gcda文件 ```python 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中无法正确运行