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:
+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