Haps平台GCOV环境搭建和工程化

251 阅读8分钟

主要解决了以下几个问题:

1.拓展到haps平台

2.重新梳理编译关系

3.拓展definegroup函数

4.Haps上自动运行脚本

环境准备

最近计划将统计覆盖率的功能集成到嵌入式系统中,希望能够在Haps上使用

先是环境准备:

适配过程

准备好这些环境之后,第一步进行代码移植,把gcov的代码下载并拷贝到工程下,然后把测试代码准备好,加上__gcov_call_constructors和__gcov_exit,再给待测试代码加上“--coverage”的编译选项。

就可以开始快乐的适配运行(解决一大堆问题)了~

2.1 无文件系统问题

遇到的第一个问题是,目前项目没有配文件系统,而gcov大部分使用的方案都是生成一个sd.bin,然后再将gcov_output.bin文件提取出来。

还好,在仔细阅读官方代码和注释之后,发现这个问题比较好解决,只需要开启串口打印即可,然后再自行保存

以下就是在gcov_public.h里找到的内容:

// 是否通过打印串口输入出覆盖率信息
// 如果没有文件系统,这是将覆盖率信息输出一种手段,平时也可以作为调试使用
// 如果需要统计覆盖率信息的文件很多,建议关闭
#define GCOV_OPT_OUTPUT_SERIAL_HEXDUMP

2.2 串口打印到一半就卡住了

第一反应应该是buffer不够大,别问我怎么知道的,python做自动化,串口获取log的时候就踩过坑,所以这次一开始就往这个方向怀疑,然后就开始在gcov的代码里找,找到这个参数:

