ci(gates): 覆盖率总闸与全量 cppcheck 移到远端 CI 强制(提交期实跑 48 根)
用户裁定 2026-10-28:慢钩子不再压在每个 git commit 上,由远端 CI 强制;本地等价入口 = scripts/ci_local.py 的 manual 段。 实测三根耗时:cppcheck 20.5 s、test-cppcheck 38.1 s、 test-coverage 105.2 s,合计 ~165 s ≈ 一次带代码提交耗时的 三分之二(test-coverage 的 files: 覆盖 App/,改一行就跑光 40 套)。 - .pre-commit-config.yaml:cppcheck / test-cppcheck / test-coverage 加 stages: [manual] + 理由注释;替换掉 test-coverage 上方「没写 stages → 所有阶段都跑、输出出现两次」 的旧坑注释(固定 manual 后不再有这一坑)。 - .gitea/workflows/ci.yml:static-gates job 在静态门禁步之后 新增「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步, if: always(),逐根 --hook-stage manual(pre_commit run 不支持 多 id),日志 ci-artifacts/manual-stage.log + 归档 + STEP_SUMMARY, rc≠0 即失败;run 块纯 ASCII。 - scripts/ci_local.py:新增 MANUAL_STAGE_HOOKS 与 run_manual_stage() (静态段之后默认跑),删 run_coverage() 死代码,--coverage 降级为兼容别名。 - AGENTS.md + 12 篇 docs/scripts/README:三数同源口径改为 声明 51 / 提交期 --all-files 实跑 47 / +gitlint = 48, 其余 3 根在 manual 阶段;历史测量数字只加「当时」不回改。 验证:pre-commit run --all-files = 47 Passed / 0 Failed rc=0; manual 三根 Passed(test-coverage line 89.8% func 93.2% 41/41 套); ci_local.py --static-only 全绿;check_doc_links --all --sections-strict rc=0、mojibake / bom-drift / id-check-new rc=0; ci.yml yaml.safe_load 通过。未改任何 .c/.h,故未跑 @build。
This commit is contained in:
+49
-1
@@ -2,6 +2,8 @@
|
||||
#
|
||||
# 覆盖范围(**只做不依赖硬件的层**,见 docs/测试说明/测试开发标准流程.md §1/§9):
|
||||
# 静态门禁 static = pre-commit 全部钩子(等价本地 `pre-commit run --all-files`)
|
||||
# + 手动阶段三根(覆盖率总闸 / 全量 cppcheck / test-cppcheck,`--hook-stage manual`;
|
||||
# 它们在 .pre-commit-config.yaml 里带 stages: [manual],提交期不跑,由本 job 强制)
|
||||
# 主机侧纯逻辑单测 host-unit = test/host/**(MSVC 离线编译,零硬件)
|
||||
#
|
||||
# ⛔ 板端测试套件(test/board/,需真机 + ST-LINK)/ 主机侧回归(@test 闭环)/ 人工目检
|
||||
@@ -311,6 +313,47 @@ jobs:
|
||||
exit $rc
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 手动阶段门禁(2026-10-28 用户裁定):主机单测覆盖率总闸 + 全量 cppcheck。
|
||||
# 这三根钩子在 .pre-commit-config.yaml 里带 `stages: [manual]`,**不在 git commit 时跑**
|
||||
# (实测合计 ~165 s = 一次带代码提交耗时的三分之二);提交期只跑快的那些,全量扫描由本 job 强制。
|
||||
# 逐根调用而不是一条命令:`pre_commit run <a> <b>` 不支持多 id(实测 argparse 报
|
||||
# "unrecognized arguments")。
|
||||
- name: 手动阶段门禁(覆盖率总闸 + 全量 cppcheck)
|
||||
if: always()
|
||||
shell: powershell
|
||||
run: |
|
||||
# Pure ASCII only (see file header). Same $ErrorActionPreference dance as the
|
||||
# static-gate step above: the runner injects 'Stop', and '2>&1 |' turns a
|
||||
# native command's stderr into an ErrorRecord that would abort the pipeline.
|
||||
New-Item -ItemType Directory -Force -Path 'ci-artifacts' | Out-Null
|
||||
$log = 'ci-artifacts/manual-stage.log'
|
||||
if (Test-Path $log) { Remove-Item $log -Force }
|
||||
$rc = 0
|
||||
$hooks = @('cppcheck', 'test-cppcheck', 'test-coverage')
|
||||
foreach ($h in $hooks) {
|
||||
Write-Host "== manual hook: $h =="
|
||||
$saved = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $env:PYTHON -s -m pre_commit run --all-files --hook-stage manual $h 2>&1 |
|
||||
Tee-Object -FilePath $log -Append
|
||||
$one = $LASTEXITCODE
|
||||
$ErrorActionPreference = $saved
|
||||
Write-Host "manual hook $h rc=$one"
|
||||
if ($one -ne 0) { $rc = $one }
|
||||
}
|
||||
$lines = Get-Content $log -ErrorAction SilentlyContinue
|
||||
$passed = @($lines | Select-String -Pattern 'Passed$').Count
|
||||
$failed = @($lines | Select-String -Pattern 'Failed$').Count
|
||||
$summary = "manual-stage: coverage + cppcheck -- passed=$passed failed=$failed (log: $log)"
|
||||
Write-Host "::notice::$summary"
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### manual stage gates (coverage + cppcheck)" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $summary -Encoding utf8
|
||||
if ($rc -ne 0) {
|
||||
Write-Host "::error::manual-stage gates failed (see output above and $log)"
|
||||
exit $rc
|
||||
}
|
||||
|
||||
- name: 归档静态门禁日志(供下载/排障)
|
||||
if: always()
|
||||
shell: powershell
|
||||
@@ -325,7 +368,12 @@ jobs:
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Archived log: ``ci-artifacts/static-pre-commit.log`` ($size bytes on runner)" -Encoding utf8
|
||||
}
|
||||
Write-Host "::notice::static-gate artifacts kept on runner at: ci-artifacts\static-pre-commit.log"
|
||||
if (Test-Path 'ci-artifacts\manual-stage.log') {
|
||||
$msize = (Get-Item 'ci-artifacts\manual-stage.log').Length
|
||||
Write-Host "archived: ci-artifacts/manual-stage.log ($msize bytes)"
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Archived log: ``ci-artifacts/manual-stage.log`` ($msize bytes on runner)" -Encoding utf8
|
||||
}
|
||||
Write-Host "::notice::static-gate artifacts kept on runner at: ci-artifacts\ (static-pre-commit.log, manual-stage.log)"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 强制体积门禁(2026-10-25):不需要 Keil,也不需要 .map
|
||||
|
||||
+16
-5
@@ -76,6 +76,11 @@ repos:
|
||||
hooks:
|
||||
- id: cppcheck
|
||||
name: cppcheck
|
||||
# ⚠ stages: [manual](2026-10-28 用户裁定):全库扫描实测 ~20 s,不压在提交期;
|
||||
# 由**远端 CI** 强制(.gitea/workflows/ci.yml 的「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步),
|
||||
# 本地的等价入口 = scripts/ci_local.py(manual 段)或
|
||||
# python -m pre_commit run --all-files --hook-stage manual cppcheck
|
||||
stages: [manual]
|
||||
entry: cppcheck
|
||||
# 注意:YAML 流序列中逗号是元素分隔符,含逗号/空格的参数必须加引号
|
||||
args: ['--enable=warning,performance,portability,style', '--error-exitcode=1',
|
||||
@@ -115,6 +120,9 @@ repos:
|
||||
# 依据:docs/审查记录/测试与架构改进建议.md §3.4;实机测试实施指南.md §8.1
|
||||
- id: test-cppcheck
|
||||
name: cppcheck (test/, warning only)
|
||||
# ⚠ stages: [manual](2026-10-28,同 cppcheck):实测 ~38 s;由远端 CI 与
|
||||
# scripts/ci_local.py 的 manual 段跑,提交期不跑。
|
||||
stages: [manual]
|
||||
entry: cppcheck
|
||||
args: ['--enable=warning', '--error-exitcode=1',
|
||||
'--inline-suppr', '--std=c99', '--quiet',
|
||||
@@ -737,13 +745,16 @@ repos:
|
||||
# 口径 / 分母 / 基线 / 盲区详解都在 docs/测试说明/主机单测覆盖率口径.md,本处只留接线事实。
|
||||
# 触发面**含门禁自身**(.pre-commit-config.yaml):不带自身就等于谁改坏这根闸的 files:
|
||||
# 模式,它就再也不跑且无人察觉(同 lv-conf-sync / bsp-err 的既有做法)。
|
||||
# ⚠ 读输出的坑(2026-10-24 连踩两次才定位):本钩子没写 stages: → 默认属于**所有阶段**,
|
||||
# 于是同一次提交的输出里它出现两次:pre-commit 阶段那一遍才可能实跑;commit-msg 阶段
|
||||
# 那一遍的"文件"是提交信息文件,几乎必然 (no files to check)Skipped。**只看输出结尾会
|
||||
# 把"实跑通过"读成"根本没跑"**——判它跑没跑要认**第一次**出现的那一行。
|
||||
# 实跑证据:只改本文件的提交(e327636 / c4ca292)里 test-coverage 与 bsp-err 都是 Passed。
|
||||
# ⚠ stages: [manual](2026-10-28 用户裁定):本钩子跑光全部 40 套单测(实测 ~105 s,
|
||||
# 占一次带代码提交耗时的三分之二)→ 不压在提交期,由**远端 CI** 强制
|
||||
# (.gitea/workflows/ci.yml 的「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步),本地等价入口 =
|
||||
# scripts/ci_local.py(manual 段);手工复跑:
|
||||
# python -m pre_commit run --all-files --hook-stage manual test-coverage
|
||||
# 另:此前它没写 stages: → 默认属于所有阶段,同一次提交输出里出现两次(commit-msg
|
||||
# 阶段那遍几乎必然 Skipped,只看结尾会把实跑读成"没跑")。固定 manual 阶段后不再有这一坑。
|
||||
- id: test-coverage
|
||||
name: 主机单测覆盖率(增量拦截:已覆盖行只增不减)
|
||||
stages: [manual]
|
||||
entry: python scripts/check_test_coverage.py
|
||||
language: system
|
||||
files: ^(App/|Drivers/BSP/|test/host/|scripts/check_test_coverage\.py|scripts/test_coverage_baseline\.json|\.pre-commit-config\.yaml$)
|
||||
|
||||
+13
-8
@@ -988,7 +988,7 @@ github.com 会在 checkout 步骤卡住/失败。三种处置:
|
||||
|
||||
| job | `runs-on` | 步骤要点 |
|
||||
|---|---|---|
|
||||
| `static-gates` | `windows` | checkout(`fetch-depth: 0`,gitlint/cubemx 需完整历史)→ 环境自检(**Python 探测走 `scripts/ci_find_python.ps1`**、cppcheck 按 `DTU_CPPCHECK` → PATH → 标准安装目录,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **提交信息门禁 `scripts/ci_gitlint_range.ps1`**(见 §7 排障表"钩子条数对不上"行)→ **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/static-pre-commit.log`)→ 打印 `::notice::static: passed/failed/skipped` 汇总行 → 归档日志说明 |
|
||||
| `static-gates` | `windows` | checkout(`fetch-depth: 0`,gitlint/cubemx 需完整历史)→ 环境自检(**Python 探测走 `scripts/ci_find_python.ps1`**、cppcheck 按 `DTU_CPPCHECK` → PATH → 标准安装目录,缺则 `::error::` + exit 1)→ `pre_commit install --install-hooks` → **提交信息门禁 `scripts/ci_gitlint_range.ps1`**(见 §7 排障表"钩子条数对不上"行)→ **决定跳过哪些钩子**(探测 `.map`,写入 `GITHUB_OUTPUT`;无 `.map` 时 `::warning::` 声明体积门禁未跑)→ `pre-commit run --all-files --show-diff-on-failure`(`env: SKIP`,输出 `Tee` 到 `ci-artifacts/static-pre-commit.log`;只跑 `pre-commit` 阶段的 47 根)→ 打印 `::notice::static: passed/failed/skipped` 汇总行 → **「手动阶段门禁」步**:逐根 `-m pre_commit run --all-files --hook-stage manual` 跑 `cppcheck` / `test-cppcheck` / `test-coverage`(`$log=ci-artifacts/manual-stage.log`,汇总行 `manual-stage: coverage + cppcheck -- passed/failed`,`if: always()` 归档日志)→ 归档日志说明 |
|
||||
| `host-unit-tests` | `windows` | checkout → **探测 MSVC**(按 VS_ROOTS 找 `Hostx64\x64\cl.exe`)→ 遍历 `test/host/**/run_unit_test.py` 逐个跑(输出 `Tee` 到 `ci-artifacts/host-unit-<模块>.log`;解析各模块 `N/M PASSED` 汇总行 → 聚合 `::notice::host-unit: K runner(s), A/B assertions passed`;失败 `::error::` 并置退出码);**未探测到 MSVC 时打印醒目 WARNING 并降级跳过**(不硬失败,避免"没装 VS 的 runner 全红") |
|
||||
|
||||
> 🔴 **两条 Python 探测链必须同序同步**:本地 `scripts/ci_local.py` 与 CI 侧
|
||||
@@ -1009,7 +1009,7 @@ github.com 会在 checkout 步骤卡住/失败。三种处置:
|
||||
| 3 | `scripts\ci_runner_bootstrap.ps1 -Status` | 计划任务已注册 / daemon 在跑 / Gitea 可达 / 三个 `DTU_*` 已设——**四项全绿** |
|
||||
| 4 | runner 上 `python --version` / `cppcheck --version` / `python -m pre_commit --version` | 三条全通(且对 SYSTEM 成立,见 §4.1) |
|
||||
| 5 | `git push` 一次(或页面 Run workflow) | Actions 页出现运行中任务 |
|
||||
| 6 | `static-gates` 日志 | `using python (…)` + `runner environment check passed`;`::notice::static: pre-commit hooks -- passed=.. failed=0 skipped=..`;无 `.map` 时见 `::warning::` 体积门禁未跑 |
|
||||
| 6 | `static-gates` 日志 | `using python (…)` + `runner environment check passed`;`::notice::static: pre-commit hooks -- passed=.. failed=0 skipped=..`;无 `.map` 时见 `::warning::` 体积门禁未跑;「手动阶段门禁」步见 `manual-stage: coverage + cppcheck -- passed=3 failed=0` |
|
||||
| 7 | `static-gates` 的 **提交信息门禁** 步骤 | `::notice::gitlint: N commit message(s) checked, 0 failed`(**这一行以前根本不存在**,见 §7) |
|
||||
| 8 | `host-unit-tests` 日志 | `::notice::host-unit: <N> runner(s), A/A assertions passed, fail=0`。**`N` = `test/host/` 下 `run_unit_test.py` 的个数**(2026-10-12 实测:`master` 3 个 / 288 断言,`v2-on-ui` 10 个);断言总数随单测增减而变,判定只看 **`fail=0` 且 A==B**,不要拿历史数字当基准 |
|
||||
| 9 | job 运行页的 **Summary** 区块 | 出现 "静态门禁 static gates" / "Commit message gate (gitlint)" / "主机侧纯逻辑单测 host-side unit tests" 小标题与汇总行 |
|
||||
@@ -1059,7 +1059,7 @@ date +%s # 对比 last_online
|
||||
| Runners 页 runner 显示 **`offline`** | daemon 进程不在:`gitea-runner` v4 的 `daemon` **首次 Declare 失败就直接退出**(Gitea 重启/机器重启后无人拉起) | §2.4 用 `ci_runner_bootstrap.ps1` 装看门狗 + 计划任务;`-Status` 复查 |
|
||||
| `static-gates` 报 `no Python with a SYSTEM-visible pre-commit` | runner 是 SYSTEM:PATH 上的 python 无 pre-commit、用户级 site 不可见、`.venv` 未入库(§4.1 三重坑) | `ci_runner_bootstrap.ps1 -SetEnv` 写机器级 `DTU_PRECOMMIT_PYTHON`,或把 pre-commit 装进机器级解释器;**改完必须重启 runner** |
|
||||
| `static-gates` 报 `runner missing cppcheck` | 机器 PATH 无 Cppcheck 且无机器级 `DTU_CPPCHECK` | `-SetEnv`(会自动发现 `C:\Program Files\Cppcheck`)或装 `Cppcheck.Cppcheck` |
|
||||
| **钩子条数对不上**(CI 报告条数 = 定义数 − 1;2026-10-12 实测 `master` 18 定义 / 17 报告,`v2-on-ui` 26 定义 / 25 报告——**增删钩子的分支必须重算这个差值**) | `gitlint` 的 `stages: [commit-msg]` 写在**上游** `.pre-commit-hooks.yaml`(项目 `.pre-commit-config.yaml` 里 grep 不到,别据此断定"没限制"),`pre-commit run --all-files` **跑不到它**,且表现为全绿——静默空转 | 已由 `scripts/ci_gitlint_range.ps1` 单独校验;**以后增删钩子必须核对"定义数 == 日志条数 + gitlint"** |
|
||||
| **钩子条数对不上**(CI 的 `--all-files` 报告条数 = 定义数 − 1 − 3 manual:定义数 **51** → 报告 **47** 行,另由「手动阶段门禁」步跑 3 根;2026-10-12 **当时**实测 `master` 18 定义 / 17 报告,`v2-on-ui` 26 定义 / 25 报告——**增删钩子的分支必须重算这个差值**) | 两处都不进 `--all-files` 报告:`gitlint` 的 `stages: [commit-msg]` 写在**上游** `.pre-commit-hooks.yaml`(项目 `.pre-commit-config.yaml` 里 grep 不到,别据此断定"没限制");`cppcheck` / `test-cppcheck` / `test-coverage` 带 `stages: [manual]`,只由「手动阶段门禁」步逐根 `--hook-stage manual` 跑。都会表现为全绿——静默空转 | commit-msg 阶段已由 `scripts/ci_gitlint_range.ps1` 单独校验、manual 三根已由「手动阶段门禁」步兜住;**以后增删钩子必须核对"定义数 == 日志条数 + gitlint + manual 三根"** |
|
||||
| gitlint 步骤 **Failed 但提交信息其实合规**(同一条命令在本地全绿),日志里是 `exit code: 254` + `Missing git configuration: please set user.name` | `actions/checkout` 把 HOME 临时指向每次任务的临时目录,而 SYSTEM 账号没有全局 git 身份 → gitlint **在读到任何规则之前**就因配置错误退出(**254 不是规则违规**)。旧脚本末尾那句 `::error::...rule: single-line Chinese title, < 100 chars` 是**写死的猜测文案**,把环境问题伪装成"提交信息不合格";且该步 `exit 1` 会让后面的 `--all-files` **整个不跑**——看着像"门禁拦住了",实际 26 个钩子一个都没执行 | `ci_gitlint_range.ps1` 先探测身份,缺失就用 `GIT_CONFIG_COUNT/KEY/VALUE` 注入一次性身份(**不写任何文件**,开发者机器不受影响),并把 254 与真实违规分开报(**2 = 门禁没跑起来**,1 = 信息确实违规)。实测同一命令:空 HOME 下改前 254、改后 `0 failed`(2026-10-12 task 31 首次暴露) |
|
||||
| push 后 Actions 页**空白/无任务** | ① 服务端开关未开 ② workflow 没 push 到远程 ③ 只改了 `docs/**`(被 `paths-ignore` 滤掉) ④ **新建分支的首次 push**(实测 2026-10-12:创建 `v2-on-ui-ci` 那次 push 没产生 run——runner 在线空闲、job 日志零新增;紧接着一次普通 push 立刻出 task。原因:`before` 是全零 SHA,变更文件集算不出来,`paths-ignore` 求值时被滤空) | ①②③ 逐条核对 §1 / §5.1 / §5.2;④ 用 `workflow_dispatch` 手动触发,或再推一个非文档路径的提交 |
|
||||
| 任务一直 **Waiting / 排队** | 没有 label 匹配 `windows` 的 runner | 检查 `--labels "windows:host"`;Settings→Runners 是否 Idle |
|
||||
@@ -1206,8 +1206,11 @@ runner 的日志走 **stderr**,于是上面两个日志文件都栽在这两
|
||||
(两个 job 共用),本地 `scripts/ci_local.py` 的候选表须与它**同序**;
|
||||
MSVC 探测仍是 workflow 内联 + `test/host/common/unit_test_harness.py` **两处一致**;
|
||||
- **钩子集变化**(`.pre-commit-config.yaml` 增删钩子):CI 自动跟随,无需改 workflow;
|
||||
但**必须核对条数**——`pre-commit run --all-files` 只跑 `pre-commit` stage,
|
||||
但**必须核对条数**——`pre-commit run --all-files` 只跑 `pre-commit` stage(现 47 根),
|
||||
`commit-msg` stage 的钩子(gitlint)不在其中,靠 `ci_gitlint_range.ps1` 单独守(见 §7);
|
||||
带 `stages: [manual]` 的三根(`cppcheck` / `test-cppcheck` / `test-coverage`)也不在其中,
|
||||
靠 static-gates 的「手动阶段门禁」一步逐根 `--hook-stage manual` 跑(新增 manual 钩子须同步加进
|
||||
该步的 `$hooks` 列表,否则只声明不执行);
|
||||
- **`@build` / 板端测试套件 / 主机侧回归**:永远留在本地,不要往 CI 加(需 Keil 授权与真机);
|
||||
- **runner 长时间不可用**时:CI 只是"多一道网",**提交前的本地 `ci_local.py` 仍是 静态门禁的唯一保障**,
|
||||
不能因"已经配了 CI"放宽(见 [docs/任务规划/登记簿.md](../任务规划/登记簿.md) 的条目「CI 远端闭环未验证」)。
|
||||
@@ -1326,11 +1329,13 @@ powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
|
||||
> 故已改写为看门狗方案;旧脚本的失效机理见 §3.2。
|
||||
|
||||
> **CI 验收标准**:两个 job 都要 `status=1`。**口径是规则,不是具体数字**(2026-10-12):
|
||||
> `static-gates` 报 **定义数 − 1 条钩子**(gitlint 属 `commit-msg` stage,不进 `--all-files` 报告,
|
||||
> 见 §7),其中 `mem-numbers` 在无 `.map` 时为 `Skipped`(本机唯一常见跳过项);另有提交信息
|
||||
> 门禁 `::notice::gitlint: N checked, 0 failed`。`host-unit-tests` 报
|
||||
> `static-gates` 先报 **定义数 − 1 − 3 manual 条钩子**(现 51 − 1 − 3 = **47** 条;`gitlint` 属 `commit-msg`
|
||||
> stage、`cppcheck` / `test-cppcheck` / `test-coverage` 属 `manual` stage,都不进 `--all-files` 报告,
|
||||
> 见 §7),其中 `mem-numbers` 在无 `.map` 时为 `Skipped`(本机唯一常见跳过项);再报「手动阶段门禁」
|
||||
> 步的 `manual-stage: coverage + cppcheck -- passed=3 failed=0`;另有提交信息门禁
|
||||
> `::notice::gitlint: N checked, 0 failed`。`host-unit-tests` 报
|
||||
> **`<N> runner(s), A/A assertions passed, fail=0`**,`N` = `test/host/` 下 `run_unit_test.py` 个数。
|
||||
> 实测快照:`master` 18 定义 / 17 报告、3 套件 / 288 断言;`v2-on-ui` 26 定义 / 25 报告、10 套件。
|
||||
> 实测快照(**当时**):`master` 18 定义 / 17 报告、3 套件 / 288 断言;`v2-on-ui` 26 定义 / 25 报告、10 套件。
|
||||
> 🗑 更早快照(勿当基准):run#8(2026-10-02)当时是"14 个钩子全 Passed + 39/39 单测"。
|
||||
> **判定只看 `fail=0`、`failed=0` 与 A==B**;增删钩子或单测后按上面的规则重算,别抄历史数字。
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
|
||||
|
||||
**工作原理**:逐个读取 C 文件 → 分析语法/数据流/控制流 → 发现问题则报错。
|
||||
|
||||
**与 pre-commit 的关系**:`pre-commit` 阶段的 `cppcheck` 钩子调用 cppcheck 命令行,任一已启用类别发现问题即**阻止提交**(`--error-exitcode=1` 对 warning/performance/portability/style 全部生效)。
|
||||
**与 pre-commit 的关系**:`cppcheck` / `test-cppcheck` 两根钩子都带 `stages: [manual]`——**`git commit` 阶段不跑**,由远端 CI(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段调用 cppcheck 命令行;任一已启用类别发现问题即失败(`--error-exitcode=1` 对 warning/performance/portability/style 全部生效)。
|
||||
|
||||
### 日常速度
|
||||
|
||||
- **增量扫描**(`git commit` 时):只检查 staged 文件,单文件约 1~2 秒
|
||||
- **全库扫描**(`pre-commit run --all-files`):约 5 秒,用于配置变更后验证
|
||||
- **提交期**:不跑(`stages: [manual]`),一次带代码的 `git commit` 只跑 48 根快钩子
|
||||
- **全库扫描**(约 20 s;2026-10-28 实测 `cppcheck` 20.5 s、`test-cppcheck` 38.1 s):手工复跑 `& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual <hook-id>`——**一次只能给一个 id**(`pre_commit run a b c` 实测报 `unrecognized arguments`),故 `cppcheck` / `test-cppcheck` 各跑一次
|
||||
|
||||
---
|
||||
|
||||
@@ -112,7 +112,7 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
|
||||
> - 上表规则均出自 cppcheck 2.21.0 `--errorlist` 中 `severity=style` 的真实清单。
|
||||
> - 部分规则(如 `duplicateBranch`、`suspiciousSemicolon`、`checkConst`)需要额外 `--inconclusive` 才会激活,为减少误报**未开启**。
|
||||
> - `unusedFunction`、`unusedStructMember`、`useStlAlgorithm`、`noExplicitConstructor` 等已在 `--suppress` 中抑制,原因见第 4.3 节「抑制规则说明」。
|
||||
> - `--error-exitcode=1` 对**所有已启用级别**(warning/performance/portability/style)生效:任何一级发现问题都会**阻止提交**。style 级别的问题若确属误报,用行内抑制(见第 5 节)。
|
||||
> - `--error-exitcode=1` 对**所有已启用级别**(warning/performance/portability/style)生效:任何一级发现问题都会让门禁**失败**(在 CI / `ci_local.py` 的 manual 段拦下)。style 级别的问题若确属误报,用行内抑制(见第 5 节)。
|
||||
|
||||
---
|
||||
|
||||
@@ -122,7 +122,7 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
|
||||
|
||||
```
|
||||
--std=c99 # 按 C99 标准检查(Keil 工程常用)
|
||||
--error-exitcode=1 # 发现问题返回非零退出码(pre-commit 用来拦截提交)
|
||||
--error-exitcode=1 # 发现问题返回非零退出码(pre-commit 据此判该钩子失败)
|
||||
--inline-suppr # 启用行内抑制注释(// cppcheck-suppress,默认关闭)
|
||||
--quiet # 只报问题,不显示扫描进度
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ git commit
|
||||
├─ check-added-large-files 阻止 >1MB 大文件
|
||||
├─ check-merge-conflict 阻止含冲突标记的文件
|
||||
├─ check-yaml 校验 YAML 语法
|
||||
├─ cppcheck C 静态分析(自有代码)
|
||||
├─ no-mojibake 阻止 U+FFFD 乱码文件
|
||||
├─ coding-standard C 代码规范检查(规范V1.0 可机械校验项)
|
||||
├─ doc-links 文档链接存在性(.md)
|
||||
@@ -30,12 +29,18 @@ git commit
|
||||
└─ font-charset UI 字符集差集检查(防缺字白框,「UI 字符集与像素回归门禁」,2026-10-15)
|
||||
└─ commit-msg(.git/hooks/commit-msg)
|
||||
└─ gitlint 校验 commit message 格式
|
||||
└─ manual(pre-commit --hook-stage manual;不随 git commit 跑)
|
||||
├─ cppcheck 全量 C 静态分析(自有代码)
|
||||
├─ test-cppcheck test/ 侧静态分析
|
||||
└─ test-coverage 主机单测覆盖率总闸(40 套 + 回退台账)
|
||||
```
|
||||
|
||||
**两阶段钩子**:`pre-commit` 阶段拦截代码问题,`commit-msg` 阶段拦截格式问题。任一失败 → commit 中止。
|
||||
**阶段**:`pre-commit` 阶段拦截代码问题、`commit-msg` 阶段拦截格式问题,任一失败 → commit 中止;另有 `manual` 阶段(`stages: [manual]` 的慢钩子)**不随 `git commit` 跑**,由远端 CI 与 `scripts/ci_local.py` 的 manual 段承担(见下)。一次 `git commit` 实跑 **48** 根 = `pre-commit` 阶段 47 根 + `gitlint` 1 根。
|
||||
|
||||
> **CI 侧的同一套门禁(2026-10-01 起)**:Gitea Actions(`.gitea/workflows/ci.yml`)在 push / PR 时
|
||||
> 以 `pre-commit run --all-files` 复跑**全部上述钩子**(`mem-numbers` 在无 Keil `.map` 的 runner 上自适应跳过)。
|
||||
> 以 `pre-commit run --all-files` 复跑 **`pre-commit` 阶段的 47 根**(`mem-numbers` 在无 Keil `.map` 的 runner 上自适应跳过),
|
||||
> 再以「手动阶段门禁」步**逐根** `--hook-stage manual` 复跑 `cppcheck` / `test-cppcheck` / `test-coverage` 三根
|
||||
> (`pre_commit run a b c` 不支持多 id,实测报 `unrecognized arguments`)。
|
||||
> 本地提交前想跑同一套判定:`& '.venv\Scripts\python.exe' scripts/ci_local.py`。详见 `../测试说明/测试开发标准流程.md` §9。
|
||||
|
||||
### 依赖清单
|
||||
@@ -88,7 +93,7 @@ git commit
|
||||
|
||||
| 机制 | 口径 |
|
||||
|---|---|
|
||||
| **两个阶段** | pre-commit 阶段 **50** 个钩子 + commit-msg 阶段 `gitlint`(口径 = YAML 解析后的 hook 条目数 **51** 减去 gitlint;自查命令与「声明 / 文档 / 实跑」三数同源核对 → `docs/门禁说明/README.md` §2);`default_install_hook_types: [pre-commit, commit-msg]` 是显式声明的——换机后 `pre-commit install` 才会带上 commit-msg 钩子 |
|
||||
| **三个阶段** | pre-commit 阶段 **47** 个钩子 + commit-msg 阶段 `gitlint`(= 一次 `git commit` 实跑 **48**)+ `manual` 阶段 3 根(`cppcheck` / `test-cppcheck` / `test-coverage`:`git commit` 不跑,只在远端 CI 的「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑;三根实测合计约 165 s ≈ 一次带代码提交耗时的三分之二)→ 口径 = YAML 解析后的 hook 条目数 **51**;自查命令与「声明 / 文档 / 实跑」三数同源核对 → `docs/门禁说明/README.md` §2;`default_install_hook_types: [pre-commit, commit-msg]` 是显式声明的——换机后 `pre-commit install` 才会带上 commit-msg 钩子 |
|
||||
| **一根钩子在输出里出现两次** | 未声明 `stages:` 的钩子**默认属于所有阶段**,pre-commit 与 commit-msg 两遍都会列出它;第二遍的"文件"是提交信息文件,几乎必然 `(no files to check)Skipped`。**只看输出结尾会把"实跑通过"读成"没跑"**——判某钩子跑没跑要认它**第一次**出现的那行(实测 2026-10-24:只改 `.pre-commit-config.yaml` 的提交里 `test-coverage` 与 `bsp-err` 都是 Passed,尾巴那一遍是 Skipped) |
|
||||
| **stash 陷阱** | pre-commit 跑钩子前会 stash 未暂存改动,钩子看到的是"没有你中间状态"的树。**改代码 + 同时更新基线/受管区的,必须先 `git add` 再跑钩子**,否则会拿到假报警(本仓踩过,`docs/门禁说明/bsp-err.md` §4 记了现场)。**它藏不住什么,见下一行** |
|
||||
| **stash 覆盖面:只覆盖已跟踪文件的未暂存改动**(2026-10-26 读源码定案,**对所有钩子通用**) | 机制在 pre-commit 的 `staged_files_only._unstaged_changes_cleared()`:`git diff-index <tree>` 取补丁 → `git checkout -- .` → 跑完 `git apply` 还原。所以「钩子实跑 = 只看到 HEAD + 本笔 staged」这句**对已跟踪文件成立、对未跟踪文件不成立**:未跟踪文件压根不在 `diff-index` 里,**不会被藏**;`git add -N`(intent-to-add)也救不了——pre-commit 只 `git rm --cached` 把它撤下索引(`_intent_to_add_cleared()`),**文件本体仍在盘上**。后果:别人写了尚未入库的文件(新页面实例 / 新单测目录 / 新文档)躺在树里时,**任何一笔碰到该文件所属钩子 `files:` 的提交都会被这份半成品拖红**——红的内容没错,但账不该记在这笔提交上。实测:`ui_page_clock.c`、`ui_page_reset.c` 处于未跟踪且未绑 `#page` 期间,`generated-fresh` 对**所有人**的 UI 提交都会 FAIL(现场日志 `docs/门禁说明/generated-fresh.md` §6 第 8 条)。三条结论:① 提交排序——会让全树变红的半成品要么先落地(绑引用 + 重跑生成器)要么先挪出工作树;② 凡「判定基准取工作区」的钩子(重跑比对类、全库统计类)在多人同树下必须配**静默窗口**;③ 真要 `--no-verify` 抢跑,正文必须点名是哪几个**未跟踪**文件拖的红,别让它被读成「钩子不可信」 |
|
||||
@@ -104,11 +109,11 @@ git commit
|
||||
|
||||
- 提交前等价自查(静态门禁 + 提交信息门禁 + 主机侧纯逻辑单测):`scripts/ci_local.py`(用 `.venv` 解释器跑)。
|
||||
- 单独跑某一条:`-m pre_commit run <hook-id> --files <改动的文件>`;全库:`--all-files`。
|
||||
- 临时跳过只在紧急情况,且必须在提交说明里写明跳过了什么、为什么、随后怎么补——**跳之前先跑 §2.4 自查表**(`--no-verify` 一次跳过 49 条,连 `gitlint` 一起跳,见 §2.2「两个阶段」行的 49/50 口径)。
|
||||
- 临时跳过只在紧急情况,且必须在提交说明里写明跳过了什么、为什么、随后怎么补——**跳之前先跑 §2.4 自查表**(`--no-verify` 一次跳过 47 条,连 `gitlint` 一起跳 = 48 条,见 §2.2「三个阶段」行)。
|
||||
|
||||
### 2.4 抢跑自查表(`--no-verify` 之前自己跑一遍:只读、不 stash、别人正在写盘也安全)
|
||||
|
||||
`--no-verify` 跳过的不是「一条格式检查」,是**全部 49 条**(外加 commit-msg 阶段的 `gitlint`)。
|
||||
`--no-verify` 跳过的不是「一条格式检查」,是**全部 47 条**(外加 commit-msg 阶段的 `gitlint`,共 48 条)。
|
||||
2026-10-26 本仓连着抢了三笔,放走三条本该被拦的真问题,都在提交正文里可复核:
|
||||
|
||||
| 抢跑那笔 | 本该由谁拦下 | 后果什么时候才看得见 |
|
||||
@@ -158,6 +163,7 @@ git commit
|
||||
| **改写型钩子** `trailing-whitespace` / `end-of-file-fixer` | 它们会**改文件**。真提交里改的是 stash 之后的树;而手跑 `--files` **不 stash**(§2.2 那两行),直接落在工作区——**列了别人在飞的文件就等于替人改文件** | 只列自己的文件:`-m pre_commit run <id> --files <自己的文件>`;或先用 A 组的一行式查出问题自己修,别让钩子替你动盘 |
|
||||
| **要靠 stash 才能正确归因**的判定:`mem-numbers` / `mem-budget`(读磁盘 `.map`)、`stack_report`(读磁盘 `.htm`)、`ui-golden` / `uinav-selftest`(要模拟器 exe 与台架锁)、`test-coverage`(会编译,可能撞别人的 `.obj`) | 磁盘上的 `.map` / `.htm` / `.exe` 属于**最后一次构建的人**,不是你的 ⇒ 手跑红常常是替别人背账(今天 `mem-budget` 就红在「台账与 `.map` 不同码」,因为 `.map` 里混着三条在飞源码道) | **照样要跑**(宁可多跑),但**红先归因**:`git status --porcelain` 看是谁在飞、`.htm` 的时间戳是哪次构建;确认不属本笔就在提交正文点名,**不要改基线数字去凑绿** |
|
||||
| **只有真提交才会碰到的面**:`gitlint`(commit-msg 阶段)与未声明 `stages:` 会跑两遍的钩子 | 手跑脚本查不到提交信息规范(标题 ≤100、**正文每行 ≤80**) | 提交信息一律 `git commit -F <UTF-8 无 BOM 文件>` 并自查最长行;或先老实提交一次让 `gitlint` 说话,再改成 `--no-verify` |
|
||||
| **`manual` 阶段的三根**(`cppcheck` / `test-cppcheck` / `test-coverage`) | `pre-commit run --all-files` 只跑 `pre-commit` 阶段(47 根),**不会碰它们**——漏跑不会报红,只会静默少一道门禁 | 抢跑时补:`& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual <hook-id>`,一次一个 id(见 §2.2「三个阶段」行) |
|
||||
|
||||
#### C. 抢跑之后(三条都来自今天的真事,不是设想)
|
||||
|
||||
@@ -260,7 +266,7 @@ git commit -m "紧急修复" --no-verify
|
||||
SKIP=cppcheck git commit -m "紧急修复"
|
||||
```
|
||||
|
||||
> ⚠️ **不推荐**:`--no-verify` 一次跳过 49 条(连 `gitlint` 一起),不是「跳过格式检查」。**跳之前必须先跑 §2.4 抢跑自查表**
|
||||
> ⚠️ **不推荐**:`--no-verify` 一次跳过 47 条(连 `gitlint` 一起 = 48 条;manual 阶段三根本来不随提交跑),不是「跳过格式检查」。**跳之前必须先跑 §2.4 抢跑自查表**
|
||||
> (尤其 `stack_report.py`——它不在钩子清单里,只有人会跑;数字与安全边界类问题不会自己冒出来)。跳之后按 §2.4 C 组
|
||||
> 补齐三件事:`git status` 对交付清单、正文点名「哪些没跑 / 为什么 / 谁在飞」、静默窗口补一次 `run --all-files`。
|
||||
|
||||
@@ -326,7 +332,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
|
||||
|
||||
| 文件 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**51 个钩子**;口径 = YAML 解析后的 hook 条目数,自查命令 → `docs/门禁说明/README.md` §2。原写「40 个,2026-10-24 清点」是当时的快照数、之后加钩子没同步——正是 §2.2 计数口径行要避免的事)。2026-10-28 复核 = 51:同日 51 → 52(加 id-check-new)、随即 52 → 51(退役 id-check),一增一减写成当日一行的两个阶段、净零。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 **22 根**合一篇,含各套件来历与「18 套无专属钩子」台账 `docs/门禁说明/host-unit-tests.md` §6;UI 侧另有 `font-charset` 与 `ui-golden` 两篇——后者此前**在旧手册里连小节都没有**,只活在配置注释里,本次补齐) |
|
||||
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**51 个钩子**声明数,一次 `git commit` 实跑 48 根;口径 = YAML 解析后的 hook 条目数,自查命令 → `docs/门禁说明/README.md` §2。原写「40 个,2026-10-24 清点」是当时的快照数、之后加钩子没同步——正是 §2.2 计数口径行要避免的事)。2026-10-28 复核 = 51:同日 51 → 52(加 id-check-new)、随即 52 → 51(退役 id-check),一增一减写成当日一行的两个阶段、净零。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 **22 根**合一篇,含各套件来历与「18 套无专属钩子」台账 `docs/门禁说明/host-unit-tests.md` §6;UI 侧另有 `font-charset` 与 `ui-golden` 两篇——后者此前**在旧手册里连小节都没有**,只活在配置注释里,本次补齐) |
|
||||
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
|
||||
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
|
||||
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
|
||||
|
||||
+10
-3
@@ -1,6 +1,7 @@
|
||||
# 主机单测覆盖率口径(覆盖率脚本 / 基线 / 盲区)
|
||||
|
||||
> **状态**:✅ 已落地,并**已挂 pre-commit**(`test-coverage` 总闸,2026-10-24 接线,缺组件按裁定 FAIL);
|
||||
> **状态**:✅ 已落地,并**已挂 pre-commit**(`test-coverage` 总闸,2026-10-24 接线,缺组件按裁定 FAIL;
|
||||
> 该钩子带 `stages: [manual]`——`git commit` 阶段不跑,由远端 CI「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑);
|
||||
> **分支覆盖**:本机可实测拿到(§2.1 的 gcc/WSL 路),但**用户裁定 2026-10-23 不引入第二条工具链**
|
||||
> → 本文把它记为**已接受缺口**,不是待办
|
||||
> **日期**:2026-10-23
|
||||
@@ -168,9 +169,13 @@
|
||||
|
||||
## 8. 接线状态与下一步
|
||||
|
||||
**已落地**:脚本、基线台账、口径(本文)、`scripts/ci_local.py --coverage` 一条入口。
|
||||
**已落地**:脚本、基线台账、口径(本文);`test-coverage` 已并进 `scripts/ci_local.py` 的 manual 段(`--coverage` 旗标保留只为兼容旧命令)。
|
||||
|
||||
**接线状态:已挂**(2026-10-24,`test-coverage` 落在 `.pre-commit-config.yaml` 第 8 节,用户裁定 **FAIL** 语义)。
|
||||
它带 `stages: [manual]`——**`git commit` 阶段不跑**(一次带代码的提交只跑 48 根快钩子),由远端 CI 的
|
||||
「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步与 `scripts/ci_local.py` 的 manual 段承担;手工复跑
|
||||
`& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual test-coverage`。
|
||||
|
||||
接线前的三条理由连同它们的现状一并留档,免得后人重问同一件事:
|
||||
|
||||
1. 39 个钩子里已有 16 个主机单测钩子(每套各编译各跑),再加一份"全量插桩"在**当时**确实是同一
|
||||
@@ -179,7 +184,8 @@
|
||||
⚠ **这条理由的事实基础已变(2026-10-24,P1 落地后)**:本仓现有
|
||||
`test/host/**/run_unit_test.py` **33 套**,而钩子只挂 **16 套**——另外 17 套(P1/P2 新增的
|
||||
全部模块)目前只被 `ci_local.py`(主机侧纯逻辑单测自动发现)与 `check_test_coverage.py` 跑到,
|
||||
**不在提交钩子里**。所以现状不是"重复两遍",而是**有一遍没做**。
|
||||
**不在提交期跑的那些钩子里**(`test-coverage` 本身带 `stages: [manual]`,提交期同样不跑)。
|
||||
所以现状不是"重复两遍",而是**总闸一遍 + 提交期一遍快归因**的分工。
|
||||
|
||||
当时唯一没解的是第 2 条:必须先定"缺 VS 组件时 skip 还是 FAIL"——那是要用户裁定的事(选 skip
|
||||
等于造一根会静默失效的门禁,与 `always: true 已失效` 同类)。**2026-10-24 用户选 FAIL,本闸已挂**,
|
||||
@@ -201,6 +207,7 @@
|
||||
hooks:
|
||||
- id: test-coverage
|
||||
name: 主机单测覆盖率(增量拦截:已覆盖行只增不减)
|
||||
stages: [manual] # 不随 git commit 跑;CI「手动阶段门禁」步与 ci_local.py manual 段跑
|
||||
entry: python scripts/check_test_coverage.py
|
||||
language: system
|
||||
files: '^(App/|Drivers/BSP/|test/host/|scripts/check_test_coverage\.py|scripts/test_coverage_baseline\.json|\.pre-commit-config\.yaml$)'
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
| A2 | 产品构建 0/0 | `& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py` | 末行 `BUILD_OK: 0 errors, 0 warnings` |
|
||||
| A3 | 构建产物为**产品态** | 检查 `test/board/test_config.h` L30-33 | 四个 `TEST_SUITE_*` **全部注释** |
|
||||
| A4 | `.map` 非测试态 | 看 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map` 的 `ER_IROM1` | 产品态基线随已接受的修复增长:262,100(初版)→ 262,836 B(10-02 第 1~6 轮台架)→ 264,284 B(第 7~9 轮台架)→ **264,936 B(当前,含 A2/A3/A4 补码)**;明显阶跃 ≈3.7KB 及以上(10-02 实测产品 262,836 → 套件 266,540)= 停在套件态 → 重跑 A2 |
|
||||
| A5 | 全量静态门禁 | `pre-commit run --all-files` | **17/17 Passed**(2026-10-03 实测;含新增 `test-cppcheck`) |
|
||||
| A5 | 全量静态门禁 | `pre-commit run --all-files`(现只覆盖 `pre-commit` 阶段 47 根;manual 阶段三根另跑 `--hook-stage manual <id>`) | **当时 17/17 Passed**(2026-10-03 实测;含新增 `test-cppcheck`) |
|
||||
| A6 | ST-LINK 探针可见 | `& '.venv\Scripts\python.exe' .dsh\skills\stlink-flash\scripts\stlink_flash.py list`(CLI 经 `DTU_STLINK_CLI` / PATH 解析) | 列出探针 SN `0B250D12A218303030303032` |
|
||||
| A7 | 串口端口正确 | 检查 `.serialtest/config.json` 的 `portName` | 端口单一配置源 = 此文件(当前 COM6,2026-09-21 台架重插后由 COM7 变更);变更后先杀旧 daemon 再 `open` |
|
||||
| A8 | 串口助手 daemon 可用 | `& '.venv\Scripts\python.exe' .dsh/skills/serial-assistant/scripts/serial_tool.py status` | 返回 JSON,`open: true/false` 可读 |
|
||||
|
||||
@@ -511,7 +511,7 @@ flowchart LR
|
||||
|
||||
| 层 | 上 CI? | 载体 | 理由 |
|
||||
|---|---|---|---|
|
||||
| **静态门禁** | ✅ | `pre-commit run --all-files`(全部钩子) | 纯 Python + cppcheck,零硬件 |
|
||||
| **静态门禁** | ✅ | `pre-commit run --all-files`(提交期 47 根快钩子)+「手动阶段门禁」步逐根 `--hook-stage manual` 跑 `cppcheck` / `test-cppcheck` / `test-coverage` | 纯 Python + cppcheck,零硬件 |
|
||||
| **主机侧纯逻辑单测** | ✅ | `test/host/**/run_unit_test.py`(MSVC 离线编译) | 纯标准库 + `cl.exe`,零硬件 |
|
||||
| 板端测试套件 | ⛔ | `test/board/` | 需真机 + 串口 + ST-LINK |
|
||||
| 主机侧回归 `@test` 闭环 | ⛔ | 烧录 + 串口采集 | 同上 |
|
||||
@@ -545,7 +545,7 @@ flowchart LR
|
||||
|
||||
| job | 覆盖层 | 一句话职责 |
|
||||
|---|---|---|
|
||||
| `static-gates` | **静态门禁** | `pre-commit run --all-files`(cppcheck / 编码规范 / 文档一致性 / 主机侧单测钩子)+ **提交信息门禁**(gitlint,见上方 🔴);无 `.map` 时自适应跳过 `mem-numbers` |
|
||||
| `static-gates` | **静态门禁** | `pre-commit run --all-files`(编码规范 / 文档一致性 / 主机侧单测快归因钩子等 47 根)+「手动阶段门禁」步逐根 `--hook-stage manual` 跑 `cppcheck` / `test-cppcheck` / `test-coverage` + **提交信息门禁**(gitlint,见上方 🔴);无 `.map` 时自适应跳过 `mem-numbers` |
|
||||
| `host-unit-tests` | **主机侧纯逻辑单测** | 探测 MSVC 后遍历 `test/host/**/run_unit_test.py` 逐个跑并聚合断言数;**未装 MSVC 时降级为 WARNING 跳过**(不硬失败,但等于该层裸奔) |
|
||||
|
||||
触发:`push`(`paths-ignore: docs/** / **/*.md / .workbuddy-ai/**` → **纯文档改动不会触发 CI**)
|
||||
@@ -566,6 +566,10 @@ CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runn
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --commits 20 # 提交信息校验最近 20 条
|
||||
```
|
||||
|
||||
- **默认段 = 静态段(47 根)+ 提交信息段 + manual 段 + 体积门禁 + 主机侧纯逻辑单测**:manual 段逐根
|
||||
`--hook-stage manual` 跑 `cppcheck` / `test-cppcheck` / `test-coverage`(三根带 `stages: [manual]`,
|
||||
`git commit` 阶段不跑;一次只能给一个 id,多 id 实测报 `unrecognized arguments`);
|
||||
`--coverage` 旗标已并入 manual 段,保留只为兼容旧命令。
|
||||
- **自动选用装了 pre-commit 的解释器**(优先级:`DTU_PRECOMMIT_PYTHON` 显式指定 → 当前解释器(重建 `.venv` 已自带 pre-commit)→ 仓库 `.venv` → PATH 的 `python` → **常见机器级安装位置**;显式值无效 → 硬报错并指明变量名,不静默回退);
|
||||
仍不可用时退出码 2 并列出**全部已尝试路径**。
|
||||
⚠️ 该候选表与 CI 侧 **`scripts/ci_find_python.ps1`** 必须**同序同步**——2026-10-11 的 静态门禁全灭就是
|
||||
@@ -575,7 +579,8 @@ CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runn
|
||||
- 退出码:`0` 通过 / `1` 门禁或单测失败 / `2` 环境错误。
|
||||
|
||||
> ⚠️ **CI 不能替代本地门禁**:`.map` 类体积门禁、`@build`、板端测试套件/主机侧回归台架项都只存在于本地。
|
||||
> 提交前跑一遍 `ci_local.py`(或 `pre-commit run --all-files`)仍是**强制**动作,见 §7.1。
|
||||
> 提交前跑一遍 `ci_local.py` 仍是**强制**动作,见 §7.1(`pre-commit run --all-files` 只覆盖提交期 47 根,
|
||||
> 走不到 `commit-msg` 与 manual 阶段)。
|
||||
|
||||
### 9.4 部署前置(需人工确认)
|
||||
|
||||
|
||||
@@ -198,14 +198,16 @@ python tools/font_gen/extract_cn.py # 任意 python3:
|
||||
|
||||
| 项 | 要求 | 说明 |
|
||||
|---|---|---|
|
||||
| pre-commit | 已安装并 `pre-commit install` | 提交门禁(pre-commit + commit-msg 两类钩子) |
|
||||
| cppcheck | 在 PATH 中 | 本地钩子直接调 `cppcheck --enable=warning,performance,portability,style`(`language: system`) |
|
||||
| pre-commit | 已安装并 `pre-commit install` | 提交门禁(`pre-commit` 阶段 47 根 + commit-msg 阶段 `gitlint`,一次提交实跑 48 根) |
|
||||
| cppcheck | 在 PATH 中 | 钩子直接调 `cppcheck --enable=warning,performance,portability,style`(`language: system`)。⚠ `cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`:`git commit` 不跑,由远端 CI 的「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑(CI runner 缺 cppcheck 则该步失败) |
|
||||
| python | 在 PATH 中 | 本地钩子调 `scripts/check_mojibake.py`(U+FFFD 乱码)、`scripts/check_coding_standard.py`(规范 V1.0,纯标准库) |
|
||||
| **MSVC `cl.exe`** | **装了 VS 即有(§2.3)** | **`watchdog-unit-test` 钩子必需**:改 `App/util/task_watchdog.*` 或 `test/host/watchdog/**` 时触发,用 `cl.exe` 离线编译真实源码跑单测。**未装 VS 时该钩子报环境错误(退出码 2)→ 拦截提交** |
|
||||
| gitlint | 随 pre-commit 自动拉取 | commit message 格式(宽松规则,兼容中文标题) |
|
||||
|
||||
> 检查范围仅项目自有代码;`Lib/`、`Middlewares/`、`Drivers/BSP/UI`、`lftpd`、`dhara`、`test/` 等被排除。
|
||||
> 全库扫描:`pre-commit run --all-files`。详见 `docs/代码规范/pre-commit使用说明.md`、`docs/代码规范/cppcheck使用说明.md`。
|
||||
> 全库扫描:`pre-commit run --all-files`(提交期 47 根);三根 manual 钩子逐根复跑:
|
||||
> `& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual <hook-id>`(一次只能一个 id)。
|
||||
> 详见 `docs/代码规范/pre-commit使用说明.md`、`docs/代码规范/cppcheck使用说明.md`。
|
||||
>
|
||||
> ⚠ **新增主机侧纯逻辑单测钩子(2026-10-01)后,"装了 VS"从"仅模拟器需要"升级为"提交门禁需要"**:
|
||||
> 只要装了 VS(§2.3)就自动具备,无需额外安装;但**完全未装 VS 的机器在改动看门狗相关文件时会被钩子拦截**。
|
||||
@@ -325,7 +327,9 @@ uv tool install pre-commit
|
||||
winget install cppcheck # 2.21.0(安装位置任意;入 PATH 或用 DTU_CPPCHECK 指向)
|
||||
# ★ 重开终端(PATH 才生效),然后:
|
||||
pre-commit install
|
||||
pre-commit run --all-files # 首次全库基线扫描
|
||||
pre-commit run --all-files # 首次全库基线扫描(提交期 47 根)
|
||||
# manual 阶段三根(cppcheck / test-cppcheck / test-coverage,提交期不跑)逐根跑,一次一个 id:
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual cppcheck
|
||||
```
|
||||
|
||||
> 手动下载:uv → https://astral.sh/uv/ (一行安装 `powershell -c "irm https://astral.sh/uv/install.ps1 | iex"`)· pre-commit → https://pre-commit.com/ · cppcheck 2.21.0 → https://github.com/CppQA/cppcheck/releases (2.21.0 tag 的 Windows x64 zip,解压至任意目录并加入 PATH,或用 `DTU_CPPCHECK` 指向 `cppcheck.exe`)
|
||||
@@ -363,7 +367,7 @@ pre-commit run --all-files # 首次全库基线扫描
|
||||
& '.venv\Scripts\python.exe' simulator\tests\check_content_pages.py # PASS(基准 ref/*.bin 已入库,无需先生成)
|
||||
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py selftest # → SELFTEST PASS
|
||||
& '.venv\Scripts\python.exe' test\host\watchdog\run_unit_test.py # 主机侧单测 → 39/39 PASSED(退出码 0)
|
||||
pre-commit run --all-files
|
||||
pre-commit run --all-files # 提交期 47 根(manual 三根另跑:--hook-stage manual <id>,或直接 ci_local.py)
|
||||
```
|
||||
|
||||
**搭建清单汇总**(随步骤逐项打勾):
|
||||
|
||||
+19
-7
@@ -1,6 +1,7 @@
|
||||
# 门禁逐项说明(每种门禁一篇)
|
||||
|
||||
> 这一目录回答一个问题:**提交时那 51 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
|
||||
> 这一目录回答一个问题:**声明的 51 个钩子到底在干什么、拦谁、放谁、什么时候会骗人**——其中 3 根慢钩子带
|
||||
> `stages: [manual]`,一次 `git commit` 只实跑 48 根(口径见下条)。
|
||||
> 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 /
|
||||
> **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**——
|
||||
> 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。
|
||||
@@ -17,11 +18,19 @@
|
||||
⚠ 反过来的坑也真踩过(2026-10-27):在配置**注释里复述那个 grep 字面量**,会让文本 grep 再虚高 1——
|
||||
数数只认 YAML 解析,注释与文档里都别写出那个字面量(`docs/门禁说明/nested-comment.md` §8 记了现场)。
|
||||
实跑侧口径:`scripts/ci_local.py` 的静态门禁汇总行 `[SUMMARY] …: pre-commit hooks -- passed=A failed=B skipped=C`(方括号里那一段是**层级名**,脚本输出与文档层的语义名正分头迁移 → 抓这行**按 `passed=` 字段匹配**,别把层级名前缀写死进字面量,否则会 grep 不到而误判「没有实跑数」),
|
||||
`A+B+C` = **commit 阶段**的钩子数(50),再加 `gitlint` 1 条(它的 commit-msg 阶段声明写在**上游**仓库自带的
|
||||
钩子清单里,本配置文件里查不到,见 `scripts/ci_local.py` 文件头)→ 50 + 1 = 51 = 声明数,三数同源。
|
||||
- 通用机制(两阶段、stash 陷阱、`always` 已废、增量拦截三段式、旧 §2.x 查号表):
|
||||
`A+B+C` = **commit 阶段**的钩子数(47 = 51 − 3 根 `stages: [manual]` − `gitlint`),再加 `gitlint` 1 条(它的
|
||||
commit-msg 阶段声明写在**上游**仓库自带的钩子清单里,本配置文件里查不到,见 `scripts/ci_local.py` 文件头)
|
||||
→ 47 + 1 = **48 = 一次 `git commit` 实跑的钩子数**;再加 3 根 manual 阶段的慢钩子才是声明数 51,三数同源。
|
||||
枚举自查(不执行任何钩子,`SKIP` 列全部 id):`SKIP=<全部 id> -m pre_commit run --all-files --hook-stage commit`
|
||||
输出 47 行、`--hook-stage manual` 输出 50 行(47 常规 + 3 慢的)。
|
||||
- **三根慢钩子在 manual 阶段**:`cppcheck`、`test-cppcheck`、`test-coverage` 带 `stages: [manual]` → `git commit`
|
||||
阶段**不跑**(实测三根合计约 165 s,占一次带代码提交耗时的约三分之二),只在**远端 CI 的「手动阶段门禁」步**
|
||||
(`.gitea/workflows/ci.yml`,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 **manual 段**里跑。
|
||||
本地手工复跑:`& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual <hook-id>`
|
||||
(**一次只能给一个 id**,多 id 实测报 `unrecognized arguments`)。声明数仍是 51,变的是「提交期实跑几根」。
|
||||
- 通用机制(三个阶段 = `pre-commit` / `commit-msg` / `manual`、stash 陷阱、`always` 已废、增量拦截三段式、旧 §2.x 查号表):
|
||||
`docs/代码规范/pre-commit使用说明.md` §2。
|
||||
- 快速自查(不等提交):`& '.venv\Scripts\python.exe' scripts/ci_local.py`(静态门禁 + 提交信息门禁 + 主机侧纯逻辑单测)。
|
||||
- 快速自查(不等提交):`& '.venv\Scripts\python.exe' scripts/ci_local.py`(静态段 47 根 + manual 段三根 + 提交信息门禁 + 体积门禁 + 主机侧纯逻辑单测;manual 段默认跑)。
|
||||
|
||||
## 1. 三种拦截姿态(先分清这个,报错时才知道该干什么)
|
||||
|
||||
@@ -37,6 +46,9 @@
|
||||
|
||||
## 2. 索引(51 个 hook → 28 篇,其中 1 篇是已退役门禁的留档 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/)
|
||||
|
||||
> 带 `stages: [manual]` 的三根(`cppcheck` / `test-cppcheck` / `test-coverage`)**提交期不跑**,只在远端 CI 的
|
||||
> 「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑;下表对应两行的「文档」列已标注。
|
||||
|
||||
| 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 文档 |
|
||||
|---|---|---|---|---|
|
||||
| [`bsp-err.md`](bsp-err.md) | `bsp-err` | BSP 的对外 API 面:错误通道类型、超时宏命名、必须写返回值、返回值不得裸丢弃 | `Drivers/BSP/**` 或门禁自身 | 已写 |
|
||||
@@ -44,7 +56,7 @@
|
||||
| [`id-check.md`](id-check.md) | ~~`id-check`~ | **已退役(2026-10-28)**:全库语料的八项校验,留档备查;现行编号门禁见下一行 | 在时有:几乎所有文本文件 | 已写(退役留档) |
|
||||
| [`id-check-new.md`](id-check-new.md) | `id-check-new` | **只查新增行**有没有新造编号(零语料前缀白名单,已停发族只告警) | 任意 md/c/h/py/yaml/txt | 已写 |
|
||||
| [`doc-links.md`](doc-links.md) | `doc-links` | 文档里的路径引用是否存在;`--sections-strict` 另查章节号是否还存在 | 任意 `.md` | 已写 |
|
||||
| [`cppcheck.md`](cppcheck.md) | `cppcheck`、`test-cppcheck` | 静态分析(真机侧与 `test/` 侧两套范围与抑制项) | C/H 源文件 | 已写 |
|
||||
| [`cppcheck.md`](cppcheck.md) | `cppcheck`、`test-cppcheck` | 静态分析(真机侧与 `test/` 侧两套范围与抑制项) | C/H 源文件 | 已写(`stages: [manual]`) |
|
||||
| [`coding-standard.md`](coding-standard.md) | `coding-standard` | 编码规范 V1.0 的可机械检查子集(命名、头文件保护宏、模块头注释块…) | C/H 源文件 | 已写 |
|
||||
| [`no-mojibake.md`](no-mojibake.md) | `no-mojibake` | 禁 `U+FFFD`——中文被按错误编码重解码后不可逆损坏的痕迹 | 全部文本 | 已写 |
|
||||
| [`mem-numbers.md`](mem-numbers.md) | `mem-numbers` | 文档里引用的 RAM/Flash 数字与最新 `.map` 报告是否一致(体积档) | `docs/内存管理/**`、相关脚本 | 已写 |
|
||||
@@ -62,7 +74,7 @@
|
||||
| [`setview-check.md`](setview-check.md) | `setview-check` | 定值查看行表与 `prot_set` 枚举序 / `g_prot_set_meta` 上下限是否同一份口径 | `tools/setview_check/**`、`ui_w_setview.*` | 已写 |
|
||||
| (专篇待补,档位与限度正文在 [`../UI设计/UI_自动化测试流程.md`](../UI设计/UI_自动化测试流程.md) §7.3) | `uinav-selftest` | 板端 UI 导航器认屏快检(黄金参考帧分类;完整闭环档=合入前/上板前手工跑,快检不互替) | `scripts/board_ui_nav.py`、`simulator/tests/ref/*.bin` | 指针 |
|
||||
| [`host-unit-tests.md`](host-unit-tests.md) | 22 个 `*-unit-test` | 主机侧单测(同一机制 22 个实例):逻辑层在无硬件的 PC 上编译并跑断言 | 各自被测模块与其测试目录 | 已写 |
|
||||
| 详解在 [`../测试说明/主机单测覆盖率口径.md`](../测试说明/主机单测覆盖率口径.md)(口径 / 分母 / 基线 / 盲区都在那篇,本目录不重复) | `test-coverage` | **覆盖率总闸**:跑全部 28 套主机单测 + 已覆盖行只增不减的回退台账 | `App/`、`Drivers/BSP/`、`test/host/`、脚本与基线 | 已写(在 `docs/测试说明/`) |
|
||||
| 详解在 [`../测试说明/主机单测覆盖率口径.md`](../测试说明/主机单测覆盖率口径.md)(口径 / 分母 / 基线 / 盲区都在那篇,本目录不重复) | `test-coverage` | **覆盖率总闸**:跑全部 40 套主机单测 + 已覆盖行只增不减的回退台账 | `App/`、`Drivers/BSP/`、`test/host/`、脚本与基线 | 已写(在 `docs/测试说明/`;`stages: [manual]`) |
|
||||
| [`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` | 别把大文件(日志/镜像/二进制)误提进仓库 | 全部 | 已写 |
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
`test-cppcheck`(只扫 `test/`,**只开 warning**,排除 `test/host/*/stub`)。
|
||||
- `entry: cppcheck`、`language: system`、`types: [c]`、`pass_filenames: true`(**逐文件**分析)、
|
||||
`--error-exitcode=1`、`--std=c99`、`--quiet`、`--inline-suppr`。
|
||||
- 两根都带 `stages: [manual]`:**`git commit` 阶段不跑**,只在远端 CI 的「手动阶段门禁」步(逐根
|
||||
`--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段跑。手工复跑:
|
||||
`& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual cppcheck`
|
||||
(`test-cppcheck` 换 id,**一次只能给一个 id**,多 id 实测报 `unrecognized arguments`)。
|
||||
|
||||
## 1. 它在查什么
|
||||
|
||||
@@ -34,9 +38,11 @@
|
||||
- 排除:`Drivers/CMSIS/`、HAL、`Drivers/BSP/NET/lftpd/`、`Drivers/BSP/dhara/`、`Lib/`、
|
||||
`Middlewares/`、`MDK-ARM/`、`test/`(`test/` 由第二个钩子负责)。
|
||||
- UI 生成文件(`ui_font_cn_12`、`ui_menu_tab`)**纳入受检**——生成器按工程约定产出,不是第三方。
|
||||
- 只在**本机装了 cppcheck** 时有效;CI 不跑构建类门禁(见 `docs/CI/README.md`)。
|
||||
- 本机须装了 cppcheck 才有效(远端 CI 的 runner 同样按 `DTU_CPPCHECK` → PATH 探测,缺则 job 直接失败);
|
||||
CI 不跑构建类门禁 `@build`(见 `docs/CI/README.md`)。
|
||||
|
||||
- 速度口径(原手册实测):**单文件约 1~2 秒、全库约 5 秒**,所以它能逐文件传参而不拖慢提交。
|
||||
- 速度口径:**单文件约 1~2 秒、全库约 20 s**(2026-10-28 实测 `cppcheck` 20.5 s、`test-cppcheck` 38.1 s)——
|
||||
提交期不跑它们,全量由 CI 与 `ci_local.py` 的 manual 段承担。
|
||||
- style 类别 95 项的逐项详解、配置参数与 YAML 引号陷阱 → `docs/代码规范/cppcheck使用说明.md`;
|
||||
本门只管钩子这一层。
|
||||
- 典型报错形态:`[path/to/file.c:123]: (style) variableScope The scope of the variable ... can be reduced`。
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
## 3. 触发面 / 看不见什么
|
||||
|
||||
- `git commit`、`--amend`、`-F`、合并生成的提交都会跑;**`--no-verify` 连它和其它 50 个钩子一起跳过**。
|
||||
- `git commit`、`--amend`、`-F`、合并生成的提交都会跑;**`--no-verify` 连它和其它 47 个 `pre-commit` 阶段钩子一起跳过**(`stages: [manual]` 的三根慢钩子(`cppcheck` / `test-cppcheck` / `test-coverage`)本来就不随提交跑,不受影响)。
|
||||
- 不校验:提交说明与 diff 是否相符、是否真的构建/测试过、是否夹带无关改动、是否该拆成多笔。
|
||||
|
||||
## 4. 报错怎么读
|
||||
|
||||
@@ -96,16 +96,18 @@
|
||||
`ftp_cfg`、`iec104_serv`、`lftpd`、`lftpd_inet`、`lftpd_io`、`net_cfg`、`net_events`、
|
||||
`net_listener`、`net_pending`、`net_select`、`net_socket`、`prot_set_store`、`soe_store`、
|
||||
`sys_log_mcu`、`task_prof`、`ui_sink`;
|
||||
- 这 18 套每次提交都被 `test-coverage` **总闸**真跑:发现规则是
|
||||
`HOST_TEST_DIR.rglob("run_unit_test.py")`(与 `scripts/ci_local.py` 的 主机侧纯逻辑单测同一口径),
|
||||
- 这 18 套每次 CI 与 `scripts/ci_local.py` 都由 `test-coverage` **总闸**真跑(该钩子带 `stages: [manual]`:
|
||||
`git commit` 阶段不跑它,这一根与 `cppcheck` / `test-cppcheck` 同属 CI 与 `ci_local.py` 的 manual 段);
|
||||
发现规则是
|
||||
`HOST_TEST_DIR.rglob("run_unit_test.py")`(与 `scripts/ci_local.py` 的主机侧纯逻辑单测同一口径),
|
||||
退出码语义写死「0 = 全部单测通过且覆盖率相对基线没回退 / 1 = 有单测失败或覆盖率回退」⇒
|
||||
**失败照样拦提交**。差的只是归因粒度(要在总闸输出里认出是哪套)与耗时(40 套全量比单根慢得多);
|
||||
**失败照样拦(CI 与 `ci_local.py` 的 manual 段)**。差的只是归因粒度(要在总闸输出里认出是哪套)与耗时(40 套全量比单根慢得多);
|
||||
- 要不要给某套补第 23 根,判据现成:pre-commit 4.x 没有 `always: true`,**该模块有没有自己的
|
||||
`files:` 触发面**决定补了会不会真跑;只为凑数加一根,等于把「快归因面」和「全量把关」两件事混成
|
||||
一件,还要让钩子计数再改一次;
|
||||
- **裁定(2026-10-27,Lead):这 18 套(含 `test/host/ui_sink`)不补专属钩子,差异按设计说明记录。**
|
||||
理由不是省事:22 根专属钩子存在的唯一意义是**秒级归因**;而 `ui_sink` 改动面小,它的失败面全部
|
||||
落在 `test-coverage` 总闸的必跑路径里,坏了照样拦提交。再加一根只是多一个必须「声明数 / 文档数 /
|
||||
落在 `test-coverage` 总闸的必跑路径里,坏了照样在 CI / `ci_local.py` 拦下。再加一根只是多一个必须「声明数 / 文档数 /
|
||||
实跑数」三处同源同步的计数点——本仓 2026-10-26 到 27 一天之内就为 40 / 50 / 51 这三个数不同源
|
||||
返工过两轮。**要读「这套跑没跑」,读总闸输出,不要来数钩子。** 两个方向的空头账一起堵住:
|
||||
① 别把没接线的套件算进钩子账(原写「23 个 / 含 ui-sink」正是这么错的;2026-10-27 复核的准数是
|
||||
|
||||
+8
-2
@@ -41,10 +41,10 @@
|
||||
|
||||
| 脚本 / 文件 | 作用 |
|
||||
|---|---|
|
||||
| `ci_local.py` | **本地跑与 CI 等价的门禁**(静态门禁 pre-commit + 提交信息门禁 + **体积门禁** + 主机侧纯逻辑单测):自动选用装了 pre-commit 的解释器;无 `.map` 时自适应跳过 `mem-numbers` / `mem-budget`(⚠ 那是 SKIP 不是通过,缺口由体积门禁兜住);退出码 0/1/2 与 CI 对齐。CI 定义见 `../.gitea/workflows/ci.yml`,覆盖边界见 `docs/测试说明/测试开发标准流程.md` §9 |
|
||||
| `ci_local.py` | **本地跑与 CI 等价的门禁**(静态门禁 pre-commit 47 根 + **manual 段**(`cppcheck` / `test-cppcheck` / `test-coverage` 三根逐根 `--hook-stage manual`,默认跑)+ 提交信息门禁 + **体积门禁** + 主机侧纯逻辑单测):自动选用装了 pre-commit 的解释器;无 `.map` 时自适应跳过 `mem-numbers` / `mem-budget`(⚠ 那是 SKIP 不是通过,缺口由体积门禁兜住);退出码 0/1/2 与 CI 对齐。CI 定义见 `../.gitea/workflows/ci.yml`,覆盖边界见 `docs/测试说明/测试开发标准流程.md` §9 |
|
||||
| `check_size_gate.py` | **体积门禁的强制路径**:`.map` 只在本地有,钩子无它时自适应 SKIP,于是"没测=通过"。本脚本用**已提交的同码凭证** `scripts/size_attestation.json` 把这条判据变成无需 Keil 也能跑:`--stamp` 只在"`.map` 存在、比全部受管源码新、且未超 92%"时才盖章(已挂在 `ram_baseline.py --update` 里,无新增人工步骤);`--local` 供 ci_local 的体积门禁步;`--ci` 供 runner(同码性 + 按凭证数字复核红线,主干 push 拦红、其余事件 `--warn-only`);`--show` 排障。**口径与边界的唯一正文在本脚本模块 docstring** |
|
||||
| `size_attestation.json` | (被 `check_size_gate.py` 读写)体积同码凭证:受管源码指纹(`git ls-files` 范围内逐文件 sha256 聚合)+ 测量数字与占比 + `.map` 出处。改固件源码后不重测 → 指纹对不上 → 体积门禁与 CI 同时拦红 |
|
||||
| `check_test_coverage.py` | 主机单测**覆盖率**(2026-10-23):mscoverage 动态插桩跑 `test/host/**` 全部 16 套,统计自研生产源码行/函数覆盖(并集)+ 与 `test_coverage_baseline.json` 比回退;`--module/--update-baseline/--md/--json/--list`;**不在 pre-commit 里**,入口是 `ci_local.py --coverage`(口径见 `docs/测试说明/主机单测覆盖率口径.md`) |
|
||||
| `check_test_coverage.py` | 主机单测**覆盖率**(2026-10-23):mscoverage 动态插桩跑 `test/host/**` 全部 40 套,统计自研生产源码行/函数覆盖(并集)+ 与 `test_coverage_baseline.json` 比回退;`--module/--update-baseline/--md/--json/--list`;挂在 `test-coverage` 钩子上(`stages: [manual]`,**`git commit` 不跑**),入口 = 远端 CI「手动阶段门禁」步 / `scripts/ci_local.py` 的 manual 段(`--coverage` 旗标保留只为兼容旧命令;口径见 `docs/测试说明/主机单测覆盖率口径.md`) |
|
||||
| `ci_gitlint_range.ps1` | CI 侧**提交信息门禁**:逐条校验本次 push/PR 引入的提交(`--all-files` 跑不到 `commit-msg` stage,故必须单独守) |
|
||||
| `ci_find_python.ps1` | CI 侧 Python 探测(候选顺序须与 `ci_local.py` 同序同步,否则 CI 与本地判定会分叉) |
|
||||
| `ci_runner_bootstrap.ps1` | **本机 runner 运维脚本**(体检 `-Status` / 干跑 `-DryRun` / 部署 `-SetEnv -StartNow`):装"等 Gitea 就绪 + 崩溃自拉起"的看门狗 + 开机计划任务 + 机器级 `DTU_*` |
|
||||
@@ -57,8 +57,14 @@
|
||||
```bash
|
||||
# 全库跑门禁(用仓库 .venv 的 python,其 pre-commit 随 requirements.txt 安装;
|
||||
# 其他装有 pre-commit 的解释器可用 DTU_PRECOMMIT_PYTHON 指定)
|
||||
# 只覆盖 `pre-commit` 阶段的 47 根(`git commit` 实跑 = 47 + gitlint = 48 根)
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run --all-files
|
||||
|
||||
# manual 阶段三根(带 stages: [manual],提交期不跑):一次只能给一个 id
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual cppcheck
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual test-cppcheck
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual test-coverage
|
||||
|
||||
# 本地跑与 CI 等价的门禁(静态门禁 + 提交信息门禁 + 体积门禁 + 主机侧纯逻辑单测)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --static-only # 只跑静态门禁
|
||||
|
||||
+57
-32
@@ -9,13 +9,19 @@
|
||||
|
||||
覆盖范围(与 CI 完全一致):
|
||||
静态门禁(static) = `pre-commit run --all-files` + 提交信息门禁(gitlint)
|
||||
手动阶段(manual,2026-10-28)= 三根带 `stages: [manual]` 的钩子:
|
||||
cppcheck(全量)/ test-cppcheck / test-coverage(覆盖率总闸)。
|
||||
它们**不在 git commit 时跑**(实测合计 ~165 s = 一次带代码提交耗时的三分之二),
|
||||
提交期只跑快的那些;CI 的 static-gates job 逐根跑同一批钩子(同口径)。
|
||||
本地复跑:本脚本的 manual 段,或 `python -m pre_commit run --all-files
|
||||
--hook-stage manual <hook-id>`(一次只能给一个 id)。
|
||||
体积门禁(size,强制路径,2026-10-25)= `scripts/check_size_gate.py --local`
|
||||
**fail-closed**:没有体积同码凭证、或代码与测量不同码即红——不再"没跑=通过"。
|
||||
CI 侧的「体积门禁」步跑的是同一条实现(那条无需 Keil,见 .gitea/workflows/ci.yml)。
|
||||
主机侧纯逻辑单测(host-unit) = `test/host/**/run_unit_test.py`
|
||||
主机单测覆盖率(coverage,**opt-in:`--coverage`**,2026-10-23)= `scripts/check_test_coverage.py`
|
||||
默认**不跑**、CI 也**不跑**:它依赖 VS 可选的「代码覆盖率工具」组件,且与主机侧单测那 16 个
|
||||
钩子跑的是同一批测试(覆盖率只是换一种看法)。口径与盲区 → docs/测试说明/主机单测覆盖率口径.md
|
||||
覆盖率口径与盲区 → docs/测试说明/主机单测覆盖率口径.md
|
||||
(旧旗标 `--coverage` 的独立一步已并入 manual 段:跑的是**同一根** pre-commit 钩子
|
||||
test-coverage,与 CI 逐字同口径,不再重复跑一遍插桩)
|
||||
|
||||
⚠️ gitlint 必须**单独跑**(2026-10-11 审计):`stages: [commit-msg]` 写在 gitlint **上游**
|
||||
的 `.pre-commit-hooks.yaml`(项目 `.pre-commit-config.yaml` 里查不到这一行),而
|
||||
@@ -519,6 +525,42 @@ def run_static_gates():
|
||||
return rc
|
||||
|
||||
|
||||
MANUAL_STAGE_HOOKS = ("cppcheck", "test-cppcheck", "test-coverage")
|
||||
|
||||
|
||||
def run_manual_stage():
|
||||
"""手动阶段门禁:三根带 stages: [manual] 的钩子(覆盖率总闸 + 全量 cppcheck)。
|
||||
|
||||
为什么单独一段而不并进 static:它们在 .pre-commit-config.yaml 里固定成 manual 阶段
|
||||
(2026-10-28 用户裁定),`pre-commit run --all-files` 不再覆盖它们;提交期跑它们
|
||||
实测要 ~165 s。本脚本是"与 CI 等价"的完整自查,所以**默认照跑**。
|
||||
|
||||
逐根调用:`pre_commit run <a> <b>` 不支持多 id(实测 argparse 报
|
||||
"unrecognized arguments"),CI 侧同样是逐根跑(.gitea/workflows/ci.yml)。
|
||||
"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" manual 手动阶段门禁(覆盖率总闸 + 全量 cppcheck,提交期不跑)")
|
||||
print("=" * 64)
|
||||
env = dict(os.environ)
|
||||
env.pop("SKIP", None) # 三根都不涉及 Keil 依赖,SKIP 与它们无关
|
||||
rc_all = 0
|
||||
for hook in MANUAL_STAGE_HOOKS:
|
||||
rc, text = _run_capture([PY, "-m", "pre_commit", "run", "--all-files",
|
||||
"--hook-stage", "manual", hook], env=env)
|
||||
tail = [l for l in text.splitlines() if l.strip()]
|
||||
verdict = tail[-1].strip() if tail else "(no output)"
|
||||
print("[SUMMARY] manual: {} -- rc={} {}".format(hook, rc, verdict[-90:]))
|
||||
if hook == "test-coverage" and rc == 2:
|
||||
print(" (rc 2 = 环境错误:多半没装 VS 的「代码覆盖率工具」组件,或需设")
|
||||
print(" DTU_MS_COVERAGE。口径与排障 → docs/测试说明/主机单测覆盖率口径.md)")
|
||||
elif hook == "test-coverage" and rc == 1:
|
||||
print(" (rc 1 = 有单测失败,或已覆盖行/函数相对基线回退;确认无误后")
|
||||
print(" 跑 scripts/check_test_coverage.py --update-baseline 重登基线)")
|
||||
rc_all = rc_all or rc
|
||||
return rc_all
|
||||
|
||||
|
||||
def run_gitlint(count):
|
||||
"""提交信息门禁:把最近 N 条非 merge 提交的 message 逐条喂给 gitlint 钩子。
|
||||
|
||||
@@ -670,30 +712,6 @@ def run_host_unit_tests(args):
|
||||
return rc_all
|
||||
|
||||
|
||||
def run_coverage():
|
||||
"""覆盖率附加步骤(opt-in,CI 不跑):主机单测覆盖率 + 基线增量门禁。
|
||||
|
||||
单独一步而不并进主机侧纯逻辑单测:那一层是**必须绿**的门禁,覆盖率是**趋势视图**,两者失败含义不同
|
||||
(前者=测试挂了,后者=覆盖回退了 / 缺工具链)。退出码原样透传,由 main 汇总。
|
||||
"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" coverage 主机单测覆盖率(mscoverage 动态插桩 + 基线增量门禁)")
|
||||
print("=" * 64)
|
||||
script = REPO_ROOT / "scripts" / "check_test_coverage.py"
|
||||
if not script.is_file():
|
||||
print("[WARN] 缺 scripts/check_test_coverage.py —— 跳过该步")
|
||||
return 0
|
||||
rc = _run([PY, str(script)])
|
||||
if rc == 2:
|
||||
print(" (退出码 2 = 环境错误:多半没装 VS 的「代码覆盖率工具」组件,或需设")
|
||||
print(" DTU_MS_COVERAGE。口径与排障 → docs/测试说明/主机单测覆盖率口径.md)")
|
||||
elif rc == 1:
|
||||
print(" (退出码 1 = 有单测失败,或已覆盖行/函数相对基线回退;确认无误后")
|
||||
print(" 跑 scripts/check_test_coverage.py --update-baseline 重登基线)")
|
||||
return rc
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
@@ -709,7 +727,8 @@ def main(argv=None):
|
||||
g.add_argument("--size-only", action="store_true",
|
||||
help="只跑体积门禁(强制路径;约 0.3 s,无需 Keil)")
|
||||
ap.add_argument("--coverage", action="store_true",
|
||||
help="额外跑主机单测覆盖率(约 25 s;需 VS 代码覆盖率组件;默认不跑,CI 也不跑)")
|
||||
help="[已并入 manual 段] 主机单测覆盖率现由 test-coverage 钩子在 manual 段跑,"
|
||||
"本旗标保留只为兼容旧命令(不再额外跑一遍)")
|
||||
ap.add_argument("--preflight-only", action="store_true",
|
||||
help="只跑钩子解释器预检(只读;缺包时当场指名钩子)")
|
||||
ap.add_argument("--keep-going", action="store_true",
|
||||
@@ -762,6 +781,10 @@ def main(argv=None):
|
||||
if not args.host_unit_only:
|
||||
rc = run_static_gates()
|
||||
ran.append("static")
|
||||
# 手动阶段三根(覆盖率总闸 + 全量 cppcheck):git commit 时不再跑,本脚本照跑——
|
||||
# 本脚本存在的意义就是"与 CI 等价"。即使静态门禁已失败也照跑,一次看全所有违规项。
|
||||
rc_manual = run_manual_stage()
|
||||
ran.append("manual")
|
||||
# 提交信息门禁独立于 --all-files(stage 不同),故单独跑一段;
|
||||
# 即使静态门禁已失败也照跑,一次看全所有违规项。
|
||||
rc_msg = run_gitlint(args.commits)
|
||||
@@ -770,20 +793,22 @@ def main(argv=None):
|
||||
# 与静态门禁/提交信息门禁的结论正交,且只要 0.3 s——少跑一次就是那个盲区。
|
||||
rc_size = run_size_gate()
|
||||
ran.append("size")
|
||||
if rc != 0 or rc_msg != 0 or rc_size != 0:
|
||||
if rc != 0 or rc_manual != 0 or rc_msg != 0 or rc_size != 0:
|
||||
if not args.keep_going:
|
||||
print("")
|
||||
print("[FAIL] 静态门禁 / 提交信息门禁 / 体积门禁有未通过项 —— 中止"
|
||||
"(--keep-going 可继续跑主机侧纯逻辑单测)")
|
||||
return 1
|
||||
rc = rc or rc_msg or rc_size
|
||||
rc = rc or rc_manual or rc_msg or rc_size
|
||||
if not args.static_only:
|
||||
rc_hu = run_host_unit_tests(args)
|
||||
ran.append("host-unit")
|
||||
rc = rc or rc_hu
|
||||
if args.coverage:
|
||||
rc = rc or run_coverage()
|
||||
ran.append("coverage")
|
||||
print("")
|
||||
print("[INFO] --coverage 已并入 manual 段(test-coverage 钩子,与 CI 同口径),"
|
||||
"不再重复跑插桩;该旗标保留只为兼容旧命令。")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print("[ERROR] 环境错误:{}".format(e))
|
||||
return 2
|
||||
|
||||
Reference in New Issue
Block a user