cover_image

中通科技代码覆盖率应用实践(二)

许多 科技中通
2022年04月19日 08:30
图片
图片

阅读《中通科技代码覆盖率应用实践(一)》,可至文章最后点击【阅读原文】。


1
 背景

原生的JaCoCo代码覆盖率工具,不支持增量代码覆盖率报告生成,因此中通测试开发团队做了二次开发,推出V1.0版,前面的推文中已有介绍。但是在实际使用场景中,存在同一分支上反复修改代码并多次部署,测试执行时不会再在没有改动的功能上重新测试,这就导致了明明测试已经覆盖了,但是生成的覆盖率报告数据为空的情况。针对这个无法满足实际场景需求的现状,我们推出了V2.0版,实现了同一个分支多次部署代码覆盖率报告的合并。


2
 JaCoCo增量代码覆盖率设计思路

这里主要需要解决的点在于获取增量代码并解析生成覆盖率报告。网上有很多相关的资料,这里就不多介绍了,简单介绍下改造大致的几个步骤:
  • 获取测试完成后的exec文件

  • 获取基线和被测分支之间的差异代码

  • 通过指定代码仓库和开发分支,解析开发分支和master之间的差异

  • 改造后的JaCoCo支持针对差异代码生成覆盖率报告

3
 同一个分支多次部署报告合并的实现

针对同一个分支多次修改代码并部署,生成多份覆盖率报告,导致明明测试已经覆盖了,但是覆盖率代码和编译代码不一致的情况下,生成的覆盖率报告数据无法合并的问题。网上也看到不少同行都有类似的问题,但一直未找到真正落地解决的方案。在此重点介绍一下我们的设计思路与实现方案。

图片

代码覆盖率流程

1) 每次生成代码覆盖率报告的同时,生成一份xml覆盖率报告。

2) 每次获取差异代码前,先解析上一次生成覆盖率报告时生成的xml报告,生成一个对象,为后续流程使用。

3) 合并覆盖率后,通过版本比较,判断方法是否在上一个版本中。如果存在于上一个版本中,则检查是否在上一个版本中被覆盖过,从而完成差异代码的获取。

4) 完成获取差异代码后,与第2)步生成的对象,通过方法名和行号,遍历比对,生成新的exec文件。

5) 解析新生成的exec文件,通过改造后的Report服务根据方法名,设置方法对应的行号,重新渲染行覆盖情况,从而生成合并后的增量代码覆盖率报告。

部分核心代码如下:

/** 
* 判断方法是否在上一个版本中
**/
private static boolean isMethodExistInLastCommit(MethodDeclaration method, String lastCommitId, GitAdapter gitAdapter, String oldJavaPath)
        throws IOException {
    String lastClassContent = gitAdapter
            .getTagRevisionSpecificFileContent(lastCommitId, oldJavaPath);
    ASTGenerator lastAstGenerator = new ASTGenerator(lastClassContent);
    MethodDeclaration[] lastMethods = lastAstGenerator.getMethods();
    Map<String, MethodDeclaration> methodsMap = new HashMap<>();
    for (int i = 0; i < lastMethods.length; i++) {
        methodsMap.put(lastMethods[i].getName().toString() + lastMethods[i].parameters().toString(), lastMethods[i]);
    }
    if (ASTGenerator.isMethodExist(method, methodsMap)) {
        return true;
    }
    return false;
}

