代码覆盖从简到繁 (三) – 划分Block
2011-06-06 13:34
162 查看
上一篇博客 《代码覆盖从简到繁 (二) – Block Coverage》介绍了Visual Studio所采用的Block覆盖中Block是如何定义的,并且展示了代码行与Block之间其实并不是严格对应的。本篇博客将通过.NET中间语言(IL)进一步分析Visual Studio是如何划分Block的,从而更准确回答代码行与Block不能严格对应的原因。
使用Visual Studio获取code coverage数据是非常简单的,只需要在配置中选择“Code Coverage”选项,然后执行测试用例就可以了,覆盖数据会直接在"Code Coverage Results”窗口中呈现出来,这些在《代码覆盖从简到繁 (一) 》中都有介绍。其实要获取覆盖数据,首先要对被测试的.exe或者.dll进行instrument,所谓instrument实际上就是向文件注入特定的用于收集覆盖数据的代码;然后,启动覆盖数据的监听服务,刚才注入代码会在被指定到时项监听程序发出报告;接下来就是要执行你的测试用例(可以是自动或者手动测试用例);停止监听服务,生成代码覆盖报告。为了易于使用,Visual Studio自动为执行了上述很多工作。除了Visual Studio IDE, 还可以通过命令行工具 VsInstr.exe,VsPerfmon和VsPerfCmd来完成获取覆盖数据的操作,Code Coverage Basics with Visual Studio Team System 中有详细的介绍,这里就不再赘述!这里需要注意:这些命令不只是用于代码覆盖,而是性能Profiling的工具。
这里我们用到了VsInstr.exe -coverage命令,它负责instrument我们前面编写的代码,然后使用.NET的 Ildasm.exe 在IL层观察上一篇博客中使用的GetInteger()函数是如何被划分block的,下面就是Instrument之后的GetInteger()函数的IL代码(这里使用的Visual Studio 2010带的C#编译器,编译器不同产生的代码也会不同):
.method public hidebysig instance int32 GetInteger(int32 arg1, int32 arg2) cil managed
{
// Code size 204 (0xcc)
.maxstack 3
.locals init ([0] int32 CS$1$0000, [1] bool CS$4$0001)
IL_0000: call void Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::Register()
IL_0005: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_000a: ldc.i4 0x5
IL_000f: ldelem.i8
IL_0010: ldc.i8 0x1
IL_0019: add
IL_001a: conv.i
IL_001b: ldc.i4.1
IL_001c: stind.i1
IL_001d: nop 判断 arg1 > 0
IL_001e: ldarg.1
IL_001f: ldc.i4.0
IL_0020: ble.s IL_0043 如果 arg1 <= 0,跳转到0043处。
IL_0022: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_0027: ldc.i4 0x5
IL_002c: ldelem.i8
IL_002d: ldc.i8 0x2
IL_0036: add
IL_0037: conv.i
IL_0038: ldc.i4.1
IL_0039: stind.i1
IL_003a: ldarg.2 判断 arg2 < 0
IL_003b: ldc.i4.0
IL_003c: clt
IL_003e: ldc.i4.0
IL_003f: ceq 如果 arg2 < 0, 向求值栈(evaluation stack)加载 0;否则为1;
IL_0041: br.s IL_005c
IL_0043: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_0048: ldc.i4 0x5
IL_004d: ldelem.i8
IL_004e: ldc.i8 0x3
IL_0057: add
IL_0058: conv.i
IL_0059: ldc.i4.1
IL_005a: stind.i1
IL_005b: ldc.i4.1 (arg1 <= 0时)向求值栈(evaluation stack)加载 1
IL_005c: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_0061: ldc.i4 0x5
IL_0066: ldelem.i8
IL_0067: ldc.i8 0x4
IL_0070: add
IL_0071: conv.i
IL_0072: ldc.i4.1
IL_0073: stind.i1
IL_0074: stloc.1 判断 arg1 > 0 && arg2 < 0 最终结果
IL_0075: ldloc.1
IL_0076: brtrue.s IL_0095
IL_0078: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_007d: ldc.i4 0x5
IL_0082: ldelem.i8
IL_0083: ldc.i8 0x5
IL_008c: add
IL_008d: conv.i
IL_008e: ldc.i4.1
IL_008f: stind.i1
IL_0090: nop 准备return 0
IL_0091: ldc.i4.0
IL_0092: stloc.0
IL_0093: br.s IL_00b2
IL_0095: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_009a: ldc.i4 0x5
IL_009f: ldelem.i8
IL_00a0: ldc.i8 0x6
IL_00a9: add
IL_00aa: conv.i
IL_00ab: ldc.i4.1
IL_00ac: stind.i1
IL_00ad: nop 准备return 1
IL_00ae: ldc.i4.1
IL_00af: stloc.0
IL_00b0: br.s IL_00b2
IL_00b2: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_00b7: ldc.i4 0x5
IL_00bc: ldelem.i8
IL_00bd: ldc.i8 0x7
IL_00c6: add
IL_00c7: conv.i
IL_00c8: ldc.i4.1
IL_00c9: stind.i1
IL_00ca: ldloc.0
IL_00cb: ret
} // end of method Program::GetInteger
与没有instrument过的IL代码相比,被instrument的代码是多出了上面用灰色标识的部分,它们就是真正用来标记哪些代码被执行的。仔细数数正好是 7 段,每一段标识了一个block划分的开始,数组的索引值(例如:IL_0010: ldc.i8 0x1 )给每个block从 1 到 7 进行了编号。当这些标识block代码被执行,则代表它们所标识真正被测试代码一定被执行到,代码覆盖收集的监听程序,会时刻监听和收集这些标记代码的执行情况,并由此生成最终的覆盖报告。再对照上篇博客多提到的block的定义 - a single entry point, a single exit point, and a set of instructions that are all run in sequence - 仔细检查一下,确实是这样每一个block都是只有一个唯一入口和一个为出口,block标记到大都是加载在br.s、brtrue.s、ble.s等分支跳转语句前面。
对于GetInteger()而言,最有意思就要数 if( arg1 >0 && arg2 < 0 ),别看只有一行代码,但由于条件与操作&&的存在,在IL级这一行代码时间上是被划分4个block的,如上面的粗体代码所示,这些代码并不是很难理解。这里出个小问题:对于GetInteger()函数,测试用例(arg1 =1, arg2 = -1),能够对 if( arg1 >0 && arg2 < 0 ) 行进行完全覆盖吗?答案:不能,因它漏掉了仅有一条IL指令(IL_005b: ldc.i4.1)的哪个block,随意仍是部分覆盖。要想达到对该if行的完全覆盖,最少需要两个用例, 例如:(arg1 = -1, arg1 =-1)和 (arg1 =1, arg2 = -1)。
最后需要提示一下:Reflector工具可以将IL代码反编译为C#等语言代码,这样阅读起来会更方便一些,但是有一些instrument过的IL,Reflector反编译的结果可能会丢失一些block划分信息。例如:GetInteger()的发编译结果如下。其中,Block#2和#3并没有显示在代码中体现出来,所以在有些情况下,直接阅读IL代码能更准确把握block的划分情况。
使用Visual Studio获取code coverage数据是非常简单的,只需要在配置中选择“Code Coverage”选项,然后执行测试用例就可以了,覆盖数据会直接在"Code Coverage Results”窗口中呈现出来,这些在《代码覆盖从简到繁 (一) 》中都有介绍。其实要获取覆盖数据,首先要对被测试的.exe或者.dll进行instrument,所谓instrument实际上就是向文件注入特定的用于收集覆盖数据的代码;然后,启动覆盖数据的监听服务,刚才注入代码会在被指定到时项监听程序发出报告;接下来就是要执行你的测试用例(可以是自动或者手动测试用例);停止监听服务,生成代码覆盖报告。为了易于使用,Visual Studio自动为执行了上述很多工作。除了Visual Studio IDE, 还可以通过命令行工具 VsInstr.exe,VsPerfmon和VsPerfCmd来完成获取覆盖数据的操作,Code Coverage Basics with Visual Studio Team System 中有详细的介绍,这里就不再赘述!这里需要注意:这些命令不只是用于代码覆盖,而是性能Profiling的工具。
这里我们用到了VsInstr.exe -coverage命令,它负责instrument我们前面编写的代码,然后使用.NET的 Ildasm.exe 在IL层观察上一篇博客中使用的GetInteger()函数是如何被划分block的,下面就是Instrument之后的GetInteger()函数的IL代码(这里使用的Visual Studio 2010带的C#编译器,编译器不同产生的代码也会不同):
.method public hidebysig instance int32 GetInteger(int32 arg1, int32 arg2) cil managed
{
// Code size 204 (0xcc)
.maxstack 3
.locals init ([0] int32 CS$1$0000, [1] bool CS$4$0001)
IL_0000: call void Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::Register()
IL_0005: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_000a: ldc.i4 0x5
IL_000f: ldelem.i8
IL_0010: ldc.i8 0x1
IL_0019: add
IL_001a: conv.i
IL_001b: ldc.i4.1
IL_001c: stind.i1
IL_001d: nop 判断 arg1 > 0
IL_001e: ldarg.1
IL_001f: ldc.i4.0
IL_0020: ble.s IL_0043 如果 arg1 <= 0,跳转到0043处。
IL_0022: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_0027: ldc.i4 0x5
IL_002c: ldelem.i8
IL_002d: ldc.i8 0x2
IL_0036: add
IL_0037: conv.i
IL_0038: ldc.i4.1
IL_0039: stind.i1
IL_003a: ldarg.2 判断 arg2 < 0
IL_003b: ldc.i4.0
IL_003c: clt
IL_003e: ldc.i4.0
IL_003f: ceq 如果 arg2 < 0, 向求值栈(evaluation stack)加载 0;否则为1;
IL_0041: br.s IL_005c
IL_0043: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_0048: ldc.i4 0x5
IL_004d: ldelem.i8
IL_004e: ldc.i8 0x3
IL_0057: add
IL_0058: conv.i
IL_0059: ldc.i4.1
IL_005a: stind.i1
IL_005b: ldc.i4.1 (arg1 <= 0时)向求值栈(evaluation stack)加载 1
IL_005c: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_0061: ldc.i4 0x5
IL_0066: ldelem.i8
IL_0067: ldc.i8 0x4
IL_0070: add
IL_0071: conv.i
IL_0072: ldc.i4.1
IL_0073: stind.i1
IL_0074: stloc.1 判断 arg1 > 0 && arg2 < 0 最终结果
IL_0075: ldloc.1
IL_0076: brtrue.s IL_0095
IL_0078: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_007d: ldc.i4 0x5
IL_0082: ldelem.i8
IL_0083: ldc.i8 0x5
IL_008c: add
IL_008d: conv.i
IL_008e: ldc.i4.1
IL_008f: stind.i1
IL_0090: nop 准备return 0
IL_0091: ldc.i4.0
IL_0092: stloc.0
IL_0093: br.s IL_00b2
IL_0095: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_009a: ldc.i4 0x5
IL_009f: ldelem.i8
IL_00a0: ldc.i8 0x6
IL_00a9: add
IL_00aa: conv.i
IL_00ab: ldc.i4.1
IL_00ac: stind.i1
IL_00ad: nop 准备return 1
IL_00ae: ldc.i4.1
IL_00af: stloc.0
IL_00b0: br.s IL_00b2
IL_00b2: ldsfld uint64[] Microsoft.VisualStudio.Coverage.Init_bbf9568946f2545aaa9b589093700f85::m_vscov
IL_00b7: ldc.i4 0x5
IL_00bc: ldelem.i8
IL_00bd: ldc.i8 0x7
IL_00c6: add
IL_00c7: conv.i
IL_00c8: ldc.i4.1
IL_00c9: stind.i1
IL_00ca: ldloc.0
IL_00cb: ret
} // end of method Program::GetInteger
与没有instrument过的IL代码相比,被instrument的代码是多出了上面用灰色标识的部分,它们就是真正用来标记哪些代码被执行的。仔细数数正好是 7 段,每一段标识了一个block划分的开始,数组的索引值(例如:IL_0010: ldc.i8 0x1 )给每个block从 1 到 7 进行了编号。当这些标识block代码被执行,则代表它们所标识真正被测试代码一定被执行到,代码覆盖收集的监听程序,会时刻监听和收集这些标记代码的执行情况,并由此生成最终的覆盖报告。再对照上篇博客多提到的block的定义 - a single entry point, a single exit point, and a set of instructions that are all run in sequence - 仔细检查一下,确实是这样每一个block都是只有一个唯一入口和一个为出口,block标记到大都是加载在br.s、brtrue.s、ble.s等分支跳转语句前面。
对于GetInteger()而言,最有意思就要数 if( arg1 >0 && arg2 < 0 ),别看只有一行代码,但由于条件与操作&&的存在,在IL级这一行代码时间上是被划分4个block的,如上面的粗体代码所示,这些代码并不是很难理解。这里出个小问题:对于GetInteger()函数,测试用例(arg1 =1, arg2 = -1),能够对 if( arg1 >0 && arg2 < 0 ) 行进行完全覆盖吗?答案:不能,因它漏掉了仅有一条IL指令(IL_005b: ldc.i4.1)的哪个block,随意仍是部分覆盖。要想达到对该if行的完全覆盖,最少需要两个用例, 例如:(arg1 = -1, arg1 =-1)和 (arg1 =1, arg2 = -1)。
最后需要提示一下:Reflector工具可以将IL代码反编译为C#等语言代码,这样阅读起来会更方便一些,但是有一些instrument过的IL,Reflector反编译的结果可能会丢失一些block划分信息。例如:GetInteger()的发编译结果如下。其中,Block#2和#3并没有显示在代码中体现出来,所以在有些情况下,直接阅读IL代码能更准确把握block的划分情况。
public int GetInteger(int arg1, int arg2) { int num; Init_bbf9568946f2545aaa9b589093700f85.Register(); Init_bbf9568946f2545aaa9b589093700f85.m_vscov[5][(int) ((ulong) 1L)] = 1; Init_bbf9568946f2545aaa9b589093700f85.m_vscov[5][(int) ((ulong) 4L)] = 1; if ((arg1 > 0) && (arg2 < 0)) { Init_bbf9568946f2545aaa9b589093700f85.m_vscov[5][(int) ((ulong) 5L)] = 1; num = 0; } else { Init_bbf9568946f2545aaa9b589093700f85.m_vscov[5][(int) ((ulong) 6L)] = 1; num = 1; } Init_bbf9568946f2545aaa9b589093700f85.m_vscov[5][(int) ((ulong) 7L)] = 1; return num; }
相关文章推荐
- magento在自己的模块 覆盖核心代码的block、helper、model
- vc++6.0编辑窗口怎么才能插入而不覆盖后面的代码
- 代码块(Block)回调一般阐述
- 转:『Sklearn』数据划分方法及python代码
- 使用Intel编译器(5)PGO(5)PGO工具之代码覆盖工具(code coverage)2
- 算法:集合的划分原理及代码实现
- 代码覆盖的15种典型情景
- 实用算法的分析与程序设计——递归法(实例,代码)(划分问题、0-1背包问题)
- python2.7进行爬虫百度POI代码(划分小网格算法)
- 自定义右键属性覆盖浏览器默认右键行为实现代码
- Block (存放代码的块)
- block(代码块)的介绍以及使用方法和变量之间的关系
- 一段代码既说明printf的入栈顺序是从右想做的,又说明inet_ntoa返回值是放在一个静态区域的,连续执行会覆盖
- unity 代码有调整,重新导出 iOS 最烦的就是 覆盖导出后项目不能打开
- Objective-C语法之代码块(block)的使用
- 《COM技术内幕》代码之 组件与客户程序的划分
- 追求代码质量: 不要被覆盖报告所迷惑
- GCD系列:代码块(dispatch_block)
- list添加集合被覆盖,利用map求和——代码应该怎么放
- C/C++代码覆盖工具gcov与lcov入门