jacoco-实战篇-增量覆盖率
我fork的jacoco源码改造好:https://github.com/exmyth/jacoco
入口:https://github.com/exmyth/jacoco/blob/master/org.jacoco.examples/src/org/jacoco/examples/report/DiffReportGenerator.java
DiffReport需要设置成你自己仓库的账号密码
https://github.com/exmyth/jacoco/blob/master/org.jacoco.examples/src/org/jacoco/examples/report/DiffReport.java
org.jacoco.example生成jar方法参考:https://www.cnblogs.com/exmyth/p/13362220.html
如果想自己动手,参考步骤如下:
获取增量覆盖率报告的改动源码的步骤:
第一步:拉取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