问题背景
网上有许多介绍如何使用一种白盒测试工具的教程,但是很少有教人自己开发一个白盒测试工具的教程。因此我做了一次这样的实验,根据Python语言的特性,开发一个Python代码的白盒测试工具,实现以下功能:记录程序执行路径、获取函数调用关系、计算代码覆盖率、语句覆盖率、分支覆盖率(判定覆盖率)、字节码覆盖率。其中字节码覆盖率是我自己造的一个词,本想用于实现计算条件覆盖率的,奈何没有成功。
实验环境
编程语言:Python
编程环境:Linux16.04 + Anaconda2 + Python2.7
完整代码可以到我的GitHub上查看:https://github.com/Jancy1072/pycoverage
原理
1、Python是基于虚拟机的语言,首先是通过编译器编译成字节码文件,然后在运行时通过解释器解释成机器文件。Python是一种先编译后解释的语言。
我们在硬盘上看到的.pyc文件就是字节码文件,它来源于:当python程序运行时,编译的结果保存在位于内存中的PyCodeObject中,当Python程序运行结束时,Python解释器则将PyCodeObject写回到.pyc文件中。当python程序第二次运行时,程序首先会在硬盘中寻找.pyc文件,如果找到,则直接载入,否则就重复上面的过程。在载入之前还会先检查一下.py文件和.pyc文件保存的最后修改日期,如果不一致则重新生成一份.pyc文件。所以.pyc文件其实是PyCodeObject的一种持久化保存方式。把.py文件编译成.pyc文件,最大的优点在于运行程序时不需要重新对该模块进行解释。
因此,利用.pyc文件,我们可以获得许多程序运行的信息。
2、.pyc文件解析
(1).pyc文件一般由3个部分组成:
- 最开始4个字节是Maigc int,标识此.pyc的版本信息,不同版本的Magic都在Python/import.c内定义。
- 接下来四个字节是.pyc产生的时间(TIMESTAMP,1970.01.01到产生.pyc时候的秒数),int型。
- 接下来是序列化了的PyCodeObject(此结构在Include/code.h内定义),序列化方法在Python/marshal.c内定义。
(2)PyCodeObject的类型定义:
(3)需要注意的几个关键点:
- 在Python代码中,每个作用域对应一个PyCodeObject对象,所以会出现嵌套,需要递归处理。
- 行号对照表co_lnotab可以看做是顺次串成的的字节数组(字符串),它是字节码在 co_opcode中的index增量,对应的源码的行号增量。
- co_code是Python opcode组成的序列,具体有哪些opcode可以参看Include/opcode.h,有些opcode有参数,有些没有参数:大于等于90的opcode有参数,参数占2个字节。
3、Python钩子函数
(1)sys.settrace(my_trace_function)是Python的系统模块提供的挂载函数。我们可以自定义代码追踪的钩子函数,挂载到虚拟机上,当程序运行时,钩子函数能够监听触发的事件并处理。执行每一行代码,都会触发事件,调用钩子函数。
(2)my_trace_function(frame, event, arg)是我们自定义的钩子函数。能够监听call、line、return、exception、opcode事件。而frame参数有f_code、f_lineno、f_back 等属性,我们可以获取程序执行时虚拟机的内部数据。
实现
1、实现钩子函数
- 记录程序执行路径:line事件触发时,保存当前行号和对应的代码语句。
- 函数调用
call事件触发时,保存被调用的函数名、调用者。
line事件触发时,词法分析,保存定义的函数名。 - 语句覆盖
line 事件触发时,保存当前行号和对应的代码语句。 - 分支覆盖
line 事件触发时,词法分析,记录前一个语句和当前的语句,构成(pre, current)这样的行号对。
2、解析.pyc文件
- 读取.pyc文件,得到PyCodeObject对象。
- 递归统计字节码的信息。
- 利用行号表,得到总行数、行号。
- 词法分析得到总的分支数。
3、字节码覆盖
(1)利用co_code属性统计程序中所有字节码的信息。
(2)行号表记录的是每一行的字节码的偏移量。每执行一行代码,都会触发一次line事件。
(3)修改行号表,重新构建一个.pyc文件,使得每执行一个字节码触发一次 line事件。
(4)自定义钩子函数,在line事件触发时统计实际执行的字节码。
(5)实现字节码覆盖是实现条件覆盖的基础。
实验结果
1、本工具包含trace.py
、general_analyse.py
、bytes_analyse.py
、opcode.py
四个源程序文件。
(1) trace.py中定义了钩子函数,能够记录程序运行执行路径,保存了可以用于计算语句覆盖和分支覆盖的中间结果。
(2) general_analyse.py中检查了函数调用关系,计算了语句覆盖率、分支覆盖率。
(3) bytes_analyse.py中统计了可用字节码的总数和实际执行的字节码数量,统计未执行的字节码,计算字节码覆盖率。并且综合所有类型的覆盖率情况,计算总体覆盖率。
(4) opcode.py中定义了操作码和操作名的映射关系。
2、待测试代码文件case1.py
测试代码中包含简单的函数调用关系、顺序结构、条件结构、循环结构。
3、运行trace.py
,输入参数case1.py
得到程序执行路径,结果保存在trace_result.txt文件中。生成coverage.txt文件,保存函数调用关系、语句执行情况、分支情况。
4、运行general_analyse.py程序。
得到函数调用关系(树形)、语句覆盖率、分支覆盖率。结果保存在analyse_result.txt文件中。
5、运行bytes_analyse.py程序。
得到字节码覆盖率、未执行的字节码。总体覆盖率保存在analyse_result.txt文件中。
总结
1、优点
深入理解了Python语言的编译解释机制和语言特性,实现了白盒测试工具的基础功能,能够分析程序常见结构流如顺序、选择与循环,记录程序执行路径,检测函数调用关系,计算语句覆盖率、分支覆盖率。而且从更小的粒度来计算覆盖率,实现了字节码覆盖,这是计算条件覆盖率的基础。
2、缺点
在计算字节码覆盖率时修改了.pyc文件,使得原有的行号等信息丢失。未能实现条件覆盖率的计算。
来源:CSDN
作者:Jancy1072
链接:https://blog.csdn.net/shelly1072/article/details/86505487