前几天面试被问到:流水线中写好的“代码行覆盖率”原子服务怎么实现的,没答出来,因为不懂原理。今天来编写博客边梳理一下:
原理很简单:
1. 插桩
2. 统计
3. 生成报告
原理
代码的原理是:
(以代码行覆盖率为例)
定义一个lineCounters[] 数组,每一行写一个lineCounters[i]++,一共n行代码,若执行了m行,则lineCounters数组中有m个元素>0,用m/n得到行覆盖率
import java.io.FileOutputStream;
import java.io.IOException;
// 方法一:手动插桩
// 在代码中手动插入计数器,记录每行代码的执行次数。
class Solution {
// 行计数器数组
private static int[] lineCounters;
public void quickSort(int[] arr, int low, int high) {
// 插桩
lineCounters[0]++;
if (low < high) {
// 插桩
lineCounters[1]++;
int pivotIndex = partition(arr, low, high);
// 插桩
lineCounters[2]++;
quickSort(arr, low, pivotIndex - 1);
// 插桩
lineCounters[3]++;
quickSort(arr, pivotIndex + 1, high);
// 插桩
lineCounters[4]++;
}
}
private int partition(int[] arr, int low, int high) {
// 插桩
lineCounters[5]++;
int pivot = arr[high];
// 插桩
lineCounters[6]++;
int i = low - 1;
// 插桩
lineCounters[7]++;
for (int j = low; j < high; j++) {
// 插桩
lineCounters[8]++;
if (arr[j] <= pivot) {
// 插桩
lineCounters[9]++;
i++;
// 插桩
lineCounters[10]++;
swap(arr, i, j);
// 插桩
lineCounters[11]++;
}
}
// 插桩
lineCounters[12]++;
swap(arr, i + 1, high);
// 插桩
lineCounters[13]++;
return i + 1;
}
private void swap(int[] arr, int i, int j) {
// 插桩
lineCounters[14]++;
int temp = arr[i];
// 插桩
lineCounters[15]++;
arr[i] = arr[j];
// 插桩
lineCounters[16]++;
arr[j] = temp;
// 插桩
lineCounters[17]++;
}
public static void main(String[] args) {
// 初始化行计数器
lineCounters = new int[18];
Solution solution = new Solution();
int[] arr = {3, 6, 8, 10, 1, 2, 1};
solution.quickSort(arr, 0, arr.length - 1);
// 计算行覆盖率
int coveredLines = 0;
for (int counter : lineCounters) {
if (counter > 0) {
coveredLines++;
}
}
double lineCoverage = (double) coveredLines / lineCounters.length;
System.out.println("行覆盖率: " + lineCoverage * 100 + "%");
}
}
那为什么不用boolean[]呢,它每一个元素只占1bit,而int[]要占4byte(32bit)
因为:
int[] 每一个元素表示32bit,更省空间。具体统计方法如下:
可以看出,成熟的代码覆盖率框架中,int[]的每个元素并不是代表一行,而是代表32行!我们上边手写的统计demo只是最简陋的版本
封装的包
JaCoCo
JaCoCo的插桩就是每个int元素代表32行,且它的插桩是在字节码层面的,即不修改源码(编译后、类加载时插桩),有两大好处:
1. 兼容所有 JVM 语言(Java/Kotlin/Groovy)
2. 更准确,因为字节码才是最终执行的指令
使用方法更是简单,不用在源代码上做任何修改,配好依赖,直接运行命令就可以生成覆盖率报告了:
# 1. 运行测试收集数据
java -javaagent:jacocoagent.jar=destfile=jacoco.exec -jar app.jar# 2. 生成报告
java -jar jacococli.jar report jacoco.exec \
--classfiles target/classes \ 字节码加载路径
--sourcefiles src/main/java \ 源代码路径(为了在报告中高亮代码)
--html report/ \ 生成html报告
--line --branch 统计行覆盖率(line)、分支覆盖率(branch)
Coverage
与JaCoCo类似,区别在于:
1. 插桩基于Python 源码行号+解释器执行追踪
2. 代码不规范时失真:因为只能统计源代码的行,而不是编译后的逻辑层面的一行