1. 程式人生 > 實用技巧 >jacoco-實戰篇-增量覆蓋率

jacoco-實戰篇-增量覆蓋率

獲取增量覆蓋率報告的改動原始碼的步驟:

第一步:拉取jacoco原始碼,原始碼下載地址:點我

第二步:修改org.jacoco.core專案中

  1、增加專案依賴

    修改pom.xml檔案,增加依賴如下:

  <!--java檔案編譯class-->
    <dependency>
      <groupId>org.eclipse.jdt</groupId>
      <artifactId>org.eclipse.jdt.core</artifactId>
      <version>3.19.0</version>
    </dependency>
<!--git操作--> <dependency> <groupId>org.eclipse.jgit</groupId> <artifactId>org.eclipse.jgit</artifactId> <version>5.5.0.201909110433-r</version> </dependency>

  2、修改專案中org.jacoco.core.analysis包下的CoverageBuilder類:

  public static List<ClassInfo> classInfos;    // 新增的成員變數

    /**
     * 分支與master對比
     * @param gitPath local gitPath
     * @param branchName new test branch name
     */
    public CoverageBuilder(String gitPath, String branchName) {
        this.classes = new HashMap<String, IClassCoverage>();
        this.sourcefiles = new HashMap<String, ISourceFileCoverage>();
        classInfos = CodeDiff.diffBranchToBranch(gitPath, branchName,CodeDiff.MASTER);
    }

    /**
     * 分支與分支之間對比
     * @param gitPath local gitPath
     * @param newBranchName newBranchName
     * @param oldBranchName oldBranchName
     */
    public CoverageBuilder(String gitPath, String newBranchName, String oldBranchName) {
        this.classes = new HashMap<String, IClassCoverage>();
        this.sourcefiles = new HashMap<String, ISourceFileCoverage>();
        classInfos = CodeDiff.diffBranchToBranch(gitPath, newBranchName, oldBranchName);
    }

    /**
     * tag與tag之間對比
     * @param gitPath local gitPath
     * @param branchName develop branchName
     * @param newTag new Tag
     * @param oldTag old Tag
     */
    public CoverageBuilder(String gitPath, String branchName, String newTag, String oldTag) {
        this.classes = new HashMap<String, IClassCoverage>();
        this.sourcefiles = new HashMap<String, ISourceFileCoverage>();
        classInfos = CodeDiff.diffTagToTag(gitPath,branchName, newTag, oldTag);
    }

第三步:新增檔案

  在org.jacoco.core專案org.jacoco.core.internal包下新增diff包(目錄),然後在diff包下新增如下檔案:

  1、新增ASTGenerator類

package org.jacoco.core.internal.diff;

import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.*;
import sun.misc.BASE64Encoder;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * AST編譯java原始檔
 */
public class ASTGenerator {
    private String javaText;
    private CompilationUnit compilationUnit;

    public ASTGenerator(String javaText) {
        this.javaText = javaText;
        this.initCompilationUnit();
    }

    /**
     * 獲取AST編譯單元,首次載入很慢
     */
    private void initCompilationUnit() {
        //  AST編譯
        final ASTParser astParser = ASTParser.newParser(8);
        final Map<String, String> options = JavaCore.getOptions();
        JavaCore.setComplianceOptions(JavaCore.VERSION_1_8, options);
        astParser.setCompilerOptions(options);
        astParser.setKind(ASTParser.K_COMPILATION_UNIT);
        astParser.setResolveBindings(true);
        astParser.setBindingsRecovery(true);
        astParser.setStatementsRecovery(true);
        astParser.setSource(javaText.toCharArray());
        compilationUnit = (CompilationUnit) astParser.createAST(null);
    }

    /**
     * 獲取java類包名
     */
    public String getPackageName() {
        if (compilationUnit == null) {
            return "";
        }
        PackageDeclaration packageDeclaration = compilationUnit.getPackage();
        if (packageDeclaration == null){
            return "";
        }
        String packageName = packageDeclaration.getName().toString();
        return packageName;
    }

    /**
     * 獲取普通類單元
     */
    public TypeDeclaration getJavaClass() {
        if (compilationUnit == null) {
            return null;
        }
        TypeDeclaration typeDeclaration = null;
        final List<?> types = compilationUnit.types();
        for (final Object type : types) {
            if (type instanceof TypeDeclaration) {
                typeDeclaration = (TypeDeclaration) type;
                break;
            }
        }
        return typeDeclaration;
    }

    /**
     * 獲取java類中所有方法
     * @return 類中所有方法
     */
    public MethodDeclaration[] getMethods() {
        TypeDeclaration typeDec = getJavaClass();
        if (typeDec == null) {
            return new MethodDeclaration[]{};
        }
        MethodDeclaration[] methodDec = typeDec.getMethods();
        return methodDec;
    }

    /**
     * 獲取新增類中的所有方法資訊
     */
    public List<MethodInfo> getMethodInfoList() {
        MethodDeclaration[] methodDeclarations = getMethods();
        List<MethodInfo> methodInfoList = new ArrayList<MethodInfo>();
        for (MethodDeclaration method: methodDeclarations) {
            MethodInfo methodInfo = new MethodInfo();
            setMethodInfo(methodInfo, method);
            methodInfoList.add(methodInfo);
        }
        return methodInfoList;
    }

