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
 *           <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;
      }
   }
}

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插件,不影响覆盖率数据的采集。

  • 35
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值