covgate 0.2.0 → 0.2.2
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 +52 -17
- package/docs/ai-workflow.md +1 -0
- package/docs/coverage-viewer.png +0 -0
- package/docs/design.md +1 -1
- package/docs/setup.md +30 -28
- package/package.json +1 -1
- package/schema/covgate.schema.json +4 -2
- package/src/args.js +14 -0
- package/src/cli.js +41 -6
- package/src/config.js +2 -2
- package/src/coverage/model.js +3 -0
- package/src/gate.js +48 -4
- package/src/git.js +86 -0
- package/src/index.js +1 -0
- package/src/report/build.js +13 -3
- package/src/report/html.js +257 -0
- package/src/report/index.js +21 -3
- package/src/report/markdown.js +3 -1
- package/src/report/text.js +5 -2
- package/src/scope.js +33 -3
- package/src/setup/agent.js +3 -1
- package/src/setup/config.js +43 -26
- package/src/setup/index.js +41 -13
- package/src/setup/project.js +82 -7
- package/templates/AGENTS.md.snippet.md +1 -0
- package/templates/covgate.config.json +2 -1
package/README.md
CHANGED
|
@@ -46,30 +46,29 @@ covgate run
|
|
|
46
46
|
|
|
47
47
|
## 安装
|
|
48
48
|
|
|
49
|
-
###
|
|
49
|
+
### 从 npm registry 安装(推荐)
|
|
50
|
+
|
|
51
|
+
包已发布到 npm:[covgate](https://www.npmjs.com/package/covgate),当前 latest 为 **0.2.2**。
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
|
-
npm install --save-dev
|
|
54
|
+
npm install --save-dev covgate
|
|
53
55
|
npx --no-install covgate setup
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
> npm 上可能还能看到 `0.0.0-stage`,它是暂存发布占位版本,不会作为 `latest` 被安装,可以忽略。
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
npm install --save-dev git+ssh://git@gitee.com/wxzzhj/covgate.git
|
|
60
|
-
```
|
|
60
|
+
### 从 Gitee 安装(源码 / 镜像)
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
```bash
|
|
63
|
+
# HTTPS
|
|
64
|
+
npm install --save-dev git+https://gitee.com/wxzzhj/covgate.git
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
# 或 SSH
|
|
67
|
+
npm install --save-dev git+ssh://git@gitee.com/wxzzhj/covgate.git
|
|
65
68
|
|
|
66
|
-
```bash
|
|
67
|
-
npm install --save-dev covgate
|
|
68
69
|
npx --no-install covgate setup
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
发布需要 npm 登录:`npm login && npm publish`。
|
|
72
|
-
|
|
73
72
|
### 本地目录安装
|
|
74
73
|
|
|
75
74
|
```bash
|
|
@@ -96,8 +95,11 @@ setup 会:
|
|
|
96
95
|
- 检测 npm/pnpm/yarn/bun 和已有测试框架;
|
|
97
96
|
- **没有测试框架时询问是否安装 Vitest + @vitest/coverage-v8**,选择是则自动安装;
|
|
98
97
|
- 写入 `coverage:gate`、`test:coverage` 脚本和 `covgate.config.json` 起始配置;
|
|
98
|
+
- Electron 项目自动生成 `client`(`src/**`,100%)和 `server`(`desktop/**`,95%)两个 scope;
|
|
99
99
|
- 生成 `AGENTS.md`、Agent 门禁脚本、GitHub Actions(如存在 `.github/`)和 `.gitignore` 条目。
|
|
100
100
|
|
|
101
|
+
**CovGate 不生成测试,也不补测试。** 行为变更的测试必须由项目自己补;没有测试时 `test:coverage` 会直接失败,不会生成空覆盖率来蒙混过关。
|
|
102
|
+
|
|
101
103
|
`postinstall` 也会尝试同样的流程,但 **npm 11.19+ 默认不执行依赖安装脚本**。如果安装时没有弹出询问,直接运行 `npx covgate setup` 即可;详细说明见 [安装与自动配置](docs/setup.md)。
|
|
102
104
|
|
|
103
105
|
### 在 CovGate 仓库中开发
|
|
@@ -183,7 +185,7 @@ node bin/covgate.js --help
|
|
|
183
185
|
},
|
|
184
186
|
"reports": {
|
|
185
187
|
"dir": ".covgate",
|
|
186
|
-
"formats": ["json", "markdown"]
|
|
188
|
+
"formats": ["json", "markdown", "html"]
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
```
|
|
@@ -246,7 +248,9 @@ npx --no-install covgate validate
|
|
|
246
248
|
| `--config <path>` | 指定配置文件。 |
|
|
247
249
|
| `--scope <name>` | 只检查指定 scope,可重复或逗号分隔。 |
|
|
248
250
|
| `--json` | stdout 只输出 JSON;过程日志走 stderr。 |
|
|
249
|
-
| `--
|
|
251
|
+
| `--changed` | 只门禁/展示本次 Git 变更文件;默认对比 `HEAD`。 |
|
|
252
|
+
| `--base <rev>` | 配合 `--changed` 指定 Git 对比版本,例如 `HEAD^`。 |
|
|
253
|
+
| `--no-files` | 不写 `.covgate/report.json` / `report.md` / `report.html`。 |
|
|
250
254
|
| `--report-dir <path>` | 覆盖报告目录(相对当前目录)。 |
|
|
251
255
|
| `--verbose` | 实时转发测试命令输出。 |
|
|
252
256
|
| `--color` / `--no-color` | 控制彩色输出。 |
|
|
@@ -264,6 +268,25 @@ setup 的完整选项见 [安装与自动配置](docs/setup.md)。
|
|
|
264
268
|
|
|
265
269
|
CI 只需要判断退出码是否为 0。
|
|
266
270
|
|
|
271
|
+
### 只检查 Git 变更文件
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# 工作区相对 HEAD 的新增/修改(包含未跟踪的新文件)
|
|
275
|
+
npx --no-install covgate run --changed
|
|
276
|
+
|
|
277
|
+
# 检查最新一次提交相对父提交的变更
|
|
278
|
+
npx --no-install covgate run --changed --base HEAD^
|
|
279
|
+
|
|
280
|
+
# PR / CI:相对目标分支
|
|
281
|
+
npx --no-install covgate run --changed --base origin/main
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`--changed` 会把每个 scope 的源码范围收窄到变更文件,并只对这些文件计算覆盖率、展示文件和执行 100%/95% 门槛;未变更文件不会因为本次门禁被误判。HTML 报告也只列出变更文件。
|
|
285
|
+
|
|
286
|
+
它**不会自动筛选测试用例**。测试命令仍是项目在 `commands` 中配置的命令(通常是全量测试),CovGate 只负责覆盖率范围。测试命令自己决定跑哪些测试,才能避免漏掉依赖变更。
|
|
287
|
+
|
|
288
|
+
如果变更文件全部不在任何 scope 的 `sources.include` 内,CovGate 会失败而不是虚假通过。
|
|
289
|
+
|
|
267
290
|
## 配置参考
|
|
268
291
|
|
|
269
292
|
### profile
|
|
@@ -326,7 +349,7 @@ CI 只需要判断退出码是否为 0。
|
|
|
326
349
|
### reports
|
|
327
350
|
|
|
328
351
|
- `dir`:输出目录,默认 `.covgate`。
|
|
329
|
-
- `formats`:`json`、`markdown`。
|
|
352
|
+
- `formats`:`json`、`markdown`、`html`。
|
|
330
353
|
|
|
331
354
|
## 写豁免:不可达代码的唯一方式
|
|
332
355
|
|
|
@@ -433,13 +456,24 @@ stdout 是纯 JSON,代理可以读取 `status`、`scopes[].failingFiles`、`is
|
|
|
433
456
|
|
|
434
457
|
每次运行默认写:
|
|
435
458
|
|
|
436
|
-
- `.covgate/report.json`:机器可读,包含 scope
|
|
459
|
+
- `.covgate/report.json`:机器可读,包含 scope 指标、失败文件、逐行未覆盖、行命中、全部豁免、排除、命令、退出码。
|
|
437
460
|
- `.covgate/report.md`:人类/评审可读的审计报告。
|
|
461
|
+
- `.covgate/report.html`:自包含网页查看器;左侧文件列表,中间源码,已覆盖行绿色背景、未覆盖行白色背景、非可执行行灰色。
|
|
462
|
+
|
|
463
|
+
打开网页:
|
|
464
|
+
|
|
465
|
+
```powershell
|
|
466
|
+
start .covgate\report.html
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
macOS 用 `open .covgate/report.html`,Linux 用 `xdg-open .covgate/report.html`。
|
|
470
|
+
|
|
471
|
+

|
|
438
472
|
|
|
439
473
|
文本输出示例:
|
|
440
474
|
|
|
441
475
|
```text
|
|
442
|
-
CovGate · 覆盖率门禁 v0.2.
|
|
476
|
+
CovGate · 覆盖率门禁 v0.2.2
|
|
443
477
|
模式 执行测试并检查 · 配置 covgate.config.json · 用时 12.4s
|
|
444
478
|
|
|
445
479
|
✗ 客户端 [client/client] 未通过
|
|
@@ -449,6 +483,7 @@ CovGate · 覆盖率门禁 v0.2.0
|
|
|
449
483
|
|
|
450
484
|
结果: 未通过
|
|
451
485
|
JSON 报告: .covgate/report.json
|
|
486
|
+
HTML 报告: .covgate/report.html
|
|
452
487
|
```
|
|
453
488
|
|
|
454
489
|
建议把 `.covgate/` 加入 `.gitignore`。报告是本次运行的产物,不是源码。
|
package/docs/ai-workflow.md
CHANGED
|
@@ -30,6 +30,7 @@ npx --no-install covgate setup
|
|
|
30
30
|
- 任何代码修改完成后,必须运行 `npx --no-install covgate run --json`;退出码 0 才能说完成。
|
|
31
31
|
- 退出码 1:补测试或修实现;2:修配置;3:修测试命令/报告。不要绕过。
|
|
32
32
|
- 禁止降低 covgate.config.json 中的门槛、扩大 exclude、删除豁免检查来让门禁通过;这些改动必须由人类评审。
|
|
33
|
+
- **行为变更必须新增或更新覆盖该行为的真实测试**;门禁失败不能作为完成理由,也不能用 `--passWithNoTests`、空报告或 ignore 绕过。
|
|
33
34
|
- 禁止 istanbul ignore / c8 ignore / v8 ignore / node:coverage ignore。
|
|
34
35
|
- 确实不可达的代码使用 covgate-ignore-next-line / start/end / file,写具体原因、owner 和 expires。
|
|
35
36
|
- 新增或修改的源码必须出现在覆盖率报告中;sources.include 是范围事实来源。
|
|
Binary file
|
package/docs/design.md
CHANGED
package/docs/setup.md
CHANGED
|
@@ -2,36 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## 在线安装
|
|
4
4
|
|
|
5
|
-
###
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# HTTPS(公开仓库)
|
|
9
|
-
npm install --save-dev git+https://gitee.com/wxzzhj/covgate.git
|
|
10
|
-
|
|
11
|
-
# 或 SSH
|
|
12
|
-
npm install --save-dev git+ssh://git@gitee.com/wxzzhj/covgate.git
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
安装完成后运行一次:
|
|
5
|
+
### npm registry(推荐)
|
|
16
6
|
|
|
17
7
|
```bash
|
|
8
|
+
npm install --save-dev covgate
|
|
18
9
|
npx --no-install covgate setup
|
|
19
10
|
```
|
|
20
11
|
|
|
21
|
-
|
|
12
|
+
包页面:[covgate](https://www.npmjs.com/package/covgate)。当前 latest 是 **0.2.2**。
|
|
22
13
|
|
|
23
|
-
|
|
14
|
+
> `0.0.0-stage` 是 npm 暂存发布产生的占位版本,不会被 `npm install covgate` 选中,可以忽略。
|
|
15
|
+
|
|
16
|
+
### Gitee(源码 / 镜像)
|
|
24
17
|
|
|
25
18
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
19
|
+
# HTTPS(公开仓库)
|
|
20
|
+
npm install --save-dev git+https://gitee.com/wxzzhj/covgate.git
|
|
29
21
|
|
|
30
|
-
|
|
22
|
+
# 或 SSH
|
|
23
|
+
npm install --save-dev git+ssh://git@gitee.com/wxzzhj/covgate.git
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
npm login
|
|
34
|
-
npm publish
|
|
25
|
+
npx --no-install covgate setup
|
|
35
26
|
```
|
|
36
27
|
|
|
37
28
|
### 从本地目录安装
|
|
@@ -47,7 +38,7 @@ npm 11.19+ 默认不执行依赖的 `postinstall`,会显示类似:
|
|
|
47
38
|
|
|
48
39
|
```text
|
|
49
40
|
npm warn install-scripts 1 package has install scripts not yet covered by allowScripts
|
|
50
|
-
npm warn install-scripts covgate@0.2.
|
|
41
|
+
npm warn install-scripts covgate@0.2.2 (postinstall: node bin/covgate-postinstall.js)
|
|
51
42
|
```
|
|
52
43
|
|
|
53
44
|
因此:
|
|
@@ -70,7 +61,7 @@ npm warn install-scripts covgate@0.2.0 (postinstall: node bin/covgate-postinst
|
|
|
70
61
|
3. 添加 `npm scripts`:
|
|
71
62
|
- `coverage:gate`:`covgate run`
|
|
72
63
|
- `test:coverage`:Vitest/Jest 的 JSON + LCOV 覆盖率命令
|
|
73
|
-
4. 生成 `covgate.config.json
|
|
64
|
+
4. 生成 `covgate.config.json`(普通项目单 scope;Electron 自动生成 client + server;已存在则不覆盖):
|
|
74
65
|
|
|
75
66
|
```json
|
|
76
67
|
{
|
|
@@ -93,6 +84,8 @@ npm warn install-scripts covgate@0.2.0 (postinstall: node bin/covgate-postinst
|
|
|
93
84
|
- 若已有 `CLAUDE.md`、`.cursor`、`.github/copilot-instructions.md`,同步写入对应说明。
|
|
94
85
|
6. 输出下一步命令。
|
|
95
86
|
|
|
87
|
+
> **CovGate 不生成测试,也不补测试。** setup 只配置测试框架和门禁;本次行为变更的测试必须由项目自己补。没有测试时 `test:coverage` 会直接失败(已移除 `--passWithNoTests`),不会产生可蒙混过关的空覆盖率。
|
|
88
|
+
|
|
96
89
|
## setup 选项
|
|
97
90
|
|
|
98
91
|
| 选项 | 作用 |
|
|
@@ -122,21 +115,30 @@ npx covgate setup --no-install --no-config
|
|
|
122
115
|
npx covgate setup --profile client --source "web/src/**/*.{ts,tsx}"
|
|
123
116
|
```
|
|
124
117
|
|
|
125
|
-
##
|
|
118
|
+
## 生成的 scope
|
|
126
119
|
|
|
127
|
-
|
|
120
|
+
普通项目只生成一个 scope:
|
|
128
121
|
|
|
129
|
-
-
|
|
130
|
-
-
|
|
122
|
+
- 有源码目录时自动选择源码(优先 `src/`,其次 `app.js`、`app/`、`lib/`);
|
|
123
|
+
- 有根目录或 `src/index.html`、React/Vue/Svelte 等依赖、Electron 时选 `client`;
|
|
131
124
|
- 否则选 `server`。
|
|
132
125
|
|
|
133
|
-
|
|
126
|
+
Electron 项目会生成两个 scope:
|
|
127
|
+
|
|
128
|
+
- `client`(profile=client):`src/**` 等渲染进程源码,目标 100%;
|
|
129
|
+
- `server`(profile=server):`desktop/**` 主进程/预加载源码,目标 95%。
|
|
130
|
+
|
|
131
|
+
两个 scope 共用同一个测试命令和覆盖率报告;CovGate 会按各自源码范围计算覆盖率。若使用 `--profile` 或 `--source` 手工覆盖,则只生成一个 scope。
|
|
132
|
+
|
|
133
|
+
生成配置后请先运行:
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
136
|
npx --no-install covgate validate
|
|
137
137
|
npx --no-install covgate run
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
门禁失败时,正确做法是让项目补覆盖该行为的真实测试;CovGate 不会替项目补测试,也不会接受空报告、`--passWithNoTests` 或降低门槛。
|
|
141
|
+
|
|
140
142
|
## AI 代理如何真正做到“自动跑”
|
|
141
143
|
|
|
142
144
|
生成的文件不等于自动触发。需要把它接到代理生命周期:
|
|
@@ -160,4 +162,4 @@ npx --no-install covgate run
|
|
|
160
162
|
- **没有弹出测试框架询问**:npm 11 默认阻止 install scripts。运行 `npx covgate setup`。
|
|
161
163
|
- **没有生成 CovGate 配置**:可能没有识别到源码目录或覆盖率命令。用 `--source`、`--test-command` 指定,或先安装框架。
|
|
162
164
|
- **Vitest 安装超时**:setup 对安装命令有 180 秒限制。检查网络/registry,手动安装 `vitest @vitest/coverage-v8` 后重跑 setup。
|
|
163
|
-
- **多 scope 项目**:setup 只生成一个;手工编辑或分目录 setup。
|
|
165
|
+
- **多 scope 项目**:setup 只生成一个;手工编辑或分目录 setup。
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -16,6 +16,8 @@ export const HELP_TEXT = [
|
|
|
16
16
|
" --config <path> 配置文件路径(默认 ./covgate.config.json)",
|
|
17
17
|
" --scope <name> 只检查指定 scope,可重复或用逗号分隔",
|
|
18
18
|
" --no-run 等同 check:不执行测试命令",
|
|
19
|
+
" --changed 只门禁/展示 Git 变更文件(默认对比 HEAD)",
|
|
20
|
+
" --base <rev> 配合 --changed,指定 Git 对比版本(如 HEAD^)",
|
|
19
21
|
" --json 向 stdout 输出纯 JSON(日志走 stderr)",
|
|
20
22
|
" --no-files 不写 .covgate/report.json 和 report.md",
|
|
21
23
|
" --report-dir <path> 覆盖报告输出目录(相对当前目录)",
|
|
@@ -61,6 +63,8 @@ export function parseArgs(argv) {
|
|
|
61
63
|
noRun: false,
|
|
62
64
|
noFiles: false,
|
|
63
65
|
reportDir: undefined,
|
|
66
|
+
changed: false,
|
|
67
|
+
base: undefined,
|
|
64
68
|
verbose: false,
|
|
65
69
|
color: undefined,
|
|
66
70
|
help: false,
|
|
@@ -137,6 +141,10 @@ export function parseArgs(argv) {
|
|
|
137
141
|
options.noRun = true;
|
|
138
142
|
} else if (key === "--no-files") {
|
|
139
143
|
options.noFiles = true;
|
|
144
|
+
} else if (key === "--changed") {
|
|
145
|
+
options.changed = true;
|
|
146
|
+
} else if (key === "--base") {
|
|
147
|
+
options.base = value();
|
|
140
148
|
} else if (key === "--report-dir") {
|
|
141
149
|
options.reportDir = value();
|
|
142
150
|
} else if (key === "--verbose") {
|
|
@@ -178,6 +186,12 @@ export function parseArgs(argv) {
|
|
|
178
186
|
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
187
|
throw new ConfigError("setup 相关选项只能和 covgate setup 一起使用");
|
|
180
188
|
}
|
|
189
|
+
if (options.base && !options.changed) {
|
|
190
|
+
throw new ConfigError("--base 必须和 --changed 一起使用");
|
|
191
|
+
}
|
|
192
|
+
if (options.changed && !["run", "check"].includes(options.command)) {
|
|
193
|
+
throw new ConfigError("--changed 只能用于 covgate run 或 covgate check");
|
|
194
|
+
}
|
|
181
195
|
return options;
|
|
182
196
|
}
|
|
183
197
|
|
package/src/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { CovgateError, ExecutionError } from "./errors.js";
|
|
10
10
|
import { loadCoverage } from "./coverage/index.js";
|
|
11
11
|
import { gateScope } from "./gate.js";
|
|
12
|
+
import { changedFileKeys, collectChangedFiles } from "./git.js";
|
|
12
13
|
import {
|
|
13
14
|
expandCoverageReportFiles,
|
|
14
15
|
findStaleReports,
|
|
@@ -249,8 +250,8 @@ function printValidate(config, scopes, options) {
|
|
|
249
250
|
* @param {any} options
|
|
250
251
|
* @param {{ json?: string, markdown?: string }} paths
|
|
251
252
|
*/
|
|
252
|
-
async function emitReport(report, options, paths) {
|
|
253
|
-
const writeErrors = await writeReportArtifacts(report, paths);
|
|
253
|
+
async function emitReport(report, options, paths, config) {
|
|
254
|
+
const writeErrors = await writeReportArtifacts(report, paths, config);
|
|
254
255
|
for (const message of writeErrors) {
|
|
255
256
|
process.stderr.write("警告: " + message + "\n");
|
|
256
257
|
}
|
|
@@ -297,6 +298,8 @@ async function executeGate(context) {
|
|
|
297
298
|
const scopeResults = [];
|
|
298
299
|
const topLevelIssues = [];
|
|
299
300
|
const paths = plannedArtifactPaths(config, options.noFiles);
|
|
301
|
+
let changed = null;
|
|
302
|
+
let changedKeys = null;
|
|
300
303
|
|
|
301
304
|
const finish = async () => {
|
|
302
305
|
const report = buildReport({
|
|
@@ -307,9 +310,10 @@ async function executeGate(context) {
|
|
|
307
310
|
topLevelIssues,
|
|
308
311
|
durationMs: Date.now() - startedMs,
|
|
309
312
|
startedAt,
|
|
310
|
-
artifactPaths: paths
|
|
313
|
+
artifactPaths: paths,
|
|
314
|
+
changed
|
|
311
315
|
});
|
|
312
|
-
return emitReport(report, options, paths);
|
|
316
|
+
return emitReport(report, options, paths, config);
|
|
313
317
|
};
|
|
314
318
|
|
|
315
319
|
try {
|
|
@@ -321,6 +325,27 @@ async function executeGate(context) {
|
|
|
321
325
|
" · " +
|
|
322
326
|
relativeDisplay(config.configPath, config.root)
|
|
323
327
|
);
|
|
328
|
+
if (options.changed) {
|
|
329
|
+
changed = await collectChangedFiles({
|
|
330
|
+
root: config.root,
|
|
331
|
+
base: options.base || "HEAD"
|
|
332
|
+
});
|
|
333
|
+
if (changed.files.length === 0) {
|
|
334
|
+
throw new ExecutionError(
|
|
335
|
+
"没有检测到相对 " +
|
|
336
|
+
changed.base +
|
|
337
|
+
" 的新增/修改文件。请确认 Git 基线,或用 --base 指定版本。"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
changedKeys = changedFileKeys(changed.files);
|
|
341
|
+
log.info(
|
|
342
|
+
"变更文件: " +
|
|
343
|
+
changed.relativeFiles.join(", ") +
|
|
344
|
+
"(base " +
|
|
345
|
+
changed.base +
|
|
346
|
+
")"
|
|
347
|
+
);
|
|
348
|
+
}
|
|
324
349
|
for (const scope of scopes) {
|
|
325
350
|
log.info("\n[" + scope.name + "] " + scope.label + " (" + scope.profile + ")");
|
|
326
351
|
/** @type {Map<string, { path: string, mtimeMs: number, size: number }>} */
|
|
@@ -376,13 +401,14 @@ async function executeGate(context) {
|
|
|
376
401
|
scope,
|
|
377
402
|
root: config.root,
|
|
378
403
|
coverage,
|
|
379
|
-
waiverPolicy: config.waivers
|
|
404
|
+
waiverPolicy: config.waivers,
|
|
405
|
+
changedKeys
|
|
380
406
|
});
|
|
381
407
|
const gated = gateScope(evaluated);
|
|
382
408
|
scopeResults.push(gated);
|
|
383
409
|
log.info(
|
|
384
410
|
" " +
|
|
385
|
-
(gated.passed ? "✓" : "✗") +
|
|
411
|
+
(gated.skipped ? "○" : gated.passed ? "✓" : "✗") +
|
|
386
412
|
" " +
|
|
387
413
|
METRICS.map((metric) => {
|
|
388
414
|
const value = gated.metrics[metric];
|
|
@@ -396,6 +422,15 @@ async function executeGate(context) {
|
|
|
396
422
|
}).join(" · ")
|
|
397
423
|
);
|
|
398
424
|
}
|
|
425
|
+
if (
|
|
426
|
+
options.changed &&
|
|
427
|
+
scopeResults.length > 0 &&
|
|
428
|
+
scopeResults.every((scope) => scope.skipped)
|
|
429
|
+
) {
|
|
430
|
+
throw new ExecutionError(
|
|
431
|
+
"变更文件不在任何 scope 的 sources.include 中,无法执行覆盖率门禁。"
|
|
432
|
+
);
|
|
433
|
+
}
|
|
399
434
|
return await finish();
|
|
400
435
|
} catch (error) {
|
|
401
436
|
const record = commandRecordFromError(error);
|
package/src/config.js
CHANGED
|
@@ -40,7 +40,7 @@ const WAIVER_KEYS = new Set([
|
|
|
40
40
|
const REPORT_KEYS = new Set(["dir", "formats"]);
|
|
41
41
|
const EXCLUDE_KEYS = new Set(["pattern", "reason", "owner", "expires", "ticket"]);
|
|
42
42
|
const COVERAGE_FORMATS = new Set(["auto", "istanbul", "lcov"]);
|
|
43
|
-
const REPORT_FORMATS = new Set(["json", "markdown"]);
|
|
43
|
+
const REPORT_FORMATS = new Set(["json", "markdown", "html"]);
|
|
44
44
|
const POLICIES = new Set(["fail", "warn"]);
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -350,7 +350,7 @@ export function normalizeConfig(raw, context) {
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
const reports = { dir: ".covgate", formats: ["json", "markdown"] };
|
|
353
|
+
const reports = { dir: ".covgate", formats: ["json", "markdown", "html"] };
|
|
354
354
|
if (raw.reports !== undefined) {
|
|
355
355
|
assertObject(raw.reports, "reports", [...REPORT_KEYS]);
|
|
356
356
|
if (raw.reports.dir !== undefined) {
|
package/src/coverage/model.js
CHANGED
|
@@ -277,6 +277,9 @@ export function fileReport(file, limit = Number.POSITIVE_INFINITY) {
|
|
|
277
277
|
return {
|
|
278
278
|
path: file.relPath,
|
|
279
279
|
metrics: metricsForFile(file),
|
|
280
|
+
lineHits: Object.fromEntries(
|
|
281
|
+
[...file.lines].sort((left, right) => left[0] - right[0])
|
|
282
|
+
),
|
|
280
283
|
uncovered: {
|
|
281
284
|
lines: uncoveredLines.values,
|
|
282
285
|
linesTotal: uncoveredLines.total,
|
package/src/gate.js
CHANGED
|
@@ -41,6 +41,52 @@ function aggregate(files, metric) {
|
|
|
41
41
|
export function gateScope(scopeData) {
|
|
42
42
|
const issues = [...(scopeData.issues ?? [])];
|
|
43
43
|
const warnings = [...(scopeData.warnings ?? [])];
|
|
44
|
+
const initialErrorIssues = issues.filter((issue) => issue.severity === "error");
|
|
45
|
+
const thresholds = Object.fromEntries(
|
|
46
|
+
METRICS.map((metric) => [metric, scopeData.thresholds[metric].value])
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (scopeData.skipped) {
|
|
50
|
+
const metrics = Object.fromEntries(
|
|
51
|
+
METRICS.map((metric) => [
|
|
52
|
+
metric,
|
|
53
|
+
{
|
|
54
|
+
metric,
|
|
55
|
+
applicable: false,
|
|
56
|
+
covered: 0,
|
|
57
|
+
total: 0,
|
|
58
|
+
percent: null,
|
|
59
|
+
threshold: thresholds[metric],
|
|
60
|
+
thresholdBasisPoints: scopeData.thresholds[metric].basisPoints,
|
|
61
|
+
passed: true,
|
|
62
|
+
note: "本次 Git 变更未涉及该 scope"
|
|
63
|
+
}
|
|
64
|
+
])
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
name: scopeData.name,
|
|
68
|
+
label: scopeData.label,
|
|
69
|
+
profile: scopeData.profile,
|
|
70
|
+
format: scopeData.format,
|
|
71
|
+
passed: initialErrorIssues.length === 0,
|
|
72
|
+
skipped: true,
|
|
73
|
+
changedOnly: Boolean(scopeData.changedOnly),
|
|
74
|
+
thresholds,
|
|
75
|
+
metrics,
|
|
76
|
+
files: [],
|
|
77
|
+
failingFiles: [],
|
|
78
|
+
expectedSourceCount: 0,
|
|
79
|
+
reportedFileCount: scopeData.reportedFileCount,
|
|
80
|
+
missingSources: [],
|
|
81
|
+
extras: [],
|
|
82
|
+
waivers: scopeData.waivers,
|
|
83
|
+
exclusions: scopeData.exclusions,
|
|
84
|
+
issues,
|
|
85
|
+
warnings,
|
|
86
|
+
reasons: errorIssues.map((issue) => issue.message)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
44
90
|
const fileReports = scopeData.files.map((file) => fileReport(file));
|
|
45
91
|
const metrics = {};
|
|
46
92
|
|
|
@@ -112,16 +158,14 @@ export function gateScope(scopeData) {
|
|
|
112
158
|
return leftPercent - rightPercent || left.path.localeCompare(right.path, "en");
|
|
113
159
|
});
|
|
114
160
|
|
|
115
|
-
const thresholds = Object.fromEntries(
|
|
116
|
-
METRICS.map((metric) => [metric, scopeData.thresholds[metric].value])
|
|
117
|
-
);
|
|
118
|
-
|
|
119
161
|
const result = {
|
|
120
162
|
name: scopeData.name,
|
|
121
163
|
label: scopeData.label,
|
|
122
164
|
profile: scopeData.profile,
|
|
123
165
|
format: scopeData.format,
|
|
124
166
|
passed,
|
|
167
|
+
skipped: false,
|
|
168
|
+
changedOnly: Boolean(scopeData.changedOnly),
|
|
125
169
|
thresholds,
|
|
126
170
|
metrics,
|
|
127
171
|
files: fileReports,
|
package/src/git.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { ConfigError } from "./errors.js";
|
|
6
|
+
import { canonicalPath, relativeDisplay } from "./util.js";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} cwd
|
|
12
|
+
* @param {string[]} args
|
|
13
|
+
*/
|
|
14
|
+
async function git(cwd, args) {
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
|
|
17
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
18
|
+
windowsHide: true
|
|
19
|
+
});
|
|
20
|
+
return stdout;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new ConfigError(
|
|
23
|
+
"Git 命令失败:" + (error.stderr?.toString().trim() || error.message),
|
|
24
|
+
{ cause: error }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Collect added/copied/modified/renamed files between a base revision and the
|
|
31
|
+
* current working tree. Untracked files are included because AI agents often
|
|
32
|
+
* add new files before staging them.
|
|
33
|
+
*
|
|
34
|
+
* @param {{ root: string, base?: string }} options
|
|
35
|
+
*/
|
|
36
|
+
export async function collectChangedFiles({ root, base = "HEAD" }) {
|
|
37
|
+
const projectRoot = path.resolve(root);
|
|
38
|
+
const gitRoot = (await git(projectRoot, ["rev-parse", "--show-toplevel"]))
|
|
39
|
+
.trim()
|
|
40
|
+
.replaceAll("\\", "/");
|
|
41
|
+
if (!gitRoot) throw new ConfigError("当前目录不在 Git 仓库中,不能使用 --changed。");
|
|
42
|
+
try {
|
|
43
|
+
await git(projectRoot, ["rev-parse", "--verify", base + "^{commit}"]);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new ConfigError('找不到 Git base 版本 "' + base + '"。', { cause: error });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tracked = (
|
|
49
|
+
await git(projectRoot, [
|
|
50
|
+
"diff",
|
|
51
|
+
"--name-only",
|
|
52
|
+
"--diff-filter=ACMR",
|
|
53
|
+
"-M",
|
|
54
|
+
base,
|
|
55
|
+
"--"
|
|
56
|
+
])
|
|
57
|
+
)
|
|
58
|
+
.split(/\r?\n/)
|
|
59
|
+
.map((line) => line.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
const untracked = (
|
|
62
|
+
await git(projectRoot, ["ls-files", "--others", "--exclude-standard"])
|
|
63
|
+
)
|
|
64
|
+
.split(/\r?\n/)
|
|
65
|
+
.map((line) => line.trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
|
|
68
|
+
const all = [...new Set([...tracked, ...untracked])]
|
|
69
|
+
.map((relative) => path.resolve(gitRoot, relative))
|
|
70
|
+
.filter((absolute) => existsSync(absolute))
|
|
71
|
+
.sort((left, right) => left.localeCompare(right, "en"));
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
base,
|
|
75
|
+
gitRoot,
|
|
76
|
+
files: all,
|
|
77
|
+
relativeFiles: all.map((file) => relativeDisplay(file, projectRoot))
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string[]} files
|
|
83
|
+
*/
|
|
84
|
+
export function changedFileKeys(files) {
|
|
85
|
+
return new Set(files.map((file) => canonicalPath(file)));
|
|
86
|
+
}
|