    /**
     * 獲取修改型別的類的資訊以及其中的所有方法,排除介面類
     */
    public ClassInfo getClassInfo(List<MethodInfo> methodInfos, List<int[]> addLines, List<int[]> delLines) {
        TypeDeclaration typeDec = getJavaClass();
        if (typeDec == null || typeDec.isInterface()) {
            return null;
        }
        ClassInfo classInfo = new ClassInfo();
        classInfo.setClassName(getJavaClass().getName().toString());
        classInfo.setPackages(getPackageName());
        classInfo.setMethodInfos(methodInfos);
        classInfo.setAddLines(addLines);
        classInfo.setDelLines(delLines);
        classInfo.setType("REPLACE");
        return classInfo;
    }

    /**
     * 獲取新增型別的類的資訊以及其中的所有方法,排除介面類
     */
    public ClassInfo getClassInfo() {
        TypeDeclaration typeDec = getJavaClass();
        if (typeDec == null || typeDec.isInterface()) {
            return null;
        }
        MethodDeclaration[] methodDeclarations = getMethods();
        ClassInfo classInfo = new ClassInfo();
        classInfo.setClassName(getJavaClass().getName().toString());
        classInfo.setPackages(getPackageName());
        classInfo.setType("ADD");
        List<MethodInfo> methodInfoList = new ArrayList<MethodInfo>();
        for (MethodDeclaration method: methodDeclarations) {
            MethodInfo methodInfo = new MethodInfo();
            setMethodInfo(methodInfo, method);
            methodInfoList.add(methodInfo);
        }
        classInfo.setMethodInfos(methodInfoList);
        return classInfo;
    }

    /**
     * 獲取修改中的方法
     */
    public MethodInfo getMethodInfo(MethodDeclaration methodDeclaration) {
        MethodInfo methodInfo = new MethodInfo();
        setMethodInfo(methodInfo, methodDeclaration);
        return methodInfo;
    }

    private void setMethodInfo(MethodInfo methodInfo,MethodDeclaration methodDeclaration) {
        methodInfo.setMd5(MD5Encode(methodDeclaration.toString()));
        methodInfo.setMethodName(methodDeclaration.getName().toString());
        methodInfo.setParameters(methodDeclaration.parameters().toString());
    }

    /**
     * 計算方法的MD5的值
     */
    public static String MD5Encode(String s) {
        String MD5String = "";
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            BASE64Encoder base64en = new BASE64Encoder();
            MD5String = base64en.encode(md5.digest(s.getBytes("utf-8")));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return MD5String;
    }

    /**
     * 判斷方法是否存在
     * @param method        新分支的方法
     * @param methodsMap    master分支的方法
     * @return
     */
    public static boolean isMethodExist(final MethodDeclaration method, final Map<String, MethodDeclaration> methodsMap) {
        // 方法名+引數一致才一致
        if (!methodsMap.containsKey(method.getName().toString() + method.parameters().toString())) {
            return false;
        }
        return true;
    }

    /**
     * 判斷方法是否一致
     */
    public static boolean isMethodTheSame(final MethodDeclaration method1,final MethodDeclaration method2) {
        if (MD5Encode(method1.toString()).equals(MD5Encode(method2.toString()))) {
            return true;
        }
        return false;
    }
}

  2、新增ClassInfo類:

package org.jacoco.core.internal.diff;

import java.util.List;

public class ClassInfo {
    /**
     * java檔案
     */
    private String classFile;
    /**
     * 類名
     */
    private String className;
    /**
     * 包名
     */
    private String packages;

    /**
     * 類中的方法
     */
    private List<MethodInfo> methodInfos;

    /**
     * 新增的行數
     */
    private List<int[]> addLines;

    /**
     * 刪除的行數
     */
    private List<int[]> delLines;

    /**
     * 修改型別
     */
    private String type;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public List<int[]> getAddLines() {
        return addLines;
    }

    public void setAddLines(List<int[]> addLines) {
        this.addLines = addLines;
    }

    public List<int[]> getDelLines() {
        return delLines;
    }

    public void setDelLines(List<int[]> delLines) {
        this.delLines = delLines;
    }

    public String getClassFile() {
        return classFile;
    }

    public void setClassFile(String classFile) {
        this.classFile = classFile;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getPackages() {
        return packages;
    }

    public void setPackages(String packages) {
        this.packages = packages;
    }

    public List<MethodInfo> getMethodInfos() {
        return methodInfos;
    }

    public void setMethodInfos(List<MethodInfo> methodInfos) {
        this.methodInfos = methodInfos;
    }
}

  3、新增CodeDiff類:

package org.jacoco.core.internal.diff;

import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.*;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;

/**
 * 程式碼版本比較
 */
public class CodeDiff {
    public final static String REF_HEADS = "refs/heads/";
    public final static  String MASTER = "master";

