本项目为编写一套 代码MCDC覆盖测试及分析系统,支持Java,Python以及C语言,会对待测代码依次进行 解析代码→生成决策树→代码插桩→生成测试用例→测试用例参数化→执行测试用例→输出MCDC覆盖率结果的操作
-
将项目克隆到本地
-
配置pom,从Maven仓库安装所需依赖
-
进入
generator
包中的MCDCAnalyzer
类 -
配置待测试代码的路径和代码语言(共支持
Java
,python
,C语言
) -
example包中已经配置好了部分测试用例,可以直接使用,本项目对于java代码的MCDC覆盖率计算支持比较好
-
运行
MCDCAnalyzer
类,会依次进行:解析代码→生成决策树→代码插桩→生成测试用例→测试用例参数化→执行测试用例→输出MCDC覆盖率结果 -
插桩后的代码会在原文件名后加
_deal
保存在example包下,编译后的可执行文件也在这个
graph TD
A[主要功能模块] --> B[文件读写]
B --> B1[读取文件]
B --> B2[写入文件]
A --> C[代码解析]
C --> C1[java解析]
C --> C2[python解析]
C --> C3[C语言解析]
A --> D[决策树生成]
D --> D1[嵌套if类型]
D --> D2[条件表达式类型]
A --> E[代码插桩]
A --> F[测试用例生成]
A --> G[测试用例参数化]
A --> H[代码执行]
A --> I[MCDC覆盖率计算]
决策树生产,代码插桩,测试用例生成与参数化,代码执行,MCDC覆盖率计算为模块核心功能,均支持Java
,python
和C语言
代码的处理。
- InputOutPut
- CodeReader
- CodeWriter
文件解析模块如下:
其中:
CodeReader | 读取指定文件路径的文件内容,并返回文件内容的字符串形式。需要提供参数filePath 文件的路径,可以是相对路径或绝对路径;return 将会返回文件的全部内容,以字符串的形式呈现。 |
CodeWriter | 将指定的内容写入到指定路径的文件中。此方法会创建新文件或覆盖已存在的文件。如果目标文件所在的目录不存在,将抛出 IOException。需要提供参数filePath 文件的路径,路径可以是相对路径或绝对路径,如果路径是相对路径,则相对于当前工作目录。还需要提供参数content 要写入文件的内容,该内容将被转换为字节序列并写入文件。 |
graph TD
A[代码解析模块] --> B[Java解析模块]
A --> C[python解析模块]
A --> D[C语言解析模块]
本项目支持Java,python和C语言的代码解析。
- 解析代码:使用
StaticJavaParser
解析给定的 Java 代码字符串,生成CompilationUnit
对象。 - 添加 import 语句:在
CompilationUnit
对象中添加com.example.CodeInstrumentor
的 import 语句。 - 查找 main 方法:在所有类中查找包含
main
方法的类,并找到main
方法。 - 添加打印语句:在
main
方法的最后添加System.out.println(com.example.CodeInstrumentor.getCoverageDataString());
语句。 - 访问 If 语句:使用
VoidVisitorAdapter
访问所有的IfStmt
,并为每个条件表达式插入日志代码。 - 处理 BinaryExpr:对于逻辑运算符
AND
和OR
的表达式,递归处理其子表达式,并将每个表达式添加到决策树中。 - 修改类名:将所有类的名称后缀加上
_deal
。 - 写入文件:将修改后的代码写入新的文件。
- 调用外部Python脚本:通过
ProcessBuilder
调用外部Python脚本parse_python.py
。 - 传递代码给Python脚本:通过进程的输入流将Python代码传递给Python脚本。
- 等待Python脚本执行完成:使用
process.waitFor()
等待Python脚本执行完成。 - 读取Python脚本输出:通过进程的输出流读取Python脚本的输出,该输出是一个JSON字符串,表示决策树。
- 解析JSON输出:将读取到的JSON字符串解析为
JSONObject
。 - 生成决策树:调用
buildDecisionTree
方法将JSONObject
构建为决策树。 - 进行代码插桩:调用
insertPythonInstrumentationCode
方法插入代码插桩。 - 写入文件:将插桩后的代码写入新的文件。
- 解析代码:使用 ANTLR 解析给定的 C 代码字符串,生成
ParseTree
对象。 - 生成决策树:定义并使用
CLanguageBaseListener
遍历ParseTree
,在遇到if
语句时,生成相应的决策节点并添加到决策树中。 - 代码插桩:调用
insertCInstrumentationCode
方法进行代码插桩。 - 写入文件:将插桩后的代码写入新的文件。
本项目的决策树生成模块支持生成简单的if语句
,复杂嵌套的if语句
以及条件表达式
,处理逻辑如下:
graph TD
A[初始化] --> B[创建根节点并将其压入栈中]
B --> C[解析代码]
C --> D{遇到 if 语句?}
D -->|是| E[获取当前栈顶节点作为当前父节点]
E --> F[创建新的决策节点表示 if 条件表达式]
F --> G[将新节点添加为当前父节点的子节点]
G --> H[将新节点入栈并更新当前上下文]
H --> I[处理 if 语句内部]
I --> J{遇到嵌套的 if 语句?}
J -->|是| E
J -->|否| K[处理完当前 if 语句后]
K --> L[从栈中弹出节点并恢复到之前的上下文]
L --> M{遇到新的条件表达式?}
M -->|是| N{复合条件表达式?}
N -->|是| O[递归处理左右子表达式]
O --> P[创建表达式节点并构建其子节点]
N -->|否| Q[创建节点并添加到当前父节点]
M -->|否| R[继续解析后续代码]
R --> D
栈(Stack
)在处理嵌套结构时非常有用。对于嵌套的 if
语句,栈可以帮助我们跟踪当前的上下文节点,以便在离开嵌套时能正确地返回到之前的节点。
- 压入栈:当进入一个新的
if
语句时,创建一个新的节点并将其压入栈中。 - 弹出栈:当处理完当前的
if
语句时,从栈中弹出节点,恢复到之前的上下文。
在解析代码时,遇到 if
语句时,需要将其条件表达式添加到当前节点的子节点中,并递归处理其子节点。通过使用栈,可以方便地管理当前的上下文节点,确保在处理嵌套结构时能够正确地维护树形结构。
- 进入
if
语句:- 当解析器遇到
if
语句时,会创建一个新的决策节点。 - 将新节点添加为当前节点的子节点。
- 将新节点压入栈中,更新当前上下文。
- 当解析器遇到
- 离开
if
语句:- 在处理完当前的
if
语句后,从栈中弹出节点。 - 恢复到之前的上下文节点,继续处理后续的代码。
- 在处理完当前的
条件表达式可以是简单的单一条件,也可以是复合条件(例如 &&
和 ||
)。处理条件表达式时,需要将每个条件表达式解析为相应的决策节点,并构建其树形结构。
- 简单条件表达式:
- 对于简单的条件表达式,直接创建一个决策节点,并将其添加到当前节点的子节点中。
- 复合条件表达式:
- 对于复合条件表达式(例如
&&
和||
),需要递归处理每个子表达式。 - 创建一个新的表达式节点,将其添加到当前节点的子节点中。
- 递归处理左、右子表达式,将每个子表达式解析为决策节点,并添加到表达式节点中。
- 对于复合条件表达式(例如
代码插桩模块流程图如下:
graph TD
A[解析并生成 AST] --> B[查找并处理 main 方法]
B --> C[在 main 方法中插入打印覆盖数据的语句]
C --> D[遍历并处理 if 语句]
D --> E[使用 VoidVisitorAdapter 遍历 AST]
E --> F[插入日志记录代码]
F --> G[处理复合条件表达式]
G --> H[递归处理左右子表达式]
H --> I[修改类名]
I --> J[将结果写入新的文件]
主要步骤:
- 解析并生成 AST:使用
JavaParser
解析 Java 代码,并生成抽象语法树。 - 查找并处理
main
方法:在main
方法中插入打印覆盖数据的语句。 - 遍历并处理
if
语句:使用VoidVisitorAdapter
遍历 AST 中的所有if
语句,并插入日志记录代码。 - 处理复合条件表达式:递归处理复合条件表达式,确保所有条件表达式都记录日志。
- 修改类名并写入文件:修改类名以区分原始代码和插桩后的代码,并将结果写入新的文件。
测试用例生成模块的执行流程图如下:
graph TD
A[初始化决策树] --> B[生成测试用例]
B --> C[遍历决策树根节点的所有子节点]
C --> D[为每个子节点生成条件为真和条件为假的测试用例]
D --> E[递归生成测试用例]
E --> F[判断节点是否为空]
F -->|是| G[返回]
F -->|否| H[修改当前节点条件]
H --> I[更新路径条件集合]
I --> J[判断是否为叶节点]
J -->|是| K[生成完整测试用例字符串]
K --> L[添加测试用例到集合中]
J -->|否| M[递归处理子节点]
M --> N[生成条件为真和条件为假的测试用例]
N --> E
此模块在代码中的位置:
逻辑:
- 判断节点是否为空:如果当前节点为空,则返回。
- 修改当前节点条件:根据真值 (
truthValue
) 修改当前节点的条件,形成新的条件表达式。 - 更新路径条件集合:将修改后的条件添加到新的路径条件集合中。
- 叶节点处理:如果当前节点是叶节点,生成完整的测试用例字符串,并添加到测试用例集合中。
- 递归处理子节点:如果当前节点有子节点,递归调用
generateMCDCases
方法,分别生成条件为真和条件为假的测试用例。
参数化测试用例模块的流程图如下:
graph TD
A[解析测试用例字符串] --> B[定义正则表达式模式]
B --> C[动态提取所有变量]
C --> D[遍历每个测试用例字符串]
D --> E[匹配条件表达式]
E --> F[检查是否有否定符号]
F --> G[提取变量名和操作符和值]
G --> H[根据操作符和否定符号计算测试值]
H --> I[添加变量名到集合]
I --> J[生成测试输入参数]
J --> K[为每个测试用例添加未定义变量的默认值]
K --> L[返回生成的测试输入参数集合]
此模块在代码中所处的位置为:
主要逻辑:
- 解析测试用例字符串:
- 使用正则表达式解析包含逻辑条件的字符串,提取变量、操作符和值。
- 根据操作符和是否存在否定符号,计算相应的测试输入值。
- 生成测试输入参数:
- 为每个测试用例生成变量和值的映射。
- 为未定义变量添加默认值。
graph TD
A[开始] --> B[初始化输出容器]
B --> C[打印测试输入]
C --> D[构建编译命令]
D --> E[执行编译命令]
E --> F{编译成功吗?}
F -->|是| G[打印编译输出]
F -->|否| H[打印编译错误]
G --> I[构建运行命令]
I --> J[执行运行命令]
J --> K[读取运行输出]
K --> L[打印运行输出]
L --> M{运行成功吗?}
M -->|是| N[返回输出结果]
M -->|否| O[打印运行错误]
O --> N
H --> N
N --> P[结束]
- 初始化输出容器:
- 创建一个
StringBuilder
对象用于累积程序执行的输出。
- 创建一个
- 打印测试输入:
- 打印传递给 Java 程序的命令行参数。
- 构建编译命令:
- 使用
javac
命令编译指定路径的 Java 源代码文件。 - 命令格式为
javac <filePath>
。
- 使用
- 执行编译命令:
- 使用
Runtime.getRuntime().exec
方法执行编译命令。 - 等待编译过程完成并检查其输出和错误信息。
- 使用
- 构建运行命令:
- 构建运行 Java 类的命令,命令格式为
java -cp <classPath> <className>
。 - 包含类路径(即源代码所在目录)和类名(从文件路径中提取)。
- 如果有传递命令行参数,则将其添加到运行命令中。
- 构建运行 Java 类的命令,命令格式为
- 执行运行命令:
- 使用
Runtime.getRuntime().exec
方法执行运行命令。 - 读取运行过程的输出,将其追加到
StringBuilder
对象中。 - 等待运行过程完成并检查其输出和错误信息。
- 使用
- 异常处理:
- 捕获并处理
IOException
和InterruptedException
异常,打印异常信息。
- 捕获并处理
- 返回结果:
- 返回累积的程序执行输出。
flowchart TD
A[初始化覆盖状态] --> B[遍历测试输入]
B --> C[将测试输入转换为字符串数组]
C --> D{根据编程语言执行代码}
D -->|Java| E[调用 executeJavaCode]
D -->|Python| F[调用 executePythonCode]
D -->|C| G[调用 executeCCode]
E --> H[捕获输出并打印]
F --> H[捕获输出并打印]
G --> H[捕获输出并打印]
H --> I[遍历测试条件]
I --> J{检查条件是否在输出中}
J -->|是| K[将条件的覆盖状态设为 true]
J -->|否| L[保持条件的覆盖状态为 false]
K --> M[统计已覆盖的条件数]
L --> M
M --> N[计算覆盖率]
N --> O[打印覆盖率结果]
- 覆盖状态初始化:
coverage
Map 初始化所有条件为false
。 - 执行代码并捕获输出:根据编程语言执行相应的代码,并捕获输出。
- 检查覆盖状态:遍历
testCases
,如果output
中包含condition
,则将该条件的覆盖状态设为true
。 - 计算覆盖率:统计覆盖的条件数,并计算覆盖率百分比。
- 打印覆盖率:输出最终的覆盖率结果。