ci(gates): 接线 test-coverage 覆盖率总闸(FAIL 语义),并修三处已成假话的门禁文档

用户裁定 2026-10-24:缺 mscoverage 组件时**失败**,不做 skip。于是 P1 之后那个
"钩子只挂 16/28 套"的缺口关闭了。

为什么挂这一根而不是补 12 根 per-module 钩子:本闸跑**全部 28 套**,并且守
"已覆盖行只增不减"的基线台账。这是 per-module 钩子结构性做不到的——删掉一套
测试,它那根钩子只会因为"没有文件要检查"而静默跳过(Skipped 不等于失败)。
实测接线代价:45 s(28 套,含 28 次 MSVC 编译与插桩运行)。

FAIL 语义已按本仓"合成自证"的纪律验证(不是只测绿的那一半):把
DTU_MS_COVERAGE 指到一个不存在的文件 → 钩子 Failed、脚本退出码 2、并打印可
执行的 [ENV] 提示(说明该 exe 装在哪、怎么覆盖)。脚本对显式环境变量刻意
"不做自动探测回退",所以指错会被点名,而不是悄悄换一份工具跑出另一本账。

顺手修掉三条**现在已经不成立**的文档说法(门禁文档最贵的就是骗人):
- 口径 §7 第 8 条:原写"只有 BuildTools 且没勾代码覆盖率工具的机器会退出码 2"。
  实测本机 Community 18.10 与 BuildTools 18.9 **两份安装都自带**该 exe(mtime
  2026-09-15 02:30 / 2026-09-05 16:17,都是各自安装当日,不是后来补勾)→ VS 18
  的 BuildTools 默认就带它。真实风险面只剩:裸 MSVC 无 VS 的机器、环境变量指错、
  以及靠 PATH 兜底的环境。
- 门禁说明 host-unit-tests.md 盲区第 4 条:原写"没有对应钩子的模块 = 零回归"——
  如今 12 套新模块由总闸兜住,这句话已经不成立,改成两种口径并写清触发面。
- 同篇第 6 条:原写"无覆盖率统计"。行/函数覆盖**已有**度量(本闸出 Cobertura 并
  守基线),仍无的只是分支覆盖——那是用户裁定 2026-10-23 记下的**已接受缺口**,
  不是待办,改口时把这个区分写清楚。

钩子计数同步(新增 1 根 → 声明 40 根 / pre-commit 阶段 39 根 + commit-msg 的
gitlint,已用脚本核对,不是手数):AGENTS.md(总数、枚举、索引表)、
pre-commit使用说明.md(66 / 91 / 234 三处,含清点日期)、门禁说明 README
(正文与索引标题,另加一行索引指向口径文档——23 篇不变,同一事实不在两处维护)、
门禁说明 gitlint.md(--no-verify 跳过的"其它 38 个"→39)、并行任务工作树方案。

