行业知识
Android增量报告生成新方案
�� 在开发精准测试覆盖率相关的功能时候,对于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"); // 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); }}开源项目的函数检测也有问题,需要改成如下所示:
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 *
* 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 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 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 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
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"]}, {"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\"]},{\"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插件,不影响覆盖率数据的采集。
-
上一篇:如何在服务器无视服主?
下一篇:像素工厂如何进入服务器?