深入jacoco覆盖率计算和源码染色过程分析

前文https://blog.youkuaiyun.com/qq_34418450/article/details/140020135?spm=1001.2014.3001.5502,经过探针还原后,InstructionsBuilder的Map<AbstractInsnNode, Instruction>instructions缓存了指令和AbstractInsnNode的关系,指令的执行情况也被记录了,下一步就是从指令的覆盖率逐步统计到行覆盖率、method的覆盖率、class的覆盖率和package的覆盖率,在全部覆盖率分析完成后,对源码进行染色。
为了方便说明,有必要把前文第三个例子复制下,还是以第三个例子说明jacoco怎么实现覆盖率的计算和染色,为了节省篇幅,这里只重点讨论行覆盖率的计算,这也是源码染色的关键。

    public void test(int num) {
14    if(num>5){
15        System.out.println("test");
16    }else {
17        System.out.println("test1");
18     }
19
20  }
    ASM翻译后
 Label label0 = new Label();
    methodVisitor.visitLabel(label0);
    methodVisitor.visitLineNumber(14, label0);
    methodVisitor.visitVarInsn(ILOAD, 1);
    methodVisitor.visitInsn(ICONST_5);
    Label label1 = new Label();
    methodVisitor.visitJumpInsn(IF_ICMPLE, label1);
    Label label2 = new Label();
    methodVisitor.visitLabel(label2);
    methodVisitor.visitLineNumber(15, label2);
    methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    methodVisitor.visitLdcInsn("test");
    methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    Label label3 = new Label();
    methodVisitor.visitJumpInsn(GOTO, label3);
    methodVisitor.visitLabel(label1);
    methodVisitor.visitLineNumber(17, label1);
    methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
    methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    methodVisitor.visitLdcInsn("test1");
    methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    methodVisitor.visitLabel(label3);
    methodVisitor.visitLineNumber(20, label3);
    methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
    methodVisitor.visitInsn(RETURN);
    Label label4 = new Label();
    methodVisitor.visitLabel(label4);
    methodVisitor.visitLocalVariable("this", "Lcom/sh/isoftstone/controller/LpController;", null, label0, label4, 0);
    methodVisitor.visitLocalVariable("num", "I", null, label0, label4, 1);
    methodVisitor.visitMaxs(2, 2);
    methodVisitor.visitEnd();

第三个例子最终生成的覆盖率效果,以下的分析过程均基于此探针数据的覆盖率的计算过程讨论。
在这里插入图片描述
覆盖率统计最小的颗粒度是Instruction,Instruction是jacoco自定义的类,关联了ASM的AbstractInsnNode,用来分析覆盖率,Instruction的几个属性的说明,属性line对应源码行数。属性branches,jacoco官方的解释是 Each instruction has at least one branch,大白话就是每个指令都至少有一个branch,一般的Instruction的branches为1,jumpInsn对应的Instruction指令branches=2,分支的覆盖率计算依赖属性branches属性,属性coveredBranches就是具体的覆盖率统计了,比如coveredBranches=[0,1]表示0 miss 1 match,表示指令没有miss,1个命中,那么这个指令的覆盖率就是100%。
method在完成指令覆盖率统计后开始统计method的覆盖率数据,方法入口是ClassAnalyzer.addMethodCoverage(stringPool.get(name), stringPool.get(desc),stringPool.get(signature), builder, methodNode);

 private void addMethodCoverage(final String name, final String desc,
                                   final String signature, final InstructionsBuilder icc,
                                   final MethodNode methodNode) {
        final MethodCoverageCalculator mcc = new MethodCoverageCalculator( icc.getInstructions());
        filter.filter(methodNode, this, mcc);
        final MethodCoverageImpl mc = new MethodCoverageImpl(name, desc,signature);
        // method的级别级别的计算,指令级别是最小的行覆盖率,这是整个覆盖率计算的基础
        mcc.calculate(mc);
        if (mc.containsCode()) {
            // class级别的覆盖率统计
            // Only consider methods that actually contain code
            coverage.addMethod(mc);
        }
    }

定义一个 final MethodCoverageCalculator mcc,这个类持有对象Map<AbstractInsnNode, Instruction>instructions。注意icc.getInstructions()这个方法,这个方法先add branch到jumpInsn指令后再返回Instructions,用于分支覆盖计算。mcc的Instructions长这样,这里重点说明下分支覆盖率,源码第十四行的jumpInsnNode关联的指令Instruction分支breaches=2,但是分支只有一个cover。
在这里插入图片描述
我们还是以第三个例子说明行覆盖率的计算过程。

   void calculate(final MethodCoverageImpl coverage) {
	applyMerges();
	applyReplacements();
	ensureCapacity(coverage);
	for (final Entry<AbstractInsnNode, Instruction> entry : instructions.entrySet()) {
		if (!ignored.contains(entry.getKey())) {
			final Instruction instruction = entry.getValue();
			coverage.increment(instruction.getInstructionCounter(),instruction.getBranchCounter(), instruction.getLine());
		}
	}
       coverage.incrementMethodCounter();
}

