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.
Files changed (35) hide show
  1. package/README.md +40 -15
  2. package/dist/src/cli.mjs +6 -0
  3. package/dist/src/commands/check-args.d.mts +2 -0
  4. package/dist/src/commands/check-args.mjs +4 -0
  5. package/dist/src/commands/check.mjs +20 -2
  6. package/dist/src/commands/html.d.mts +14 -0
  7. package/dist/src/commands/html.mjs +164 -0
  8. package/dist/src/commands/store-put.d.mts +1 -0
  9. package/dist/src/commands/store-put.mjs +41 -7
  10. package/dist/src/commands/summary/args.d.mts +2 -0
  11. package/dist/src/commands/summary/args.mjs +60 -0
  12. package/dist/src/commands/summary/groups.d.mts +2 -0
  13. package/dist/src/commands/summary/groups.mjs +84 -0
  14. package/dist/src/commands/summary/index.d.mts +8 -0
  15. package/dist/src/commands/summary/index.mjs +118 -0
  16. package/dist/src/commands/summary/markdown.d.mts +5 -0
  17. package/dist/src/commands/summary/markdown.mjs +71 -0
  18. package/dist/src/commands/summary/types.d.mts +36 -0
  19. package/dist/src/commands/summary/types.mjs +1 -0
  20. package/dist/src/commands/summary.d.mts +2 -0
  21. package/dist/src/commands/summary.mjs +1 -0
  22. package/dist/src/coverage-check.d.mts +2 -1
  23. package/dist/src/coverage-check.mjs +1 -0
  24. package/dist/src/diff-parser-content.d.mts +11 -0
  25. package/dist/src/diff-parser-content.mjs +80 -0
  26. package/dist/src/diff-parser.d.mts +2 -0
  27. package/dist/src/diff-parser.mjs +23 -6
  28. package/dist/src/for-each-line.d.mts +7 -0
  29. package/dist/src/for-each-line.mjs +28 -0
  30. package/dist/src/lcov-merge.mjs +7 -3
  31. package/dist/src/lcov-parser.mjs +41 -18
  32. package/dist/src/lcov-to-istanbul.d.mts +30 -0
  33. package/dist/src/lcov-to-istanbul.mjs +155 -0
  34. package/dist/src/types.d.mts +2 -0
  35. 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
+ }
@@ -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.2.3",
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",