一、背景:
质量保障的流程中,对于需求和研发质量都有比较完善的衡量标准,需求可以通过评审通过率,需求的吞吐率等,研发质量可以通过静态扫描、单测、冒烟、自动化回归通过率等多个维度进行推进。然而当进入了测试阶段,对于测试的覆盖度一直缺少精确客观的度量依据,过去只能通过用例执行情况,而用例本身就是依托于测试人员主观设计的,加上测试手段无论如何丰富,对于新feature的人工测试都无法避免,所以需要一套能够很亮在测试阶段,对于提测的功能测试程度进行有效度量的方法,可以覆盖在这个阶段的所有测试手段的覆盖率集合。
二、调研:
经过调研,决定基于jacoco进行二次开发来支持增量代码的覆盖率统计。考虑到对于应用的无侵入性的需求,采用jacoco的on-the-fly模式,在 JVM 中通过 -javaagent 参数指定 jar 文件启动 Instrumentation 的代理程序,代理程序在通过 Class Loader 装载一个 class 前判断是否需要注入 class 文件,将统计代码插入 class ,测试覆盖率分析就可以在 JVM 执行测试的过程中完成。
三、方案:
大体思路:
- 通过jacoco-agent代理开启tcp端口,在测试过程中可以记录探针的覆盖执行信息。
- 用户在需要的时机触发获取测试完成后的 exec 文件,需要merge整个测试周期内的历史exec文件,这个过程中有个问题,jacoco是依据claasid来区分,每次代码变更后生成的class文件会是不同的classid,当代码发生变更,重新编译部署后,老版本的jacoco.exec和新版本的class是不匹配的,会导致无法如预想的merge成功,丢失上次的覆盖率数据,这个问题通过对merge的源码进行修改,如果classid不匹配,再尝试比较classname来进行兜底。
- diff分析差异代码(考虑到整个测试过程中,需要支持分支之间以及commit之间两种颗粒度),
- 改造 JaCoCo ,使它支持仅对差异代码生成覆盖率报告;
Step1: dump覆盖率文件(集群环境下会生成多份文件)
private void dumpExecFile(String folderPath, String podsInfo) throws IOException {
if (!new File(folderPath).exists()) {
new File(folderPath).mkdirs();
}
//获取jacoco agent开启的tcp端口信息
JSONObject podInfoObj = JSONObject.parseObject(podsInfo);
Integer podPort = podInfoObj.getIntValue("agentPort");
String[] podIps = podInfoObj.getString("podIps").split(",");
int podCount = 0;
String jacocoFileName;
for (String podIp : podIps) {
jacocoFileName = "jacoco-" + podIp + "-" + System.currentTimeMillis() + ".exec";
FileOutputStream localFile = new FileOutputStream(FilenameUtils.concat(folderPath, jacocoFileName));
ExecutionDataWriter localWriter = new ExecutionDataWriter(localFile);
//socket访问jacoco agent的tcp端口
Socket socket = new Socket(InetAddress.getByName(podIp), podPort);
RemoteControlWriter writer = new RemoteControlWriter(socket.getOutputStream());
RemoteControlReader reader = new RemoteControlReader(socket.getInputStream());
reader.setSessionInfoVisitor(localWriter);
reader.setExecutionDataVisitor(localWriter);
// 发送Dump命令,获取Exec数据
writer.visitDumpCommand(true, false);
if (!reader.read()) {
socket.close();
localFile.close();
throw new IOException("Socket连接失败,请检查jacoco agent在当前pod中是否正常启动!");
}
socket.close();
localFile.close();
log.info(String.format("生成dump文件%s成功!", jacocoFileName));
}
}
Step2: Load生成的覆盖率文件(目录下多个exec文件按照时间进行顺序加载)
private ExecFileLoader loadExecFile(String execPath) throws IOException {
ExecFileLoader execFileLoader = new ExecFileLoader();
//遍历load当前分支或者commit下的所有exec文件
Path start = FileSystems.getDefault().getPath(execPath);
List<Path> jacocoFiles = Files.walk(start, 1)
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".exec"))
.sorted((arg0,arg1) ->{
String arg0FileTime = StringUtils.substringBefore(
StringUtils.substringAfterLast(arg0.getFileName().toString(),"-"),".");
String arg1FileTime = StringUtils.substringBefore(
StringUtils.substringAfterLast(arg1.getFileName().toString(),"-"),".");
return arg1FileTime.compareTo(arg0FileTime);
})
.collect(Collectors.toList());
for (Path path : jacocoFiles) {
execFileLoader.load(path.toFile());
}
return execFileLoader;
}
Step3:基于分支或者commit获取差异代码(需要对jacoco进行二次开发支持差异代码分析)
try {
log.info("开始分析差异文件......");
if (taskType == 0) {
//当前分支与master比对
this.bundleCoverage = analyzeByBranch(execFileLoader.getExecutionDataStore(),
gitPath, "origin/" + adBuildInfoDO.getBuildBranch());
} else if (taskType == 1) {
//当前部署的commit与上次commit比对
this.bundleCoverage = analyzeByTagOrCommit(execFileLoader.getExecutionDataStore(), gitPath,
"commit", adBuildInfoDO.getBuildCommit(),adBuildInfoDO.getLastBuildCommit());
}
}catch(Exception e){
e.printStackTrace();
log.error("分析差异文件失败!");
return false;
}
Step4:生成差异覆盖率报告
private void createReport(IBundleCoverage bundleCoverage,ExecFileLoader execFileLoader)
throws IOException {
final HTMLFormatter htmlFormatter = new HTMLFormatter();
final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory));
visitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(),execFileLoader.getExecutionDataStore().getContents());
// Populate the report structure with the bundle coverage information.
// Call visitGroup if you need groups in your report.
visitor.visitBundle(bundleCoverage, sourceDirectory);
visitor.visitEnd();
}