cover_image

质量系列 - 基于Jacoco的增量覆盖率实现与落地

WYG 拍码场
2021年06月02日 11:48

前言

测试团队在执行自动化或者黑盒测试时,希望同时获取代码的覆盖率,测研团队由此开发了第一代自动化覆盖率平台。随着业务迭代,存量代码越来越多,使用过程中遇到了很多新的问题,例如:

  1. 无法统计增量代码覆盖率,以便量化测试完整度
  2. 不支持合并覆盖率报告,多人多环境协作测试时无法获得完整统计数据
  3. 报告手动生成,以及生成报告的必要信息也需要人肉收集,系统间自动化程度低,用户使用效率低

针对上述的问题,测试研发团队开发了覆盖率平台2.0版本,实现了增量代码覆盖率,包括定时采样,自动合并报告等功能,以赋能团队精准测试能力。

方案设计

增量代码覆盖率基于Jacoco实现,Jacoco是基于JVM虚拟机的使用最广的第三方代码覆盖率开源工具。我们的设计主要针对JacocoCore模块,Analyze模块进行功能扩展,在数据分析中加入增量行计数逻辑,以实现增量覆盖率统计。出于体系建设考虑,我们集成增量覆盖率功能到DevOps发布流程,完善了质量量化和风险约束能力,设计方案如下:

图片增量覆盖率实现方案

Rubik自动化平台会根据发布系统推送的发布事件自动触发定时采样,数据合并,另外项目管理系统按一定规则读取统计数据,并且会对覆盖率未达标的发布流程进行卡点约束。

主要功能说明

CodeDiff数据解析

增量行数据是计算增量覆盖率的前提,Rubik平台通过发布系统获得被测站点的包版本与生产包版本,调用GitLabApi获取差异数据,差异代码数据为纯字符串格式,解析转换为差异行数据,转换逻辑如下:

    /**
* 解析GitDiff数据
* @param diff 代码差异生数据
* @return 增量行数组
*/

public static int[] parseIncrLines(String diff) {
GitDiffHelper helper = new GitDiffHelper(diff);
helper.parse();
return helper.newLines;
}


private void parse() {
if (diff == null || diff.length() == 0) {
return;
}
// 跳过文件信息
nextLineIfMinusFile();
nextLineIfPlusFile();

while (!eof()) {
// 解析差异行数据块
parseBlock();
}
}

采样数据分析

Jacoco通过各个维度的计数器逐层累加实现,分别为:

  • 指令计数器(CounterImpl)
  • 行计数器(LineImpl)
  • 方法计算节点(MethodCoverageImpl)
  • 类计算节点(ClassCoverageImpl)
  • Package计算节点(PackageCoverageImpl)
  • Module计算节点(BundleCoverageImpl)
  • 站点计算节点(Jacoco未提供,可自行实现)

通过从底层指令计数器开始逐层累加,最终得到站点级统计信息。为实现增量行统计,我们将增量行与全量行分开,在计算节点父类中(CoverageNodeImpl)中增加了增量行计数器。

public class CoverageNodeImpl implements ICoverageNode {
...

/**
* 全量行计数器
*/

protected CounterImpl lineCounter;

/**
* 增量行计数器
*/

protected CounterImpl diffLineCounter;

...

在原计数逻辑中加入增量行计数逻辑,如下:

public class SourceNodeImpl extends CoverageNodeImpl implements ISourceNode {

private LineImpl[] lines;

// 由GitDiff计算得到的差异行数据
private int[] diffLines;

...

// 由行内指令计数器累加行计数器
private void incrementLine(final ICounter instructions,
final ICounter branches, final int line)
{
ensureCapacity(line, line);
final LineImpl l = getLine(line);
final int oldTotal = l.getInstructionCounter().getTotalCount();
final int oldCovered = l.getInstructionCounter().getCoveredCount();

boolean isDiffLine;
if (l == LineImpl.EMPTY) {
// 确定是否为增量行
isDiffLine = diffLines != null && Arrays.binarySearch(diffLines, line) >= 0;
} else {
isDiffLine = l.isDiffLine();
}

lines[line - offset] = l.increment(instructions, branches, isDiffLine);


// Increment line counter:
if (instructions.getTotalCount() > 0) {
if (instructions.getCoveredCount() == 0) {
if (oldTotal == 0) {
lineCounter = lineCounter.increment(CounterImpl.COUNTER_1_0);
// 增量行处理逻辑:处理已覆盖行
if (isDiffLine) {
diffLineCounter = diffLineCounter.increment(CounterImpl.COUNTER_1_0);
}
}
} else {
if (oldTotal == 0) {
lineCounter = lineCounter.increment(CounterImpl.COUNTER_0_1);
// 增量行处理逻辑:处理未覆盖行
if (isDiffLine) {
diffLineCounter = diffLineCounter.increment(CounterImpl.COUNTER_0_1);
}
} else {
if (oldCovered == 0) {
lineCounter = lineCounter.increment(-1, +1);
// 增量行处理逻辑:处理部分覆盖行
if (isDiffLine) {
diffLineCounter = diffLineCounter.increment(-1, +1);
}
}
}
}
}
}

另外行计数器中Jacoco通过四维数组单例,用固定数量的对象表示8^4(4096)种计数情况,实现了计数缓存,以提高内存使用率,这里增加了增量行标志位以区别全量行计数器,但是Jacoco自身的缓存计数器(Fix类)无法适配增量的情况,所以这里也同样增加了增量行缓存计数器,即DiffFix类,这里会带来固定的4096个DiffFix对象的额外开销,但是对整体性能影响几乎可以忽略。

public abstract class LineImpl implements ILine {
...
private final boolean isDiffLine;
private static final LineImpl[][][][] SINGLETONS = new LineImpl[SINGLETON_INS_LIMIT + 1][][][];
private static final LineImpl[][][][] DIFF_SINGLETONS = new LineImpl[SINGLETON_INS_LIMIT + 1][][][];

static {
// 全量行计数缓存
for (int i = 0; i <= SINGLETON_INS_LIMIT; i++) {
SINGLETONS[i] = new LineImpl[SINGLETON_INS_LIMIT + 1][][];
for (int j = 0; j <= SINGLETON_INS_LIMIT; j++) {
SINGLETONS[i][j] = new LineImpl[SINGLETON_BRA_LIMIT + 1][];
for (int k = 0; k <= SINGLETON_BRA_LIMIT; k++) {
SINGLETONS[i][j][k] = new LineImpl[SINGLETON_BRA_LIMIT + 1];
for (int l = 0; l <= SINGLETON_BRA_LIMIT; l++) {
SINGLETONS[i][j][k][l] = new Fix(i, j, k, l);
}
}
}
}
// 增量行计数缓存
for (int i = 0; i <= SINGLETON_INS_LIMIT; i++) {
DIFF_SINGLETONS[i] = new LineImpl[SINGLETON_INS_LIMIT + 1][][];
for (int j = 0; j <= SINGLETON_INS_LIMIT; j++) {
DIFF_SINGLETONS[i][j] = new LineImpl[SINGLETON_BRA_LIMIT + 1][];
for (int k = 0; k <= SINGLETON_BRA_LIMIT; k++) {
DIFF_SINGLETONS[i][j][k] = new LineImpl[SINGLETON_BRA_LIMIT + 1];
for (int l = 0; l <= SINGLETON_BRA_LIMIT; l++) {
DIFF_SINGLETONS[i][j][k][l] = new DiffFix(i, j, k, l);
}
}
}
}

}

覆盖率报告

出于对可读性的要求,我们没有采用Jacoco原生的Html报告,而是独立开发了相对更为简洁的增量/全量报告,如下:

图片

全环境覆盖率报告如下:

图片

数据合并

测试过程往往经过多次发布,可能因为分批提测,也可能因为修复缺陷,每次JVM启动后需要将之前的采样数据合并到下一次采样数据中继续累加,Rubik平台接收发布事件并按以下规则自动合并:

