代码覆盖率测试

Jacoco基本使用

源码存在的情况

装载Agent-javaagent:/home/coderss/Documents/jacoco/lib/jacocoagent.jar=includes=*,output=tcpserver,port=9100,address=127.0.0.1

生成状态文件
java -jar jacococli.jar dump --address 127.0.0.1 --port 9100 --destfile ~/Desktop/jacoco.exec

导出报告
java -jar jacococli.jar report ~/Desktop/jacoco.exec --classfiles /home/coderss/Documents/some_java/SecExample/target/classes --sourcefiles /home/coderss/Documents/some_java/SecExample/src/main/java --encoding utf-8 --html ~/Desktop/jacoReport

源码不存在的情况

我们将使用procyon的算法进行批量反编译
反编译整个Jar:java -jar crf-0.152.jar -jar myJar.jar --outputdir ~/Document/sourcefile

再出导出报告
java -jar jacococli.jar report ~/Desktop/jacoco.exec --classfiles /home/coderss/Documents/some_java/SecExample/target/classes --sourcefiles /home/coderss/Documents/some_java/SecExample/src/main/java --encoding utf-8 --html ~/Desktop/jacoReport

不同点

源码存在 源码不存在

反编译只要行数尚有差距, 结果就无法匹配

SecPoint内部原理

CodeCoverage内部

探针插入策略

  • 如何统计某个方法是否被触发
  • 如何统计不同分支的执行情况
  • 如果统计执行的代码块的执行情况

方法是否被触发

  • 方法尾加:这种处理比较麻烦, 可能有多个return或者throw.能说明方法被执行过, 且说明了探针上面的方法被执行了, 同时也说明了下个语句准备.
  • 方法头加: 处理很简单, 但只能说明方法有进去过.

不同分支的执行情况

不同的分支指遇到了例如if判断语句, for判断语句, while, switch等, 会跳到不同代码块执行, 中间可能会漏执行部分代码
因为jacoco是针对字节码工作的, 因此这类跳转指令对应的字节码为 GOTO, IFx, TABLESWITCH or LOOKUPSWITCH, 统称为JUMP类型

1
2
3
4
5
6
7
function() {
指令1
if (条件){
指令3
}
指令5
}

统计执行的代码块的执行情况

这个比较简单, 只要在每行代码前都插入探针即可, 但这样会性能问题, 需要插入大量的探针. 那有没有办法优化一下呢?

如果几行代码都是顺序执行的, 那只要在代码段前, 代码段后放置探针即可. 但还会有问题, 某行代码抛异常了怎么办?

JaCoCo考虑到非方法调用的指令一般出现异常的概率比较低. 因此对非方法调用的指令不插入探针, 而对每个方法调用之前都插入探针.
这当然会存在问题, 例如 NullPointerExceptionorArrayIndexOutOfBoundsException 异常出现会导致在临近的非方法调用的指令的覆盖率会有异常.

下图是在 a/0抛出了异常, 但除了test1()上面的探针能捕获 int a = 10; 这个语句之外其他都无法判定是否执行.

对代码的修改点, 看了上面的反编译后的例子, 可以看到具体改了3个地方.

  • 类增加了$jacocoData属性
  • 每个方法开头都增加了一个boolean数组的局部变量, 并调用$jacocoInit进行赋值
  • 类增加了$jacocoInit方法
  • 对方法里面的语句进行boolean数组里面元素的修改.

具体流程

  • ProbeArrayStrategy: 是boolean数组的生成策略类. 用于实现上面1 $jacocoData属性,2 (增加boolean数组并赋值) 和3 $jacocoInit初始化方法
    因为设计到class的处理和method的处理, 因此在这两者的处理类里面都能看到他的身影.
  • ClassProbesAdapter: 没有太多的逻辑
  • ClassInstrumenter: ClassProbesAdapter的代理的类了, 其实也没有太多的逻辑, 因为IProbeArrayStrategy 已经把类级别的事情做了,ClassInstrumenter 调用一下就可以了. 并且还要创建方法处理器.
  • ClassInstrumenter 其实是一个具体实现, 继承 ClassProbesVisitor, 还有另一个实现是 ProbeCounter 作用是统计所有探针的数量, 但不做任何处理, 在ProbeArrayStrategyFactory 里面负责统计完之后生成不同的实现类. 例如探针数为0, 则用NoneProbeArrayStategy即可.
  • MethodProbesAdapter: 也是一个适配器, 作用是找到哪些指令需要插入探针的, 再调用MethodInstrumenter来插入.
  • MethodInstrumenter: 这个是解决如何插探针的问题. 大部分情况可能直接插入就可以了, 但少部分情况需要做些额外处理才能插入.
  • ProbeInserter: 这个负责生成插入探针的代码, 例如 插入 arrbl[2] = true; 且因为在方法头增加了一个局部变量, 因此还要处理一些class文件修改层面的事情, 例如剩余代码对局部变量的引用都要+1, StackSize 等都要进行修改. 这个需要了解class文件的格式和字节码一些基础知识.