关于后端:基于Jacoco的增量覆盖率实现与落地

56次阅读

共计 5568 个字符,预计需要花费 14 分钟才能阅读完成。

前言

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

  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++) {SINGLETONSi = new LineImpl[SINGLETON_BRA_LIMIT + 1][]; for (int k = 0; k <= SINGLETON_BRA_LIMIT; k++) {SINGLETONSi[k] = new LineImpl[SINGLETON_BRA_LIMIT + 1]; for (int l = 0; l <= SINGLETON_BRA_LIMIT; l++) {SINGLETONSik = 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_SINGLETONSi = new LineImpl[SINGLETON_BRA_LIMIT + 1][]; for (int k = 0; k <= SINGLETON_BRA_LIMIT; k++) {DIFF_SINGLETONSi[k] = new LineImpl[SINGLETON_BRA_LIMIT + 1]; for (int l = 0; l <= SINGLETON_BRA_LIMIT; l++) {DIFF_SINGLETONSik = 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 体系化建设上又推动了一步。接下来效力研发团队还将在精准测试方向上进行更多尝试,包含主动回归范畴剖析,代码调用链路等,欢送大家持续关注。

正文完
 0