coverage-check 0.2.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -15
- package/dist/src/cli.mjs +6 -0
- package/dist/src/commands/check-args.d.mts +2 -0
- package/dist/src/commands/check-args.mjs +4 -0
- package/dist/src/commands/check.mjs +20 -2
- package/dist/src/commands/html.d.mts +14 -0
- package/dist/src/commands/html.mjs +164 -0
- package/dist/src/commands/store-put.d.mts +1 -0
- package/dist/src/commands/store-put.mjs +41 -7
- package/dist/src/commands/summary/args.d.mts +2 -0
- package/dist/src/commands/summary/args.mjs +60 -0
- package/dist/src/commands/summary/groups.d.mts +2 -0
- package/dist/src/commands/summary/groups.mjs +84 -0
- package/dist/src/commands/summary/index.d.mts +8 -0
- package/dist/src/commands/summary/index.mjs +118 -0
- package/dist/src/commands/summary/markdown.d.mts +5 -0
- package/dist/src/commands/summary/markdown.mjs +71 -0
- package/dist/src/commands/summary/types.d.mts +36 -0
- package/dist/src/commands/summary/types.mjs +1 -0
- package/dist/src/commands/summary.d.mts +2 -0
- package/dist/src/commands/summary.mjs +1 -0
- package/dist/src/coverage-check.d.mts +2 -1
- package/dist/src/coverage-check.mjs +1 -0
- package/dist/src/diff-parser-content.d.mts +11 -0
- package/dist/src/diff-parser-content.mjs +80 -0
- package/dist/src/diff-parser.d.mts +2 -0
- package/dist/src/diff-parser.mjs +23 -6
- package/dist/src/for-each-line.d.mts +7 -0
- package/dist/src/for-each-line.mjs +28 -0
- package/dist/src/lcov-merge.mjs +7 -3
- package/dist/src/lcov-parser.mjs +41 -18
- package/dist/src/lcov-to-istanbul.d.mts +30 -0
- package/dist/src/lcov-to-istanbul.mjs +155 -0
- package/dist/src/types.d.mts +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
function loc(line) {
|
|
2
|
+
return { start: { line, column: 0 }, end: { line, column: 999 } };
|
|
3
|
+
}
|
|
4
|
+
function normalizeFilePath(filePath, stripPrefixes) {
|
|
5
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
6
|
+
for (const prefix of stripPrefixes) {
|
|
7
|
+
const p = prefix.replace(/\\/g, "/").replace(/\/$/, "");
|
|
8
|
+
if (normalized === p)
|
|
9
|
+
return "";
|
|
10
|
+
if (normalized.startsWith(`${p}/`))
|
|
11
|
+
return normalized.slice(p.length + 1);
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
export function lcovBufferToIstanbul(lcov, stripPrefixes) {
|
|
16
|
+
const coverage = {};
|
|
17
|
+
let filePath = null;
|
|
18
|
+
const pendingFnLines = new Map();
|
|
19
|
+
// Accumulate branches at file scope so counts from all records for the same
|
|
20
|
+
// file are merged before converting to Istanbul arrays.
|
|
21
|
+
// filePath → blockKey → branchId → count
|
|
22
|
+
const fileBranches = new Map();
|
|
23
|
+
function flush() {
|
|
24
|
+
pendingFnLines.clear();
|
|
25
|
+
}
|
|
26
|
+
// Optimization: Instead of using `text.split("\n")` which allocates a massive
|
|
27
|
+
// array of strings in memory and causes significant garbage collection overhead
|
|
28
|
+
// for large LCOV files, we manually traverse the string using `indexOf("\n")`.
|
|
29
|
+
// This reduces memory allocations and improves parsing speed.
|
|
30
|
+
const text = lcov.toString("utf8");
|
|
31
|
+
let start = 0;
|
|
32
|
+
while (start < text.length) {
|
|
33
|
+
let end = text.indexOf("\n", start);
|
|
34
|
+
if (end === -1)
|
|
35
|
+
end = text.length;
|
|
36
|
+
let lineEnd = end;
|
|
37
|
+
if (lineEnd > start && text.charCodeAt(lineEnd - 1) === 13) {
|
|
38
|
+
lineEnd--;
|
|
39
|
+
}
|
|
40
|
+
const line = text.slice(start, lineEnd);
|
|
41
|
+
start = end + 1;
|
|
42
|
+
if (line === "end_of_record") {
|
|
43
|
+
flush();
|
|
44
|
+
filePath = null;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (line.startsWith("SF:")) {
|
|
48
|
+
flush();
|
|
49
|
+
filePath = normalizeFilePath(line.slice(3).trim(), stripPrefixes);
|
|
50
|
+
if (filePath && !coverage[filePath]) {
|
|
51
|
+
coverage[filePath] = {
|
|
52
|
+
path: filePath,
|
|
53
|
+
statementMap: {},
|
|
54
|
+
s: {},
|
|
55
|
+
fnMap: {},
|
|
56
|
+
f: {},
|
|
57
|
+
branchMap: {},
|
|
58
|
+
b: {},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!filePath)
|
|
64
|
+
continue;
|
|
65
|
+
const fileCov = coverage[filePath]; // filePath was validated against coverage when SF: was processed
|
|
66
|
+
if (line.startsWith("DA:")) {
|
|
67
|
+
const [lineNo, hits] = line.slice(3).split(",", 2);
|
|
68
|
+
const l = Number.parseInt(lineNo, 10);
|
|
69
|
+
const h = Number.parseInt(hits ?? "", 10);
|
|
70
|
+
if (!Number.isInteger(l) || !Number.isInteger(h))
|
|
71
|
+
continue;
|
|
72
|
+
const key = String(l);
|
|
73
|
+
if (fileCov.statementMap[key] === undefined) {
|
|
74
|
+
fileCov.statementMap[key] = loc(l);
|
|
75
|
+
fileCov.s[key] = h;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
fileCov.s[key] = fileCov.s[key] + h;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (line.startsWith("FN:") || line.startsWith("FNL:")) {
|
|
82
|
+
const rest = line.slice(line.startsWith("FNL:") ? 4 : 3);
|
|
83
|
+
const firstComma = rest.indexOf(",");
|
|
84
|
+
if (firstComma === -1)
|
|
85
|
+
continue;
|
|
86
|
+
const l = Number.parseInt(rest.slice(0, firstComma), 10);
|
|
87
|
+
const afterFirst = rest.slice(firstComma + 1);
|
|
88
|
+
// Handle both FN:start,name and FN:start,end,name (LCOV 2.x three-field form)
|
|
89
|
+
const secondComma = afterFirst.indexOf(",");
|
|
90
|
+
const name = secondComma === -1 ? afterFirst : afterFirst.slice(secondComma + 1);
|
|
91
|
+
if (Number.isInteger(l) && name)
|
|
92
|
+
pendingFnLines.set(name, l);
|
|
93
|
+
}
|
|
94
|
+
else if (line.startsWith("FNDA:") || line.startsWith("FNA:")) {
|
|
95
|
+
const rest = line.slice(line.startsWith("FNA:") ? 4 : 5);
|
|
96
|
+
const commaIdx = rest.indexOf(",");
|
|
97
|
+
if (commaIdx === -1)
|
|
98
|
+
continue;
|
|
99
|
+
const h = Number.parseInt(rest.slice(0, commaIdx), 10);
|
|
100
|
+
const name = rest.slice(commaIdx + 1);
|
|
101
|
+
if (!Number.isInteger(h) || !name)
|
|
102
|
+
continue;
|
|
103
|
+
const startLine = pendingFnLines.get(name) ?? 0;
|
|
104
|
+
const key = startLine > 0 ? `${name}@${startLine}` : name;
|
|
105
|
+
const fnLoc = loc(startLine);
|
|
106
|
+
if (fileCov.fnMap[key] === undefined) {
|
|
107
|
+
fileCov.fnMap[key] = { name, decl: fnLoc, loc: fnLoc };
|
|
108
|
+
fileCov.f[key] = h;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
fileCov.f[key] = fileCov.f[key] + h;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (line.startsWith("BRDA:")) {
|
|
115
|
+
const parts = line.slice(5).split(",", 4);
|
|
116
|
+
const lineNo = Number.parseInt(parts[0], 10);
|
|
117
|
+
const blockId = parts[1] ?? "";
|
|
118
|
+
const branchId = parts[2] ?? "";
|
|
119
|
+
const taken = parts[3] === "-" ? 0 : Number.parseInt(parts[3] ?? "", 10);
|
|
120
|
+
if (!Number.isInteger(lineNo) || !blockId || !branchId || !Number.isInteger(taken))
|
|
121
|
+
continue;
|
|
122
|
+
const blockKey = `${lineNo}-${blockId}`;
|
|
123
|
+
let fileBlocks = fileBranches.get(filePath);
|
|
124
|
+
if (!fileBlocks) {
|
|
125
|
+
fileBlocks = new Map();
|
|
126
|
+
fileBranches.set(filePath, fileBlocks);
|
|
127
|
+
}
|
|
128
|
+
let blockBranches = fileBlocks.get(blockKey);
|
|
129
|
+
if (!blockBranches) {
|
|
130
|
+
blockBranches = new Map();
|
|
131
|
+
fileBlocks.set(blockKey, blockBranches);
|
|
132
|
+
}
|
|
133
|
+
blockBranches.set(branchId, (blockBranches.get(branchId) ?? 0) + taken);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
flush();
|
|
137
|
+
// Convert per-file branch accumulator to Istanbul branchMap/b arrays.
|
|
138
|
+
// All records for a file are fully merged before sorting, so branchId
|
|
139
|
+
// sets that differ across records are handled correctly.
|
|
140
|
+
for (const [fp, blocks] of fileBranches) {
|
|
141
|
+
const fileCov = coverage[fp]; // fp was validated against coverage when added to fileBranches
|
|
142
|
+
for (const [blockKey, branches] of blocks) {
|
|
143
|
+
const lineNo = Number.parseInt(blockKey.split("-")[0], 10); // blockKey is always "N-blockId"
|
|
144
|
+
const branchLoc = loc(lineNo);
|
|
145
|
+
const sorted = [...branches.entries()].sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }));
|
|
146
|
+
fileCov.branchMap[blockKey] = {
|
|
147
|
+
loc: branchLoc,
|
|
148
|
+
type: "branch",
|
|
149
|
+
locations: sorted.map(() => branchLoc),
|
|
150
|
+
};
|
|
151
|
+
fileCov.b[blockKey] = sorted.map(([, count]) => count);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return coverage;
|
|
155
|
+
}
|
package/dist/src/types.d.mts
CHANGED
|
@@ -2,6 +2,8 @@ export type CoverageRule = {
|
|
|
2
2
|
paths: string;
|
|
3
3
|
patch_coverage_min: number;
|
|
4
4
|
};
|
|
5
|
+
/** Map from repo-root-relative file path to map of added line number → trimmed source text. */
|
|
6
|
+
export type DiffLineContent = Map<string, Map<number, string>>;
|
|
5
7
|
export type CoverageRules = {
|
|
6
8
|
rules: CoverageRule[];
|
|
7
9
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coverage-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Patch-coverage gate: checks that newly added lines meet per-path coverage thresholds. Supports per-suite LCOV accumulation for conditional CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan Ong",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@aws-sdk/client-s3": "^3.1048.0",
|
|
35
|
-
"js-yaml": "^4.1.1"
|
|
35
|
+
"js-yaml": "^4.1.1",
|
|
36
|
+
"monocart-coverage-reports": "^2.12.11"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@types/js-yaml": "^4.0.9",
|