Jacoco简析原理 和 改造新增代码覆盖率标识进入报告

Posted q55091

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jacoco简析原理 和 改造新增代码覆盖率标识进入报告相关的知识,希望对你有一定的参考价值。

  首先从注入方式开始:

  • On-the-fly插桩:

JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,代理程序在通过Class Loader装载一个class前判断是否转换修改class文件,将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。

 

开始 找入口,找入口

在jvm 启动参数指定javaagent后,指定了jacoco的jar,启动jvm实例会调用程序里面的permain方法

位于org.jacoco.agent.rt.internal.PreMain

 

//接受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

 

 最后效果:

新增代码前面会有 “+++” 标识覆盖

 

以上是关于Jacoco简析原理 和 改造新增代码覆盖率标识进入报告的主要内容,如果未能解决你的问题,请参考以下文章

vivo 基于 JaCoCo 的测试覆盖率设计与实践

JAVA代码覆盖率工具JaCoCo-原理篇

JAVA代码覆盖率工具JaCoCo--原理

腾讯TMQJAVA代码覆盖率工具JaCoCo-原理篇

解决Sonar扫描Lombok注解的代码没有覆盖率

iOS单元测试-04-覆盖率上传sonarqube