jacoco增量覆蓋率實踐


能找到這里,說明對jacoco的原理和使用有了一定的了解,而我寫這邊文章主要是網絡上基本沒有完整文檔加代碼的jaocco增量覆蓋說明,所以我想分享些東西讓需要這方面的人快速去實現自己想要的功能,那么如果想實現增量代碼覆蓋率需要做到哪些工作呢?

大家在網絡上找到的實現方式無外乎三種

  1. 獲取到增量代碼,在jacoco進行插樁時判斷是否是增量代碼后再進行插樁,這樣需要兩個步驟,一是獲取增量代碼,二是找到jacoco的插樁邏輯進行修改
  2. 獲取增量代碼,在report階段去判斷方法是否是增量,再去生成報告
  3. 獲取差異代碼,解析生成的report報告,再過濾出差異代碼的報告

首先第一種需要對java字節碼操作比較熟悉,難度較高,我們不談,第三種去解析生成的報告,可能存在誤差

所以我們一般選擇第二種,而網絡上所有的增量實現基本是基於第二種,我們先看看下面的圖

上圖說明了jacoco測試覆蓋率的生成流程,而我們要做的是在report的時候加入我們的邏輯

根據我們的方案,我們需要三個動作

  • 計算出兩個版本的差異代碼(基於git)
  • 將差異代碼在jacoco的report階段傳給jacoco
  • 修改jacoco源碼,生成報告時判斷代碼是否是增量代碼,只有增量代碼才去生成報告

下面我們逐步講解上述步驟

計算差異代碼

計算差異代碼我實現了一個簡單的工程:差異代碼獲取

主要用到了兩個工具類

  1.  
    <dependency>
  2.  
    <groupId>org.eclipse.jgit</groupId>
  3.  
    <artifactId>org.eclipse.jgit</artifactId>
  4.  
    </dependency>
  5.  
     
  6.  
    <!-- https://mvnrepository.com/artifact/com.github.javaparser/javaparser-core -->
  7.  
    <dependency>
  8.  
    <groupId>com.github.javaparser</groupId>
  9.  
    <artifactId>javaparser-core</artifactId>
  10.  
    </dependency>

org.eclipse.jgit主要用於從git獲取代碼,並獲取到存在變更的文件

javaparser-core是一個java解析類,能將class類文件解析成樹狀,方便我們去獲取差異類

  1.  
    /**
  2.  
    * 獲取差異類
  3.  
    *
  4.  
    * @param diffMethodParams
  5.  
    * @return
  6.  
    */
  7.  
    public List<ClassInfoResult> diffMethods(DiffMethodParams diffMethodParams) {
  8.  
    try {
  9.  
    //原有代碼git對象
  10.  
    Git baseGit = cloneRepository(diffMethodParams.getGitUrl(), localBaseRepoDir + diffMethodParams.getBaseVersion(), diffMethodParams.getBaseVersion());
  11.  
    //現有代碼git對象
  12.  
    Git nowGit = cloneRepository(diffMethodParams.getGitUrl(), localBaseRepoDir + diffMethodParams.getNowVersion(), diffMethodParams.getNowVersion());
  13.  
    AbstractTreeIterator baseTree = prepareTreeParser(baseGit.getRepository(), diffMethodParams.getBaseVersion());
  14.  
    AbstractTreeIterator nowTree = prepareTreeParser(nowGit.getRepository(), diffMethodParams.getNowVersion());
  15.  
    //獲取兩個版本之間的差異代碼
  16.  
    List<DiffEntry> diff = nowGit.diff().setOldTree(baseTree).setNewTree(nowTree).setShowNameAndStatusOnly(true).call();
  17.  
    //過濾出有效的差異代碼
  18.  
    Collection<DiffEntry> validDiffList = diff.stream()
  19.  
    //只計算java文件
  20.  
    .filter(e -> e.getNewPath().endsWith( ".java"))
  21.  
    //排除測試文件
  22.  
    .filter(e -> e.getNewPath().contains( "src/main/java"))
  23.  
    //只計算新增和變更文件
  24.  
    .filter(e -> DiffEntry.ChangeType.ADD.equals(e.getChangeType()) || DiffEntry.ChangeType.MODIFY.equals(e.getChangeType()))
  25.  
    .collect(Collectors.toList());
  26.  
    if (CollectionUtils.isEmpty(validDiffList)) {
  27.  
    return null;
  28.  
    }
  29.  
    /**
  30.  
    * 多線程獲取舊代碼和新代碼的差異類及差異方法
  31.  
    */
  32.  
    List<CompletableFuture<ClassInfoResult>> priceFuture = validDiffList.stream().map(item -> getClassMethods(getClassFile(baseGit, item.getNewPath()), getClassFile(nowGit, item.getNewPath()), item)).collect(Collectors.toList());
  33.  
    return priceFuture.stream().map(CompletableFuture::join).filter(Objects::nonNull).collect(Collectors.toList());
  34.  
    } catch (GitAPIException e) {
  35.  
    e.printStackTrace();
  36.  
    }
  37.  
    return null;
  38.  
    }

