jacoco增量代码覆盖率

Jacoco增量覆盖率说明 格式不清楚请到这里

https://blog.csdn.net/tushuping/article/details/112613528


能找到这里,说明对jacoco的原理和使用有了一定的了解,而我写这边文章主要是网络上基本没有完整文档加代码的jaocco增量覆盖说明,所以我想分享些东西让需要这方面的人快速去实现自己想要的功能,那么如果想实现增量代码覆盖率需要做到哪些工作呢?

大家在网络上找到的实现方式无外乎三种

获取到增量代码,在jacoco进行插桩时判断是否是增量代码后再进行插桩,这样需要两个步骤,一是获取增量代码,二是找到jacoco的插桩逻辑进行修改

获取增量代码,在report阶段去判断方法是否是增量,再去生成报告

获取差异代码,解析生成的report报告,再过滤出差异代码的报告

首先第一种需要对java字节码操作比较熟悉,难度较高,我们不谈,第三种去解析生成的报告,可能存在误差

所以我们一般选择第二种,而网络上所有的增量实现基本是基于第二种,我们先看看下面的图

上图说明了jacoco测试覆盖率的生成流程,而我们要做的是在report的时候加入我们的逻辑

根据我们的方案,我们需要三个动作

计算出两个版本的差异代码(基于git)

将差异代码在jacoco的report阶段传给jacoco

修改jacoco源码,生成报告时判断代码是否是增量代码,只有增量代码才去生成报告

下面我们逐步讲解上述步骤

计算差异代码

计算差异代码我实现了一个简单的工程:差异代码获取

主要用到了两个工具类

<dependency>

    <groupId>org.eclipse.jgit</groupId>

    <artifactId>org.eclipse.jgit</artifactId>

</dependency>

<!-- https://mvnrepository.com/artifact/com.github.javaparser/javaparser-core -->

<dependency>

    <groupId>com.github.javaparser</groupId>

    <artifactId>javaparser-core</artifactId>

</dependency>

org.eclipse.jgit主要用于从git获取代码,并获取到存在变更的文件

javaparser-core是一个java解析类,能将class类文件解析成树状,方便我们去获取差异类

/**

* 获取差异类

*

* @param diffMethodParams

* @return

*/

public List<ClassInfoResult> diffMethods(DiffMethodParams diffMethodParams) {

    try {

        //原有代码git对象

        Git baseGit = cloneRepository(diffMethodParams.getGitUrl(), localBaseRepoDir + diffMethodParams.getBaseVersion(), diffMethodParams.getBaseVersion());

        //现有代码git对象

        Git nowGit = cloneRepository(diffMethodParams.getGitUrl(), localBaseRepoDir + diffMethodParams.getNowVersion(), diffMethodParams.getNowVersion());

        AbstractTreeIterator baseTree = prepareTreeParser(baseGit.getRepository(), diffMethodParams.getBaseVersion());

        AbstractTreeIterator nowTree = prepareTreeParser(nowGit.getRepository(), diffMethodParams.getNowVersion());

        //获取两个版本之间的差异代码

        List<DiffEntry> diff = nowGit.diff().setOldTree(baseTree).setNewTree(nowTree).setShowNameAndStatusOnly(true).call();

        //过滤出有效的差异代码

        Collection<DiffEntry> validDiffList = diff.stream()

                //只计算java文件

                .filter(e -> e.getNewPath().endsWith(".java"))

                //排除测试文件

                .filter(e -> e.getNewPath().contains("src/main/java"))

                //只计算新增和变更文件

                .filter(e -> DiffEntry.ChangeType.ADD.equals(e.getChangeType()) || DiffEntry.ChangeType.MODIFY.equals(e.getChangeType()))

                .collect(Collectors.toList());

        if (CollectionUtils.isEmpty(validDiffList)) {

            return null;

        }

        /**

        * 多线程获取旧代码和新代码的差异类及差异方法

        */

        List<CompletableFuture<ClassInfoResult>> priceFuture = validDiffList.stream().map(item -> getClassMethods(getClassFile(baseGit, item.getNewPath()), getClassFile(nowGit, item.getNewPath()), item)).collect(Collectors.toList());

        return priceFuture.stream().map(CompletableFuture::join).filter(Objects::nonNull).collect(Collectors.toList());

    } catch (GitAPIException e) {

        e.printStackTrace();

    }

    return null;

}

