devops JaCoCo 增量覆盖率二次开发之用 API 初探

hello · September 27, 2019 · Last by zhntester replied at January 13, 2020 · 3384 hits

概述

测试覆盖率看看@yelanting 的篇帖子就够了 Java 覆盖率 Jacoco 插桩的不同形式总结和踩坑记录。他把他调查参考的文档,遇到的问题,和解决思路,总结非常到。我也是参考他的帖子,向他请教很多学习的,大佬非常热心。
百度到很多通过Jenkins,Maven,Ant去收集Dump exec 生成Report,由于想集成到 DevOps 平台上,每次发布时,拉取增量覆盖率,所以就想通过API收集一下数据,这里就把这几天的研究测试代码分享一下,不正确的地方欢迎批评指正。

talk is cheap,show me the code

这里主要是使用socket模式即Jacoco配置方式是:-javaagent:$jacocoJarPath=includes=*,output=tcpserver,port=1024,address=192.168.0.11",记住这个端口和IP以备后用

  • Dump exec
package com.keking.report;

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;

public class ExecutionDataClient {
private static final String DESTFILE = "E:\\Git-pro\\JacocoTest\\jacoco.exec";//导出的文件路径

private static final String ADDRESS = "192.168.0.11";//配置的Jacoco的IP

private static final int PORT = 1024;//Jacoco监听的端口

/**
* Starts the execution data request.
*
* @param args
* @throws IOException
*/

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() {
}
}
这样就非常方便的不停服务的情况下,去导出exec文件了。

  • ReportGenerator
package com.keking.report;

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;


/**
* This example creates a HTML report for eclipse like projects based on a
* single execution data store called jacoco.exec. The report contains no
* grouping information.
*
* The class files under test must be compiled with debug information, otherwise
* source highlighting will not work.
*/

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;

/**
* Create a new generator based for the given project.
*
* @param projectDirectory
*/

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包地址即可
this.sourceDirectory = new File(projectDirectory, "src/main/java");//源码目录
this.reportDirectory = new File(projectDirectory, "coveragereport");////要保存报告的地址
}

/**
* Create the report.
*
* @throws IOException
*/

public void create() throws IOException {

// Read the jacoco.exec file. Multiple data files could be merged
// at this point
loadExecutionData();

// Run the structure analyzer on a single class folder to build up
// the coverage model. The process would be similar if your classes
// were in a jar file. Typically you would create a bundle for each
// class folder and each jar you want in your report. If you have
// more than one bundle you will need to add a grouping node to your
// report
final IBundleCoverage bundleCoverage = analyzeStructure();

createReport(bundleCoverage);

}

private void createReport(final IBundleCoverage bundleCoverage)
throws IOException {

// Create a concrete report visitor based on some supplied
// configuration. In this case we use the defaults
final HTMLFormatter htmlFormatter = new HTMLFormatter();
final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory));

// Initialize the report with all of the execution and session
// information. At this point the report doesn't know about the
// structure of the report being created
visitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(),execFileLoader.getExecutionDataStore().getContents());

// Populate the report structure with the bundle coverage information.
// Call visitGroup if you need groups in your report.
visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(sourceDirectory, "utf-8", 4));


// //多源码路径 设置,针对一个项目有多个模块,class文件不在一起的情况
// MultiSourceFileLocator sourceLocator = new MultiSourceFileLocator(4);
// sourceLocator.add( new DirectorySourceFileLocator(sourceDirectory1, "utf-8", 4));
// sourceLocator.add( new DirectorySourceFileLocator(sourceDirectory2, "utf-8", 4));
// sourceLocator.add( new DirectorySourceFileLocator(sourceDirectoryN, "utf-8", 4));
// visitor.visitBundle(bundleCoverage,sourceLocator);

// Signal end of structure information to allow report to write all
// information out
visitor.visitEnd();

}

private void loadExecutionData() throws IOException {
execFileLoader = new ExecFileLoader();
execFileLoader.load(executionDataFile);
}

private IBundleCoverage analyzeStructure() throws IOException {

final CoverageBuilder coverageBuilder = new CoverageBuilder()

final Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder);

analyzer.analyzeAll(classesDirectory);

return coverageBuilder.getBundle(title);
}

/**
* Starts the report generation process
*
* @param args
* Arguments to the application. This will be the location of the
* eclipse projects that will be used to generate reports for
* @throws IOException
*/

public static void main(final String[] args) throws IOException {

final ReportGenerator generator = new ReportGenerator(new File("E:\\Git-pro\\JacocoTest"));//项目的目录
generator.create();
}

}
执行完后就可以生成报告了。通过第一步,第二步结合,就可以随时导出Ecec文件,生成报告,查看测试覆盖情况了。那种需要启停服务的

  • 基于jgit增量覆盖

通过jgit获取分支文件差分


/*******************************************************************************
* Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Marc R. Hoffmann - initial API and implementation
*
*******************************************************************************/

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.Ref;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;

import java.io.ByteArrayOutputStream;
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>();
}

/**
* 多线程执行对比
* @return
*/

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>>() {
@Override
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;
}

/**
* 单个差异文件对比
* @param gitAdapter
* @param branchName
* @param oldBranchName
* @param df
* @param diffEntry
* @return
*/

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;
}
}

  • 待续
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 10 条回复 时间 点赞

有成果了哦,不错哦~~~你们用diff的话,可以把报告搞的再细一点,diff可以精确到行,把变更方法里面没有变更的行其实也可以舍弃掉,这样可以更精确。

反正做平台不是个简单的事情,跟做业务又不同,有更多的坑要踩,要适配的场景也会更多~~~哈哈

多谢指点,我再研究研究 🙏

Author only

应该点了吧,我去补个双击😂

您好,我尝试使用您的代码跑下demo。请问下 org.jacoco.core.internal.diff.GitAdapter; 这个是使用的什么依赖。我尝试使用jacoco.core:0.8.4 -jacoco.core:0.8.5,都找不到类呢
使用官方

hello #6 · October 29, 2019 作者
542928492 回复

按照readme 操作的么?

hello 回复

readme? 没看到这个文件,请问下readme 文件项目在什么地方?

hello #8 · October 29, 2019 作者
542928492 回复

楼主你好,请教一下:您的代码使用增量的时候是一个类有变化报告中就会显示整个类全部信息,还是一个类中某个方法变化,报告中这个类就仅显示这个方法,在这个方法上做覆盖率呢?

你好,请问针对 生成jacoco.exec的 你们有封装成接口吗 ? 他们这个是socket的请求 如果弄成get和post的怎么弄呀

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up