以上代碼為獲取差異類的核心代碼

  1.  
     
  2.  
    /**
  3.  
    * 獲取類的增量方法
  4.  
    *
  5.  
    * @param oldClassFile 舊類的本地地址
  6.  
    * @param mewClassFile 新類的本地地址
  7.  
    * @param diffEntry 差異類
  8.  
    * @return
  9.  
    */
  10.  
    private CompletableFuture<ClassInfoResult> getClassMethods(String oldClassFile, String mewClassFile, DiffEntry diffEntry) {
  11.  
    //多線程獲取差異方法,此處只要考慮增量代碼太多的情況下,每個類都需要遍歷所有方法,采用多線程方式加快速度
  12.  
    return CompletableFuture.supplyAsync(() -> {
  13.  
    String className = diffEntry.getNewPath().split("\\.")[0].split("src/main/java/")[1];
  14.  
    //新增類直接標記,不用計算方法
  15.  
    if (DiffEntry.ChangeType.ADD.equals(diffEntry.getChangeType())) {
  16.  
    return ClassInfoResult.builder()
  17.  
    .classFile(className)
  18.  
    .type(DiffEntry.ChangeType.ADD.name())
  19.  
    .build();
  20.  
    }
  21.  
    List<MethodInfoResult> diffMethods;
  22.  
    //獲取新類的所有方法
  23.  
    List<MethodInfoResult> newMethodInfoResults = MethodParserUtils.parseMethods(mewClassFile);
  24.  
    //如果新類為空,沒必要比較
  25.  
    if (CollectionUtils.isEmpty(newMethodInfoResults)) {
  26.  
    return null;
  27.  
    }
  28.  
    //獲取舊類的所有方法
  29.  
    List<MethodInfoResult> oldMethodInfoResults = MethodParserUtils.parseMethods(oldClassFile);
  30.  
    //如果舊類為空,新類的方法所有為增量
  31.  
    if (CollectionUtils.isEmpty(oldMethodInfoResults)) {
  32.  
    diffMethods = newMethodInfoResults;
  33.  
    } else { //否則,計算增量方法
  34.  
    List<String> md5s = oldMethodInfoResults.stream().map(MethodInfoResult::getMd5).collect(Collectors.toList());
  35.  
    diffMethods = newMethodInfoResults.stream().filter(m -> !md5s.contains(m.getMd5())).collect(Collectors.toList());
  36.  
    }
  37.  
    //沒有增量方法,過濾掉
  38.  
    if (CollectionUtils.isEmpty(diffMethods)) {
  39.  
    return null;
  40.  
    }
  41.  
    ClassInfoResult result = ClassInfoResult.builder()
  42.  
    .classFile(className)
  43.  
    .methodInfos(diffMethods)
  44.  
    .type(DiffEntry.ChangeType.MODIFY.name())
  45.  
    .build();
  46.  
    return result;
  47.  
    }, executor);
  48.  
    }

以上代碼為獲取差異方法的核心代碼