applyMerges();applyReplacements();一般用不到,ensureCapacity(coverage);由于通过ASM的访问者模式访问的方法,所以在进行方法级别覆盖率计算时候需要每次都更新 MethodCoverageImpl coverage offset和last line,对应源码的行数,offset是第一行代码行数,因为是根据方法级别的计算,所以可以推断出offset是类的第一个方法第一行代码的行数,这里的test方法第一行是14,所以offset=14。coverage lines属性是所有行覆盖率的数组,coverage lines属性是源码染色的依据。

   public ICounter getBranchCounter() {
        if (branches < 2) {
            return CounterImpl.COUNTER_0_0;
        }
        final int covered = coveredBranches.cardinality();
        return CounterImpl.getInstance(branches - covered, covered);
    }

注意getBranchCounter方法,只要braches<2,那么默认返回[0,0],也就是说branches>=2的指令才需要计算分支。
接下来开始遍历method指令集合,计算方法行覆盖率、方法指令覆盖率和方法分支覆盖率。由于hashMap的特性,instructions遍历顺序不会按照顺序进行,为了方便说明,我这里先排序后再进行说明,访问顺序也不会影响覆盖率计算。

 public void increment(final ICounter instructions, final ICounter branches,
			final int line) {
		if (line != UNKNOWN_LINE) {
			incrementLine(instructions, branches, line);
		}
		instructionCounter = instructionCounter.increment(instructions);
		branchCounter = branchCounter.increment(branches);
	}

先看第一个指令 methodVisitor.visitVarInsn(ILOAD, 1),这个指令是被覆盖的,且line=14!= UNKNOWN_LINE,所以需要计算行覆盖率,incrementLine看方法名称就知道了,增量计算行覆盖率,因为一个方法行可能包含多个指令,也可能包括多个branch,需要增量计算。重点看incrementLine方法,这个是源码染色的关键。

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();
		lines[line - offset] = l.increment(instructions, branches);

		// Increment line counter:
		if (instructions.getTotalCount() > 0) {
			if (instructions.getCoveredCount() == 0) {
				if (oldTotal == 0) {
					lineCounter = lineCounter
							.increment(CounterImpl.COUNTER_1_0);
				}
			} else {
				if (oldTotal == 0) {
					lineCounter = lineCounter.increment(CounterImpl.COUNTER_0_1);
				} else {
					if (oldCovered == 0) {
						lineCounter = lineCounter.increment(-1, +1);
					}
				}
			}
		}
	}

ensureCapacity(line, line);前面已经分析过,跳过。final LineImpl l = getLine(line);从lines数组中取出LineImpl l,因为是第一次获取l,所以lines还是null,lines = new LineImpl[last - first + 1],lines[line - offset] = l.increment(instructions, branches),增量计算行覆盖率,
计算完成后这时候lines[0]=[0,1];branch=[0,0]。后面的部分代码就是整个类的行覆盖率统计了,略过。开始访问第二个指令methodVisitor.visitInsn,此时final LineImpl l = getLine(line)拿到的就是lines[0],经过 l.increment(instructions, branches)计算后lines[0]=[0,2];branch=[0,0],开始方法第三个指令methodVisitor.visitJumpInsn(IF_ICMPLE, label1),此时getBranchCounter返回[1,1],根据上面的分析计算完成后lines[0]=[0,3];branch=[1,1],这时候源码行14的相关指令就全部计算完成了,后面的行覆盖率一样的计算逻,略过不表。这时候有同学就发现了,如果我们现在去按照行读取此类的源码文件,那么在第14行是不是就可以实现染色了。jacoco也正是这么做的。

在统计全部的覆盖率后,开始调用方法writeReports(bundle, loader, nowout)写入报告,前面的过程各位看官老爷懂得都懂,我们直接看SourceHighlighter的highlight源码标记style的方法,这个方法实现了源码样式标记,可以看到四种样式,NOT_COVERED,行未覆盖,FULLY_COVERED,全覆盖,PARTLY_COVERED,部分覆盖,default,不标记。

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);
	}
}

根据我们前面的统计,第14行的覆盖率为lines[0]=[0,3];branch=[1,1],根据getStatus的计算,可以得知instructions.getStatus()=FULLY_COVERED,branches.getStatus()=NOT_COVERED,所以line.getStatus()=PARTLY_COVERED,即部分覆盖。

public int getStatus() {
		return instructions.getStatus() | branches.getStatus();
	}

	public int getStatus() {
		int status = covered > 0 ? FULLY_COVERED : EMPTY;
		if (missed > 0) {
			status |= NOT_COVERED;
		}
		return status;
	}

所以给span标记了颜色 Styles.PARTLY_COVERED, Styles.PARTLY_COVERED对应的span class为pc,这里可以验证下。OVER。
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

czx758

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值