您的位置:首页 > 编程语言

代码覆盖从简到繁 (三) – 划分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的划分情况。

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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: