refactor(编号): 测试验证层级去号——CI job / 门禁输出 / 命令行旗标换语义名 + ASCII 段名
用户裁定「我不想看到编号」。测试验证层级这一族的整族清退分三道并行:本笔是**代码与 CI 道** (CI 步骤与门禁输出面),代码注释面另走、文档面由文档道走。 口径(旧 → 语义名 → ASCII 段名,段名只进日志/旗标/job id/文件名): L0 静态门禁 static | L0a 钩子解释器预检 preflight | L0b 提交信息门禁 commitmsg L0c 体积门禁 size | L0.5 主机侧纯逻辑单测 host-unit | L0.5c 覆盖率 coverage - scripts/ci_local.py 43 站点:段标题与 [SUMMARY] 标签改 ASCII 段名(static: / host-unit:),函数 run_l0/run_l05 → run_static_gates/run_host_unit_tests, 收尾「已跑:」列表用段名。旗标改 --static-only / --host-unit-only,**旧旗标 --l0-only / --l05-only 保留为 argparse 别名**(肌肉记忆与既有脚本片段不断档, 实测 --help 两名列出)。 - .gitea/workflows/ci.yml 42 站点:job id l0-gates → static-gates、 l05-host-unit-tests → host-unit-tests;步骤名与 step-summary 标题改语义名; 日志名 l0-pre-commit.log → static-pre-commit.log、l05-<模块>.log → host-unit-<模块>.log;::error::/::notice:: 文案同步。yaml.safe_load 核过 两个 job id 与步骤名。 host-unit-tests;步骤名与 step-summary 标题改语义名;日志名 l0-pre-commit.log → static-pre-commit.log、l05-<模块>.log → host-unit-<模块>.log;::error::/::notice:: 文案同步。yaml.safe_load 核过两个 job id 与步骤名。 - 门禁脚本 18 站点:check_registration(登记簿理由串 7)、check_test_coverage 5、 README 8、check_generated_fresh 2、ram_baseline 2、check_size_gate 1、 ci_find_python.ps1 2、ci_runner_bootstrap.ps1 2、.pre-commit-config.yaml 2、 requirements.txt 1(保持纯 ASCII)。 - skills 7 站点:task-worktree(含 [SUMMARY] 样例两行)、mem-budget、ui-verify、 task_worktree.py 的实跑标签。 - AGENTS.md 3 站点:CI 段「只跑静态门禁 + 主机侧纯逻辑单测」、自查命令段、 「2026-10-11 静态门禁全灭」事件名。 - 归档:编号治理历史.md 新增 §10 换算表 + **撞族警示**(同形 L0/L1/L2/L3 还属 UI 设计 令牌层 / UI 菜单层级 / 采集链 / 链路时延段 / CubeMX 三层 / 硬件位号与 FTP 应答字面量 等六个族,其中 6 处在字符串字面量里,改了就是改行为——故全程只按行内置换、先判族)。 - docs/产品需求 §7.4 测试分层表:去号 + 顺带修掉上一批**错族替换疤**(该表 L1/L2 行曾被 采集链层名「语义取数与有效性 / 全周傅氏相量」「保护判据」顶替,现按内容改写为板端测试 套件 / 主机侧回归两行)。同文档 §1.5 G5 判据行同步。 门禁:ci_local --help 两面旗标并列;py_compile 7 个 .py 全过;yaml 解析通过; check_ids_new --worktree rc=0;git diff --check 干净;17 个改动文件 FFFD=0、无 CR、 BOM 与原文件一致(含 requirements.txt 纯 ASCII)。本族无机器识别面(单字母+数字是 新增行门禁的自述盲区),口径来自自建逐行扫描器,复扫本族形态归零。 - 交接说明:文档道 teammate 在改 CI 指南时中断,剩余 9 处双写疤与 29 处 CI 标识 (job id / 日志名 / 汇总行字面量)由 Lead 接手改完,逐处仍走断言式行内置换。
This commit is contained in:
+7
-7
@@ -41,9 +41,9 @@
|
||||
|
||||
| 脚本 / 文件 | 作用 |
|
||||
|---|---|
|
||||
| `ci_local.py` | **本地跑与 CI 等价的门禁**(L0 pre-commit + L0b 提交信息 + **L0c 体积门禁** + L0.5 主机侧单测):自动选用装了 pre-commit 的解释器;无 `.map` 时自适应跳过 `mem-numbers` / `mem-budget`(⚠ 那是 SKIP 不是通过,缺口由 L0c 兜住);退出码 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 的 L0c 步;`--ci` 供 runner(同码性 + 按凭证数字复核红线,主干 push 拦红、其余事件 `--warn-only`);`--show` 排障。**口径与边界的唯一正文在本脚本模块 docstring** |
|
||||
| `size_attestation.json` | (被 `check_size_gate.py` 读写)体积同码凭证:受管源码指纹(`git ls-files` 范围内逐文件 sha256 聚合)+ 测量数字与占比 + `.map` 出处。改固件源码后不重测 → 指纹对不上 → L0c 与 CI 同时拦红 |
|
||||
| `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 |
|
||||
| `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`) |
|
||||
| `ci_gitlint_range.ps1` | CI 侧**提交信息门禁**:逐条校验本次 push/PR 引入的提交(`--all-files` 跑不到 `commit-msg` stage,故必须单独守) |
|
||||
| `ci_find_python.ps1` | CI 侧 Python 探测(候选顺序须与 `ci_local.py` 同序同步,否则 CI 与本地判定会分叉) |
|
||||
@@ -59,11 +59,11 @@
|
||||
# 其他装有 pre-commit 的解释器可用 DTU_PRECOMMIT_PYTHON 指定)
|
||||
& '.venv\Scripts\python.exe' -m pre_commit run --all-files
|
||||
|
||||
# 本地跑与 CI 等价的门禁(L0 + L0b + L0c + L0.5)
|
||||
# 本地跑与 CI 等价的门禁(静态门禁 + 提交信息门禁 + 体积门禁 + 主机侧纯逻辑单测)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --l0-only # 只跑 L0
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --l05-only # 只跑 L0.5
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --size-only # 只跑 L0c 体积门禁(强制路径)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --static-only # 只跑静态门禁
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --host-unit-only # 只跑主机侧纯逻辑单测
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --size-only # 只跑体积门禁(强制路径)
|
||||
& '.venv\Scripts\python.exe' scripts/check_size_gate.py --show # 体积同码凭证现状(0.3 s,无需 Keil)
|
||||
& '.venv\Scripts\python.exe' scripts/check_size_gate.py --stamp # 用新鲜 .map 重新盖戳(--update 已自动带做)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
- `§9` 三批计划(按「值不值得动门禁」排序)
|
||||
- `§9` 别再做什么
|
||||
- `§9` 批次五换算表:「数据可信」任务卡号(2026-10-27 整族清退)
|
||||
- `§10` 批次六换算表:测试验证层级号(2026-10-28 整族清退)
|
||||
|
||||
---
|
||||
|
||||
@@ -277,6 +278,44 @@ P-6 号必须能反查到定义(属主登记处得存在)· P-7 编号不语
|
||||
|
||||
---
|
||||
|
||||
## 10. 批次六换算表:测试验证层级号(2026-10-28 整族清退)
|
||||
|
||||
用户裁定:「我不想看到编号」→ 测试验证层级的 `L0 / L0a / L0b / L0c / L0.5 / L0.5c / L1 / L2 / L3` 整族停用,
|
||||
活文档、代码注释、CI 步骤名、门禁输出标签一律改语义名。**只有 CI 与门禁输出需要一个 ASCII 段名**(日志与命令行
|
||||
要纯 ASCII,中文进 PowerShell 汇总行会有代码页风险),它不是编号、是可读的英文段名。
|
||||
|
||||
| 旧层级号 | 语义名(文档 / CI 步骤名 / 表格) | ASCII 段名(`[SUMMARY]` 标签、CI job id、日志文件名、命令行旗标) |
|
||||
|---|---|---|
|
||||
| `L0` | 静态门禁 | `static`(job `static-gates`、`static-pre-commit.log`、旗标 `--static-only`) |
|
||||
| `L0a` | 钩子解释器预检 | `preflight`(`--preflight-only`;无 `[SUMMARY]` 行,只在收尾「已跑:」列表里出现) |
|
||||
| `L0b` | 提交信息门禁 | `commitmsg`(`[SUMMARY] gitlint:` 这个既有标签不改,与层级号无关) |
|
||||
| `L0c` | 体积门禁 | `size`(`--size-only`、`[SUMMARY] size-gate:` 保持不变) |
|
||||
| `L0.5` | 主机侧纯逻辑单测 | `host-unit`(job `host-unit-tests`、`host-unit-<模块>.log`、`--host-unit-only`) |
|
||||
| `L0.5c` | 主机单测覆盖率 | `coverage`(`--coverage`,opt-in) |
|
||||
| `L1` | 板端测试套件 | 无(只出现在文档,从不进日志) |
|
||||
| `L2` | 主机侧回归 | 无 |
|
||||
| `L3` | 人工目检 | 无 |
|
||||
|
||||
**旧旗标保留为别名**:`scripts/ci_local.py --l0-only` / `--l05-only` 仍可用(argparse 双选项名),肌肉记忆与既有脚本片段不断档;文档与新写的命令一律用新旗标。
|
||||
|
||||
### 10.1 撞族警示——同形字面量还属于另外六个族,读旧文先判族
|
||||
|
||||
这一族清掉**不等于**仓库里的 `L0`/`L1`/`L2`/`L3` 都消失了。同形形态仍属这些族,各自另有口径:
|
||||
|
||||
- **UI 设计令牌层**:`Drivers/BSP/UI/ui_tokens.h` 的「设计令牌层 L0」——层级架构名,**保留**。
|
||||
- **UI 菜单层级**:`L0~L3` 已于批次三a 改「主界面 / 一级 / 二级 / 三级菜单」,`docs/产品需求/产品需求设计文档.md` 尚有 2 处残留待清。
|
||||
- **采集链分层**:批次一已整族改语义层名(语义取数与有效性 → 标定折算 → 全周傅氏相量 → 测频与遥测快照 → 保护判据 → 出口与记录 → 动作留痕)。
|
||||
- **链路时延分段**:批次三b 改为中文段名 + ASCII 短码(`SPI_CMD` 等 11 个);`test/board/` 仍有残留待清。
|
||||
- **CubeMX 防丢失三层优先级**:批次三c 改为「写进 `.ioc` / 搬进 USER CODE 区 / 审计兜底」。
|
||||
- **硬件位号与封装尺寸**:磁珠电感位号 `L1`/`L2`/`L3`、封装 `SOT-23_L2.9-W1.3`、`CRYSTAL-L3.2`,以及 FTP 应答 `"215 UNIX Type: L8"` 这类**协议字符串字面量**——永不改(改了就是改行为)。
|
||||
|
||||
> 这就是 AGENTS「绝不全局查找替换」那条的又一次实测:本族改名如果按裸形 `L1` 全局替换,会顺手把上面六个族的
|
||||
> 字面量一起改掉,其中 6 处直接改到 C 断言文案与 FTP 应答。**每一步都要先判族再改,且只按行内置换。**
|
||||
|
||||
### 10.2 读史口径
|
||||
|
||||
历史文档、变更记录行、提交信息、`.gitea` 历史 run 里出现的 `L0`/`L0.5` 一律按本表换算来读;不要拿它们当"现行名"去搜代码或 CI(现行 CI 里搜 `L0` 只会命中别的族)。本表所在目录不参与编号门禁(`check_ids_new.py` 排除 `scripts/archive/`)。
|
||||
|
||||
## 变更记录(2026-10-28 瘦身以前的全量流水)
|
||||
|
||||
| 日期 | 变更 | 说明 |
|
||||
|
||||
@@ -41,7 +41,7 @@ r"""check_generated_fresh.py — 生成器幂等性门禁(重跑到临时目
|
||||
没装 fontTools,于是那次提交实测报:
|
||||
[FAIL] cn-font tools/font_gen/make_font.py 退出码 1:ERROR: No module named 'fontTools'
|
||||
纯标准库的 menu-gen / cn-chars 照旧绿,所以这是一根**只在字库链上假红**的闸:换任何一台没把
|
||||
.venv 排在 PATH 前面的机器都会重犯(同 scripts/ci_local.py 文件头 2026-10-11 记的 L0 全灭)。
|
||||
.venv 排在 PATH 前面的机器都会重犯(同 scripts/ci_local.py 文件头 2026-10-11 记的静态门禁全灭)。
|
||||
修法:脚本自己按优先级挑解释器(显式 DTU_GEN_PYTHON > 当前解释器 > 仓库 .venv > PATH 探到的
|
||||
机器级)**且逐个探针校验**(要真能 import fontTools 才算数),整轮生成器统一用挑出来的这一个
|
||||
跑(换解释器就可能换输出格式,逐字节比对要的是同一把尺子);一个都挑不出来时打 [ENV] + 候选
|
||||
@@ -167,7 +167,7 @@ def ws_path(rel):
|
||||
# 显式变量 DTU_GEN_PYTHON > 当前解释器 > 仓库 .venv > PATH 上探到的机器级,并且**必须探针校验**
|
||||
# 候选真的跑得动(装了 pre-commit ≠ 装了 fontTools;Microsoft Store 的 python 占位程序更是跑不起
|
||||
# 来)。为什么不能只把钩子 entry 写死成 .venv 里的解释器:ci_local.py 文件头记着 2026-10-11 那次
|
||||
# 「workflow 把候选收窄成 PATH python + 仓库 .venv,runner 上两者都是死路,L0 全灭」——收窄候选
|
||||
# 「workflow 把候选收窄成 PATH python + 仓库 .venv,runner 上两者都是死路,静态门禁全灭」——收窄候选
|
||||
# 等于把门禁绑死在一台机器上,本处保留全部候选 + 逐个探针,就是为了不再犯第三次。
|
||||
PROBE_CODE = (
|
||||
"import sys\n"
|
||||
|
||||
@@ -88,20 +88,20 @@ SIM_EXEMPT = [
|
||||
("App/algo/adc_input.c", "依赖 HAL/TPAFE 取数,MCU 专属"),
|
||||
("App/algo/phasor.c", "全周傅氏相量纯算法,模拟器无消费路径"),
|
||||
("App/algo/prot_elm.c", "保护判据元件纯算法,模拟器无消费路径"),
|
||||
("App/util/boot_stats.c", "开机账本(SD2506 用户 SRAM + RCC_CSR 快照,依赖 I2C/HAL 位掩码),MCU 专属;L0.5 覆盖于 test/host/boot_stats"),
|
||||
("App/util/boot_stats.c", "开机账本(SD2506 用户 SRAM + RCC_CSR 快照,依赖 I2C/HAL 位掩码),MCU 专属;主机侧纯逻辑单测覆盖于 test/host/boot_stats"),
|
||||
("App/algo/prot_set.c", "动作留痕保护定值纯算法,模拟器无消费路径"),
|
||||
("App/algo/freq.c", "全周傅氏相量派生测频纯算法(输入=phasor 使用区),模拟器无消费路径"),
|
||||
("App/cfg/prot_set_store.c", "定值持久化 MCU 胶水(依赖 prot_set 纯算法),模拟器无消费路径"),
|
||||
("App/log/soe_rec.c", "SOE 记录编解码纯逻辑(B9),L0.5 主机单测覆盖,模拟器无消费路径"),
|
||||
("App/log/soe_rec.c", "SOE 记录编解码纯逻辑(B9),主机侧纯逻辑单测覆盖,模拟器无消费路径"),
|
||||
("App/log/soe_store.c", "SOE 落盘 MCU 胶水(队列+FatFs 单写者,B9),模拟器无消费路径"),
|
||||
("App/algo/sample_ring.c", "全周傅氏相量环形缓冲纯算法,模拟器无消费路径"),
|
||||
("App/algo/adc_cal.c", "标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"),
|
||||
("App/algo/meas.c", "测频与遥测快照发布纯算法(唯一发布者 = MCU 侧 adcTask 200 ms 分频;104 改读快照与 UI 侧消费属另一波次另行接线,模拟器无消费路径;L0.5 单测 test/host/meas 覆盖),模拟器无消费路径"),
|
||||
("App/algo/run_par.c", "运行参数段纯算法 + [run_par] cfg 段注册 + 104 地址映射(写者 = iec104Task 直通、属主落盘 defaultTask;模拟器不跑 104 协议,模拟器无消费路径;L0.5 单测 test/host/run_par 覆盖),模拟器无消费路径"),
|
||||
("App/algo/meas.c", "测频与遥测快照发布纯算法(唯一发布者 = MCU 侧 adcTask 200 ms 分频;104 改读快照与 UI 侧消费属另一波次另行接线,模拟器无消费路径;主机侧纯逻辑单测 test/host/meas 覆盖),模拟器无消费路径"),
|
||||
("App/algo/run_par.c", "运行参数段纯算法 + [run_par] cfg 段注册 + 104 地址映射(写者 = iec104Task 直通、属主落盘 defaultTask;模拟器不跑 104 协议,模拟器无消费路径;主机侧纯逻辑单测 test/host/run_par 覆盖),模拟器无消费路径"),
|
||||
("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"),
|
||||
("App/app/ui_data_feed.c", "UI 快照装配器(依赖 phasor/prot_set/iec104 端口常量等固件侧数据源);模拟器不跑装配链,快照由 simulator/main.c 直接发布固定帧,模拟器无消费路径"),
|
||||
("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"),
|
||||
("App/fs/vol_idle_sync.c", "MCU 存储属主后台空闲同步(FatFs disk_ioctl + 卷锁 + sys_events),L0.5 覆盖于 test/host/vol_idle_sync/,模拟器无消费路径"),
|
||||
("App/fs/vol_idle_sync.c", "MCU 存储属主后台空闲同步(FatFs disk_ioctl + 卷锁 + sys_events),主机侧纯逻辑单测覆盖于 test/host/vol_idle_sync/,模拟器无消费路径"),
|
||||
("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"),
|
||||
("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"),
|
||||
("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||||
@@ -111,8 +111,8 @@ SIM_EXEMPT = [
|
||||
("App/task/net_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||||
("App/task/rs485_task.c", "固件 RS-485 任务,MCU 专属(另:uvprojx 缺口见 KNOWN_GAPS)"),
|
||||
("App/task/ui_task.c", "固件 UI 任务,模拟器另有 main.c 驱动 UI"),
|
||||
("App/protocol/iec104/iec104_asdu.c", "104 ASDU 编解码纯逻辑,L0.5 主机单测覆盖,模拟器无消费路径"),
|
||||
("App/protocol/iec104/iec104_link.c", "104 链路状态机纯逻辑(I/O 注入),L0.5 覆盖,模拟器无消费路径"),
|
||||
("App/protocol/iec104/iec104_asdu.c", "104 ASDU 编解码纯逻辑,主机侧纯逻辑单测覆盖,模拟器无消费路径"),
|
||||
("App/protocol/iec104/iec104_link.c", "104 链路状态机纯逻辑(I/O 注入),主机侧纯逻辑单测覆盖,模拟器无消费路径"),
|
||||
("App/protocol/iec104/iec104_pts.c", "104 点表/数据源适配(phasor/prot 依赖),模拟器无消费路径"),
|
||||
("App/util/sha256.c", "纯工具(FTP 鉴权在 MCU 端),模拟器无消费路径"),
|
||||
("App/util/sys_clock.c", "RTC/SD2506 依赖墙钟,MCU 专属"),
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
Grand Totals;2) `.map` 不早于指纹范围内任何源码的 mtime(防"改完代码
|
||||
拿旧构建盖章");3) 受管区占比 <= `ram_baseline.THRESHOLD_PCT`(与 mem-budget
|
||||
同一根线、同一个常数,不另立第二套口径)。
|
||||
--local 开发者自查 / ci_local 的 L0c 步:工作树指纹必须等于凭证指纹。
|
||||
--local 开发者自查 / ci_local 的体积门禁步:工作树指纹必须等于凭证指纹。
|
||||
--ci runner 上跑(无 Keil、无 `.map`):A. 同码性;B. 凭证里记录的受管区
|
||||
占比 <=92%(数字来自一次真实链接);C. 与 `ram_baseline.tsv` 末行同源=OK,
|
||||
不同源=WARN(多半只 --stamp 没 --update,不拦)。
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""
|
||||
主机侧单元测试(L0.5)的**覆盖率**口径、报告与增量门禁(纯标准库)。
|
||||
主机侧纯逻辑单元测试的**覆盖率**口径、报告与增量门禁(纯标准库)。
|
||||
|
||||
做什么:
|
||||
1. 发现 test/host/**/run_unit_test.py(与 scripts/ci_local.py 的 L0.5 同一套发现规则),
|
||||
1. 发现 test/host/**/run_unit_test.py(与 scripts/ci_local.py 的主机侧纯逻辑单测同一套发现规则),
|
||||
直接 import 各模块运行器、复用其中声明的 TestBuild(**单一事实源**:include 目录、
|
||||
桩顺序、测试单元路径都不在这里重复一遍);
|
||||
2. 用**与门禁完全相同的编译开关**(unit_test_harness._BASE_FLAGS)另建一份带 /Zi 调试
|
||||
@@ -16,7 +16,7 @@ r"""
|
||||
|
||||
为什么要它(2026-10-23,W5 验证常态):
|
||||
原评审报告 §4.8 记的两处空白之一(该报告 2026-10-25 已删除):无覆盖率口径——
|
||||
L0.5 只回答“跑过哪些 TC”,回答不了“生产源码里哪些行根本没进过”。用例数与 TC 追溯率
|
||||
主机侧纯逻辑单测只回答“跑过哪些 TC”,回答不了“生产源码里哪些行根本没进过”。用例数与 TC 追溯率
|
||||
会随时间上升,但覆盖率照的是另一类真相:**新增分支一行没测**(那类“读代码
|
||||
才发现”的缺陷,覆盖率是最便宜的机器照法)。
|
||||
|
||||
@@ -127,7 +127,7 @@ def load_harness():
|
||||
|
||||
|
||||
def discover_modules():
|
||||
"""{模块名: run_unit_test.py 绝对路径};发现规则与 ci_local.py 的 L0.5 一致。"""
|
||||
"""{模块名: run_unit_test.py 绝对路径};发现规则与 ci_local.py 的主机侧纯逻辑单测一致。"""
|
||||
out = {}
|
||||
if not HOST_TEST_DIR.is_dir():
|
||||
return out
|
||||
@@ -825,7 +825,7 @@ def run(args):
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
prog="check_test_coverage.py",
|
||||
description="主机侧单测(L0.5)覆盖率:插桩跑测试 + 统计自研生产源码行/函数覆盖 + 基线增量门禁")
|
||||
description="主机侧纯逻辑单测覆盖率:插桩跑测试 + 统计自研生产源码行/函数覆盖 + 基线增量门禁")
|
||||
ap.add_argument("--module", action="append", metavar="NAME",
|
||||
help="只处理指定模块(可重复;默认全部)")
|
||||
ap.add_argument("--list", action="store_true", help="只列出发现的模块名")
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
The interpreter that does work (a machine-wide install such as
|
||||
C:\ProgramData\anaconda3\python.exe) was no longer probed, and no
|
||||
machine-level DTU_PRECOMMIT_PYTHON had ever been set -> every L0 job died
|
||||
machine-level DTU_PRECOMMIT_PYTHON had ever been set -> every static-gate job died
|
||||
in "environment check" before running a single hook.
|
||||
|
||||
So discovery lives here now: one implementation, shared by both CI jobs and
|
||||
@@ -38,7 +38,7 @@
|
||||
%LOCALAPPDATA%\Programs\Python), newest version first
|
||||
|
||||
.PARAMETER NeedPreCommit
|
||||
Require the interpreter to expose the pre-commit module (L0 needs it).
|
||||
Require the interpreter to expose the pre-commit module (static gates need it).
|
||||
Probed with `-s` (no user site-packages) on purpose: the runner service
|
||||
runs as SYSTEM, which cannot see a USER-scope site install at all, so only
|
||||
an interpreter that works without user-site is accepted.
|
||||
|
||||
+53
-53
@@ -8,13 +8,13 @@
|
||||
跑同一套判定,命令与退出码与 CI 一一对应。
|
||||
|
||||
覆盖范围(与 CI 完全一致):
|
||||
L0 静态门禁 = `pre-commit run --all-files` + 提交信息门禁(gitlint)
|
||||
L0c 体积门禁(强制路径,2026-10-25)= `scripts/check_size_gate.py --local`
|
||||
静态门禁(static) = `pre-commit run --all-files` + 提交信息门禁(gitlint)
|
||||
体积门禁(size,强制路径,2026-10-25)= `scripts/check_size_gate.py --local`
|
||||
**fail-closed**:没有体积同码凭证、或代码与测量不同码即红——不再"没跑=通过"。
|
||||
CI 侧的「体积门禁」步跑的是同一条实现(那条无需 Keil,见 .gitea/workflows/ci.yml)。
|
||||
L0.5 主机侧纯逻辑单测 = `test/host/**/run_unit_test.py`
|
||||
L0.5c 覆盖率(**opt-in:`--coverage`**,2026-10-23)= `scripts/check_test_coverage.py`
|
||||
默认**不跑**、CI 也**不跑**:它依赖 VS 可选的「代码覆盖率工具」组件,且与 L0.5 那 16 个
|
||||
主机侧纯逻辑单测(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
|
||||
|
||||
⚠️ gitlint 必须**单独跑**(2026-10-11 审计):`stages: [commit-msg]` 写在 gitlint **上游**
|
||||
@@ -28,25 +28,25 @@
|
||||
- `@build`(需 Keil 授权;CI 也不跑,真编译是开发者本地红线)
|
||||
- `mem-numbers` / `mem-budget` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map
|
||||
就照常跑**,与 CI 的自适应逻辑一致)。⚠ 这两根闸在"无 .map"时是 **SKIP 而不是
|
||||
PASS**(等于这一趟什么都没查);那个缺口由上面的 L0c 体积门禁补住:同码凭证
|
||||
PASS**(等于这一趟什么都没查);那个缺口由上面的体积门禁(size)补住:同码凭证
|
||||
+ 92% 红线复核,无需 Keil 也能判,且 fail-closed。
|
||||
|
||||
退出码:
|
||||
0 = 全部通过(或不适用项已按规则跳过)
|
||||
1 = 有门禁/单测失败
|
||||
2 = 环境错误(缺 pre-commit / python 模块等;L0a 钩子解释器预检未过也是 2)
|
||||
2 = 环境错误(缺 pre-commit / python 模块等;钩子解释器预检未过也是 2)
|
||||
|
||||
汇总行(2026-10-02 起,对齐 CI 的 ::notice::):
|
||||
[SUMMARY] L0: pre-commit hooks -- passed=N failed=M skipped=K
|
||||
[SUMMARY] static: pre-commit hooks -- passed=N failed=M skipped=K
|
||||
[SUMMARY] gitlint: N commit message(s) checked, M failed
|
||||
[SUMMARY] L0.5: K runner(s), A/B assertions passed, fail=F
|
||||
[SUMMARY] host-unit: K runner(s), A/B assertions passed, fail=F
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --l0-only
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --static-only
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --size-only
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --l05-only
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --preflight-only # 只跑 L0a 钩子解释器预检
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --host-unit-only
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --preflight-only # 只跑钩子解释器预检
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --keep-going # 失败也继续跑完
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --commits 20 # 提交信息校验条数
|
||||
|
||||
@@ -69,7 +69,7 @@ def _global_python_candidates():
|
||||
"""常见**机器级** Python 安装位置(必须与 scripts/ci_find_python.ps1 同序)。
|
||||
|
||||
两处探测一旦不同步,就会出现"本地能跑、CI 挂"的老毛病:2026-10-11 的
|
||||
L0 全灭就是 workflow 把候选收窄成 PATH python + 仓库 .venv(runner 上两者
|
||||
静态门禁全灭就是 workflow 把候选收窄成 PATH python + 仓库 .venv(runner 上两者
|
||||
都是死路),而真正装了 pre-commit 的机器级解释器(如
|
||||
C:\\ProgramData\\anaconda3\\python.exe)不再被探测。
|
||||
|
||||
@@ -362,7 +362,7 @@ def run_hook_preflight():
|
||||
"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" L0a 钩子解释器预检(只读,不改环境)")
|
||||
print(" preflight 钩子解释器预检(只读,不改环境)")
|
||||
print("=" * 64)
|
||||
|
||||
hook = REPO_ROOT / ".git" / "hooks" / "pre-commit"
|
||||
@@ -467,7 +467,7 @@ def _run(cmd, env=None):
|
||||
def _run_capture(cmd, env=None):
|
||||
"""跑子进程,输出**既直通终端又捕获**(对齐 CI 的 Tee-Object)。
|
||||
|
||||
返回 (退出码, 全部输出文本)。用于 L0.5 解析各模块汇总行。
|
||||
返回 (退出码, 全部输出文本)。用于主机侧纯逻辑单测解析各模块汇总行。
|
||||
"""
|
||||
print("$ " + " ".join(str(c) for c in cmd))
|
||||
cp = subprocess.run(cmd, cwd=str(REPO_ROOT), env=env,
|
||||
@@ -485,11 +485,11 @@ def _run_capture(cmd, env=None):
|
||||
return cp.returncode, text
|
||||
|
||||
|
||||
def run_l0():
|
||||
"""L0:pre-commit 全部钩子。返回退出码(0/1/2)。"""
|
||||
def run_static_gates():
|
||||
"""静态门禁:pre-commit 全部钩子。返回退出码(0/1/2)。"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" L0 静态门禁(pre-commit --all-files)")
|
||||
print(" static 静态门禁(pre-commit --all-files)")
|
||||
print("=" * 64)
|
||||
|
||||
env = dict(os.environ)
|
||||
@@ -514,7 +514,7 @@ def run_l0():
|
||||
passed = sum(1 for l in lines if l.rstrip().endswith("Passed"))
|
||||
failed = sum(1 for l in lines if l.rstrip().endswith("Failed"))
|
||||
skipd = sum(1 for l in lines if l.rstrip().endswith("Skipped"))
|
||||
print("[SUMMARY] L0: pre-commit hooks -- passed={} failed={} skipped={}".format(
|
||||
print("[SUMMARY] static: pre-commit hooks -- passed={} failed={} skipped={}".format(
|
||||
passed, failed, skipd))
|
||||
return rc
|
||||
|
||||
@@ -528,7 +528,7 @@ def run_gitlint(count):
|
||||
"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" L0b 提交信息门禁(gitlint / commit-msg stage,最近 {} 条)".format(count))
|
||||
print(" commitmsg 提交信息门禁(gitlint / commit-msg stage,最近 {} 条)".format(count))
|
||||
print("=" * 64)
|
||||
|
||||
def _git(*args):
|
||||
@@ -589,7 +589,7 @@ def run_gitlint(count):
|
||||
|
||||
|
||||
def run_size_gate():
|
||||
"""L0c:体积门禁(强制路径)。返回退出码(0/1/2)。
|
||||
"""体积门禁(强制路径)。返回退出码(0/1/2)。
|
||||
|
||||
为什么必须单列一步(2026-10-25):`mem-numbers` / `mem-budget` 的事实源是 Keil
|
||||
`.map`,无 `.map` 时两者**自适应 SKIP 并返回 0**——"改了代码但根本没编译"
|
||||
@@ -603,7 +603,7 @@ def run_size_gate():
|
||||
"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" L0c 体积门禁(同码凭证 + 92% 红线,无需 Keil)")
|
||||
print(" size 体积门禁(同码凭证 + 92% 红线,无需 Keil)")
|
||||
print("=" * 64)
|
||||
script = REPO_ROOT / "scripts" / "check_size_gate.py"
|
||||
if not script.is_file():
|
||||
@@ -619,11 +619,11 @@ def run_size_gate():
|
||||
return rc
|
||||
|
||||
|
||||
def run_l05(args):
|
||||
"""L0.5:主机侧单测。返回退出码(0/1/2)。"""
|
||||
def run_host_unit_tests(args):
|
||||
"""主机侧纯逻辑单测。返回退出码(0/1/2)。"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" L0.5 主机侧纯逻辑单测(MSVC,零硬件)")
|
||||
print(" host-unit 主机侧纯逻辑单测(MSVC,零硬件)")
|
||||
print("=" * 64)
|
||||
|
||||
runners = _find_unit_runners()
|
||||
@@ -665,7 +665,7 @@ def run_l05(args):
|
||||
else:
|
||||
print("[OK] {}".format(rel))
|
||||
|
||||
print("[SUMMARY] L0.5: {} runner(s), {}/{} assertions passed, fail={}".format(
|
||||
print("[SUMMARY] host-unit: {} runner(s), {}/{} assertions passed, fail={}".format(
|
||||
runner_count, sum_pass, sum_total, rc_all))
|
||||
return rc_all
|
||||
|
||||
@@ -673,12 +673,12 @@ def run_l05(args):
|
||||
def run_coverage():
|
||||
"""覆盖率附加步骤(opt-in,CI 不跑):主机单测覆盖率 + 基线增量门禁。
|
||||
|
||||
单独一步而不并进 L0.5:L0.5 是**必须绿**的门禁,覆盖率是**趋势视图**,两者失败含义不同
|
||||
单独一步而不并进主机侧纯逻辑单测:那一层是**必须绿**的门禁,覆盖率是**趋势视图**,两者失败含义不同
|
||||
(前者=测试挂了,后者=覆盖回退了 / 缺工具链)。退出码原样透传,由 main 汇总。
|
||||
"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" L0.5c 主机单测覆盖率(mscoverage 动态插桩 + 基线增量门禁)")
|
||||
print(" coverage 主机单测覆盖率(mscoverage 动态插桩 + 基线增量门禁)")
|
||||
print("=" * 64)
|
||||
script = REPO_ROOT / "scripts" / "check_test_coverage.py"
|
||||
if not script.is_file():
|
||||
@@ -702,16 +702,16 @@ def main(argv=None):
|
||||
pass
|
||||
|
||||
ap = argparse.ArgumentParser(
|
||||
description="本地跑与 CI 等价的门禁(L0 + L0.5,纯标准库)")
|
||||
description="本地跑与 CI 等价的门禁(静态门禁 + 主机侧纯逻辑单测,纯标准库)")
|
||||
g = ap.add_mutually_exclusive_group()
|
||||
g.add_argument("--l0-only", action="store_true", help="只跑 L0 静态门禁")
|
||||
g.add_argument("--l05-only", action="store_true", help="只跑 L0.5 主机侧单测")
|
||||
g.add_argument("--static-only", "--l0-only", dest="static_only", action="store_true", help="只跑静态门禁(旧旗标 --l0-only 仍可用)")
|
||||
g.add_argument("--host-unit-only", "--l05-only", dest="host_unit_only", action="store_true", help="只跑主机侧纯逻辑单测(旧旗标 --l05-only 仍可用)")
|
||||
g.add_argument("--size-only", action="store_true",
|
||||
help="只跑 L0c 体积门禁(强制路径;约 0.3 s,无需 Keil)")
|
||||
help="只跑体积门禁(强制路径;约 0.3 s,无需 Keil)")
|
||||
ap.add_argument("--coverage", action="store_true",
|
||||
help="额外跑主机单测覆盖率(约 25 s;需 VS 代码覆盖率组件;默认不跑,CI 也不跑)")
|
||||
ap.add_argument("--preflight-only", action="store_true",
|
||||
help="只跑 L0a 钩子解释器预检(只读;缺包时当场指名钩子)")
|
||||
help="只跑钩子解释器预检(只读;缺包时当场指名钩子)")
|
||||
ap.add_argument("--keep-going", action="store_true",
|
||||
help="某步失败也继续跑后续步骤(默认首错即停)")
|
||||
ap.add_argument("--commits", type=int, default=10, metavar="N",
|
||||
@@ -740,50 +740,50 @@ def main(argv=None):
|
||||
print(" - 或在当前环境安装:pip install pre-commit")
|
||||
return 2
|
||||
|
||||
# L0a 钩子解释器预检:把"裸 python 条目 + 缺第三方包 + 脚本不自解析"这条隐性依赖
|
||||
# 钩子解释器预检(preflight):把"裸 python 条目 + 缺第三方包 + 脚本不自解析"这条隐性依赖
|
||||
# 提到所有钩子之前判掉(道G task-33)。--preflight-only 时它是唯一一段。
|
||||
rc_pre = run_hook_preflight()
|
||||
if args.preflight_only:
|
||||
return rc_pre
|
||||
if rc_pre == 2:
|
||||
print("")
|
||||
print("[FAIL] L0a 预检未过——先按上面指名的钩子/包修配置或环境,再跑门禁")
|
||||
print("[FAIL] 钩子解释器预检未过——先按上面指名的钩子/包修配置或环境,再跑门禁")
|
||||
if not args.keep_going:
|
||||
return 2
|
||||
|
||||
rc = 0
|
||||
# 实际跑过哪些段——收尾那行只报这些。理由与那个盲区同源且是同一类错误:
|
||||
# "--l05-only 却宣布 L0/L0b/L0c 全部通过"就是把没跑的说成跑过了(SKIP 冒充 PASS
|
||||
# 的镜像版)。2026-10-25 做 task-10 时从 --l05-only 的输出里发现,当场改掉。
|
||||
ran = ["L0a"] # 钩子解释器预检已在本脚本开头无条件跑过,收尾行照实列出
|
||||
# "--host-unit-only 却宣布静态门禁/提交信息门禁/体积门禁全部通过"就是把没跑的说成跑过了(SKIP 冒充 PASS
|
||||
# 的镜像版)。2026-10-25 做 task-10 时从 --host-unit-only 的输出里发现,当场改掉。
|
||||
ran = ["preflight"] # 钩子解释器预检已在本脚本开头无条件跑过,收尾行照实列出
|
||||
try:
|
||||
if args.size_only:
|
||||
return run_size_gate()
|
||||
if not args.l05_only:
|
||||
rc = run_l0()
|
||||
ran.append("L0")
|
||||
if not args.host_unit_only:
|
||||
rc = run_static_gates()
|
||||
ran.append("static")
|
||||
# 提交信息门禁独立于 --all-files(stage 不同),故单独跑一段;
|
||||
# 即使 L0 已失败也照跑,一次看全所有违规项。
|
||||
# 即使静态门禁已失败也照跑,一次看全所有违规项。
|
||||
rc_msg = run_gitlint(args.commits)
|
||||
ran.append("L0b")
|
||||
# L0c 体积门禁同样**无条件照跑**:它回答的是"这份代码测过体积没有",
|
||||
# 与 L0/L0b 的结论正交,且只要 0.3 s——少跑一次就是那个盲区。
|
||||
ran.append("commitmsg")
|
||||
# 体积门禁同样**无条件照跑**:它回答的是"这份代码测过体积没有",
|
||||
# 与静态门禁/提交信息门禁的结论正交,且只要 0.3 s——少跑一次就是那个盲区。
|
||||
rc_size = run_size_gate()
|
||||
ran.append("L0c")
|
||||
ran.append("size")
|
||||
if rc != 0 or rc_msg != 0 or rc_size != 0:
|
||||
if not args.keep_going:
|
||||
print("")
|
||||
print("[FAIL] L0 / L0b / L0c 有未通过项 —— 中止"
|
||||
"(--keep-going 可继续跑 L0.5)")
|
||||
print("[FAIL] 静态门禁 / 提交信息门禁 / 体积门禁有未通过项 —— 中止"
|
||||
"(--keep-going 可继续跑主机侧纯逻辑单测)")
|
||||
return 1
|
||||
rc = rc or rc_msg or rc_size
|
||||
if not args.l0_only:
|
||||
rc05 = run_l05(args)
|
||||
ran.append("L0.5")
|
||||
rc = rc or rc05
|
||||
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("L0.5c")
|
||||
ran.append("coverage")
|
||||
except FileNotFoundError as e:
|
||||
print("[ERROR] 环境错误:{}".format(e))
|
||||
return 2
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
2. NO MACHINE-LEVEL TOOLCHAIN. The runner service runs as SYSTEM.
|
||||
pre-commit / cppcheck were only reachable through the interactive
|
||||
user's PATH, and no machine-level DTU_* variable was ever set, so
|
||||
even a live runner died in the L0 environment check.
|
||||
even a live runner died in the static-gate environment check.
|
||||
|
||||
This script installs the missing supervision (a watchdog launcher + a
|
||||
scheduled task) and, with -SetEnv, writes the machine-level DTU_* variables
|
||||
@@ -361,7 +361,7 @@ if ($SetEnv) {
|
||||
|
||||
$cl = Find-MsvcCl
|
||||
if ($cl) { Set-MachineEnv 'DTU_MSVC_CL' $cl 'DTU_MSVC_CL' }
|
||||
else { Warn 'MSVC cl.exe (Hostx64\x64) not found -- L0.5 will degrade to "skipped" without it' }
|
||||
else { Warn 'MSVC cl.exe (Hostx64\x64) not found -- host-side unit tests will degrade to "skipped" without it' }
|
||||
|
||||
Info 'machine env vars are read when a PROCESS STARTS: the daemon must be restarted below'
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ def cmd_gate(args):
|
||||
print("[SKIP] 无 .map(未做 Keil 构建)→ RAM 预算门禁不适用")
|
||||
print(" ⚠ **SKIP 不等于 PASS**:本钩子这一趟什么都没检查。")
|
||||
print(" 强制兜底(无需 Keil,改代码后必然拦红):")
|
||||
print(" 本地 scripts/ci_local.py 的 L0c 步 / CI 的「体积门禁」步,")
|
||||
print(" 本地 scripts/ci_local.py 的体积门禁步 / CI 的「体积门禁」步,")
|
||||
print(" 都跑 scripts/check_size_gate.py(同码凭证 + 92% 红线复核)。")
|
||||
print(" 改过代码请先 @build,再 python scripts/ram_baseline.py --update。")
|
||||
return 0
|
||||
@@ -334,7 +334,7 @@ def cmd_update(args):
|
||||
# 强制路径(2026-10-25):同一次「真实测量」顺带刷新**体积同码凭证**
|
||||
# scripts/size_attestation.json——这样「每次 @build 后跑 --update」这条**既有
|
||||
# 纪律**同时喂饱那条无需 Keil 也能判的强制门禁(check_size_gate --ci /
|
||||
# --local,即 ci_local 的 L0c 步与 CI 的体积门禁步),不新增人工步骤。
|
||||
# --local,即 ci_local 的体积门禁步与 CI 的体积门禁步),不新增人工步骤。
|
||||
# 盖戳失败只告警、不改本命令返回值:凭证新鲜度由门禁自己 fail-closed 兜,
|
||||
# 失败原因(.map 比源码旧 / 已超线)会在门禁那一侧明确报出来。
|
||||
# 为什么在函数内 import:check_size_gate 模块级 import 本模块,反过来即成环。
|
||||
|
||||
Reference in New Issue
Block a user