/** 
* 判断是否在上一个版本中被覆盖过
**/
private static void isCoveredByLastCommit(JSONObject reportObject, String packageName, String className,
        MethodDeclaration method, GitAdapter gitAdapter, String lastTag, String oldJavaPath, MethodInfo methodInfo) {
    if (null == className) {
        return;
    }
    if (null == reportObject) {
        methodInfo.setIsCovered(false);
        return;
    }
    String lastClassContent = null;
    try {
        lastClassContent = gitAdapter.getTagRevisionSpecificFileContent(lastTag, oldJavaPath);
    } catch (IOException e) {
        e.printStackTrace();
    }
    ASTGenerator lastAstGenerator = new ASTGenerator(lastClassContent);
    MethodDeclaration[] lastMethods = lastAstGenerator.getMethods();
    Map<String, MethodDeclaration> lastMethodsMap = new HashMap<String, MethodDeclaration>();
    for (int i = 0; i < lastMethods.length; i++) {
        lastMethodsMap
                .put(lastMethods[i].getName().toString() + lastMethods[i].parameters().toString(), lastMethods[i]);
    }
    for (int i = 0; i < reportObject.size(); i++) {
        JSONArray packageDtoList = reportObject.getJSONArray("packageDtoList");
        if (null == packageDtoList) {
            methodInfo.setIsCovered(false);
        }
        for (int j = 0; j < packageDtoList.size(); j++) {
            JSONObject packageDto = packageDtoList.getJSONObject(j);
            if (!packageName.equals(packageDto.getString("name").replace("/""."))) {
                continue;
            }
            JSONArray classDtoList = packageDto.getJSONArray("classDtoList");
            if (null == classDtoList) {
                methodInfo.setIsCovered(false);
                return;
            }
            for (int b = 0; b < classDtoList.size(); b++) {
                JSONObject classDto = classDtoList.getJSONObject(b);
                if (!className.equals(classDto.getString("sourceFileName").split("\\.")[0])) {
                    continue;
                }
                JSONArray methodDtoList = classDto.getJSONArray("reportMethodDtoList");
                if (null == methodDtoList) {
                    methodInfo.setIsCovered(false);
                    return;
                }
                for (int l = 0; l < methodDtoList.size(); l++) {
                    JSONObject methodDto = methodDtoList.getJSONObject(l);
                    if (!method.getName().toString().equals(methodDto.getString("name"))) {
                        continue;
                    }
                    methodInfo.setReportLineList(methodDto.getJSONArray("reportLineDtoList"));
                    JSONArray methodReportCoveredDtoList = methodDto.getJSONArray("reportCoveredDtoList");
                    for (int m = 0; m < methodReportCoveredDtoList.size(); m++) {
                        JSONObject methodReportCoveredDto = methodReportCoveredDtoList.getJSONObject(m);
                        String coveredName = methodReportCoveredDto.getString("name");
                        if (!coveredName.equals("INSTRUCTION")) {
                            continue;
                        }
                        if (methodReportCoveredDto.getInteger("missed") == 0 && ASTGenerator.isMethodTheSame(method,
                                lastMethodsMap.get(method.getName().toString() + method.parameters().toString()))) {
                            methodInfo.setIsCovered(true);
                            return;
                        } else {
                            methodInfo.setIsCovered(false);
                            return;
                        }
                    }
                }
            }
        }
    }
}

/**
*重写增量法计数器
**/
public void incrementMethodCounter(CounterImpl instructionCounter) {
   this.instructionCounter = instructionCounter;
   final ICounter base = instructionCounter.getCoveredCount() == 0 ? CounterImpl.COUNTER_1_0
         : CounterImpl.COUNTER_0_1;
   this.methodCounter = this.methodCounter.increment(base);
   this.complexityCounter = this.complexityCounter.increment(base);
}

/**
*重新设置方法的第一行与最后一行
**/
public static void setFirstAndLastLine(List<org.jacoco.report.util.MethodInfo> methodInfoList) {
    if (null == methodInfoList || methodInfoList.size() == 0 || null == CoverageBuilder.classInfos) {
        return;
    }
    for (ClassInfo classInfo : CoverageBuilder.classInfos) {
        String sourceFileName = classInfo.getPackages() + "." + classInfo.getClassName();
        for (org.jacoco.report.util.MethodInfo methodInfo : methodInfoList) {
            String sourceFileNameTemp = methodInfo.getPackageName() + "." + methodInfo.getClassName();
            if (!sourceFileName.equals(sourceFileNameTemp)) {
                continue;
            }
            for (org.jacoco.core.internal.diff.MethodInfo methodInfoTemp : classInfo.getMethodInfos()) {
                if (!methodInfo.getMethodName().replaceAll(" """)
                        .equals(getMethodName(methodInfoTemp.getMethodName(), methodInfoTemp.getParameters()))) {
                    continue;
                }
                methodInfoTemp.setFirstLine(methodInfo.getFirstLine());
                methodInfoTemp.setLastLine(methodInfo.getLastLine());
                break;
            }
        }
    }
}