  • 站点发布新版本前进行最后一次采样
  • 站点发布新版本中,健康检查通过后立即进行一次采样,并且同时开启定时采样
  • 任意一次采样都将进行向前自动合并数据,向前查找规则为:同一站点,同一环境,同一代码分支的最近一次采样数据

虽然可以在PAones项目管理平台的发布工单中查看站点覆盖率报告,但想实时查看站点的增量覆盖情况,用户可登录Rubik平台,指定自己的测试环境,就可以方便的看到被测环境内所有站点的增量覆盖率(按每小时定时采样,也可手动触发实时采样),从而相对精准的控制测试进度,减少漏测问题。效果如下图。

图片

项目实施中遇到的问题

数据合并问题

现象

覆盖率平台平均每天需要对400+个站点提供分析服务,另外算上每小时定时采样,一天完成超过8000次采样分析以及报告生成,在上线运行一段时间后发现,偶尔会出现服务响应慢或卡顿,甚至不可用现象。

分析

针对异常时内存分析发现,主要堆积的对象是SessionInfoStore。

图片

SessionInfoStore是Jacoco用来进行代码分析展示的底层类,包含所有的执行类信息,在自动合并过程中Jacoco默认对SessionInfo进行了累加而非合并,导致每进行一次合并采样文件数据量都会增加30%到50%,随着不断对采样数据合并,加载文件所消耗内存急剧增加,直到并发加载几个文件就导致内存耗尽,频繁触发GC,通过复盘发现,一份采样文件由最初的10K到问题出现时可以扩大到800M到1.5G。

优化方案

通过调查发现SessionInfo只在原生Html报告中需要使用,去掉后不影响自研报告展示,也不会破坏Jacoco分析数据流程,于是优(cu)雅(bao)的将合并数据中的对应逻辑直接去除,最终解决了这个问题,修改代码如下:

/**
* Deserialization of execution data from binary streams.
*/

public class ExecutionDataReader {
...

// Rubik报告无需合并SessionInfo
private void readSessionInfo() throws IOException {
// if (sessionInfoVisitor == null) {
// throw new IOException("No session info visitor.");
// }
// final String id = in.readUTF();
// final long start = in.readLong();
// final long dump = in.readLong();
// sessionInfoVisitor.visitSessionInfo(new SessionInfo(id, start, dump));
}

    // 合并采样数据
private void readExecutionData() throws IOException {
if (executionDataVisitor == null) {
throw new IOException("No execution data visitor.");
}
final long id = in.readLong();
final String name = in.readUTF();
final boolean[] probes = in.readBooleanArray();
executionDataVisitor.visitClassExecution(new ExecutionData(id, name,
probes));
}

后续规划

增量覆盖率为测试结果量化提供了能力支撑,一定程度上解决了测试结果的信任问题,也为测试团队质量分提供了基础能力,帮助信也研发中心在Devops体系化建设上又推进了一步。接下来效能研发团队还将在精准测试方向上进行更多尝试,包括自动回归范围分析,代码调用链路等,欢迎大家继续关注。


继续滑动看下一个
拍码场
向上滑动看下一个