covgate 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +502 -2
- package/bin/covgate-postinstall.js +13 -0
- package/bin/covgate.js +5 -0
- package/docs/ai-workflow.md +196 -0
- package/docs/design.md +327 -0
- package/docs/setup.md +163 -0
- package/package.json +48 -4
- package/schema/covgate.schema.json +295 -0
- package/src/annotations.js +224 -0
- package/src/args.js +189 -0
- package/src/cli.js +486 -0
- package/src/config.js +442 -0
- package/src/coverage/index.js +85 -0
- package/src/coverage/istanbul.js +215 -0
- package/src/coverage/lcov.js +234 -0
- package/src/coverage/model.js +316 -0
- package/src/coverage/paths.js +40 -0
- package/src/errors.js +34 -0
- package/src/gate.js +203 -0
- package/src/glob.js +242 -0
- package/src/index.js +28 -0
- package/src/process.js +170 -0
- package/src/reason.js +77 -0
- package/src/report/build.js +135 -0
- package/src/report/index.js +29 -0
- package/src/report/markdown.js +343 -0
- package/src/report/text.js +267 -0
- package/src/reports.js +113 -0
- package/src/runner.js +95 -0
- package/src/scope.js +440 -0
- package/src/setup/agent.js +327 -0
- package/src/setup/config.js +112 -0
- package/src/setup/index.js +242 -0
- package/src/setup/project.js +316 -0
- package/src/setup/prompt.js +43 -0
- package/src/util.js +163 -0
- package/src/version.js +8 -0
- package/templates/AGENTS.md.snippet.md +9 -0
- package/templates/agent-stop-hook.sh +7 -0
- package/templates/covgate.config.json +98 -0
- package/templates/github-actions-coverage.yml +41 -0
- package/templates/gitignore +5 -0
- package/templates/pre-commit +10 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CoverageDataError } from "../errors.js";
|
|
4
|
+
import { canonicalPath } from "../util.js";
|
|
5
|
+
import {
|
|
6
|
+
createFileCoverage,
|
|
7
|
+
deriveLinesFromStatements,
|
|
8
|
+
mergeFileCoverage
|
|
9
|
+
} from "./model.js";
|
|
10
|
+
import { resolveCoverageSourcePath } from "./paths.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {unknown} value
|
|
14
|
+
* @returns {value is Record<string, any>}
|
|
15
|
+
*/
|
|
16
|
+
function isObject(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {any} data
|
|
22
|
+
* @param {string} reportPath
|
|
23
|
+
*/
|
|
24
|
+
function entriesFromReport(data, reportPath) {
|
|
25
|
+
if (Array.isArray(data)) return data;
|
|
26
|
+
if (!isObject(data)) {
|
|
27
|
+
throw new CoverageDataError(`Istanbul 报告不是对象:${reportPath}`);
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
typeof data.path === "string" &&
|
|
31
|
+
(isObject(data.statementMap) || isObject(data.s))
|
|
32
|
+
) {
|
|
33
|
+
return [data];
|
|
34
|
+
}
|
|
35
|
+
const values = Object.values(data).filter(
|
|
36
|
+
(value) => isObject(value) && typeof value.path === "string"
|
|
37
|
+
);
|
|
38
|
+
if (values.length === 0) {
|
|
39
|
+
throw new CoverageDataError(
|
|
40
|
+
`在 ${reportPath} 中找不到 Istanbul 文件覆盖率记录;请确认 coverage-final.json 的格式,或把 coverage.format 设为正确值。`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return values;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {any} value
|
|
48
|
+
* @param {string} key
|
|
49
|
+
* @param {string} message
|
|
50
|
+
*/
|
|
51
|
+
function integerCount(value, key, message) {
|
|
52
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
53
|
+
throw new CoverageDataError(message);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {any} entry
|
|
60
|
+
* @param {string} reportPath
|
|
61
|
+
* @param {string} root
|
|
62
|
+
*/
|
|
63
|
+
function normalizeEntry(entry, reportPath, root) {
|
|
64
|
+
if (typeof entry.path !== "string") {
|
|
65
|
+
throw new CoverageDataError(`Istanbul 记录缺少 path 字段:${reportPath}`);
|
|
66
|
+
}
|
|
67
|
+
const absPath = resolveCoverageSourcePath(entry.path, root);
|
|
68
|
+
const file = createFileCoverage(absPath, "istanbul", root);
|
|
69
|
+
const statementMap = isObject(entry.statementMap) ? entry.statementMap : {};
|
|
70
|
+
const statementCounts = isObject(entry.s) ? entry.s : {};
|
|
71
|
+
for (const [id, location] of Object.entries(statementMap)) {
|
|
72
|
+
if (!isObject(location) || !isObject(location.start) || !isObject(location.end)) {
|
|
73
|
+
throw new CoverageDataError(`${reportPath}: 无效的 statementMap 记录 ${id}`);
|
|
74
|
+
}
|
|
75
|
+
const startLine = Number(location.start.line);
|
|
76
|
+
const startColumn = Number(location.start.column ?? 0);
|
|
77
|
+
const endLine = Number(location.end.line ?? location.start.line);
|
|
78
|
+
const endColumn = Number(location.end.column ?? startColumn);
|
|
79
|
+
const key = `${startLine}:${startColumn}:${endLine}:${endColumn}`;
|
|
80
|
+
const count = integerCount(
|
|
81
|
+
statementCounts[id] ?? 0,
|
|
82
|
+
key,
|
|
83
|
+
`${reportPath}: 无效的语句计数 ${id}`
|
|
84
|
+
);
|
|
85
|
+
file.statements?.set(key, {
|
|
86
|
+
key,
|
|
87
|
+
line: startLine,
|
|
88
|
+
column: startColumn,
|
|
89
|
+
endLine,
|
|
90
|
+
count
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const fnMap = isObject(entry.fnMap) ? entry.fnMap : {};
|
|
94
|
+
const fnCounts = isObject(entry.f) ? entry.f : {};
|
|
95
|
+
for (const [id, meta] of Object.entries(fnMap)) {
|
|
96
|
+
if (!isObject(meta)) {
|
|
97
|
+
throw new CoverageDataError(`${reportPath}: 无效的 fnMap 记录 ${id}`);
|
|
98
|
+
}
|
|
99
|
+
const decl = isObject(meta.decl)
|
|
100
|
+
? meta.decl
|
|
101
|
+
: isObject(meta.loc)
|
|
102
|
+
? meta.loc
|
|
103
|
+
: null;
|
|
104
|
+
const start = decl && isObject(decl.start) ? decl.start : null;
|
|
105
|
+
const end = decl && isObject(decl.end) ? decl.end : start;
|
|
106
|
+
const line = Number(meta.line ?? start?.line ?? 0);
|
|
107
|
+
const column = Number(start?.column ?? 0);
|
|
108
|
+
const endLine = Number(end?.line ?? line);
|
|
109
|
+
const name = String(meta.name ?? `fn@${line}`);
|
|
110
|
+
const key = `${line}:${column}:${name}`;
|
|
111
|
+
const count = integerCount(
|
|
112
|
+
fnCounts[id] ?? 0,
|
|
113
|
+
key,
|
|
114
|
+
`${reportPath}: 无效的函数计数 ${id}`
|
|
115
|
+
);
|
|
116
|
+
file.functions.set(key, { key, name, line, column, endLine, count });
|
|
117
|
+
}
|
|
118
|
+
const branchMap = isObject(entry.branchMap) ? entry.branchMap : {};
|
|
119
|
+
const branchCounts = isObject(entry.b) ? entry.b : {};
|
|
120
|
+
for (const [id, meta] of Object.entries(branchMap)) {
|
|
121
|
+
if (!isObject(meta)) {
|
|
122
|
+
throw new CoverageDataError(`${reportPath}: 无效的 branchMap 记录 ${id}`);
|
|
123
|
+
}
|
|
124
|
+
const locations = Array.isArray(meta.locations) ? meta.locations : [];
|
|
125
|
+
const groupLoc = isObject(meta.loc) ? meta.loc : null;
|
|
126
|
+
const groupStart = groupLoc && isObject(groupLoc.start) ? groupLoc.start : null;
|
|
127
|
+
const groupLine = Number(groupStart?.line ?? 0);
|
|
128
|
+
const groupColumn = Number(groupStart?.column ?? 0);
|
|
129
|
+
const type = String(meta.type ?? "branch");
|
|
130
|
+
const counts = Array.isArray(branchCounts[id]) ? branchCounts[id] : [];
|
|
131
|
+
if (locations.length === 0) {
|
|
132
|
+
const key = `${groupLine}:${groupColumn}:${type}:0`;
|
|
133
|
+
const count = integerCount(
|
|
134
|
+
counts[0] ?? 0,
|
|
135
|
+
key,
|
|
136
|
+
`${reportPath}: 无效的分支计数 ${id}`
|
|
137
|
+
);
|
|
138
|
+
file.branches.set(key, {
|
|
139
|
+
key,
|
|
140
|
+
line: groupLine,
|
|
141
|
+
column: groupColumn,
|
|
142
|
+
type,
|
|
143
|
+
groupLine,
|
|
144
|
+
index: 0,
|
|
145
|
+
count
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
locations.forEach((location, index) => {
|
|
150
|
+
const start = isObject(location) && isObject(location.start)
|
|
151
|
+
? location.start
|
|
152
|
+
: null;
|
|
153
|
+
const line = Number(start?.line ?? groupLine);
|
|
154
|
+
const column = Number(start?.column ?? groupColumn);
|
|
155
|
+
const key = `${groupLine}:${groupColumn}:${type}:${index}`;
|
|
156
|
+
const count = integerCount(
|
|
157
|
+
counts[index] ?? 0,
|
|
158
|
+
key,
|
|
159
|
+
`${reportPath}: 无效的分支计数 ${id}[${index}]`
|
|
160
|
+
);
|
|
161
|
+
file.branches.set(key, {
|
|
162
|
+
key,
|
|
163
|
+
line,
|
|
164
|
+
column,
|
|
165
|
+
type,
|
|
166
|
+
groupLine,
|
|
167
|
+
index,
|
|
168
|
+
count
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
file.lines = deriveLinesFromStatements(file.statements);
|
|
173
|
+
return file;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {string[]} reportFiles
|
|
178
|
+
* @param {string} root
|
|
179
|
+
*/
|
|
180
|
+
export async function parseIstanbulReports(reportFiles, root) {
|
|
181
|
+
/** @type {Map<string, import("./model.js").FileCoverage>} */
|
|
182
|
+
const files = new Map();
|
|
183
|
+
for (const reportFile of reportFiles) {
|
|
184
|
+
let text;
|
|
185
|
+
try {
|
|
186
|
+
text = await readFile(reportFile, "utf8");
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new CoverageDataError(
|
|
189
|
+
`无法读取覆盖率报告 ${reportFile}: ${error.message}`,
|
|
190
|
+
{ cause: error }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
let data;
|
|
194
|
+
try {
|
|
195
|
+
data = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new CoverageDataError(
|
|
198
|
+
`无法解析 Istanbul JSON 报告 ${reportFile}: ${error.message}`,
|
|
199
|
+
{ cause: error }
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const entries = entriesFromReport(data, reportFile);
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (!isObject(entry)) {
|
|
205
|
+
throw new CoverageDataError(`Istanbul 报告包含非对象记录:${reportFile}`);
|
|
206
|
+
}
|
|
207
|
+
const file = normalizeEntry(entry, reportFile, root);
|
|
208
|
+
const key = canonicalPath(file.absPath);
|
|
209
|
+
const existing = files.get(key);
|
|
210
|
+
if (existing) mergeFileCoverage(existing, file);
|
|
211
|
+
else files.set(key, file);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return files;
|
|
215
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { CoverageDataError } from "../errors.js";
|
|
3
|
+
import { canonicalPath } from "../util.js";
|
|
4
|
+
import { createFileCoverage, mergeFileCoverage } from "./model.js";
|
|
5
|
+
import { resolveCoverageSourcePath } from "./paths.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} text
|
|
9
|
+
* @param {string} reportFile
|
|
10
|
+
* @param {string} root
|
|
11
|
+
* @returns {Map<string, import("./model.js").FileCoverage>}
|
|
12
|
+
*/
|
|
13
|
+
export function parseLcovText(text, reportFile, root) {
|
|
14
|
+
/** @type {Map<string, import("./model.js").FileCoverage>} */
|
|
15
|
+
const files = new Map();
|
|
16
|
+
let current = null;
|
|
17
|
+
/** @type {Array<{ name: string, line: number, endLine: number, count?: number }>} */
|
|
18
|
+
let functionDefs = [];
|
|
19
|
+
/** @type {Array<{ name: string, count: number }>} */
|
|
20
|
+
let functionHits = [];
|
|
21
|
+
let sawAnyRecord = false;
|
|
22
|
+
|
|
23
|
+
const flush = () => {
|
|
24
|
+
if (!current) return;
|
|
25
|
+
if (!current.sourcePath) {
|
|
26
|
+
current = null;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const absPath = resolveCoverageSourcePath(current.sourcePath, root);
|
|
30
|
+
const file = createFileCoverage(absPath, "lcov", root);
|
|
31
|
+
|
|
32
|
+
const defsByName = new Map();
|
|
33
|
+
for (const definition of functionDefs) {
|
|
34
|
+
const list = defsByName.get(definition.name) ?? [];
|
|
35
|
+
list.push(definition);
|
|
36
|
+
defsByName.set(definition.name, list);
|
|
37
|
+
}
|
|
38
|
+
const assigned = new Set();
|
|
39
|
+
for (const hit of functionHits) {
|
|
40
|
+
const candidates = defsByName.get(hit.name) ?? [];
|
|
41
|
+
let definition = candidates.find((item) => !assigned.has(item));
|
|
42
|
+
if (!definition && candidates.length > 0) definition = candidates[0];
|
|
43
|
+
if (definition) {
|
|
44
|
+
assigned.add(definition);
|
|
45
|
+
definition.count = (definition.count ?? 0) + hit.count;
|
|
46
|
+
} else {
|
|
47
|
+
const line = 0;
|
|
48
|
+
const key = `${line}:0:${hit.name}`;
|
|
49
|
+
const existing = file.functions.get(key);
|
|
50
|
+
if (existing) existing.count += hit.count;
|
|
51
|
+
else {
|
|
52
|
+
file.functions.set(key, {
|
|
53
|
+
key,
|
|
54
|
+
name: hit.name,
|
|
55
|
+
line,
|
|
56
|
+
column: 0,
|
|
57
|
+
endLine: line,
|
|
58
|
+
count: hit.count
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const definition of functionDefs) {
|
|
64
|
+
if (assigned.has(definition)) continue;
|
|
65
|
+
const key = `${definition.line}:0:${definition.name}`;
|
|
66
|
+
if (!file.functions.has(key)) {
|
|
67
|
+
file.functions.set(key, {
|
|
68
|
+
key,
|
|
69
|
+
name: definition.name,
|
|
70
|
+
line: definition.line,
|
|
71
|
+
column: 0,
|
|
72
|
+
endLine: definition.endLine,
|
|
73
|
+
count: 0
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const definition of functionDefs) {
|
|
78
|
+
if (definition.count === undefined) continue;
|
|
79
|
+
const key = `${definition.line}:0:${definition.name}`;
|
|
80
|
+
const existing = file.functions.get(key);
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.count = Math.max(existing.count, definition.count);
|
|
83
|
+
} else {
|
|
84
|
+
file.functions.set(key, {
|
|
85
|
+
key,
|
|
86
|
+
name: definition.name,
|
|
87
|
+
line: definition.line,
|
|
88
|
+
column: 0,
|
|
89
|
+
endLine: definition.endLine,
|
|
90
|
+
count: definition.count
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const line of current.lines) {
|
|
96
|
+
const existing = file.lines.get(line.number);
|
|
97
|
+
if (existing === undefined || line.count > existing) {
|
|
98
|
+
file.lines.set(line.number, line.count);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const branch of current.branches) {
|
|
102
|
+
const key = `${branch.line}:${branch.block}:${branch.branch}`;
|
|
103
|
+
const existing = file.branches.get(key);
|
|
104
|
+
if (existing) existing.count += branch.taken;
|
|
105
|
+
else {
|
|
106
|
+
file.branches.set(key, {
|
|
107
|
+
key,
|
|
108
|
+
line: branch.line,
|
|
109
|
+
column: 0,
|
|
110
|
+
type: "branch",
|
|
111
|
+
groupLine: branch.line,
|
|
112
|
+
index: branch.branch,
|
|
113
|
+
count: branch.taken
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const mapKey = canonicalPath(file.absPath);
|
|
118
|
+
const existingFile = files.get(mapKey);
|
|
119
|
+
if (existingFile) mergeFileCoverage(existingFile, file);
|
|
120
|
+
else files.set(mapKey, file);
|
|
121
|
+
current = null;
|
|
122
|
+
functionDefs = [];
|
|
123
|
+
functionHits = [];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
127
|
+
for (const rawLine of lines) {
|
|
128
|
+
const line = rawLine.trimEnd();
|
|
129
|
+
if (line.startsWith("SF:")) {
|
|
130
|
+
if (current) flush();
|
|
131
|
+
current = { sourcePath: line.slice(3).trim(), lines: [], branches: [] };
|
|
132
|
+
functionDefs = [];
|
|
133
|
+
functionHits = [];
|
|
134
|
+
sawAnyRecord = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!current) continue;
|
|
138
|
+
if (line === "end_of_record") {
|
|
139
|
+
flush();
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (line.startsWith("DA:")) {
|
|
143
|
+
const fields = line.slice(3).split(",");
|
|
144
|
+
const number = Number(fields[0]);
|
|
145
|
+
const count = Number(fields[1] ?? 0);
|
|
146
|
+
if (!Number.isInteger(number) || number <= 0 || !Number.isFinite(count)) {
|
|
147
|
+
throw new CoverageDataError(`${reportFile}: 无效的 DA 记录:${line}`);
|
|
148
|
+
}
|
|
149
|
+
current.lines.push({ number, count: Math.max(0, count) });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (line.startsWith("FN:")) {
|
|
153
|
+
const fields = line.slice(3).split(",");
|
|
154
|
+
if (fields.length >= 3 && /^\d+$/.test(fields[0]) && /^\d+$/.test(fields[1])) {
|
|
155
|
+
const startLine = Number(fields[0]);
|
|
156
|
+
const endLine = Number(fields[1]);
|
|
157
|
+
const name = fields.slice(2).join(",");
|
|
158
|
+
if (name) functionDefs.push({ name, line: startLine, endLine });
|
|
159
|
+
} else {
|
|
160
|
+
const startLine = Number(fields[0]);
|
|
161
|
+
const name = fields.slice(1).join(",");
|
|
162
|
+
if (Number.isInteger(startLine) && startLine > 0 && name) {
|
|
163
|
+
functionDefs.push({ name, line: startLine, endLine: startLine });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (line.startsWith("FNDA:")) {
|
|
169
|
+
const separator = line.indexOf(",", 5);
|
|
170
|
+
if (separator === -1) continue;
|
|
171
|
+
const count = Number(line.slice(5, separator));
|
|
172
|
+
const name = line.slice(separator + 1);
|
|
173
|
+
if (Number.isFinite(count) && name) {
|
|
174
|
+
functionHits.push({ name, count: Math.max(0, count) });
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (line.startsWith("BRDA:")) {
|
|
179
|
+
const fields = line.slice(5).split(",");
|
|
180
|
+
if (fields.length < 4) continue;
|
|
181
|
+
const number = Number(fields[0]);
|
|
182
|
+
const block = Number(fields[1]);
|
|
183
|
+
const branch = Number(fields[2]);
|
|
184
|
+
const takenRaw = fields[3];
|
|
185
|
+
const taken = takenRaw === "-" ? 0 : Number(takenRaw);
|
|
186
|
+
if (
|
|
187
|
+
!Number.isInteger(number) ||
|
|
188
|
+
!Number.isInteger(block) ||
|
|
189
|
+
!Number.isInteger(branch) ||
|
|
190
|
+
!Number.isFinite(taken)
|
|
191
|
+
) {
|
|
192
|
+
throw new CoverageDataError(`${reportFile}: 无效的 BRDA 记录:${line}`);
|
|
193
|
+
}
|
|
194
|
+
current.branches.push({
|
|
195
|
+
line: number,
|
|
196
|
+
block,
|
|
197
|
+
branch,
|
|
198
|
+
taken: Math.max(0, taken)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (current) flush();
|
|
203
|
+
if (!sawAnyRecord) {
|
|
204
|
+
throw new CoverageDataError(`${reportFile} 中没有找到任何 LCOV SF 记录`);
|
|
205
|
+
}
|
|
206
|
+
return files;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {string[]} reportFiles
|
|
211
|
+
* @param {string} root
|
|
212
|
+
*/
|
|
213
|
+
export async function parseLcovReports(reportFiles, root) {
|
|
214
|
+
/** @type {Map<string, import("./model.js").FileCoverage>} */
|
|
215
|
+
const files = new Map();
|
|
216
|
+
for (const reportFile of reportFiles) {
|
|
217
|
+
let text;
|
|
218
|
+
try {
|
|
219
|
+
text = await readFile(reportFile, "utf8");
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new CoverageDataError(
|
|
222
|
+
`无法读取 LCOV 报告 ${reportFile}: ${error.message}`,
|
|
223
|
+
{ cause: error }
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const parsed = parseLcovText(text.replace(/^\uFEFF/, ""), reportFile, root);
|
|
227
|
+
for (const [key, file] of parsed) {
|
|
228
|
+
const existing = files.get(key);
|
|
229
|
+
if (existing) mergeFileCoverage(existing, file);
|
|
230
|
+
else files.set(key, file);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return files;
|
|
234
|
+
}
|