阅读《中通科技代码覆盖率应用实践(一)》,可至文章最后点击【阅读原文】。
获取测试完成后的exec文件
获取基线和被测分支之间的差异代码
通过指定代码仓库和开发分支,解析开发分支和master之间的差异
改造后的JaCoCo支持针对差异代码生成覆盖率报告
针对同一个分支多次修改代码并部署,生成多份覆盖率报告,导致明明测试已经覆盖了,但是覆盖率代码和编译代码不一致的情况下,生成的覆盖率报告数据无法合并的问题。网上也看到不少同行都有类似的问题,但一直未找到真正落地解决的方案。在此重点介绍一下我们的设计思路与实现方案。
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")));
}
通过对JaCoCo的改造,进一步完善了JaCoCo的功能与持续交付的流程,更适合中通的实践应用。
根据测试实际情况,支持多次更新报告;
多次部署,合并报告,满足实际测试需求;
整个流程全自动收集、生成代码覆盖率报告。
未来关于JaCoco的实践应用,我们团队还有更多的想法:
与DevOps对接,无感知接入;
引入前端、Golang语言代码覆盖率;
深入应用代码覆盖率工具降低代码漏测率;
让我们保持更多的期待和更多的交流。
欢迎各位技术大佬向本公众号积极投稿,提升经验分享、信息互通的技术交流氛围,共同解决技术难题、共同进步!(投稿咨询请联系科技与信息中心助理室 徐蕊)
点击阅读原文
可查看《中通科技代码覆盖率应用实践(一)》