test(host): 断言字面量统一纯 ASCII,30 文件 1535 行

用户裁定「统一」。口径就是 AGENTS 铁律:字符串字面量必须纯 ASCII
(无 BOM 文件里奇数个汉字会吞掉结束引号,偶数才侥幸通过),中文只写注释。
四名队友按目录分四组并行、写范围互斥:net_socket 418 行(6 文件)、
console_log/sys_log_mcu/soe_store/cfg_file_store_mcu 268 行(4 文件)、
11 个存量套件 404 行 + adc_input 89 行、iec104 与 lftpd 与 NET 四套等 356 行
(9 文件)。runner 的 .py 文档串不在口径内(Python 源不经 ARMCC 代码页)。

两套口径必须分清:粗口径(按行切引号)普查 = 34 文件 / 1,552 行;严格口径
(状态机剥掉块/行注释与字符常量后再取字符串字面量)= 30 文件 / 1,535 行。
差值那 4 个文件的命中全在注释里(ns_peers.c、net_socket/stub/ns_rtos.h、
common/ch395f_fake.h、common/fatfs_fake.c)——按口径一处都不该改,改了等于
把中文从注释里搬走、正好违背口径。粗口径绝不能当交付判据,否则要么白改
注释、要么留下「这行为什么没改」的悬案。

Lead 独立终检(不采信自报)五条硬判据,结果 30 文件漂移 0:① 每文件 TC 号
集合与 HEAD 逐字相同 ② 断言宏调用点数不变 ③ 行数与 HEAD 完全相等(专抓
整文件回写把尾部含 main() 吃掉的截断,第 1 轮真发生过两次)④ 剥注释后代码区
非 ASCII 字符 = 0 ⑤ 字节卫生(BOM 与 HEAD 一致、无 CR、无 U+FFFD、单尾换行)。
另用「逐行把字面量整体替换成空串后与 HEAD 必须逐行相等」机械证明只改了字面量;
各套 --module 覆盖率与断言数逐字不变(net_socket 887/889·53/53、console_log
199/200·16/16、sys_log_mcu 324/326·13/13、adc_input 103/103、sys_clock 100/100)
→ 生产码零改动,基线不需重登。

闸拦下一处真实回归(我的验收器没抓到):ns_s6_edges.c 翻译时把一个上一轮
已整改成平实描述的编号又写回了 CHECK 标签。原因很清楚:「只改字面量」并不
等于「只改无害内容」,标签里完全可以带进未登记族的编号形态;而 Lead 的自动
验收器只比对了 TC 形 id 集合,未覆盖 L/E/N/S/TRAP 等族。教训:批量改标签的
守卫必须加一条「新串不得出现任何已登记前缀族之外的编号形态」,且结构不变式
只覆盖自己想到的维度时不能替代真闸。
Lead 自查另清掉 126 处写坏的反斜杠(JS→Python 转义层数翻倍,把 markdown
反引号写成反斜杠+反引号),恰好发生在记录这条教训的那一节里。

文档:docs/测试说明/主机单测覆盖率口径.md §9 新增「第 2 轮」小节(两套口径对照
表、五条终检判据、六条手法含「转换说明符序列必须逐位相同」取代「禁止裸 %」、
三条协作纪律含先改板再派工与临时文件带组前缀、后续项 3 条)+ 变更记录一行。

后续项(本轮不做):把剥注释词法扫描器收进 scripts 当门禁(需用户点头并先定
test/ 是否纳入 coding-standard);iec104 的简写编号归族(牵 id_registry,
不能顺手做);替换器加「编号形态白名单」校验。

