背景技术
软件测试是指在软件开发的整个生命周期中对软件质量进行有效的控制,是软件质量的重要保证。据统计,测试可以使软件的缺陷数至少降低75%,从而减少软件的投资风险,提高投资回报率。
代码覆盖测试是评价软件测试的质量的一个重要指标,它通过收集覆盖率数据,对测试结果的可信度进行评价。根据覆盖内容的不同,覆盖可以分为语句覆盖、判定覆盖、条件覆盖、判定条件覆盖、条件组合覆盖、路径覆盖。其中常用的为前五种。
语句覆盖指设计足够多的测试用例,使每条语句至少执行一次。其覆盖率计算公式为:
判定覆盖指设计足够多的测试用例,使每个判定的每个分支至少执行一次。其覆盖率计算公式为:
条件覆盖指设计足够多的测试用例,使每个判定的每个条件取到各种可能的值,其覆盖率计算公式为:
判定/条件覆盖指设计足够多的测试用例,使判定中每个条件取到各种可能的值,并使每个判定取到各种可能的结果,其覆盖率计算公式为:
条件组合覆盖指设计足够多的测试用例,使每个判定中条件各种可能组合都至少出现一次。其覆盖率计算公式为:
插桩是代码覆盖测试中收集数据的一个主要途径,是软件测试中的一个关键步骤。它指在被测程序的适当位置植入探针函数,以此采集被测程序的执行相关信息。当被测程序运行时,由探针函数记录其运行时的特征信息,提供给后续的分析,确定语句执行次数和覆盖率。而这些特征信息正是评价测试完全程度的重要因素,也是衡量测试质量的重要指标。由于向被测程序中植入了探针函数,插桩必然会带来一定的代码膨胀和额外开销。所以设计一种高效的插桩方法来降低代码膨胀,对嵌入式软件测试有着重要的意义。
探针函数又称为桩函数,用来记录被测程序运行过程中测试关注的信息。设计合理高效的探针函数,对插桩的效果有很重要的影响。探针函数的插入必须不能影响被测程序的正确运行,而且其传递的被测程序的执行信息量也应适当。为了保证探针函数的独立性和复用性,探针函数的函数体不应直接插入被测程序源文件,而应以库函数的方式链接到源文件中,这样使得插桩的过程更加灵活。
现有的插桩方法,主要是基于基本块的插桩。基本块指程序中一个顺序执行的语句序列,它只有一个入口语句和一个出口语句,执行时只能从入口语句进入,从出口语句退出。基本块中的所有语句的执行次数一定是相同的,一般由多个顺序执行语句后跟一个跳转语句组成。这类插桩方法将程序划分为一系列顺序的基本块,在基本块的入口语句和出口语句插桩。但这类方法打乱了程序的层级关系和嵌套关系,不利于程序结构的分析。
程序块结构是将整个程序分成若干个信息块或子程序块。使用一对大括号“{”和“}”将程序中的若干连续的代码行括起来,被括起的这些代码行就被组织成为一个逻辑执行块。逻辑执行块以左大括号为入口,以右大括号为顺序出口,以跳转语句(包括continue、return、break等)为非顺序出口。每个逻辑执行块只有一个入口语句和一个顺序出口语句,但可以有多个非顺序出口语句。以逻辑执行块为单位对程序进行插桩,在其入口和出口处植入探针函数,既保持了程序的层级关系、嵌套关系,也减小插桩引起的代码膨胀。
发明内容
本发明的目的在于设计一种基于逻辑执行块的嵌入式软件测试插桩方法,解决现有插桩方法的不足,在理清程序层级关系、嵌套关系、语句类型的基础上,准确快速的分析出语句的执行次数和覆盖率,减小插桩后代码的膨胀率。
本发明的解决方案:一种基于逻辑执行块的嵌入式软件测试插桩方法,其具体步骤如下:
(1)对程序进行预处理。在适当的位置补加成对的大括号(如分支的单条语句、循环体的单条语句),使程序的逻辑执行结构易于分析处理。根据不同的覆盖测试类型,处理程序中的分支语句和循环语句。将原分支体、循环体中的语句抽象为一个函数,便于在条件拆分后的各个分支中调用,以减少代码量。预处理可能变动了程序语句所在的行号,所以将生成预处理前后程序的行号对应关系表。
(2)程序语句按类型被分为18类。以超前搜索的方式,从语句中识别出每种类型的标志字符串,从而得到语句的类型,确定语句的可执行性(可执行或非可执行)。在分析过程中,需要记录插桩相关的语句信息,包括语句类型、语句所处的嵌套层级以及语句的执行次数。其中嵌套层级初始化为0,在程序分块过程中填充。可执行语句的执行次数初始化为-1,非可执行语句的执行次数初始化为-2,在执行次数统计过程中计算。
(3)根据配对的大括号将程序划分为不同的逻辑执行块。逻辑执行块以左大括号为入口,以右大括号为顺序出口,以跳转语句(包括continue、return、break等)为非顺序出口。一个逻辑执行块有且只有一个入口和一个顺序出口,但是可以有多个非顺序出口。为嵌套层级定义一个全局变量并初始化为0。每遇到一个逻辑执行块的入口,嵌套层级变量加1,即嵌套深入一层,用其值更新步骤(2)中记录的该入口语句的嵌套层级信息;每遇到一个逻辑执行块的顺序出口,用当前嵌套层级更新步骤(2)中记录的该出口语句的嵌套层级信息,并将嵌套层级变量减1,即嵌套跳出一层。遇到逻辑执行块的非顺序出口,查找到该块的入口语句,将入口语句的嵌套层级作为当前非顺序出口的嵌套层级。根据逻辑执行块入口语句和顺序出口语句可以确定该块所处的嵌套层级。
(4)初始化探针函数和调用探针函数所需的环境,在逻辑执行块的入口语句、出口语句插桩。探针函数设计为发送当前插桩点的位置信息。
(5)每次执行到插桩点,由探针函数发送当前的位置信息。统计接收到的桩信息,并更新步骤(2)中记录的语句信息中相应行号的执行次数。遍历语句信息,用栈结构来匹配逻辑执行块的入口语句和出口语句。按照逻辑执行块所处的嵌套层级及其入口出口语句,计算出该块内所有语句的执行次数。
(6)计算不同类型的覆盖率。
具体实施方式
结合图1和实施例对本发明做进一步的详细说明:
1.对被测程序进行整理,补全每个逻辑执行块的起始和终止大括号,生成整理前后文件的行号对应关系。再根据覆盖类型的不同,处理程序中的分支语句和循环语句。具体策略如下:
(1)若为语句覆盖率测试,不需要处理。
(2)若为判定覆盖率测试,对do-while语句添加条件不成立时的分支,对所有缺少else的if语句进行补全,添加else分支。添加的分支不包含具体的执行语句,但作为一个逻辑执行块。
(3)若为条件覆盖测试、判定/条件覆盖测试、条件组合覆盖测试,预处理方式相同。对do-while语句,添加条件不成立时的分支,该分支不包含具体的执行语句,但作为逻辑执行块。对单一条件的判定表达式,如果是缺少else的if语句,补全其else分支,该else分支不包含具体的执行语句,但作为逻辑执行块。对多个条件的判定表达式,将判定表达式的各个条件拆分为多个嵌套关系的if-else语句。假设每个条件真为1,条件假为0,将拆分后的执行路径,按条件取值以二进制码递增序排列。
为了减少代码量,将原分支中的执行语句抽象为一个函数,便于在条件拆分后的各个分支中调用。
统计被测程序的语句数,建立相应大小的语句信息数组lineinfo[]。数组元素结构如下:
其中,exenum记录语句的执行次数,值为-2代表该语句为非可执行语句,值为-1代表该语句还没有计算执行次数。layer记录语句所处的嵌套层级,type记录语句所属的类型。
2.分析被测程序结构,确定语句的类型、可执行性和嵌套层级。语句的类型划分为18类,具体分类如表1所示。其中类型3~类型7为非可执行语句,其余为可执行语句。
表1程序语句类型表
类型号 |
类型名 |
标志字符串 |
说明 |
0 |
普通语句 |
无 |
除了特殊分类外的可执行语句 |
1 |
左大括号 |
{ |
逻辑执行块的入口语句 |
2 |
右大括号 |
} |
逻辑执行块的顺序出口语句 |
3 |
文件包含指令 |
#、include |
#include指令 |
4 |
宏定义指令 |
#、define |
#define指令 |
5 |
其他预编译指令 |
#、非include、非define |
除了#include和#define外的预编译指令 |
6 |
声明 |
类型关键字、(、)、; |
变量声明语句和函数声明语句 |
7 |
函数体起始语句 |
类型关键字、(、) |
函数定义中函数首部 |
8 |
循环语句 |
while、do-while、for |
while循环、do-while循环和for循环 |
9 |
二路分支语句 |
if、else |
if语句、else语句、ifelse语句 |
10 |
多路分支语句 |
switch |
switch语句 |
11 |
switch的分支 |
case、default |
case语句、default语句 |
12 |
分支结束语句 |
在case或default分支中, |
switch的最后一个分支中的最后一条语句 |
|
|
且下一条语句为} |
|
13 |
break语句 |
break |
break语句 |
14 |
continue语句 |
continue |
continue语句 |
15 |
return语句 |
return |
return语句 |
16 |
goto语句 |
goto、标号、: |
goto语句、goto的标号所在语句 |
17 |
主函数起始语句 |
main、(、) |
主函数定义中的函数首部 |
3.逐条读入被测程序的语句,以超前搜索的方式识别各类语句的标志字符串,识别过程的状态转移图如图2所示。识别过程中,根据语句类型的可执行性初始化lineinfo[]中相应元素的exenum字段(非可执行语句初始化为-2,可执行语句初始化为-1)。
在识别过程中,同时利用栈结构匹配逻辑执行块的入口和出口,对程序进行分块插桩,具体流程如图3所示。定义全局变量layer记录当前逻辑执行块所处的嵌套层级。
识别到逻辑执行块的入口语句,按顺序进行如下操作:
(1)将该入口语句入栈。
(2)执行layer++操作。
(3)将当前全局变量layer的值,写入lineinfo[]中该入口语句对应元素的layer字段。
识别到逻辑执行块的顺序出口语句,按顺序进行如下操作:
(1)将当前栈顶元素出栈。
(2)将当前全局变量layer的值,写入lineinfo[]中该顺序出口语句对应元素的layer字段。
(3)执行layer一操作。
识别到逻辑执行块的非顺序出口语句分为四种情况:
(1)continue语句:从栈顶开始搜索,直到找到这样一条语句,该入口语句的前一条语句为循环类型的语句。将找到的入口语句的嵌套层级赋给continue语句在lineinfo[]中对应元素的layer字段。
(2)break语句:从该语句向前搜索,直到找到这样一条语句,该入口语句的前一条语句为循环类型或switch的分支类型。将找到的入口语句的嵌套层级赋给break语句在lineinfo[]中对应元素的layer字段。
(3)goto语句:取栈顶元素,将该语句的嵌套层级赋给goto语句在lineinfo[]中对应元素的layer字段。
(4)return语句:return语句的嵌套层级为1,将1直接赋给该语句在lineinfo[]中对应元素的layer字段。
4.初始化探针函数。在被测文件的开头插入一条文件包含命令,将探针函数所在的库引入被测文件。在被测文件的逻辑执行块入口、顺序出口以及非顺序出口插桩。其中goto和switch的相关语句需要特殊处理,具体策略如下:
switch语句:
(1)将case或default语句视为特殊的逻辑执行块入口,插桩。
(2)若case或default的前一条语句既不是{也不是break,其前一条语句视为特殊的逻辑执行块出口,插桩
(3)将最后一个分支的最后一条语句视为特殊的逻辑执行块出口,插桩。
goto语句:
(1)goto语句本身视为逻辑执行块的非顺序出口语句,插桩。
(2)将goto语句的标号的前一条语句,视为特殊的逻辑执行块出口(前一个逻辑执行块的顺序出口),插桩。
(3)将goto语句的标号的后一条语句,视为特殊的逻辑执行入口语句(下一个逻辑执行块的入口),插桩。
5.根据运行插桩后被测程序获得的桩信息数据,统计插桩点语句的执行次数,并填充这些语句在lineinfo[]中对应元素的exenum字段。遍历lineinfo[]数组统计所有语句的执行次数,用栈结构匹配其中的逻辑执行块入口和出口。对每个逻辑执行块,选择适当的标准执行次数来填充块内所有exenum字段值为-1的语句的执行次数。如果标准执行次数的值为-1,则将0作为标准执行次数来填充。具体流程如图4所示。
根据逻辑执行块入口和出口的不同,具体策略如下:
(1)遇到逻辑执行块入口语句,入栈。
(2)遇到逻辑执行块顺序出口语句(其行号记为i),出栈(出栈语句行号记为j)。确定一个标准执行次数,填充lineinfo[]中,区间[i,j]内所有exenum字段值为-1的语句的执行次数。标准执行次数的选择具体分为两种情况:
①若语句j为入口语句,将语句j的exenum字段值作为标准执行次数。
②若语句j为非顺序出口语句,将语句i的exenum字段值作为标准执行次数。
(3)遇到逻辑执行块非顺序出口语句(其行号记为i),具体操作如下:
①出栈一条语句(其行号记为j)。
若语句j为入口语句,选择语句j的exenum字段值为标准执行次数。填充lineinfo[]中,区间[i,j]内所有exenum字段值为-1的语句的执行次数。
若语句j为非顺序出口语句,将语句j压入临时栈。
②重复步骤①,直到语句i的layer字段值与语句j的layer字段值相等。
③将临时栈内的数据出栈,依次压入原匹配逻辑执行块出入口的栈。
6.计算不同类型的覆盖率。
(1)语句覆盖率。可执行语句为lineinfo[]中exenum字段不等于-2的语句,执行到的语句为lineinfo[]中exenum字段大于0的语句。统计两者的值,可以计算出语句覆盖率。
(2)判定覆盖率。总的判定路径数为lineinfo[]中type字段等于10的语句的总数,评价到的判定路径数为lineinfo[]中type字段等于10且exenum字段大于0的语句的总数。统计两者的值,可以计算出判定覆盖率。
(3)条件覆盖率。统计程序中出现的所有条件数,其值乘以2,作为条件取值的总数。根据执行到的分支的序号,分析出该分支包含的条件取值的真假,统计出评价到的条件取值数。可以计算出条件覆盖率。
(4)判定/条件覆盖率。统计判定路径总数和条件取值总数,两者求和作为判定/条件覆盖公式中的分母。统计评价到的判定路径数和条件取值数,两者求和作为判定/条件覆盖率公式中的分子。可以计算出判定/条件覆盖率。
(5)条件组合覆盖率。总的条件取值组合数为lineinfo[]中type字段等于10的语句的总数。执行的到条件取值组合数为lineinfo[]中type字段等于10且exenum字段大于0的语句的总数,统计两者的值,可以计算出条件组合覆盖率。
7.最后根据预处理前后文件的行号对应关系表,将lineinfo[]中记录的执行次数转换成原被测文件中语句的执行次数。