Gcov,lcov测试嵌入式平台的代码覆盖率

参考博客:https://www.cnblogs.com/StitchSun/p/4480229.html

Gcov 查看代码覆盖率 - 知乎

gcov可以测试行覆盖率、函数覆盖率,分支覆盖率的数据

前提条件:

1、对应平台的交叉编译器需要包含gcov功能

2、需要安装lcov

测试步骤:

1、在组件编译中添加gcov编译参数,编译debug版本,同时生成gcno文件

2、将生成的elf文件到嵌入式平台上运行

3、运行后将生成的gcda文件从memory中dump下来

4、用lcov工具解析gcno文件与gcda文件,生成html报表文件

接下来需要详细介绍一下工作:

添加编译参数:

CFLAGS := -O0 -g -Wall -fprofile-arcs -ftest-coverage

这种添加编译参数的方法会所有的C文件都生成gcno文件, 所以除了这种方法还可以只对目标文件生成gcno文件,我在使用的时候发现很多时候 确实不知道程序是怎么运行的,不知道程序调了那些文件,运行了那些文件里面的函数,所以我是对所有文件都添加了编译参数,也就是在makefile中添加上面的编译参数,这样会造成生成的gcda文件较大,所以需要确认一下嵌入式平台的memory,一般来说可以先试试。

这里我需要强调一下,官方文件说需要加上对应的库-lcov,但是其实我没有添加,好像也没问题。

ifeq ($(TARGET), utest)

CFLAGS += -fprofile-arcs -ftest-coverage-O0 -g -Wall

endif

 make后将生成的elf文件放到嵌入式平台上运行,大家可以看看gcov文件的寄存器地址,以及里面内置的一些函数:我主要做的修改是

在(gcc_dr1m90_ddr.ld)文件中加上了下面这些东西:

.fini           :
    {
        KEEP (*(SORT_NONE(.fini)))
    } >ram AT>ram

    . = ALIGN(4);

    .preinit_array  :
    {
        PROVIDE_HIDDEN (__preinit_array_start = .);
        KEEP (*(.preinit_array))
        PROVIDE_HIDDEN (__preinit_array_end = .);
    } >ram AT>ram

    .init_array     :
    {
        PROVIDE_HIDDEN (__init_array_start = .);
        KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
        KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
        PROVIDE_HIDDEN (__init_array_end = .);
    } >ram AT>ram

    .fini_array     :
    {
        PROVIDE_HIDDEN (__fini_array_start = .);
        KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
        KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
        PROVIDE_HIDDEN (__fini_array_end = .);
    } >ram AT>ram

    .ctors          :
    {
        /* gcc uses crtbegin.o to find the start of
        the constructors, so we make sure it is
        first.  Because this is a wildcard, it
        doesn't matter if the user does not
        actually link against crtbegin.o; the
        linker won't look for a file to match a
        wildcard.  The wildcard also means that it
        doesn't matter which directory crtbegin.o
        is in.  */
        KEEP (*crtbegin.o(.ctors))
        KEEP (*crtbegin?.o(.ctors))
        /* We don't want to include the .ctor section from
        the crtend.o file until after the sorted ctors.
        The .ctor section from the crtend file contains the
        end of ctors marker and it must be last */
        KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
        KEEP (*(SORT(.ctors.*)))
        KEEP (*(.ctors))
    } >ram AT>ram

    .dtors          :
    {
        KEEP (*crtbegin.o(.dtors))
        KEEP (*crtbegin?.o(.dtors))
        KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
        KEEP (*(SORT(.dtors.*)))
        KEEP (*(.dtors))
    } >ram AT>ram

我的gdb.init文件

target remote 10.8.9.4:3333
load ./utest_dr1m90_PINMUX_MODE1.elf
file ./utest_dr1m90_PINMUX_MODE1.elf
#b __gcov_init
#b __gcov_call_constructors
#b __gcov_call_constructors
b gcov_public.c:397
b __gcov_exit
c

 go.文件

 /opt/toolchain/7.5.0/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-elf/bin/aarch64-elf-gdb --command=./gdb.init

 然后我程序中

static unsigned char *gcov_output_buffer = (unsigned char *)(0x61000000);

