diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c8396bca..b5e15eac 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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 ` 不支持多 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7335372..c41d7b78 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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$) diff --git a/AGENTS.md b/AGENTS.md index 6470bdb6..e9d7f4b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,7 +92,7 @@ ## 提交门禁(pre-commit) -- 任一失败即拦截,共 **51 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(= 52),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,路线见 `docs/编号规范.md` §8.5);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check-new`(**编号门禁**:只查 `git diff` 新增行里有没有未登记形态的「字母+数字」标识,零语料、零维护;白名单是前缀级两张表,见 `docs/门禁说明/id-check-new.md`。旧的八项全库校验 `id-check` 已于 2026-10-28 退役,脚本与两份语料移入 `scripts/archive/id_gate/`,只在手工复查存量时才跑;退役路线见 `docs/编号规范.md` §8.5)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 51)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 40 套带 `run_unit_test.py`,其余 **18 套**(含 `test/host/ui_sink`)没有专属钩子,但每次提交都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,18 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 40 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,登记簿条目「rs485_task 白名单缺口」)+ `generated-fresh`(**生成器幂等性**:把 `tools/menu_gen/gen_menu.py`、中文字库链、`tools/arch_diagram/gen_*.py` 拷进**系统临时目录**重跑,与工作区**逐字节**必须一致——钩子只读、不写仓库也不 stash 别人在飞的改动;实证 `0f7b5a87` 改了模板文字没重跑生成物,接线前实测另有 5 张 `docs/images/` 配图已漂 → 钉成只减不增基线,**业务生成物零豁免面** → `docs/门禁说明/generated-fresh.md`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 60 张黄金参考)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3))+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**)) +- 任一失败即拦截,共 **51 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(= 52),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,路线见 `docs/编号规范.md` §8.5);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check-new`(**编号门禁**:只查 `git diff` 新增行里有没有未登记形态的「字母+数字」标识,零语料、零维护;白名单是前缀级两张表,见 `docs/门禁说明/id-check-new.md`。旧的八项全库校验 `id-check` 已于 2026-10-28 退役,脚本与两份语料移入 `scripts/archive/id_gate/`,只在手工复查存量时才跑;退役路线见 `docs/编号规范.md` §8.5)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 51)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 40 套带 `run_unit_test.py`,其余 **18 套**(含 `test/host/ui_sink`)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,18 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 40 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,登记簿条目「rs485_task 白名单缺口」)+ `generated-fresh`(**生成器幂等性**:把 `tools/menu_gen/gen_menu.py`、中文字库链、`tools/arch_diagram/gen_*.py` 拷进**系统临时目录**重跑,与工作区**逐字节**必须一致——钩子只读、不写仓库也不 stash 别人在飞的改动;实证 `0f7b5a87` 改了模板文字没重跑生成物,接线前实测另有 5 张 `docs/images/` 配图已漂 → 钉成只减不增基线,**业务生成物零豁免面** → `docs/门禁说明/generated-fresh.md`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 ;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 60 张黄金参考)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3))+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**))。**提交期实跑 48 根**(2026-10-28 用户裁定):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual `(一次只能一个 id) - **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5) - ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关 - **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/` diff --git a/docs/CI/远程CI完整指南.md b/docs/CI/远程CI完整指南.md index 9bdf71d2..03a8d52c 100644 --- a/docs/CI/远程CI完整指南.md +++ b/docs/CI/远程CI完整指南.md @@ -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: 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` 报 > **` 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**;增删钩子或单测后按上面的规则重算,别抄历史数字。 diff --git a/docs/代码规范/cppcheck使用说明.md b/docs/代码规范/cppcheck使用说明.md index 9772b27d..636fcf87 100644 --- a/docs/代码规范/cppcheck使用说明.md +++ b/docs/代码规范/cppcheck使用说明.md @@ -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 `——**一次只能给一个 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 # 只报问题,不显示扫描进度 diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index 85d4939f..8766ad8b 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -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 ` 取补丁 → `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 --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 --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 ` 并自查最长行;或先老实提交一次让 `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 `,一次一个 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 检测脚本 | diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index 30a27b9d..a4db3c84 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -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$)' diff --git a/docs/测试说明/实机测试实施指南.md b/docs/测试说明/实机测试实施指南.md index 410c2c34..68357540 100644 --- a/docs/测试说明/实机测试实施指南.md +++ b/docs/测试说明/实机测试实施指南.md @@ -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 `) | **当时 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` 可读 | diff --git a/docs/测试说明/测试开发标准流程.md b/docs/测试说明/测试开发标准流程.md index 6a1efe1f..51ad98f1 100644 --- a/docs/测试说明/测试开发标准流程.md +++ b/docs/测试说明/测试开发标准流程.md @@ -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 部署前置(需人工确认) diff --git a/docs/环境与部署/项目部署环境要求.md b/docs/环境与部署/项目部署环境要求.md index 8808a947..dff605d1 100644 --- a/docs/环境与部署/项目部署环境要求.md +++ b/docs/环境与部署/项目部署环境要求.md @@ -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 `(一次只能一个 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 ,或直接 ci_local.py) ``` **搭建清单汇总**(随步骤逐项打勾): diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index 564454f8..befd2c52 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -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 ` + (**一次只能给一个 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` | 别把大文件(日志/镜像/二进制)误提进仓库 | 全部 | 已写 | diff --git a/docs/门禁说明/cppcheck.md b/docs/门禁说明/cppcheck.md index ffab7b9b..842f191a 100644 --- a/docs/门禁说明/cppcheck.md +++ b/docs/门禁说明/cppcheck.md @@ -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`。 diff --git a/docs/门禁说明/gitlint.md b/docs/门禁说明/gitlint.md index 19fb0137..13c7996e 100644 --- a/docs/门禁说明/gitlint.md +++ b/docs/门禁说明/gitlint.md @@ -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. 报错怎么读 diff --git a/docs/门禁说明/host-unit-tests.md b/docs/门禁说明/host-unit-tests.md index 34db6d75..8ef1553a 100644 --- a/docs/门禁说明/host-unit-tests.md +++ b/docs/门禁说明/host-unit-tests.md @@ -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 复核的准数是 diff --git a/scripts/README.md b/scripts/README.md index f6acfa5a..833242e6 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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 # 只跑静态门禁 diff --git a/scripts/ci_local.py b/scripts/ci_local.py index dc99473e..ce605e7e 100644 --- a/scripts/ci_local.py +++ b/scripts/ci_local.py @@ -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 `(一次只能给一个 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 ` 不支持多 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