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,343 @@
|
|
|
1
|
+
import {
|
|
2
|
+
displayPercent,
|
|
3
|
+
escapeMarkdown,
|
|
4
|
+
formatDuration,
|
|
5
|
+
formatThreshold
|
|
6
|
+
} from "../util.js";
|
|
7
|
+
|
|
8
|
+
const METRICS = ["lines", "statements", "functions", "branches"];
|
|
9
|
+
const METRIC_LABELS = {
|
|
10
|
+
lines: "行",
|
|
11
|
+
statements: "语句",
|
|
12
|
+
functions: "函数",
|
|
13
|
+
branches: "分支"
|
|
14
|
+
};
|
|
15
|
+
const BT = String.fromCharCode(96);
|
|
16
|
+
const FENCE = BT.repeat(3);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {unknown} value
|
|
20
|
+
*/
|
|
21
|
+
function code(value) {
|
|
22
|
+
return BT + String(value) + BT;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} value
|
|
27
|
+
*/
|
|
28
|
+
function fence(value) {
|
|
29
|
+
let marker = FENCE;
|
|
30
|
+
while (value.includes(marker)) marker += BT;
|
|
31
|
+
return [marker + "text", value, marker];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {any} scope
|
|
36
|
+
*/
|
|
37
|
+
function scopeTableRow(scope) {
|
|
38
|
+
const cells = [
|
|
39
|
+
escapeMarkdown(scope.label) + " (" + code(scope.name) + ")",
|
|
40
|
+
scope.profile
|
|
41
|
+
];
|
|
42
|
+
for (const metric of METRICS) {
|
|
43
|
+
const value = scope.metrics[metric];
|
|
44
|
+
if (!value.applicable || value.percent === null) {
|
|
45
|
+
cells.push("N/A");
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
cells.push(
|
|
49
|
+
displayPercent(value.percent) +
|
|
50
|
+
" (" +
|
|
51
|
+
value.covered +
|
|
52
|
+
"/" +
|
|
53
|
+
value.total +
|
|
54
|
+
") / " +
|
|
55
|
+
formatThreshold(value.threshold) +
|
|
56
|
+
"% " +
|
|
57
|
+
(value.passed ? "✓" : "✗")
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
cells.push(scope.passed ? "✓ 通过" : "✗ 未通过");
|
|
61
|
+
return "| " + cells.join(" | ") + " |";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {any} report
|
|
66
|
+
*/
|
|
67
|
+
export function renderMarkdownReport(report) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
const statusText =
|
|
70
|
+
report.status === "pass"
|
|
71
|
+
? "通过"
|
|
72
|
+
: report.status === "fail"
|
|
73
|
+
? "未通过"
|
|
74
|
+
: "运行失败";
|
|
75
|
+
lines.push("# CovGate 覆盖率报告");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("- 状态:**" + statusText + "**");
|
|
78
|
+
lines.push("- 生成时间:" + report.generatedAt);
|
|
79
|
+
lines.push(
|
|
80
|
+
"- 模式:" + (report.mode === "check" ? "只检查已有报告" : "执行测试并检查")
|
|
81
|
+
);
|
|
82
|
+
lines.push(
|
|
83
|
+
"- 配置:" +
|
|
84
|
+
code(report.config.path) +
|
|
85
|
+
"(sha256: " +
|
|
86
|
+
code(report.config.sha256 ?? "n/a") +
|
|
87
|
+
")"
|
|
88
|
+
);
|
|
89
|
+
lines.push("- 用时:" + formatDuration(report.durationMs));
|
|
90
|
+
lines.push("");
|
|
91
|
+
|
|
92
|
+
for (const issue of report.issues) {
|
|
93
|
+
lines.push(
|
|
94
|
+
"> " + (issue.severity === "error" ? "✗" : "!") + " " + issue.message
|
|
95
|
+
);
|
|
96
|
+
if (issue.outputTail) {
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push(...fence(issue.outputTail));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (report.issues.length > 0) lines.push("");
|
|
102
|
+
|
|
103
|
+
lines.push("## 总览");
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push("| Scope | 类型 | 行 | 语句 | 函数 | 分支 | 结果 |");
|
|
106
|
+
lines.push("| --- | --- | --- | --- | --- | --- | --- |");
|
|
107
|
+
for (const scope of report.scopes) lines.push(scopeTableRow(scope));
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push(
|
|
110
|
+
"总计:scope " +
|
|
111
|
+
report.summary.scopes +
|
|
112
|
+
" 个,通过 " +
|
|
113
|
+
report.summary.passedScopes +
|
|
114
|
+
" 个;豁免 " +
|
|
115
|
+
report.summary.waivers +
|
|
116
|
+
" 条;配置排除 " +
|
|
117
|
+
report.summary.exclusions +
|
|
118
|
+
" 条;缺失源码报告 " +
|
|
119
|
+
report.summary.missingSources +
|
|
120
|
+
" 个。"
|
|
121
|
+
);
|
|
122
|
+
lines.push("");
|
|
123
|
+
|
|
124
|
+
for (const scope of report.scopes) {
|
|
125
|
+
lines.push("## " + escapeMarkdown(scope.label) + "(" + scope.name + ")");
|
|
126
|
+
lines.push("");
|
|
127
|
+
lines.push("- 结果:**" + (scope.passed ? "通过" : "未通过") + "**");
|
|
128
|
+
lines.push("- 覆盖率格式:" + code(scope.format));
|
|
129
|
+
lines.push(
|
|
130
|
+
"- 源码文件:" +
|
|
131
|
+
scope.expectedSourceCount +
|
|
132
|
+
";报告文件:" +
|
|
133
|
+
scope.reportedFileCount
|
|
134
|
+
);
|
|
135
|
+
lines.push(
|
|
136
|
+
"- 豁免:" +
|
|
137
|
+
scope.waivers.length +
|
|
138
|
+
";配置排除:" +
|
|
139
|
+
scope.exclusions.length
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (scope.missingSources.length > 0) {
|
|
143
|
+
lines.push("");
|
|
144
|
+
lines.push("### 缺失覆盖率报告");
|
|
145
|
+
lines.push("");
|
|
146
|
+
for (const file of scope.missingSources) lines.push("- " + code(file));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const issues = scope.issues.concat(
|
|
150
|
+
scope.warnings.map((warning) => ({ ...warning, severity: "warning" }))
|
|
151
|
+
);
|
|
152
|
+
if (issues.length > 0) {
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push("### 问题");
|
|
155
|
+
lines.push("");
|
|
156
|
+
for (const issue of issues) {
|
|
157
|
+
const location = issue.file ? " " + code(issue.file) : "";
|
|
158
|
+
lines.push(
|
|
159
|
+
"- **" + issue.severity + "**" + location + ": " + issue.message
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
lines.push("");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (scope.failingFiles.length > 0) {
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("### 未覆盖明细");
|
|
168
|
+
lines.push("");
|
|
169
|
+
for (const file of scope.failingFiles) {
|
|
170
|
+
lines.push("#### " + code(file.path));
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push(
|
|
173
|
+
"- 行:" +
|
|
174
|
+
displayPercent(file.metrics.lines?.percent ?? null) +
|
|
175
|
+
" (" +
|
|
176
|
+
(file.metrics.lines?.covered ?? 0) +
|
|
177
|
+
"/" +
|
|
178
|
+
(file.metrics.lines?.total ?? 0) +
|
|
179
|
+
")"
|
|
180
|
+
);
|
|
181
|
+
if (file.metrics.statements) {
|
|
182
|
+
lines.push(
|
|
183
|
+
"- 语句:" +
|
|
184
|
+
displayPercent(file.metrics.statements.percent) +
|
|
185
|
+
" (" +
|
|
186
|
+
file.metrics.statements.covered +
|
|
187
|
+
"/" +
|
|
188
|
+
file.metrics.statements.total +
|
|
189
|
+
")"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
lines.push(
|
|
193
|
+
"- 函数:" +
|
|
194
|
+
displayPercent(file.metrics.functions.percent) +
|
|
195
|
+
" (" +
|
|
196
|
+
file.metrics.functions.covered +
|
|
197
|
+
"/" +
|
|
198
|
+
file.metrics.functions.total +
|
|
199
|
+
")"
|
|
200
|
+
);
|
|
201
|
+
lines.push(
|
|
202
|
+
"- 分支:" +
|
|
203
|
+
displayPercent(file.metrics.branches.percent) +
|
|
204
|
+
" (" +
|
|
205
|
+
file.metrics.branches.covered +
|
|
206
|
+
"/" +
|
|
207
|
+
file.metrics.branches.total +
|
|
208
|
+
")"
|
|
209
|
+
);
|
|
210
|
+
if (file.uncovered.linesTotal > 0) {
|
|
211
|
+
lines.push(
|
|
212
|
+
"- 未覆盖行:" + code(file.uncovered.lines.join(", "))
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (file.uncovered.statementsTotal > 0) {
|
|
216
|
+
lines.push(
|
|
217
|
+
"- 未覆盖语句行:" +
|
|
218
|
+
code(file.uncovered.statements.map((unit) => unit.line).join(", "))
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (file.uncovered.functionsTotal > 0) {
|
|
222
|
+
lines.push(
|
|
223
|
+
"- 未覆盖函数:" +
|
|
224
|
+
file.uncovered.functions
|
|
225
|
+
.map((unit) => code(unit.name) + " @L" + unit.line)
|
|
226
|
+
.join(", ")
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (file.uncovered.branchesTotal > 0) {
|
|
230
|
+
lines.push(
|
|
231
|
+
"- 未覆盖分支:" +
|
|
232
|
+
file.uncovered.branches
|
|
233
|
+
.map((unit) => "L" + unit.line + "#" + unit.index)
|
|
234
|
+
.join(", ")
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
lines.push("");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (scope.waivers.length > 0) {
|
|
242
|
+
lines.push("### 豁免审计");
|
|
243
|
+
lines.push("");
|
|
244
|
+
lines.push("| 文件 | 行 | 状态 | 原因 | owner | expires | ticket |");
|
|
245
|
+
lines.push("| --- | --- | --- | --- | --- | --- | --- |");
|
|
246
|
+
for (const waiver of scope.waivers) {
|
|
247
|
+
const range =
|
|
248
|
+
waiver.kind === "file"
|
|
249
|
+
? "整个文件"
|
|
250
|
+
: waiver.startLine + "-" + waiver.endLine;
|
|
251
|
+
lines.push(
|
|
252
|
+
"| " +
|
|
253
|
+
code(escapeMarkdown(waiver.file)) +
|
|
254
|
+
" | " +
|
|
255
|
+
range +
|
|
256
|
+
" | " +
|
|
257
|
+
waiver.status +
|
|
258
|
+
" | " +
|
|
259
|
+
escapeMarkdown(waiver.reason) +
|
|
260
|
+
" | " +
|
|
261
|
+
escapeMarkdown(waiver.owner ?? "") +
|
|
262
|
+
" | " +
|
|
263
|
+
(waiver.expires ?? "") +
|
|
264
|
+
" | " +
|
|
265
|
+
escapeMarkdown(waiver.ticket ?? "") +
|
|
266
|
+
" |"
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
lines.push("");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (scope.exclusions.length > 0) {
|
|
273
|
+
lines.push("### 配置排除");
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push("| 模式 | 文件数 | 状态 | 原因 | owner | expires |");
|
|
276
|
+
lines.push("| --- | --- | --- | --- | --- | --- |");
|
|
277
|
+
for (const exclusion of scope.exclusions) {
|
|
278
|
+
lines.push(
|
|
279
|
+
"| " +
|
|
280
|
+
code(escapeMarkdown(exclusion.pattern)) +
|
|
281
|
+
" | " +
|
|
282
|
+
exclusion.fileCount +
|
|
283
|
+
" | " +
|
|
284
|
+
exclusion.status +
|
|
285
|
+
" | " +
|
|
286
|
+
escapeMarkdown(exclusion.reason) +
|
|
287
|
+
" | " +
|
|
288
|
+
escapeMarkdown(exclusion.owner ?? "") +
|
|
289
|
+
" | " +
|
|
290
|
+
(exclusion.expires ?? "") +
|
|
291
|
+
" |"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
lines.push("");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (report.commands.length > 0) {
|
|
299
|
+
lines.push("## 测试命令");
|
|
300
|
+
lines.push("");
|
|
301
|
+
lines.push("| Scope | 命令 | 退出码 | 用时 |");
|
|
302
|
+
lines.push("| --- | --- | --- | --- |");
|
|
303
|
+
for (const command of report.commands) {
|
|
304
|
+
lines.push(
|
|
305
|
+
"| " +
|
|
306
|
+
command.scope +
|
|
307
|
+
" | " +
|
|
308
|
+
code(escapeMarkdown(command.command)) +
|
|
309
|
+
" | " +
|
|
310
|
+
(command.exitCode ?? "null") +
|
|
311
|
+
" | " +
|
|
312
|
+
formatDuration(command.durationMs) +
|
|
313
|
+
" |"
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
lines.push("");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const targetLabels = report.scopes
|
|
320
|
+
.map(
|
|
321
|
+
(scope) =>
|
|
322
|
+
scope.label +
|
|
323
|
+
":" +
|
|
324
|
+
METRICS.filter((metric) => scope.metrics[metric].applicable)
|
|
325
|
+
.map(
|
|
326
|
+
(metric) =>
|
|
327
|
+
METRIC_LABELS[metric] +
|
|
328
|
+
"≥" +
|
|
329
|
+
formatThreshold(scope.metrics[metric].threshold) +
|
|
330
|
+
"%"
|
|
331
|
+
)
|
|
332
|
+
.join("、")
|
|
333
|
+
)
|
|
334
|
+
.filter(Boolean);
|
|
335
|
+
if (targetLabels.length > 0) {
|
|
336
|
+
lines.push("## 门槛");
|
|
337
|
+
lines.push("");
|
|
338
|
+
for (const label of targetLabels) lines.push("- " + label);
|
|
339
|
+
lines.push("");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return lines.join("\n") + "\n";
|
|
343
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
displayPercent,
|
|
3
|
+
formatDuration,
|
|
4
|
+
formatThreshold
|
|
5
|
+
} from "../util.js";
|
|
6
|
+
|
|
7
|
+
const METRIC_LABELS = {
|
|
8
|
+
lines: "行",
|
|
9
|
+
statements: "语句",
|
|
10
|
+
functions: "函数",
|
|
11
|
+
branches: "分支"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ESC = String.fromCharCode(27);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {any} report
|
|
18
|
+
* @param {{ color?: boolean }} [options]
|
|
19
|
+
*/
|
|
20
|
+
export function renderTextReport(report, options = {}) {
|
|
21
|
+
const useColor = options.color === true && !process.env.NO_COLOR;
|
|
22
|
+
const paint = (code, value) =>
|
|
23
|
+
useColor ? ESC + "[" + code + "m" + value + ESC + "[0m" : value;
|
|
24
|
+
const green = (value) => paint("32", value);
|
|
25
|
+
const red = (value) => paint("31", value);
|
|
26
|
+
const yellow = (value) => paint("33", value);
|
|
27
|
+
const dim = (value) => paint("2", value);
|
|
28
|
+
const bold = (value) => paint("1", value);
|
|
29
|
+
const symbols = {
|
|
30
|
+
pass: green("✓"),
|
|
31
|
+
fail: red("✗"),
|
|
32
|
+
error: red("✗"),
|
|
33
|
+
warning: yellow("!")
|
|
34
|
+
};
|
|
35
|
+
const lines = [];
|
|
36
|
+
|
|
37
|
+
lines.push(bold("CovGate · 覆盖率门禁 v" + report.tool.version));
|
|
38
|
+
lines.push(
|
|
39
|
+
dim(
|
|
40
|
+
"模式 " +
|
|
41
|
+
(report.mode === "check" ? "只检查已有报告" : "执行测试并检查") +
|
|
42
|
+
" · 配置 " +
|
|
43
|
+
report.config.path +
|
|
44
|
+
" · 用时 " +
|
|
45
|
+
formatDuration(report.durationMs)
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
lines.push("");
|
|
49
|
+
|
|
50
|
+
for (const issue of report.issues) {
|
|
51
|
+
lines.push((symbols[issue.severity] ?? "?") + " " + issue.message);
|
|
52
|
+
if (issue.outputTail) lines.push(dim(issue.outputTail));
|
|
53
|
+
}
|
|
54
|
+
if (report.issues.length > 0) lines.push("");
|
|
55
|
+
|
|
56
|
+
for (const scope of report.scopes) {
|
|
57
|
+
const symbol = scope.passed ? symbols.pass : symbols.fail;
|
|
58
|
+
lines.push(
|
|
59
|
+
symbol +
|
|
60
|
+
" " +
|
|
61
|
+
bold(scope.label) +
|
|
62
|
+
" " +
|
|
63
|
+
dim("[" + scope.name + "/" + scope.profile + "]") +
|
|
64
|
+
" " +
|
|
65
|
+
(scope.passed ? green("通过") : red("未通过"))
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const metricParts = [];
|
|
69
|
+
for (const metric of ["lines", "statements", "functions", "branches"]) {
|
|
70
|
+
const value = scope.metrics[metric];
|
|
71
|
+
if (!value.applicable || value.percent === null) {
|
|
72
|
+
metricParts.push(METRIC_LABELS[metric] + " N/A");
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const state = value.passed ? "" : red(" ✗");
|
|
76
|
+
metricParts.push(
|
|
77
|
+
METRIC_LABELS[metric] +
|
|
78
|
+
" " +
|
|
79
|
+
displayPercent(value.percent) +
|
|
80
|
+
" " +
|
|
81
|
+
dim(
|
|
82
|
+
"(" +
|
|
83
|
+
value.covered +
|
|
84
|
+
"/" +
|
|
85
|
+
value.total +
|
|
86
|
+
", 目标 " +
|
|
87
|
+
formatThreshold(value.threshold) +
|
|
88
|
+
"%)"
|
|
89
|
+
) +
|
|
90
|
+
state
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
lines.push(" " + metricParts.join(" · "));
|
|
94
|
+
lines.push(
|
|
95
|
+
dim(
|
|
96
|
+
" 源码 " +
|
|
97
|
+
scope.expectedSourceCount +
|
|
98
|
+
" 个 · 报告 " +
|
|
99
|
+
scope.reportedFileCount +
|
|
100
|
+
" 个 · 豁免 " +
|
|
101
|
+
scope.waivers.length +
|
|
102
|
+
" 条 · 排除 " +
|
|
103
|
+
scope.exclusions.length +
|
|
104
|
+
" 条"
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const errorIssues = scope.issues.filter((issue) => issue.severity === "error");
|
|
109
|
+
const warningIssues = scope.warnings.concat(
|
|
110
|
+
scope.issues.filter((issue) => issue.severity === "warning")
|
|
111
|
+
);
|
|
112
|
+
const shownIssues = errorIssues.concat(warningIssues).slice(0, 8);
|
|
113
|
+
if (shownIssues.length > 0) {
|
|
114
|
+
lines.push("");
|
|
115
|
+
for (const issue of shownIssues) {
|
|
116
|
+
const prefix = issue.severity === "error" ? red(" ✗") : yellow(" !");
|
|
117
|
+
const location = issue.file
|
|
118
|
+
? " " + issue.file + (issue.line ? ":" + issue.line : "")
|
|
119
|
+
: "";
|
|
120
|
+
lines.push(prefix + location + " " + issue.message);
|
|
121
|
+
}
|
|
122
|
+
const rest = errorIssues.length + warningIssues.length - shownIssues.length;
|
|
123
|
+
if (rest > 0) lines.push(dim(" … 还有 " + rest + " 条问题,见报告文件"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!scope.passed && scope.failingFiles.length > 0) {
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push(" 未覆盖明细:");
|
|
129
|
+
for (const file of scope.failingFiles.slice(0, 12)) {
|
|
130
|
+
const percent = file.metrics.lines?.percent;
|
|
131
|
+
lines.push(
|
|
132
|
+
" " +
|
|
133
|
+
file.path +
|
|
134
|
+
(percent === null || percent === undefined
|
|
135
|
+
? ""
|
|
136
|
+
: dim(" (行 " + displayPercent(percent) + ")"))
|
|
137
|
+
);
|
|
138
|
+
if (file.uncovered.linesTotal > 0) {
|
|
139
|
+
const values = file.uncovered.lines.slice(0, 20).join(", ");
|
|
140
|
+
const more =
|
|
141
|
+
file.uncovered.linesTotal > 20
|
|
142
|
+
? " … + " + (file.uncovered.linesTotal - 20)
|
|
143
|
+
: "";
|
|
144
|
+
lines.push(" 行: " + values + more);
|
|
145
|
+
}
|
|
146
|
+
if ((file.uncovered.statementsTotal ?? 0) > 0) {
|
|
147
|
+
const values = file.uncovered.statements
|
|
148
|
+
.map((unit) => "L" + unit.line)
|
|
149
|
+
.slice(0, 15)
|
|
150
|
+
.join(", ");
|
|
151
|
+
const more =
|
|
152
|
+
(file.uncovered.statementsTotal ?? 0) > 15
|
|
153
|
+
? " … + " + ((file.uncovered.statementsTotal ?? 0) - 15)
|
|
154
|
+
: "";
|
|
155
|
+
lines.push(" 语句: " + values + more);
|
|
156
|
+
}
|
|
157
|
+
if (file.uncovered.functionsTotal > 0) {
|
|
158
|
+
const values = file.uncovered.functions
|
|
159
|
+
.slice(0, 10)
|
|
160
|
+
.map((unit) => unit.name + "@L" + unit.line)
|
|
161
|
+
.join(", ");
|
|
162
|
+
const more =
|
|
163
|
+
file.uncovered.functionsTotal > 10
|
|
164
|
+
? " … + " + (file.uncovered.functionsTotal - 10)
|
|
165
|
+
: "";
|
|
166
|
+
lines.push(" 函数: " + values + more);
|
|
167
|
+
}
|
|
168
|
+
if (file.uncovered.branchesTotal > 0) {
|
|
169
|
+
const values = file.uncovered.branches
|
|
170
|
+
.slice(0, 10)
|
|
171
|
+
.map((unit) => "L" + unit.line + "#" + unit.index)
|
|
172
|
+
.join(", ");
|
|
173
|
+
const more =
|
|
174
|
+
file.uncovered.branchesTotal > 10
|
|
175
|
+
? " … + " + (file.uncovered.branchesTotal - 10)
|
|
176
|
+
: "";
|
|
177
|
+
lines.push(" 分支: " + values + more);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (scope.failingFiles.length > 12) {
|
|
181
|
+
lines.push(
|
|
182
|
+
dim(" … 还有 " + (scope.failingFiles.length - 12) + " 个文件,见报告文件")
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (scope.exclusions.length > 0) {
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push(" 配置排除(均需人工复核):");
|
|
190
|
+
for (const exclusion of scope.exclusions.slice(0, 10)) {
|
|
191
|
+
const status =
|
|
192
|
+
exclusion.status === "applied"
|
|
193
|
+
? ""
|
|
194
|
+
: red(" [" + exclusion.status + "]");
|
|
195
|
+
lines.push(
|
|
196
|
+
" " +
|
|
197
|
+
exclusion.pattern +
|
|
198
|
+
" (" +
|
|
199
|
+
exclusion.fileCount +
|
|
200
|
+
" 个文件) — " +
|
|
201
|
+
exclusion.reason +
|
|
202
|
+
status
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (scope.exclusions.length > 10) {
|
|
206
|
+
lines.push(
|
|
207
|
+
dim(" … 还有 " + (scope.exclusions.length - 10) + " 条,见报告文件")
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (scope.waivers.length > 0) {
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push(" 豁免(均需人工复核):");
|
|
215
|
+
for (const waiver of scope.waivers.slice(0, 10)) {
|
|
216
|
+
const location =
|
|
217
|
+
waiver.kind === "file"
|
|
218
|
+
? waiver.file
|
|
219
|
+
: waiver.file +
|
|
220
|
+
":" +
|
|
221
|
+
waiver.startLine +
|
|
222
|
+
(waiver.endLine !== waiver.startLine
|
|
223
|
+
? "-" + waiver.endLine
|
|
224
|
+
: "");
|
|
225
|
+
const status =
|
|
226
|
+
waiver.status === "applied" ? "" : red(" [" + waiver.status + "]");
|
|
227
|
+
const meta = [
|
|
228
|
+
waiver.owner ? "owner=" + waiver.owner : null,
|
|
229
|
+
waiver.expires ? "expires=" + waiver.expires : null
|
|
230
|
+
]
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.join(", ");
|
|
233
|
+
lines.push(
|
|
234
|
+
" " +
|
|
235
|
+
location +
|
|
236
|
+
" — " +
|
|
237
|
+
waiver.reason +
|
|
238
|
+
(meta ? dim(" (" + meta + ")") : "") +
|
|
239
|
+
status
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (scope.waivers.length > 10) {
|
|
243
|
+
lines.push(dim(" … 还有 " + (scope.waivers.length - 10) + " 条,见报告文件"));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
lines.push("");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const statusColor = report.status === "pass" ? green : red;
|
|
250
|
+
const statusText =
|
|
251
|
+
report.status === "pass"
|
|
252
|
+
? "通过"
|
|
253
|
+
: report.status === "fail"
|
|
254
|
+
? "未通过"
|
|
255
|
+
: "运行失败";
|
|
256
|
+
lines.push(bold("结果") + ": " + statusColor(statusText));
|
|
257
|
+
if (report.artifacts.json) {
|
|
258
|
+
lines.push(dim("JSON 报告: " + report.artifacts.json));
|
|
259
|
+
}
|
|
260
|
+
if (report.artifacts.markdown) {
|
|
261
|
+
lines.push(dim("Markdown 报告: " + report.artifacts.markdown));
|
|
262
|
+
}
|
|
263
|
+
if (report.status !== "pass" && report.summary.failedScopes.length > 0) {
|
|
264
|
+
lines.push(dim("失败 scope: " + report.summary.failedScopes.join(", ")));
|
|
265
|
+
}
|
|
266
|
+
return lines.join("\n") + "\n";
|
|
267
|
+
}
|