基于 Jacoco 获取增量代码覆盖率的方案社区上已经有很多帖子了,这个帖子就不赘述了,有兴趣的可以在社区搜索或参考我的这个工程-jacoco
方案的实现大体分成两类,一类是基于 jacoco 的插件通过命令行生成,这种方式对于了解个中原理的童鞋来说比较轻便且排错简单,但是对于不熟悉 maven 或 gradle 的童鞋来说还是有一定门槛的,所以就出现另一种方式即将整个方案平台化,通过一些平台的封装提升方案的易用性,但是对于大多数公司来说,可能不允许做过多投入来做平台(特别在社区也有童鞋提出过领导不认可覆盖率报告的情况),因此需要一种相对折中的方案,即无需做过多改造但又可轻便实现整套方案。
Jenkins 作为 CICD 使用最广泛的开源工具/平台,可轻易实现任务的编排,且丰富的插件体系使得它可以方便地集成各种功能,其中集成 Jacoco 生成覆盖率报告也是功能之一。因此我们考虑改造 Jacoco 插件以使得它可以支撑获取增量代码覆盖率,结合 Jenkins 自带的自动任务调度,就可以非常方便地实现增量代码覆盖率方案了。
其实增量代码覆盖率的实现是比较简单的,主要就是两个点,一个是获取增量代码,另一个是过滤报告。其中获取增量代码可结合 AST 语法树实现,过滤报告则只需要对 Jacoco 的报告生成部分做少量改造即可,细节请参考这个工程-jacoco。因此我们要做的就是把这两个点直接照搬至 Jacoco 插件的实现即可。
这一步因为在其他介绍增量代码覆盖率获取方案的帖子中已经重复介绍过很多遍,因此不再重复说明,请在社区搜索相关帖子。
Jacoco 插件的实现是结合 Jacoco,因此第一步我们需要做的就是替换默认的 Analyzer,使得其在生成报告时能够插入我们自定义的逻辑,即根据变更方法来过滤报告。我们需要增加 3 个 Analyzer,包括Analyzer
,ClassAnalyzer
,MethodAnalyzer
,其中主要代码变更在MethodAnalyzer
,代码片段如下:
@Override
public void accept(final MethodNode methodNode,
final MethodVisitor methodVisitor) {
filter.filter(methodNode, filterContext, this);
if (shoudHackMethod(methodNode, JacocoPublisher.methodInfos)){
methodVisitor.visitCode();
}
for (final TryCatchBlockNode n : methodNode.tryCatchBlocks) {
n.accept(methodVisitor);
}
currentNode = methodNode.instructions.getFirst();
while (currentNode != null) {
currentNode.accept(methodVisitor);
currentNode = currentNode.getNext();
}
if (shoudHackMethod(methodNode, JacocoPublisher.methodInfos)){
methodVisitor.visitEnd();
}
}
private boolean shoudHackMethod(final MethodNode methodNode,
final List<MethodInfo> methodInfos) {
if(methodInfos.isEmpty()){
return true;
}
for (final MethodInfo methodInfo : methodInfos) {
final String methodName = methodInfo.getMethodName();
final String clazzName = methodInfo.getPackages().replace(".", "/")
+ "/" + methodInfo.getClassName();
if (methodNode.name.equals(methodName)
&& className.equals(clazzName)) {
return true;
}
}
return false;
}
然后替换默认的 Analyzer,入口在包hudson.plugins.jacoco
下的ExecutionFileLoader
。
private IBundleCoverage analyzeStructure() throws IOException {
File classDirectory = new File(classDir.getRemote());
if (!classDirectory.exists()) {
return null;
}
final CoverageBuilder coverageBuilder = new CoverageBuilder();
final Analyzer analyzer = new Analyzer(executionDataStore,
coverageBuilder);
...
由于需要实现代码对比,因此需要一个基线代码信息的输入入口,因此我们需要在插件前端增加相关入口。修改hudson/plugins/jacoco/JacocoPublisher/config.jelly
:
<table width="78%">
<col width="39%"/>
<col width="39%"/>
<tr>
<td>Path to class directories (e.g.: **/target/classDir, **/classes)</td>
<td>SshRepo (e.g.: git@127.0.0.1:angrytest/test.git)</td>
<td>Basic Tag (e.g.: dev_20180917)</td>
</tr>
<tr>
<td>
<f:textbox field="classPattern" default="**/classes"/>
</td>
<td>
<f:textbox field="sshRepoPattern" default=""/>
</td>
<td>
<f:textbox field="basicTagPattern" default=""/>
</td>
</tr>
</table>
修改报告发布机制,修改hudson.plugins.jacoco
下的JacocoPublisher
:
// 变更方法
methodInfos = new ArrayList<MethodInfo>();
String baseTag = "";
if (!basicTagPattern.equals("") && !sshRepoPattern.equals("")) {
baseTag = basicTagPattern;
String tagPath = filePath + DiffAST.SEPARATOR + baseTag;
logger.println("开始clone历史版本:" + baseTag);
try {
GitClone.cloneFiles(sshRepoPattern, baseTag, tagPath);
} catch (GitAPIException e) {
e.printStackTrace();
}
logger.println("clone历史版本:" + baseTag + "结束");
if (methodInfos.isEmpty()) {
DiffAST.diffBaseDir(filePath.toString(), tagPath);
logger.println("变更方法数为:" + methodInfos.size());
}
} else {
baseTag = "kdbczdtag";
}
更新后的插件前端如下图:
以工程-springboot-crud-demo为例,两个 tag(20200121_01 和 20200121_02)之间的变更只涉及一个方法。
在新增入口输入基线代码:
执行 Job 日志中会出现增量代码相关日志:
查看覆盖率报告只显示变更代码部分:
若两个输入框都为空,则显示所有代码报告:
插件工程地址:https://github.com/AngryTester/jacoco-plugin
打包命令:mvn clean package -Dmaven.test.skip=true
通过 Jenkins 安装官方 Jacoco 插件(这一步的主要目的是安装 Jacoco 插件依赖的其他插件),然后删除 Jenkins 主目录 plugins 下的 jacoco:
将上面打包后 target 目录下的 jacoco.hpi 复制至 Jenkins 主目录下 plugins 目录,重启 Jenkins 即可。