static gcov_unsigned_t gcov_output_index

 所以在程序运行结束以后,我需要将运行文件从memory中dump出来

可以先在gdb中打印一下gcov_output_index的值,然后dump

dump binary memory gcda.data 0x61000000 0x61000000+gcov_output_index

然后目前在你服务器上的目录中刷新一下就会生成一个gcda.data文件,然后因为嵌入式平台上没有文件系统,所以我们要将这一坨memory中dump出来的文件分类,分成各个子文件的gcda文件。

在服务器上搭建python运行环境

#!usr/bin/python

import sys

import struct

import os

if len(sys.argv) < 3:

    print("Usage: python dump_gcov.py <file2dump.bin> <output path>")

    sys.exit(1)

fname = sys.argv[1]

out_path = sys.argv[2]

os.mkdir(out_path)

with open(fname, "rb") as f:

    raw = f.read()

while len(raw) > 0:

    fullpath, _, load = raw.partition(b'\x00')

    path, _, name = fullpath.decode("ASCII").rpartition('/')

    print("Found {:s}".format(name), end=":\t")

    length, = struct.unpack(">I", load[0: 4])

    out_file = os.path.join(out_path, name)

    with open(out_file, "wb") as f:

        f.write(load[4: 4 + length])

    print("{:d} bytes".format(length))

    raw = load[4 + length: ]

 python dump_gcov.py gcda.data + <PATH>

 路径下最后的文件夹中最好不要放东西。这个python文件最后运行下来会报错,但是没关系,所有的子文件的gcda文件都在的。

这里有这么一个问题,就是生成的gcno和gcda文件都是以源文件名加上原后缀名加上.gcno 或者.gcda结尾,这个是不对的

eg:

源文件名:test.c

在编译后生成的gcno文件名: test.c.gcno

在运行后生成的gcda文件名是: test.c.gcda

这个是有问题的,需要将.c删掉。

然后需要将服务器上交叉编译器的lcov的gcov版本改一下,lcov默认使用linux自带的gcov,需要修改为使用交叉编译器的gcov工具,修改的文件为:bin/geninfo,把原来的gcov改成交叉编译的gcov。

然后调用交叉编译器的gcov:之前教程中都是直接gcov test.c但是这里不行,需要改成
我这里是这样的
/opt/toolchain/7.5.0/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-elf/bin/aarch64-elf-gcov al_uart_dev.c
然后

lcov -d . -t 'al_uart_dev' -o 'al_uart_dev.info' -b . -c

-d表示目录,.表示当前目录。-t表示生成的覆盖率文件的标题,-o表示生成的覆盖率数据文件的输出路径和文件名,-b表示要生成覆盖率报告的基准目录,-c表示制定生成覆盖率文件是否同时收集代码的执行基数信息,否则只收集覆盖率信息。
genhtml -o result al_uart_dev.info

 然后查看result中的Index.html就行,这个需要复制到win下。

 

 明天详细了解一下gcov的分支覆盖率和覆盖率报告的一些信息,周末更新!

Lcov在1.10以及之后的版本默认是关闭分支覆盖率的,开启的话需要在参数中加上 

--rc lcov_branch_coverage = 1

才能使能

ENABLE_BRANCH="--rc lcov_branch_coverage=1"
lcov -d . -t 'al_uart_dev' -o 'al_uart_dev.info' -b . -c

 另外还需要再genhtml中添加参数

genhtml --branch-coverage  -o $COVERAGE_DIR $GENHTML_OPTIONS ${ENABLE_BRANCH} $info_file 1>/dev/null

genhtml --branch-coverage -o result al_uart_dev.info

 lcov分支覆盖率的分析和总结_lcov 分支覆盖率_Type真是太帅了的博客-CSDN博客

覆盖率显示规则
一行从左到右分别代表:

1.代码行号(空白代表分支显示不过来产生换行)

2.分支覆盖情况 3.该行调用次数 4.该行源代码以及行覆盖情况

 

其中,分支覆盖情况详细介绍如下:
中括号代表生成的一对子分支,+代表该子分支被覆盖,-代表该子分支未覆盖,但对应的另一分支被覆盖,#代表两个子分支均未被覆盖。

