通过jacoco agent生成覆盖率报告

445 阅读3分钟
一、背景:

质量保障的流程中,对于需求和研发质量都有比较完善的衡量标准,需求可以通过评审通过率,需求的吞吐率等,研发质量可以通过静态扫描、单测、冒烟、自动化回归通过率等多个维度进行推进。然而当进入了测试阶段,对于测试的覆盖度一直缺少精确客观的度量依据,过去只能通过用例执行情况,而用例本身就是依托于测试人员主观设计的,加上测试手段无论如何丰富,对于新feature的人工测试都无法避免,所以需要一套能够很亮在测试阶段,对于提测的功能测试程度进行有效度量的方法,可以覆盖在这个阶段的所有测试手段的覆盖率集合。

二、调研:

经过调研,决定基于jacoco进行二次开发来支持增量代码的覆盖率统计。考虑到对于应用的无侵入性的需求,采用jacoco的on-the-fly模式,在 JVM 中通过 -javaagent 参数指定 jar 文件启动 Instrumentation 的代理程序,代理程序在通过 Class Loader 装载一个 class 前判断是否需要注入 class 文件,将统计代码插入 class ,测试覆盖率分析就可以在 JVM 执行测试的过程中完成。 FiXMzUPynEIAlh9gwD_1lLWvyJ7B.png

三、方案:

大体思路:

  1. 通过jacoco-agent代理开启tcp端口,在测试过程中可以记录探针的覆盖执行信息。
  2. 用户在需要的时机触发获取测试完成后的 exec 文件,需要merge整个测试周期内的历史exec文件,这个过程中有个问题,jacoco是依据claasid来区分,每次代码变更后生成的class文件会是不同的classid,当代码发生变更,重新编译部署后,老版本的jacoco.exec和新版本的class是不匹配的,会导致无法如预想的merge成功,丢失上次的覆盖率数据,这个问题通过对merge的源码进行修改,如果classid不匹配,再尝试比较classname来进行兜底。
  3. diff分析差异代码(考虑到整个测试过程中,需要支持分支之间以及commit之间两种颗粒度),
  4. 改造 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();

}