在开发精准测试覆盖率相关的功能时候,对于Android的增量报告,由于担心修改jacoco插件会影响App的打包,所以一直没有修改。在网上查了一下,没有找到合适的方案。只有一个diff-cover开源项目:https://github.com/abmaonline/diff-cover
经过测试,可以生成对比分支的增量报告,可是对于对比版本,就不太适应了。于是就各种调研,最后通过最低层的方案,过滤文件和函数,于是有了第一个版本的增量覆盖率报告生成方案。
一,增量覆盖率报告方案一
第一版本的增量覆盖率报告如下流程:
此方案虽然能生成增量覆盖率报告,但存在如下问题:
1,diff-cover生成的报告只能对比分支,不能对比版本;并且生成的覆盖率报告和jacoco相差很大;
2,生成的报告数据欠缺,diff-cover只有行覆盖率;而对比版本过滤的方案,生成的有行,方法和类,但没有分支数据;
3,操作比较繁琐,要过滤很多数据,如果diff文件较多,生成时间比较长;
4,最终的代码渲染没有办法按增量处理,只能过滤出diff类对应的文件,渲染则是全量的。
二,增量覆盖率报告方案二
Android jacoco生成增量报告,早期没有找到合适的方案,后面做了一些调研,发现下面有个方案:
1,参考文档
Android 增量代码测试覆盖率工具实践:https://juejin.cn/post/6920029313316159502
2,开源项目
AndJacoco:https://github.com/ttpai/AndJacoco
此方案的核心思想是:
-
通过指定的对比方式或是分支,或是版本,找到diff的文件及函数列表;
-
在jacoco进行注入的时候,只对增量代码做注入;
-
最后根据采集的覆盖率数据,生成报告,即为增量报告。
经过各种尝试,发现这个项目在文件: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");// OutputFileNameexpectLine(br, sourceFileName);// DefaultStratumIdexpectLine(br, "Kotlin");// StratumSectionexpectLine(br, "*S Kotlin");// FileSectionexpectLine(br, "*F");final BitSet sourceFileIds = new BitSet();String line;while (!"*L".equals(line = br.readLine())) {// AbsoluteFileNamebr.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");}// LineSectionint 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 StringReaderthrow new AssertionError(e);}
}
开源项目的函数检测也有问题,需要改成如下所示:
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/StringString[] 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;}}
}
3,构建项目
从https://gitee.com/Dray/jacoco下载项目,修改KotlinInlineFilter.java 文件,如上面所示,执行打包命令:
mvn clean install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true
项目会下载很多依赖的包,耐心等待即可。最后命令执行完成后,发现出错了,如下所示:
相应的jar包打包完成,没有问题。
4,测试修改后的包
看项目的介绍,需要使用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/com/yiqixie/kem/im/ui/messages/roomlist/RoomListFragment.class --sourcefiles /Users/sxf/Documents/精准测试/KimCode/kim-android/packages/kim-android/eek-features/module-kim/src/main/java/ --encoding utf-8 --html ./jacoco15
没有指定diff信息,可以生成指定类的全量覆盖率报告:
(2)生成diff报告信息
通过其他的功能,拿到diff文件信息,格式如下所示:
[{"classFile":"com/yiqixie/kem/im/ui/messages/roomlist/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/61/appstoreRelease/com/yiqixie/kem/im/ui/messages/roomlist/RoomListFragment.class
--sourcefiles /Users/sxf/Documents/精准测试/KimCode/kim-android/packages/kim-android/eek-features/module-kim/src/main/java/ --encoding utf-8 --html ./jacoco16
--diffCode "[{\"classFile\":\"com/yiqixie/kem/im/ui/messages/roomlist/RoomListFragment\",\"methodInfos\":[{\"methodName\":\"updateFolderTabLayout\",\"parameters\":[\"folderList: List<ConversationFolderUIEntity>\"]},{\"methodName\":\"onCreate\",\"parameters\":[\"savedInstanceState: Bundle?\"]}],\"type\":\"MODIFY\"}]"
生成的增量报告结果如下:
渲染结果如下:
通过上面的测试,可以达到想要的效果,现在就需要再修改一下Android agent先找到diff的信息,再去执行新的增量覆盖率的命令。
(3) 如果diff文件过多,则无法使用命令行方式
建议改成以json文件传递。
java -jar ./newjacococli.jar report ./packages/kim-android/app/build/outputs/code-coverage/connected/mergedcoverage.ec
--classfiles ./build_classes_12397/62/appstoreRelease/com/yiqixie/kem/im/ui/messages/media/MediaFragment.class
--classfiles ./build_classes_12397/62/appstoreRelease/com/yiqixie/kem/im/ui/messages/media/MediaLayout.class
--classfiles ./build_classes_12397/62/appstoreRelease/com/yiqixie/kem/im/ui/messages/media/MediaSlideActivity.class
--sourcefiles ./packages/kim-android/eek-features/module-kim/src/main/java
--sourcefiles ./packages/kim-android/eek-features/module-kim/src/main/java
--sourcefiles ./packages/kim-android/eek-features/module-kim/src/main/java
--encoding utf-8 --html ./diffreportsxf
--diffCodeFiles ./diff_files.json
5,最新的生成覆盖率报告流程
通过借助于上面修改后的jacococli.jar包,最新生成增量报告的流程如下所示:
新的生成增量报告的方案做到了如下几点:
-
统一对比版本和对比分支生成增量报告的逻辑,最终生成jacoco格式的报告;
-
补全了所有增量报告数据,分支,行,函数和类的覆盖率数据都是准确的;
-
增量方案渲染代码页覆盖情况,方便测试同学根据增量函数进行排查问题。
-
无需修改jacoco插件,不影响覆盖率数据的采集。