/**
*获取方法名
**/
public static String getMethodName(String methodName, String params) {
    if (null == methodName) {
        return "";
    }
    if (null == params) {
        return methodName + "()";
    }
    if (params.startsWith("[")) {
        params = params.replaceFirst("\\[""");
    }
    if (params.endsWith("]")) {
        params = params.substring(0, params.lastIndexOf("]"));
    }
    Pattern pattern1 = Pattern.compile("Map<(.*?)>");
    Matcher matcher1 = pattern1.matcher(params);
    while(matcher1.find()) {
        params = params.replace(matcher1.group(), "Map");
    }
    Pattern pattern = Pattern.compile("(?<=\\()(.+?)(?=\\))");
    Matcher matcher = pattern.matcher(params);
    while(matcher.find()) {
        params = params.replace(matcher.group(), "");
    }
    String[] paramsArr = params.split(",");
    StringBuilder sb = new StringBuilder();
    sb.append("(");
    for (String str : paramsArr) {
        try {
            String newStr = str.trim();
            if (newStr.startsWith("@")) {
                newStr = newStr.substring(newStr.indexOf(" "), newStr.length());
            }
            newStr = newStr.trim().split(" ")[0].trim();
            if (newStr.contains("<")) {
                newStr = newStr.split("<")[0];
            }
            sb.append(newStr);
            sb.append(",");
        } catch (Exception e) {
            logger.error("Parse Method Name error.methodName = {}, params = {}", methodName, params, e);
        }
    }
    methodName = methodName + sb.substring(0, sb.length() - 1) + ")";
    return methodName;
}

/**
*根据方法覆盖状态重新计算行覆盖度
**/
public static ILine getNewLine(ILine line, ClassInfo classInfo, int lineNr) {
    if (null == classInfo) {
        return line;
    }
    List<MethodInfo> methodInfoList = classInfo.getMethodInfos();
    if (null != methodInfoList && methodInfoList.size() > 0) {
        for (MethodInfo methodInfo : methodInfoList) {
            JSONArray methodInfoReportLineList = methodInfo.getReportLineList();
            if (null != methodInfoReportLineList && methodInfoReportLineList.size() > 0) {
                for (int i = 0; i < methodInfoReportLineList.size(); i++) {
                    JSONObject methodInfoReportLine = methodInfoReportLineList.getJSONObject(i);
                    if (null == methodInfoReportLine || lineNr < methodInfo.getFirstLine() || lineNr > methodInfo .getLastLine()) {
                        break;
                    }
                    if (null == methodInfo.getIsCovered() || !methodInfo.getIsCovered()) {
                        return line;
                    }
                    return getLine(methodInfoReportLine);
                }
            }
        }
    }
    return line;
}

public static LineImpl getLine(final JSONObject methodInfoReportLine) {
    final LineImpl line = LineImpl.EMPTY;
    return line.increment(CounterImpl.getInstance(0, methodInfoReportLine.getInteger("mi") + methodInfoReportLine.getInteger("ci")),
            CounterImpl.getInstance(0, methodInfoReportLine.getInteger("mb") + methodInfoReportLine.getInteger("cb")));
}


4
 效果

至此,我们实现了只对有所变更的方法进行覆盖率统计,生成合并后的增量代码覆盖率报告,效果如图2,包括从包、类、方法、代码各个级别的报告:
图片

5
总结

通过对JaCoCo的改造,进一步完善了JaCoCo的功能与持续交付的流程,更适合中通的实践应用。

  • 根据测试实际情况,支持多次更新报告;

  • 多次部署,合并报告,满足实际测试需求;

  • 整个流程全自动收集、生成代码覆盖率报告。

6
展望

未来关于JaCoco的实践应用,我们团队还有更多的想法:

  • 与DevOps对接,无感知接入;

  • 引入前端、Golang语言代码覆盖率;

  • 深入应用代码覆盖率工具降低代码漏测率;

让我们保持更多的期待和更多的交流。


图片


图片

RECOMMEND

往期干货

•大数据测试|中通科技大数据质量保障探索与实践(上)

•大数据测试|中通科技大数据质量保障探索与实践(下)

•质量度量|中通科技全面质量管理实践探索

•中间件测试|ZCAT监控平台测试探索与实践

•扩展Kubernetes调度器(上)

欢迎各位技术大佬向本公众号积极投稿,提升经验分享、信息互通的技术交流氛围,共同解决技术难题、共同进步!(投稿咨询请联系科技与信息中心助理室 徐蕊)

图片


点击阅读原文

可查看《中通科技代码覆盖率应用实践(一)》

图片

继续滑动看下一个
科技中通
向上滑动看下一个