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
package/src/args.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { ConfigError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
const COMMANDS = new Set(["run", "check", "validate", "setup"]);
|
|
4
|
+
const NL = String.fromCharCode(10);
|
|
5
|
+
|
|
6
|
+
export const HELP_TEXT = [
|
|
7
|
+
"CovGate — AI 代码覆盖率门禁",
|
|
8
|
+
"",
|
|
9
|
+
"用法:",
|
|
10
|
+
" covgate setup [选项] 检测/安装测试框架并生成门禁与 AI 接入配置",
|
|
11
|
+
" covgate run [选项] 执行测试并检查覆盖率(默认命令)",
|
|
12
|
+
" covgate check [选项] 不执行测试,只检查已有覆盖率报告",
|
|
13
|
+
" covgate validate [选项] 只校验配置",
|
|
14
|
+
"",
|
|
15
|
+
"通用选项:",
|
|
16
|
+
" --config <path> 配置文件路径(默认 ./covgate.config.json)",
|
|
17
|
+
" --scope <name> 只检查指定 scope,可重复或用逗号分隔",
|
|
18
|
+
" --no-run 等同 check:不执行测试命令",
|
|
19
|
+
" --json 向 stdout 输出纯 JSON(日志走 stderr)",
|
|
20
|
+
" --no-files 不写 .covgate/report.json 和 report.md",
|
|
21
|
+
" --report-dir <path> 覆盖报告输出目录(相对当前目录)",
|
|
22
|
+
" --verbose 实时转发测试命令输出到 stderr",
|
|
23
|
+
" --color / --no-color 强制开启/关闭彩色输出",
|
|
24
|
+
" --version 显示版本",
|
|
25
|
+
" --help 显示帮助",
|
|
26
|
+
"",
|
|
27
|
+
"setup 选项:",
|
|
28
|
+
" --yes, -y 无框架时直接安装 Vitest,并接受默认配置",
|
|
29
|
+
" --no-install 不安装测试框架",
|
|
30
|
+
" --no-agent 不生成 AGENTS.md / Agent 脚本 / CI",
|
|
31
|
+
" --no-config 不生成 covgate.config.json",
|
|
32
|
+
" --profile <client|server> 强制 scope 类型,默认自动识别",
|
|
33
|
+
" --source <glob> 源码范围,默认自动识别",
|
|
34
|
+
" --coverage <path> 覆盖率报告路径,默认 coverage/coverage-final.json",
|
|
35
|
+
" --test-command <command> 覆盖测试命令",
|
|
36
|
+
" --force 覆盖已有配置和 setup 生成的文件",
|
|
37
|
+
" --dry-run 只打印计划,不写文件、不安装依赖",
|
|
38
|
+
"",
|
|
39
|
+
"硬性门槛(配置不可低于):",
|
|
40
|
+
" profile=client 行/语句/函数/分支覆盖率必须 100%",
|
|
41
|
+
" profile=server 行/语句/函数/分支覆盖率必须至少 95%",
|
|
42
|
+
"",
|
|
43
|
+
"覆盖不到的代码必须写 covgate-ignore-* 说明,例如:",
|
|
44
|
+
" // covgate-ignore-next-line: 平台灰度开关在 v1 永远不会开启 | owner=web | expires=2027-12-31",
|
|
45
|
+
" // covgate-ignore-file: OpenAPI 生成文件,不手工维护 | owner=api",
|
|
46
|
+
"",
|
|
47
|
+
"退出码:0 通过;1 覆盖率/审计不通过;2 配置或用法错误;3 测试失败/报告读取失败。",
|
|
48
|
+
"",
|
|
49
|
+
"参见 README.md 与 docs/design.md。"
|
|
50
|
+
].join(NL);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string[]} argv
|
|
54
|
+
*/
|
|
55
|
+
export function parseArgs(argv) {
|
|
56
|
+
const options = {
|
|
57
|
+
command: "run",
|
|
58
|
+
configPath: undefined,
|
|
59
|
+
scopes: [],
|
|
60
|
+
json: false,
|
|
61
|
+
noRun: false,
|
|
62
|
+
noFiles: false,
|
|
63
|
+
reportDir: undefined,
|
|
64
|
+
verbose: false,
|
|
65
|
+
color: undefined,
|
|
66
|
+
help: false,
|
|
67
|
+
version: false,
|
|
68
|
+
yes: false,
|
|
69
|
+
installFramework: true,
|
|
70
|
+
noAgent: false,
|
|
71
|
+
noConfig: false,
|
|
72
|
+
force: false,
|
|
73
|
+
dryRun: false,
|
|
74
|
+
profile: undefined,
|
|
75
|
+
source: undefined,
|
|
76
|
+
coverage: undefined,
|
|
77
|
+
testCommand: undefined
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let index = 0;
|
|
81
|
+
let explicitCommand = false;
|
|
82
|
+
if (argv[0] === "help") {
|
|
83
|
+
options.help = true;
|
|
84
|
+
return options;
|
|
85
|
+
}
|
|
86
|
+
if (argv[0] && COMMANDS.has(argv[0])) {
|
|
87
|
+
options.command = argv[0];
|
|
88
|
+
explicitCommand = true;
|
|
89
|
+
index = 1;
|
|
90
|
+
} else if (argv[0] && !argv[0].startsWith("-")) {
|
|
91
|
+
throw new ConfigError('未知命令 "' + argv[0] + '",可用命令:setup、run、check、validate');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (; index < argv.length; index += 1) {
|
|
95
|
+
const argument = argv[index];
|
|
96
|
+
let key = argument;
|
|
97
|
+
let inlineValue;
|
|
98
|
+
if (argument.startsWith("--") && argument.includes("=")) {
|
|
99
|
+
const separator = argument.indexOf("=");
|
|
100
|
+
key = argument.slice(0, separator);
|
|
101
|
+
inlineValue = argument.slice(separator + 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const value = () => {
|
|
105
|
+
if (inlineValue !== undefined) return inlineValue;
|
|
106
|
+
index += 1;
|
|
107
|
+
if (index >= argv.length || argv[index].startsWith("--")) {
|
|
108
|
+
throw new ConfigError("选项 " + key + " 缺少值");
|
|
109
|
+
}
|
|
110
|
+
return argv[index];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (argument === "help") {
|
|
114
|
+
options.help = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (COMMANDS.has(argument) && !explicitCommand && !argument.startsWith("-")) {
|
|
118
|
+
options.command = argument;
|
|
119
|
+
explicitCommand = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (COMMANDS.has(argument) && explicitCommand && index > 0) {
|
|
123
|
+
throw new ConfigError("命令只能指定一次");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (key === "--config") {
|
|
127
|
+
options.configPath = value();
|
|
128
|
+
} else if (key === "--scope") {
|
|
129
|
+
const scopeValue = value();
|
|
130
|
+
for (const name of scopeValue.split(",")) {
|
|
131
|
+
const trimmed = name.trim();
|
|
132
|
+
if (trimmed && !options.scopes.includes(trimmed)) options.scopes.push(trimmed);
|
|
133
|
+
}
|
|
134
|
+
} else if (key === "--json") {
|
|
135
|
+
options.json = true;
|
|
136
|
+
} else if (key === "--no-run") {
|
|
137
|
+
options.noRun = true;
|
|
138
|
+
} else if (key === "--no-files") {
|
|
139
|
+
options.noFiles = true;
|
|
140
|
+
} else if (key === "--report-dir") {
|
|
141
|
+
options.reportDir = value();
|
|
142
|
+
} else if (key === "--verbose") {
|
|
143
|
+
options.verbose = true;
|
|
144
|
+
} else if (key === "--color") {
|
|
145
|
+
options.color = true;
|
|
146
|
+
} else if (key === "--no-color") {
|
|
147
|
+
options.color = false;
|
|
148
|
+
} else if (key === "--yes" || key === "-y") {
|
|
149
|
+
options.yes = true;
|
|
150
|
+
} else if (key === "--no-install") {
|
|
151
|
+
options.installFramework = false;
|
|
152
|
+
} else if (key === "--no-agent") {
|
|
153
|
+
options.noAgent = true;
|
|
154
|
+
} else if (key === "--no-config") {
|
|
155
|
+
options.noConfig = true;
|
|
156
|
+
} else if (key === "--force") {
|
|
157
|
+
options.force = true;
|
|
158
|
+
} else if (key === "--dry-run") {
|
|
159
|
+
options.dryRun = true;
|
|
160
|
+
} else if (key === "--profile") {
|
|
161
|
+
options.profile = value();
|
|
162
|
+
} else if (key === "--source") {
|
|
163
|
+
options.source = value();
|
|
164
|
+
} else if (key === "--coverage") {
|
|
165
|
+
options.coverage = value();
|
|
166
|
+
} else if (key === "--test-command") {
|
|
167
|
+
options.testCommand = value();
|
|
168
|
+
} else if (key === "--help" || key === "-h") {
|
|
169
|
+
options.help = true;
|
|
170
|
+
} else if (key === "--version") {
|
|
171
|
+
options.version = true;
|
|
172
|
+
} else {
|
|
173
|
+
throw new ConfigError('未知选项 "' + argument + '";运行 covgate --help 查看用法');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (options.command === "check") options.noRun = true;
|
|
178
|
+
if (options.command !== "setup" && (options.yes || options.profile || options.source || options.coverage || options.testCommand || options.noAgent || options.noConfig || options.force || options.dryRun || !options.installFramework)) {
|
|
179
|
+
throw new ConfigError("setup 相关选项只能和 covgate setup 一起使用");
|
|
180
|
+
}
|
|
181
|
+
return options;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {string[]} argv
|
|
186
|
+
*/
|
|
187
|
+
export function wantsJson(argv) {
|
|
188
|
+
return argv.includes("--json");
|
|
189
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parseArgs, HELP_TEXT, wantsJson } from "./args.js";
|
|
3
|
+
import {
|
|
4
|
+
loadConfig,
|
|
5
|
+
resolveConfigPath,
|
|
6
|
+
selectScopes,
|
|
7
|
+
METRICS
|
|
8
|
+
} from "./config.js";
|
|
9
|
+
import { CovgateError, ExecutionError } from "./errors.js";
|
|
10
|
+
import { loadCoverage } from "./coverage/index.js";
|
|
11
|
+
import { gateScope } from "./gate.js";
|
|
12
|
+
import {
|
|
13
|
+
expandCoverageReportFiles,
|
|
14
|
+
findStaleReports,
|
|
15
|
+
snapshotReportFiles
|
|
16
|
+
} from "./reports.js";
|
|
17
|
+
import {
|
|
18
|
+
buildReport,
|
|
19
|
+
plannedArtifactPaths,
|
|
20
|
+
renderTextReport,
|
|
21
|
+
writeReportArtifacts
|
|
22
|
+
} from "./report/index.js";
|
|
23
|
+
import { printCommandFailure, runScopeCommands } from "./runner.js";
|
|
24
|
+
import { evaluateScope } from "./scope.js";
|
|
25
|
+
import { runSetup } from "./setup/index.js";
|
|
26
|
+
import { validateReason } from "./reason.js";
|
|
27
|
+
import {
|
|
28
|
+
formatThreshold,
|
|
29
|
+
relativeDisplay,
|
|
30
|
+
tail
|
|
31
|
+
} from "./util.js";
|
|
32
|
+
import { VERSION } from "./version.js";
|
|
33
|
+
|
|
34
|
+
const METRIC_LABELS = {
|
|
35
|
+
lines: "行",
|
|
36
|
+
statements: "语句",
|
|
37
|
+
functions: "函数",
|
|
38
|
+
branches: "分支"
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const NL = String.fromCharCode(10);
|
|
42
|
+
|
|
43
|
+
function logger() {
|
|
44
|
+
return {
|
|
45
|
+
info(message) {
|
|
46
|
+
process.stderr.write(message + "\n");
|
|
47
|
+
},
|
|
48
|
+
warn(message) {
|
|
49
|
+
process.stderr.write("警告: " + message + "\n");
|
|
50
|
+
},
|
|
51
|
+
error(message) {
|
|
52
|
+
process.stderr.write("错误: " + message + "\n");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {unknown} error
|
|
59
|
+
* @param {boolean} json
|
|
60
|
+
* @param {boolean} includeStack
|
|
61
|
+
*/
|
|
62
|
+
function printEarlyError(error, json, includeStack = false) {
|
|
63
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
+
const exitCode = error instanceof CovgateError ? error.exitCode : 2;
|
|
65
|
+
if (includeStack && error instanceof Error && error.stack) {
|
|
66
|
+
process.stderr.write(error.stack + "\n");
|
|
67
|
+
}
|
|
68
|
+
if (json) {
|
|
69
|
+
const body = {
|
|
70
|
+
schemaVersion: 1,
|
|
71
|
+
tool: { name: "covgate", version: VERSION },
|
|
72
|
+
status: "error",
|
|
73
|
+
exitCode,
|
|
74
|
+
generatedAt: new Date().toISOString(),
|
|
75
|
+
issues: [
|
|
76
|
+
{
|
|
77
|
+
code: error instanceof CovgateError ? error.kind : "error",
|
|
78
|
+
severity: "error",
|
|
79
|
+
message
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
process.stdout.write(JSON.stringify(body, null, 2) + "\n");
|
|
84
|
+
} else {
|
|
85
|
+
process.stderr.write("CovGate 错误: " + message + "\n");
|
|
86
|
+
}
|
|
87
|
+
return exitCode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {any} options
|
|
92
|
+
*/
|
|
93
|
+
async function runSetupCommand(options) {
|
|
94
|
+
const log = options.json
|
|
95
|
+
? (message) => process.stderr.write(message + NL)
|
|
96
|
+
: (message) => process.stdout.write(message + NL);
|
|
97
|
+
try {
|
|
98
|
+
const summary = await runSetup({
|
|
99
|
+
root: process.cwd(),
|
|
100
|
+
yes: options.yes,
|
|
101
|
+
installFramework: options.installFramework,
|
|
102
|
+
profile: options.profile,
|
|
103
|
+
source: options.source,
|
|
104
|
+
coverage: options.coverage,
|
|
105
|
+
testCommand: options.testCommand,
|
|
106
|
+
noAgent: options.noAgent,
|
|
107
|
+
noConfig: options.noConfig,
|
|
108
|
+
force: options.force,
|
|
109
|
+
dryRun: options.dryRun,
|
|
110
|
+
log
|
|
111
|
+
});
|
|
112
|
+
if (options.json) {
|
|
113
|
+
process.stdout.write(JSON.stringify({
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
tool: { name: "covgate", version: VERSION },
|
|
116
|
+
status: "ok",
|
|
117
|
+
...summary
|
|
118
|
+
}, null, 2) + NL);
|
|
119
|
+
}
|
|
120
|
+
return 0;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return printEarlyError(error, options.json, options.verbose);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {any} config
|
|
128
|
+
* @param {any[]} scopes
|
|
129
|
+
* @param {any} options
|
|
130
|
+
*/
|
|
131
|
+
function printValidate(config, scopes, options) {
|
|
132
|
+
const problems = [];
|
|
133
|
+
for (const scope of scopes) {
|
|
134
|
+
for (const entry of scope.exclude) {
|
|
135
|
+
const validation = validateReason(
|
|
136
|
+
entry.reason,
|
|
137
|
+
config.waivers,
|
|
138
|
+
{ owner: entry.owner, expires: entry.expires },
|
|
139
|
+
new Date()
|
|
140
|
+
);
|
|
141
|
+
for (const message of validation.errors) {
|
|
142
|
+
problems.push({
|
|
143
|
+
code: "invalid-exclusion",
|
|
144
|
+
severity: "error",
|
|
145
|
+
scope: scope.name,
|
|
146
|
+
message: "exclude \"" + entry.pattern + "\" 的说明无效:" + message
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (validation.expired) {
|
|
150
|
+
problems.push({
|
|
151
|
+
code: "expired-exclusion",
|
|
152
|
+
severity: config.waivers.expiry === "fail" ? "error" : "warning",
|
|
153
|
+
scope: scope.name,
|
|
154
|
+
message: "exclude \"" + entry.pattern + "\" 已于 " + entry.expires + " 过期"
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (problems.some((problem) => problem.severity === "error")) {
|
|
160
|
+
if (options.json) {
|
|
161
|
+
process.stdout.write(
|
|
162
|
+
JSON.stringify(
|
|
163
|
+
{
|
|
164
|
+
schemaVersion: 1,
|
|
165
|
+
tool: { name: "covgate", version: VERSION },
|
|
166
|
+
status: "error",
|
|
167
|
+
exitCode: 2,
|
|
168
|
+
config: relativeDisplay(config.configPath, config.root),
|
|
169
|
+
issues: problems
|
|
170
|
+
},
|
|
171
|
+
null,
|
|
172
|
+
2
|
|
173
|
+
) + "\n"
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
for (const problem of problems) {
|
|
177
|
+
process.stderr.write("配置错误: " + problem.message + "\n");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return 2;
|
|
181
|
+
}
|
|
182
|
+
for (const problem of problems) {
|
|
183
|
+
process.stderr.write("警告: " + problem.message + "\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const rows = scopes.map((scope) => ({
|
|
187
|
+
name: scope.name,
|
|
188
|
+
label: scope.label,
|
|
189
|
+
profile: scope.profile,
|
|
190
|
+
thresholds: Object.fromEntries(
|
|
191
|
+
METRICS.map((metric) => [metric, scope.thresholds[metric].value])
|
|
192
|
+
),
|
|
193
|
+
commands: scope.commands.length,
|
|
194
|
+
coverageFormat: scope.coverage.format,
|
|
195
|
+
coveragePaths: scope.coverage.paths,
|
|
196
|
+
sourceInclude: scope.sources.include,
|
|
197
|
+
excludeCount: scope.exclude.length
|
|
198
|
+
}));
|
|
199
|
+
if (options.json) {
|
|
200
|
+
process.stdout.write(
|
|
201
|
+
JSON.stringify(
|
|
202
|
+
{
|
|
203
|
+
schemaVersion: 1,
|
|
204
|
+
tool: { name: "covgate", version: VERSION },
|
|
205
|
+
status: "ok",
|
|
206
|
+
config: relativeDisplay(config.configPath, config.root),
|
|
207
|
+
scopes: rows
|
|
208
|
+
},
|
|
209
|
+
null,
|
|
210
|
+
2
|
|
211
|
+
) + "\n"
|
|
212
|
+
);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
process.stdout.write("✓ 配置有效: " + config.configPath + "\n");
|
|
216
|
+
for (const row of rows) {
|
|
217
|
+
process.stdout.write("\n[" + row.name + "] " + row.label + " (" + row.profile + ")\n");
|
|
218
|
+
process.stdout.write(
|
|
219
|
+
" 门槛: " +
|
|
220
|
+
METRICS.map(
|
|
221
|
+
(metric) =>
|
|
222
|
+
METRIC_LABELS[metric] + " ≥ " + formatThreshold(row.thresholds[metric]) + "%"
|
|
223
|
+
).join(" · ") +
|
|
224
|
+
"\n"
|
|
225
|
+
);
|
|
226
|
+
process.stdout.write(
|
|
227
|
+
" 测试命令: " +
|
|
228
|
+
row.commands +
|
|
229
|
+
" 条 · 覆盖率格式: " +
|
|
230
|
+
row.coverageFormat +
|
|
231
|
+
" · 报告: " +
|
|
232
|
+
row.coveragePaths.join(", ") +
|
|
233
|
+
"\n"
|
|
234
|
+
);
|
|
235
|
+
process.stdout.write(
|
|
236
|
+
" 源码: " +
|
|
237
|
+
row.sourceInclude.join(", ") +
|
|
238
|
+
" · 配置排除: " +
|
|
239
|
+
row.excludeCount +
|
|
240
|
+
" 条\n"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
process.stdout.write("\n报告目录: " + config.reports.dir + "\n");
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {any} report
|
|
249
|
+
* @param {any} options
|
|
250
|
+
* @param {{ json?: string, markdown?: string }} paths
|
|
251
|
+
*/
|
|
252
|
+
async function emitReport(report, options, paths) {
|
|
253
|
+
const writeErrors = await writeReportArtifacts(report, paths);
|
|
254
|
+
for (const message of writeErrors) {
|
|
255
|
+
process.stderr.write("警告: " + message + "\n");
|
|
256
|
+
}
|
|
257
|
+
if (options.json) {
|
|
258
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
259
|
+
} else {
|
|
260
|
+
const color =
|
|
261
|
+
options.color === false
|
|
262
|
+
? false
|
|
263
|
+
: options.color === true
|
|
264
|
+
? true
|
|
265
|
+
: Boolean(process.stdout.isTTY);
|
|
266
|
+
process.stdout.write(renderTextReport(report, { color }));
|
|
267
|
+
}
|
|
268
|
+
return report.exitCode;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {unknown} error
|
|
273
|
+
*/
|
|
274
|
+
function commandRecordFromError(error) {
|
|
275
|
+
if (!(error instanceof Error)) return null;
|
|
276
|
+
const details = /** @type {any} */ (error).details;
|
|
277
|
+
if (details && typeof details.stdout === "string" && typeof details.stderr === "string") {
|
|
278
|
+
return details;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @param {{
|
|
285
|
+
* config: any,
|
|
286
|
+
* scopes: any[],
|
|
287
|
+
* options: any,
|
|
288
|
+
* startedAt: string,
|
|
289
|
+
* startedMs: number,
|
|
290
|
+
* log: any
|
|
291
|
+
* }} context
|
|
292
|
+
*/
|
|
293
|
+
async function executeGate(context) {
|
|
294
|
+
const { config, scopes, options, startedAt, startedMs, log } = context;
|
|
295
|
+
const mode = options.noRun ? "check" : "run";
|
|
296
|
+
const commands = [];
|
|
297
|
+
const scopeResults = [];
|
|
298
|
+
const topLevelIssues = [];
|
|
299
|
+
const paths = plannedArtifactPaths(config, options.noFiles);
|
|
300
|
+
|
|
301
|
+
const finish = async () => {
|
|
302
|
+
const report = buildReport({
|
|
303
|
+
config,
|
|
304
|
+
mode,
|
|
305
|
+
scopes: scopeResults,
|
|
306
|
+
commands,
|
|
307
|
+
topLevelIssues,
|
|
308
|
+
durationMs: Date.now() - startedMs,
|
|
309
|
+
startedAt,
|
|
310
|
+
artifactPaths: paths
|
|
311
|
+
});
|
|
312
|
+
return emitReport(report, options, paths);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
log.info(
|
|
317
|
+
"CovGate " +
|
|
318
|
+
VERSION +
|
|
319
|
+
" · " +
|
|
320
|
+
(mode === "run" ? "执行测试" : "只检查已有报告") +
|
|
321
|
+
" · " +
|
|
322
|
+
relativeDisplay(config.configPath, config.root)
|
|
323
|
+
);
|
|
324
|
+
for (const scope of scopes) {
|
|
325
|
+
log.info("\n[" + scope.name + "] " + scope.label + " (" + scope.profile + ")");
|
|
326
|
+
/** @type {Map<string, { path: string, mtimeMs: number, size: number }>} */
|
|
327
|
+
let before = new Map();
|
|
328
|
+
if (mode === "run") {
|
|
329
|
+
const existingReports = await expandCoverageReportFiles(scope, config.root);
|
|
330
|
+
before = await snapshotReportFiles(existingReports);
|
|
331
|
+
const ran = await runScopeCommands(scope, {
|
|
332
|
+
verbose: options.verbose,
|
|
333
|
+
root: config.root,
|
|
334
|
+
logger: log
|
|
335
|
+
});
|
|
336
|
+
commands.push(...ran);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const reportFiles = await expandCoverageReportFiles(scope, config.root);
|
|
340
|
+
if (reportFiles.length === 0) {
|
|
341
|
+
throw new ExecutionError(
|
|
342
|
+
"scope " +
|
|
343
|
+
scope.name +
|
|
344
|
+
" 没有找到覆盖率报告。检查 coverage.paths:" +
|
|
345
|
+
scope.coverage.paths.join(", ")
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (mode === "run") {
|
|
350
|
+
const stale = await findStaleReports(before, reportFiles);
|
|
351
|
+
if (stale.length > 0) {
|
|
352
|
+
throw new ExecutionError(
|
|
353
|
+
"以下覆盖率报告在本次测试后没有更新,拒绝使用旧数据:\n " +
|
|
354
|
+
stale.map((file) => relativeDisplay(file, config.root)).join("\n "),
|
|
355
|
+
{
|
|
356
|
+
details: {
|
|
357
|
+
staleReports: stale.map((file) => relativeDisplay(file, config.root))
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
log.info(
|
|
365
|
+
" 覆盖率报告: " +
|
|
366
|
+
reportFiles
|
|
367
|
+
.map((file) => relativeDisplay(file, config.root))
|
|
368
|
+
.join(", ")
|
|
369
|
+
);
|
|
370
|
+
const coverage = await loadCoverage({
|
|
371
|
+
reportFiles,
|
|
372
|
+
format: scope.coverage.format,
|
|
373
|
+
root: config.root
|
|
374
|
+
});
|
|
375
|
+
const evaluated = await evaluateScope({
|
|
376
|
+
scope,
|
|
377
|
+
root: config.root,
|
|
378
|
+
coverage,
|
|
379
|
+
waiverPolicy: config.waivers
|
|
380
|
+
});
|
|
381
|
+
const gated = gateScope(evaluated);
|
|
382
|
+
scopeResults.push(gated);
|
|
383
|
+
log.info(
|
|
384
|
+
" " +
|
|
385
|
+
(gated.passed ? "✓" : "✗") +
|
|
386
|
+
" " +
|
|
387
|
+
METRICS.map((metric) => {
|
|
388
|
+
const value = gated.metrics[metric];
|
|
389
|
+
return (
|
|
390
|
+
METRIC_LABELS[metric] +
|
|
391
|
+
" " +
|
|
392
|
+
(value.applicable
|
|
393
|
+
? (value.percent === null ? "N/A" : value.percent.toFixed(2) + "%")
|
|
394
|
+
: "N/A")
|
|
395
|
+
);
|
|
396
|
+
}).join(" · ")
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return await finish();
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const record = commandRecordFromError(error);
|
|
402
|
+
if (record) {
|
|
403
|
+
printCommandFailure(record);
|
|
404
|
+
if (Array.isArray(record.priorCommands)) commands.push(...record.priorCommands);
|
|
405
|
+
commands.push(record);
|
|
406
|
+
}
|
|
407
|
+
const issue = {
|
|
408
|
+
code: error instanceof CovgateError ? error.kind : "run-error",
|
|
409
|
+
severity: "error",
|
|
410
|
+
message: error instanceof Error ? error.message : String(error)
|
|
411
|
+
};
|
|
412
|
+
if (record) {
|
|
413
|
+
issue.outputTail = tail(
|
|
414
|
+
((record.stdout ?? "") + "\n" + (record.stderr ?? "")).trim(),
|
|
415
|
+
12_000
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const details = /** @type {any} */ (error)?.details;
|
|
419
|
+
if (Array.isArray(details?.staleReports)) issue.files = details.staleReports;
|
|
420
|
+
topLevelIssues.push(issue);
|
|
421
|
+
if (options.verbose && error instanceof Error && error.stack) {
|
|
422
|
+
log.error(error.stack);
|
|
423
|
+
} else if (error instanceof Error) {
|
|
424
|
+
log.error(error.message);
|
|
425
|
+
}
|
|
426
|
+
return await finish();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @param {string[]} argv
|
|
432
|
+
*/
|
|
433
|
+
export async function main(argv) {
|
|
434
|
+
const startedAt = new Date().toISOString();
|
|
435
|
+
const startedMs = Date.now();
|
|
436
|
+
const jsonRequested = wantsJson(argv);
|
|
437
|
+
let options;
|
|
438
|
+
try {
|
|
439
|
+
options = parseArgs(argv);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
return printEarlyError(error, jsonRequested);
|
|
442
|
+
}
|
|
443
|
+
if (options.help) {
|
|
444
|
+
process.stdout.write(HELP_TEXT + "\n");
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
if (options.version) {
|
|
448
|
+
process.stdout.write(VERSION + "\n");
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (options.command === "setup") {
|
|
453
|
+
return runSetupCommand(options);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let config;
|
|
457
|
+
let scopes;
|
|
458
|
+
try {
|
|
459
|
+
const configPath = resolveConfigPath(process.cwd(), options.configPath);
|
|
460
|
+
config = await loadConfig(configPath);
|
|
461
|
+
if (options.reportDir) {
|
|
462
|
+
config.reports.dir = path.resolve(process.cwd(), options.reportDir);
|
|
463
|
+
}
|
|
464
|
+
scopes = selectScopes(config, options.scopes);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
return printEarlyError(error, options.json, options.verbose);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (options.command === "validate") {
|
|
470
|
+
return printValidate(config, scopes, options);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const log = logger();
|
|
474
|
+
try {
|
|
475
|
+
return await executeGate({
|
|
476
|
+
config,
|
|
477
|
+
scopes,
|
|
478
|
+
options,
|
|
479
|
+
startedAt,
|
|
480
|
+
startedMs,
|
|
481
|
+
log
|
|
482
|
+
});
|
|
483
|
+
} catch (error) {
|
|
484
|
+
return printEarlyError(error, options.json, options.verbose);
|
|
485
|
+
}
|
|
486
|
+
}
|