2.3.4 JacocoCli二次开发
在覆盖率测试过程中,根据业务需要,一次测试需要同时生成全量报告和增量报告,对比网上现存在的方案,经过各种尝试后决定采取对jacococli做二次开发。
一,二次开发Jacoco
jacoco二开,主要加入了增量代码匹配的功能,使用的开源项目:jacoco: jacoco二开,支持增量代码覆盖率
1,修改KotlinInlineFilter.java
经过各种尝试,发现这个项目在文件:org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinInlineFilter.java 中与jacoco github上的有差别::https://github.com/jacoco/jacoco/blob/master/org.jacoco.core/src/org/jacoco/core/internal/analysis/filter/KotlinInlineFilter.java
主要是下面这个函数,修改成与github上的一样就可以,可能是这个jacoco项目比较早的原因:
private static int getFirstGeneratedLineNumber(final String sourceFileName,
final String smap) {
try {
final BufferedReader br = new BufferedReader(
new StringReader(smap));
expectLine(br, "SMAP");
// OutputFileName
expectLine(br, sourceFileName);
// DefaultStratumId
expectLine(br, "Kotlin");
// StratumSection
expectLine(br, "*S Kotlin");
// FileSection
expectLine(br, "*F");
final BitSet sourceFileIds = new BitSet();
String line;
while (!"*L".equals(line = br.readLine())) {
// AbsoluteFileName
br.readLine();
final Matcher m = FILE_INFO_PATTERN.matcher(line);
if (!m.matches()) {
throw new IllegalStateException(
"Unexpected SMAP line: " + line);
}
final String fileName = m.group(2);
if (fileName.equals(sourceFileName)) {
sourceFileIds.set(Integer.parseInt(m.group(1)));
}
}
if (sourceFileIds.isEmpty()) {
throw new IllegalStateException("Unexpected SMAP FileSection");
}
// LineSection
int min = Integer.MAX_VALUE;
while (true) {
line = br.readLine();
if (line.equals("*E") || line.equals("*S KotlinDebug")) {
break;
}
final Matcher m = LINE_INFO_PATTERN.matcher(line);
if (!m.matches()) {
throw new IllegalStateException(
"Unexpected SMAP line: " + line);
}
final int inputStartLine = Integer.parseInt(m.group(1));
final int lineFileID = Integer
.parseInt(m.group(2).substring(1));
final int outputStartLine = Integer.parseInt(m.group(4));
if (sourceFileIds.get(lineFileID)
&& inputStartLine == outputStartLine) {
continue;
}
min = Math.min(outputStartLine, min);
}
return min;
} catch (final IOException e) {
// Must not happen with StringReader
throw new AssertionError(e);
}
}
2,修改CodeDiffUtil.java
开源项目的函数检测CodeDiffUtil.java也有问题,需要改成如下所示:
/*******************************************************************************
* Copyright (c) 2009, 2021 Mountainminds GmbH & Co. KG and Contributors
* This program and the accompanying materials are made available under
* the terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Marc R. Hoffmann - initial API and implementation
*
*******************************************************************************/
package org.jacoco.core.internal.diff;
import org.jacoco.core.analysis.CoverageBuilder;
import org.objectweb.asm.Type;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
/**
* @ProjectName: root
* @Package: org.jacoco.core.internal.diff
* @Description: 差异代码处理类
* @Author: duanrui
* @CreateDate: 2021/1/12 15:17
* @Version: 1.0
* <p>
* Copyright: Copyright (c) 2021
*/
public class CodeDiffUtil {
private final static String OPERATE_ADD = "ADD";
private static String OutputStream;
/**
* 检测类是否在差异代码中
*
* @param className
* @return Boolean
*/
public static Boolean checkClassIn(String className,
List<ClassInfoDto> classInfos) {
if (null == classInfos || classInfos.isEmpty() || null == className) {
return Boolean.FALSE;
}
System.out.println("className="+className)
// 这里要考虑匿名内部类的问题
return classInfos.stream()
.anyMatch(c -> className.equals(c.getClassFile())
|| className.split("\\$")[0].equals(c.getClassFile()));
}
/**
* 检测方法是否在差异代码中
*
* @param className
* @param methodName
* @return Boolean
*/
public static Boolean checkMethodIn(String className, String methodName,
String desc, List<ClassInfoDto> classInfos) {
// 参数校验
if (null == classInfos || classInfos.isEmpty() || null == methodName
|| null == className) {
return Boolean.FALSE;
}
ClassInfoDto classInfoDto = classInfos.stream()
.filter(c -> className.equals(c.getClassFile())
|| className.split("\\$")[0].equals(c.getClassFile()))
.findFirst().orElse(null);
if (null == classInfoDto) {
return Boolean.FALSE;
}
// 如果是新增类,不用匹配方法,直接运行
if (OPERATE_ADD.equals(classInfoDto.getType())) {
return Boolean.TRUE;
}
if (null == classInfoDto.getMethodInfos()
|| classInfoDto.getMethodInfos().isEmpty()) {
return Boolean.FALSE;
}
// 匹配了方法,参数也需要校验
return classInfoDto.getMethodInfos().stream().anyMatch(m -> {
if (methodName.equals(m.getMethodName())) {
// System.out.println("className=" + className + ",methodName="
// + methodName + ",parmas=" + desc + ",m.getParameters()="
// + m.getParameters().toString());
return checkParamsIn(m.getParameters(), desc);
// lambda表示式匹配
} else if (methodName.contains("lambda$")
&& methodName.split("\\$")[1].equals(m.getMethodName())) {
return Boolean.TRUE;
} else {
return Boolean.FALSE;
}
});
}
/**
* 匹配餐数
* @param params
* 格式:String a
* @param desc
* 转换后格式: java.lang.String
* @return
*/
public static Boolean checkParamsIn(List<String> params, String desc) {
// 解析ASM获取的参数
Type[] argumentTypes = Type.getArgumentTypes(desc);
// 处理一下params,直接使用list有问题
String ckparams = "";
if (params.size() == 1) {
ckparams = params.get(0);
ckparams = ckparams.trim();
if (ckparams.length() == 0 && argumentTypes.length == 0) {
return Boolean.TRUE;
} else {
String[] diffParams = ckparams.split(",");
// 只有参数数量完全相等才做下一次比较,Type格式:I C Ljava/lang/String;
if (diffParams.length > 0
&& argumentTypes.length == diffParams.length) {
for (int i = 0; i < argumentTypes.length; i++) {
// 去掉包名只保留最后一位匹配,getClassName格式: int java/lang/String
String[] args = argumentTypes[i].getClassName()
.split("\\.");
String arg = args[args.length - 1];
// 如果参数是内部类类型,再截取下
if (arg.contains("$")) {
arg = arg.split("\\$")[arg.split("\\$").length - 1];
}
if (!diffParams[i].toLowerCase()
.contains(arg.toLowerCase())) {
return Boolean.FALSE;
}
}
// 只有个数和类型全匹配到才算匹配
return Boolean.TRUE;
}
return Boolean.FALSE;
}
} else {
return Boolean.FALSE;
}
}
}
二,构建项目
从jacoco: jacoco二开,支持增量代码覆盖率下载项目,修改上面提到的文件后,对项目进行打包构建,执行打包命令:
mvn clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true
项目会下载很多依赖的包,耐心等待即可。最后命令执行完成后,发现出错了,如下所示:
其实核心的内容已经构建完成,这几个失败和跳过的项目没有关系,找到我们需要的jacococli项目,查看构建效果。
相应的jar包打包完成,没有问题。
三,测试修改后的包
看项目的介绍,需要使用org.jacoco.cli-0.8.7-SNAPSHOT-nodeps.jar,修改成newjacococli.jar以便进行测试。
1,生成正常的覆盖率报告
执行如下命令:
java -jar ./newjacococli.jar report ./coverage-4.1.100.66230-2023-08-11-17_48_31.ec --classfiles /Users/sxf/Downloads/jacocoincrease/build_classes_12354/61/appstoreRelease/*********/RoomListFragment.class --sourcefiles /Users/********/src/main/java/ --encoding utf-8 --html ./jacoco
没有指定diff信息,可以生成指定类的全量覆盖率报告:
2,生成diff报告信息
通过其他的功能,拿到diff文件信息,格式如下所示:
[ {"classFile":"com/*********t/RoomListFragment", "methodInfos":[ {"methodName":"updateFolderTabLayout","parameters":["folderList: List<ConversationFolderUIEntity>"]}, {"methodName":"onCreate","parameters":["savedInstanceState: Bundle?"]} ], "type":"MODIFY" } ]
将上面的json转换成String,执行如下命令生成增量报告:
java -jar ./newjacococli.jar report ./coverage-4.1.100.66230-2023-08-11-17_48_31.ec --classfiles /Users/sxf/Downloads/jacocoincrease/build_classes_12354/************/RoomListFragment.class --sourcefiles /Users/********/src/main/java/ --encoding utf-8 --html ./jacoco2 --diffCode "[{\"classFile\":\"com/*******/RoomListFragment\",\"methodInfos\":[{\"methodName\":\"updateFolderTabLayout\",\"parameters\":[\"folderList: List<ConversationFolderUIEntity>\"]},{\"methodName\":\"onCreate\",\"parameters\":[\"savedInstanceState: Bundle?\"]}],\"type\":\"MODIFY\"}]"
生成的增量报告结果如下:
渲染结果如下:
通过上面的测试,可以达到想要的效果,现在就需要再修改一下Android agent先找到diff的信息,再去执行新的增量覆盖率的命令。
四,文件格式指定增量信息
如果diff文件过多,则无法使用命令行方式建议改成以json文件传递。将diff信息放到diff_files.json文件中,命令变成:
java -jar ./newjacococli.jar report ./packages/******/outputs/code-coverage/connected/mergedcoverage.ec
--classfiles ./build_classes_12397/62/appstoreRelease/*******/media/MediaFragment.class
--sourcefiles ./packages/********8/src/main/java
--encoding utf-8 --html ./diffreportsxf --diffCodeFiles
./diff_files.json