jacoco-實戰篇-增量覆蓋率
阿新 • • 發佈:2020-07-21
獲取增量覆蓋率報告的改動原始碼的步驟:
第一步:拉取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