大家可以下載代碼后運行,下面我們展示下,運行代碼后獲取到的差異代碼內容(參數可以是兩次commitId,也可以是兩個分支,按自己的業務場景來)

  1.  
    {
  2.  
    "code": 10000,
  3.  
    "msg": "業務處理成功",
  4.  
    "data": [
  5.  
    {
  6.  
    "classFile": "com/dr/application/InstallCert",
  7.  
    "methodInfos": null,
  8.  
    "type": "ADD"
  9.  
    },
  10.  
    {
  11.  
    "classFile": "com/dr/application/app/controller/Calculable",
  12.  
    "methodInfos": null,
  13.  
    "type": "ADD"
  14.  
    },
  15.  
    {
  16.  
    "classFile": "com/dr/application/app/controller/JenkinsPluginController",
  17.  
    "methodInfos": null,
  18.  
    "type": "ADD"
  19.  
    },
  20.  
    {
  21.  
    "classFile": "com/dr/application/app/controller/LoginController",
  22.  
    "methodInfos": [
  23.  
    {
  24.  
    "md5": "2C9D2AE2B1864A2FCDDC6D47CEBEBD4C",
  25.  
    "methodName": "captcha",
  26.  
    "parameters": "HttpServletRequest request,HttpServletResponse response"
  27.  
    },
  28.  
    {
  29.  
    "md5": "3D6DFADD2171E893D99D3D6B335B22EA",
  30.  
    "methodName": "login",
  31.  
    "parameters": "@RequestBody LoginUserParam loginUserParam,HttpServletRequest request"
  32.  
    },
  33.  
    {
  34.  
    "md5": "90842DFA5372DCB74335F22098B36A53",
  35.  
    "methodName": "logout",
  36.  
    "parameters": ""
  37.  
    },
  38.  
    {
  39.  
    "md5": "D0B2397D04624D2D60E96AB97F679779",
  40.  
    "methodName": "testInt",
  41.  
    "parameters": "int a,char b"
  42.  
    },
  43.  
    {
  44.  
    "md5": "34219E0141BAB497DCB5FB71BAE1BDAE",
  45.  
    "methodName": "testInt",
  46.  
    "parameters": "String a,int b"
  47.  
    },
  48.  
    {
  49.  
    "md5": "F9BF585A4F6E158CD4475700847336A6",
  50.  
    "methodName": "testInt",
  51.  
    "parameters": "short a,int b"
  52.  
    },
  53.  
    {
  54.  
    "md5": "0F2508A33F719493FFA66C5118B41D77",
  55.  
    "methodName": "testInt",
  56.  
    "parameters": "int[] a"
  57.  
    },
  58.  
    {
  59.  
    "md5": "381C8CBF1F381A58E1E93774AE1AF4EC",
  60.  
    "methodName": "testInt",
  61.  
    "parameters": "AddUserParam param"
  62.  
    },
  63.  
    {
  64.  
    "md5": "64BF62C11839F45030198A8D8D7821C5",
  65.  
    "methodName": "testInt",
  66.  
    "parameters": "T[] a"
  67.  
    },
  68.  
    {
  69.  
    "md5": "D091AB0AD9160407AED4182259200B9B",
  70.  
    "methodName": "testInt",
  71.  
    "parameters": "Calculable calc,int n1,int n2"
  72.  
    },
  73.  
    {
  74.  
    "md5": "693BBA0A8A57F2FD19F61BA06F23365C",
  75.  
    "methodName": "display",
  76.  
    "parameters": ""
  77.  
    },
  78.  
    {
  79.  
    "md5": "F9DFE0E75C78A31AFB6A8FD46BDA2B81",
  80.  
    "methodName": "a",
  81.  
    "parameters": "InnerClass a"
  82.  
    }
  83.  
    ],
  84.  
    "type": "MODIFY"
  85.  
    },
  86.  
    {
  87.  
    "classFile": "com/dr/application/app/controller/RoleController",
  88.  
    "methodInfos": null,
  89.  
    "type": "ADD"
  90.  
    },
  91.  
    {
  92.  
    "classFile": "com/dr/application/app/controller/TestController",
  93.  
    "methodInfos": [
  94.  
    {
  95.  
    "md5": "B1840C873BF0BA74CB6749E1CEE93ED7",
  96.  
    "methodName": "getPom",
  97.  
    "parameters": "HttpServletResponse response"
  98.  
    },
  99.  
    {
  100.  
    "md5": "9CEE68771972EAD613AF237099CD2349",
  101.  
    "methodName": "getDeList",
  102.  
    "parameters": ""
  103.  
    }
  104.  
    ],
  105.  
    "type": "MODIFY"
  106.  
    },
  107.  
    {
  108.  
    "classFile": "com/dr/application/app/controller/UserController",
  109.  
    "methodInfos": [
  110.  
    {
  111.  
    "md5": "7F2AD08CE732ADDFC902C46D238A9EB3",
  112.  
    "methodName": "add",
  113.  
    "parameters": "@RequestBody AddUserParam addUserParam"
  114.  
    },
  115.  
    {
  116.  
    "md5": "D41D8CD98F00B204E9800998ECF8427E",
  117.  
    "methodName": "get",
  118.  
    "parameters": ""
  119.  
    },
  120.  
    {
  121.  
    "md5": "2B35EA4FB5054C6EF13D557C2ACBB581",
  122.  
    "methodName": "list",
  123.  
    "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"
  124.  
    }
  125.  
    ],
  126.  
    "type": "MODIFY"
  127.  
    },
  128.  
    {
  129.  
    "classFile": "com/dr/application/app/controller/view/RoleViewController",
  130.  
    "methodInfos": null,
  131.  
    "type": "ADD"
  132.  
    },
  133.  
    {
  134.  
    "classFile": "com/dr/application/app/controller/view/UserViewController",
  135.  
    "methodInfos": [
  136.  
    {
  137.  
    "md5": "9A1DDA3F41B36026FC2F3ACDAE85C1DB",
  138.  
    "methodName": "user",
  139.  
    "parameters": ""
  140.  
    }
  141.  
    ],
  142.  
    "type": "MODIFY"
  143.  
    },
  144.  
    {
  145.  
    "classFile": "com/dr/application/app/param/AddRoleParam",
  146.  
    "methodInfos": null,
  147.  
    "type": "ADD"
  148.  
    },
  149.  
    {
  150.  
    "classFile": "com/dr/application/app/vo/DependencyVO",
  151.  
    "methodInfos": null,
  152.  
    "type": "ADD"
  153.  
    },
  154.  
    {
  155.  
    "classFile": "com/dr/application/app/vo/JenkinsPluginsVO",
  156.  
    "methodInfos": null,
  157.  
    "type": "ADD"
  158.  
    },
  159.  
    {
  160.  
    "classFile": "com/dr/jenkins/vo/DeviceVo",
  161.  
    "methodInfos": null,
  162.  
    "type": "ADD"
  163.  
    },
  164.  
    {
  165.  
    "classFile": "com/dr/jenkins/vo/GoodsVO",
  166.  
    "methodInfos": null,
  167.  
    "type": "ADD"
  168.  
    },
  169.  
    {
  170.  
    "classFile": "com/dr/jenkins/vo/JobAddVo",
  171.  
    "methodInfos": null,
  172.  
    "type": "ADD"
  173.  
    },
  174.  
    {
  175.  
    "classFile": "com/dr/repository/user/dto/query/RoleQueryDto",
  176.  
    "methodInfos": null,
  177.  
    "type": "ADD"
  178.  
    },
  179.  
    {
  180.  
    "classFile": "com/dr/repository/user/dto/query/UserQueryDto",
  181.  
    "methodInfos": null,
  182.  
    "type": "ADD"
  183.  
    },
  184.  
    {
  185.  
    "classFile": "com/dr/repository/user/dto/result/MenuDTO",
  186.  
    "methodInfos": null,
  187.  
    "type": "ADD"
  188.  
    },
  189.  
    {
  190.  
    "classFile": "com/dr/repository/user/dto/result/RoleResultDto",
  191.  
    "methodInfos": null,
  192.  
    "type": "ADD"
  193.  
    },
  194.  
    {
  195.  
    "classFile": "com/dr/repository/user/dto/result/UserResultDto",
  196.  
    "methodInfos": null,
  197.  
    "type": "ADD"
  198.  
    },
  199.  
    {
  200.  
    "classFile": "com/dr/user/service/impl/RoleServiceImpl",
  201.  
    "methodInfos": [
  202.  
    {
  203.  
    "md5": "D2AAADF53B501AE6D2206B2951256329",
  204.  
    "methodName": "getRoleCodeByUserId",
  205.  
    "parameters": "Long id"
  206.  
    },
  207.  
    {
  208.  
    "md5": "47405162B3397D02156DE636059049F2",
  209.  
    "methodName": "getListByPage",
  210.  
    "parameters": "RoleQueryDto roleQueryDto"
  211.  
    }
  212.  
    ],
  213.  
    "type": "MODIFY"
  214.  
    },
  215.  
    {
  216.  
    "classFile": "com/dr/user/service/impl/UserServiceImpl",
  217.  
    "methodInfos": [
  218.  
    {
  219.  
    "md5": "D41D8CD989ABCDEFFEDCBA98ECF8427E",
  220.  
    "methodName": "selectListByPage",
  221.  
    "parameters": "UserQueryDto userQueryDto"
  222.  
    }
  223.  
    ],
  224.  
    "type": "MODIFY"
  225.  
    }
  226.  
    ]
  227.  
    }

