在第一篇文章super-jacoco单元测试覆盖率度量实践-1中,笔者介绍了Super-Jacoco的单元测试覆盖率统计只要向Super-Jacoco服务发送如下的一个post请求
代码语言:javascript复制启动覆盖率收集
URL:/cov/triggerUnitCover
调用方法:POST
参数(body方式传入):
{"uuid":"uuid","type":1,"gitUrl":"git@git","subModule":"",
"baseVersion":"master","nowVersion":"feature","envType":"-Ptest"}
返回:{"code":200,"data":true,"msg":"msg"}
Super-Jacoco在接收到请求后,就会自动完成单元测试和覆盖率统计的整个过程并入库。用户可以通过结果查询接口根据事前指定的uuid来查询结果。
在本文中,笔者将结合Super-Jacoco的源码进行分析介绍上述功能是如何实现的,并结合实际项目介绍对Super-Jacoco的增量改动。
使用JGit操作Git
JGit 是一个轻量级纯Java的类库,用来实现 类似命令行的Git 版本控制。例如,以下是super-jacoco的GitHandler类中进行代码库克隆并检出指定分支的方法。
代码语言:javascript复制public Git cloneRepository(String gitUrl, String codePath, String commitId) throws GitAPIException {
Git git = Git.cloneRepository()
.setURI(gitUrl)
.setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password))
.setDirectory(new File(codePath))
.setBranch(commitId)
.call();
// 切换到指定commitId
checkoutBranch(git, commitId);
return git;
}
private static Ref checkoutBranch(Git git, String branch) {
try {
return git.checkout()
.setName(branch)
.call();
} catch (GitAPIException e) {
throw new IllegalStateException(e);
}
}
JGit的API还是非常流畅的,基本上不需要太多解读,熟悉Git的同学看着操作就能明白代码实现的功能。
Git diff获取差异代码
对于增量覆盖率统计来说,如何甄别出目标分支与基线分支之间的代码差异,是整个算法的基础。我们知道,在命令行中,可以通过类似如下的方式获取到两个SHA,如commitID或者branch之间的代码差异。
$ git diff SHA1 SHA2
在super-jacoco中,则需要通过JGit实现类似的功能。通过查阅源码,发现是在JDiffFiles类中实现这个功能的。核心的代码行如下:
代码语言:javascript复制File newF = new File(coverageReport.getNowLocalPath());
File oldF = new File(coverageReport.getBaseLocalPath());
Git newGit;
Git oldGit;
Repository newRepository;
Repository oldRepository;
newGit = Git.open(newF);
newRepository = newGit.getRepository();
oldGit = Git.open(oldF);
oldRepository = oldGit.getRepository();
ObjectId baseObjId = oldGit.getRepository().resolve(coverageReport.getBaseVersion());
ObjectId nowObjId = newGit.getRepository().resolve(coverageReport.getNowVersion());
AbstractTreeIterator newTree = prepareTreeParser(newRepository, nowObjId);
AbstractTreeIterator oldTree = prepareTreeParser(oldRepository, baseObjId);
Listdiff = newGit.diff().setOldTree(oldTree).setNewTree(newTree).setShowNameAndStatusOnly(true).call();
Super-Jacoo的开发同学通过JGit通过两次克隆代码库,作为oldRepo和newRepo,并分别切换到了基线和目标两个分支,以此作为增量覆盖率统计分析的对象,并通过上述代码中的最后一行获取到了目标分支相对于基线的差异部分,即Listdiff。
由于是做增量代码覆盖率统计,后续只要再过滤出来代码变动的部分,如新增和修改即可。删除部分由于已不存在,可以直接忽略。最后,将存在变动的各个类的相关方法保存到一个Map中返回,为后续的Jacoco分析提供源数据。
关于使用JGit操作Git的部分就简要介绍到这里了。
对Super-Jacoco的改造以适应代码库结构
场景
在单元测试覆盖率统计的场景中,Super-Jacoco使用了检出代码库后,自行编译执行单测用例的方式来获取覆盖率数据。例如,以下是来自CodeCompilerExecutor类中的一行代码,用于执行代码编译。
代码语言:javascript复制String[] compileCmd = new String[]{"cd "
coverageReport.getNowLocalPath()
"&&mvn clean compile "
(StringUtils.isEmpty(coverageReport.getEnvType())
? "" : "-P=" coverageReport.getEnvType()) ">>" logFile};
这里,Super-Jacoco团队做了一个假设,也就是说pom.xml文件应该位于项目的根目录上。
对于纯后台类的项目,或者是一般的小型项目,这是一个比较自然的事情。不过在笔者服务的公司,一般来说一个系统的代码库由于历史原因,按照了类似的结构进行存放。假设有ProjectA,则代码库目录为:
代码语言:javascript复制ProjectA--01frondend
--02backend
--app1
--03database
假设代码库检出到目录/home/super-jacoco/projectA目录下,则app1这个java后台应用的pom.xml文件的绝对路径是:
代码语言:javascript复制/home/super-jacoco/projectA/02backend/app1/pom.xml
与团队假定的目录coverageReport.NowLocalPath,有一个相对的偏移。
为了能让这种代码结构的项目也能使用Super-Jacoco,笔者对其进行了一个简单的改造。
需求:
在Super-Jacoco单测时,能够适应适应项目存放pom.xml的不同位置,并正确执行该项目的编译、测试、覆盖率收集等工作。
方案分析:
通过阅读代码,笔者发现Super-Jacoco使用了CoverageReportEntity 这个类来作为整个被测项目的数据。
代码语言:javascript复制/**
* @description:
* @author: gaoweiwei_v
* @time: 2019/7/29 2:29 PM
*/
@Data
public class CoverageReportEntity {
private Integer id;
private String uuid;
private String gitUrl;
private String baseVersion;
private String nowVersion;
private String nowLocalPath = "";
private String baseLocalPath = "";
}
然后在克隆代码库的类CodeCloneExecutor中,有如下的代码,
代码语言:javascript复制String nowLocalPath = CODE_ROOT uuid "/"
coverageReport.getNowVersion().replace("/", "_");
coverageReport.setNowLocalPath(nowLocalPath);
这样,就通过coverageReport.getNowLocalPath来获取项目库检出之后保存在执行环境中的绝对路径,也就是假设的pom.xml所在的路径了。
此外,NowLocalPath还用于表示代码库的根目录。
综合上述分析,可以发现NowLocalPath实际上存在着两个含义,即:代码库的根目录,以及pom.xml的所在目录。
为了能应对pom.xml不在代码库根目录下的场景,考虑通过额外使用一个变量来表示代码库相对于代码库根目录的偏移,如在本文开头的案例中,后台应用的pom.xml文件的绝对路径是:
代码语言:javascript复制/home/super-jacoco/projectA/02backend/app1/pom.xml
假设:
代码语言:javascript复制coverageReport.NowLocalPath=/home/super-jacoco/projectA
coverageReport.codePath=02backend/app1
由此,即可推导出pom.xml所在的位置了。
改造实现
理清楚Super-Jacoco的工作过程之后,修改相对就比较容易了。
首先,笔者借用了CoverageReportEntity这个类中的codePath变量来表示pomx.xml相对项目根目录的偏移位置。通过搜索,发现这个变量在此处定义后在项目中并无使用,因此考虑借用。
然后,参照这nowLocalPath和baseLocalPath, 额外定义了nowLocalCodePath和baseLocalCodePath来指代Super-Jacoco将不同分支克隆到不同目录后的pom.xml所在的不同目录位置。
给目录变量赋值
在CodeCloneExecutor中,通过接口传入相关数据,并根据运行时的实际结果,赋值给上述变量
修改部分调用
对于原先使用了getNowLocalPath的方法来获取pom.xml所在位置的,则需要替换为getNowLocalCodePath。
这样,经过上述修改后,pom.xml不在项目根目录而是某个子目录中的场景,也能使用Super-Jacoco来实现覆盖率的统计了。