自查:end-of-file-fixer 与 trailing-whitespace 复跑 Passed(首轮 --all-files 只
因末行空行红过一次,已按钩子自身规范化);doc-links --all --sections-strict 通过
(178 个文档);id-check --strict 零台账外新账;8 个改动文件 U+FFFD = 0、无 CRLF。
零代码改动,故未跑 @build。未 push。
This commit is contained in:
2026-10-24 07:50:53 +08:00
parent 2c3939494d
commit 744a1cb086
8 changed files with 55 additions and 21 deletions
+3 -3
View File
@@ -63,7 +63,7 @@ git commit
> **2026-10-23 迁移**:原本节 27 个"逐钩子详解"小节,已全部迁往 `docs/门禁说明/`——**每种门禁
> 一篇**,八段固定结构(查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 / **限制与盲区** /
> 豁免与基线口径 / 改门禁本身)。39 个 hook 的总览与"三种拦截姿态" → `docs/门禁说明/README.md`。
> 豁免与基线口径 / 改门禁本身)。40 个 hook 的总览与"三种拦截姿态" → `docs/门禁说明/README.md`。
> 搬走的原因:只写"拦了什么"的文档会让人以为绿就是对的,而"什么时候会骗人"必须有归处。
> 同一事实只在一处详述(`docs/文档规范.md` 同此要求),所以本节不再重复逐钩子表格。
@@ -88,7 +88,7 @@ git commit
| 机制 | 口径 |
|---|---|
| **两个阶段** | pre-commit 阶段 38 个钩子 + commit-msg 阶段 `gitlint`;`default_install_hook_types: [pre-commit, commit-msg]` 是显式声明的——换机后 `pre-commit install` 才会带上 commit-msg 钩子 |
| **两个阶段** | pre-commit 阶段 39 个钩子 + commit-msg 阶段 `gitlint`;`default_install_hook_types: [pre-commit, commit-msg]` 是显式声明的——换机后 `pre-commit install` 才会带上 commit-msg 钩子 |
| **stash 陷阱** | pre-commit 跑钩子前会 stash 未暂存改动,钩子看到的是"没有你中间状态"的树。**改代码 + 同时更新基线/受管区的,必须先 `git add` 再跑钩子**,否则会拿到假报警(本仓踩过,`docs/门禁说明/bsp-err.md` §4 记了现场) |
| **`always: true` 已废** | pre-commit 4.x 对 `local` 钩子里这个键**只告警 `Unexpected key(s)` 然后忽略** → "想总是跑"只能把 `files:` 写全(教训 → §5.5) |
| **改写型与只读型的豁免不对称** | YAML 锚点 `&cubemx-generated` 只豁免 `trailing-whitespace` 与 `end-of-file-fixer`;`cppcheck` / `no-mojibake` / `cubemx-survival` / `task-table` / `mem-numbers` **仍覆盖** `Src/`、`Inc/` |
@@ -231,7 +231,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
| 文件 | 位置 | 说明 |
|---|---|---|
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**39 个钩子**,2026-10-23 清点)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 16 个合一篇,含各套件来历;UI 侧另有 `font-charset` 与 `ui-golden` 两篇——后者此前**在旧手册里连小节都没有**,只活在配置注释里,本次补齐) |
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**40 个钩子**,2026-10-24 清点:新增 `test-coverage` 覆盖率总闸)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 16 个合一篇,含各套件来历;UI 侧另有 `font-charset` 与 `ui-golden` 两篇——后者此前**在旧手册里连小节都没有**,只活在配置注释里,本次补齐) |
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
@@ -1,6 +1,6 @@
# 主机单测覆盖率口径(覆盖率脚本 / 基线 / 盲区)
> **状态**:✅ 已落地(脚本 + 基线 + 口径),⏸ 未挂 pre-commit(理由见 §8);
> **状态**:✅ 已落地,并**已挂 pre-commit**(`test-coverage` 总闸,2026-10-24 接线,缺组件按裁定 FAIL);
> **分支覆盖**:本机可实测拿到(§2.1 的 gcc/WSL 路),但**用户裁定 2026-10-23 不引入第二条工具链**
> → 本文把它记为**已接受缺口**,不是待办
> **日期**:2026-10-23
@@ -152,8 +152,16 @@
(全量跑时以"模块未跑到"判 REGRESS),子集运行明确降级(§5)。
7. **头文件里的代码归 `.h`。** 统计面含 `.h`(静态内联与被展开的行落在头文件上);当前 16 套
几乎没触及 `.h`,表里看不到它们——不要以为头文件"天生 100%"。
8. **依赖 VS 的可选组件。** 只有 BuildTools 且没勾"代码覆盖率工具"的机器会退出码 2;
CI runner 与本机同一台(`docs/CI/README.md`),但**换机先跑 `--list` 再跑全量**。
8. **依赖 `Microsoft.CodeCoverage.Console.exe`。** 探测顺序:`DTU_MS_COVERAGE`(设了就必须
有效、**不静默回退**)→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\` →
PATH;找不到即退出码 2(**不是 0**)。2026-10-24 起该结果直接红掉 `test-coverage` 钩子(用户裁定
FAIL 语义),并已合成自证:把 `DTU_MS_COVERAGE` 指到一个不存在的文件 → 钩子 Failed、脚本退出码 2、
打印可执行的 `[ENV]` 提示。
⚠ **更正本文旧说法**(原写"只有 BuildTools 且没勾代码覆盖率工具的机器会退出码 2"):实测本机
Community 18.10 与 BuildTools 18.9 **两份安装都自带**该 exe(mtime 2026-09-15 02:30 / 2026-09-05
16:17,都是各自安装当日,不是后来补勾)→ VS 18 的 BuildTools 默认就带它。真实风险面只剩:没有
VS 只有裸 MSVC 的机器、`DTU_MS_COVERAGE` 指错、以及靠 PATH 兜底的环境。CI runner 与本机同一台
(`docs/CI/README.md`);**换机仍先跑 `--list` 再跑全量**。
9. **并集口径下"单套百分比"没有意义。** 同一文件被多套触及很常见(`sha256.c` 被 `sha256` 与
`ftp_auth` 都编译),所以对外只报**并集**,单套只报"触及几个文件 / 多少行"。
@@ -161,7 +169,8 @@
**已落地**:脚本、基线台账、口径(本文)、`scripts/ci_local.py --coverage` 一条入口。
**未挂 pre-commit**,三条理由(不是忘了):
**接线状态:已挂**(2026-10-24,`test-coverage` 落在 `.pre-commit-config.yaml` 第 8 节,用户裁定 **FAIL** 语义)。
接线前的三条理由连同它们的现状一并留档,免得后人重问同一件事:
1. 39 个钩子里已有 16 个主机单测钩子(每套各编译各跑),再加一份"全量插桩"在**当时**确实是同一
件事做两遍,还要多约 25 s;
@@ -171,9 +180,10 @@
全部模块)目前只被 `ci_local.py`(L0.5 自动发现)与 `check_test_coverage.py` 跑到,
**不在提交钩子里**。所以现状不是"重复两遍",而是**有一遍没做**。
仍未挂,只剩第 2 条那一个原因:必须先定"缺 VS 组件时 skip 还是 FAIL"——那是留给用户裁定的事
(选 skip 等于造一根会静默失效的门禁,与 `always: true 已失效` 那条同类坑)。与其悄悄挂一根
可能假绿的钩子,不如把缺口写在这里。
当时唯一没解的是第 2 条:必须先定"缺 VS 组件时 skip 还是 FAIL"——那是要用户裁定的事(选 skip
等于造一根会静默失效的门禁,与 `always: true 已失效` 同类)。**2026-10-24 用户选 FAIL,本闸已挂**,
缺口关闭。接线时的两个实测数:全闸跑完 **45 s**(28 套;本文别处写的"约 25 s"是只有 16 套时
的值,已过时);缺组件时钩子 **Failed**、脚本退出码 2 并打印 `[ENV]` 提示(合成自证)。
2. 它依赖 VS 可选组件(§7 第 8 条)。挂之前必须先定"缺组件时 skip 还是 FAIL"——
选 skip 就是一个会静默失效的门禁(与 `AGENTS.md` 里那条 `always: true 已失效、必须靠 files: 覆盖`
是同一类坑);
@@ -182,8 +192,7 @@
不会假报回退(首版会,已修:并集口径下子集必然变小,所以 `--module` 子集跑跳过全部回退判定)。
真实事件(某次改动真把覆盖行弄少了)仍待观察。
真到接线那天,片段形状如下(这份 YAML 由接线者写进 `.pre-commit-config.yaml`,说明写进
`docs/门禁说明/` 对应那篇,不在本文维护):
落库的就是下面这份(`.pre-commit-config.yaml` 第 8 节,与文件里逐字一致,只多不少);`docs/门禁说明/README.md`
```yaml
- repo: local
@@ -397,7 +397,7 @@ docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内
| 主题 | 去处 |
|---|---|
| 提交门禁 39 个钩子 | `docs/代码规范/pre-commit使用说明.md` |
| 提交门禁 40 个钩子 | `docs/代码规范/pre-commit使用说明.md` |
| 自动化测试与台架流程 | `docs/测试说明/自动化测试流程.md` |
| 文档写作规范(编码/链接/登记) | `docs/文档规范.md` |
| 工具路径环境变量(换机) | `docs/环境与部署/本地工具路径环境变量方案.md` |
+3 -2
View File
@@ -1,6 +1,6 @@
# 门禁逐项说明(每种门禁一篇)
> 这一目录回答一个问题:**提交时那 39 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
> 这一目录回答一个问题:**提交时那 40 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
> 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 /
> **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**——
> 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。
@@ -21,7 +21,7 @@
⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。
每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。
## 2. 索引(39 个 hook → 23 篇)
## 2. 索引(40 个 hook → 23 篇 + 1 根在 docs/测试说明/)
| 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 文档 |
|---|---|---|---|---|
@@ -42,6 +42,7 @@
| [`font-charset.md`](font-charset.md) | `font-charset` | UI 用到的汉字是否都在子集字体里(缺字 = 白框) | `Drivers/BSP/UI/**`、字体脚本 | 已写 |
| [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 11 页黄金参考图比对 | UI 源码、模拟器 | 已写 |
| [`host-unit-tests.md`](host-unit-tests.md) | 16 个 `*-unit-test` | 主机侧单测(同一机制 16 个实例):逻辑层在无硬件的 PC 上编译并跑断言 | 各自被测模块与其测试目录 | 已写 |
| 详解在 [`../测试说明/主机单测覆盖率口径.md`](../测试说明/主机单测覆盖率口径.md)(口径 / 分母 / 基线 / 盲区都在那篇,本目录不重复) | `test-coverage` | **覆盖率总闸**:跑全部 28 套主机单测 + 已覆盖行只增不减的回退台账 | `App/`、`Drivers/BSP/`、`test/host/`、脚本与基线 | 已写(在 `docs/测试说明/`) |
| [`trailing-whitespace.md`](trailing-whitespace.md) | `trailing-whitespace` | 行尾空白(改写型;CubeMX/Keil 生成文件豁免) | 全部 | 已写 |
| [`end-of-file-fixer.md`](end-of-file-fixer.md) | `end-of-file-fixer` | 文件末尾必须恰好一个换行(改写型;同豁免) | 全部 | 已写 |
| [`check-added-large-files.md`](check-added-large-files.md) | `check-added-large-files` | 别把大文件(日志/镜像/二进制)误提进仓库 | 全部 | 已写 |
+1 -1
View File
@@ -27,7 +27,7 @@
## 3. 触发面 / 看不见什么
- `git commit`、`--amend`、`-F`、合并生成的提交都会跑;**`--no-verify` 连它和其它 38 个钩子一起跳过**。
- `git commit`、`--amend`、`-F`、合并生成的提交都会跑;**`--no-verify` 连它和其它 39 个钩子一起跳过**。
- 不校验:提交说明与 diff 是否相符、是否真的构建/测试过、是否夹带无关改动、是否该拆成多笔。
## 4. 报错怎么读
+7 -3
View File
@@ -81,11 +81,15 @@
行为不同"。桩签名改了而测试没跟着改,编译器未必报错(尤其桩返回宽类型)。
3. `include_dirs` 与源文件清单是**手写**的:新依赖一个目录就会假失败(见 §4 第 2 类),反过来,
漏了某个真实依赖可能让测试**编过却少编了一个翻译单元**(少测而不自知)。
4. **没有对应钩子的模块 = 零回归**:本层只覆盖上表 16 个模块;其余逻辑层(含 UI 逻辑、网络
状态机主体)只能靠 `ui-golden`、板级套件与真机 `@test`。
4. **专属钩子不是唯一防线**(2026-10-24 更正,原写"没有对应钩子的模块 = 零回归",如今不成立):上表 16
个模块各有专属钩子;仓库另有 12 套(P1 新增)没有专属钩子,改坏了由 `test-coverage` **总闸**兜
住——它跑全部 28 套,并守"已覆盖行只增不减"。总闸的触发面是 `App/`、`Drivers/BSP/`、
`test/host/`、覆盖率脚本与基线;纯文档改动不触发它(那时确实没有可回归的行为)。
5. 时序与并发不在范围:中断抢占、DMA 完成回调、临界区、真机 tick 抖动都照不到(历史上最贵的
几个坑正是这类,见 `docs/问题记录/*_Trap_Records.md`)。
6. 断言写的是"作者当时想到的分支":分支覆盖是否充分,没有任何度量(无覆盖率统计)。
6. 断言写的是"作者当时想到的分支":**行 / 函数覆盖有度量了**(`test-coverage` 总闸出
Cobertura 并守基线台账),但**分支覆盖仍无**——按用户裁定 2026-10-23 记为**已接受缺口**,
口径与理由见 `docs/测试说明/主机单测覆盖率口径.md` §2.1,不是待办。
## 7. 豁免与基线