以if(condition)为例,如果该condition没有子条件,即不是其他条件"与"、"或"产生,那么会产生两个分支,即condition == true 和 condition == false,若只能满足condition == true 或false,则分支覆盖结果为[+ -]或[- +],如果多次调用时condition == true 或false都能满足,则分支覆盖结果为[+ +]。如果condition == true或false都不能发生,那么覆盖结果为[# #](虽然在最简单的条件下这个结果并不会发生)。

行覆盖情况详细介绍如下:
如果该行代码被覆盖到,则其底色为蓝色,没有被覆盖到,则底色为蓝色,若该行是上一行代码的续行,或为return 语句、class声明等,其底色为白色,代表不会进行检测。注意,没有被覆盖的行不会产生分支。

除了常见的逻辑判断外,还有很多产生分支的情况,并且难以完全覆盖,总结如下:

一、 常见逻辑判断
1. if (condition1) - elseif (condition2) - else [if(true)和if(false)除外]

2. for(init;condition/incre)/while(condition) [while(true)、while(false)和for(init;;incre)等没有条件判断除外]

3.条件运算符 condition ? exp1 : exp2

4.switch - case

5.多条件的与或。常见的为return (mulit-condition) ,例如return (x==0 || x==1)。

二、空间/文件管理操作
解析:可能存在操作没有权限、内存分配失败等情况。

1. new /delete 操作符

2. 文件操作:打开、关闭、输出等

例如

cv::FileStorage cameraSettings{std::string(camera_file), cv::FileStorage::READ};
cameraSettings.isOpened()
cameraSettings.close()

以及fprintf等。

3.memcpy

三、宏/内联函数自带的判断
如果宏/内联函数声明中自带判断,那么预处理时会在调用处原地展开,最好的做法是用普通函数代替宏/内联函数,这样不会在每个调用的代码段都产生分支。

四、字符串转换
1.char *到String类

String类的构造函数如下:(加粗部分含有判断)

basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())

      : _M_dataplus(_M_local_data(), __a)

      { _M_construct(__s, __s ? __s + traits_type::length(__s) : __s+npos); }

char *转String时会判断char *是否为nullptr,即自带一个判断。

2.String类到char *

与char *到String类相似。

五、类成员函数调用
即使成员函数内部没有判断,也没有返回值。

六、cpp文件末尾。
七、其他库函数调用。
但是一般情况下只能使用测试样例实现对常见逻辑判断的覆盖,以及使用普通函数代替宏或者内联函数来提高分支覆盖率,其他情况下不能覆盖,只能给予说明。

复杂情形:
一、
if ((std::abs(p.arr[2]) < 1e-10) 
&& (std::abs(p.arr[3]) < 1e-10) 
&& (std::abs(p.arr[4]) < 1e-10) 
&&(std::abs(p.arr[5]) < 1e-10))
该判断共四个子条件,但lcov显示共10个分支,包括四个子条件分别成立和不成立,共八个分支,外加总条件,即这些子条件的与的成立和不成立两个条件,共十个分支:

std::abs(p.arr[2]) < 1e-10 成立
std::abs(p.arr[2]) < 1e-10 不成立
std::abs(p.arr[3]) < 1e-10 成立
std::abs(p.arr[3]) < 1e-10 不成立
std::abs(p.arr[4]) < 1e-10 成立
std::abs(p.arr[4]) < 1e-10 不成立
std::abs(p.arr[5]) < 1e-10 成立
std::abs(p.arr[5]) < 1e-10 不成立
std::abs产生,不明
std::abs产生,不明
二、
if ((!load_with_general_format) &&       
loadCameraModelParametersWithSVFormat(
std::string(camera_file), camera_param)) 
{return camera;}
看起来平平无奇只有两个子条件,其实蕴藏杀机,衍生出了14个分支,分别为:

load_with_general_format == true
load_with_general_format == false
函数loadCameraModelParametersWithSVFormat返回值为true
函数loadCameraModelParametersWithSVFormat返回值为false
std::string(camera_file)中camera_file[char *]为空
std::string(camera_file)中camera_file[char *]不为空
函数调用,不明
函数调用,不明
以及最后6个分支为函数loadCameraModelParametersWithSVFormat内部有三个if实现,即6个分支,共14个。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值