以上代码为获取差异类的核心代码

/**

* 获取类的增量方法

*

* @param oldClassFile 旧类的本地地址

* @param mewClassFile 新类的本地地址

* @param diffEntry    差异类

* @return

*/

private CompletableFuture<ClassInfoResult> getClassMethods(String oldClassFile, String mewClassFile, DiffEntry diffEntry) {

    //多线程获取差异方法,此处只要考虑增量代码太多的情况下,每个类都需要遍历所有方法,采用多线程方式加快速度

    return CompletableFuture.supplyAsync(() -> {

        String className = diffEntry.getNewPath().split("\\.")[0].split("src/main/java/")[1];

        //新增类直接标记,不用计算方法

        if (DiffEntry.ChangeType.ADD.equals(diffEntry.getChangeType())) {

            return ClassInfoResult.builder()

                    .classFile(className)

                    .type(DiffEntry.ChangeType.ADD.name())

                    .build();

        }

        List<MethodInfoResult> diffMethods;

        //获取新类的所有方法

        List<MethodInfoResult> newMethodInfoResults = MethodParserUtils.parseMethods(mewClassFile);

        //如果新类为空,没必要比较

        if (CollectionUtils.isEmpty(newMethodInfoResults)) {

            return null;

        }

        //获取旧类的所有方法

        List<MethodInfoResult> oldMethodInfoResults = MethodParserUtils.parseMethods(oldClassFile);

        //如果旧类为空,新类的方法所有为增量

        if (CollectionUtils.isEmpty(oldMethodInfoResults)) {

            diffMethods = newMethodInfoResults;

        } else {  //否则,计算增量方法

            List<String> md5s = oldMethodInfoResults.stream().map(MethodInfoResult::getMd5).collect(Collectors.toList());

            diffMethods = newMethodInfoResults.stream().filter(m -> !md5s.contains(m.getMd5())).collect(Collectors.toList());

        }

        //没有增量方法,过滤掉

        if (CollectionUtils.isEmpty(diffMethods)) {

            return null;

        }

        ClassInfoResult result = ClassInfoResult.builder()

                .classFile(className)

                .methodInfos(diffMethods)

                .type(DiffEntry.ChangeType.MODIFY.name())

                .build();

        return result;

    }, executor);

}

以上代码为获取差异方法的核心代码

大家可以下载代码后运行,下面我们展示下,运行代码后获取到的差异代码内容(参数可以是两次commitId,也可以是两个分支,按自己的业务场景来)