    /**
     * 分支和分支之間的比較
     * @param gitPath           git路徑
     * @param newBranchName     新分支名稱
     * @param oldBranchName     舊分支名稱
     * @return
     */
    public static List<ClassInfo> diffBranchToBranch(String gitPath, String newBranchName, String oldBranchName) {
        List<ClassInfo> classInfos = diffMethods(gitPath, newBranchName, oldBranchName);
        return classInfos;
    }
    private static List<ClassInfo> diffMethods(String gitPath, String newBranchName, String oldBranchName) {
        try {
            //  獲取本地分支
            GitAdapter gitAdapter = new GitAdapter(gitPath);
            Git git = gitAdapter.getGit();
            Ref localBranchRef = gitAdapter.getRepository().exactRef(REF_HEADS + newBranchName);
            Ref localMasterRef = gitAdapter.getRepository().exactRef(REF_HEADS + oldBranchName);
            //  更新本地分支
            gitAdapter.checkOutAndPull(localMasterRef, oldBranchName);
            gitAdapter.checkOutAndPull(localBranchRef, newBranchName);
            //  獲取分支資訊
            AbstractTreeIterator newTreeParser = gitAdapter.prepareTreeParser(localBranchRef);
            AbstractTreeIterator oldTreeParser = gitAdapter.prepareTreeParser(localMasterRef);
            //  對比差異
            List<DiffEntry> diffs = git.diff().setOldTree(oldTreeParser).setNewTree(newTreeParser).setShowNameAndStatusOnly(true).call();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            DiffFormatter df = new DiffFormatter(out);
            //設定比較器為忽略空白字元對比(Ignores all whitespace)
            df.setDiffComparator(RawTextComparator.WS_IGNORE_ALL);
            df.setRepository(git.getRepository());
            List<ClassInfo> allClassInfos = batchPrepareDiffMethod(gitAdapter, newBranchName, oldBranchName, df, diffs);
            return allClassInfos;
        }catch (Exception e) {
            e.printStackTrace();
        }
        return  new ArrayList<ClassInfo>();
    }