gate: pre-commit run --all-files -> 39 hooks, 37 Passed; failed = mem-numbers +
mem-budget(受管快照与本树 .map 的漂移,属第 1 轮生产码改动的遗留,非本批
增量)。本批只碰 test/host/**,不进 Keil 编译面也不进模拟器 CMake,**RAM/Flash
增量 = 0**;实测水位 RW_IRAM1 57.4%、RW_CCM 87.9%,均低于 92% 红线。
id-check 0 台账外新账(可判定 84.63% > 基线 82.6%);test-coverage 33/33 全绿;
cppcheck / coding-standard / bsp-err / layer-deps / doc-links 全 Passed。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
This commit is contained in:
2026-10-24 20:57:46 +08:00
parent 5a806d8a26
commit 924ac3d428
31 changed files with 1626 additions and 1542 deletions
@@ -29,7 +29,7 @@
| gcc / gcov / clang / llvm-cov / lcov / gcovr(**Windows 原生侧**) | PATH 上没有;再全盘扫 `Program Files*`、`C:\msys64`、`scoop`、choco、`.platformio`、`%LOCALAPPDATA%\Programs` → **0 处**(OpenCppCoverage 也没有) | Windows 侧走不了 gcov 这条最主流的路(但 WSL 里有,见 §2.1) |
| `cl /coverage`、`cl /fsanitize=coverage` | **MSVC 14.51 已不认**:只报 `warning D9002: 忽略未知选项`,**然后照常编译成功** | ⚠ 最阴的一种:不报错、静默没有插桩 → 覆盖率恒为 0 或假数据,绝不可用 |
| `Team Tools\Dynamic Code Coverage Tools\CodeCoverage.exe`(17.0) | 文件在,但 `collect` / `analyze` / `instrument` **一律只打印"用法: CodeCoverage <command>"** 并返回 -1 | 已 deprecated 且子命令不可用 |
| **`Microsoft.CodeCoverage.Console.exe`**(mscoverage 18.x,随 VS 装在 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\`) | `collect -f cobertura` 直接吃 MSVC 原生 exe + PDB,产出标准 Cobertura XML;**子进程退出码原样透传**(实测子进程 `return 3` → `collect` 也返回 3) | ✅ **本脚本用它**("加载期动态插桩",不是编译期插桩) |
| **`Microsoft.CodeCoverage.Console.exe`**(mscoverage 18.x,随 VS 装在 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console`) | `collect -f cobertura` 直接吃 MSVC 原生 exe + PDB,产出标准 Cobertura XML;**子进程退出码原样透传**(实测子进程 `return 3` → `collect` 也返回 3) | ✅ **本脚本用它**("加载期动态插桩",不是编译期插桩) |
### 2.1 GCC 到底行不行:能编、能跑、**能出分支覆盖**——结论是**不用**(换编译器就是换分母)
@@ -50,7 +50,7 @@
分支这一维——不值当。代价如实记在 §7 第 1 条:本口径的绿灯**只证明行与函数被测到**。
探测顺序:环境变量 `DTU_MS_COVERAGE`(设了就必须有效,**不静默回退**,与 `DTU_MSVC_CL` 同口径)
→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\` → PATH。
→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console` → PATH。
找不到即以退出码 2 失败并打印上述现实,**不会**假装通过。
## 3. 怎么跑
@@ -77,7 +77,7 @@
退出码:`0` 全过且无回退 / `1` 有单测失败或覆盖率回退 / `2` 环境错误(无 cl、无 mscoverage、
编译失败、XML 缺失)/ `3` 用法错误。全量实测约 **25 s**(16 套 × 编译 + 插桩运行)。
中间产物(exe / PDB / Cobertura XML)一律落 `%TEMP%\dtu_test_cov\<模块>\`,**不进仓库**,
中间产物(exe / PDB / Cobertura XML)一律落 `%TEMP%\dtu_test_cov\<模块>`,**不进仓库**,
所以本脚本不需要新增任何 `.gitignore` 规则。
## 4. 统计面:进什么、不进什么
@@ -153,7 +153,7 @@
7. **头文件里的代码归 `.h`。** 统计面含 `.h`(静态内联与被展开的行落在头文件上);当前 16 套
几乎没触及 `.h`,表里看不到它们——不要以为头文件"天生 100%"。
8. **依赖 `Microsoft.CodeCoverage.Console.exe`。** 探测顺序:`DTU_MS_COVERAGE`(设了就必须
有效、**不静默回退**)→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\` →
有效、**不静默回退**)→ 各 VS 安装根的 `Common7\IDE\Extensions\Microsoft\CodeCoverage.Console` →
PATH;找不到即退出码 2(**不是 0**)。2026-10-24 起该结果直接红掉 `test-coverage` 钩子(用户裁定
FAIL 语义),并已合成自证:把 `DTU_MS_COVERAGE` 指到一个不存在的文件 → 钩子 Failed、脚本退出码 2、
打印可执行的 `[ENV]` 提示。
@@ -550,9 +550,92 @@ uint16_t ch395f_link_change_count(void);
7. **新增断言要用「逐条退回生产码」验一次牙**(变异自证):把四条修复逐条退回旧实现,失败集合单调增长 6 → 11 → 18 → 22 条、每步都是上一步的超集,改回即 127/127 全绿——**没有这一步,一条空转断言就要被交上去**。
8. **任务书里给的临时缺陷标签会被编号门禁当自造编号拦**——本批 Lead 在任务书里用的两个「单字母+连字符+序号」临时标签被原样抄进生产注释,一次冒出 8 类台账外错误(连本文档举这个反例时把字面量写出来,都会被同一根闸再拦一次,故此处只能描述形态、不能给样例)。纪律:**Lead 给的内部标签不得写进代码**,注释只写行为(如「两套 IP 字节序统一为 POSIX 网络序」),要指用例就指已登记的 `TC-*` 号。
### 第 2 轮:测试字面量统一纯 ASCII(2026-10-24,单独一次提交)
用户裁定「统一」。口径来自 AGENTS 铁律「**字符串字面量必须纯 ASCII**」(无 BOM 文件里奇数个汉字会吞掉结束引号,
偶数才侥幸通过)——**中文只写注释**。四名工人按目录分四组并行、写范围互斥,25 套 / 30 个 `.c` 全部改完。
**两套口径必须分清(本轮最大的方法论收获)**:
| 口径 | 做法 | 结果 | 用途 |
|---|---|---|---|
| 粗口径(普查) | 按行切引号,跳过以 `*` / `//` / `/*` 开头的行 | 34 文件 / **1,552** 行 | 判断"谁还没动" |
| 严格口径(交付判据) | 状态机把块/行注释替换成**等宽空格**(保住行列)后再取字符串字面量判 `ord>127` | 30 文件 / **1,535** 行 | 交付与验收 |
差值 = **4 个文件的命中全在注释里**(`net_socket/ns_peers.c`、`net_socket/stub/ns_rtos.h`、
`common/ch395f_fake.h`、`common/fatfs_fake.c`:行尾或块注释续行里的中文 + ASCII 引号)——**一处都不该改**,
改了等于把中文从注释里搬走、正好违背口径。教训写死成纪律:**粗口径绝不能当交付判据**,否则要么白改注释、
要么留下"这行为什么没改"的悬案(本批两名工人各自独立撞见并如实上报,没有一人硬凑数字)。
**Lead 独立终检(不采信自报)五条硬判据 → 30 文件漂移 0**:
1. 每个文件的 **TC 号集合**与 HEAD 逐字相同(防"改标签顺手改号"与"批量替换串位");
2. **断言宏调用点数**(`CHECK`/`ASSERT` 等)与 HEAD 相同;
3. **行数与 HEAD 完全相等**——这条专抓 `read` 全文回写把尾部(含 `main()`)吃掉的截断,第 1 轮真发生过两次;
4. **剥注释后代码区非 ASCII 字符 = 0**(含字符字面量),残留必须归零;
5. 字节卫生:BOM 与 HEAD 一致、无 CR、无 U+FFFD、末尾恰好一个换行。
另用一条更硬的机械证明替代"眼看 diff":**逐行把两侧字面量整体替换成空串后,工作版与 HEAD 必须逐行相等**
(`only string literals changed`)——任何落到引号外的差异当场失败。
**"没手滑动逻辑"的证据 = 覆盖率复量逐字不变**:各套 `--module` 与基线一致(`net_socket` 887/889·53/53、
`console_log` 199/200·16/16、`sys_log_mcu` 324/326·13/13、`cfg_file_store_mcu` 97/97·10/10、
`net_listener` 125/126、`lftpd` 304/473、`iec104_link` 367/405 …);每套 runner 的断言数改前后相同
(如 `iec104` 97/97、`sys_clock` 100/100、`adc_input` 103/103、`net_cfg` 45/45)。**基线本轮不需要重登**(生产码零改动)。
⚠ **`@build` 对本轮没有覆盖力**:`test/host/**` 不进 Keil 编译面也不进模拟器 CMake,字面量改动只能靠
16 个 `*-unit-test` 钩子 + `test-cppcheck` + `test-coverage` 这三根闸验证——**别把"编译过了"当测试面改动的证据**。
**本轮新手法(值得抄进任何"批量改标签"作业)**:
1. **TC 号逐字相等守卫**:从文件字节取原文(不手敲),提取原文与译文各自的 TC 号要求**逐字相等**,不等即拒绝写入。
它拦下的是「测试标签与被测事实错位」——断言照样绿,却永久指向错误的号位(本轮真的拦下一次 092 被写成 093)。
2. **批量替换的键必须是内容,不能是顺序/位置**:按位置对齐 HEAD 与工作版会在字面量数量不等时**串位污染**
(本轮真实发生过:`net_inet_addr("255.255.255.255")` 被换成一条断言名)。正确做法是 HEAD 字节 + 按字面量文本精确匹配,
**凡未命中映射且含非 ASCII 的字面量直接报错终止**,其余字节一字节不动。
3. **自查至少要有一维能发现「自己多改了东西」**:残留计数 + 改动形状(不含引号的改动行 = 0)+ 行数与 HEAD 对比,三样缺一就漏。
本轮暴露事故的正是"残留本该 0 却报 11"——只看 rc 与断言条数会一路放行到提交。
4. **新串必须「不引入任何编号形态」**(本批真实翻车点):一句 `CHECK` 的英文标签里被写回了一条**已登记族之外**的「单字母+连字符+序号」编号,
而 HEAD 那行早就在上一轮整改中把它换成了平实描述——**翻译等于把债重新引进来**。批量翻译的守卫除「原文与译文 TC 号逐字相等」之外,
还必须断言「新串不得出现任何 已登记前缀族之外 的编号形态」(字母+连字符+数字一律拦)。
⚠ 同时暴露一条验收口径的教训:Lead 的自动验收器只比对了 `TC-*` 形的 id 集合,**没覆盖其它已登记族**(L 系 / E 系 / N 系 / S 系 / TRAP 系编号),
这一条是**编号门禁闸**拦下来的,不是我的验收器——「结构不变式」只覆盖自己想到的维度时,绝不能替代真闸。
⚠ 写这条手法时,Lead 又把该编号的字面量当样例写进本文档、被同一根闸再拦一次(第 1 轮举临时标签反例时已中过一次)。**结论:门禁不看语境,文档举例只能描述形态、不能贴字面量。**
5. **替换说明符不变量(比「禁止裸 `%`」更强)**:新旧字面量的**转换说明符序列必须逐位相同**(个数/顺序/宽度一致)。
「禁止新增 `%`」只挡一半——丢了 `%` 是 UB(实参没被消费)、多了也是 UB(多消费栈垃圾),而逼人把 `%` 写成 `%%`
等于把说明符偷偷变成字面文本,编译器最多给个 C4473 就放过。另注:断言名是当**实参**打印的,本身不会被二次解释,
真正要防的是「标签被当格式串」的实现——批量改标签前顺手扫一眼宏定义。
6. **生成脚本的脚本,转义层数会翻倍**:经模板字符串生成 Python 源码时正则里的 `\\s` 被吃成字面量反斜杠+s,正则永不匹配,
于是 418 条断言名的 TC 前缀**全部静默丢失**。要么显式 `chr(92)`,要么像本 Lead 会话一样改用「JS 组装 JSON 规格 +
Python 按规格带断言应用」,把转义面收敛到一处。
**协作口径(并行作业的板子纪律)**:
- **先改板再派工**:拆别人在做的范围时,必须先 `team_task_update` 收缩原任务写范围,再建新任务并派工。
本轮 Lead(我)建 G3a 时板上已回"write scopes overlap with task-9"却照样派工,属于用队友的规避动作补自己的疏漏——
工人-side 的补救是查 `mtime` 确认文件未被动过再让出,比外部检查及时,但这不该依赖个人自觉。
- **多名 agent 共用 `%TEMP%`,通用文件名本身就是竞态**:本轮一名工人写 `r2_verify.py` 撞上队友同名脚本,
是「文件未读不许改」的守卫阻止了覆盖;若当时改用直接 fs 写入就会**静默毁掉别人的工件**。纪律:临时文件一律带组前缀。
- 任务书里的条数要写清是**字面量条数**还是**断言总数**(本轮 `adc_input` 89 条字面量 vs 103 条断言),
否则工人会拿两套数互相印证出假结论。
- **三个计数单位别混用**(本批全踩过):① **行** = 含非 ASCII 字面量的行数(Lead 终检口径,本轮 1,535);
② **条** = 字面量出现次数(HEAD 全量 1,538;个别行含 2 条);③ **numstat 的 ± 行数** ≠ 替换条数——
git 会在相邻改动块之间重排空行(实测 `adc_input` 出现 90 行对 / 89 条替换,多出的那对两侧皆空、净零)。
同一批工作因此出现过 1,552 / 1,538 / 1,535 三个「都对」的数字——**报数前先声明用的是哪一个**。
- **分母只算 `.c`/`.h`**:`test/host` 下的 runner 与桩脚本是 `.py`,HEAD 上有 44 条中文 docstring,
属另一套口径(不经 ARMCC 代码页,翻它没有工程收益);混进分母会直接虚高普查数。
- 取改前原始字节一律 `git show HEAD:<路径>`:**不要**用 `git checkout` 取版本(会丢弃未提交工作,是铁律),
也别拿工作区文件当「改前基线」——队友可能正在改它。
**后续项(本轮不做)**:① 把「剥注释词法扫描」收进 `scripts/` 当门禁(需用户点头,并先定 `test/` 是否纳入
`coding-standard` 的口径——该钩子现在 `exclude` 了 `test/`);② `test/host/iec104/` 里「`TC-` 直接接三位序号、不带模块码」这类**简写形态**
归族(属编号登记与可追溯性改动,牵 `id_registry.json`,不能顺手做);③ `runner .py` 的中文 docstring **不动**
(口径是 `.c/​.h`,且 Python 源不经 ARMCC 代码页,翻它无工程收益)。
**给下一批的可复用手法与坑**(完整清单写在各套件 `.c` 文件头,这里只留跨模块通用的):
1. **测试 TU 里 `#define fputc <改名符号>` 再纳入生产源**:`App/log/console_log.c`:492 的
`int fputc(int, FILE*)` 与宿主 CRT 撞符号(`libucrt.lib(fputc.obj) error LNK2005` + `LNK1169`)。
改名纳入后函数体与行覆盖一字不动;顺带实测确认**宿主 printf 不走用户 `fputc`**(测试输出不会被吞进环形缓冲)。
@@ -660,3 +743,4 @@ P2 5 个 2,226 行、P3 29 个 4,814 行明确不做**(四条对得上:13+5+
绕过 netTask、`console_log.c` 冷却哨兵与空位判据),**生产码一行未改**,逐条登记在 §9「P2 落地」表内待裁定;
§8 第 1 条同步改为「清单已吃穿」 |
| 2026-10-24 | **第 1 轮修复(同日续)**:用户裁定「全部修」,11 条现状里 **10 条已处置 + 1 条撤销**(`net_recv_locked` 那条是误报:非阻塞分支已能报 EOF,`:1885` 核过)。生产码改动跨 `sys_log_mcu.c`(整批回投 / 文案与节流 / 删不可达截断支 / 新增 `s_is_selfheal_code` 收敛自愈触发集 / post 两层留痕节流)、`soe_store.c`(注释更正 + 触发集同源更保守子集)、`cfg_file_store_mcu.c`(`FR_NO_SYSTEM` 不存在)、`sys_log.h`(读侧容量契约)、`net_socket.c/.h/net_types.h`(`s_addr` 统一到 POSIX 网络序 + UDP 两入口执行任务守卫,新码 `NET_ERR_NOT_IN_NETTASK = -12`)、`net_socket_sim.c`(同步 + 有意不对称)、`console_log.c/.h`(冷却独立标志 / 空位判据收进临界区 / 未初始化分诊 / 三本账 + 新读口)。**`@build` 0 错 0 警**(`Code=213720 RO=18860 RW=1204 ZI=131500`);全量闸 **33/33 全绿**、`5605/6275(89.3%)`、`396/429(92.3%)`、逐文件比对**下降 0**。设计文档同步:`控制台单写者日志设计说明.md`(§4.1 口径 + §13-O2 销项 + v1.5)、`运行与自检记录设计说明.md`(§4.4/§4.5 四条)、`STM32F4-Base存储架构说明.md`(§10.1 第 10 条推论)、`CH395F_Trap_Records.md`(TRAP-NET-21 后续)、`BSD_Socket_API_使用指南.md`(新增 §9.0 唯一约定)。本轮新增处置表、后续项 5 条、台架复验 7 条、新踩坑 8 条见 §9 |
| 2026-10-24 | **第 2 轮:测试字面量统一纯 ASCII**(单独提交):用户裁定「统一」,口径 = AGENTS 铁律「字符串字面量必须纯 ASCII」(无 BOM 文件里奇数个汉字会吞掉结束引号),中文只写注释。四名工人四组互斥目录并行,25 套 / 30 个 `.c` 的非 ASCII 字面量全部改成等值英文。粗口径普查 34 文件/1,552 行、严格口径(剥注释后取字面量)30 文件/**1,535** 行——差值那 4 个文件的命中全在注释里(`ns_peers.c`、`net_socket/stub/ns_rtos.h`、`common/ch395f_fake.h`、`common/fatfs_fake.c`),按口径**不改**。**Lead 独立终检五条硬判据**(TC 号集合不变 / 断言宏计数不变 / **行数与 HEAD 相等** / 代码区非 ASCII=0 / 字节卫生)→ **30 文件漂移 0**;每套断言数与 `--module` 覆盖率复量逐字不变(`net_socket` 887/889·53/53、`console_log` 199/200·16/16、`sys_log_mcu` 324/326·13/13、`adc_input` 103/103、`sys_clock` 100/100…),生产码零改动故基线不需重登。⚠ `@build` 对 `test/host/**` 无覆盖力,本轮证据只能来自 `*-unit-test` + `test-cppcheck` + `test-coverage`。方法、两版事故与协作纪律见 §9「第 2 轮」小节 |