{

  "code": 10000,

  "msg": "业务处理成功",

  "data": [

    {

      "classFile": "com/dr/application/InstallCert",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/application/app/controller/Calculable",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/application/app/controller/JenkinsPluginController",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/application/app/controller/LoginController",

      "methodInfos": [

        {

          "md5": "2C9D2AE2B1864A2FCDDC6D47CEBEBD4C",

          "methodName": "captcha",

          "parameters": "HttpServletRequest request,HttpServletResponse response"

        },

        {

          "md5": "3D6DFADD2171E893D99D3D6B335B22EA",

          "methodName": "login",

          "parameters": "@RequestBody LoginUserParam loginUserParam,HttpServletRequest request"

        },

        {

          "md5": "90842DFA5372DCB74335F22098B36A53",

          "methodName": "logout",

          "parameters": ""

        },

        {

          "md5": "D0B2397D04624D2D60E96AB97F679779",

          "methodName": "testInt",

          "parameters": "int a,char b"

        },

        {

          "md5": "34219E0141BAB497DCB5FB71BAE1BDAE",

          "methodName": "testInt",

          "parameters": "String a,int b"

        },

        {

          "md5": "F9BF585A4F6E158CD4475700847336A6",

          "methodName": "testInt",

          "parameters": "short a,int b"

        },

        {

          "md5": "0F2508A33F719493FFA66C5118B41D77",

          "methodName": "testInt",

          "parameters": "int[] a"

        },

        {

          "md5": "381C8CBF1F381A58E1E93774AE1AF4EC",

          "methodName": "testInt",

          "parameters": "AddUserParam param"

        },

        {

          "md5": "64BF62C11839F45030198A8D8D7821C5",

          "methodName": "testInt",

          "parameters": "T[] a"

        },

        {

          "md5": "D091AB0AD9160407AED4182259200B9B",

          "methodName": "testInt",

          "parameters": "Calculable calc,int n1,int n2"

        },

        {

          "md5": "693BBA0A8A57F2FD19F61BA06F23365C",

          "methodName": "display",

          "parameters": ""

        },

        {

          "md5": "F9DFE0E75C78A31AFB6A8FD46BDA2B81",

          "methodName": "a",

          "parameters": "InnerClass a"

        }

      ],

      "type": "MODIFY"

    },

    {

      "classFile": "com/dr/application/app/controller/RoleController",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/application/app/controller/TestController",

      "methodInfos": [

        {

          "md5": "B1840C873BF0BA74CB6749E1CEE93ED7",

          "methodName": "getPom",

          "parameters": "HttpServletResponse response"

        },

        {

          "md5": "9CEE68771972EAD613AF237099CD2349",

          "methodName": "getDeList",

          "parameters": ""

        }

      ],

      "type": "MODIFY"

    },

    {

      "classFile": "com/dr/application/app/controller/UserController",

      "methodInfos": [

        {

          "md5": "7F2AD08CE732ADDFC902C46D238A9EB3",

          "methodName": "add",

          "parameters": "@RequestBody AddUserParam addUserParam"

        },

        {

          "md5": "D41D8CD98F00B204E9800998ECF8427E",

          "methodName": "get",

          "parameters": ""

        },

        {

          "md5": "2B35EA4FB5054C6EF13D557C2ACBB581",

          "methodName": "list",

          "parameters": "@ApiParam(required = true, name = \"page\", defaultValue = \"1\", value = \"当前页码\") @RequestParam(name = \"page\") Integer page,@ApiParam(required = true, name = \"pageSize\", defaultValue = \"10\", value = \"每页数量\") @RequestParam(name = \"pageSize\") Integer pageSize,@ApiParam(name = \"userId\", value = \"用户id\") @RequestParam(name = \"userId\", required = false) Long userId,@ApiParam(name = \"username\", value = \"用户名\") @RequestParam(name = \"username\", required = false) String username,@ApiParam(name = \"userSex\", value = \"性别\") @RequestParam(name = \"userSex\", required = false) Integer userSex,@ApiParam(name = \"mobile\", value = \"手机号\") @RequestParam(name = \"mobile\", required = false) String mobile"

        }

      ],

      "type": "MODIFY"

    },

    {

      "classFile": "com/dr/application/app/controller/view/RoleViewController",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/application/app/controller/view/UserViewController",

      "methodInfos": [

        {

          "md5": "9A1DDA3F41B36026FC2F3ACDAE85C1DB",

          "methodName": "user",

          "parameters": ""

        }

      ],

      "type": "MODIFY"

    },

    {

      "classFile": "com/dr/application/app/param/AddRoleParam",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/application/app/vo/DependencyVO",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/application/app/vo/JenkinsPluginsVO",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/jenkins/vo/DeviceVo",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/jenkins/vo/GoodsVO",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/jenkins/vo/JobAddVo",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/repository/user/dto/query/RoleQueryDto",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/repository/user/dto/query/UserQueryDto",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/repository/user/dto/result/MenuDTO",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/repository/user/dto/result/RoleResultDto",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/repository/user/dto/result/UserResultDto",

      "methodInfos": null,

      "type": "ADD"

    },

    {

      "classFile": "com/dr/user/service/impl/RoleServiceImpl",

      "methodInfos": [

        {

          "md5": "D2AAADF53B501AE6D2206B2951256329",

          "methodName": "getRoleCodeByUserId",

          "parameters": "Long id"

        },

        {

          "md5": "47405162B3397D02156DE636059049F2",

          "methodName": "getListByPage",

          "parameters": "RoleQueryDto roleQueryDto"

        }

      ],

      "type": "MODIFY"

    },

    {

      "classFile": "com/dr/user/service/impl/UserServiceImpl",

      "methodInfos": [

        {

          "md5": "D41D8CD989ABCDEFFEDCBA98ECF8427E",

          "methodName": "selectListByPage",

          "parameters": "UserQueryDto userQueryDto"

        }

      ],

      "type": "MODIFY"

    }

  ]

}

