本文共 4886 字,大约阅读时间需要 16 分钟。
上一篇博客 《》介绍了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来完成获取覆盖数据的操作, 中有详细的介绍,这里就不再赘述!这里需要注意:这些命令不只是用于代码覆盖,而是性能Profiling的工具。
这里我们用到了 -coverage命令,它负责instrument我们前面编写的代码,然后使用.NET的 在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的划分情况。
转载地址:http://awgki.baihongyu.com/