代码覆盖率分析
本章详细描述了 Coco 执行的代码分析。
为了分析例如测试套件的代码覆盖率,需要编译一个应用程序版本,在其中插入语句以记录源代码每一部分的执行情况。这种修改程序的版本生成的过程称为 instrumentation。
Coco 支持的覆盖率指标
Coco 支持多种代码覆盖率。以下表格总结了最常见的覆盖率指标。
指标 | 描述 |
---|---|
语句块覆盖率 | 通过将程序的语句分组到块中来验证所有语句的执行情况。如果语句总是一起执行,则属于同一块。语句块大致对应于大多数编程语言的 块。程序覆盖率是程序中执行语句块的数量除以块总数。 |
决策(或分支)覆盖率 | 验证所有语句都执行了,所有决策都有所有可能的结果。程序覆盖率是执行语句块和决策的数量除以语句和决策的总数。在这里,每个决策计两次,一次为 true 情况,一次为 false 情况。 |
条件覆盖率 | 与决策覆盖率类似,但决策被拆分成了由 and 或 or 运算符连接的基本子表达式(或 条件)。程序覆盖率是执行语句块和条件数量除以它们总数量。 在这里,每个条件计两次,可能会导致复杂决策中的可能结果数量很大。 |
修改的条件/决策覆盖率(MC/DC) | 与条件覆盖率类似,但每个条件都必须独立测试以达到完全覆盖率。对于每个条件,必须有两次执行,只在不同条件的结果上有差异,而其他条件的结果相同。此外,还需要证明每个条件独立影响决策。 程序覆盖率是执行语句块和独立测试条件的数量除以程序中语句块和条件的数量。 |
多重条件覆盖率 | 所有语句都必须执行,并且每个决策中所有真理值的组合至少出现一次,才能达到完整覆盖率。程序的覆盖率是已执行语句块和条件组合的数量除以程序中的总数。 |
还支持其他简单覆盖率指标。
指标 | 描述 |
---|---|
函数覆盖率 | 计算哪些函数被调用的次数和频率。像往常一样,Coco 中的函数也包括对象的成员函数。因此,程序的覆盖率是至少被调用一次的函数数除以函数的总数。 对安全标准的相关性:IEC 61508 强烈建议 SIL 1、2、3 和 4 的入口点(即函数)的 100% 结构测试覆盖率。 |
语句覆盖率 | 统计执行代码行的数量和每个行被执行了多少次。仅对包含可执行语句的行进行仪表化,不包含纯声明的行。因此,程序的覆盖率是执行语句数除以仪表化语句数。 语句覆盖率是不稳定的覆盖率度量,因为它强烈依赖于代码的格式方式(参见线覆盖率存在的问题)。我们不推荐使用语句覆盖率。 |
覆盖率指标和安全标准
在许多安全标准中,需要指定特定的覆盖率水平
- IEC 61508 是关于电气和电子可编程系统功能安全性的标准。
- ISO 26262 是关于汽车电气和电子系统功能安全性的标准。
- EN 50128 是关于铁路系统安全相关软件的标准。
- DO 178 是用于基于软件的商业航空航天系统的安全标准。
以下表格显示这些安全标准要求的覆盖率级别。
IEC 61508 | ISO 26262 | EN 50128 | DO 178 | |
---|---|---|---|---|
函数覆盖率(入口点) | ||||
语句块覆盖率 | ||||
决策覆盖率(分支覆盖率) | ||||
修改的条件/决策覆盖率 | ||||
多重条件覆盖率 |
根据安全级别,覆盖率要求可能是建议、强烈建议或必需的。更详细的信息可以在以下部分的描述中找到。
覆盖率指标描述
在本节中,我们将更详细地描述 Coco 支持的覆盖率指标。在代码插入中详细描述了在仪表化过程中插入的代码。
在下文中,我们将使用以下函数来说明覆盖率指标和仪表化过程。它用 C++ 编写,使用其他语言的类似。
void foo() { bool found=false; for (int i=0; (i<100) && (!found); ++i) { if (i==50) break; if (i==20) found=true; if (i==30) found=true; } printf("foo\n"); }
语句块覆盖率
最基本的仪表化是记录程序运行时执行的语句。然而,不需要记录每个语句的执行来获取此信息。如果几个语句形成一个序列,只需要记录最后一个语句被执行的次数,因为它们都组成一个要么整体执行要么完全不执行的块。因此,Coco 只在每块的末尾插入仪表化语句,所得的覆盖率指标称为语句块覆盖率。
以下列出中,仪表化的语句是下划线的
void foo() { bool found=false; // not instrumented for (int i=0; (i<100) && (!found); ++i) { if (i==50) break; if (i==20) found=true; if (i==30) found=true; } printf("foo\n"); // not instrumented }
可以看到并非所有行都进行了代码插装。由于它们本身不是语句,所以if
和for
语句的条件没有涵盖。有些其他语句不需要进行插装,因为它们是块的一部分。在示例中,这些语句位于第3行和第12行:它们属于从第2行开始到第13行结束的块。在第13行放置一个插装点就足够了,以验证所有这些行都已执行。
安全标准的关联性
ISO 26262强烈推荐ASIL A和B的语句覆盖率。ASIL C和D也推荐,但更严格的分支覆盖和修改后的条件/决策覆盖率被高度推荐。
决策覆盖率
一个更详细的覆盖率指标也会记录分支和循环语句(如if
、while
、for
等)中布尔条件的值。要达到完全覆盖,此类结构中的决策必须至少有一次评估为true
和至少一次评估为false
。这种代码覆盖率称为决策覆盖率或有时称为分支覆盖率。
决策覆盖率也处理switch
语句。在它们中,如果程序执行期间代码中的每个case
标签至少被到达一次,则达到完全覆盖。
决策覆盖率还包括语句的覆盖率,如同语句块覆盖率。下面列出的决策覆盖率插装的条件以灰色背景显示。插装语句如下划线所示,就像之前一样。它们与之前的列表相同。
void foo() { bool found=false; for (int i=0; (i$<$100) && (!found); ++i) { if (i==50) break; if (i==20) found=true; if (i==30) found=true; } printf("foo\n"); }
安全标准的关联性
- ISO 26262推荐ASIL A的分支覆盖率,并高度推荐该方法用于ASIL B、C和D。
- EN 50128推荐SIL 1和2的分支覆盖率。对于SIL 3和4,此级别甚至被高度推荐。
- DO 178规定,软件级别A和B应独立满足决策覆盖率。
- IEC 61508推荐SIL 1和2的分支覆盖率,并高度推荐此级别用于SIL 3和4。
条件覆盖率
要更详细地分析if
、while
、for
和类似语句中的布尔决策,请使用条件覆盖率。
在此覆盖率指标中,每个决策被分解为更简单的语句(或条件),它们通过布尔运算符(如 ~
、||
和&&
)连接。为了完全覆盖决策,在程序执行时,每个条件必须评估为true
和false
。
请注意,||
和 &&
是简写运算符,因此在复杂的决策中,并不总是执行所有条件。一个例子是代码片段 if (a || b) return 0;
,其中条件是变量 a
和 b
。要使它们都评估为 true
和 false
,必须使用 a == true
运行代码片段一次,并将 b
任意设置(因为它不进行评估),并且还需要两次使用 a == false
运行:在一次运行中,设置 b
为 true
,在另一次运行中设置为 false
。
这与决策覆盖的情况形成对比,其中只要整个决策,即 a || b
,评估为 true
和 false
,覆盖就足够了。
在条件覆盖的情况下,我们的示例函数以以下方式进行了设置。就像之前一样,设置好的布尔表达式为 灰色
void foo() { bool found=false; for (int i=0; (i$<$100) && (!found); ++i) { if (i==50) break; if (i==20) found=true; if (i==30) found=true; } printf("foo\n"); }
我们可以看到,for
语句的决定已经被分成两个分别设置了条件的部分。
分配的仪器化
CoverageScanner 还会仪器化布尔表达式分配给变量的操作。常量或静态表达式不会进行仪器化,因为它们在编译时或在程序初始化期间只计算一次,所以无论如何都无法达到完全覆盖。
在下面的列表中,仪器化的布尔表达式再次为 灰色
void foo() { static bool a = x && y; bool b = x && y; bool c = b; int d = b ? 0 : 1; }
我们可以看到,第 3 和 5 行的语句没有仪器化——第一条是在编译时进行评估的,第二条不涉及布尔运算。
布尔重载的潜在问题
由于 Coco 在语法层面上工作,它不能区分只涉及布尔运算符的表达式和具有布尔运算符支持的其他数据类型——所有这些都进行了仪器化。
仪器化尝试不对程序产生影响,但在某些情况下这并不可能。一个例子是较老的 C# 版本。它们需要用 --cs-no-csharp-dynamic
选项进行仪器化。然后仪器化的代码将布尔运算符(如 ||
和 &&
)的操作数首先转换为布尔值,然后再应用运算符。短路评估仍然有效。这与未仪器化的程序的工作方式不同。
因此,对于在 Microsoft® Visual Studio® 2010 年之前的版本中编译的 C# 程序,必须在布尔运算符的参数对象中定义转换运算符 true
和 false
。对于更新的 C# 版本,可以使用 CoverageScanner 的默认设置,仪器化的代码不会出现此问题。
多个条件覆盖
多个条件覆盖和 MC/DC 的特性只有在程序包含具有许多条件的复杂决策时才会变得明显。因此,我们的前一个示例不能再使用了。相反,我们将使用以下程序,该程序将字符串中的某些字母替换为下划线:在字母表中的 a
和 e
(包括)之间,以及句尾的句点之后。在示例文本中没有句点之后的字符,但是故意如此,因为我们需要一些失败的条件。
#include <stdio.h> int main() { char text[] = "The quick brown dog jumps over the lazy fox."; for (char *p = text; *p; p++) { if ((*p >= 'a' && *p <= 'e') || (p != text && *(p - 1) == '.')) *p = '_'; } puts(text); return 0; }
我们将关注第 8 行的复杂条件。当此程序的覆盖范围在 CoverageBrowser 中显示时,您可以将鼠标移至该行,并查看以下表格,该表格描述了覆盖数据。或者,您也可以单击该行,在 解释 窗口中查看类似的表格。
*p >= 'a' | *p <= 'e' | p != text | *(p - 1) == '.' | 描述 |
---|---|---|---|---|
TRUE | FALSE | TRUE | TRUE | 从未执行 |
FALSE | TRUE | TRUE | 从未执行 | |
TRUE | FALSE | TRUE | FALSE | 由 1 个测试执行了 27 次 |
FALSE | TRUE | FALSE | 由 1 个测试执行了 9 次 | |
TRUE | FALSE | FALSE | 从未执行 | |
FALSE | FALSE | 由 1 个测试执行了 1 次 | ||
TRUE | TRUE | 由 1 个测试执行了 7 次 |
在本表中,每一行包含一组条件结果。前四列包含单个条件的结果。由于 C 使用了简写运算符,并不是每行都显示出所有条件都已执行。已执行的行背景为绿色,未执行的行背景为红色。
从当前表中我们可以看到,有三个条件组合未执行,并且未出现在完全条件覆盖中。例如,为了执行第二行红色线中的条件组合,需要使条件 *p >= 'a'
为假,同时使条件 p != text
和 *(p - 1) == ' '
为真。要完成此操作,可以在 text
中的句号(对于其他两个条件)之后放置一个大写字母(对于第一个条件)。
对安全标准的相关性
EN 50128 推荐对于 SIL 1 和 2 使用 MCC(或修改条件/决策覆盖)。对于 SIL 3 和 4,这一级别甚至强烈推荐。
MC/DC
修改条件/决策覆盖(MC/DC)是多种条件覆盖的一种变体,需要更少的测试。其目的是确保在复杂的决策中,每个条件的两次执行只在该条件的结果上有所不同。
MC/DC的条件表是多种条件覆盖表的更复杂版本
*p >= 'a' | *p <= 'e' | p != text | *(p - 1) == '.' | 决策 | 描述 |
---|---|---|---|---|---|
TRUE | FALSE | TRUE | TRUE | TRUE | 从未执行 |
FALSE | TRUE | TRUE | TRUE | 从未执行 | |
TRUE | FALSE | TRUE | FALSE | FALSE | 由 1 个测试执行了 27 次 |
FALSE | TRUE | FALSE | FALSE | 由 1 个测试执行了 9 次 | |
FALSE | FALSE | FALSE | 由 1 个测试执行了 1 次 | ||
TRUE | TRUE | TRUE | 由 1 个测试执行了 7 次 | ||
TRUE | FALSE | FALSE | FALSE | 从未执行 |
其大规模结构与多种条件覆盖相同,但有一些新功能
- 条件和决策的列标题有彩色背景。如果条件有 绿色 背景,则表示该条件的 MC/DC 已满足,否则为 红色。
- 红色行包含程序执行时没有出现的真值组合。执行其中一个将增加代码覆盖率。
- 浅绿色行包含程序执行时确实出现的真值组合。如果它们有助于覆盖条件,则它们包含带有 深绿色 背景的字段。
要查看哪些行有助于已覆盖条件的覆盖,请在属于此条件的列中搜索深绿色字段。如果有两个带有深绿色字段的行,则只要其中一个字段带有
TRUE
条目,另一个带有FALSE
条目,这两个行就共同覆盖条件。在示例中,我们可以看到第一个条件,
*p >= 'a'
,以两种不同的方式被覆盖:通过绿色第二行和第四行一起,以及通过绿色第三行和第四行一起。第二个条件,*p <= 'e'
,由绿色第一行和第四行共同覆盖。还可以看到这两个行仅在第二列和决策中有所不同;其他列包含相同的结果或至少一个空白字段。 - 浅红色行包含没有执行的,但执行其中一个不会增加代码覆盖率。
你可能已经注意到,此表与前一个多条件覆盖率表的排序方式不同。首先是最上面的一行红色,然后是绿色的行,最后是浅红色的行。这始终如此。
与安全标准的相关性
- ISO 26262 建议使用 MC/DC(或多个条件覆盖率)对 ASIL A、B、C 进行评估,并强烈建议该方法对 ASIL D 进行评估。
- EN 50128 建议对 SIL 1 和 2 使用 MC/DC(或多个条件覆盖率),对于 SIL 3 和 4,该级别甚至被强烈推荐。
- DO 178 规定,对于软件级别 A,MC/DC 应满足独立性条件。
- IEC 61508 建议对 SIL 1、2 和 3 使用 MC/DC,并强烈建议对 SIL 4 使用此级。
结果展示
CoverageBrowser 是一个图形用户界面程序,用于显示仪表化的分析结果。它使用颜色编码来表示语句的状态。在本手册中,我们使用与程序相同的颜色。
在仪表化的程序中,有两种语句。其中一些包含仪表化点,即 Coco 插入的代码片段,当执行时会增加计数器。如果一行包含仪表化点,由 CoverageBrowser 显示在深色背景下,并在 HTML 报告中显示。
出于效率的考虑,并非所有语句都包含仪表化点。如果一行不包含仪表化点,但其覆盖率状态可以从其他语句中推断,则它将以浅色背景显示。
因此形成的颜色方案如下
- 浅绿色 深绿色 和 浅绿色 的行已被执行。
- 深红色 深红色 和 浅红色 的行未被执行。
- 橙色 橙色 的行包含部分执行的布尔表达式。这些表达式通常出现在
while
和if
这样的控制结构中,或者作为布尔变量的赋值右侧。布尔表达式始终进行仪表化,因此不需要浅橙色的背景。
我们示例函数的输出说明了这一点
void foo() { bool found=false; for (int i=0; (i < 100) && (!found); ++i) { if (i==50) break; if (i==20) found=true; if (i==30) found=true; } printf("foo\textbackslash n"); }
第 4 行的 for
语句以橙色显示,因为它包含 i<100
表达式,这个表达式只部分被执行:当函数执行时,它始终返回 true
。表达式 i==50
和 i==30
也只部分被执行。从 break;
和 found=true;
语句的颜色可以判定,在两种情况下条件都评估为 false
。在 CoverageBrowser 中,还以悬停提示的形式显示了条件的值。
第 3、5 和 13 行没有直接包含在覆盖率测量中,因此以浅色背景显示。它们的覆盖率状态由 Coco 从随后(或未)执行的其它语句推断。在我们的示例中,第 3 和 13 行必须已执行,因为第 14 行的闭合括号已执行,第 3 行由于第 12 行而执行。因此,所有浅色背景的行都显示为绿色。
性能
在仪表化过程中插入代码会增加代码大小,并影响仪表化应用程序的性能。它将使用更多内存,运行速度较慢。
对于非条件表达式,代码插入点仅向固定内存位置的计数器写入指令。然而,对于条件表达式,需要更详细的分析,这会消耗更多的计算资源。
总的来说,插入了代码的应用程序将比原来的大60%至90%,运行速度也会慢10%至30%。
注意:详细的测量结果请参阅代码覆盖率基准。
统计数据
一些开发者仅仅通过特定的编码风格(例如,将大括号放在“if”语句的同一行,而不是开发者自己的单独一行)就写了更多的代码行。默认情况下,Coco使用的覆盖率度量不容易受到这种编码风格差异的影响。它的计算是基于已执行指令数目与仪器指令总数的比较。
每个代码插入点(如《code translate="no">return》、《code translate="no">break、函数的最后一行指令等)都由单个仪器计数器记录。一个完全插入的《code translate="no">IF...THEN...ENDIF块中的条件有2个计量器:一个用于真值情况,一个用于假值情况。如果代码只是部分插入的,只记录一个条件(要么是假值情况,要么是真值情况)。
这些统计数据本身取决于仪器类型。通常不可能将语句块级别的代码覆盖率与决策级别的代码覆盖率进行比较:在决策级别达到80%的覆盖率并不能告诉我们语句块级别的覆盖率,它可能更大或更小。我们只能确定,在条件覆盖率方面,达到100%的覆盖率比在决策或语句块覆盖率方面更具挑战性。
在我们示例代码中,覆盖率在60%到75%之间。以下表格展示了每种插入类型的详细情况。
代码覆盖率类型 | 仪器化 | 已执行 | 覆盖率 |
---|---|---|---|
语句块(见语句块覆盖率示例) | 5 | 3 | 60% |
决策完整(见完整决策覆盖率示例) | 13 | 9 | 69% |
决策部分(见部分决策覆盖率示例) | 9 | 7 | 77% |
条件完整(见完整条件覆盖率示例) | 15 | 10 | 66% |
条件部分(见部分条件覆盖率示例) | 12 | 9 | 75% |
以下示例中,计算的详细情况用下标表示。下标中的第一个数字显示已执行的仪器化语句数量;第二个数字是仪器化语句总数。
语句块覆盖率示例
void foo() { bool found=false; for (int i=0; (i < 100) && (!found); ++i) { if (i==50) break;0/1 [not executed] if (i==20) found=true;1/1 [executed] if (i==30) found=true;0/1 [not executed] }1/1 [executed] printf("foo\textbackslash n"); }1/1 [executed]
完整决策覆盖率示例
void foo() { bool found=false; for (int i=0; (i < 100) && (!found)2/2 [was false and true]; ++i) { if (i==50)1/2 [was false but not true] break;0/1 [not executed] if (i==20)2/2 [was false and true] found=true;1/1 [executed] if (i==30)1/2 [was false but not true] found=true;0/1 [not executed] }1/1 [executed] printf("foo\textbackslash n"); }1/1 [executed]
部分决策覆盖率示例
void foo() { bool found=false; for (int i=0; (i < 100) && (!found)1/1 [was false]; ++i) { if (i==50)1/1 [was false] break;0/1 [not executed] if (i==20)1/1 [was false] found=true;1/1 [executed] if (i==30)1/1 [was false] found=true;0/1 [not executed] }1/1 [executed] printf("foo\textbackslash n"); }1/1 [executed]
完整条件覆盖率示例
void foo() { bool found=false; for (int i=0; (i < 100)1/2 [was true but not false] && (!found)2/2 [was false and true]; ++i) { if (i==50)1/2 [was false but not true] break;0/1 [not executed] if (i==20)2/2 [was false and true] found=true;1/1 [executed] if (i==30)1/2 [was false but not true] found=true;0/1 [not executed] }1/1 [executed] printf("foo\textbackslash n"); }1/1 [executed]
部分条件覆盖率示例
void foo() { bool found=false; for (int i=0; (i < 100)1/2 [was true but not false] && (!found)2/2 [was false and true]; ++i) { if (i==50)1/1 [was false] break;0/1 [not executed] if (i==20)1/1 [was false] found=true;1/1 [executed] if (i==30)1/1 [was false] found=true;0/1 [not executed] }1/1 [executed] printf("foo\textbackslash n"); }1/1 [executed]
行覆盖率有问题
行覆盖率是一个自然指标,可以显示出哪些代码行被执行,但它比语句块级别的代码插入精度低,并且其结果依赖于开发者的编码风格。
以下例子说明了这个问题
int main() { if (true) return 1; foo(); return 0; }
执行它会产生以下结果
int main() { if (true) return 1; // Executed foo(); // Not executed return 0; // Not executed }
这次执行对应于33%的覆盖率。
由于《code translate="no">main的第一个代码行包含两个执行语句,将其分为两部分会增加执行的行数,从而增加测试覆盖率。因此,如果我们按照如下方式重新格式化主函数
int main() { if (true) return 1; foo(); return 0; }
则此代码的执行结果为66%的覆盖率。
int main() { if (true) // Executed return 1; // Executed foo(); // Not executed return 0; // Not executed }
通过重新格式化来增加覆盖率的其他方法是将一个未覆盖的语句隐藏在已执行的语句后面。要做到这一点,只需要将整个main函数的代码写在一行中
int main() { if (true) return 1; foo(); return 0; }
此代码具有100%的行覆盖率
int main()
{
if (true) return 1; foo(); return 0; // Executed
}
此小型示例演示了结果如何取决于源代码格式。因此,Coco提供了行覆盖率作为额外的测量指标,以及判定和条件覆盖率,并且不允许仅在行级别对源代码进行插装。
Coco v7.2.0©2024Qt公司有限公司。
Qt及其相关商标是芬兰及/或其他国家Qt公司的商标。所有其他商标均为各自所有者的财产。