data部分为差异代码的具体内容

将差异代码传递到jaocco

大家可以参考:jacoco增量代码改造

我们只需要找到Report类,加入可选参数

@Option(name = "--diffCode", usage = "input file for diff", metaVar = "<file>") String diffCode;

这样,我们就可以在jacoco内部接受到传递的参数了,如果report命令加上--diffCode就计算增量,不加则计算全量,不影响正常功能,灵活性高

我们这里改造了analyze方法,将增量代码塞给CoverageBuilder对象,我们需要用时直接去获取

private IBundleCoverage analyze(final ExecutionDataStore data,

      final PrintWriter out) throws IOException {

  CoverageBuilder builder;

  // 如果有增量参数将其设置进去

  if (null != this.diffCode) {

      builder = new CoverageBuilder(this.diffCode);

  } else {

      builder = new CoverageBuilder();

  }

  final Analyzer analyzer = new Analyzer(data, builder);

  for (final File f : classfiles) {

      analyzer.analyzeAll(f);

  }

  printNoMatchWarning(builder.getNoMatchClasses(), out);

  return builder.getBundle(name);

}

差异代码匹配

jacoco采用AMS类去解析class类,我们需要去修改org.jacoco.core包下面的Analyzer类

private void analyzeClass(final byte[] source) {

  final long classId = CRC64.classId(source);

  final ClassReader reader = InstrSupport.classReaderFor(source);

  if ((reader.getAccess() & Opcodes.ACC_MODULE) != 0) {

      return;

  }

  if ((reader.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) {

      return;

  }

  // 字段不为空说明是增量覆盖

  if (null != CoverageBuilder.classInfos

        && !CoverageBuilder.classInfos.isEmpty()) {

      // 如果没有匹配到增量代码就无需解析类

      if (!CodeDiffUtil.checkClassIn(reader.getClassName())) {

        return;

      }

  }

  final ClassVisitor visitor = createAnalyzingVisitor(classId,

        reader.getClassName());

  reader.accept(visitor, 0);

}

主要是判断如果需要的是增量代码覆盖率,则匹配类是否是增量的(这里是jacoco遍历解析每个类的地方)

然后修改ClassProbesAdapter类的visitMethod方法(这个是遍历类里面每个方法的地方)

整个比较的代码逻辑在这里,注释写的你叫详细了

修改完成后,大家只要构建出org.jacoco.cli-0.8.7-SNAPSHOT-nodeps.jar包,然后report时传入增量代码即可

全量报告

增量报告

所遇到问题

差异方法的参数匹配

由于我们使用javaparser解析出的参数格式为String a,int b

而ASM解析出的 为Ljava/lang/String,I;在匹配参数的时候遇到了问题,最终我找到了Type类的方法

Type.getArgumentTypes(desc)

然后

argumentTypes[i].getClassName()

将AmS的参数解析成String,int(做了截取),然后再去匹配,就能正确匹配到参数的格式了

为什么不将整个生成报告做成一个平台

jacoco生成报告的时候,需要传入源码,编译后的class文件,而编译这些东西我们一般都有自己的ci平台去做,我们可以将我们的覆盖率功能集成到我们的devops平台,从那边去获取源码或编译出的class文件,而且可以做业务上的整合,所以没有像supper-jacoco那样做成一个平台

考资料:super-jacoco里面有些bug,使用的时候请注意

jacoco-plus

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容