data部分為差異代碼的具體內容

將差異代碼傳遞到jaocco

大家可以參考:jacoco增量代碼改造

我們只需要找到Report類,加入可選參數

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

這樣,我們就可以在jacoco內部接受到傳遞的參數了,如果report命令加上--diffCode就計算增量,不加則計算全量,不影響正常功能,靈活性高

我們這里改造了analyze方法,將增量代碼塞給CoverageBuilder對象,我們需要用時直接去獲取

  1.  
    private IBundleCoverage analyze(final ExecutionDataStore data,
  2.  
    final PrintWriter out) throws IOException {
  3.  
    CoverageBuilder builder;
  4.  
    // 如果有增量參數將其設置進去
  5.  
    if (null != this.diffCode) {
  6.  
    builder = new CoverageBuilder( this.diffCode);
  7.  
    } else {
  8.  
    builder = new CoverageBuilder();
  9.  
    }
  10.  
    final Analyzer analyzer = new Analyzer(data, builder);
  11.  
    for (final File f : classfiles) {
  12.  
    analyzer.analyzeAll(f);
  13.  
    }
  14.  
    printNoMatchWarning(builder.getNoMatchClasses(), out);
  15.  
    return builder.getBundle(name);
  16.  
    }

差異代碼匹配

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

  1.  
    private void analyzeClass(final byte[] source) {
  2.  
    final long classId = CRC64.classId(source);
  3.  
    final ClassReader reader = InstrSupport.classReaderFor(source);
  4.  
    if ((reader.getAccess() & Opcodes.ACC_MODULE) != 0) {
  5.  
    return;
  6.  
    }
  7.  
    if ((reader.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) {
  8.  
    return;
  9.  
    }
  10.  
    // 字段不為空說明是增量覆蓋
  11.  
    if (null != CoverageBuilder.classInfos
  12.  
    && !CoverageBuilder.classInfos.isEmpty()) {
  13.  
    // 如果沒有匹配到增量代碼就無需解析類
  14.  
    if (!CodeDiffUtil.checkClassIn(reader.getClassName())) {
  15.  
    return;
  16.  
    }
  17.  
    }
  18.  
    final ClassVisitor visitor = createAnalyzingVisitor(classId,
  19.  
    reader.getClassName());
  20.  
    reader.accept(visitor, 0);
  21.  
     
  22.  
    }

主要是判斷如果需要的是增量代碼覆蓋率,則匹配類是否是增量的(這里是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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM