首先从注入方式开始:
- On-the-fly插桩:
JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,代理程序在通过Class Loader装载一个class前判断是否转换修改class文件,将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。
在jvm 启动参数指定javaagent后,指定了jacoco的jar,启动jvm实例会调用程序里面的permain方法
//接受jvm參數
package org.jacoco.agent.rt.internal.PreMain public static void premain(final String options, final Instrumentation inst) throws Exception { final AgentOptions agentOptions = new AgentOptions(options); final Agent agent = Agent.getInstance(agentOptions); final IRuntime runtime = createRuntime(inst); runtime.startup(agent.getData()); inst.addTransformer(new CoverageTransformer(runtime, agentOptions, IExceptionLogger.SYSTEM_ERR)); }
//ASM 注入class method public byte[] instrument(final ClassReader reader) { final ClassWriter writer = new ClassWriter(reader, 0) { @Override protected String getCommonSuperClass(final String type1, final String type2) { throw new IllegalStateException(); } }; final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory .createFor(reader, accessorGenerator); final ClassVisitor visitor = new ClassProbesAdapter( new ClassInstrumenter(strategy, writer), true); reader.accept(visitor, ClassReader.EXPAND_FRAMES); return writer.toByteArray(); }
程序保持运行,当调用接口覆盖了代码后
//ASM回調方法,同时jacoco调用分析方法 Override public final MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { final MethodProbesVisitor methodProbes; final MethodProbesVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); if (mv == null) { // We need to visit the method in any case, otherwise probe ids // are not reproducible methodProbes = EMPTY_METHOD_PROBES_VISITOR; } else { methodProbes = mv; } return new MethodSanitizer(null, access, name, desc, signature, exceptions) { @Override public void visitEnd() { super.visitEnd(); LabelFlowAnalyzer.markLabels(this); final MethodProbesAdapter probesAdapter = new MethodProbesAdapter( methodProbes, ClassProbesAdapter.this); if (trackFrames) { final AnalyzerAdapter analyzer = new AnalyzerAdapter( ClassProbesAdapter.this.name, access, name, desc, probesAdapter); probesAdapter.setAnalyzer(analyzer); methodProbes.accept(this, analyzer); //注入数据分析 } else { methodProbes.accept(this, probesAdapter); } } }; }
//覆盖率统计 public void increment(final ISourceNode child) { instructionCounter = instructionCounter.increment(child .getInstructionCounter()); branchCounter = branchCounter.increment(child.getBranchCounter()); complexityCounter = complexityCounter.increment(child .getComplexityCounter()); methodCounter = methodCounter.increment(child.getMethodCounter()); classCounter = classCounter.increment(child.getClassCounter()); final int firstLine = child.getFirstLine(); if (firstLine != UNKNOWN_LINE) { final int lastLine = child.getLastLine(); ensureCapacity(firstLine, lastLine); for (int i = firstLine; i <= lastLine; i++) { final ILine line = child.getLine(i); incrementLine(line.getInstructionCounter(), line.getBranchCounter(), i); } } }
在我們操作后,覆蓋率數據也在生成。在我們dump數據后,會調用
package org.jacoco.ant.ReportTask
順著createReport方法 ,我們看到最後是調用
private void createReport(final IReportGroupVisitor visitor, final GroupElement group) throws IOException { if (group.name == null) { throw new BuildException("Group name must be supplied", getLocation()); } if (group.children.isEmpty()) { final IBundleCoverage bundle = createBundle(group); final SourceFilesElement sourcefiles = group.sourcefiles; final AntResourcesLocator locator = new AntResourcesLocator( sourcefiles.encoding, sourcefiles.tabWidth); locator.addAll(sourcefiles.iterator()); if (!locator.isEmpty()) { checkForMissingDebugInformation(bundle); } visitor.visitBundle(bundle, locator); } else { final IReportGroupVisitor groupVisitor = visitor .visitGroup(group.name); for (final GroupElement child : group.children) { createReport(groupVisitor, child); } } }
接着我们看看这些highlight是如何生成的:
这些红红绿绿的覆盖效果(highlight)
1.获取class每一行和之前运行生成的覆盖率行的类型做对比(之前应该有做class的一致性校验,不然行数就没意义)
2.根据type给予css做颜色标识(绿色为覆盖,红色为未覆盖)
public void render(final HTMLElement parent, final ISourceNode source, final Reader contents) throws IOException { final HTMLElement pre = parent.pre(Styles.SOURCE + " lang-" + lang + " linenums"); final BufferedReader lineBuffer = new BufferedReader(contents); String line; int nr = 0; while ((line = lineBuffer.readLine()) != null) { nr++; renderCodeLine(pre, line, source.getLine(nr), nr); } } HTMLElement highlight(final HTMLElement pre, final ILine line, final int lineNr) throws IOException { final String style; switch (line.getStatus()) { case ICounter.NOT_COVERED: style = Styles.NOT_COVERED; break; case ICounter.FULLY_COVERED: style = Styles.FULLY_COVERED; break; case ICounter.PARTLY_COVERED: style = Styles.PARTLY_COVERED; break; default: return pre; } final String lineId = "L" + Integer.toString(lineNr); final ICounter branches = line.getBranchCounter(); switch (branches.getStatus()) { case ICounter.NOT_COVERED: return span(pre, lineId, style, Styles.BRANCH_NOT_COVERED, "All %2$d branches missed.", branches); case ICounter.FULLY_COVERED: return span(pre, lineId, style, Styles.BRANCH_FULLY_COVERED, "All %2$d branches covered.", branches); case ICounter.PARTLY_COVERED: return span(pre, lineId, style, Styles.BRANCH_PARTLY_COVERED, "%1$d of %2$d branches missed.", branches); default: return pre.span(style, lineId); } } pre.source span.pc { background-color:#ffffcc; }
如果我们要加入增量代码的覆盖率标识怎么做:
1.git diff出增加了哪些代码
2.重写highlight方法,如果读取的class的line是新增的话,往html里面加标识(“+++”)
3.重新构建javaagent.jar
最后效果:
新增代码前面会有 “+++” 标识覆盖