gcov_unsigned_t gcov_buf[10240];

    while (listptr) {
        gcov_unsigned_t *buffer = NULL; // Need buffer to be 32-bit-aligned for type-safe internal usage
        u32 bytesNeeded;

        /* Do pretend conversion to see how many bytes are needed */
        bytesNeeded = gcov_convert_to_gcda(NULL, allocate_fn, listptr->info);

#ifdef GCOV_OPT_USE_MALLOC
        buffer = malloc(bytesNeeded);
#else
        if (bytesNeeded > sizeof(gcov_buf)/(sizeof(char))) {
            buffer = (gcov_unsigned_t *)NULL;
        } else {
            buffer = gcov_buf;
        }
#endif // GCOV_OPT_USE_MALLOC else

有些嵌入式系统是不支持GCOV_OPT_USE_MALLOC的,在没有开启GCOV_OPT_USE_MALLOC的时候,是需要人为的修改这个大小的。

看起来应该找到原因了,然而改大到80960之后问题并没有解决,其实想想也能理解,虽然打印了覆盖率信息比较多,但应该还没有到10k。也就是说原本10240目测是够的。

于是继续寻找,然后想起来我们系统里的ULOG也有一个buffer配置的大小,于是赶紧去改,改大之后,果然ok了。

至此,已经能够把覆盖率信息完整的在串口打印出来了。

就是这样一堆数字

2.3 如何处理串口获取的数据

因为串口里除了这部分覆盖率的数据,还会打印测试的log,那就自己写个python脚本,把数据处理一下,然后再调用lcov工具,生成网页版的测试覆盖率信息。

其中log转gcda的函数如下:

转完gcda之后,脚本再用os库,执行lcov的命令生成网页版的报告。

用户不需要知道细节,只需要执行这个脚本就可以了。

好了,这样就能够成功输出覆盖率报告了。

工程化

以上都是遇到问题解决问题,最后把整个流程跑通了,可是当他要作为一个发布的功能,给所有人去使用,就不能像这样稀里糊涂了,要怀着一颗做产品的心,让所有使用者不需要知道内部细节,然后通过最简单的方法能够使用他。

所以原则是两个,一个是尽可能规范,一个是尽可能精简

我们从头重新梳理一下,第一步先确定文件夹的相对位置

工程主目录
├───platform
│   ├───components  
│           └───memblock   # 即将被测试模块
│                   ├───code_be_test   # 即将被测试的代码
│                   ├───Kconfig   # 被测模块的配置项,需修改
│                   └───SConscript     # 被测模块的编译脚本
│           └───embedded-gcov   # 用于进行覆盖率测试的工具,用户无需修改
│                   ├───.c   # gcov核心代码
│                   └───.h   # test文件中需要include
│   └───scripts
│        └───gcov
│                   ├───gcov_auto.py   # 后处理脚本,执行该脚本可生成覆盖率结果
│                   ├───xxxx.txt   # log文件,Haps或qemu运行完生成的log拷贝到这里
│                   └───coverage     # 文件夹,执行完python脚本后生成
├───tests
│    └───platform
│        └───memblk
│                  ├───test code   # 测试代码
│                  └───配置项  #测试模块的配置项

把embedded-gcov移植过去之后,要为他编写kconfig,sconscript,确保能编译进去

menuconfig CODE_COVERAGE
    bool "enable code coverage"
    default n
    help
        get test coverage information if selected
import os
from building import *

cwd     = GetCurrentDir()

src     = Glob('*.c') + Glob('*.cpp')

CPPPATH = [cwd]

group = DefineGroup('gcov', src, depend = ['CODE_COVERAGE'], CPPPATH = CPPPATH)

Return('group')

这样就为这个模块创建了一个配置项:CODE_COVERAGE,接下来我们就开始考虑测试代码和被测试代码了,被测试代码怎样插桩,测试case执行怎样开启覆盖率。

3.1 配置相互的依赖关系

之所以需要考虑这个问题,是一开始参考这个项目:rtt-gcov: 本项目使用RT-Thread的QEMU VExpress A9板级支持包,演示如何在RT-Thread项目中进行单元测试和覆盖率测试。 (gitee.com)

针对被测代码,需要添加编译参数“--coverage”,在被测代码的sconscript里:

import os
from building import *

cwd     = GetCurrentDir()
src     = Glob('*.c') + Glob('*.cpp')
CPPPATH = [cwd]

if GetDepend(["CODE_COVERAGE"]):
    # 如果定义了PKG_USING_GCOV,则开启覆盖率编译选项
    group = DefineGroup('src', src, depend = [''], CPPPATH = CPPPATH, LOCAL_CFLAGS=' --coverage')
else:
    group = DefineGroup('src', src, depend = [''], CPPPATH = CPPPATH)

Return('group')

但这样写不适合一个大型的项目,虽然作者用了if else,但如果提交到库上会有两个问题,

  1. 如果不止一个模块,比如A和B,都这样写的话,A打开了覆盖率,B模块也会打开覆盖率。
  2. 如果每个模块都加这一段,那么就很冗余,代码很丑

针对问题一,可以重新定义一个模块的覆盖率的标签,xxx_COVERAGE,例如内存管理:MEMBLOCK_COVERAGE,

这样想要进行覆盖率检测的时候,只需要在.conf文件里或者rtconfig.h或者menuconfig中打开这个配置项即可

在测试代码中,加上这三句

#ifdef AAA_COVERAGE
#include <gcov_public.h>
#endif

3.2 扩展DefineGroup接口

针对上面提到的第二个问题:”代码冗余“,可以选择在defineGroup函数里内部处理,自动判断当前的配置项里有没有覆盖率这个标签,如果有,就把当前的LOCAL_CFLAG加上编译选项。

一开始我想这个简单呀,但实际执行的时候卡在了要找到对应位置,而不会给所有文件都加上了配置项,经过一番调试才成功。在这过程中,也学会了看compile_commands.json和scons -verbose这个命令,能看到整个编译过程的所有的详细参数,其中细节就暂且不表,分享一下build.py中在defineGroup函数中新增的代码吧:

    if type(depend) == type([]) and len(depend) >= 1:
        if GetDepend(depend[0]+'_COVERAGE'):
            if 'LOCAL_CFLAGS' in group:
                group['LOCAL_CFLAGS'] += ' --coverage'
            else:
                group['LOCAL_CFLAGS'] = ' --coverage'

至此,用户就可以最简单的,仅仅通过一个配置项,就能开启覆盖率功能了,一切都令人舒适。

3.3 buffer大小设为可配置项

那么我想还有什么可以优化的呢。

回忆起2.2遇到的log打印不出来的问题,那么如果其他人遇到了,岂不是需要他们自己去修改代码,no no no,这样不行,那就索性把

#ifdef GCOV_OPT_OUTPUT_BINARY_MEMORY

是否输出到内存区以及到内存哪个地址

以及

#ifndef GCOV_OPT_USE_MALLOC

是否动态分配,以及分配的buffer大小

这两个选项作为配置项,这样使用者可以直接根据需要修改和设定。

3.4 Haps上自动化执行

在Haps上运行的话,不可避免的出现编译和运行在两个环境,为了尽可能让用户更方便使用,所以写了一个自动化脚本,这个脚本其实是我之前写的自动化测试的那一套改了一点直接拿来用了,基本也就是根据用户指定cmm脚本、elf路径、用trace32跑完,把串口log保存下来再存回去。

3.5 其余问题

其余一些小问题,也做了下调研:

不使用utest框架,只需要在main函数,开始执行和结束时分别加上__gcov_call_constructors和__gcov_exit,就可以使用了

多次测试结果合并覆盖率, lcov提供了这个命令可以实现

lcov -a add.info -a sub.info -o total.info # combine them into total.info