关于后台的代码增量的逻辑已经有比较成熟的方案了。 根据javaparser解析前后的文件的方法列表,判断是否有新增或者修改的方法。
前端代码覆盖率增量覆盖的困难
针对前端代码覆盖率并不能像java那块那么简单,有专门的javascript的解析器,能够获取到这个js文件中所有的方法。所以套用原有的java那套逻辑基本是不太可行的。所以我们需要另辟蹊径来解决这个问题。
java的增量代码diff 我们是从解析源码的文件入手的,那针对js既然这套不行,有没有方式能够从覆盖率结果数据入手,去解决这个事情呢?
如果了解前端代码覆盖率的同学可能都清楚,前端的覆盖率收集是根据浏览器提交的coverage数据来的。而coverage的数据其实是大有来头的。
我们看一个数据
/**
*
* * `path` - the file path for which coverage is being tracked
* * `statementMap` - map of statement locations keyed by statement index
* * `fnMap` - map of function metadata keyed by function index
* * `branchMap` - map of branch metadata keyed by branch index
* * `s` - hit counts for statements
* * `f` - hit count for functions
* * `b` - hit count for branches
*/
{
path: filePath,
statementMap: {
"5": {
"end": {
"line": 10,
"column": 30
},
"start": {
"line": 10,
"column": 26
}
},
},
fnMap: {},
branchMap: {},
s: {
"5": 10290,
},
f: {},
b: {}
}
以上的内容中statementMap
中的5
代表标记的对应的代码块为第10行,列则是从26到30, 同时映射到s
中 这个代码块被执行了10290次。
这块的数据其实也完整的说明了对应的文件中,代码/分支/方法是否覆盖的情况。
思路
那么是不是可以这么去考虑呢?从git diff中对比得到对应文件的改动行数,然后再对应到这块的数据上,如果修改的代码行 是在statemanMap
/ fnMap
/ branchMap
的覆盖范围的话就保留这块的数据,如果说改动行中,不存在有这块的内容则从对象中将这块的内容剔除掉。这样子就可以得到增量的数据了。
但是我们是不是直接针对用户提交的coverage数据做处理呢?
答案是不行的。 我们需要了解一个问题,之前我们在 聊聊前端代码覆盖率 (长文慎入) 中提到过用户提交的coverage数据并不是完整的反应到原本的代码行上,主要是针对typescript这块,因为如果你的编译是经过ts-loader -> babel-loader 处理的话。得到的coverage中的数据中的line的值,其实跟源码中的line会出现不一致的情况。而istanbul这块是会根据sourceMap 重新映射回去的。
那哪里的数据才是正在正确的呢? 答案其实在通过nyc api生成的报告目录下, 当你的api指定了reporter包含有 json的情况下,就会在覆盖率报告的目录下生成有 coverage-final.json
。 这里的数据其实跟coverage数据基本是一致的,并且这里的数据已经经过istanbul校正过。所以我们可以信任这块的数据。
解决
从git api中获取到改动行,判断statemanMap
/ fnMap
/ branchMap
的开始行及结束行的范围是否包含了改动行。如果在对应的范围那么则保留对象数据,如果不在,则移除掉对应的对象。如此剩下的就是改动范围的覆盖情况
所以我们可以这么处理
/**
* 根据代码codeDiff,过滤掉未改动的语句
* @param fileFinal 对应文件的覆盖率数据情况,格式就是我们上述提到的数据
* @param statements
* @return
*/
private void handleStatements(List<Integer> codeDiff, Coverage fileFinal, CoverageSummary statements, CoverageSummary lines) {
List<String> keys = new ArrayList<>();
Map<Integer, Integer> line = new HashMap<>();
// 这里的key 是0, 1, 2, 3 "statementMap":{"0":{"start":{"line":9,"column":22},"end":{"line":39,"column":1}} ;; "s":{"0":10150,"1":10150,"2":0,"3":0},
for (String key : fileFinal.getStatementMap().keySet()) {
if (!isDiff(codeDiff, fileFinal.getStatementMap().get(key).getStart().getLine(), fileFinal.getStatementMap().get(key).getEnd().getLine())) {
keys.add(key);
}
}
// 将不包含改动行的桩删除
for (String key : keys) {
fileFinal.getStatementMap().remove(key);
fileFinal.getS().remove(key);
}
computeCoverageSummary(getLineCoverage(fileFinal), lines);
computeCoverageSummary(fileFinal.getS(), statements);
}
这里关于statement/line/fun的统计计算这里就不详细描述了,主要可以参考 file-coverage 中的各个值的计算逻辑。
所以我们举一个简单的例子来说明下:假设 client/src/utils/location.js
的coverage数据如下:
{
"/app/thirdCode/c94933a0-7250-11eb-8c93-c9620716ef34_1/xxx/client/src/utils/location.js": {
"path": "/app/thirdCode/c94933a0-7250-11eb-8c93-c9620716ef34_1/xxx/client/src/utils/location.js",
"statementMap": {
"0": {
"start": {
"line": 9,
"column": 39
},
"end": {
"line": 9,
"column": 54
}
},
"1": {
"start": {
"line": 10,
"column": 25
},
"end": {
"line": 10,
"column": 52
}
},
"2": {
"start": {
"line": 11,
"column": 26
},
"end": {
"line": 11,
"column": 48
}
},
"3": {
"start": {
"line": 13,
"column": 4
},
"end": {
"line": 19,
"column": 5
}
},
"4": {
"start": {
"line": 14,
"column": 8
},
"end": {
"line": 17,
"column": 9
}
},
"5": {
"start": {
"line": 15,
"column": 12
},
"end": {
"line": 15,
"column": 37
}
},
"6": {
"start": {
"line": 16,
"column": 12
},
"end": {
"line": 16,
"column": 21
}
},
"7": {
"start": {
"line": 18,
"column": 8
},
"end": {
"line": 18,
"column": 37
}
},
"8": {
"start": {
"line": 20,
"column": 22
},
"end": {
"line": 20,
"column": 45
}
},
"9": {
"start": {
"line": 21,
"column": 20
},
"end": {
"line": 21,
"column": 72
}
},
"10": {
"start": {
"line": 23,
"column": 4
},
"end": {
"line": 25,
"column": 5
}
},
"11": {
"start": {
"line": 24,
"column": 8
},
"end": {
"line": 24,
"column": 50
}
},
"12": {
"start": {
"line": 27,
"column": 4
},
"end": {
"line": 27,
"column": 19
}
},
"13": {
"start": {
"line": 36,
"column": 25
},
"end": {
"line": 36,
"column": 78
}
},
"14": {
"start": {
"line": 37,
"column": 19
},
"end": {
"line": 40,
"column": 10
}
},
"15": {
"start": {
"line": 38,
"column": 8
},
"end": {
"line": 38,
"column": 31
}
},
"16": {
"start": {
"line": 39,
"column": 8
},
"end": {
"line": 39,
"column": 19
}
},
"17": {
"start": {
"line": 42,
"column": 4
},
"end": {
"line": 44,
"column": 5
}
},
"18": {
"start": {
"line": 43,
"column": 8
},
"end": {
"line": 43,
"column": 29
}
},
"19": {
"start": {
"line": 46,
"column": 4
},
"end": {
"line": 48,
"column": 5
}
},
"20": {
"start": {
"line": 47,
"column": 8
},
"end": {
"line": 47,
"column": 35
}
},
"21": {
"start": {
"line": 50,
"column": 4
},
"end": {
"line": 50,
"column": 18
}
}
},
"s": {
"0": 43,
"1": 43,
"2": 43,
"3": 43,
"4": 43,
"5": 0,
"6": 0,
"7": 43,
"8": 43,
"9": 43,
"10": 43,
"11": 43,
"12": 43,
"13": 2184,
"14": 2184,
"15": 6552,
"16": 6552,
"17": 2184,
"18": 2184,
"19": 0,
"20": 0,
"21": 0
},
"_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9",
"hash": "26596d813cde8cfd5d6449001d4e49f4283c164a"
}
}
以上的数据我们省掉了 fnMap以及branchMap的数据。
而我们的的codeDiff的数据:
说明改动的行数只是49-50行
所以处理过的coverage的结果数据为
{
"s": {
"21": 0
},
"hash": "26596d813cde8cfd5d6449001d4e49f4283c164a",
"path": "/app/thirdCode/c94933a0-7250-11eb-8c93-c9620716ef34_1/xxx/client/src/utils/location.js",
"statementMap": {
"21": {
"end": {
"line": 50,
"column": 18
},
"start": {
"line": 50,
"column": 4
}
}
},
"_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9"
}
相应的fn, branch也是相应的处理。
不足:
上述的方式其实存在一个问题,前端的增量覆盖计算的逻辑并不是跟java的增量的逻辑一致的,java的最小增量的计算单位是方法,而前端的最小增量单位是语句。所以并不能很好的得到结果是前端某个代码改动后,需要覆盖这个代码所在的方法的内容,而只是需要覆盖到改动的语句就可以了。