    /**
     * 單分支Tag版本之間的比較
     * @param gitPath    本地git程式碼倉庫路徑
     * @param newTag     新Tag版本
     * @param oldTag     舊Tag版本
     * @return
     */
    public static List<ClassInfo> diffTagToTag(String gitPath, String branchName, String newTag, String oldTag) {
        if(StringUtils.isEmptyOrNull(gitPath) || StringUtils.isEmptyOrNull(branchName)  || StringUtils.isEmptyOrNull(newTag)  || StringUtils.isEmptyOrNull(oldTag) ){
            throw new IllegalArgumentException("Parameter(local gitPath,develop branchName,new Tag,old Tag) can't be empty or null !");
        }else if(newTag.equals(oldTag)){
            throw new IllegalArgumentException("Parameter new Tag and old Tag can't be the same");
        }
        File gitPathDir = new File(gitPath);
        if(!gitPathDir.exists()){
            throw new IllegalArgumentException("Parameter local gitPath is not exit !");
        }

        List<ClassInfo> classInfos = diffTagMethods(gitPath,branchName, newTag, oldTag);
        return classInfos;
    }
    private static List<ClassInfo> diffTagMethods(String gitPath,String branchName, String newTag, String oldTag) {
        try {
            //  init local repository
            GitAdapter gitAdapter = new GitAdapter(gitPath);
            Git git = gitAdapter.getGit();
            Repository repo =  gitAdapter.getRepository();
            Ref localBranchRef = repo.exactRef(REF_HEADS + branchName);

            //  update local repository
            gitAdapter.checkOutAndPull(localBranchRef, branchName);

            ObjectId head = repo.resolve(newTag+"^{tree}");
            ObjectId previousHead = repo.resolve(oldTag+"^{tree}");

            // Instanciate a reader to read the data from the Git database
            ObjectReader reader = repo.newObjectReader();
            // Create the tree iterator for each commit
            CanonicalTreeParser oldTreeIter = new CanonicalTreeParser();
            oldTreeIter.reset(reader, previousHead);
            CanonicalTreeParser newTreeIter = new CanonicalTreeParser();
            newTreeIter.reset(reader, head);

            //  對比差異
            List<DiffEntry> diffs = git.diff().setOldTree(oldTreeIter).setNewTree(newTreeIter).setShowNameAndStatusOnly(true).call();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            DiffFormatter df = new DiffFormatter(out);
            //設定比較器為忽略空白字元對比(Ignores all whitespace)
            df.setDiffComparator(RawTextComparator.WS_IGNORE_ALL);
            df.setRepository(repo);
            List<ClassInfo> allClassInfos = batchPrepareDiffMethodForTag(gitAdapter, newTag,oldTag, df, diffs);
            return allClassInfos;
        }catch (Exception e) {
            e.printStackTrace();
        }
        return  new ArrayList<ClassInfo>();
    }
    /**
     * 多執行緒執行對比
     */
    private static List<ClassInfo> batchPrepareDiffMethodForTag(final GitAdapter gitAdapter, final String newTag, final String oldTag, final DiffFormatter df, List<DiffEntry> diffs) {
        int threadSize = 100;
        int dataSize = diffs.size();
        int threadNum = dataSize / threadSize + 1;
        boolean special = dataSize % threadSize == 0;
        ExecutorService executorService = Executors.newFixedThreadPool(threadNum);

        List<Callable<List<ClassInfo>>> tasks = new ArrayList<Callable<List<ClassInfo>>>();
        Callable<List<ClassInfo>> task = null;
        List<DiffEntry> cutList = null;
        //  分解每條執行緒的資料
        for (int i = 0; i < threadNum; i++) {
            if (i == threadNum - 1) {
                if (special) {
                    break;
                }
                cutList = diffs.subList(threadSize * i, dataSize);
            } else {
                cutList = diffs.subList(threadSize * i, threadSize * (i + 1));
            }
            final List<DiffEntry> diffEntryList = cutList;
            task = new Callable<List<ClassInfo>>() {
                public List<ClassInfo> call() throws Exception {
                    List<ClassInfo> allList = new ArrayList<ClassInfo>();
                    for (DiffEntry diffEntry : diffEntryList) {
                        ClassInfo classInfo = prepareDiffMethodForTag(gitAdapter, newTag, oldTag, df, diffEntry);
                        if (classInfo != null) {
                            allList.add(classInfo);
                        }
                    }
                    return allList;
                }
            };
            // 這裡提交的任務容器列表和返回的Future列表存在順序對應的關係
            tasks.add(task);
        }
        List<ClassInfo> allClassInfoList = new ArrayList<ClassInfo>();
        try {
            List<Future<List<ClassInfo>>> results = executorService.invokeAll(tasks);
            //結果彙總
            for (Future<List<ClassInfo>> future : results ) {
                allClassInfoList.addAll(future.get());
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            // 關閉執行緒池
            executorService.shutdown();
        }
        return allClassInfoList;
    }

    /**
     * 單個差異檔案對比
     */
    private synchronized static ClassInfo prepareDiffMethodForTag(GitAdapter gitAdapter, String newTag, String oldTag, DiffFormatter df, DiffEntry diffEntry) {
        List<MethodInfo> methodInfoList = new ArrayList<MethodInfo>();
        try {
            String newJavaPath = diffEntry.getNewPath();
            //  排除測試類
            if (newJavaPath.contains("/src/test/java/")) {
                return null;
            }
            //  非java檔案 和 刪除型別不記錄
            if (!newJavaPath.endsWith(".java") || diffEntry.getChangeType() == DiffEntry.ChangeType.DELETE){
                return null;
            }
            String newClassContent = gitAdapter.getTagRevisionSpecificFileContent(newTag,newJavaPath);
            ASTGenerator newAstGenerator = new ASTGenerator(newClassContent);
            /*  新增型別   */
            if (diffEntry.getChangeType() == DiffEntry.ChangeType.ADD) {
                return newAstGenerator.getClassInfo();
            }
            /*  修改型別  */
            //  獲取檔案差異位置,從而統計差異的行數,如增加行數,減少行數
            FileHeader fileHeader = df.toFileHeader(diffEntry);
            List<int[]> addLines = new ArrayList<int[]>();
            List<int[]> delLines = new ArrayList<int[]>();
            EditList editList = fileHeader.toEditList();
            for(Edit edit : editList){
                if (edit.getLengthA() > 0) {
                    delLines.add(new int[]{edit.getBeginA(), edit.getEndA()});
                }
                if (edit.getLengthB() > 0 ) {
                    addLines.add(new int[]{edit.getBeginB(), edit.getEndB()});
                }
            }
            String oldJavaPath = diffEntry.getOldPath();
            String oldClassContent = gitAdapter.getTagRevisionSpecificFileContent(oldTag,oldJavaPath);
            ASTGenerator oldAstGenerator = new ASTGenerator(oldClassContent);
            MethodDeclaration[] newMethods = newAstGenerator.getMethods();
            MethodDeclaration[] oldMethods = oldAstGenerator.getMethods();
            Map<String, MethodDeclaration> methodsMap = new HashMap<String, MethodDeclaration>();
            for (int i = 0; i < oldMethods.length; i++) {
                methodsMap.put(oldMethods[i].getName().toString()+ oldMethods[i].parameters().toString(), oldMethods[i]);
            }
            for (final MethodDeclaration method : newMethods) {
                // 如果方法名是新增的,則直接將方法加入List
                if (!ASTGenerator.isMethodExist(method, methodsMap)) {
                    MethodInfo methodInfo = newAstGenerator.getMethodInfo(method);
                    methodInfoList.add(methodInfo);
                    continue;
                }
                // 如果兩個版本都有這個方法,則根據MD5判斷方法是否一致
                if (!ASTGenerator.isMethodTheSame(method, methodsMap.get(method.getName().toString()+ method.parameters().toString()))) {
                    MethodInfo methodInfo =  newAstGenerator.getMethodInfo(method);
                    methodInfoList.add(methodInfo);
                }
            }
            return newAstGenerator.getClassInfo(methodInfoList, addLines, delLines);
        }catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 多執行緒執行對比
     */
    private static List<ClassInfo> batchPrepareDiffMethod(final GitAdapter gitAdapter, final String branchName, final String oldBranchName, final DiffFormatter df, List<DiffEntry> diffs) {
        int threadSize = 100;
        int dataSize = diffs.size();
        int threadNum = dataSize / threadSize + 1;
        boolean special = dataSize % threadSize == 0;
        ExecutorService executorService = Executors.newFixedThreadPool(threadNum);

        List<Callable<List<ClassInfo>>> tasks = new ArrayList<Callable<List<ClassInfo>>>();
        Callable<List<ClassInfo>> task = null;
        List<DiffEntry> cutList = null;
        //  分解每條執行緒的資料
        for (int i = 0; i < threadNum; i++) {
            if (i == threadNum - 1) {
                if (special) {
                    break;
                }
                cutList = diffs.subList(threadSize * i, dataSize);
            } else {
                cutList = diffs.subList(threadSize * i, threadSize * (i + 1));
            }
            final List<DiffEntry> diffEntryList = cutList;
            task = new Callable<List<ClassInfo>>() {
                public List<ClassInfo> call() throws Exception {
                    List<ClassInfo> allList = new ArrayList<ClassInfo>();
                    for (DiffEntry diffEntry : diffEntryList) {
                        ClassInfo classInfo = prepareDiffMethod(gitAdapter, branchName, oldBranchName, df, diffEntry);
                        if (classInfo != null) {
                            allList.add(classInfo);
                        }
                    }
                    return allList;
                }
            };
            // 這裡提交的任務容器列表和返回的Future列表存在順序對應的關係
            tasks.add(task);
        }
        List<ClassInfo> allClassInfoList = new ArrayList<ClassInfo>();
        try {
            List<Future<List<ClassInfo>>> results = executorService.invokeAll(tasks);
            //結果彙總
            for (Future<List<ClassInfo>> future : results ) {
                allClassInfoList.addAll(future.get());
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            // 關閉執行緒池
            executorService.shutdown();
        }
        return allClassInfoList;
    }

    /**
     * 單個差異檔案對比
     */
    private synchronized static ClassInfo prepareDiffMethod(GitAdapter gitAdapter, String branchName, String oldBranchName, DiffFormatter df, DiffEntry diffEntry) {
        List<MethodInfo> methodInfoList = new ArrayList<MethodInfo>();
        try {
            String newJavaPath = diffEntry.getNewPath();
            //  排除測試類
            if (newJavaPath.contains("/src/test/java/")) {
                return null;
            }
            //  非java檔案 和 刪除型別不記錄
            if (!newJavaPath.endsWith(".java") || diffEntry.getChangeType() == DiffEntry.ChangeType.DELETE){
                return null;
            }
            String newClassContent = gitAdapter.getBranchSpecificFileContent(branchName,newJavaPath);
            ASTGenerator newAstGenerator = new ASTGenerator(newClassContent);
            /*  新增型別   */
            if (diffEntry.getChangeType() == DiffEntry.ChangeType.ADD) {
                return newAstGenerator.getClassInfo();
            }
            /*  修改型別  */
            //  獲取檔案差異位置,從而統計差異的行數,如增加行數,減少行數
            FileHeader fileHeader = df.toFileHeader(diffEntry);
            List<int[]> addLines = new ArrayList<int[]>();
            List<int[]> delLines = new ArrayList<int[]>();
            EditList editList = fileHeader.toEditList();
            for(Edit edit : editList){
                if (edit.getLengthA() > 0) {
                    delLines.add(new int[]{edit.getBeginA(), edit.getEndA()});
                }
                if (edit.getLengthB() > 0 ) {
                    addLines.add(new int[]{edit.getBeginB(), edit.getEndB()});
                }
            }
            String oldJavaPath = diffEntry.getOldPath();
            String oldClassContent = gitAdapter.getBranchSpecificFileContent(oldBranchName,oldJavaPath);
            ASTGenerator oldAstGenerator = new ASTGenerator(oldClassContent);
            MethodDeclaration[] newMethods = newAstGenerator.getMethods();
            MethodDeclaration[] oldMethods = oldAstGenerator.getMethods();
            Map<String, MethodDeclaration> methodsMap = new HashMap<String, MethodDeclaration>();
            for (int i = 0; i < oldMethods.length; i++) {
                methodsMap.put(oldMethods[i].getName().toString()+ oldMethods[i].parameters().toString(), oldMethods[i]);
            }
            for (final MethodDeclaration method : newMethods) {
                // 如果方法名是新增的,則直接將方法加入List
                if (!ASTGenerator.isMethodExist(method, methodsMap)) {
                    MethodInfo methodInfo = newAstGenerator.getMethodInfo(method);
                    methodInfoList.add(methodInfo);
                    continue;
                }
                // 如果兩個版本都有這個方法,則根據MD5判斷方法是否一致
                if (!ASTGenerator.isMethodTheSame(method, methodsMap.get(method.getName().toString()+ method.parameters().toString()))) {
                    MethodInfo methodInfo =  newAstGenerator.getMethodInfo(method);
                    methodInfoList.add(methodInfo);
                }
            }
            return newAstGenerator.getClassInfo(methodInfoList, addLines, delLines);
        }catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

  4、新增GitAdapter類:

package org.jacoco.core.internal.diff;

import org.eclipse.jgit.api.CreateBranchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.*;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;

import java.io.*;
import java.util.*;

/**
 * Git操作類
 */
public class GitAdapter {
    private Git git;
    private Repository repository;
    private String gitFilePath;

    //  Git授權
    private static UsernamePasswordCredentialsProvider usernamePasswordCredentialsProvider;

    public GitAdapter(String gitFilePath) {
        this.gitFilePath = gitFilePath;
        this.initGit(gitFilePath);
    }
    private void initGit(String gitFilePath) {
        try {
            git = Git.open(new File(gitFilePath));
            repository = git.getRepository();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String getGitFilePath() {
        return gitFilePath;
    }

    public Git getGit() {
        return git;
    }

    public Repository getRepository() {
        return repository;
    }

    /**
     * git授權。需要設定擁有所有許可權的使用者
     * @param username  git使用者名稱
     * @param password  git使用者密碼
     */
    public static void setCredentialsProvider(String username, String password) {
        if(usernamePasswordCredentialsProvider == null || !usernamePasswordCredentialsProvider.isInteractive()){
            usernamePasswordCredentialsProvider = new UsernamePasswordCredentialsProvider(username,password);
        }
    }

    /**
     * 獲取指定分支的指定檔案內容
     * @param branchName        分支名稱
     * @param javaPath          檔案路徑
     * @return  java類
     * @throws IOException
     */
    public String getBranchSpecificFileContent(String branchName, String javaPath) throws IOException {
        Ref branch = repository.exactRef("refs/heads/" + branchName);
        ObjectId objId = branch.getObjectId();
        RevWalk walk = new RevWalk(repository);
        RevTree tree = walk.parseTree(objId);
        return  getFileContent(javaPath,tree,walk);
    }

    /**
     * 獲取指定分支指定Tag版本的指定檔案內容
     * @param tagRevision       Tag版本
     * @param javaPath          件路徑
     * @return  java類
     * @throws IOException
     */
    public String getTagRevisionSpecificFileContent(String tagRevision, String javaPath) throws IOException {
        ObjectId objId = repository.resolve(tagRevision);
        RevWalk walk = new RevWalk(repository);
        RevCommit revCommit = walk.parseCommit(objId);
        RevTree tree = revCommit.getTree();
        return  getFileContent(javaPath,tree,walk);
    }
    
    /**
     * 獲取指定分支指定的指定檔案內容
     * @param javaPath      件路徑
     * @param tree          git RevTree
     * @param walk          git RevWalk
     * @return  java類
     * @throws IOException
     */
    private String getFileContent(String javaPath,RevTree tree,RevWalk walk) throws IOException {
        TreeWalk treeWalk = TreeWalk.forPath(repository, javaPath, tree);
        ObjectId blobId = treeWalk.getObjectId(0);
        ObjectLoader loader = repository.open(blobId);
        byte[] bytes = loader.getBytes();
        walk.dispose();
        return new String(bytes);
    }

    /**
     * 分析分支樹結構資訊
     * @param localRef      本地分支
     * @return
     * @throws IOException
     */
    public AbstractTreeIterator prepareTreeParser(Ref localRef) throws IOException {
        RevWalk walk = new RevWalk(repository);
        RevCommit commit = walk.parseCommit(localRef.getObjectId());
        RevTree tree = walk.parseTree(commit.getTree().getId());
        CanonicalTreeParser treeParser = new CanonicalTreeParser();
        ObjectReader reader = repository.newObjectReader();
        treeParser.reset(reader, tree.getId());
        walk.dispose();
        return treeParser;
    }
    /**
     * 切換分支
     * @param branchName    分支名稱
     * @throws GitAPIException GitAPIException
     */
    public void checkOut(String branchName) throws GitAPIException {
        //  切換分支
        git.checkout().setCreateBranch(false).setName(branchName).call();
    }

    /**
     * 更新分支程式碼
     * @param localRef      本地分支
     * @param branchName    分支名稱
     * @throws GitAPIException GitAPIException
     */
    public void checkOutAndPull(Ref localRef, String branchName) throws GitAPIException {
        boolean isCreateBranch = localRef == null;
        if (!isCreateBranch && checkBranchNewVersion(localRef)) {
            return;
        }
        //  切換分支
        git.checkout().setCreateBranch(isCreateBranch).setName(branchName).setStartPoint("origin/" + branchName).setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.SET_UPSTREAM).call();
        //  拉取最新程式碼
        git.pull().setCredentialsProvider(usernamePasswordCredentialsProvider).call();
    }

    /**
     * 判斷本地分支是否是最新版本。目前不考慮分支在遠端倉庫不存在,本地存在
     * @param localRef  本地分支
     * @return  boolean
     * @throws GitAPIException GitAPIException
     */
    private boolean checkBranchNewVersion(Ref localRef) throws GitAPIException {
        String localRefName = localRef.getName();
        String localRefObjectId = localRef.getObjectId().getName();
        //  獲取遠端所有分支
        Collection<Ref> remoteRefs = git.lsRemote().setCredentialsProvider(usernamePasswordCredentialsProvider).setHeads(true).call();
        for (Ref remoteRef : remoteRefs) {
            String remoteRefName = remoteRef.getName();
            String remoteRefObjectId = remoteRef.getObjectId().getName();
            if (remoteRefName.equals(localRefName)) {
                if (remoteRefObjectId.equals(localRefObjectId)) {
                    return true;
                }
                return false;
            }
        }
        return false;
    }
}

  5、新增MethodInfo類:

package org.jacoco.core.internal.diff;

public class MethodInfo {
    /**
     * 方法的md5
     */
    public String md5;
    /**
     * 方法名
     */
    public String methodName;
    /**
     * 方法引數
     */
    public String parameters;

    public String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public String getParameters() {
        return parameters;
    }

    public void setParameters(String parameters) {
        this.parameters = parameters;
    }
}

第四步:修改org.jacoco.core.internal.flow包下的ClassProbesAdapter類:

  1、修改程式碼第66行visitMethod方法:

  @Override
    public final MethodVisitor visitMethod(final int access, final String name,
            final String desc, final String signature, final String[] exceptions) {
        final MethodProbesVisitor methodProbes;
        final MethodProbesVisitor mv = cv.visitMethod(access, name, desc,
                signature, exceptions);
        //    增量計算覆蓋率
        if (mv !=null && isContainsMethod(name, CoverageBuilder.classInfos)) {
            methodProbes = mv;
        } else {
            // We need to visit the method in any case, otherwise probe ids
            // are not reproducible
            methodProbes = EMPTY_METHOD_PROBES_VISITOR;
        }
        return new MethodSanitizer(null, access, name, desc, signature,
                exceptions) {

            @Override
            public void visitEnd() {
                super.visitEnd();
                LabelFlowAnalyzer.markLabels(this);
                final MethodProbesAdapter probesAdapter = new MethodProbesAdapter(
                        methodProbes, ClassProbesAdapter.this);
                if (trackFrames) {
                    final AnalyzerAdapter analyzer = new AnalyzerAdapter(
                            ClassProbesAdapter.this.name, access, name, desc,
                            probesAdapter);
                    probesAdapter.setAnalyzer(analyzer);
                    methodProbes.accept(this, analyzer);
                } else {
                    methodProbes.accept(this, probesAdapter);
                }
            }
        };
    }

  2、新增私有方法

  private boolean isContainsMethod(String currentMethod, List<ClassInfo> classInfos) {
        if (classInfos== null || classInfos.isEmpty()) {
            return true;
        }
        String currentClassName = name.replaceAll("/",".");
        for (ClassInfo classInfo : classInfos) {
            String className = classInfo.getPackages() + "." + classInfo.getClassName();
            if (currentClassName.equals(className)) {
                for (MethodInfo methodInfo: classInfo.getMethodInfos()) {
                    String methodName = methodInfo.getMethodName();
                    if (currentMethod.equals(methodName)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

 

第五步:修改org.jacoco.report專案中org.jacoco.report.internal.html.page包下的SourceHighlighter類:

  1、修改程式碼第72行的render方法:

public void render(final HTMLElement parent, final ISourceNode source,
            final Reader contents) throws IOException {
        final HTMLElement pre = parent.pre(Styles.SOURCE + " lang-" + lang
                + " linenums");
        final BufferedReader lineBuffer = new BufferedReader(contents);
        String classPath = ((SourceFileCoverageImpl) source).getPackageName() + "." + source.getName().replaceAll(".java","");
        classPath = classPath.replaceAll("/",".");
        String line;
        int nr = 0;
        while ((line = lineBuffer.readLine()) != null) {
            nr++;
            renderCodeLine(pre, line, source.getLine(nr), nr,classPath);
        }
    }

  2、修改程式碼第87行renderCodeLine方法:

private void renderCodeLine(final HTMLElement pre, final String linesrc,
            final ILine line, final int lineNr, final String classPath) throws IOException {
        if (CoverageBuilder.classInfos == null || CoverageBuilder.classInfos.isEmpty()) {
            //    全量覆蓋
            highlight(pre, line, lineNr).text(linesrc);
            pre.text("\n");
        } else {
            //    增量覆蓋
            boolean existFlag = true;
            for (ClassInfo classInfo : CoverageBuilder.classInfos) {
                String tClassPath = classInfo.getPackages() + "." + classInfo.getClassName();
                if (classPath.equals(tClassPath)) {
                    //    新增的類
                    if ("ADD".equalsIgnoreCase(classInfo.getType())) {
                        highlight(pre, line, lineNr).text("+ " + linesrc);
                        pre.text("\n");
                    } else {
                        //    修改的類
                        boolean flag = false;
                        List<int[]> addLines = classInfo.getAddLines();
                        for (int[] ints: addLines) {
                            if (ints[0] <= lineNr &&  lineNr <= ints[1]){
                                flag = true;
                                break;
                            }
                        }
                        if (flag) {
                            highlight(pre, line, lineNr).text("+ " + linesrc);
                            pre.text("\n");
                        } else {
                            highlight(pre, line, lineNr).text(" " + linesrc);
                            pre.text("\n");
                        }
                    }
                    existFlag = false;
                    break;
                }
            }
            if (existFlag) {
                highlight(pre, line, lineNr).text(" " + linesrc);
                pre.text("\n");
            }
        }
    }

使用方式:

  在org.jacoco.examples專案中,新增一個包,然後新增如下類:

  1、用於生成exec的ExecutionDataClient類:

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import org.jacoco.core.data.ExecutionDataWriter;
import org.jacoco.core.runtime.RemoteControlReader;
import org.jacoco.core.runtime.RemoteControlWriter;

/**
* 用於生成exec檔案
*/ public class ExecutionDataClient { private static final String DESTFILE = "D:\\Git\\Jacoco-Test\\jacoco.exec";//匯出的檔案路徑 private static final String ADDRESS = "127.0.0.1";//配置的Jacoco的IP private static final int PORT = 9001;//Jacoco監聽的埠 public static void main(final String[] args) throws IOException { final FileOutputStream localFile = new FileOutputStream(DESTFILE); final ExecutionDataWriter localWriter = new ExecutionDataWriter( localFile); //連線Jacoco服務 final Socket socket = new Socket(InetAddress.getByName(ADDRESS), PORT); final RemoteControlWriter writer = new RemoteControlWriter(socket.getOutputStream()); final RemoteControlReader reader = new RemoteControlReader(socket.getInputStream()); reader.setSessionInfoVisitor(localWriter); reader.setExecutionDataVisitor(localWriter); // 傳送Dump命令,獲取Exec資料 writer.visitDumpCommand(true, false); if (!reader.read()) { throw new IOException("Socket closed unexpectedly."); } socket.close(); localFile.close(); } private ExecutionDataClient() { } }

  2、用於根據exec檔案生成覆蓋率報告的ReportGenerator類:

import java.io.File;
import java.io.IOException;

import org.jacoco.core.analysis.Analyzer;
import org.jacoco.core.analysis.CoverageBuilder;
import org.jacoco.core.analysis.IBundleCoverage;
import org.jacoco.core.internal.diff.GitAdapter;
import org.jacoco.core.tools.ExecFileLoader;
import org.jacoco.report.DirectorySourceFileLocator;
import org.jacoco.report.FileMultiReportOutput;
import org.jacoco.report.IReportVisitor;
import org.jacoco.report.MultiSourceFileLocator;
import org.jacoco.report.html.HTMLFormatter;

/**
* 用於根據exec檔案生成增量覆蓋率報告
*/ public class ReportGenerator { private final String title; private final File executionDataFile; private final File classesDirectory; private final File sourceDirectory; private final File reportDirectory; private ExecFileLoader execFileLoader; public ReportGenerator(final File projectDirectory) { this.title = projectDirectory.getName(); this.executionDataFile = new File(projectDirectory, "jacoco.exec");   //第一步生成的exec的檔案 this.classesDirectory = new File(projectDirectory, "bin");        //目錄下必須包含原始碼編譯過的class檔案,用來統計覆蓋率。所以這裡用server打出的jar包地址即可,執行的jar或者Class目錄 this.sourceDirectory = new File(projectDirectory, "src/main/java");   //原始碼目錄 this.reportDirectory = new File(projectDirectory, "coveragereport");  //要儲存報告的地址 } public void create() throws IOException { loadExecutionData(); final IBundleCoverage bundleCoverage = analyzeStructure(); createReport(bundleCoverage); } private void createReport(final IBundleCoverage bundleCoverage) throws IOException { final HTMLFormatter htmlFormatter = new HTMLFormatter(); final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory)); visitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(),execFileLoader.getExecutionDataStore().getContents()); visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(sourceDirectory, "utf-8", 4)); // //多原始碼路徑 // MultiSourceFileLocator sourceLocator = new MultiSourceFileLocator(4); // sourceLocator.add( new DirectorySourceFileLocator(sourceDir1, "utf-8", 4)); // sourceLocator.add( new DirectorySourceFileLocator(sourceDir2, "utf-8", 4)); // sourceLocator.add( new DirectorySourceFileLocator(sourceDir3, "utf-8", 4)); // visitor.visitBundle(bundleCoverage,sourceLocator); visitor.visitEnd(); } private void loadExecutionData() throws IOException { execFileLoader = new ExecFileLoader(); execFileLoader.load(executionDataFile); } private IBundleCoverage analyzeStructure() throws IOException { // git登入授權 GitAdapter.setCredentialsProvider("QQ512433465", "mima512433465"); // 全量覆蓋      // final CoverageBuilder coverageBuilder = new CoverageBuilder(); // 基於分支比較覆蓋,引數1:本地倉庫,引數2:開發分支(預發分支),引數3:基線分支(不傳時預設為master) // 本地Git路徑,新分支 第三個引數不傳時預設比較maser,傳引數為待比較的基線分支 final CoverageBuilder coverageBuilder = new CoverageBuilder("E:\\Git-pro\\JacocoTest","daily"); // 基於Tag比較的覆蓋 引數1:本地倉庫,引數2:程式碼分支,引數3:新Tag(預發版本),引數4:基線Tag(變更前的版本) //final CoverageBuilder coverageBuilder = new CoverageBuilder("E:\\Git-pro\\JacocoTest","daily","v004","v003"); final Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder); analyzer.analyzeAll(classesDirectory); return coverageBuilder.getBundle(title); } public static void main(final String[] args) throws IOException { final ReportGenerator generator = new ReportGenerator(new File("D:\\Git\\Jacoco-Test")); generator.create(); } }

參考程式碼:

  https://github.com/512433465/JacocoPlus

  https://github.com/fang-yan-peng/diff-jacoco