From 794906cdc8a1a46579127ee53fe956db1164873a Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sat, 24 Oct 2026 17:54:02 +0800 Subject: [PATCH 1/4] =?UTF-8?q?test(host):=20P2=20=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=8E=87=E6=89=B9=E6=AC=A1=EF=BC=8C5=20=E5=A5=97=E4=B8=BB?= =?UTF-8?q?=E6=9C=BA=E5=8D=95=E6=B5=8B=E4=B8=8E=E5=85=B1=E4=BA=AB=E5=81=87?= =?UTF-8?q?=20FatFs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P2 五个文件全部进分母(三名队友并行,写范围互斥、零越界): net_socket 420 条 / 881-883=99.8%、52-52 函数(S1~S6 分片) sys_log_mcu 75 条 / 310-313=99.0%、12-12 console_log 104 条 / 188-189=99.5%、15-15 cfg_file_store_mcu 34 条 / 97-97=100%、10-10 soe_store 26 条 / 78-79=98.7%、4-4 合计 659 条断言,P2 本体 1554-1561=99.6%、93-93 函数。 账:套件 28→33 全绿;files_tested 32→37;未触及 112→107(.c 34→29, 恒等式 29 = 0(P1剩) + 0(P2) + 29(P3) 咬合,§9 终点判据达成);分母 4684→6245 插桩行;line 85.8%→89.3%;func 90.1%→92.2%(这批 5 个文件 全都以 ≥98.7% 进分母,故未摊薄)。--update-baseline 前后逐文件比对: 下降 0 个文件。 基建:新建 test/host/common/fatfs_fake.{h,c}(以真实 ff.h 为声明源、只 定义三文件实际调用的 9 个 f_*,缺哪个链接期响;卷忙/短写/FR_DENIED 可注入;短名违规分档);扩展 ch395f_fake 为纯新增(g_fx/g_fxchip 另开 一份世界 + ext_reset 挂进既有复位,既有三套面一字不动),并按真头修正 两个错值常量(SOCKET_OPEN 0x01→0x05、TCP_ESTABLISHED 0x05→0x04), 另在 net_socket runner 加真头/假件同名常量文本核对闸(负例验证过)。 存量回归:net_listener 34、net_events 34、net_pending 43、net_select 22、 nand_port 149、lftpd_io 33、vol_lock 33、soe 32、cfg_file 151 全绿。 编号:补 TC-HOST-{NETSOCK,SYSLOG,CFGFST,SOESTORE,CONLOG} 五族(22→27, 先补族再引号);net_socket 模块码由自取的两字母 NS 正名为 NETSOCK; id-check 增量拦截无台账外新账,可判定比例 83.27%(基线 82.6%)。 文档:覆盖率口径 §9 新增「P2 落地」(含两套分母口径差、10 条现状表、 12 条可复用手法与并发红线),§8 第 1 条改「清单已吃穿」,补变更记录。 本提交未改任何生产代码(App/Drivers/Src/Lib/MDK-ARM 零触碰)。 10 条现状已逐条登记在 §9(含 sys_log_mcu.c:509~537 卷忙整批丢日志、 net_socket.c 同文件两套 IP 字节序、UDP 两 API 绕过 netTask),已获指示 全部修复,另开一轮(含测试字面量统一纯 ASCII)。 gate: pre-commit run --all-files → 39 hooks, 38 Passed; only mem-numbers Failed(本工作树从未 @build,无 .map 事实源,属分支期已知陷阱); mem-budget Passed;test-coverage 与 id-check Passed。RAM/Flash 增量 = 0 (纯测试批次)。按仓内分支约定走 --no-verify,台账回填留待主树合并后。 --- docs/测试说明/主机单测覆盖率口径.md | 152 +- scripts/id_registry.json | 110 ++ scripts/test_coverage_baseline.json | 121 +- .../cfg_file_store_mcu_test.c | 666 +++++++ test/host/cfg_file_store_mcu/run_unit_test.py | 68 + test/host/cfg_file_store_mcu/stub/FreeRTOS.h | 37 + test/host/common/ch395f_fake.c | 405 ++++- test/host/common/ch395f_fake.h | 203 ++- test/host/common/fatfs_fake.c | 1077 ++++++++++++ test/host/common/fatfs_fake.h | 149 ++ test/host/console_log/console_log_test.c | 922 ++++++++++ test/host/console_log/run_unit_test.py | 62 + test/host/console_log/stub/FreeRTOS.h | 16 + test/host/console_log/stub/cmsis_os.h | 13 + test/host/console_log/stub/cmsis_os2.h | 18 + test/host/console_log/stub/stm32f4_fake.h | 274 +++ test/host/console_log/stub/task.h | 16 + test/host/console_log/stub/usart.h | 20 + test/host/net_socket/net_socket_test.c | 657 +++++++ test/host/net_socket/ns_peers.c | 379 ++++ test/host/net_socket/ns_peers.h | 82 + test/host/net_socket/ns_rtos.c | 334 ++++ test/host/net_socket/ns_s3_msg.c | 613 +++++++ test/host/net_socket/ns_s4_state.c | 346 ++++ test/host/net_socket/ns_s5_io.c | 609 +++++++ test/host/net_socket/ns_s6_edges.c | 395 +++++ test/host/net_socket/run_unit_test.py | 161 ++ test/host/net_socket/stub/FreeRTOS.h | 40 + test/host/net_socket/stub/ch395f.h | 10 + test/host/net_socket/stub/cmsis_os.h | 14 + test/host/net_socket/stub/dbg_log.h | 23 + test/host/net_socket/stub/main.h | 35 + test/host/net_socket/stub/net_latency_test.h | 9 + test/host/net_socket/stub/ns_rtos.h | 84 + test/host/net_socket/stub/task.h | 32 + test/host/net_socket/stub/test_config.h | 11 + test/host/soe_store/run_unit_test.py | 74 + test/host/soe_store/soe_store_test.c | 708 ++++++++ test/host/soe_store/stub/cmsis_os.h | 91 + test/host/soe_store/stub/dbg_log.h | 30 + test/host/soe_store/stub/main.h | 34 + test/host/sys_log_mcu/run_unit_test.py | 72 + test/host/sys_log_mcu/stub/cmsis_os.h | 91 + test/host/sys_log_mcu/stub/dbg_log.h | 29 + test/host/sys_log_mcu/stub/main.h | 34 + test/host/sys_log_mcu/sys_log_mcu_test.c | 1534 +++++++++++++++++ 46 files changed, 10821 insertions(+), 39 deletions(-) create mode 100644 test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c create mode 100644 test/host/cfg_file_store_mcu/run_unit_test.py create mode 100644 test/host/cfg_file_store_mcu/stub/FreeRTOS.h create mode 100644 test/host/common/fatfs_fake.c create mode 100644 test/host/common/fatfs_fake.h create mode 100644 test/host/console_log/console_log_test.c create mode 100644 test/host/console_log/run_unit_test.py create mode 100644 test/host/console_log/stub/FreeRTOS.h create mode 100644 test/host/console_log/stub/cmsis_os.h create mode 100644 test/host/console_log/stub/cmsis_os2.h create mode 100644 test/host/console_log/stub/stm32f4_fake.h create mode 100644 test/host/console_log/stub/task.h create mode 100644 test/host/console_log/stub/usart.h create mode 100644 test/host/net_socket/net_socket_test.c create mode 100644 test/host/net_socket/ns_peers.c create mode 100644 test/host/net_socket/ns_peers.h create mode 100644 test/host/net_socket/ns_rtos.c create mode 100644 test/host/net_socket/ns_s3_msg.c create mode 100644 test/host/net_socket/ns_s4_state.c create mode 100644 test/host/net_socket/ns_s5_io.c create mode 100644 test/host/net_socket/ns_s6_edges.c create mode 100644 test/host/net_socket/run_unit_test.py create mode 100644 test/host/net_socket/stub/FreeRTOS.h create mode 100644 test/host/net_socket/stub/ch395f.h create mode 100644 test/host/net_socket/stub/cmsis_os.h create mode 100644 test/host/net_socket/stub/dbg_log.h create mode 100644 test/host/net_socket/stub/main.h create mode 100644 test/host/net_socket/stub/net_latency_test.h create mode 100644 test/host/net_socket/stub/ns_rtos.h create mode 100644 test/host/net_socket/stub/task.h create mode 100644 test/host/net_socket/stub/test_config.h create mode 100644 test/host/soe_store/run_unit_test.py create mode 100644 test/host/soe_store/soe_store_test.c create mode 100644 test/host/soe_store/stub/cmsis_os.h create mode 100644 test/host/soe_store/stub/dbg_log.h create mode 100644 test/host/soe_store/stub/main.h create mode 100644 test/host/sys_log_mcu/run_unit_test.py create mode 100644 test/host/sys_log_mcu/stub/cmsis_os.h create mode 100644 test/host/sys_log_mcu/stub/dbg_log.h create mode 100644 test/host/sys_log_mcu/stub/main.h create mode 100644 test/host/sys_log_mcu/sys_log_mcu_test.c diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index ae236eb0..f07eff21 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -208,10 +208,13 @@ **下一步(按性价比排序)**: -1. **扩大覆盖面**:按 **§9 的建桩优先级清单**吃。**P1 已吃完**(2026-10-24 合入主干,13 个 .c 全部 - 进分母,`files_tested` 19 → 32);下一步是 **P2 的 5 个 / 2,226 行**(`net_socket.c` 1,251 行需分片、 - `sys_log_mcu`+`cfg_file_store_mcu`+`soe_store` 共用一套假 FatFs、`console_log`)。P3 的 29 个 / - 4,814 行**刻意不做**并已写明理由。比把已覆盖文件从 85% 推到 95% 值钱得多。 +1. **扩大覆盖面**:按 **§9 的建桩优先级清单**吃 —— **清单已吃穿**:P1 13 个 `.c`(`files_tested` 19 → 32) + 与 P2 5 个 `.c`(`files_tested` 32 → **37**,实测见 §9「P2 落地」)都已进分母,**剩下的 29 个 `.c` + 全部是 P3「刻意不做」**、每条都能在 P3 表里找到归属(本节终点判据就此达成)。于是这一条**没有下一批可排**, + 只剩两类事:① 把 §9 登记为本批「现状」的条目按用户裁定落成 fix(体量最大的一块是 + `App/log/sys_log_mcu.c` 卷忙时整批丢弃日志);② 真想把百分比再往上推,就只能回头补**已进分母文件**的 + 未测分支(缺口最大的三块:`lftpd_io.c` 53.5%、`net_pending.c` 87.1%、`lftpd.c` 64.3%)—— + 按 §5 那仍是自分数、不是 KPI,性价比低于 ①。 2. **不做**:第二条覆盖率通道(gcc/WSL、`dotnet-coverage`、OpenCppCoverage 三条路都不走,用户裁定 2026-10-23)。要重开这个裁定,前提是能同时给出"两份分母怎么对齐"的答案,否则只是多一本对不上的账。 @@ -415,6 +418,137 @@ uint16_t ch395f_link_change_count(void); 3. **`console_log.c`(265)** —— 单写者契约 K1/K2 已经写成表,照抄成用例最省力: `docs/子系统设计/控制台单写者日志设计说明.md`。 +### P2 落地(2026-10-24,5 个文件全部进分母,套件 28 → 33 全绿) + +三个模块码并行落地(`net_socket` 分片吞、存储三件套共用新建假 FatFs、`console_log` 独立), +**新增 5 套 659 条断言**:`net_socket` 420、`console_log` 104、`sys_log_mcu` 75、`cfg_file_store_mcu` 34、`soe_store` 26。 + +| 新纳入分母的 `.c` | 插桩行 / 函数 | 断言 | 套件目录 | +|---|---|---|---| +| `net_socket.c` | **881/883 = 99.8%**、52/52 | 420 | `test/host/net_socket/` | +| `sys_log_mcu.c` | **310/313 = 99.0%**、12/12 | 75 | `test/host/sys_log_mcu/` | +| `console_log.c` | **188/189 = 99.5%**、15/15 | 104 | `test/host/console_log/` | +| `cfg_file_store_mcu.c` | **97/97 = 100%**、10/10 | 34 | `test/host/cfg_file_store_mcu/` | +| `soe_store.c` | **78/79 = 98.7%**、4/4 | 26 | `test/host/soe_store/` | +| **P2 合计** | **1,554/1,561 = 99.6%**、93/93 | 659 | — | + +**账**:套件 28 → **33** 全绿、`files_tested` 32 → **37**、未触及 **112 → 107**(`.c` **34 → 29**、`.h` 仍 78)、 +分母 4,684 → **6,245** 插桩行,`line=4020/4684(85.8%)` → **`5574/6245(89.3%)`**、 +`func=300/333(90.1%)` → **`393/426(92.2%)`**。恒等式跟着更新且仍然咬合:`29 = 0(P1 剩) + 0(P2) + 29(P3)`, +并且**本节终点判据(下面「顺序建议与记账」第 3 条)就此达成**:剩下的 29 条 `.c` 逐条都能在 P3 那张表里找到归属 +(芯片驱动 5 + UI 17 + 任务编排 6 + 内核前装配 1 = 29),没有一条是"既没进分母、又说不清为什么不测"的真窟窿。 + +⚠ **这批罕见地没有摊薄、反而 +3.5pp** —— 不是因为更勤奋,而是 5 个文件**全都以 ≥98.7% 进分母** +(P1 那批是 `lftpd_io.c` 53.5%、`lftpd.c` 64.3% 这种"半份进分母")。**别把它读成"百分比才是 KPI"**: +按 §5 口径这批该盯的仍是 `files_tested` 32→37 与未触及 `.c` 34→29 这两个数,89.3% 只是自分数。 + +**两套分母别再混用**(写给下次估工作量的人):本节清单里的"有效代码行"是**去注释/去空行**的静态计数, +插桩分母只数 mscoverage 认为可执行的行——`net_socket.c` 清单 1,251 / 插桩 **883**、`sys_log_mcu.c` 458 / **313**、 +`console_log.c` 265 / **189**、`cfg_file_store_mcu.c` 129 / **97**、`soe_store.c` 123 / **79**。 +按 1,251 估进度会把 99.8% 读成 70.4%。另:`for (;;)` 的收尾大括号会被算进可执行行 +(`soe_store.c:202` 就是这种非真语句),别当成漏测去"补"。 + +**画像期两处口径被落地推翻**(就地更正,不回改上文原话):① "⚠ **没有 `ch395f.h`**——NET 域那批必须新建它" +已由 P1 建好(`test/host/net_listener/stub/ch395f.h` 转发到共享假芯片);② "FatFs 侧**不建假头**,沿用真实 +`ff.h` + 测试单元内假 diskio"——P2 走的是**另一条并存的路**:`test/host/common/fatfs_fake.{h,c}` 以真实 +`Lib/FatFs/ff.h` 为**声明源**、在 `fatfs_fake.c` 里**定义** `f_*`(签名一漂移就编译期报错), +提供内存卷 + 可注入返回码/短写/`FR_DENIED`。两条路的共同点是**都不自造假头**。假件**刻意只提供三个生产文件 +实际调用的那 9 个 `f_*`**(`f_close f_lseek f_mkdir f_open f_read f_rename f_stat f_unlink f_write`), +不给 `f_mount/f_mkfs/f_opendir/f_readdir/f_sync` —— 于是"自愈逻辑把名字/参数类错误码升级成卷操作"这类违规 +会在**链接期**就响,而不是靠评审眼神(`docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 陷阱二的机器化兜底)。 + +**编号**:`TC-HOST-*` 族 22 → **27**(本批补 `NETSOCK`/`SYSLOG`/`CFGFST`/`SOESTORE`/`CONLOG`, +游标分别 `TC-NETSOCK-545`/`TC-SYSLOG-100`/`TC-CFGFST-053`/`TC-SOESTORE-024`/`TC-CONLOG-135`, +属主登记处 `test/host/<目录>/*`,`cursor_owner_only`/`allow_orphans`/`subitem_ok` 照 B 层口径)。 +**先补族再引号**这条链本批又走了一遍,而且踩在收口前:`net_socket` 起初自取两字母码 `TC-NS-*`(465 处), +那会整批落进通用 `TC` 族(属主 `docs/测试说明/*_Test_Guide.md`、**无** `allow_orphans`) += 458 条台账外新账直接拦提交;正名为 `TC-NETSOCK-` 是 bytes 级替换(465 处 / 0 残留 / 兄弟目录 `git diff` 全空 / +复跑 420 全绿)。**写死成纪律**:队友起手就按 Lead 给的模块码写号,两字母不可检索的码(`NS`)不要取。 +本批**未**给新套件单挂 `*-unit-test` 钩子(与 P1 那 12 套同口径):聚合闸 `test-coverage` 的触发面 +`^(App/|Drivers/BSP/|test/host/|…)` 已自动发现新模块并按基线守回退。 + +**假件常量必须与真头逐字对齐(本批最值钱的机制结论)**:共享假芯片里 `CH395F_SOCKET_OPEN` 曾是 `0x01U` +(真头 `Drivers/BSP/CH395F/ch395f.h`:220 = **`0x05U`**、`docs/芯片手册/CH395F.md`:527)、 +`CH395F_TCP_ESTABLISHED` 曾是 `0x05U`(真头:229 = **`0x04U`**、手册:539)。 +**三套存量单测一条都没红**——host 编译里生产码看到的也是假宏,宏对宏自洽 → 错在"值层", +表现正是"把断言写在假件规则上而门禁毫无反应"。修法不是改断言而是**加机械闸**: +`test/host/net_socket/run_unit_test.py` 在编译之前做真头/假件同名 `CH395F_*` 常量的文本核对 +(真头 106 项 / 假件 32 项 / 同名 22 项,只比纯字面量取值),并做过负例验证:把值改回 `0x01U` → +输出 `[DRIFT] CH395F_SOCKET_OPEN 真=0x05 假=0x01` 且 rc=1 拒绝编译。跨头 `_Static_assert` 做不到, +文本核对是目前唯一能常驻的拦法。**推论**:任何"照抄真头常量"的共享假件都该配这样一根闸。 + +**两条 FatFs 实证陷阱都做了正反两面**:`FA_OPEN_APPEND` 不含 `FA_WRITE`(缺写位 → `f_write` 恒 +`FR_DENIED` 的对照保真断言 3 处)与"`FR_OK` + 短写"判失败必须校验写回字节数;短名违规**分档** +(体 > 8 / 扩展 > 3 / 非法字符 / 以点开头 → `FR_INVALID_NAME`;含 `:` → `FR_INVALID_DRIVE`), +`FF_USE_LFN=0` 下 `.fschk` 走的正是"扩展域 5 字符超限"这条真实路径;自愈只动自己的文件 +(反复失败 20 轮后 `/log` 目录与邻居文件未被触碰)。 + +**本批照出的现状(只登记、未改生产码;逐条已上报用户待裁定)**: + +| # | 位置 | 现象与影响 | 钉住它的用例 | +|---|---|---|---| +| 1 | `App/log/sys_log_mcu.c:509~537` | **疑似数据丢失(建议按 P1 定级)**:批量路径 `done = n` 只有 `rc==-3`/`rc==-1` 两个分支会改它,**`rc==-2`(卷忙)没有分支** → 紧随其后的 `for (i = done; i < n; …)` 回投循环一次都不执行 → 最多 8 条日志既不落盘也不回投、不计故障、不打日志。同段 `:531` 注释与 `docs/子系统设计/运行与自检记录设计说明.md` §4.4 都写"条目不丢(旧实现 -2 丢那一条)"→ **现状比旧实现更糟**(旧丢 1 条、现一次丢 8 条)。对照:**逐条慢路径遇 `-2` 会正确回投尾部**,同一错误码在两条路径行为相反。触发面常见:卷锁 v1.2 之后 ftpTask 传文件期间 defaultTask 每 2 s 排空一次 | TC-SYSLOG-035 / 035b / 035c | +| 2 | `App/log/sys_log_mcu.c:527~529` | `rc==-1` 打 `DBG_ERROR("drop %lu entries …")`,但该分支 `done=0` → 整批其实**全部回投**、一条没丢 → 文案与行为相反,且持续写故障时每轮 drain 都重投重试一遍 | TC-SYSLOG-036 | +| 3 | `App/log/sys_log_mcu.c:616` vs `627~628` | 守卫写成 `desc_len <= SYSLOG_DESC_MAX` 就 `-1`,而 `dl` 恒 ≤ 28 → 截断分支**永不可达**。无害(宁可拒绝不越界),但"传小缓冲能拿到截断内容"的直觉是错的 | TC-SYSLOG-098 / 057 | +| 4 | `Drivers/BSP/NET/net_socket.c:2825` / `:2869` / `:3080` | **同一文件两套 IP 字节序约定,`inet_addr` 与 `ntoa`/`getsockname` 不互逆**:`net_inet_addr` 小端组装后又 `NET_HTONL` → `0xC0A80164`;`net_inet_ntoa` 交换后高段在前 → 只能正确还原 `getsockname` 那族(`0x6401A8C0`);`net_connect_locked:1571` 用 `>>24` 取首段 → 只配 `inet_addr` 那族。**为什么长期不被看见**:`ntoa` 自己 `:2865~2867` 那段 E13 修复注释里的例子「192.168.1.100 → 0xC0A80164 → "192.168.1.100"」取的是**另一族**的值(交换宏下实算 `"100.1.168.192"`)。前提已核清:`Drivers/BSP/NET/net_types.h`:56 是 `#ifdef __BIG_ENDIAN` 才恒等,STM32 与 x86/MSVC 都走交换分支 → **不存在"主机语义≠目标语义"**。当前无调用者跨族(`lftpd.c`:864 走 `getpeername→ntoa` 同族恰好正确),属埋在库 API 面上的地雷;统一必须跨 `inet_addr`/`ntoa`/`getsockname`/`connect` 解析/`lftpd.c`:548 一起过 | TC-NETSOCK-100b / 100c / 100d、107f、430h(钉的是**具体数值**,统一时会五条一起红并互相指认,不会被误读成单点回归) | +| 5 | `Drivers/BSP/NET/net_socket.c:2408` / `:2537` | **UDP 两个公开 API 绕过 netTask 串行化**:`net_sendto`/`net_recvfrom` 直接 `net_poll()`(`:2482`/`:2501`/`:2563`)+ `ch395f_write_send_buf`(`:2487`)/`ch395f_read_recv_buf`(`:2575`/`:2604`),**全程不经 `netMsgQueue`** → 与 AGENTS「所有 `net_*` 经 netTask 串行执行」及 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-14 正面冲突。全仓无生产调用者(只有 `test/board` 与模拟器)所以未爆。建议:改走消息队列,或在 `net_socket.h` 显式标注"未串行化,仅限 netTask" | TC-NETSOCK-440 | +| 6 | `App/log/soe_store.c` 函数头 ④ | 注释写"打开/写入结构性错误 → `unlink` 重试一次(同 run.log)",实现里 `f_open` 失败**直接 return -1、不重试**(只有 `f_write` 失败才重试);且 `run.log` 侧 `s_append_rec` 的重试码集合还含 `FR_INVALID_NAME`/`FR_NO_PATH` → 两套自愈集合不一致。属"注释夸大实现"还是"实现少一半"待裁 | TC-SOESTORE-022 | +| 7 | `App/cfg/cfg_file_store_mcu.c:50` | 函数头写"未挂载时 `f_open` 返回 `FR_NO_SYSTEM`"——**`FRESULT` 里没有这个码**(`Lib/FatFs/ff.h` 共 20 个码,未挂载实际是 `FR_NOT_READY`/`FR_NO_FILESYSTEM`)。只在注释里不参与编译,危害是后人照它写判据会写出永不成立的分支 | —(注释面) | +| 8 | `App/log/console_log.c:317` vs `:285` | 冷却起点用 `s_con_cool_until = (uint32_t)HAL_GetTick()`,而判据拿 `!= 0U` 当哨兵 → 故障恰好落在 `HAL_GetTick()==0` 那一拍(开机头 1 ms,或 49.7 天回绕到 0)时**冷却等于没记**,下一轮 drain 立刻再耗一次 100 ms 块超时。撞的正是控制台域那条已登记的"背靠背块超时拖长主循环 → 喂狗节奏饥饿"。建议:另用一个"在冷却"标志,别拿 tick 值当哨兵 | TC-CONLOG-081 / 082(按现状钉) | +| 9 | `App/log/console_log.c:394~412` | `con_append` 的**空位判据在 `taskENTER_CRITICAL` 之外**(check-then-act)。生产者按 `docs/子系统设计/控制台单写者日志设计说明.md` §3.1 是"全部任务 + 内核前主线程 + ISR",而同文档 §4.1 的 head/tail 行写作"单生产者/单写者"——**两处口径互斥,后者需要更正**。多生产者可在环 >95% 满时同时过判据 → `head` 各自推进后合计越过容量 → `con_used()` 回绕变小 → 写者少发一段,被覆盖的行**既不打 `[ERR]` 也不进 `dropped`**(静默损坏;索引恒取模,不会越界)。**这条没有用例、只报推断**:单线程宿主构造不出真抢占点(把注入点挂在 `taskENTER_CRITICAL` 上已在区内,不算抢占点) | —(无用例,仅登记) | +| 10 | `App/log/console_log.c:181~183` + `:306~331` | `huart1.Instance == NULL`(HAL 早期 / 断言-故障上报路径的专用短路,返回 `HAL_ERROR`)在 drain 里被当成**僵尸通道**走 `con_uart_reinit()` → `MX_USART1_UART_Init()`(含 MspInit 的 RCC/GPIO 重配)。当前接线顺序(`MX_USART1_UART_Init` 早于 `dbg_console_writer_attach`)挡住了,但"未初始化"与"通道卡死"共用一条自愈支路,attach 时机一旦被提前就会在 HAL 未就绪时重配外设 | TC-CONLOG-103(钉现状) | + +**给下一批的可复用手法与坑**(完整清单写在各套件 `.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`**(测试输出不会被吞进环形缓冲)。 +2. **钩寄存器读点不必改生产码**:判据里的掩码宏是桩宏 → 定义成**函数调用**就钩住每一次 `u->SR` 位读; + `#define DWT (con_dwt_touch(), &g_dwt)` 让 `CYCCNT` 每次访问自走。于是 + `CON_TX_TIMEOUT_CYC = SystemCoreClock/10` 这类编译期表达式原样不动,也能把"自旋几次超时"变成可断言量。 + ⚠ 结构体成员访问本身(`u->SR`/`u->DR`)钩不到,别绕进去。 +3. **跑飞保险要挂在被测模块的天然计数点上**:`net_send_locked`/`net_sendto`/`net_recvfrom` 的等待环里 + **没有 `osDelay`**,只给 `osDelay` 加保险挡不住这些死循环(实测第一次跑就真卡死)。挂在 `net_poll()` + 每轮必读的 `HAL_GPIO_ReadPin`(INT#)上,附带让 `NET_SEND_SPIN_MS` 这类有界自旋预算**第一次可量测**。 + 同一纪律的另一半:假内核对 `xTaskNotifyWait(portMAX_DELAY)` 泵 50 轮仍无通知就 `exit(2)` 打 `FATAL` —— + 它当场抓出一处把 `net_close` 返回语义判错的用例,而不是让门禁卡死。 +4. **未插桩全绿、插桩后 `0xC0000005`** 基本都是测试自己的越界读:假 UART 的上线字节日志是二进制缓冲没补 + `NUL` 就被 `strstr` 读穿;以及把 192 B 栈数组当 2048 B 源传进 `con_append`(它只钳写入长度、不钳读取范围)。 + 守则:凡是把缓冲当 C 字符串读的地方先补 `NUL` 或改 `memcmp` 定长比。 +5. **注入槽的"未命中"不能用默认值判**:`FF_FAKE_ACT_RC` 恰好 `= 0` = 变量默认值 → `f_open` 误走注入分支、 + 返回"`FR_OK` 但没建句柄",表现是十几条断言莫名红。要显式 `hit` 标志 + `act` 双条件。同类:假卷锁/注入若按 + "第 N 次调用"取值,会被场景前几步消耗掉下标 → 注入看着设了其实没生效;改成"接下来 N 次返回忙"计数器 + + `skip` 参数才能表达"取锁前后各 `f_stat` 一次"这种锁后二次校验分支。 +6. **假卷每文件簇池必须大于生产单文件容量上限**(`run.log`/`soe.log` = 1024×64 = 65,536 B)。原来给 2,048 → + "容量满 `unlink` 重建"场景里追加位落在池外、`f_write` 恒 0 字节短写,**轮转分支根本跑不到**(一片红且看不出根因)。 +7. **共享调用日志会被长轮询挤掉**:假芯片 `g_chip.log` 只有 96 格,一次 `sock_reset` 的几十轮把更早的顺序信息 + 挤出可查范围 → 顺序断言假绿/假红。做顺序断言前先 `clear_log`、用 `log_find` 查。 +8. **电平型条件位 ≠ 一次性脉冲**:`SINT` 的 `SENBUF_FREE` 在多块发送时从第二块起还要再看到,一次性位图会让 + "写完一块再也等不到空闲"变成假死;同类 `rst_keep_open`(造"永远关不掉"的芯片)用来量上层 600 ms 复位收口, + 否则那两条超时分支永远量不到。**新旋钮必须默认关**,存量套件行为才逐字不变。 +9. **读操作也会占槽**:`net_get_errno()` 内部同样走 `net_errno_slot()`,首次读就分配槽位并进一次临界区 → + "只有写才占槽"这类直觉断言会红。这是真实语义,不是缺陷。 +10. **注释与字符串的两颗哑雷**:块注释文本里写 `hton*/ntoh*` 里的 `*/` 会**提前闭合注释**,后面中文裸露成代码, + 刷出几十条 `C3872『U+3001 不允许作标识符』`,长得像编码乱码;字符串字面量里内嵌 ASCII 双引号(强调中文词) + → 字符串提前截断,报一串 `C2143`,根因完全看不出来。要引用一律全角「」。另:用 heredoc / + `python - <`:并行期每人只跑 `--module <自己模块>` + (子目录互不冲突);**绝不加 `--clean`**(它 `rmtree` 整棵共享根,会把队友正在编译的目录连根删掉, + 表现为"莫名其妙的编译失败、重跑就好");**不带 `--module` 的全量闸只在收口时由 Lead 单跑**。 + 另注:逐套 `run_unit_test.py` 写的是固定 `tempfile/_obj` 与 `.exe`,所以**同一套件同一时刻 + 只能一人跑**(不同套件互不干扰)。 + +**两处口径问答(免得下次重新推导)**:① 子集数不能和并集基线直接比 —— `soe_store` 单套对 +`App/log/soe_rec.c` 只到 78/87(89.7%),而台账是全部 33 套的**并集** 81/87(93.1%);本批 +`--update-baseline` 前后逐文件比对**下降文件数 = 0**,所以那不是回退。判据只有一条:全量闸的并集 ≥ 基线。 +② `console_log.c` 与 `soe_store.c` 那 1~2 行未覆盖都不是漏测:`console_log.c:352` 是 `snprintf` 溢出后的 +钳位(`note[64]`,最长 42 B → 数学上到不了)、`soe_store.c:202` 是 `for (;;)` 收尾大括号。 + ### P3|**明确不做**(把理由写死,免得下次又被列回待办:29 个 `.c` = 4,814 行) | 类别 | 体量 | 为什么不做 | 那它靠什么把关 | @@ -462,3 +596,13 @@ uint16_t ch395f_link_change_count(void); 逻辑度=1−硬件符号密度 / 要桩的外部头 / 可复用现有桩 / fix 提交数),分为 **P1 13 个 2,617 行、 P2 5 个 2,226 行、P3 29 个 4,814 行明确不做**(四条对得上:13+5+29=47、三段相加=9,657); §8 下一步第 1 条改为指向本节 | +| 2026-10-24 | **P2 落地(三名队友并行,本分支)**:新增 5 套 **659 条断言**(`net_socket` 420 / + `console_log` 104 / `sys_log_mcu` 75 / `cfg_file_store_mcu` 34 / `soe_store` 26),套件 28 → **33** 全绿、 + `files_tested` 32 → **37**、未触及 `.c` **34 → 29**(恒等式 `29 = 0(P1) + 0(P2) + 29(P3)`,清单吃穿)、 + 分母 4,684 → **6,245**、`line` 85.8% → **89.3%**、`func` 90.1% → **92.2%**(这批**没有摊薄**:5 个文件 + 全都以 ≥98.7% 进分母)。新建共享假 FatFs `test/host/common/fatfs_fake.{h,c}`、共享假 CH395F 纯扩展 + (两个与真头不符的常量改成真值 + 加真头/假件常量文本核对闸,负例验证过)。补 `TC-HOST-*` 五族 + (22 → 27)与 `scripts/test_coverage_baseline.json` 重登(逐文件比对:下降 0 个)。照出 **10 条现状** + (重点:`sys_log_mcu.c:509~537` 卷忙整批丢日志、`net_socket.c` 同文件两套 IP 字节序、UDP 两个公开 API + 绕过 netTask、`console_log.c` 冷却哨兵与空位判据),**生产码一行未改**,逐条登记在 §9「P2 落地」表内待裁定; + §8 第 1 条同步改为「清单已吃穿」 | diff --git a/scripts/id_registry.json b/scripts/id_registry.json index 5d704efa..c9a60b6f 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1549,6 +1549,116 @@ "owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓", "cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌" }, + { + "name": "TC-HOST-NETSOCK", + "class": "TC", + "title": "主机侧单测用例号:CH395F socket 栈核心(fd 表分配/跨上下文消息/错误码经通知值回传/监听门控)(net_socket)", + "pattern": "\\bTC-NETSOCK-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/net_socket/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-NETSOCK-545", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓", + "cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌" + }, + { + "name": "TC-HOST-SYSLOG", + "class": "TC", + "title": "主机侧单测用例号:运行与自检日志存储(定长记录 + 批量写 + 读窗口)(sys_log_mcu)", + "pattern": "\\bTC-SYSLOG-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/sys_log_mcu/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-SYSLOG-100", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓", + "cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌" + }, + { + "name": "TC-HOST-CFGFST", + "class": "TC", + "title": "主机侧单测用例号:配置文件存储引擎(落盘与回读校验)(cfg_file_store_mcu)", + "pattern": "\\bTC-CFGFST-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/cfg_file_store_mcu/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-CFGFST-053", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓", + "cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌" + }, + { + "name": "TC-HOST-SOESTORE", + "class": "TC", + "title": "主机侧单测用例号:SOE 事件顺序记录持久化(soe_store)", + "pattern": "\\bTC-SOESTORE-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/soe_store/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-SOESTORE-024", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓", + "cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌" + }, + { + "name": "TC-HOST-CONLOG", + "class": "TC", + "title": "主机侧单测用例号:控制台单写者日志(环形缓冲 + TX 软复位自愈)(console_log)", + "pattern": "\\bTC-CONLOG-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/console_log/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-CONLOG-135", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓", + "cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌" + }, { "name": "TC-HOST-ADCHEALTH", "class": "TC", diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index 30ca48fc..6d54c06b 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -64,6 +64,14 @@ "lines_covered": 572, "lines_valid": 652 }, + "App/cfg/cfg_file_store_mcu.c": { + "func_rate": 1.0, + "funcs_covered": 10, + "funcs_valid": 10, + "line_rate": 1.0, + "lines_covered": 97, + "lines_valid": 97 + }, "App/cfg/prot_set_store.c": { "func_rate": 1.0, "funcs_covered": 4, @@ -80,6 +88,14 @@ "lines_covered": 17, "lines_valid": 17 }, + "App/log/console_log.c": { + "func_rate": 1.0, + "funcs_covered": 15, + "funcs_valid": 15, + "line_rate": 0.9947, + "lines_covered": 188, + "lines_valid": 189 + }, "App/log/soe_rec.c": { "func_rate": 1.0, "funcs_covered": 7, @@ -88,6 +104,22 @@ "lines_covered": 81, "lines_valid": 87 }, + "App/log/soe_store.c": { + "func_rate": 1.0, + "funcs_covered": 4, + "funcs_valid": 4, + "line_rate": 0.9873, + "lines_covered": 78, + "lines_valid": 79 + }, + "App/log/sys_log_mcu.c": { + "func_rate": 1.0, + "funcs_covered": 12, + "funcs_valid": 12, + "line_rate": 0.9904, + "lines_covered": 310, + "lines_valid": 313 + }, "App/protocol/iec104/iec104_asdu.c": { "func_rate": 1.0, "funcs_covered": 10, @@ -240,6 +272,14 @@ "lines_covered": 100, "lines_valid": 117 }, + "Drivers/BSP/NET/net_socket.c": { + "func_rate": 1.0, + "funcs_covered": 52, + "funcs_valid": 52, + "line_rate": 0.9977, + "lines_covered": 881, + "lines_valid": 883 + }, "Drivers/BSP/NETCFG/ftp_cfg.c": { "func_rate": 1.0, "funcs_covered": 4, @@ -280,6 +320,20 @@ "lines_total": 652, "ok": true }, + "cfg_file_store_mcu": { + "files": { + "App/cfg/cfg_file_store_mcu.c": 97 + }, + "lines_total": 97, + "ok": true + }, + "console_log": { + "files": { + "App/log/console_log.c": 189 + }, + "lines_total": 189, + "ok": true + }, "freq": { "files": { "App/algo/freq.c": 130, @@ -390,6 +444,13 @@ "lines_total": 117, "ok": true }, + "net_socket": { + "files": { + "Drivers/BSP/NET/net_socket.c": 883 + }, + "lines_total": 883, + "ok": true + }, "phasor": { "files": { "App/algo/phasor.c": 55, @@ -439,6 +500,14 @@ "lines_total": 190, "ok": true }, + "soe_store": { + "files": { + "App/log/soe_rec.c": 87, + "App/log/soe_store.c": 79 + }, + "lines_total": 166, + "ok": true + }, "sys_clock": { "files": { "App/util/sys_clock.c": 157 @@ -446,6 +515,13 @@ "lines_total": 157, "ok": true }, + "sys_log_mcu": { + "files": { + "App/log/sys_log_mcu.c": 313 + }, + "lines_total": 313, + "ok": true + }, "task_prof": { "files": { "App/util/task_prof.c": 93 @@ -486,9 +562,13 @@ "App/algo/prot_set.c", "App/algo/sample_ring.c", "App/cfg/cfg_file.c", + "App/cfg/cfg_file_store_mcu.c", "App/cfg/prot_set_store.c", "App/fs/vol_lock.c", + "App/log/console_log.c", "App/log/soe_rec.c", + "App/log/soe_store.c", + "App/log/sys_log_mcu.c", "App/protocol/iec104/iec104_asdu.c", "App/protocol/iec104/iec104_link.c", "App/protocol/iec104/iec104_pts.c", @@ -508,27 +588,24 @@ "Drivers/BSP/NET/net_listener.c", "Drivers/BSP/NET/net_pending.c", "Drivers/BSP/NET/net_select.c", + "Drivers/BSP/NET/net_socket.c", "Drivers/BSP/NETCFG/ftp_cfg.c", "Drivers/BSP/NETCFG/net_cfg.c" ], "totals": { "files_generated_excluded": 5, - "files_tested": 32, - "files_untested": 112, - "func_rate": 0.9009, - "funcs_covered": 300, - "funcs_valid": 333, - "line_rate": 0.8582, - "lines_covered": 4020, - "lines_valid": 4684, - "modules_ok": 28, - "modules_total": 28 + "files_tested": 37, + "files_untested": 107, + "func_rate": 0.9225, + "funcs_covered": 393, + "funcs_valid": 426, + "line_rate": 0.8926, + "lines_covered": 5574, + "lines_valid": 6245, + "modules_ok": 33, + "modules_total": 33 }, "untested_sources": [ - { - "code_lines": 1251, - "path": "Drivers/BSP/NET/net_socket.c" - }, { "code_lines": 801, "path": "Drivers/BSP/CH395F/ch395f.c" @@ -537,10 +614,6 @@ "code_lines": 689, "path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c" }, - { - "code_lines": 458, - "path": "App/log/sys_log_mcu.c" - }, { "code_lines": 329, "path": "Drivers/BSP/UI/ui_menu_view.c" @@ -557,10 +630,6 @@ "code_lines": 281, "path": "App/task/default_task.c" }, - { - "code_lines": 265, - "path": "App/log/console_log.c" - }, { "code_lines": 264, "path": "Drivers/BSP/SD2506/sd2506.c" @@ -593,18 +662,10 @@ "code_lines": 135, "path": "App/app/app_main.c" }, - { - "code_lines": 129, - "path": "App/cfg/cfg_file_store_mcu.c" - }, { "code_lines": 126, "path": "Drivers/BSP/UI/ui_menu.c" }, - { - "code_lines": 123, - "path": "App/log/soe_store.c" - }, { "code_lines": 123, "path": "App/task/net_task.c" diff --git a/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c b/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c new file mode 100644 index 00000000..70d5f4b9 --- /dev/null +++ b/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c @@ -0,0 +1,666 @@ +/* + * 模块名称:配置存储后端(cfg_file_store_mcu)主机侧单元测试(L0.5) + * 模块功能:在 PC 上离线验证 App/cfg/cfg_file_store_mcu.c 的五个卷操作 + 三个堆操作: + * cfg_store_read / cfg_store_write_atomic / cfg_store_stat / + * cfg_store_remove / 两个路径 getter / alloc-free-heap_free + * 重心放在**原子写**与它的 v1.2 修复(E-01)上:本 FatFs 移植的 f_rename + * 对已存在目标返回 FR_EXIST、**不覆盖**,所以"覆盖写"必须先 f_unlink 目标 + * 再 f_rename——少了这一步,一切非首启保存(UI 改配置、模板重建)静默失败, + * 而模拟器后端(POSIX rename 覆盖语义)照不出来。 + * + * 被测对象:真实源文件 App/cfg/cfg_file_store_mcu.c(#include 原样纳入)。 + * 假件与桩: + * test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明; + * 它的 f_rename **如实返回 FR_EXIST**,正是 E-01 的那颗雷) + * stub/FreeRTOS.h —— 三个分配器原型(签名逐条对齐 portable.h:163/164/166) + * 假卷锁 / 假堆(可注入分配失败)写在本文件里 + * + * 为什么要主机侧单测(而不是只靠台架): + * 1. **E-01 原子写**:unlink→rename 这个非原子窗口是"掉电时配置暂失、开机按首启 + * 重建工厂模板"的有意取舍。任何一次"顺手优化"(比如给 rename 加覆盖、或把 + * unlink 挪到 rename 之后)都会让配置保存静默失败或留下脏 TMP,而这在 POSIX + * 后端上完全看不出来。→ 假卷把 FR_EXIST 原样喂进来。 + * 2. **失败不留半写**:TMP 写失败/短写必须 unlink TMP 且**不碰目标**——"目标文件 + * 保持不动"是函数头承诺,也是现场"改坏配置还能开机"的唯一保证。 + * 3. **卷锁 try 语义**:v1.3 把四个后端函数全部纳入 vol_lock_wait(0);写反了就是 + * "defaultTask 与 ftpTask 并发写 FTL → dhara journal 竞争"(实证过)或者 + * "UI 保存时死等卷 → 喂狗点阻塞 → 整机复位"。四个函数的 try/release 配对 + * 只能靠计数断言。 + * 4. **mtime 令牌的分钟粒度盲区**:cfg_file 的 poll 靠"令牌不等 = 可能变更", + * 同一分钟内同长度重写令牌不变 → 变更检不出来。这是设计接受的限制,但必须 + * 钉在测试里,否则后人会以为令牌是内容哈希。 + * 5. **MicroLIB 512B C 堆**:cfg_store_alloc 是共享核心唯一的取内存入口 + * (优化调研 P0 第 1 条),分配失败必须如实返回 NULL;宿主 CRT 堆永远不会失败 → 只有假堆能测。 + * + * 覆盖点(TC 号族 = CFGFST(三位序号)): + * —— 读 —— + * TC-CFGFST-001 读成功:内容与长度正确,打开模式恒为 FA_READ(不含写位) + * TC-CFGFST-002 p_buf / p_nread 空指针 -> -1 且**不取卷锁** + * TC-CFGFST-003 卷忙 -> -1、零 FatFs、不 release(try 失败没有锁可放) + * TC-CFGFST-004 文件缺失 -> -1 **且已 release** + * TC-CFGFST-005 cap 大于文件 -> br = 实际大小(部分读不是错误) + * TC-CFGFST-006 f_read 失败 -> -1 + release + close 已配对 + * TC-CFGFST-007 短读(介质只回一部分)-> 返回 0 + nread 如实变小(现状钉住) + * —— 原子写 —— + * TC-CFGFST-010 首启写:TMP 建 -> 写 -> unlink(目标,FR_NO_FILE 容忍) -> rename + * TC-CFGFST-011 **覆盖写必须先 unlink 目标**:假卷 rename 对已存在目标返 FR_EXIST, + * 少了 unlink 就是 -1 且旧内容原封不动(E-01 的直接复现) + * TC-CFGFST-012 rename 失败 -> -1 + TMP 清理 + 目标内容保持不动 + * TC-CFGFST-013 TMP 打开失败 -> -1、零 write、release + * TC-CFGFST-014 写短(FR_OK + 字节数不足)-> -1 + TMP 被删 + 目标原内容不动 + * TC-CFGFST-015 len=0 -> 成功(清空配置是合法语义),目标变 0 字节 + * TC-CFGFST-016 卷忙 -> -1 且整条链零 FatFs + * TC-CFGFST-017 p_buf NULL -> -1 且不取锁 + * TC-CFGFST-017b p_path NULL -> 由 s_build_tmp_path 的空指针守卫挡住 + * TC-CFGFST-018 临时名由目标推导:/config.ini -> /config.TMP;/cfg/a.ini -> + * /cfg/config.TMP(同目录,避免跨目录改名) + * TC-CFGFST-019 目标目录过长 -> s_build_tmp_path 返回 -1(守卫在取锁之前) + * TC-CFGFST-020 TMP 的打开模式必须含 FA_WRITE(|FA_CREATE_ALWAYS)——陷阱一在本 + * 模块同样成立:只给 FA_CREATE_ALWAYS 的句柄 f_write 恒 FR_DENIED + * —— 状态与删除 —— + * TC-CFGFST-030 stat:size + mtime = (fdate<<16)|ftime,两次写之间令牌必变 + * TC-CFGFST-031 盲区:同一 dostime、同长度重写 -> 令牌不变(分钟粒度,非内容哈希) + * TC-CFGFST-032 stat:文件缺失 / p_st NULL / 卷忙 三种 -1 与 release 配对 + * TC-CFGFST-033 remove:成功;缺失与卷忙的 -1 与 release 配对 + * —— 路径与 8.3 —— + * TC-CFGFST-040 config_path / tmp_path 返回值在严格 8.3 卷上真的打得开 + * TC-CFGFST-041 短名违规分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME; + * 含 ':' -> FR_INVALID_DRIVE(假件保真,也是"改名=静默失败"的防线) + * TC-CFGFST-042 陷阱二:非法名只让本次操作失败,**不触发任何卷级动作** + * (条目总数不变、目录不没、不留 TMP) + * —— 堆 —— + * TC-CFGFST-050 alloc/free 走 FreeRTOS 堆:字节数增减可观测、成对归还 + * TC-CFGFST-051 分配失败注入 -> cfg_store_alloc 返回 NULL(MicroLIB 512B 的形态) + * TC-CFGFST-052 cfg_store_free(NULL) 安全(不计数、不崩) + * TC-CFGFST-053 heap_free 原样转发 xPortGetFreeHeapSize + * + * 本套照出的现状(只登记、不改生产代码;进交 Lead 的报告): + * cfg_file_store_mcu.c:50 的函数头写"卷已挂载(未挂载时 f_open 返回 + * FR_NO_SYSTEM,转 -1)"——**FRESULT 里根本没有 FR_NO_SYSTEM 这个码** + * (Lib/FatFs/ff.h 的枚举是 FR_OK..FR_INVALID_PARAMETER 共 20 个,未挂载卷 + * 在本移植里实际返回 FR_NOT_READY / FR_NO_FILESYSTEM)。这条只写在注释里、 + * 不参与编译,所以一直没暴露;危害是后人照它写判据时会写出一个永不成立的 + * 分支(本模块实际也没写:它把所有非 FR_OK 一律转 -1,因此运行结果正确)。 + * + * 故意没测(不留"看起来全覆盖"的错觉): + * - **真掉电**:unlink 与 rename 之间断电图象只能靠"目标缺失 -> 上层按首启重建" + * 的既有单测 + 真机验证;本套只钉"窗口存在"(TC-CFGFST-011 里 unlink 恰好 1 次)与 + * "失败路径不碰目标"。 + * - 目录不存在时的 f_open 行为差异(FR_NO_PATH)在 cfg 侧只体现在 -1,与缺失同 + * 出口,已并入 TC-CFGFST-004 的语义,不再单独造一条重复断言。 + * - 卷未挂载(真实 ff.c 的 FR_NOT_READY / FR_NO_FILESYSTEM):假卷不提供 f_mount + * 概念——这三个模块的代码里也确实一次都没调 f_mount(挂载属 defaultTask), + * 留给 default_task 那一层的集成验证。 + * - cfg_file.c 共享核心(928 行)不在本批:它有自己的存量单测口径。 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2) + */ + +#include +#include +#include +#include + +#include "ff.h" +#include "fatfs_fake.h" +#include "FreeRTOS.h" /* 桩:三个分配器原型 */ +#include "cfg_file_store.h" /* 真实头 */ +#include "vol_lock.h" /* 真实头 */ + +/* ===================== 断言框架 ===================== */ + +static int s_total; +static int s_passed; +static int s_failed; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +#define P_CFG "/config.ini" +#define P_TMP "/config.TMP" + +/* ===================== 假件与生产源码纳入 ===================== */ + +#include "fatfs_fake.c" +#include "cfg_file_store_mcu.c" + +/* ===================== 假卷锁 ===================== */ + +static int s_lock_busy_left; +static int s_lock_waits; +static int s_lock_releases; +static int s_lock_held; +static int s_lock_seq_err; + +static void locks_clear(void) +{ + s_lock_busy_left = 0; + s_lock_waits = 0; + s_lock_releases = 0; + s_lock_held = 0; + s_lock_seq_err = 0; +} + +static void lock_busy_next(int n) { s_lock_busy_left = n; } + +int vol_lock_init(void) { return 0; } + +int vol_lock_wait(uint32_t timeout_ms) +{ + int r; + + (void)timeout_ms; + if (s_lock_waits > 20000) { + (void)fprintf(stderr, "[FATAL] vol_lock_wait guard tripped\n"); + exit(2); + } + r = (s_lock_busy_left > 0) ? 0 : 1; + if (s_lock_busy_left > 0) { + s_lock_busy_left--; + } + s_lock_waits++; + if (r != 0) { + s_lock_held = 1; + } + return r; +} + +void vol_lock_release(void) +{ + s_lock_releases++; + if (s_lock_held == 0) { + s_lock_seq_err++; /* try 失败却 release = 释放了别人的锁 */ + } + s_lock_held = 0; +} + +/* ===================== 假 FreeRTOS 堆(可注入失败) ===================== */ + +#define FAKE_HEAP_CAP 4096u + +static uint8_t s_heap_pool[FAKE_HEAP_CAP]; +static uint32_t s_heap_used; +static int s_heap_fail; /* 1 = 下次 pvPortMalloc 直接返回 NULL */ +static int s_heap_alloc_calls; +static int s_heap_free_calls; +static int s_heap_free_err; /* free 了指针之外的地址 = 分配器被写脏 */ + +static void heap_clear(void) +{ + s_heap_used = 0u; + s_heap_fail = 0; + s_heap_alloc_calls = 0; + s_heap_free_calls = 0; + s_heap_free_err = 0; +} + +void *pvPortMalloc(size_t xWantedSize) +{ + void *p; + + if (s_heap_alloc_calls++ > 20000) { + (void)fprintf(stderr, "[FATAL] pvPortMalloc guard tripped\n"); + exit(2); + } + if ((s_heap_fail != 0) || (xWantedSize == 0u) + || ((s_heap_used + (uint32_t)xWantedSize) > FAKE_HEAP_CAP)) { + return NULL; + } + p = (void *)(&s_heap_pool[s_heap_used]); + s_heap_used += (uint32_t)xWantedSize; + return p; +} + +void vPortFree(void *pv) +{ + if (s_heap_free_calls++ > 20000) { + (void)fprintf(stderr, "[FATAL] vPortFree guard tripped\n"); + exit(2); + } + if (pv == NULL) { + return; + } + if (((uint8_t *)pv < &s_heap_pool[0]) + || ((uint8_t *)pv >= &s_heap_pool[FAKE_HEAP_CAP])) { + s_heap_free_err++; /* 释放栈地址/野指针 */ + return; + } + /* 只支持栈式使用(本模块的实际用法:借一块、用完还) */ + s_heap_used = (uint32_t)((uint8_t *)pv - &s_heap_pool[0]); +} + +size_t xPortGetFreeHeapSize(void) +{ + return (size_t)(FAKE_HEAP_CAP - s_heap_used); +} + +/* ===================== 复位 ===================== */ + +static void reset_all(void) +{ + ff_fake_reset(); + locks_clear(); + heap_clear(); + ff_fake_set_dostime(0x5A21u, 0x6E40u); +} + +/* ===================== 读 ===================== */ + +static void tc_read(void) +{ + uint8_t buf[64]; + size_t n = 999u; + int ok; + + printf("-- TC-CFGFST-001..007 读 --\n"); + + /* 001 成功 + 只读模式 */ + reset_all(); + (void)ff_fake_mkfile(P_CFG, (const uint8_t *)"net ip=192.168.1.100\n", 20u); + ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 20u) + && (memcmp(buf, "net ip=192.168.1.100\n", 20u) == 0) + && (g_ff_obs.last_open_mode == (BYTE)FA_READ) + && (s_lock_waits == 1) && (s_lock_releases == 1) && (s_lock_seq_err == 0); + CHECK(ok, "TC-CFGFST-001 读成功:内容与长度正确,打开模式就是 FA_READ(不带写位)"); + + /* 002 空指针守卫在取锁之前 */ + reset_all(); + ok = (cfg_store_read(P_CFG, NULL, sizeof(buf), &n) == -1) + && (cfg_store_read(P_CFG, buf, sizeof(buf), NULL) == -1); + CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), + "TC-CFGFST-002 空指针 -> -1 且不取卷锁(守卫顺序正确)"); + + /* 003 卷忙 */ + reset_all(); + lock_busy_next(1); + ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 0) + && (s_lock_seq_err == 0), + "TC-CFGFST-003 卷忙 -> -1:零 FatFs、不 release(没拿到锁不放锁)"); + + /* 004 文件缺失:必须已放锁 */ + reset_all(); + ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1); + CHECK(ok && (s_lock_releases == 1) && (s_lock_seq_err == 0), + "TC-CFGFST-004 文件缺失 -> -1 **且已释放卷锁**(漏放 = 之后所有卷操作全瘫)"); + + /* 005 cap > 文件大小 */ + reset_all(); + (void)ff_fake_mkfile(P_CFG, (const uint8_t *)"abc", 3u); + n = 999u; + ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 3u) + && (g_ff_obs.last_read_btr == 64u) && (g_ff_obs.last_read_br == 3u); + CHECK(ok, "TC-CFGFST-005 要 64 只给 3 -> nread=3 且仍算成功(短读不是错误)"); + + /* 006 f_read 失败 */ + reset_all(); + (void)ff_fake_mkfile(P_CFG, (const uint8_t *)"abc", 3u); + ff_fake_inject_rc(P_CFG, FF_FAKE_OP_READ, FR_DISK_ERR, 1); + ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1) + && (s_lock_releases == 1) && (s_lock_seq_err == 0), + "TC-CFGFST-006 读失败 -> -1 + close 已配对 + 放锁"); + + /* 007 介质短读:现状是"返回 0 + nread 如实变小",调用方靠 nread 定长度 */ + reset_all(); + (void)ff_fake_mkfile(P_CFG, (const uint8_t *)"0123456789", 10u); + ff_fake_inject_read_short(P_CFG, 4u, 1); + n = 999u; + ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 4u); + CHECK(ok, "TC-CFGFST-007 注入短读 -> 返回 0、nread=4(上层按 nread 截断使用)"); +} + +/* ===================== 原子写 ===================== */ + +static void tc_write_atomic(void) +{ + uint8_t buf[64]; + size_t n; + int ok; + + printf("-- TC-CFGFST-010..020 原子写(E-01 核心)--\n"); + + /* 010 首启写(目标不存在) */ + reset_all(); + ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"v1", 2u) == 0); + CHECK(ok && (ff_fake_size(P_CFG) == 2u) && (ff_fake_exists(P_TMP) == 0) + && (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 1) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) /* unlink 目标:FR_NO_FILE 无害 */ + && (strcmp(g_ff_obs.last_rename_old, "/CONFIG.TMP") == 0) + && (strcmp(g_ff_obs.last_rename_new, "/CONFIG.INI") == 0) + && (s_lock_releases == 1) && (s_lock_seq_err == 0), + "TC-CFGFST-010 首启写:TMP -> rename -> 目标 2 字节、TMP 不留痕"); + + /* 011 覆盖写:假卷的 f_rename 对已存在目标返回 FR_EXIST —— 这就是 E-01 */ + reset_all(); + ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"old-content", 11u) == 0); + { + int unlinks_before = g_ff_obs.calls[FF_FAKE_OP_UNLINK]; + ok = ok && (cfg_store_write_atomic(P_CFG, (const uint8_t *)"new", 3u) == 0); + CHECK(ok && (ff_fake_size(P_CFG) == 3u) + && (ff_fake_fetch(P_CFG, buf, 0u, sizeof(buf)) == 3u) + && (memcmp(buf, "new", 3u) == 0) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlinks_before + 1), + "TC-CFGFST-011 覆盖写:unlink 目标恰一次再 rename -> 新内容落地(E-01)"); + } + /* 同一条的"反面":直接 rename 到已存在目标必败(证明上一步的 unlink 不是多余的) */ + { + static FIL probe; + FRESULT fr; + + (void)ff_fake_mkfile(P_TMP, (const uint8_t *)"x", 1u); + fr = f_rename(P_TMP, P_CFG); + CHECK(fr == FR_EXIST, + "TC-CFGFST-011b 假件保真:目标已存在时 f_rename 返回 FR_EXIST、不覆盖"); + if (fr == FR_OK) { + (void)f_close(&probe); + } + } + + /* 012 rename 失败 -> TMP 清理 + 目标不动 */ + reset_all(); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"keep-me", 7u); + ff_fake_inject_rc(P_TMP, FF_FAKE_OP_RENAME, FR_DENIED, 1); /* 键 = 旧路径 */ + ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"replace", 7u) == -1); + /* 目标在 rename 之前已被 unlink(非原子窗口),所以这里钉的是: + * TMP 一定被清掉、锁一定释放、绝不留半成品 */ + CHECK(ok && (ff_fake_exists(P_TMP) == 0) && (ff_fake_exists(P_CFG) == 0) + && (s_lock_releases == 2) && (s_lock_seq_err == 0), + "TC-CFGFST-012 rename 失败 -> -1 + TMP 清理 + 放锁( unlink→rename 窗口如实存在)"); + + /* 013 TMP 打不开 */ + reset_all(); + (void)ff_fake_mkfile(P_CFG, (const uint8_t *)"orig", 4u); + ff_fake_inject_rc(P_TMP, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1); + ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"new", 3u) == -1); + CHECK(ok && (ff_fake_size(P_CFG) == 4u) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0) + && (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 0) + && (s_lock_releases == 1) && (s_lock_seq_err == 0), + "TC-CFGFST-013 TMP 打开失败 -> -1 且**目标原内容一个字节没动**"); + + /* 014 短写 = 失败:TMP 被删、目标不动、不 rename */ + reset_all(); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"good-config", 11u); + /* 本次要写 11 字节,注入"只回 5"才算真短 */ + ff_fake_inject_write_short(P_TMP, 5u, 1); + ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"bad-content", 11u) == -1); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 1) + && (ff_fake_exists(P_TMP) == 0) + && (ff_fake_size(P_CFG) == 11u) + && (ff_fake_fetch(P_CFG, buf, 0u, sizeof(buf)) == 11u) + && (memcmp(buf, "good-config", 11u) == 0) + && (s_lock_seq_err == 0), + "TC-CFGFST-014 TMP 短写判失败 -> 删 TMP、不 rename、目标仍是旧内容"); + + /* 015 len = 0 合法(清空配置) */ + reset_all(); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"x", 1u); + ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"", 0u) == 0); + n = 999u; + CHECK(ok && (ff_fake_size(P_CFG) == 0u) + && (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 0u), + "TC-CFGFST-015 写 0 字节成功(清空配置),回读 nread=0"); + + /* 016 卷忙:整条链零 I/O */ + reset_all(); + (void)ff_fake_mkfile(P_CFG, (const uint8_t *)"orig", 4u); + lock_busy_next(1); + ok = (cfg_store_write_atomic(P_CFG, (const uint8_t *)"new", 3u) == -1); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) + && (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 0) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0) + && (ff_fake_size(P_CFG) == 4u) && (s_lock_releases == 0), + "TC-CFGFST-016 卷忙 -> -1:不建 TMP、不动目标、不 release"); + + /* 017 p_buf NULL */ + reset_all(); + ok = (cfg_store_write_atomic(P_CFG, NULL, 3u) == -1); + CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), + "TC-CFGFST-017 p_buf NULL -> -1 且不取锁"); + + /* 017b 目标路径本身是 NULL:s_build_tmp_path 的空指针分支(读函数有守卫, + * 写函数**没有** p_path 的显式守卫,全靠推导函数把它挡在取锁之前) */ + reset_all(); + (void)ff_fake_mkfile(P_CFG, (const uint8_t *)"keep", 4u); + ok = (cfg_store_write_atomic(NULL, (const uint8_t *)"x", 1u) == -1); + CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) + && (ff_fake_size(P_CFG) == 4u), + "TC-CFGFST-017b p_path NULL -> -1:不取锁、不开文件、目标不动"); + + /* 018 临时名由目标推导(同目录) */ + reset_all(); + (void)ff_fake_mkdir_direct("/cfg"); + ok = (cfg_store_write_atomic("/cfg/a.ini", (const uint8_t *)"z", 1u) == 0); + CHECK(ok && (strcmp(g_ff_obs.last_open_path, "/CFG/CONFIG.TMP") == 0) + && (ff_fake_exists("/cfg/config.TMP") == 0) /* 已被 rename 走,不是留档 */ + && (strcmp(g_ff_obs.last_rename_new, "/CFG/A.INI") == 0) + && (ff_fake_size("/cfg/a.ini") == 1u), + "TC-CFGFST-018 目标是 /cfg/a.ini -> 临时文件落在同目录 /cfg/config.TMP"); + CHECK(strcmp(cfg_store_tmp_path(), P_TMP) == 0, + "TC-CFGFST-018b getter 仍是卷根 /config.TMP(与 s_path_tmp 同源)"); + + /* 019 目录太长 -> 推导失败,且发生在取锁之前 */ + reset_all(); + ok = (cfg_store_write_atomic("/this/is/a/very/deep/dir/name/x.ini", + (const uint8_t *)"y", 1u) == -1); + CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), + "TC-CFGFST-019 临时名缓冲放不下 -> -1 且不取锁(不拿半截路径去开文件)"); + + /* 020 陷阱一在本模块同样成立 */ + reset_all(); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"m", 1u); + CHECK((g_ff_obs.last_open_mode & FA_WRITE) != 0u, + "TC-CFGFST-020 写 TMP 的句柄必带 FA_WRITE(这里用 CREATE_ALWAYS,不是 APPEND)"); + reset_all(); + { + static FIL probe; + UINT bw = 7u; + FRESULT fr1; + FRESULT fr2; + + fr1 = f_open(&probe, P_TMP, FA_CREATE_ALWAYS); /* 故意漏掉 FA_WRITE */ + fr2 = f_write(&probe, "abc", 3u, &bw); + (void)f_close(&probe); + CHECK((fr1 == FR_OK) && (fr2 == FR_DENIED) && (bw == 0u), + "TC-CFGFST-020b 只给 FA_CREATE_ALWAYS -> 打开成功但 f_write 恒 FR_DENIED"); + } +} + +/* ===================== stat / remove ===================== */ + +static void tc_stat_remove(void) +{ + cfg_file_stat_t st; + uint32_t tok1; + uint32_t tok2; + int rel; + int ok; + + printf("-- TC-CFGFST-030..033 状态令牌与删除 --\n"); + + /* 030 size + 令牌 */ + reset_all(); + ff_fake_set_dostime(0x5A21u, 0x6E40u); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"abcd", 4u); + tok1 = ((0x5A21u << 16) | 0x6E40u); + ok = (cfg_store_stat(P_CFG, &st) == 0) && (st.size == 4u) && (st.mtime == tok1); + ff_fake_set_dostime(0x5A22u, 0x6E40u); /* 次日同一时刻 */ + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"efgh", 4u); + tok2 = ((0x5A22u << 16) | 0x6E40u); + ok = ok && (cfg_store_stat(P_CFG, &st) == 0) && (st.mtime == tok2) + && (tok1 != tok2); + CHECK(ok && (s_lock_seq_err == 0) && (s_lock_releases == s_lock_waits), + "TC-CFGFST-030 令牌 = (fdate<<16)|ftime,日期进位一定换令牌"); + + /* 031 盲区:同一分钟、同长度重写 -> 令牌不变 */ + reset_all(); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"aaaa", 4u); + (void)cfg_store_stat(P_CFG, &st); + ok = (st.size == 4u); + { + uint32_t before = st.mtime; + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"bbbb", 4u); + (void)cfg_store_stat(P_CFG, &st); + CHECK(ok && (st.size == 4u) && (st.mtime == before), + "TC-CFGFST-031 同分钟同长度重写 -> 令牌不变(poll 检不出,设计已接受)"); + } + + /* 032 stat 三种 -1 */ + reset_all(); + rel = s_lock_releases; + ok = (cfg_store_stat(P_CFG, NULL) == -1) && (s_lock_releases == rel); /* 守卫在取锁前 */ + ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel + 1); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"x", 1u); + rel = s_lock_releases; + lock_busy_next(1); + ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel); + rel = s_lock_releases; + ff_fake_inject_rc(P_CFG, FF_FAKE_OP_STAT, FR_DISK_ERR, 1); + ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel + 1) + && (s_lock_seq_err == 0); + CHECK(ok, "TC-CFGFST-032 stat:NULL 守卫/缺失/卷忙/读错误四种出口的放锁配对"); + + /* 033 remove */ + reset_all(); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"x", 1u); + rel = s_lock_releases; + ok = (cfg_store_remove(P_CFG) == 0) && (ff_fake_exists(P_CFG) == 0) + && (s_lock_releases == rel + 1); + ok = ok && (cfg_store_remove(P_CFG) == -1) /* 再删:文件已不在 */ + && (s_lock_releases == rel + 2); + lock_busy_next(1); + ok = ok && (cfg_store_remove(P_CFG) == -1) && (s_lock_releases == rel + 2) + && (s_lock_seq_err == 0); + CHECK(ok, "TC-CFGFST-033 remove:成功 / 已不存在的 -1 / 卷忙的 -1 与放锁配对"); +} + +/* ===================== 路径与 8.3(陷阱二) ===================== */ + +static void tc_names(void) +{ + static FIL probe; + uint8_t rbuf[16]; + size_t rn; + cfg_file_stat_t st_dummy; + uint32_t ents_before; + int rel; + FRESULT fr_body; + FRESULT fr_ext; + FRESULT fr_bad; + FRESULT fr_dot; + FRESULT fr_colon; + int ok; + + printf("-- TC-CFGFST-040..042 路径合规与「名字错不升级成卷操作」 --\n"); + + /* 040 生产路径真的可用(改名即静默失败的第一道防线) */ + reset_all(); + ok = (strcmp(cfg_store_config_path(), P_CFG) == 0) + && (f_open(&probe, cfg_store_config_path(), FA_WRITE | FA_CREATE_ALWAYS) == FR_OK) + && (f_close(&probe) == FR_OK) + && (f_open(&probe, cfg_store_tmp_path(), FA_WRITE | FA_CREATE_ALWAYS) == FR_OK) + && (f_close(&probe) == FR_OK); + CHECK(ok, "TC-CFGFST-040 /config.ini 与 /config.TMP 都是合法 8.3,真能在卷上建出来"); + + /* 041 短名违规分档 */ + reset_all(); + fr_body = f_open(&probe, "/configlooooong.ini", FA_WRITE | FA_CREATE_ALWAYS); + if (fr_body == FR_OK) { (void)f_close(&probe); } + fr_ext = f_open(&probe, "/config.ini2", FA_WRITE | FA_CREATE_ALWAYS); + if (fr_ext == FR_OK) { (void)f_close(&probe); } + fr_bad = f_open(&probe, "/conf?ig.ini", FA_WRITE | FA_CREATE_ALWAYS); + if (fr_bad == FR_OK) { (void)f_close(&probe); } + fr_dot = f_open(&probe, "/.fschk", FA_WRITE | FA_CREATE_ALWAYS); + if (fr_dot == FR_OK) { (void)f_close(&probe); } + fr_colon = f_open(&probe, "1:/config.ini", FA_READ); + if (fr_colon == FR_OK) { (void)f_close(&probe); } + CHECK((fr_body == FR_INVALID_NAME) && (fr_ext == FR_INVALID_NAME) + && (fr_bad == FR_INVALID_NAME) && (fr_dot == FR_INVALID_NAME) + && (fr_colon == FR_INVALID_DRIVE), + "TC-CFGFST-041 分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME;':' -> FR_INVALID_DRIVE"); + + /* 042 名字类错误码绝不升级成卷操作:非法目标名走一遍原子写 */ + reset_all(); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"keep", 4u); + ents_before = (uint32_t)ff_fake_count_entries(); + rn = 0u; + rel = s_lock_releases; + ok = (cfg_store_read("/.fschk", rbuf, sizeof(rbuf), &rn) == -1) + && (cfg_store_stat("/.fschk", &st_dummy) == -1) + && (cfg_store_remove("/.fschk") == -1); + CHECK(ok && ((uint32_t)ff_fake_count_entries() == ents_before) + && (ff_fake_size(P_CFG) == 4u) && (s_lock_seq_err == 0) + && (s_lock_releases == rel + 3), + "TC-CFGFST-042 非法名只让本次调用失败:条目数不变、已有配置不动、锁把把放过"); + /* 非法目标名进原子写:推导出的 TMP 也非法 -> 第一步开文件就失败, + * 绝不能把已经存在的目标删掉 */ + reset_all(); + (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"keep", 4u); + ents_before = (uint32_t)ff_fake_count_entries(); + ok = (cfg_store_write_atomic("/badname-tooooo-long.ini", (const uint8_t *)"x", 1u) == -1); + CHECK(ok && (ff_fake_size(P_CFG) == 4u) + && ((uint32_t)ff_fake_count_entries() == ents_before), + "TC-CFGFST-042b 非法目标名的原子写整条失败,**不碰已有配置文件**"); +} + +/* ===================== 堆 ===================== */ + +static void tc_heap(void) +{ + void *p; + void *q; + + printf("-- TC-CFGFST-050..053 分配后端(优化调研 P0 第 1 条)--\n"); + + /* 050 成对归还 */ + reset_all(); + p = cfg_store_alloc(256u); + (void)cfg_store_free(p); + CHECK((p != NULL) && (s_heap_alloc_calls == 1) && (s_heap_free_calls == 1) + && (s_heap_used == 0u) && (s_heap_free_err == 0), + "TC-CFGFST-050 alloc/free 走 FreeRTOS 堆且成对归还(借多少还多少)"); + + /* 051 分配失败如实返回 NULL */ + reset_all(); + s_heap_fail = 1; + p = cfg_store_alloc(2048u); + q = cfg_store_alloc(8u); + cfg_store_free(p); /* NULL 必须安全 */ + CHECK((p == NULL) && (q == NULL) && (s_heap_free_calls == 0), + "TC-CFGFST-051 堆见底 -> cfg_store_alloc 返回 NULL(MicroLIB 512B 的真形态)"); + + /* 052 free(NULL) 安全 + 不计数 */ + reset_all(); + cfg_store_free(NULL); + CHECK((s_heap_free_calls == 0) && (s_heap_free_err == 0), + "TC-CFGFST-052 cfg_store_free(NULL) 不触碰分配器"); + + /* 053 heap_free 转发 */ + reset_all(); + (void)cfg_store_alloc(100u); + CHECK(cfg_store_heap_free() == (FAKE_HEAP_CAP - 100u), + "TC-CFGFST-053 cfg_store_heap_free 原样转发 xPortGetFreeHeapSize"); +} + +/* ===================== 入口 ===================== */ + +int main(void) +{ + printf("=== Cfg File Store Mcu Tests ===\n"); + + tc_read(); + tc_write_atomic(); + tc_stat_remove(); + tc_names(); + tc_heap(); + + printf("\n=== Cfg File Store Mcu Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/cfg_file_store_mcu/run_unit_test.py b/test/host/cfg_file_store_mcu/run_unit_test.py new file mode 100644 index 00000000..f9d2b382 --- /dev/null +++ b/test/host/cfg_file_store_mcu/run_unit_test.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +r"""配置存储后端(cfg_file_store_mcu)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c(以 + #include 纳入**生产源** App/cfg/cfg_file_store_mcu.c,并纳入共享假卷 + test/host/common/fatfs_fake.c),在 PC 上离线跑读、原子写(E-01 的 + unlink->rename 全链)、状态令牌、删除、路径 8.3 合规、假堆分配,失败即非 0 退出。 + 工具链与判定委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只用台架): + 1. **E-01 原子写**:本仓 FatFs 移植的 f_rename 对**已存在目标返回 FR_EXIST、不 + 覆盖**,所以覆盖写必须先 unlink 目标。模拟器后端(POSIX rename 覆盖语义) + 永远照不出这一条 → 曾经"一切非首启保存静默失败"。假卷按真实 ff.c 返回 + FR_EXIST,把这条做成必然失败的断言。 + 2. **失败不留半写**:TMP 打开失败 / 短写 / rename 失败三条路径都必须"目标原内容 + 一个字节没动 + TMP 清理干净 + 卷锁已释放",这是现场"改坏配置还能开机"的前提。 + 3. **卷锁 try 语义(v1.3)**:四个后端函数全在 vol_lock_wait(0) 区间内;写反了 + 就是与 ftpTask 并发写 FTL(dhara journal 竞争实证过),或 UI 保存死等卷把 + 喂狗点阻塞死。只能靠调用计数断言。 + 4. **mtime 令牌是 FAT 打包时间、不是内容哈希**:同一分钟同长度重写检不出变更, + 这个盲区必须钉在测试里,否则后人会拿它当哈希用。 + 5. **MicroLIB C 堆只有 512 B**(startup Heap_Size=0x200):cfg_store_alloc 是 + 共享核心唯一取内存入口,分配失败必须如实返回 NULL —— 宿主 CRT 堆永不失败。 + +桩的边界:ff.h / cfg_file_store.h / vol_lock.h 用**真实头**;FreeRTOS.h 是桩 + (真实头会拉整棵 portmacro),三条原型逐字对齐 portable.h:163/164/166。 + FatFs 由 test/host/common/fatfs_fake.c 给 RAM 实现(**显式纳入,不用目录通配**, + 避免被卷进别人的构建);假卷锁与假堆强符号写在本测试单元里。 + +用法(仓库根目录): + & '.venv\Scripts\python.exe' test/host/cfg_file_store_mcu/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:docs/子系统设计/配置文件设计说明.md §6.2(mtime 令牌)、 +docs/架构设计/STM32F4-Base存储架构说明.md §10.1 第 10/11 条、 +docs/测试说明/主机单测覆盖率口径.md §9 P2 第 2 项 +创建日期:2026-10-24 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 必须最先(覆盖 FreeRTOS.h),其余全用真实头。 +# 假卷 fatfs_fake.c 由测试单元 #include 纳入(同 ch395f_fake.c 惯例,显式列文件、不通配)。 +BUILD = TestBuild( + name="Cfg File Store Mcu Tests", + src=os.path.join(HERE, "cfg_file_store_mcu_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "test", "host", "common"), + os.path.join(ROOT, "App", "cfg"), + os.path.join(ROOT, "App", "fs"), + os.path.join(ROOT, "Lib", "FatFs"), + HERE, + ], + summary_marker="=== Cfg File Store Mcu Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/cfg_file_store_mcu/stub/FreeRTOS.h b/test/host/cfg_file_store_mcu/stub/FreeRTOS.h new file mode 100644 index 00000000..fd2e66e4 --- /dev/null +++ b/test/host/cfg_file_store_mcu/stub/FreeRTOS.h @@ -0,0 +1,37 @@ +/* + * 模块名称:主机侧单测桩 - FreeRTOS.h(cfg_file_store_mcu 套件专用) + * 模块功能:App/cfg/cfg_file_store_mcu.c 只经这个头拿三个分配器原型 + * (pvPortMalloc / vPortFree / xPortGetFreeHeapSize),它自己不碰任何内核 + * API——卷是 vol_lock 的事、卷操作是 ff.h 的事。真实 FreeRTOS.h 会拉整棵 + * portmacro + 编译器抽象层,PC 上编不动,所以只给这三条原型。 + * + * ⚠ 仅供 test/host/cfg_file_store_mcu/ 使用,不参与 MCU 构建。 + * + * 签名逐条对齐真实头(改一处即编译期报错,这就是桩存在的意义): + * Middlewares/Third_Party/FreeRTOS/Source/include/portable.h:163 + * void *pvPortMalloc( size_t xSize ) PRIVILEGED_FUNCTION; + * 同文件 :164 + * void vPortFree( void *pv ) PRIVILEGED_FUNCTION; + * 同文件 :166 + * size_t xPortGetFreeHeapSize( void ) PRIVILEGED_FUNCTION; + * (真实实现 Middlewares/.../portable/MemMang/heap_4.c:315 定义 xPortGetFreeHeapSize) + * PRIVILEGED_FUNCTION 在 pc 端口展开为空,桩里不写等价。 + * + * 实现放在测试单元里,是一份**可注入失败的假堆**:真机上 MicroLIB 的 C 堆只有 + * 512 B(startup Heap_Size=0x200),配置引擎的 KB 级临时缓冲必然走 FreeRTOS 堆, + * 堆见底时 pvPortMalloc 返回 NULL —— 这条分支模拟器(宿主 CRT 堆)永远走不到, + * 只有假堆能稳定构造(cfg_store_alloc 是共享核心的唯一取内存入口,必须测)。 + * + * 创建日期:2026-10-24 + */ + +#ifndef FREERTOS_H +#define FREERTOS_H + +#include + +void *pvPortMalloc(size_t xWantedSize); +void vPortFree(void *pv); +size_t xPortGetFreeHeapSize(void); + +#endif /* FREERTOS_H */ diff --git a/test/host/common/ch395f_fake.c b/test/host/common/ch395f_fake.c index 717b490f..e74e0c60 100644 --- a/test/host/common/ch395f_fake.c +++ b/test/host/common/ch395f_fake.c @@ -9,8 +9,13 @@ #include "ch395f_fake.h" +/* 时基:假复位机的最短沉降窗要用(与真实 main.h 同名同签名,各测试 TU 自带实现) */ +uint32_t HAL_GetTick(void); + ch395f_fake_world_t g_fake; ch395f_fake_obs_t g_chip; +ch395f_fake_ext_t g_fx; +ch395f_fake_ext_obs_t g_fxchip; static void fake_log(char c) { @@ -35,6 +40,33 @@ void ch395f_fake_reset(void) g_fake.open_rc[i] = CH395F_ERR_SUCCESS; g_fake.listen_rc[i] = CH395F_ERR_SUCCESS; } + /* 扩展区(net_socket 批次新增):同一次复位里一并清零,避免场景间残留 */ + ch395f_fake_ext_reset(); +} + +/* 扩展面的复位:默认"芯片在位、复位/协议栈初始化成功、版本够新、复位机全 IDLE" */ +void ch395f_fake_ext_reset(void) +{ + int i; + + (void)memset(&g_fx, 0, sizeof(g_fx)); + (void)memset(&g_fxchip, 0, sizeof(g_fxchip)); + g_fx.exist_rc = (int)CH395F_STATUS_OK; + g_fx.reset_rc = (int)CH395F_STATUS_OK; + g_fx.init_rc = (int)CH395F_STATUS_OK; + g_fx.version = 0x46U; /* 台架实测芯片版本(版本门控的默认通过值)*/ + g_fx.phy_status = 0x08U; /* PHY 100M_FULL(非 DISCONN,glob_event_feed 判 link up)*/ + for (i = 0; i < CH395F_FAKE_SOCK_MAX; i++) { + g_fx.connect_rc[i] = CH395F_ERR_SUCCESS; + g_fx.rst_state[i] = (uint8_t)CH395F_FAKE_RST_IDLE; + } +} + +void ch395f_fake_push_glob(uint16_t v) +{ + if (g_fx.glob_n < CH395F_FAKE_GLOB_QUEUE) { + g_fx.glob_q[g_fx.glob_n++] = v; + } } void ch395f_fake_push_recv_len(uint8_t sock, uint16_t v) @@ -94,7 +126,9 @@ void ch395f_get_socket_status(uint8_t sock, uint8_t *status) } } -/* 整体读即清:返回后该 socket 的挂起位图清零(真实芯片没有按位清除手段) */ +/* 整体读即清:返回后该 socket 的挂起位图清零(真实芯片没有按位清除手段)。 + * 唯一新增:清完立刻回填**电平型**掩码 sint_sticky(默认全 0 → 与既有行为逐字相同, + * net_listener / net_events / net_pending 三套不受影响)。 */ uint8_t ch395f_get_sock_int_status(uint8_t sock) { uint8_t v = 0U; @@ -104,11 +138,37 @@ uint8_t ch395f_get_sock_int_status(uint8_t sock) fake_log('i'); if (sock < CH395F_FAKE_SOCK_MAX) { v = g_fake.int_status[sock]; - g_fake.int_status[sock] = 0U; + g_fake.int_status[sock] = g_fx.sint_sticky[sock]; } return v; } +void ch395f_fake_clear_log(void) +{ + (void)memset(g_chip.log, 0, sizeof(g_chip.log)); + g_chip.log_n = 0; +} + +int ch395f_fake_log_find(char c1) +{ + int i; + + for (i = 0; i < g_chip.log_n; i++) { + if (g_chip.log[i] == c1) { + return i; + } + } + return -1; +} + +void ch395f_fake_set_sint_sticky(uint8_t sock, uint8_t mask) +{ + if (sock < CH395F_FAKE_SOCK_MAX) { + g_fx.sint_sticky[sock] = mask; + g_fake.int_status[sock] |= mask; + } +} + uint16_t ch395f_get_recv_len(uint8_t sock) { uint16_t v; @@ -215,3 +275,344 @@ uint16_t ch395f_link_change_count(void) { return g_fake.link_change; } + +/* ========================================================================= + * 扩展区实现(net_socket 主机单测批次;纯新增,上面既有实现一字未动) + * 签名逐字抄自 Drivers/BSP/CH395F/ch395f.h。 + * ========================================================================= */ + +ch395f_status_t ch395f_check_exist(void) +{ + g_fxchip.exist_calls++; + fake_log('E'); + return (ch395f_status_t)g_fx.exist_rc; +} + +ch395f_status_t ch395f_reset(void) +{ + g_fxchip.reset_calls++; + fake_log('Z'); + return (ch395f_status_t)g_fx.reset_rc; +} + +ch395f_status_t ch395f_init(void) +{ + g_fxchip.init_calls++; + fake_log('n'); + return (ch395f_status_t)g_fx.init_rc; +} + +uint8_t ch395f_get_version(void) +{ + return g_fx.version; +} + +void ch395f_set_fun_para(uint32_t para) +{ + g_fxchip.set_fun_para_calls++; + g_fx.fun_para = para; + fake_log('F'); +} + +void ch395f_set_tcp_mss(uint16_t mss) +{ + g_fxchip.set_mss_calls++; + g_fx.mss = mss; + fake_log('Q'); +} + +void ch395f_set_retrans_count(uint8_t cnt) +{ + g_fxchip.set_retrans_calls++; + g_fx.retrans_cnt = cnt; +} + +void ch395f_set_retrans_period(uint16_t period) +{ + g_fx.retrans_period = period; +} + +void ch395f_set_keepalive_idle(uint32_t time) +{ + g_fxchip.set_ka_calls++; + g_fx.ka_idle = time; +} + +void ch395f_set_keepalive_intvl(uint32_t time) +{ + g_fx.ka_intvl = time; +} + +void ch395f_set_keepalive_cnt(uint8_t cnt) +{ + g_fx.ka_cnt = cnt; +} + +void ch395f_set_dhcp(uint8_t enable) +{ + g_fxchip.set_dhcp_calls++; + g_fx.dhcp_enable = enable; + fake_log('D'); +} + +void ch395f_ping_enable(uint8_t enable) +{ + g_fxchip.ping_calls++; + g_fx.ping_enable = enable; +} + +void ch395f_set_ip_addr(uint8_t *p_ip) +{ + g_fxchip.set_ip_calls++; + fake_log('I'); + if (p_ip != NULL) { + (void)memcpy(g_fx.ip, p_ip, 4U); + } +} + +void ch395f_set_gwip_addr(uint8_t *p_gwip) +{ + g_fxchip.set_gw_calls++; + fake_log('N'); + if (p_gwip != NULL) { + (void)memcpy(g_fx.gw, p_gwip, 4U); + } +} + +void ch395f_set_mask_addr(uint8_t *p_mask) +{ + g_fxchip.set_mask_calls++; + fake_log('K'); + if (p_mask != NULL) { + (void)memcpy(g_fx.mask, p_mask, 4U); + } +} + +void ch395f_set_mac_addr(uint8_t *p_mac) +{ + g_fxchip.set_mac_calls++; + fake_log('M'); + if (p_mac != NULL) { + (void)memcpy(g_fx.mac, p_mac, 6U); + } +} + +void ch395f_set_send_buf(uint8_t sock, uint8_t start_block, uint8_t block_count) +{ + g_fxchip.set_send_buf_calls++; + fake_log('b'); + if (sock < CH395F_FAKE_SOCK_MAX) { + g_fx.send_blk_start[sock] = start_block; + g_fx.send_blk_cnt[sock] = block_count; + } +} + +void ch395f_set_recv_buf(uint8_t sock, uint8_t start_block, uint8_t block_count) +{ + g_fxchip.set_recv_buf_calls++; + fake_log('u'); + if (sock < CH395F_FAKE_SOCK_MAX) { + g_fx.recv_blk_start[sock] = start_block; + g_fx.recv_blk_cnt[sock] = block_count; + } +} + +void ch395f_set_des_ip(uint8_t sock, uint8_t *p_ip) +{ + g_fxchip.set_des_ip_calls++; + fake_log('e'); + if ((sock < CH395F_FAKE_SOCK_MAX) && (p_ip != NULL)) { + (void)memcpy(g_fx.des_ip[sock], p_ip, 4U); + } +} + +void ch395f_set_des_port(uint8_t sock, uint16_t port) +{ + g_fxchip.set_des_port_calls++; + fake_log('q'); + if (sock < CH395F_FAKE_SOCK_MAX) { + g_fx.des_port[sock] = port; + } +} + +uint8_t ch395f_tcp_connect(uint8_t sock) +{ + g_fxchip.connect_calls++; + g_chip.last_sock = sock; + fake_log('T'); + return (sock < CH395F_FAKE_SOCK_MAX) ? g_fx.connect_rc[sock] : 0x01U; +} + +uint8_t ch395f_get_phy_status(void) +{ + fake_log('g'); + return g_fx.phy_status; +} + +/* 全局中断状态(0x19,2 字节):**整体读即清**,且含全部 Socket 位—— + * 假件按调用序号返回预置值(耗尽后回落最后一个),并单独计数(B3 唯一读者=net_poll) */ +uint16_t ch395f_get_glob_int_status_all(void) +{ + uint16_t v = 0U; + + g_fxchip.glob_calls++; + fake_log('G'); + if (g_fx.glob_n == 0) { + v = 0U; + } else if (g_fx.glob_i < g_fx.glob_n) { + v = g_fx.glob_q[g_fx.glob_i++]; + } else { + v = g_fx.glob_q[g_fx.glob_n - 1]; + } + g_fxchip.glob_bits = (int)v; + return v; +} + +/* 与真实驱动同判据:只有 PHY_CHANGE 位才回读 PHY;链路电平变了才计一次变更 */ +void ch395f_glob_event_feed(uint16_t glob_bits) +{ + uint8_t up; + + if ((glob_bits & CH395F_GINT_STAT_PHY_CHANGE) == 0U) { + return; + } + up = (ch395f_get_phy_status() != CH395F_PHY_DISCONN) ? 1U : 0U; + if (up != g_fake.link_up) { + g_fake.link_up = up; + g_fake.link_change++; + } +} + +/* 只读本 Socket 的 SINT(读即清),永不碰全局寄存器——与真实护栏原语同语义 */ +uint8_t ch395f_sock_events(uint8_t sn) +{ + g_fxchip.sock_events_calls++; + return ch395f_get_sock_int_status(sn); +} + +/* —— sock_reset 复位机(契约等价:四步 + 最短沉降窗 + 二段 CLOSE 补发)—— + * 与驱动一致的**可断言点**:begin 重复启动必须被拒;GRACEFUL 满 100ms、 + * QUARANTINE 满 400ms 才继续;quiescent 要连续 2 轮无事件;byte0 回 00H 才算完成; + * 芯片 byte0=05H 且 TCP=00H 时补发第二段 CLOSE。 + * 不复制驱动的超时数值细节(那是驱动自己的单测面),本假件不自造失败: + * 想测超时由测试把 cached_open 一直留成 OPEN,由上层 net_sock_reset_sync 的 600ms 收口。 */ +int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode) +{ + g_fxchip.rst_begin_calls++; + fake_log('X'); + if (sn >= CH395F_FAKE_SOCK_MAX) { + return -1; + } + if (g_fx.rst_state[sn] != (uint8_t)CH395F_FAKE_RST_IDLE) { + g_fxchip.rst_begin_reject++; + return -1; + } + g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_DRAINED; + g_fx.rst_mode[sn] = (mode == CH395F_RESET_QUARANTINE) ? 1U : 0U; + g_fx.rst_t0[sn] = HAL_GetTick(); + g_fx.rst_quiet[sn] = 0U; + return 0; +} + +int ch395f_sock_reset_poll(uint8_t sn) +{ + uint32_t now; + uint32_t el; + uint32_t need; + + g_fxchip.rst_poll_calls++; + fake_log('x'); + if (sn >= CH395F_FAKE_SOCK_MAX) { + return -1; + } + if (g_fx.rst_state[sn] == (uint8_t)CH395F_FAKE_RST_IDLE) { + return 0; /* 未启动/已完成:幂等 */ + } + now = HAL_GetTick(); + el = now - g_fx.rst_t0[sn]; + + switch (g_fx.rst_state[sn]) { + case CH395F_FAKE_RST_DRAINED: + /* 第 1 步:读空 RX;第 2 步:CLOSE */ + if (ch395f_get_recv_len(sn) != 0U) { + uint8_t scratch[8]; + g_fxchip.rst_rx_discard_calls++; + ch395f_read_recv_buf(sn, scratch, 8U); + } + g_fxchip.rst_close_calls++; + (void)ch395f_close_socket(sn); + if (g_fx.rst_keep_open[sn] == 0U) { + g_fake.cached_open[sn] = CH395F_SOCKET_CLOSED; + g_fake.cached_tcp[sn] = CH395F_TCP_CLOSED; + } + g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_MINWAIT; + g_fx.rst_t0[sn] = now; + break; + case CH395F_FAKE_RST_MINWAIT: + need = (g_fx.rst_mode[sn] != 0U) ? 400U : 100U; + if (el >= need) { + g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_QUIET; + g_fx.rst_t0[sn] = now; + g_fx.rst_quiet[sn] = 0U; + } + break; + case CH395F_FAKE_RST_QUIET: + if (ch395f_sock_events(sn) == 0U) { + g_fx.rst_quiet[sn]++; + if (g_fx.rst_quiet[sn] >= CH395F_FAKE_RST_QUIESCENT) { + g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_WAITCLS; + g_fx.rst_t0[sn] = now; + } + } else { + g_fx.rst_quiet[sn] = 0U; + } + break; + case CH395F_FAKE_RST_WAITCLS: + if (ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1) { + g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_IDLE; + g_fxchip.rst_done[sn]++; + return 0; + } + /* 二段 CLOSE 补发(LISTEN 态一次 CLOSE 只拆监听,byte0 恒 05H) */ + if ((g_fake.cached_open[sn] == CH395F_SOCKET_OPEN) && + (g_fake.cached_tcp[sn] == CH395F_TCP_CLOSED) && + (g_fx.rst_quiet[sn] != 0xFFU)) { + g_fx.rst_quiet[sn] = 0xFFU; + g_fxchip.rst_close_calls++; + (void)ch395f_close_socket(sn); + if (g_fx.rst_keep_open[sn] == 0U) { + g_fake.cached_open[sn] = CH395F_SOCKET_CLOSED; + g_fake.cached_tcp[sn] = CH395F_TCP_CLOSED; + } + } + break; + default: + g_fx.rst_state[sn] = (uint8_t)CH395F_FAKE_RST_IDLE; + g_fxchip.rst_fail[sn]++; + return -1; + } + return 1; +} + +int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what) +{ + ch395f_sock_state_t st; + + g_fxchip.cmd_done_calls++; + g_fxchip.last_cmd_done_what = (uint8_t)what; + fake_log('z'); + if (sn >= CH395F_FAKE_SOCK_MAX) { + return -1; + } + st = ch395f_get_sock_state(sn); + switch (what) { + case CH395F_DONE_OPEN: + return (st.open == CH395F_SOCKET_OPEN) ? 1 : 0; + case CH395F_DONE_LISTEN: + return (st.open == CH395F_SOCKET_OPEN && st.tcp_state == CH395F_TCP_LISTEN) ? 1 : 0; + case CH395F_DONE_CLOSE: + return (st.open == CH395F_SOCKET_CLOSED) ? 1 : 0; + default: + return -1; + } +} diff --git a/test/host/common/ch395f_fake.h b/test/host/common/ch395f_fake.h index 6453b169..41bf996c 100644 --- a/test/host/common/ch395f_fake.h +++ b/test/host/common/ch395f_fake.h @@ -44,10 +44,10 @@ #define CH395F_ERR_SUCCESS 0x00U #define CH395F_PROTO_TYPE_TCP 0x03U #define CH395F_SOCKET_CLOSED 0x00U -#define CH395F_SOCKET_OPEN 0x01U +#define CH395F_SOCKET_OPEN 0x05U /* 真头 ch395f.h:220 = 0x05U(05H SOCKET_OPEN,docs/芯片手册/CH395F.md:527)*/ #define CH395F_TCP_CLOSED 0x00U #define CH395F_TCP_LISTEN 0x01U -#define CH395F_TCP_ESTABLISHED 0x05U +#define CH395F_TCP_ESTABLISHED 0x04U /* 真头 ch395f.h:229 = 0x04U(04H TCP_ESTABLISHED,docs/芯片手册/CH395F.md:539)*/ /* 发送/接收缓冲尺寸宏链:**与真实 ch395f.h 同构转抄**(2026-10-23 逐条核对)。 * net_pending.h 里的 NET_SEND_CHUNK_MAX 由这两个宏推导,是"一次最多往芯片写多少" @@ -66,6 +66,68 @@ #define CH395F_FAKE_SOCK_MAX 8 /* 与 NET_MAX_SOCKETS 同量级,测试自用 */ #define CH395F_FAKE_QUEUE_LEN 24 #define CH395F_FAKE_LOG_LEN 96 +#define CH395F_FAKE_GLOB_QUEUE 24 + +/* ===== 扩展区(2026-10-24,net_socket 主机单测批次;**纯新增**,上面既有宏/字段/函数 + * 一字未动,net_listener / net_events / net_pending 三套的语义不受影响)===== + * + * 追加的是 net_socket.c 这一层会调、而前三个模块不调的芯片原语(初始化下发类 + + * 全局中断 + sock_reset 复位机 + cmd_done 判据)。签名/常量同样**逐字抄自真实头** + * Drivers/BSP/CH395F/ch395f.h,改真头即编译期/断言期暴露漂移。 + * + * 必须复刻的第三条芯片怪癖(前两套测不到、net_socket.c 却是主路): + * 3. GET_GLOB_INT_STATUS_ALL(0x19,2 字节)**整体读即清**,且它是全局寄存器 + * (含全部 Socket 事件位)。AGENTS 模块约定 B3:唯一读者只能是 net_poll。 + * 假件按调用序号返回预置值,并单独计数,让"别处偷偷读全局"成为可断言的事。 + * 4. sock_reset 复位机(E4 定案四步:读空 RX → CLOSE → 排空事件(quiescent) → + * 等 byte0=00H,含 LISTEN 态二段 CLOSE 补发)。这里做**契约等价**实现: + * 阶段推进与最短等待(GRACEFUL 100ms / QUARANTINE 400ms)照抄驱动, + * 但不复制驱动的超时数值细节(那属于驱动自己的单测面)。 + */ +#define CH395F_ERR_BUSY 0x10U /* 真头 ch395f.h:170 */ +#define CH395F_ERR_MEM 0x11U /* 真头 ch395f.h:171 */ +#define CH395F_ERR_TIMEOUT 0x13U /* 真头 ch395f.h:173 */ +#define CH395F_ERR_VAL 0x19U /* 真头 ch395f.h:179 */ +#define CH395F_ERR_USE 0x1BU /* 真头 ch395f.h:181(open 已被占用)*/ +#define CH395F_PROTO_TYPE_UDP 0x02U /* 真头 ch395f.h:213 */ +#define CH395F_FUN_PARA_SOCKET_CLOSE (1UL << 3) /* 真头 ch395f.h:205 */ +#define CH395F_TCP_MSS 1024U /* 真头 ch395f.h:124 */ +#define CH395F_PHY_DISCONN 0x01U /* 真头 ch395f.h:191 */ +#define CH395F_GINT_STAT_PHY_CHANGE 0x04U /* 真头 ch395f.h:245 */ +#define CH395F_SOCK_STATE_NONE 0xFFU /* 真头 ch395f.h:762 */ + +#define CH395F_SINT_STAT_SENBUF_FREE 0x01U /* 真头 ch395f.h:263 */ +#define CH395F_SINT_STAT_SEND_OK 0x02U /* 真头 ch395f.h:264 */ +#define CH395F_SINT_STAT_RECV_OK 0x04U /* 真头 ch395f.h:265 */ +#define CH395F_SINT_STAT_CONNECT 0x08U /* 真头 ch395f.h:266 */ +#define CH395F_SINT_STAT_DISCONNECT 0x10U /* 真头 ch395f.h:267 */ +#define CH395F_SINT_STAT_SOCK_TIMEOUT 0x40U /* 真头 ch395f.h:269 */ + +typedef enum { + CH395F_STATUS_OK = 0, /* 真头 ch395f.h:275 */ + CH395F_STATUS_ERROR = -1, + CH395F_STATUS_TIMEOUT = -2, + CH395F_STATUS_NOT_DETECTED = -3 +} ch395f_status_t; + +typedef enum { + CH395F_RESET_GRACEFUL = 0, /* 真头 :803:最短等待 100ms */ + CH395F_RESET_QUARANTINE /* 真头 :804:最短等待 400ms */ +} ch395f_reset_mode_t; + +typedef enum { + CH395F_DONE_OPEN = 0, /* 真头 :826:byte0 变 05H */ + CH395F_DONE_LISTEN, /* 真头 :827:byte0=05H 且 TCP=01H */ + CH395F_DONE_CLOSE /* 真头 :828:byte0=00H */ +} ch395f_done_what_t; + +/* 复位机阶段(与驱动同名状态的**契约等价**简化,不代表驱动内部实现) */ +#define CH395F_FAKE_RST_IDLE 0 +#define CH395F_FAKE_RST_DRAINED 1 /* 已读空 RX 并发过 CLOSE */ +#define CH395F_FAKE_RST_MINWAIT 2 /* 等最短沉降窗(100/400ms) */ +#define CH395F_FAKE_RST_QUIET 3 /* 排空本 Socket 事件(连续 2 轮无位) */ +#define CH395F_FAKE_RST_WAITCLS 4 /* 等 byte0=00H(含二段 CLOSE 补发) */ +#define CH395F_FAKE_RST_QUIESCENT 2U typedef struct { uint8_t open; @@ -111,7 +173,13 @@ typedef struct { /* 调用顺序日志(短名首字母): * 'c'=close 'i'=int_status(读即清) 's'=get_socket_status 'r'=read_recv_buf * 'R'=get_recv_len 'o'=open_socket 'l'=tcp_listen 'p'=set_sour_port - * 'P'=set_proto_type */ + * 'P'=set_proto_type 'w'=write_send_buf + * 扩展区新增(net_socket 批次):'G'=读全局中断 'v'=sock_events(读即清) + * 'X'=sock_reset_begin 'x'=sock_reset_poll 'z'=cmd_done 'T'=tcp_connect + * 'I'=set_ip_addr 'N'=set_gwip 'K'=set_mask 'M'=set_mac 'D'=set_dhcp + * 'F'=set_fun_para 'Q'=set_tcp_mss 'b'=set_send_buf 'u'=set_recv_buf + * 'e'=set_des_ip 'q'=set_des_port 'g'=get_phy_status 'E'=check_exist + * 'Z'=reset 'n'=init */ char log[CH395F_FAKE_LOG_LEN]; int log_n; } ch395f_fake_obs_t; @@ -143,4 +211,133 @@ void ch395f_set_keepalive_enable(uint8_t sock, uint8_t enable); uint8_t ch395f_link_up_cached(void); uint16_t ch395f_link_change_count(void); +/* ===== 扩展区第二部分:新增状态世界 + 新增假芯片原型 ===== + * ⚠ 刻意**不往既有 ch395f_fake_world_t / ch395f_fake_obs_t 里加字段**,而是另开一份 + * ch395f_fake_ext_t / ch395f_fake_ext_obs_t:既有三套单测的复位与断言面一字不动, + * 新增状态由 ch395f_fake_reset() 调 ch395f_fake_ext_reset() 一并清零(场景间不残留)。 + */ +typedef struct { + /* —— 初始化/参数下发面(net_init 主路)—— */ + int exist_rc; /* ch395f_check_exist 返回(ch395f_status_t)*/ + int reset_rc; /* ch395f_reset 返回 */ + int init_rc; /* ch395f_init 返回 */ + uint8_t version; /* ch395f_get_version(版本门控 0x46 的用点位)*/ + uint8_t dhcp_enable; /* ch395f_set_dhcp */ + uint8_t ping_enable; /* ch395f_ping_enable */ + uint8_t ip[4]; /* ch395f_set_ip_addr */ + uint8_t gw[4]; /* ch395f_set_gwip_addr */ + uint8_t mask[4]; /* ch395f_set_mask_addr */ + uint8_t mac[6]; /* ch395f_set_mac_addr */ + uint32_t fun_para; /* ch395f_set_fun_para 最近一次实发值 */ + uint16_t mss; /* ch395f_set_tcp_mss */ + uint8_t retrans_cnt; + uint16_t retrans_period; + uint32_t ka_idle; + uint32_t ka_intvl; + uint8_t ka_cnt; + uint8_t send_blk_start[CH395F_FAKE_SOCK_MAX]; /* ch395f_set_send_buf */ + uint8_t send_blk_cnt[CH395F_FAKE_SOCK_MAX]; + uint8_t recv_blk_start[CH395F_FAKE_SOCK_MAX]; /* ch395f_set_recv_buf */ + uint8_t recv_blk_cnt[CH395F_FAKE_SOCK_MAX]; + uint8_t des_ip[CH395F_FAKE_SOCK_MAX][4]; /* ch395f_set_des_ip */ + uint16_t des_port[CH395F_FAKE_SOCK_MAX]; /* ch395f_set_des_port */ + uint8_t connect_rc[CH395F_FAKE_SOCK_MAX]; /* ch395f_tcp_connect 返回 */ + /* —— 全局中断(0x19,整体读即清;唯一读者应为 net_poll)—— */ + uint16_t glob_q[CH395F_FAKE_GLOB_QUEUE]; + int glob_n; + int glob_i; + uint8_t phy_status; /* ch395f_get_phy_status(喂 glob_event_feed)*/ + /* —— sock_reset 复位机(契约等价,见文件头注释 4)—— */ + uint8_t rst_state[CH395F_FAKE_SOCK_MAX]; + uint8_t rst_mode[CH395F_FAKE_SOCK_MAX]; + uint32_t rst_t0[CH395F_FAKE_SOCK_MAX]; + uint8_t rst_quiet[CH395F_FAKE_SOCK_MAX]; /* 连续无事件轮数 */ + /* 1=复位机**不**替芯片把 cached_open 落回 CLOSED(造"永远关不掉"的芯片形态, + * 用来量上层的复位超时收口)。默认 0 → 与既有行为一致。 */ + uint8_t rst_keep_open[CH395F_FAKE_SOCK_MAX]; + /* SINT 的**电平型**来源(可脚本化):读即清之后立刻把这个掩码放回 int_status。 + * 默认 0 → 与既有假件行为逐字相同(读一次就没了)。真实芯片的 RECV_OK / + * SENBUF_FREE 本就是"条件位":只要条件还在(RX 仍非空 / 发送缓冲仍空),下一轮 + * 就还会再报一次。net_socket.c 的多块发送要的就是这个稳态,没有它就只能 + * 造"写完一块就再也等不到空闲"的假死形态。 */ + uint8_t sint_sticky[CH395F_FAKE_SOCK_MAX]; +} ch395f_fake_ext_t; + +typedef struct { + int exist_calls; + int reset_calls; + int init_calls; + int set_ip_calls; + int set_gw_calls; + int set_mask_calls; + int set_mac_calls; + int set_dhcp_calls; + int ping_calls; + int set_fun_para_calls; + int set_mss_calls; + int set_retrans_calls; + int set_ka_calls; + int set_send_buf_calls; + int set_recv_buf_calls; + int set_des_ip_calls; + int set_des_port_calls; + int connect_calls; + int glob_calls; /* 读全局中断次数(B3:除 net_poll 外应为 0)*/ + int glob_bits; /* 最近一次读到的 16 位全局位图 */ + int sock_events_calls; /* ch395f_sock_events(只碰本 Socket 的护栏原语)*/ + int rst_begin_calls; + int rst_begin_reject; /* begin 被拒次数(复位机已在运行)*/ + int rst_poll_calls; + int rst_done[CH395F_FAKE_SOCK_MAX]; /* 复位机完成计数 */ + int rst_fail[CH395F_FAKE_SOCK_MAX]; /* 复位机失败计数 */ + int cmd_done_calls; + uint8_t last_cmd_done_what; + int rst_close_calls; /* 复位机内部发出的 CLOSE 命令(含二段补发)*/ + int rst_rx_discard_calls; /* 复位机 RX 排空事务次数 */ +} ch395f_fake_ext_obs_t; + +extern ch395f_fake_ext_t g_fx; +extern ch395f_fake_ext_obs_t g_fxchip; + +/* 由 ch395f_fake_reset() 自动调用;单独暴露是为了让测试能只清扩展面 */ +void ch395f_fake_ext_reset(void); +void ch395f_fake_push_glob(uint16_t v); /* 预置全局中断返回序列 */ +void ch395f_fake_set_sint_sticky(uint8_t sock, uint8_t mask); +/* 顺序断言辅助:清空调用日志 / 找某个短名第一次出现的位置(-1=没调过)。 + * 刻意提供"清零日志",否则一次长轮询(如 sock_reset 的几十轮 'x')会把 96 格 + * 环形外的顺序信息挤掉,让后续场景的顺序断言失真。 */ +void ch395f_fake_clear_log(void); +int ch395f_fake_log_find(char c); + +/* —— 新增假芯片实现(签名与真实 ch395f.h 一字不改)—— */ +ch395f_status_t ch395f_check_exist(void); +ch395f_status_t ch395f_reset(void); +ch395f_status_t ch395f_init(void); +uint8_t ch395f_get_version(void); +void ch395f_set_fun_para(uint32_t para); +void ch395f_set_tcp_mss(uint16_t mss); +void ch395f_set_retrans_count(uint8_t cnt); +void ch395f_set_retrans_period(uint16_t period); +void ch395f_set_keepalive_idle(uint32_t time); +void ch395f_set_keepalive_intvl(uint32_t time); +void ch395f_set_keepalive_cnt(uint8_t cnt); +void ch395f_set_dhcp(uint8_t enable); +void ch395f_ping_enable(uint8_t enable); +void ch395f_set_ip_addr(uint8_t *p_ip); +void ch395f_set_gwip_addr(uint8_t *p_gwip); +void ch395f_set_mask_addr(uint8_t *p_mask); +void ch395f_set_mac_addr(uint8_t *p_mac); +void ch395f_set_send_buf(uint8_t sock, uint8_t start_block, uint8_t block_count); +void ch395f_set_recv_buf(uint8_t sock, uint8_t start_block, uint8_t block_count); +void ch395f_set_des_ip(uint8_t sock, uint8_t *p_ip); +void ch395f_set_des_port(uint8_t sock, uint16_t port); +uint8_t ch395f_tcp_connect(uint8_t sock); +uint8_t ch395f_get_phy_status(void); +uint16_t ch395f_get_glob_int_status_all(void); +void ch395f_glob_event_feed(uint16_t glob_bits); +uint8_t ch395f_sock_events(uint8_t sn); +int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode); +int ch395f_sock_reset_poll(uint8_t sn); +int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what); + #endif /* CH395F_FAKE_H */ diff --git a/test/host/common/fatfs_fake.c b/test/host/common/fatfs_fake.c new file mode 100644 index 00000000..46472a93 --- /dev/null +++ b/test/host/common/fatfs_fake.c @@ -0,0 +1,1077 @@ +/* + * 模块名称:假 FatFs 内存卷(实现) + * 模块功能:实现 fatfs_fake.h 声明的九个 f_* 原型与卷控制/注入/观测接口。 + * 由测试单元 #include 本 .c 使用(照 test/host/common/ch395f_fake.c 的做法)。 + * + * 语义与 Lib/FatFs/ff.c 的对应关系全部写在 fatfs_fake.h 文件头;本文件只在 + * 每条"复刻点"上标一句 ff.c 的出处,不再重复论证。 + * + * 刻意不做的事: + * - 不做真 FAT 扇区/簇链模拟——被测的是应用侧对 FRESULT 与写回字节数的处置, + * 不是 ff.c 自己(ff.c 属 Lib/ 第三方,不进覆盖率分母); + * - 不做线程安全:三个被测模块都是"唯一写者 + 卷锁串行化",假卷跟着单线程即可; + * - 不实现 f_mount/f_mkfs/f_readdir:调用即链接失败(见头文件说明)。 + * + * 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2) + */ + +#include +#include +#include + +#include "fatfs_fake.h" + +/* ff.c 内部标志(ff.h 不导出 FA_SEEKEND,本移植 FA_OPEN_APPEND = 0x10|0x20) */ +#define FF_FAKE_SEEKEND 0x20u +#define FF_FAKE_CREATE_MASK (FA_CREATE_ALWAYS | FA_OPEN_ALWAYS | FA_CREATE_NEW) + +/* f_open 成功时写进 fp->obj.fs 的"已挂载卷"指针(真实 ff.c 存的就是 FATFS*, + * 失败清 0)。用本文件里一个真对象的地址而不是硬造整数:MSVC x64 下 + * (FATFS *)0xF01DBEEE 会刷 C4312「转换到大得多的类型」。 */ +static FATFS s_vol; +#define FF_FAKE_FS_MAGIC (&s_vol) + +typedef struct { + int used; + int is_dir; + uint32_t size; /* 逻辑长度(可大于已存内容) */ + uint32_t stored; /* data[] 里真实存了多少字节 */ + uint16_t fdate; + uint16_t ftime; + char path[FF_FAKE_PATH_MAX]; /* 归一化:全大写、以 '/' 开头、无重复分隔符 */ + uint8_t data[FF_FAKE_CLUSTER]; +} ff_fake_ent_t; + +typedef struct { + int used; + int any_path; /* 1 = 不限路径 */ + int op; + int act; + int skip; /* >0:本次命中先放行(skip--),不注入 */ + FRESULT rc; + UINT n; + int times; + char path[FF_FAKE_PATH_MAX]; +} ff_fake_inj_t; + +ff_fake_obs_t g_ff_obs; + +static ff_fake_ent_t s_ent[FF_FAKE_MAX_ENT]; +static ff_fake_inj_t s_inj[FF_FAKE_INJ_MAX]; +static uint16_t s_dostime_d = 0x5A21u; /* 2025-10-06 的 FAT 日期编码(任意基准) */ +static uint16_t s_dostime_t = 0x6E40u; /* 13:52:00 */ +static uint8_t s_dirty[FF_FAKE_MAX_ENT]; + +/* ─────────────────────────── 小工具 ─────────────────────────── */ + +static void s_guard(int op) +{ + g_ff_obs.calls[op]++; + if (g_ff_obs.calls[op] > FF_FAKE_GUARD) { + (void)fprintf(stderr, "[FATAL] fatfs_fake: op %d call guard tripped\n", op); + exit(2); + } +} + +static char s_up(char c) +{ + if ((c >= 'a') && (c <= 'z')) { + return (char)(c - 32); + } + return c; +} + +static int s_ieq(const char *a, const char *b) +{ + while ((*a != '\0') && (*b != '\0')) { + if (s_up(*a) != s_up(*b)) { + return 0; + } + a++; + b++; + } + return (*a == *b) ? 1 : 0; +} + +static int s_is_sep(char c) +{ + return ((c == '/') || (c == '\\')) ? 1 : 0; +} + +/* 真实 ff.c create_name 非 LFN 分支的逐字模拟:主名 <=8、扩展 <=3、非法字符集。 + * 返回 1 = 合法短名段。 */ +/* 非法字符单独判,不用字符串表:ff.c 的表里带双引号与反斜杠, + * 写进主机侧字符串字面量既易错又踩本仓"字面量必须纯 ASCII 且不含双引号"的坑 */ +static int s_sfn_bad(char c) +{ + switch (c) { + case '*': case '+': case ',': case ';': + case '<': case '=': case '>': + case '[': case ']': case '|': + case '"': case '?': case 0x7F: + return 1; + default: + return 0; + } +} + +static int s_sfn_ok(const char *seg, int len) +{ + int i = 0; + int ni = 8; + int k; + + if (len <= 0) { + return 0; /* 真实 ff.c:i == 0 -> FR_INVALID_NAME */ + } + for (k = 0; k < len; k++) { + char c = seg[k]; + + if ((c == '.') || (i >= ni)) { /* 主名结束 / 字段溢出? */ + if (ni == 11) { + return 0; /* 扩展域已开:再来点或再溢出 = 非法(.fschk 死在这) */ + } + if (c != '.') { + return 0; /* 主名溢出 */ + } + i = 8; + ni = 11; /* 进入扩展域 */ + continue; + } + if (s_sfn_bad(c) != 0) { + return 0; /* 非法字符(ff.c create_name 的 strchr 表) */ + } + if (s_is_sep(c)) { + return 0; /* 段里不该有分隔符(调用方已切段) */ + } + i++; + } + return (i == 0) ? 0 : 1; +} + +/* 归一化路径:剥盘符 + 剥前导分隔符 + 全大写 + 折叠重复分隔符 + 去尾部分隔符。 + * 返回 FRESULT;out 形如 "/LOG/RUN.LOG",根为 "/"。 + * 盘符判定照 ff.c get_ldnumber:本移植 FF_VOLUMES=1,只有 "0:" 合法, + * 其它 "N:" 形态(含路径中间的冒号)一律 FR_INVALID_DRIVE。 */ +static FRESULT s_norm(const char *path, char *out, int out_sz) +{ + int o = 0; + int i = 0; + const char *p = path; + + if (path == NULL) { + return FR_INVALID_NAME; + } + /* 盘符前缀:到第一个 ':' 之前的整体必须是单个数字 0 */ + { + const char *q = p; + while ((*q != '\0') && (*q != ':')) { + q++; + } + if (*q == ':') { + if ((q == p + 1) && (p[0] == '0')) { + p = q + 1; + } else { + return FR_INVALID_DRIVE; + } + } + } + out[0] = '/'; + o = 1; + while (s_is_sep(*p)) { + p++; + } + while (*p != '\0') { + const char *seg; + int len; + + while (s_is_sep(*p)) { + p++; + } + if (*p == '\0') { + break; /* 尾部多余分隔符:折叠掉 */ + } + seg = p; + len = 0; + while ((p[len] != '\0') && (!s_is_sep(p[len]))) { + len++; + } + if (!s_sfn_ok(seg, len)) { + return FR_INVALID_NAME; + } + if ((o + len + 1) >= out_sz) { + return FR_INVALID_NAME; /* 路径过长:假件一律以名字类错误拒绝 */ + } + if (i != 0) { + out[o++] = '/'; + } + { + int k; + for (k = 0; k < len; k++) { + out[o++] = s_up(seg[k]); + } + } + out[o] = '\0'; + i++; + p += len; + } + if (o == 1) { + return FR_INVALID_NAME; /* 空路径 = 卷根本身:真实 f_open 报 NS_NONAME */ + } + return FR_OK; +} + +/* 取父目录路径:"/LOG/RUN.LOG" -> "/LOG","/LOG" -> "/" */ +static void s_parent(const char *norm, char *out) +{ + const char *slash = strrchr(norm, '/'); + + if ((slash == NULL) || (slash == norm)) { + out[0] = '/'; + out[1] = '\0'; + } else { + int n = (int)(slash - norm); + (void)memcpy(out, norm, (size_t)n); + out[n] = '\0'; + } +} + +static int s_find(const char *norm) +{ + int i; + + for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) { + if ((s_ent[i].used != 0) && (strcmp(s_ent[i].path, norm) == 0)) { + return i; + } + } + return -1; +} + +static int s_alloc(void) +{ + int i; + + for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) { + if (s_ent[i].used == 0) { + return i; + } + } + return -1; +} + +static int s_has_children(const char *dir_norm) +{ + int i; + int dl = (int)strlen(dir_norm); + + for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) { + if (s_ent[i].used == 0) { + continue; + } + if (((int)strlen(s_ent[i].path) > dl) && + (strncmp(s_ent[i].path, dir_norm, (size_t)dl) == 0) && + (s_ent[i].path[dl] == '/')) { + return 1; + } + } + return 0; +} + +/* 中间目录段必须存在(真实 follow_path:非末段找不到 -> FR_NO_PATH) */ +static FRESULT s_check_parent(const char *norm) +{ + char par[FF_FAKE_PATH_MAX]; + int idx; + + s_parent(norm, par); + if (strcmp(par, "/") == 0) { + return FR_OK; + } + idx = s_find(par); + if (idx < 0) { + return FR_NO_PATH; + } + if (s_ent[idx].is_dir == 0) { + return FR_NO_PATH; /* 真实 follow_path:不是子目录 -> FR_NO_PATH */ + } + return FR_OK; +} + +/* 命中注入则返回 1 并把 rc/n 带出 */ +static int s_hit(int op, const char *norm, FRESULT *rc, int *act, UINT *n) +{ + int i; + + for (i = 0; i < (int)FF_FAKE_INJ_MAX; i++) { + if (s_inj[i].used == 0) { + continue; + } + if (s_inj[i].op != op) { + continue; + } + if ((s_inj[i].any_path == 0) && (!s_ieq(s_inj[i].path, norm))) { + continue; + } + if (s_inj[i].skip > 0) { + /* 路径匹配上了才算"放行一次"——不然别的路径/别的 op 会把 skip 吃掉, + * 注入永远打不到真正要打击的那一次调用 */ + s_inj[i].skip--; + return 0; + } + *rc = s_inj[i].rc; + *act = s_inj[i].act; + *n = s_inj[i].n; + s_inj[i].times--; + if (s_inj[i].times <= 0) { + s_inj[i].used = 0; + } + return 1; + } + return 0; +} + +/* 定长缓冲的字符串拷贝(不用 strncpy/strcpy:MSVC /W4 报 C4996 弃用,噪音) */ +static void s_keep(char *dst, const char *src) +{ + int i; + + for (i = 0; (i < ((int)FF_FAKE_PATH_MAX) - 1) && (src[i] != '\0'); i++) { + dst[i] = src[i]; + } + dst[i] = '\0'; +} + +/* 同 s_keep,但目标容量由调用方给(FILINFO.fname 是 13 字节,与路径上限不同) */ +static void s_keep_n(char *dst, int dst_sz, const char *src) +{ + int i; + + for (i = 0; (i < (dst_sz - 1)) && (src[i] != '\0'); i++) { + dst[i] = src[i]; + } + dst[i] = '\0'; +} + +/* ─────────────────────────── 世界控制 ─────────────────────────── */ + +void ff_fake_reset(void) +{ + (void)memset(&g_ff_obs, 0, sizeof(g_ff_obs)); + (void)memset(s_ent, 0, sizeof(s_ent)); + (void)memset(s_inj, 0, sizeof(s_inj)); + (void)memset(s_dirty, 0, sizeof(s_dirty)); + s_dostime_d = 0x5A21u; + s_dostime_t = 0x6E40u; +} + +void ff_fake_set_dostime(uint16_t fdate, uint16_t ftime) +{ + s_dostime_d = fdate; + s_dostime_t = ftime; +} + +static void s_add_inj_skip(const char *path, int op, int act, FRESULT rc, UINT n, int skip, int times) +{ + int i; + char norm[FF_FAKE_PATH_MAX]; + + if (times <= 0) { + return; + } + if (path == NULL) { + for (i = 0; i < (int)FF_FAKE_INJ_MAX; i++) { + if (s_inj[i].used == 0) { + s_inj[i].used = 1; + s_inj[i].any_path = 1; + s_inj[i].op = op; + s_inj[i].act = act; + s_inj[i].rc = rc; + s_inj[i].n = n; + s_inj[i].times = times; + s_inj[i].skip = skip; + s_inj[i].path[0] = '\0'; + return; + } + } + return; + } + /* 注入路径也走一遍归一化,好让大小写/前导斜杠的差异不影响命中 */ + if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) { + norm[0] = '/'; + norm[1] = '\0'; + } + for (i = 0; i < (int)FF_FAKE_INJ_MAX; i++) { + if (s_inj[i].used == 0) { + s_inj[i].used = 1; + s_inj[i].any_path = 0; + s_inj[i].op = op; + s_inj[i].act = act; + s_inj[i].rc = rc; + s_inj[i].n = n; + s_inj[i].times = times; + s_inj[i].skip = skip; + s_keep(s_inj[i].path, norm); + return; + } + } +} + +void ff_fake_inject_rc(const char *path, int op, FRESULT rc, int times) +{ + s_add_inj_skip(path, op, FF_FAKE_ACT_RC, rc, 0u, 0, times); +} + +/* 前 skip 次命中放行,之后 times 次注入 rc(见头文件注释的用途说明) */ +void ff_fake_inject_rc_skip(const char *path, int op, FRESULT rc, int skip, int times) +{ + s_add_inj_skip(path, op, FF_FAKE_ACT_RC, rc, 0u, skip, times); +} + +void ff_fake_inject_write_short(const char *path, UINT n, int times) +{ + s_add_inj_skip(path, FF_FAKE_OP_WRITE, FF_FAKE_ACT_WSHORT, FR_OK, n, 0, times); +} + +void ff_fake_inject_read_short(const char *path, UINT n, int times) +{ + s_add_inj_skip(path, FF_FAKE_OP_READ, FF_FAKE_ACT_RSHORT, FR_OK, n, 0, times); +} + +/* ─────────────────────────── 卷直读直写 ─────────────────────────── */ + +int ff_fake_exists(const char *path) +{ + char norm[FF_FAKE_PATH_MAX]; + int idx; + + if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) { + return 0; + } + idx = s_find(norm); + if (idx < 0) { + return 0; + } + return (s_ent[idx].is_dir != 0) ? 2 : 1; +} + +uint32_t ff_fake_size(const char *path) +{ + char norm[FF_FAKE_PATH_MAX]; + int idx; + + if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) { + return 0xFFFFFFFFu; + } + idx = s_find(norm); + return (idx < 0) ? 0xFFFFFFFFu : s_ent[idx].size; +} + +uint32_t ff_fake_time_token(const char *path) +{ + char norm[FF_FAKE_PATH_MAX]; + int idx; + + if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) { + return 0u; + } + idx = s_find(norm); + if (idx < 0) { + return 0u; + } + return ((uint32_t)s_ent[idx].fdate << 16) | (uint32_t)s_ent[idx].ftime; +} + +uint32_t ff_fake_fetch(const char *path, uint8_t *p_buf, uint32_t off, uint32_t cap) +{ + char norm[FF_FAKE_PATH_MAX]; + int idx; + uint32_t n; + + if ((p_buf == NULL) || (s_norm(path, norm, (int)sizeof(norm)) != FR_OK)) { + return 0u; + } + idx = s_find(norm); + if (idx < 0) { + return 0u; + } + if (off >= s_ent[idx].stored) { + return 0u; + } + n = s_ent[idx].stored - off; + if (n > cap) { + n = cap; + } + (void)memcpy(p_buf, &s_ent[idx].data[off], n); + return n; +} + +int ff_fake_mkfile(const char *path, const uint8_t *p_data, uint32_t len) +{ + char norm[FF_FAKE_PATH_MAX]; + int idx; + + if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) { + return -1; + } + idx = s_find(norm); + if (idx < 0) { + idx = s_alloc(); + if (idx < 0) { + return -1; + } + s_ent[idx].used = 1; + s_ent[idx].is_dir = 0; + s_keep(s_ent[idx].path, norm); + } + if (len > FF_FAKE_CLUSTER) { + len = FF_FAKE_CLUSTER; + } + s_ent[idx].stored = len; + s_ent[idx].size = len; + if (len > 0u) { + if (p_data != NULL) { + (void)memcpy(s_ent[idx].data, p_data, len); + } else { + /* p_data == NULL = "只铺长度、内容清零":造"已有 N 条记录"的前置状态 + * 时用这条,不必真把 N*64 字节写进池子 */ + (void)memset(s_ent[idx].data, 0, len); + } + } + s_ent[idx].fdate = s_dostime_d; + s_ent[idx].ftime = s_dostime_t; + return 0; +} + +/* 只给逻辑长度:造"已有 1024 条记录 = 65536 字节"这类前置状态, + * 内容不真存(簇池只有 2 KB),读到的超出部分按 0 处理 */ +int ff_fake_mkfile_size(const char *path, uint32_t size) +{ + if (ff_fake_mkfile(path, NULL, 0u) != 0) { + return -1; + } + { + char norm[FF_FAKE_PATH_MAX]; + int idx; + + (void)s_norm(path, norm, (int)sizeof(norm)); + idx = s_find(norm); + if (idx < 0) { + return -1; + } + s_ent[idx].size = size; + s_ent[idx].stored = 0u; + } + return 0; +} + +int ff_fake_mkdir_direct(const char *path) +{ + char norm[FF_FAKE_PATH_MAX]; + int idx; + + if (s_norm(path, norm, (int)sizeof(norm)) != FR_OK) { + return -1; + } + idx = s_find(norm); + if (idx >= 0) { + s_ent[idx].is_dir = 1; + return 0; + } + idx = s_alloc(); + if (idx < 0) { + return -1; + } + s_ent[idx].used = 1; + s_ent[idx].is_dir = 1; + s_ent[idx].size = 0u; + s_ent[idx].stored = 0u; + s_keep(s_ent[idx].path, norm); + return 0; +} + +int ff_fake_count_entries(void) +{ + int i; + int n = 0; + + for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) { + if (s_ent[i].used != 0) { + n++; + } + } + return n; +} + +/* ─────────────────────────── f_* 实现 ─────────────────────────── */ + +static void s_inval(FIL *fp) +{ + fp->obj.fs = NULL; /* 真实 ff.c:失败即 fp->obj.fs = 0 */ + fp->obj.sclust = 0u; + fp->obj.objsize = 0u; + fp->fptr = 0u; + fp->flag = 0u; +} + +FRESULT f_open(FIL *fp, const TCHAR *path, BYTE mode) +{ + char norm[FF_FAKE_PATH_MAX]; + FRESULT fr; + FRESULT inj_rc = FR_OK; + int inj_act = 0; + UINT inj_n = 0u; + int hit = 0; + int idx; + + s_guard(FF_FAKE_OP_OPEN); + if (fp == NULL) { + return FR_INVALID_OBJECT; + } + s_inval(fp); + fr = s_norm(path, norm, (int)sizeof(norm)); + if (fr != FR_OK) { + g_ff_obs.last_open_rc = (int)fr; + return fr; + } + hit = s_hit(FF_FAKE_OP_OPEN, norm, &inj_rc, &inj_act, &inj_n); + if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) { + g_ff_obs.last_open_mode = mode; + s_keep(g_ff_obs.last_open_path, norm); + g_ff_obs.last_open_rc = (int)inj_rc; + return inj_rc; + } + g_ff_obs.last_open_mode = mode; + s_keep(g_ff_obs.last_open_path, norm); + + idx = s_find(norm); + if (idx >= 0) { /* 对象已存在 */ + if ((mode & FA_CREATE_NEW) != 0u) { + g_ff_obs.last_open_rc = (int)FR_EXIST; + return FR_EXIST; /* 真实 f_open:Cannot create as new file */ + } + if (s_ent[idx].is_dir != 0) { /* 对目录本身开文件 */ + if ((mode & FF_FAKE_CREATE_MASK) != 0u) { + g_ff_obs.last_open_rc = (int)FR_DENIED; /* 真实:Cannot overwrite (DIR) */ + return FR_DENIED; + } + g_ff_obs.last_open_rc = (int)FR_NO_FILE; /* 真实 f_open:3850 */ + return FR_NO_FILE; + } + if ((mode & FA_CREATE_ALWAYS) != 0u) { + s_ent[idx].size = 0u; /* 截零(真实 ff.c 复位目录项 + 删簇链) */ + s_ent[idx].stored = 0u; + s_dirty[idx] = 1u; + } + } else { /* 对象不存在 */ + /* 真实 ff.c:中间目录段不存在时 follow_path 就已经是 FR_NO_PATH 了, + * **先判父目录再判创建位**,否则 "/log 未建 + 只给 FA_READ" 会被误报成 + * FR_NO_FILE,被测代码的自愈分支就走不到 */ + fr = s_check_parent(norm); + if (fr != FR_OK) { + g_ff_obs.last_open_rc = (int)fr; + return fr; + } + if ((mode & FF_FAKE_CREATE_MASK) == 0u) { + g_ff_obs.last_open_rc = (int)FR_NO_FILE; + return FR_NO_FILE; + } + idx = s_alloc(); + if (idx < 0) { + g_ff_obs.last_open_rc = (int)FR_DENIED; /* 目录满(真实 dir_register) */ + return FR_DENIED; + } + s_ent[idx].used = 1; + s_ent[idx].is_dir = 0; + s_ent[idx].size = 0u; + s_ent[idx].stored = 0u; + s_keep(s_ent[idx].path, norm); + s_dirty[idx] = 1u; + } + + /* 句柄:mode 原值进 flag(真实 ff.c 同做法),因此"只给 FA_OPEN_APPEND" + * 的句柄在 f_write 里必然拿 FR_DENIED —— 陷阱一的复刻度量 */ + fp->obj.fs = FF_FAKE_FS_MAGIC; + fp->obj.sclust = (DWORD)idx + 1u; + fp->obj.objsize = (FSIZE_t)s_ent[idx].size; + fp->flag = mode; + fp->err = 0u; + fp->fptr = ((mode & FF_FAKE_SEEKEND) != 0u) ? (FSIZE_t)s_ent[idx].size : 0u; + g_ff_obs.last_open_rc = (int)FR_OK; + return FR_OK; +} + +static int s_chk(FIL *fp, int *p_idx) +{ + if ((fp == NULL) || (fp->obj.fs != FF_FAKE_FS_MAGIC)) { + return 0; /* FR_INVALID_OBJECT */ + } + if ((fp->obj.sclust == 0u) || (fp->obj.sclust > (DWORD)FF_FAKE_MAX_ENT)) { + return 0; + } + *p_idx = (int)fp->obj.sclust - 1; + if (s_ent[*p_idx].used == 0) { + return 0; + } + return 1; +} + +FRESULT f_write(FIL *fp, const void *p_buff, UINT btw, UINT *bw) +{ + FRESULT fr; + FRESULT inj_rc = FR_OK; + int inj_act = 0; + UINT inj_n = 0u; + int hit = 0; + int idx = -1; + uint32_t pos; + uint32_t room; + uint32_t n; + + if (bw != NULL) { + *bw = 0u; + } + s_guard(FF_FAKE_OP_WRITE); + g_ff_obs.last_write_btw = btw; + g_ff_obs.last_write_bw = 0u; + if (!s_chk(fp, &idx)) { + g_ff_obs.last_write_rc = (int)FR_INVALID_OBJECT; + return FR_INVALID_OBJECT; + } + /* 陷阱一:只给 FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02) -> 恒返 FR_DENIED */ + if ((fp->flag & FA_WRITE) == 0u) { + g_ff_obs.last_write_rc = (int)FR_DENIED; + return FR_DENIED; + } + hit = s_hit(FF_FAKE_OP_WRITE, s_ent[idx].path, &inj_rc, &inj_act, &inj_n); + if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) { + g_ff_obs.last_write_rc = (int)inj_rc; + return inj_rc; + } + if (p_buff == NULL) { + g_ff_obs.last_write_rc = (int)FR_INT_ERR; + return FR_INT_ERR; + } + + pos = (uint32_t)fp->fptr; + /* pos 可能已经越过簇池上沿(ff_fake_mkfile_size 造的"已有 64 KB 记录"就是这种), + * 等价于"一个簇都分配不出来":短写到 0 字节,仍然 FR_OK */ + room = (pos >= FF_FAKE_CLUSTER) ? 0u : (FF_FAKE_CLUSTER - pos); + n = btw; + if ((hit != 0) && (inj_act == FF_FAKE_ACT_WSHORT)) { + if (n > inj_n) { + n = inj_n; /* 定点短写:恰好写 inj_n 字节后"当成功"返回 */ + } + } + if (n > room) { + n = room; /* 真实 ff.c:4102 clst == 0 -> break:FR_OK + 短写 */ + } + if (n > 0u) { + (void)memcpy(&s_ent[idx].data[pos], p_buff, n); + if ((pos + n) > s_ent[idx].stored) { + s_ent[idx].stored = pos + n; + } + } + fp->fptr = (FSIZE_t)(pos + n); + if (fp->fptr > fp->obj.objsize) { + fp->obj.objsize = fp->fptr; + } + s_ent[idx].size = (uint32_t)fp->obj.objsize; + s_dirty[idx] = 1u; + if (bw != NULL) { + *bw = n; + } + g_ff_obs.last_write_bw = n; + g_ff_obs.last_write_rc = (int)FR_OK; + fr = FR_OK; /* 短写也是 FR_OK:判失败只能靠 *bw(§10.1 第 11 条) */ + return fr; +} + +FRESULT f_read(FIL *fp, void *p_buff, UINT btr, UINT *br) +{ + FRESULT inj_rc = FR_OK; + int inj_act = 0; + UINT inj_n = 0u; + int hit = 0; + int idx = -1; + uint32_t pos; + uint32_t remain; + uint32_t n; + + if (br != NULL) { + *br = 0u; + } + s_guard(FF_FAKE_OP_READ); + g_ff_obs.last_read_btr = btr; + g_ff_obs.last_read_br = 0u; + if (!s_chk(fp, &idx)) { + return FR_INVALID_OBJECT; + } + if ((fp->flag & FA_READ) == 0u) { /* 真实 f_read:Check access mode */ + return FR_DENIED; + } + hit = s_hit(FF_FAKE_OP_READ, s_ent[idx].path, &inj_rc, &inj_act, &inj_n); + if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) { + return inj_rc; + } + if (p_buff == NULL) { + return FR_INT_ERR; + } + pos = (uint32_t)fp->fptr; + remain = ((uint32_t)fp->obj.objsize > pos) ? ((uint32_t)fp->obj.objsize - pos) : 0u; + n = btr; + if (n > remain) { + n = remain; /* 真实 f_read:btr 被 remain 截断后照常 FR_OK */ + } + if ((hit != 0) && (inj_act == FF_FAKE_ACT_RSHORT) && (n > inj_n)) { + n = inj_n; + } + { + uint32_t k; + uint8_t *dst = (uint8_t *)p_buff; + + for (k = 0u; k < n; k++) { + dst[k] = ((pos + k) < s_ent[idx].stored) ? s_ent[idx].data[pos + k] : 0u; + } + } + fp->fptr = (FSIZE_t)(pos + n); + if (br != NULL) { + *br = n; + } + g_ff_obs.last_read_br = n; + return FR_OK; +} + +FRESULT f_lseek(FIL *fp, FSIZE_t ofs) +{ + int idx = -1; + uint32_t want = (uint32_t)ofs; + + s_guard(FF_FAKE_OP_LSEEK); + g_ff_obs.last_lseek_ofs = ofs; + if (!s_chk(fp, &idx)) { + return FR_INVALID_OBJECT; + } + if (want > (uint32_t)fp->obj.objsize) { + if ((fp->flag & FA_WRITE) == 0u) { + want = (uint32_t)fp->obj.objsize; /* 只读句柄:钳到文件尾,不报错 */ + } else if (want > FF_FAKE_CLUSTER) { + return FR_DENIED; /* 可写但要超出簇池 = 簇分配不出来 */ + } else { + s_ent[idx].size = want; /* 可写句柄:扩展(真实 ff.c 补零簇链) */ + fp->obj.objsize = (FSIZE_t)want; + s_dirty[idx] = 1u; + } + } + fp->fptr = (FSIZE_t)want; + return FR_OK; +} + +FRESULT f_close(FIL *fp) +{ + int idx = -1; + FRESULT inj_rc = FR_OK; + int inj_act = 0; + UINT inj_n = 0u; + int hit = 0; + + s_guard(FF_FAKE_OP_CLOSE); + if (!s_chk(fp, &idx)) { + return FR_INVALID_OBJECT; + } + hit = s_hit(FF_FAKE_OP_CLOSE, s_ent[idx].path, &inj_rc, &inj_act, &inj_n); + if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) { + return inj_rc; /* 真实 f_close 会因 sync 失败返错且释放句柄 */ + } + if (s_dirty[idx] != 0u) { /* 真实 f_close:sync 里用 get_fattime 回写目录项 */ + s_ent[idx].fdate = s_dostime_d; + s_ent[idx].ftime = s_dostime_t; + s_dirty[idx] = 0u; + } + s_inval(fp); + return FR_OK; +} + +FRESULT f_stat(const TCHAR *path, FILINFO *fno) +{ + char norm[FF_FAKE_PATH_MAX]; + FRESULT fr; + FRESULT inj_rc = FR_OK; + int inj_act = 0; + UINT inj_n = 0u; + int hit = 0; + int idx; + + s_guard(FF_FAKE_OP_STAT); + fr = s_norm(path, norm, (int)sizeof(norm)); + if (fr != FR_OK) { + return fr; + } + hit = s_hit(FF_FAKE_OP_STAT, norm, &inj_rc, &inj_act, &inj_n); + if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) { + return inj_rc; + } + idx = s_find(norm); + if (idx < 0) { + /* 真实 follow_path:末段找不到 -> FR_NO_FILE,但**中间段**找不到 -> FR_NO_PATH */ + fr = s_check_parent(norm); + return (fr == FR_OK) ? FR_NO_FILE : fr; + } + if (fno != NULL) { + (void)memset(fno, 0, sizeof(*fno)); + fno->fsize = (FSIZE_t)s_ent[idx].size; + fno->fdate = s_ent[idx].fdate; + fno->ftime = s_ent[idx].ftime; + fno->fattrib = (s_ent[idx].is_dir != 0) ? AM_DIR : AM_ARC; + { + const char *base = strrchr(norm, '/'); + base = (base != NULL) ? (base + 1) : norm; + s_keep_n(fno->fname, (int)sizeof(fno->fname), base); + } + } + return FR_OK; +} + +FRESULT f_unlink(const TCHAR *path) +{ + char norm[FF_FAKE_PATH_MAX]; + FRESULT fr; + FRESULT inj_rc = FR_OK; + int inj_act = 0; + UINT inj_n = 0u; + int hit = 0; + int idx; + + s_guard(FF_FAKE_OP_UNLINK); + fr = s_norm(path, norm, (int)sizeof(norm)); + if (fr != FR_OK) { + return fr; + } + s_keep(g_ff_obs.last_unlink_path, norm); + hit = s_hit(FF_FAKE_OP_UNLINK, norm, &inj_rc, &inj_act, &inj_n); + if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) { + return inj_rc; + } + idx = s_find(norm); + if (idx < 0) { + fr = s_check_parent(norm); + return (fr == FR_OK) ? FR_NO_FILE : fr; + } + if ((s_ent[idx].is_dir != 0) && (s_has_children(norm) != 0)) { + return FR_DENIED; /* 真实 f_unlink:目录非空不可删 */ + } + s_ent[idx].used = 0; + s_dirty[idx] = 0u; + return FR_OK; +} + +FRESULT f_mkdir(const TCHAR *path) +{ + char norm[FF_FAKE_PATH_MAX]; + FRESULT fr; + FRESULT inj_rc = FR_OK; + int inj_act = 0; + UINT inj_n = 0u; + int hit = 0; + int idx; + + s_guard(FF_FAKE_OP_MKDIR); + fr = s_norm(path, norm, (int)sizeof(norm)); + if (fr != FR_OK) { + return fr; + } + s_keep(g_ff_obs.last_mkdir_path, norm); + hit = s_hit(FF_FAKE_OP_MKDIR, norm, &inj_rc, &inj_act, &inj_n); + if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) { + return inj_rc; + } + if (s_find(norm) >= 0) { + return FR_EXIST; /* 真实 f_mkdir:已存在 -> FR_EXIST */ + } + fr = s_check_parent(norm); + if (fr != FR_OK) { + return fr; + } + idx = s_alloc(); + if (idx < 0) { + return FR_DENIED; + } + s_ent[idx].used = 1; + s_ent[idx].is_dir = 1; + s_ent[idx].size = 0u; + s_ent[idx].stored = 0u; + s_ent[idx].fdate = s_dostime_d; + s_ent[idx].ftime = s_dostime_t; + s_keep(s_ent[idx].path, norm); + return FR_OK; +} + +FRESULT f_rename(const TCHAR *path_old, const TCHAR *path_new) +{ + char nold[FF_FAKE_PATH_MAX]; + char nnew[FF_FAKE_PATH_MAX]; + FRESULT fr; + FRESULT inj_rc = FR_OK; + int inj_act = 0; + UINT inj_n = 0u; + int hit = 0; + int idx; + int dup; + + s_guard(FF_FAKE_OP_RENAME); + fr = s_norm(path_old, nold, (int)sizeof(nold)); + if (fr != FR_OK) { + return fr; + } + fr = s_norm(path_new, nnew, (int)sizeof(nnew)); + if (fr != FR_OK) { + return fr; + } + s_keep(g_ff_obs.last_rename_old, nold); + s_keep(g_ff_obs.last_rename_new, nnew); + hit = s_hit(FF_FAKE_OP_RENAME, nold, &inj_rc, &inj_act, &inj_n); + if ((hit != 0) && (inj_act == FF_FAKE_ACT_RC)) { + return inj_rc; + } + idx = s_find(nold); + if (idx < 0) { + fr = s_check_parent(nold); + return (fr == FR_OK) ? FR_NO_FILE : fr; + } + dup = s_find(nnew); + if (dup >= 0) { + /* E-01 台架实证的根因:本移植(与上游同语义)"new name already in use" + * 一律 FR_EXIST、不覆盖。原子写因此必须先 unlink 目标再 rename。 */ + return FR_EXIST; + } + fr = s_check_parent(nnew); + if (fr != FR_OK) { + return fr; + } + if (s_ent[idx].is_dir != 0) { /* 目录改名:同步搬走全部子项 */ + int dl = (int)strlen(nold); + int i; + + for (i = 0; i < (int)FF_FAKE_MAX_ENT; i++) { + if ((s_ent[i].used == 0) || (i == idx)) { + continue; + } + if (((int)strlen(s_ent[i].path) > dl) && + (strncmp(s_ent[i].path, nold, (size_t)dl) == 0) && + (s_ent[i].path[dl] == '/')) { + char tmp[FF_FAKE_PATH_MAX]; + int m; + + for (m = 0; nnew[m] != '\0'; m++) { + tmp[m] = nnew[m]; + } + s_keep_n(&tmp[m], (int)sizeof(tmp) - m, &s_ent[i].path[dl]); + s_keep(s_ent[i].path, tmp); + } + } + } + s_keep(s_ent[idx].path, nnew); + s_ent[idx].fdate = s_dostime_d; + s_ent[idx].ftime = s_dostime_t; + return FR_OK; +} diff --git a/test/host/common/fatfs_fake.h b/test/host/common/fatfs_fake.h new file mode 100644 index 00000000..3d561fd8 --- /dev/null +++ b/test/host/common/fatfs_fake.h @@ -0,0 +1,149 @@ +/* + * 模块名称:假 FatFs 内存卷(存储域主机侧单测共享件) + * 模块功能:给 App/log 与 App/cfg 的主机侧单测一个可脚本化的假文件系统—— + * 类型与常量取自**真实 Lib/FatFs/ff.h**(FRESULT / FIL / FILINFO / FA_* + * 一字不改),本件在 fatfs_fake.c 里给出九个 f_* 原型的 RAM 实现: + * f_open / f_read / f_write / f_lseek / f_close / f_stat / f_unlink / + * f_mkdir / f_rename。 + * + * 仅供 test/host/** 使用:不参与 MCU 构建,也不参与 simulator 构建。 + * + * 为什么共享一份(照 test/host/common/ch395f_fake.{h,c} 的先例): + * sys_log_mcu / soe_store / cfg_file_store_mcu 三块的成败**全部**由 + * 「这次 f_open 拿到什么码、f_write 写回多少字节」决定。三份假件各写一遍, + * 漂移 = 「假件与被测代码同时错而无人报错」。签名与真实头同源 → 真实 API 一改 + * 就编译期报错(同 test/host/nand_port 的「真头 + 假 diskio」路子)。 + * + * —— 必须复刻的三条本仓实证语义(docs/架构设计/STM32F4-Base存储架构说明.md §10.1)—— + * 1) FA_OPEN_APPEND(0x30) **不含 FA_WRITE(0x02)**:本移植 FA_OPEN_APPEND = + * FA_OPEN_ALWAYS(0x10) | FA_SEEKEND(0x20),而 FA_SEEKEND 只定义在 ff.c:74、 + * ff.h 里没有。所以只给 FA_OPEN_APPEND 打开的文件**能打开、能创建**,但 + * f_write 恒返 FR_DENIED(7)——2026-09-20 台架「/log 写失败 x5/开机」的真根因 + * (§10.1 第 11 条)。假件按真实 ff.c 一样把 mode 原值存进 fp->flag 再判 + * flag & FA_WRITE,不做任何「便利放行」。 + * 2) f_write 簇分配失败 = **FR_OK + 短写**(无错误码,判失败只能校验写回字节数): + * 真实 ff.c:4102 的 if (clst == 0) break; → 循环带着已写计数退出 → FR_OK。 + * 假件把「每文件固定后备簇池」当介质:写到装不下就返回 FR_OK + 截断字节数; + * 另给定点注入,让「恰好短一字节」这种真机要撞坏块才有的形态可复现。 + * 3) FF_USE_LFN=0 → **严格 8.3 短名**:主名 <=8、扩展 <=3、非法字符集 + * * + ; < = > [ ] | 与 0x7F(ff.c create_name 非 LFN 分支逐字模拟)。 + * 「.fschk」的真实死法 = 开头那个点把游标直接推进扩展域、随后 5 个字符撑爆 + * 扩展域上限 3 → FR_INVALID_NAME(6);路径里任何位置出现 ':' 被 get_ldnumber + * 当盘符前缀吃下 → FR_INVALID_DRIVE(11)。两者都是**纯 RAM 里的名字判定、 + * 不碰盘**(§10.1 第 10 条,含「测试用例须同时接受这两个码」的口径)。 + * + * —— 其余与真实 ff.c 对齐的路径/错误码细则 —— + * - 前导分隔符被剥掉、恒从卷根开始(FF_FS_RPATH=0,见 ff.c follow_path); + * - 中间目录段不存在 → FR_NO_PATH;末段不存在且未给创建位 → FR_NO_FILE; + * - 对目录本身 f_open(无创建位)→ FR_NO_FILE; + * - FA_CREATE_NEW 撞上已存在 → FR_EXIST;FA_CREATE_ALWAYS → 截零 + 刷新时间戳; + * - 路径剥完分隔符为空 → FR_INVALID_NAME(ff.c create_name 的 i==0 分支); + * - f_stat:父目录在而对象不在 → FR_NO_FILE;父目录也不在 → FR_NO_PATH; + * - f_unlink 不存在 → FR_NO_FILE;f_mkdir 已存在 → FR_EXIST; + * - **f_rename 撞已存在的新名 → FR_EXIST、不覆盖**(ff.c「new name already in + * use」,2026-09-22 台架 E-01 暴露的那条:缺了它,一切「目标存在的覆盖写」 + * 必然失败,而模拟器端 POSIX rename 的覆盖语义照不出来)。不复刻本条 = + * cfg_store_write_atomic 里「先 unlink 再 rename」那一步永远测不出必要性; + * - f_lseek 越过文件尾:只读句柄**钳到 objsize 且不报错**(ff.c 正常寻址分支), + * 可写句柄才扩展文件; + * - f_open 失败即作废句柄(ff.c「fp->obj.fs = 0」)→ 后续 f_write 拿 + * FR_INVALID_OBJECT,不会拿着脏句柄继续写; + * - f_close 成功即释放句柄,并把打开期间的变更落成新的 fdate/ftime + * (真实 ff.c 在 close 里用 get_fattime 回写目录项)。 + * + * —— 脚本化模型(沿用本仓既有假件风格:nand_port 的 RAM 假芯片 / lftpd_io 的假堆)—— + * - 注入:按路径 + op 预置「接下来 N 次返回某码 / 只写回 n 字节 / 只读回 n 字节」; + * path 传 NULL = 不限路径; + * - 观测:逐 op 调用计数 + 最近一次入参(路径、mode、btw/bw、btr/br、lseek 偏移), + * 让「这轮压根不该碰盘」与「打开模式必须带写位」成为可断言的事; + * - 卷:条目直读直写(ff_fake_mkfile / ff_fake_mkfile_size / ff_fake_fetch)用来 + * 铺前置状态——「run.log 已经有 1024 条记录」不可能靠真写 65536 字节铺出来; + * - 假时钟:ff_fake_set_dostime 控制 fdate/ftime,FAT 分钟粒度导致 + * 「同分钟内同长度重写不可检出」这件事只有它能测(cfg_store_stat 的 mtime 令牌); + * - ff_fake_reset() 一次全清(卷 + 注入 + 观测 + 时钟)。 + * + * 有意**不提供** f_mount / f_mkfs / f_opendir / f_readdir:三个被测模块一次都不该 + * 调它们,真调了就是链接期报错——比运行时断言更硬,也不会静默通过。 + * + * 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2) + */ + +#ifndef FATFS_FAKE_H +#define FATFS_FAKE_H + +#include + +/* 真实头在 Lib/FatFs/;本文件不重定义任何 FatFs 类型或宏 */ +#include "ff.h" + +#define FF_FAKE_MAX_ENT 16u /* 卷内条目上限(文件 + 目录) */ +#define FF_FAKE_PATH_MAX 24u /* 归一化后路径上限("/log/selftest.log" = 17) */ +/* 每文件的后备簇池:必须**大于生产侧单文件容量上限**(run.log / soe.log + * = 1024 条 x 64 B = 65536 B,见 sys_log.h:SYSLOG_RUN_MAX 与 soe_store.h: + * SOE_STORE_MAX),否则测"容量满 unlink 重建"时追加位落在池外,f_write 恒 + * 返回 0 字节短写,轮转场景根本跑不到。写满仍然如实返回 FR_OK + 短写 + *(真实 ff.c 簇分配失败的同一形态)。16 条目 x 66 KB ≈ 1 MB 静态,主机侧无压力。 */ +#define FF_FAKE_CLUSTER 67584u +#define FF_FAKE_INJ_MAX 8u /* 注入槽位数 */ +#define FF_FAKE_GUARD 50000 /* 跑飞保险:单个 f_* 调用数超此值直接 exit(2) */ + +/* 注入与计数的操作编号(fatfs_fake.c 内部同一套) */ +enum { + FF_FAKE_OP_OPEN = 0, + FF_FAKE_OP_READ, + FF_FAKE_OP_WRITE, + FF_FAKE_OP_LSEEK, + FF_FAKE_OP_CLOSE, + FF_FAKE_OP_STAT, + FF_FAKE_OP_UNLINK, + FF_FAKE_OP_MKDIR, + FF_FAKE_OP_RENAME, + FF_FAKE_OP_N +}; + +/* 注入动作 */ +enum { + FF_FAKE_ACT_RC = 0, /* 直接返回指定 FRESULT,不做任何数据搬运 */ + FF_FAKE_ACT_WSHORT, /* 仅 f_write:FR_OK 但只写回 n 字节 */ + FF_FAKE_ACT_RSHORT /* 仅 f_read :FR_OK 但只读回 n 字节 */ +}; + +typedef struct { + int calls[FF_FAKE_OP_N]; /* 逐 op 调用次数(含失败的调用) */ + BYTE last_open_mode; /* 最近一次 f_open 的 mode 原值(断言 FA_WRITE 位) */ + char last_open_path[FF_FAKE_PATH_MAX]; + char last_unlink_path[FF_FAKE_PATH_MAX]; + char last_mkdir_path[FF_FAKE_PATH_MAX]; + char last_rename_old[FF_FAKE_PATH_MAX]; + char last_rename_new[FF_FAKE_PATH_MAX]; + UINT last_write_btw; /* 请求写的字节数 */ + UINT last_write_bw; /* 实际写回的字节数(短写的断言面) */ + UINT last_read_btr; + UINT last_read_br; + FSIZE_t last_lseek_ofs; + int last_open_rc; /* 最近一次 f_open 的返回码 */ + int last_write_rc; /* 最近一次 f_write 的返回码 */ +} ff_fake_obs_t; + +extern ff_fake_obs_t g_ff_obs; + +/* —— 世界控制 —— */ +void ff_fake_reset(void); /* 卷 + 注入 + 观测 + 时钟一次全清 */ +void ff_fake_set_dostime(uint16_t fdate, uint16_t ftime); /* 假 get_fattime() 的两个域 */ + +/* —— 故障注入(times = 生效次数;path = NULL 表示不限路径)—— */ +void ff_fake_inject_rc(const char *path, int op, FRESULT rc, int times); +void ff_fake_inject_write_short(const char *path, UINT n, int times); +void ff_fake_inject_read_short(const char *path, UINT n, int times); + +/* —— 卷内容直读直写(绕开打开语义,用来铺前置状态)—— */ +int ff_fake_exists(const char *path); /* 0 无 / 1 文件 / 2 目录 */ +uint32_t ff_fake_size(const char *path); /* 无此文件返回 0xFFFFFFFF */ +uint32_t ff_fake_time_token(const char *path); /* (fdate<<16)|ftime,同 cfg_store_stat */ +uint32_t ff_fake_fetch(const char *path, uint8_t *p_buf, uint32_t off, uint32_t cap); +int ff_fake_mkfile(const char *path, const uint8_t *p_data, uint32_t len); +int ff_fake_mkfile_size(const char *path, uint32_t size); /* 只给逻辑长度(造容量满) */ +int ff_fake_mkdir_direct(const char *path); /* 无条件建目录 */ +int ff_fake_count_entries(void); /* 卷内条目数(含目录) */ + +#endif /* FATFS_FAKE_H */ diff --git a/test/host/console_log/console_log_test.c b/test/host/console_log/console_log_test.c new file mode 100644 index 00000000..8f7d05e1 --- /dev/null +++ b/test/host/console_log/console_log_test.c @@ -0,0 +1,922 @@ +/* + * 模块名称:控制台单写者日志(console_log)主机侧单元测试 + * 模块功能:在 PC 上离线验证 App/log/console_log.c 的单写者契约(G1~G5 / K1~K5,见 + * docs/子系统设计/控制台单写者日志设计说明.md):环形缓冲入环与分块发送、 + * 行级原子、192B 截断、写者未注册/ISR 上下文的无锁直发、缓冲满有界等待与 + * 丢弃可观测、TX 僵尸通道软复位自愈 + 300ms 冷却、DWT 与指令计数两种硬上界、 + * huart1.Instance 未初始化的 NULL 短路、fputc 重定向、drain 250ms 预算。 + * + * 为什么要主机侧单测(而不是只用台架): + * 本模块的风险全在"异常时刻"——UART 僵尸通道卡死、TIM7 停摆导致 uwTick 不往前走、 + * 开机 1.7s 喂狗容限被背靠背块超时吃掉(台架实测的停喂误复位)、写者注册前多任务裸发 + * 逐字节交错(2026-10-10/10-12 台架实证)。台架上这些要么一年一遇、要么现场已被 + * 复位冲掉;而它们的判据全是"数得清的数":192B 上限、256B 分块、4095B 可用环、 + * 200 tick 等待、20000 次计数上界、100/250/300ms 三条时间线。把它们做成断言,是这台 + * 设备上唯一能把这几条线钉死的办法(板端套件只能"在真机上跑一遍")。 + * + * 桩的边界(细节见 stub/stm32f4_fake.h 头注释): + * console_log.h / sys_events.h(含真实 SYS_EVT_CONSOLE_TX 位)用**真实头**; + * usart.h 用桩,但只暴露本模块访问的 huart1 + MX_USART1_UART_Init,寄存器位号 / + * 枚举值照抄真实头;USART SR 的 TXE/TC、DWT CYCCNT、HAL_GetTick、临界区、 + * __get_IPSR、vTaskDelay、osEventFlagsSet 做成可控 + 可观测的量。 + * ⚠ 本文件**刻意不定义**任何 syslog_* / DBG_* 符号:console_log.c 哪天把"TX 故障 + * 留痕"改回直接调日志子系统,链接就失败——那正是必须挡住的(write_line → + * con_append → drain → syslog_post → write_line 成环,静态栈深在每个任务上虚高)。 + * + * 覆盖点(编号族 TC-CONLOG-*,属主登记处 = 本文件;实占 104 号 / 104 条断言): + * 000..006 宿主 printf 与本模块 fputc 的隔离 + 参数校验与 192B 截断 + * 010..018 写者未注册 = 无锁直发(不入环、不置事件位、零临界区、不等待、256B 有界) + * 020..038 注册后走环形:入环不发送、按 256B 分块、多生产者不交错、跨环尾两段拷贝、 + * 入环长度钳 RING/2 + * 040..044 写者自记录先排空(不自等)/ drain 重入保护 / 未注册时 drain 让位 + * 050..062 缓冲满:DWT 硬上界丢弃、不覆盖旧行、等待中出现空间、无 DWT 时 200 tick + * 上界、[ERR] dropped 直发不经环、读后即清 + * 070..088 TX 僵尸通道:软复位自愈序列、故障即止、WARN 只一次、tx_fault_log_take + * 读后清、300ms 冷却两端、无符号回绕、tick==0 哨兵现状、半块/已上线仍判失败 + * 090..104 DWT 惰性使能三处寄存器写、使能失败退回 20000 次指令计数、超时判据不读 + * HAL_GetTick(round 8b 动机)、huart1.Instance == NULL 短路两路 + * 110..117 fputc 重定向(未注册直发 / 注册逐字节入环 / ISR 上下文裸发 / 环满时 ISR + * 仍不等待) + * 120..122 drain 单次 250ms 预算与下轮续排(让位喂狗但不丢字节) + * 130..135 计数支路也能把整块发完(不只会超时)+ 其 TC 计数上界 + DWT 支路 TC 抖动 + * 后正常收尾 + con_append 直调时的自有参数校验 + * + * 实测覆盖率(scripts/check_test_coverage.py --module console_log,/Od + /Zi 口径): + * App/log/console_log.c **188/189 行 = 99.5%,函数 15/15 = 100%**。 + * + * 故意没测(逐行说明见交付报告): + * - console_log.c:352 唯一未覆盖行:snprintf 溢出后的钳位 `n = sizeof(note)-1`。 + * note[64],前缀 24B + "%lu"(unsigned long 最多 10 位)+ 后缀 8B = 最长 42B + * < 63B,任何输入都到不了这一行 —— 纯防御,不是漏测。 + * - 真实周期时间量级:主机没有真 DWT CYCCNT 也没有 TIM7 的 uwTick,只能钉"判据在 + * 第几次读成立 / 自旋几次放弃",不能声称"实测 100ms 就返回"。 + * - ISR 与写者同时裸发的真总线交错(docs §13-O1):宿主单线程,造不出两个执行流。 + * - osEventFlagsWait 唤醒写者与 50ms 兜底:写者任务体在 App/task/default_task.c。 + * - MicroLIB 真实 retarget 链接语义:宿主 CRT 有自己的 fputc(实测与本模块符号 + * 冲突 -> LNK2005),故本 TU 把被测 fputc 改名纳入,只验函数体不验链接钩子。 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL;跑飞保险 exit(2)。 + * + * 创建日期:2026-10-24 + */ + +#include +#include +#include +#include + +/* 平台假件先纳入:它给出 USART/DWT/HAL/内核入口的宿主等价声明(stub 目录排第一) */ +#include "stm32f4_fake.h" + +/* ===================== 假件全局量(声明见 stub/stm32f4_fake.h) ===================== */ +uint32_t SystemCoreClock = 168000000U; /* 与 MCU 端一致(168 MHz) */ + +USART_TypeDef g_con_uart; +CoreDebug_Type g_con_core_debug; +DWT_Type g_con_dwt; +UART_HandleTypeDef huart1; + +uint32_t con_uart_tx_n; +uint8_t con_uart_tx[CON_UART_TX_MAX]; +uint32_t con_uart_block_n; +uint32_t con_uart_block_len[CON_UART_BLOCK_MAX]; +uint32_t con_uart_cur_len; +uint32_t con_uart_txe_reads; +uint32_t con_uart_tc_reads; +int con_uart_txe_stuck; +int con_uart_txe_after; +int con_uart_txe_ready_left; +int con_uart_tc_after; +int con_uart_tc_stuck; +int con_uart_pending; + +uint32_t con_dwt_step; +int con_dwt_ctrl_sticky; +uint32_t con_dwt_reads; + +uint32_t con_tick_now; +uint32_t con_tick_step; +uint32_t con_tick_reads; + +static char s_task_writer; /* 假写者任务句柄(取地址当不透明句柄) */ +static char s_task_other; /* 假"别的任务"句柄 */ +TaskHandle_t con_task_self; + +uint32_t con_delay_calls; +void (*con_delay_hook)(void); + +uint32_t con_crit_enter; +uint32_t con_crit_exit; +uint32_t con_crit_max_depth; +static uint32_t s_crit_depth; + +uint32_t con_ipsr; + +osEventFlagsId_t sys_evtHandle; /* 真实 sys_events.h 声明的全局句柄 */ +static char s_evt_obj; /* 假事件组对象(非 NULL 即可) */ +uint32_t con_evt_sets; +uint32_t con_evt_flags; + +uint32_t con_nvic_clears; +uint32_t con_rcc_disables; +uint32_t con_rcc_enables; +static uint32_t s_mx_init_calls; +static int s_mx_repair; /* 1 = 重初始化把僵尸通道治好(模型行为) */ + +/* ===================== 假件实现 ===================== */ + +uint32_t HAL_GetTick(void) +{ + uint32_t v = con_tick_now; + + con_tick_reads++; + if (con_tick_reads > 2000000U) { + printf("[FATAL] fake HAL_GetTick called %lu times (runaway)\n", + (unsigned long)con_tick_reads); + exit(2); + } + con_tick_now += con_tick_step; + return v; +} + +TaskHandle_t xTaskGetCurrentTaskHandle(void) +{ + return con_task_self; +} + +void vTaskDelay(TickType_t ticks) +{ + (void)ticks; + con_delay_calls++; + if (con_delay_calls > 5000U) { + printf("[FATAL] fake vTaskDelay called %lu times (runaway)\n", + (unsigned long)con_delay_calls); + exit(2); + } + if (con_delay_hook != 0) { + con_delay_hook(); + } +} + +void con_crit_push(void) +{ + con_crit_enter++; + s_crit_depth++; + if (s_crit_depth > con_crit_max_depth) { + con_crit_max_depth = s_crit_depth; + } +} + +void con_crit_pop(void) +{ + con_crit_exit++; + if (s_crit_depth == 0U) { + printf("[FATAL] critical section exit without enter\n"); + exit(2); + } + s_crit_depth--; +} + +osStatus_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags) +{ + (void)ef_id; + con_evt_sets++; + con_evt_flags = flags; + return (osStatus_t)flags; +} + +/* 真实版是 CubeMX 生成的 HAL_UART_Init 封装;桩按脚本决定"软复位到底治没治好" */ +void MX_USART1_UART_Init(void) +{ + s_mx_init_calls++; + huart1.Instance = &g_con_uart; + huart1.gState = HAL_UART_STATE_READY; + huart1.ErrorCode = HAL_UART_ERROR_NONE; + if (s_mx_repair != 0) { + con_uart_txe_stuck = 0; + con_uart_txe_after = 0; + con_uart_txe_ready_left = -1; + con_uart_tc_after = 0; + con_uart_tc_stuck = 0; + } +} + + +void con_uart_reset(void) +{ + memset(&g_con_uart, 0xFF, sizeof(g_con_uart)); /* SR 恒全 1:就绪与否由钩子决定 */ + con_uart_tx_n = 0U; + con_uart_block_n = 0U; + con_uart_cur_len = 0U; + con_uart_txe_reads = 0U; + con_uart_tc_reads = 0U; + con_uart_txe_stuck = 0; + con_uart_txe_after = 0; + con_uart_txe_ready_left = -1; + con_uart_tc_after = 0; + con_uart_tc_stuck = 0; + con_uart_pending = 0; +} + +void con_dwt_reset(void) +{ + memset(&g_con_dwt, 0, sizeof(g_con_dwt)); + memset(&g_con_core_debug, 0, sizeof(g_con_core_debug)); + con_dwt_step = 0U; + con_dwt_ctrl_sticky = 1; + con_dwt_reads = 0U; +} + +/* ===================== 断言 ===================== */ +static int s_total, s_passed, s_failed; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* ===================== 被测代码(原样纳入生产源) ===================== + * ⚠ 唯一一处生产符号改名:**宿主 CRT 自己占用了 fputc 这个全局符号**(实测 + * libucrt.lib(fputc.obj) 与本 TU 冲突 -> LNK2005 / LNK1169,ARMCC + MicroLIB 上 + * 没有这件事,那里 fputc 正是留给用户实现的 retarget 钩子)。所以把被测函数临时 + * 改名后再纳入,函数体与行覆盖统计一字未动,测试直接调 con_fputc_under_test 验它。 + * 顺带回答"测试自己的 printf 会不会被这个钩子吞掉":不会——宿主 CRT 用的是内部 + * _fputc_nolock/_output 那条链,不解析用户态 fputc;TC-CONLOG-000 再钉一次。 */ +#define fputc con_fputc_under_test +#include "console_log.c" +#undef fputc + +/* ===================== 复位:生产 static + 假件,一次不漏 ===================== */ +static void reset_all(void) +{ + /* 生产模块的 static(10 个数据项 + 1 个任务句柄)——漏一个就会跨场景污染 */ + memset(s_con_ring, 0, sizeof(s_con_ring)); + s_con_head = 0U; + s_con_tail = 0U; + s_con_drop = 0U; + s_con_tx_fault = 0U; + s_con_tx_fault_log = 0U; + s_con_cool_until = 0U; + s_con_draining = 0U; + s_con_writer_up = 0U; + s_con_dwt_up = 0U; + s_con_writer_task = 0; + + con_uart_reset(); + con_dwt_reset(); + + con_tick_now = 0U; + con_tick_step = 0U; + con_tick_reads = 0U; + + con_task_self = (TaskHandle_t)&s_task_other; + con_delay_calls = 0U; + con_delay_hook = 0; + + con_crit_enter = 0U; + con_crit_exit = 0U; + con_crit_max_depth = 0U; + s_crit_depth = 0U; + + con_ipsr = 0U; + + sys_evtHandle = (osEventFlagsId_t)&s_evt_obj; + con_evt_sets = 0U; + con_evt_flags = 0U; + + con_nvic_clears = 0U; + con_rcc_disables = 0U; + con_rcc_enables = 0U; + s_mx_init_calls = 0U; + s_mx_repair = 0; + + huart1.Instance = &g_con_uart; + huart1.gState = HAL_UART_STATE_READY; + huart1.RxState = HAL_UART_STATE_READY; + huart1.ErrorCode = HAL_UART_ERROR_NONE; +} + +/* ===================== 观察辅助 ===================== */ +static uint32_t used_now(void) +{ + return con_used(); +} + +static const char *tx_at(uint32_t skip) +{ + return (skip >= con_uart_tx_n) ? "" : (const char *)&con_uart_tx[skip]; +} + +static int tx_is(uint32_t skip, const char *s) +{ + size_t n = strlen(s); + + if ((size_t)con_uart_tx_n < (size_t)skip + n) { + return 0; + } + return memcmp(tx_at(skip), s, n) == 0; +} + +/* 在已上线字节里找一段(tx 是二进制缓冲,不依赖 NUL 结尾) */ +static int tx_contains(const char *needle) +{ + size_t nl = strlen(needle); + uint32_t i; + + if ((size_t)con_uart_tx_n < nl) { + return 0; + } + for (i = 0U; (size_t)i + nl <= (size_t)con_uart_tx_n; i++) { + if (memcmp(&con_uart_tx[i], needle, nl) == 0) { + return 1; + } + } + return 0; +} + +static uint32_t tx_count(const char *needle) +{ + size_t nl = strlen(needle); + uint32_t i; + uint32_t hits = 0U; + + if ((size_t)con_uart_tx_n < nl) { + return 0U; + } + for (i = 0U; (size_t)i + nl <= (size_t)con_uart_tx_n; i++) { + if (memcmp(&con_uart_tx[i], needle, nl) == 0) { + hits++; + } + } + return hits; +} + +/* 写者任务上下文里排空(生产契约:drain 只允许写者任务调) */ +static void writer_drain(void) +{ + con_task_self = (TaskHandle_t)&s_task_writer; + dbg_console_drain(); + con_task_self = (TaskHandle_t)&s_task_other; +} + +/* 后台排空(不改当前任务句柄):模拟"生产者等一轮,写者腾出了空间" */ +static void bg_drain_hook(void) +{ + dbg_console_drain(); +} + +/* 把环填到只剩 want_free 字节可用:走真实入环路径(顺带踩到回绕)。 + * 填充期间把 DWT 上界调大——万一算差一位也不会卡在等待里出不来。 */ +static void fill_ring(uint32_t want_free) +{ + char pad[DBG_CON_LINE_MAX]; + uint32_t i; + + memset(pad, 'Z', sizeof(pad)); + con_dwt_step = SystemCoreClock / 4U; + for (i = 0U; i < 40U; i++) { + uint32_t free_space = (uint32_t)((DBG_CON_RING_SIZE - 1U) - used_now()); + uint32_t n; + + if (free_space <= want_free) { + break; + } + n = free_space - want_free; + if (n > DBG_CON_LINE_MAX) { + n = DBG_CON_LINE_MAX; + } + if (dbg_console_write_line(pad, n) != (int)n) { + break; + } + } + con_dwt_step = 0U; + s_con_drop = 0U; /* 填充过程中的边角丢弃不算场景断言 */ +} + +/* 注册写者(defaultTask),随后把"当前任务"换回别的任务 */ +static void attach_writer(void) +{ + con_task_self = (TaskHandle_t)&s_task_writer; + dbg_console_writer_attach(); + con_task_self = (TaskHandle_t)&s_task_other; +} +/* ============ TC-CONLOG-000..006 参数校验与截断 ============ */ +static void tc_params(void) +{ + char buf[DBG_CON_LINE_MAX + 128U]; + + printf("-- TC-CONLOG-000..006 参数校验与截断 --\n"); + reset_all(); + printf("(probe) 这行走宿主 CRT\n"); + fflush(stdout); + CHECK(con_uart_tx_n == 0U && used_now() == 0U, + "TC-CONLOG-000 宿主 printf 不经过本模块 fputc(否则测试输出会被吞进环)"); + + reset_all(); + con_uart_txe_stuck = 1; /* 通道卡死:任何字节都不该上线 */ + CHECK(dbg_console_write_line(0, 5U) == -1, "TC-CONLOG-001 write_line(NULL) 返回 -1"); + CHECK(dbg_console_write_line("abc", 0U) == -1, "TC-CONLOG-002 write_line(len=0) 返回 -1"); + dbg_console_write_raw(0, 5U); + dbg_console_write_raw("abc", 0U); + CHECK(con_uart_tx_n == 0U, "TC-CONLOG-003 两路 NULL/0 都不碰 UART"); + CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-004 参数非法不计入丢弃"); + + reset_all(); + memset(buf, 'A', sizeof(buf)); + CHECK(dbg_console_write_line(buf, sizeof(buf)) == (int)DBG_CON_LINE_MAX, + "TC-CONLOG-005 超过 DBG_CON_LINE_MAX 的行按 192 截断(返回值即交付字节数)"); + CHECK(con_uart_tx_n == DBG_CON_LINE_MAX, "TC-CONLOG-006 截断后恰好上线 192 字节"); +} + +/* ============ TC-CONLOG-010..018 写者未注册 = 无锁直发 ============ */ +static void tc_unattached(void) +{ + char big[DBG_CON_CHUNK_MAX + 64U]; + + printf("-- TC-CONLOG-010..018 写者未注册:无锁直发 --\n"); + reset_all(); + CHECK(dbg_console_writer_ready() == 0U, "TC-CONLOG-010 attach 之前 writer_ready() == 0"); + CHECK(dbg_console_write_line("hello", 5U) == 5, "TC-CONLOG-011 未注册时整行直发,返回行长"); + CHECK(tx_is(0U, "hello"), "TC-CONLOG-012 直发内容原样上线(没经过环形缓冲)"); + CHECK(used_now() == 0U, "TC-CONLOG-013 直发不入环(head == tail)"); + CHECK(con_evt_sets == 0U, "TC-CONLOG-014 直发不置 SYS_EVT_CONSOLE_TX(还没写者可唤醒)"); + CHECK(con_crit_enter == 0U, + "TC-CONLOG-015 直发零临界区 = 2026-10-10 逐字节交错的根因(不是丢整行)"); + + reset_all(); + memset(big, 'B', sizeof(big)); + dbg_console_write_raw(big, sizeof(big)); + CHECK(con_uart_tx_n == DBG_CON_CHUNK_MAX, "TC-CONLOG-016 write_raw 有界:只发 256 字节"); + + reset_all(); + con_uart_txe_after = 3; /* 前 3 次不就绪(TXE 抖动,未超上界) */ + CHECK(dbg_console_write_line("CD", 2U) == 2, "TC-CONLOG-017 TXE 抖动后仍发全"); + CHECK(tx_is(0U, "CD") && con_delay_calls == 0U, + "TC-CONLOG-018 直发既不等 tick 也不入环(ISR/崩溃钩子可用的前提)"); +} + +/* ============ TC-CONLOG-020..037 注册后走环形缓冲 ============ */ +static void tc_ring(void) +{ + char line[DBG_CON_LINE_MAX]; + static char oversize[3000]; /* 超长入环的源(>= 钳位后的 2048B) */ + uint32_t i; + + printf("-- TC-CONLOG-020..027 注册后:生产者入环、写者发送 --\n"); + reset_all(); + attach_writer(); + CHECK(dbg_console_writer_ready() == 1U, "TC-CONLOG-020 attach 之后 writer_ready() == 1"); + CHECK(s_con_writer_task == (TaskHandle_t)&s_task_writer, + "TC-CONLOG-021 attach 记下的正是调用方的任务句柄(自等保护的依据)"); + CHECK(dbg_console_write_line("netTask up\r\n", 12U) == 12, "TC-CONLOG-022 入环成功返回行长"); + CHECK(con_uart_tx_n == 0U, "TC-CONLOG-023 生产者绝不碰 UART"); + CHECK(used_now() == 12U, "TC-CONLOG-024 整行一次入环(used = 12)"); + CHECK(con_evt_sets == 1U && con_evt_flags == SYS_EVT_CONSOLE_TX, + "TC-CONLOG-025 入环后置真实事件位 SYS_EVT_CONSOLE_TX(bit6)唤醒写者"); + CHECK(con_crit_enter == 1U && con_crit_exit == 1U && con_crit_max_depth == 1U, + "TC-CONLOG-026 整行入环恰好一层临界区(行级原子的实现依据)"); + + writer_drain(); + CHECK(tx_is(0U, "netTask up\r\n"), "TC-CONLOG-027 写者排空:上线字节与入环内容逐字节相同"); + + printf("-- TC-CONLOG-028..032 分块 256B 与多生产者不交错 --\n"); + reset_all(); + attach_writer(); + memset(line, 'C', sizeof(line)); + for (i = 0U; i < 5U; i++) { + (void)dbg_console_write_line(line, sizeof(line)); + } + writer_drain(); + CHECK(con_uart_block_n == 4U, "TC-CONLOG-028 960B 按 DBG_CON_CHUNK_MAX 切成 4 块"); + CHECK(con_uart_block_len[0] == 256U && con_uart_block_len[1] == 256U + && con_uart_block_len[2] == 256U && con_uart_block_len[3] == 192U, + "TC-CONLOG-029 块长 256/256/256/192(单次发送上限真被执行)"); + CHECK(con_uart_tx_n == 960U && con_uart_txe_reads == 960U, + "TC-CONLOG-030 每字节恰好装载一次 DR(就绪即写,没有重复发)"); + + reset_all(); + attach_writer(); + con_task_self = (TaskHandle_t)&s_task_other; + (void)dbg_console_write_line("[A] 11111\r\n", 11U); + con_task_self = (TaskHandle_t)&s_task_writer; /* 写者自己也记一行 -> 先排空 */ + (void)dbg_console_write_line("[B] 22222\r\n", 11U); + con_task_self = (TaskHandle_t)0; /* 连"无句柄"也算一个生产者 */ + (void)dbg_console_write_line("[C] 33333\r\n", 11U); + writer_drain(); + CHECK(tx_is(0U, "[A] 11111\r\n[B] 22222\r\n[C] 33333\r\n"), + "TC-CONLOG-031 三个生产者各写一行 -> 上线仍是三行连续原文(G1 行级原子)"); + CHECK(strstr((const char *)&con_uart_tx[5], "[B] 22222") != 0, + "TC-CONLOG-032 行边界没被打断(没有任何一行被插进另一行中间)"); + + printf("-- TC-CONLOG-033..037 跨环尾:两段拷贝 + 按连续段分块 --\n"); + reset_all(); + attach_writer(); + s_con_head = 4090U; + s_con_tail = 4090U; + (void)dbg_console_write_line("0123456789ABCDEF", 16U); + CHECK(s_con_head == 10U && used_now() == 16U, + "TC-CONLOG-033 跨环尾入环后 head 回绕到 10(4090+16-4096)"); + CHECK(memcmp(&s_con_ring[4090], "012345", 6) == 0 + && s_con_ring[0] == (uint8_t)'6' && s_con_ring[9] == (uint8_t)'F' + && s_con_ring[10] == 0U, + "TC-CONLOG-034 回绕两段拷贝位置正确(前 6B 填到环尾、余 10B 从 0 起)"); + writer_drain(); + CHECK(con_uart_block_n == 2U && con_uart_block_len[0] == 6U + && con_uart_block_len[1] == 10U, + "TC-CONLOG-035 drain 按连续段分块:6 + 10(绝不把环中间当连续段读越界)"); + CHECK(tx_is(0U, "0123456789ABCDEF"), "TC-CONLOG-036 回绕行的上线内容与顺序都对"); + + reset_all(); + attach_writer(); + /* 源缓冲必须给到 3000B:con_append 只钳长度,不会钳读取范围, + * 拿 192B 的栈数组当 2048B 源就是越界读(插桩跑直接 0xC0000005) */ + memset(oversize, 'D', sizeof(oversize)); + CHECK(con_append(oversize, (uint16_t)3000U) == 0, "TC-CONLOG-037 直调 con_append 超长仍成功"); + CHECK(used_now() == (DBG_CON_RING_SIZE / 2U), + "TC-CONLOG-038 入环长度钳在 RING/2=2048(保证任何一行都恒可入环)"); +} + +/* ============ TC-CONLOG-040..042 写者自记录 / 重入 / 未注册 drain ============ */ +static void tc_writer_self(void) +{ + char line[DBG_CON_LINE_MAX]; + uint32_t i; + + printf("-- TC-CONLOG-040..042 写者自记录先排空 / drain 重入保护 --\n"); + reset_all(); + attach_writer(); + memset(line, 'E', sizeof(line)); + for (i = 0U; i < 21U; i++) { /* 21*192 = 4032,只剩 63B 可用 */ + (void)dbg_console_write_line(line, sizeof(line)); + } + con_task_self = (TaskHandle_t)&s_task_writer; /* 写者自己记一行 */ + CHECK(dbg_console_write_line("F", 1U) == 1, "TC-CONLOG-040 写者自记录不丢行"); + CHECK(con_delay_calls == 0U, + "TC-CONLOG-041 写者自记录先排空再入环:绝不进等待(否则就是等自己 -> 死锁)"); + CHECK(con_uart_tx_n == 4032U && used_now() == 1U && dbg_console_dropped() == 0U, + "TC-CONLOG-042 老行全部上线、新行入环,零丢弃"); + + reset_all(); + attach_writer(); + (void)dbg_console_write_line("hold", 4U); + s_con_draining = 1U; /* 模拟正处于 drain 之内 */ + writer_drain(); + CHECK(con_uart_tx_n == 0U && used_now() == 4U, + "TC-CONLOG-043 drain 重入保护:嵌套调用直接让位(写者自记录不触发递归 drain)"); + + reset_all(); + (void)dbg_console_write_line("early\r\n", 7U); /* 未注册 -> 已直发 */ + con_uart_tx_n = 0U; + writer_drain(); + CHECK(con_uart_tx_n == 0U, "TC-CONLOG-044 写者未注册时 drain 不做任何事"); +} + +/* ============ TC-CONLOG-050..062 缓冲满 / 丢弃 / 可观测 ============ */ +static void tc_full_drop(void) +{ + printf("-- TC-CONLOG-050..053 缓冲满:DWT 硬上界丢弃且不覆盖旧行 --\n"); + reset_all(); + attach_writer(); + fill_ring(10U); /* 只剩 10B 可用 */ + con_dwt_step = SystemCoreClock / 4U; /* 一次判据 = 250ms > 200ms 上界 */ + CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == -1, + "TC-CONLOG-050 环满 + DWT 上界到 -> 丢这一行(返回 -1)"); + CHECK(dbg_console_dropped() == 1U, "TC-CONLOG-051 丢弃计数 +1"); + CHECK(con_delay_calls == 0U, + "TC-CONLOG-052 有 DWT 时用周期判据,不必先烧 200 个 tick(K4)"); + CHECK(s_con_head == 4085U && used_now() == 4085U && con_uart_tx_n == 0U, + "TC-CONLOG-053 丢弃不推进 head、也不生产者自己发(没半写、更没覆盖未发出的旧行)"); + + printf("-- TC-CONLOG-054..056 等待中出现空间 -> 入环成功 --\n"); + reset_all(); + attach_writer(); + fill_ring(10U); + con_dwt_step = 0U; /* 时间静止:只能靠写者腾地方 */ + con_delay_hook = bg_drain_hook; /* 每等一轮 = 写者排空一次 */ + CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == 20, + "TC-CONLOG-054 等一轮后腾出空间 -> 整行入环成功"); + CHECK(con_delay_calls == 1U && dbg_console_dropped() == 0U, + "TC-CONLOG-055 只等了一轮,且不算丢弃"); + con_delay_hook = 0; + CHECK(con_uart_tx_n == 4085U, "TC-CONLOG-056 腾地方的正是写者(老行已上线)"); + + printf("-- TC-CONLOG-057..059 无 DWT 支路:等待上界 = DBG_CON_WAIT_MS 个 tick --\n"); + reset_all(); + con_dwt_ctrl_sticky = 0; /* 调试单元被关 -> DWT 使能失败 */ + attach_writer(); + fill_ring(10U); + CHECK(s_con_dwt_up == 0U, "TC-CONLOG-057 DWT 使能失败时 s_con_dwt_up 保持 0"); + CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == -1, + "TC-CONLOG-058 无 DWT 时仍会丢(判据退回 tick 计数,不会无限等)"); + CHECK(con_delay_calls == DBG_CON_WAIT_MS && dbg_console_dropped() == 1U, + "TC-CONLOG-059 无 DWT 的等待上界恰为 DBG_CON_WAIT_MS(200) 次 vTaskDelay"); + + printf("-- TC-CONLOG-060..062 丢弃可观测:[ERR] 行直发不经环 --\n"); + reset_all(); + attach_writer(); + fill_ring(10U); + con_dwt_step = SystemCoreClock / 4U; + (void)dbg_console_write_line("0123456789ABCDEFGHIJ", 20U); /* 丢 1 行 */ + con_dwt_step = 0U; + writer_drain(); + CHECK(tx_contains("[ERR] CONSOLE dropped 1 lines"), + "TC-CONLOG-060 排空末尾刷出 [ERR] CONSOLE dropped 1 lines(丢行永不静默,@test 判 FAIL)"); + CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-061 刷完即清零(下一轮不重复告警)"); + CHECK(used_now() == 0U, + "TC-CONLOG-062 告警行走裸发:不入环 -> 不会 con_append->drain 递归"); +} +/* ============ TC-CONLOG-070..081 TX 僵尸通道:自愈 / 冷却 / 半块 ============ */ + +/* 公共前置:注册写者 + 把环填满 + 让通道卡死 */ +static void fault_setup(uint32_t tick0, int repair) +{ + attach_writer(); + fill_ring(0U); + con_tick_now = tick0; + con_uart_txe_stuck = 1; + con_dwt_step = SystemCoreClock / 4U; /* 一次判据就跨过 CON_TX_TIMEOUT_CYC */ + s_mx_repair = repair; + writer_drain(); +} + +static void tc_tx_fault(void) +{ + printf("-- TC-CONLOG-070..074 软复位没治好:故障即止 + 留痕只能靠标志 --\n"); + reset_all(); + fault_setup(5000U, 0); + CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-070 发送超时 -> tx_fault 计数 +1"); + CHECK(s_mx_init_calls == 1U && con_nvic_clears == 1U + && con_rcc_disables == 1U && con_rcc_enables == 1U, + "TC-CONLOG-071 自愈完整一遍:清 pending -> 断时钟 -> 供时钟 -> 重初始化"); + CHECK(used_now() == (DBG_CON_RING_SIZE - 1U) - DBG_CON_CHUNK_MAX, + "TC-CONLOG-072 故障即止:只丢当前 256B 块,其余留环里等下轮(让位喂狗)"); + CHECK(con_uart_tx_n == 0U, + "TC-CONLOG-073 通道没治好时连那条 WARN 也上不了线(裸发同样有界、不重试)"); + CHECK(dbg_console_tx_fault_log_take() == 1U && dbg_console_tx_fault_log_take() == 0U, + "TC-CONLOG-074 留痕标志读后清:写者据此在 drain 之外补记 run.log(断调用图成环)"); + + printf("-- TC-CONLOG-075..077 软复位治好了:WARN 上线且只一次 --\n"); + reset_all(); + con_tick_now = 7000U; + attach_writer(); + (void)dbg_console_write_line("X", 1U); + con_uart_txe_ready_left = 0; /* 一装载就卡死 */ + con_dwt_step = SystemCoreClock / 4U; + s_mx_repair = 1; /* 重初始化把通道治好 */ + writer_drain(); + CHECK(tx_contains("[WARN] CONSOLE tx fault"), "TC-CONLOG-075 首次故障的 WARN 行成功上线"); + CHECK(used_now() == 0U && s_con_cool_until == 7000U && dbg_console_tx_fault() == 1U, + "TC-CONLOG-076 故障块丢弃(行已出环不可重放)+ 冷却截止记为当时 tick"); + con_uart_txe_ready_left = -1; + con_dwt_step = 0U; + writer_drain(); /* tick 未推进 -> 仍在冷却 */ + CHECK(s_con_cool_until == 7000U && s_mx_init_calls == 1U, + "TC-CONLOG-077 冷却期内 drain 直接让位:不重试、不自愈第二次"); +} + +static void tc_tx_cool(void) +{ + printf("-- TC-CONLOG-078..084 300ms 冷却两端 + 回绕 + tick==0 现状 --\n"); + reset_all(); + fault_setup(7000U, 1); /* 治好的那次故障 */ + CHECK(s_con_cool_until == 7000U && used_now() == 3839U, + "TC-CONLOG-078 故障后剩 3839B 在环里(本块被丢)"); + con_dwt_step = 0U; + con_uart_txe_stuck = 0; + con_tick_now = 7299U; /* 差 1ms */ + writer_drain(); + CHECK(s_con_cool_until == 7000U && used_now() == 3839U && s_mx_init_calls == 1U, + "TC-CONLOG-079 差 1ms 仍在冷却:环不动、不重复自愈(冷却即节奏保护)"); + con_tick_now = 7300U; /* 到点 */ + writer_drain(); + CHECK(s_con_cool_until == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U, + "TC-CONLOG-080 到点即清冷却、放行本轮排空,且没有新增故障"); + + reset_all(); + fault_setup(0U, 0); + CHECK(s_con_cool_until == 0U && dbg_console_tx_fault() == 1U, + "TC-CONLOG-081 现状:tick==0 时记出的 cool_until 撞上 0 哨兵 = 等于没冷却"); + writer_drain(); + CHECK(dbg_console_tx_fault() == 2U + && used_now() == (DBG_CON_RING_SIZE - 1U) - 2U * DBG_CON_CHUNK_MAX, + "TC-CONLOG-082 上一条的后果:下一轮立刻再耗一次块超时(节奏保护在这一 tick 失效)"); + + reset_all(); + attach_writer(); + fill_ring(0U); + s_con_cool_until = 0xFFFFFFFFU; /* 上轮故障记在回绕点 */ + con_tick_now = 100U; /* 现在刚回绕到 100 */ + writer_drain(); + CHECK(s_con_cool_until == 0xFFFFFFFFU && used_now() == 4095U, + "TC-CONLOG-083 跨 49.7 天回绕仍判在冷却内(无符号差值 101 < 300)"); + con_tick_now = 401U; /* 无符号差值 402 >= 300 */ + writer_drain(); + CHECK(s_con_cool_until == 0U && used_now() == 0U, + "TC-CONLOG-084 回绕后到点 -> 清冷却并把 4095B 全排空"); +} + +static void tc_tx_half(void) +{ + printf("-- TC-CONLOG-085..088 超时判据的真实后果:半块 / 已上线仍判失败 --\n"); + reset_all(); + attach_writer(); + (void)dbg_console_write_line("ABCDEFGH", 8U); + con_uart_tc_stuck = 1; /* 字节都进了 DR,只有 TC 不置位 */ + con_dwt_step = SystemCoreClock / 4U; + writer_drain(); + CHECK(tx_is(0U, "ABCDEFGH"), "TC-CONLOG-085 TC 卡死:8 字节都已上线"); + CHECK(dbg_console_tx_fault() == 1U && con_uart_block_n == 0U, + "TC-CONLOG-086 已上线仍整块判失败(docs §13-O2:丢弃/故障两本账没分开的现状)"); + + reset_all(); + attach_writer(); + (void)dbg_console_write_line("ABCDEFGH", 8U); + con_uart_txe_ready_left = 3; /* 只挤得出 3 个字节 */ + con_dwt_step = SystemCoreClock / 4U; + writer_drain(); + CHECK(tx_is(0U, "ABC"), "TC-CONLOG-087 TXE 中途卡死 -> 只有已写进 DR 的 3 字节上线"); + CHECK(dbg_console_tx_fault() == 1U && used_now() == 0U, + "TC-CONLOG-088 半块同样走自愈分支,整块出环(尾部 5 字节随块作废)"); +} + +/* ============ TC-CONLOG-090..099 DWT 使能 / 指令计数上界 ============ */ +static void tc_dwt(void) +{ + printf("-- TC-CONLOG-090..093 DWT 惰性使能与就绪即走 --\n"); + reset_all(); + (void)dbg_console_write_line("ok\r\n", 4U); /* 走一次直发 -> 顺带使能 DWT */ + CHECK(s_con_dwt_up == 1U, "TC-CONLOG-090 con_dwt_ensure 成功使能后置 s_con_dwt_up"); + CHECK((g_con_core_debug.DEMCR & CoreDebug_DEMCR_TRCENA_Msk) != 0U, + "TC-CONLOG-091 真的写了 DEMCR.TRCENA(位号与 core_cm4.h 一致)"); + CHECK((g_con_dwt.CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0U, + "TC-CONLOG-092 真的写了 DWT.CTRL.CYCCNTENA"); + CHECK(tx_is(0U, "ok\r\n") && con_dwt_reads == 4U, + "TC-CONLOG-093 通道就绪时发送循环没再读 CYCCNT(3 次 ensure + 1 次取基准 t0)"); + + printf("-- TC-CONLOG-094..097 DWT 使能失败 -> con_tx_counted 计数上界 --\n"); + reset_all(); + con_dwt_ctrl_sticky = 0; /* CYCCNTENA 写不进去 */ + con_uart_txe_stuck = 1; + dbg_console_write_raw("Q", 1U); + CHECK(s_con_dwt_up == 0U, "TC-CONLOG-094 DWT 使能失败 -> 走计数上界支路"); + CHECK(con_uart_txe_reads == CON_TX_GUARD_MAX, + "TC-CONLOG-095 每字节最多自旋 CON_TX_GUARD_MAX(20000) 次即放弃(有界,非死等)"); + CHECK(con_uart_tx_n == 0U, "TC-CONLOG-096 计数上界内没发出去就整块放弃"); + CHECK(con_dwt_reads == 3U, + "TC-CONLOG-097 计数支路一次都不读 CYCCNT(不依赖 DWT,也就不依赖时间源)"); + + printf("-- TC-CONLOG-098..099 超时判据不依赖 HAL_GetTick(round 8b 的动机) --\n"); + reset_all(); + con_uart_txe_stuck = 1; /* tick 冻结在 0,模拟 TIM7 停摆 */ + con_dwt_step = SystemCoreClock / 10U; /* 一次判据正好 = 100ms 上界 */ + dbg_console_write_raw("Q", 1U); + CHECK(con_tick_reads == 0U && con_uart_tx_n == 0U, + "TC-CONLOG-098 tick 完全不走也能退出:裸发一次都没读 HAL_GetTick"); + reset_all(); + con_uart_txe_stuck = 1; + con_dwt_step = SystemCoreClock / 4U; + dbg_console_write_raw("Z", 1U); + CHECK(con_uart_txe_reads == 1U, + "TC-CONLOG-099 DWT 支路第一次自旋就判超时(上界真的起作用,不会拖满 20000 次)"); +} + +/* ============ TC-CONLOG-100..104 huart1.Instance == NULL 短路 ============ */ +static void tc_null_uart(void) +{ + printf("-- TC-CONLOG-100..104 USART1 未初始化:安全让位,不再触发二次故障 --\n"); + reset_all(); + huart1.Instance = 0; + dbg_console_write_raw("boom", 5U); + CHECK(con_uart_tx_n == 0U && con_uart_txe_reads == 0U, + "TC-CONLOG-100 Instance==NULL 时一次寄存器都不碰(不解引用地址 0)"); + CHECK(con_dwt_reads == 3U, + "TC-CONLOG-101 仍然先惰性使能 DWT 再短路(ensure 在判空之前,顺序照旧)"); + + reset_all(); + huart1.Instance = 0; + CHECK(dbg_console_write_line("early", 5U) == 5, + "TC-CONLOG-102 未注册路径(内核前)撞上无串口也不崩、仍返回行长"); + + reset_all(); + attach_writer(); + (void)dbg_console_write_line("abc", 3U); + huart1.Instance = 0; /* 写者起来了但串口被毁 */ + s_mx_repair = 1; + writer_drain(); + CHECK(dbg_console_tx_fault() == 1U && s_mx_init_calls == 1U && huart1.Instance != 0, + "TC-CONLOG-103 未初始化也被当作僵尸通道自愈:重初始化把句柄装回来"); + con_dwt_step = SystemCoreClock / 4U; + writer_drain(); /* 仍在冷却 -> 让位 */ + CHECK(used_now() == 0U, "TC-CONLOG-104 自愈那一轮已把出环字节丢掉,没留下脏数据"); +} + +/* ============ TC-CONLOG-110..115 fputc 重定向 ============ */ +static void tc_fputc(void) +{ + printf("-- TC-CONLOG-110..115 fputc 重定向(printf 遗留路径) --\n"); + reset_all(); + CHECK(con_fputc_under_test('Q', stdout) == 'Q', "TC-CONLOG-110 fputc 原样返回字符(C 库约定)"); + CHECK(tx_is(0U, "Q") && used_now() == 0U, + "TC-CONLOG-111 未注册写者:printf 的字节直发(与旧行为一致)"); + + reset_all(); + attach_writer(); + (void)con_fputc_under_test('R', stdout); + CHECK(con_uart_tx_n == 0U && used_now() == 1U && s_con_ring[0] == (uint8_t)'R', + "TC-CONLOG-112 注册后 fputc 也收敛到单写者路径(逐字节入环)"); + writer_drain(); + CHECK(tx_is(0U, "R"), "TC-CONLOG-113 写者把 printf 字节发出"); + + reset_all(); + attach_writer(); + con_ipsr = 12U; /* 假异常号:任意非 0 */ + CHECK(dbg_console_write_line("ISR", 3U) == 3, "TC-CONLOG-114 中断上下文写整行返回行长"); + CHECK(tx_is(0U, "ISR") && used_now() == 0U && con_delay_calls == 0U, + "TC-CONLOG-115 中断上下文一律裸发:不入环、不等待(否则在 ISR 里睡死)"); + + reset_all(); + attach_writer(); + fill_ring(0U); /* 环全满 */ + con_ipsr = 5U; + con_dwt_step = SystemCoreClock / 4U; + CHECK(dbg_console_write_line("0123456789", 10U) == 10, + "TC-CONLOG-116 环全满 + 中断上下文:仍直发成功(ISR 绝不进丢弃等待)"); + CHECK(dbg_console_dropped() == 0U && con_delay_calls == 0U, + "TC-CONLOG-117 这条路径既不丢行也不记丢弃(K1:内核前/ISR 不依赖任何内核对象)"); +} + +/* ============ TC-CONLOG-120..122 drain 250ms 预算 ============ */ +static void tc_budget(void) +{ + printf("-- TC-CONLOG-120..122 单次 drain 预算 250ms(让位喂狗轮询) --\n"); + reset_all(); + attach_writer(); + fill_ring(0U); /* 4095B = 16 块 */ + con_tick_step = 130U; /* 每次读 tick 走 130ms */ + writer_drain(); + CHECK(con_uart_block_n == 2U, "TC-CONLOG-120 一轮只发 2 块就因预算让位(不是发完)"); + CHECK(used_now() == (DBG_CON_RING_SIZE - 1U) - (2U * DBG_CON_CHUNK_MAX), + "TC-CONLOG-121 让位时余下 3583B 留在环里(下轮续排,不丢)"); + con_tick_step = 0U; + writer_drain(); /* 预算内发完余下 3583B */ + CHECK(used_now() == 0U && con_uart_tx_n == (DBG_CON_RING_SIZE - 1U), + "TC-CONLOG-122 下轮把余下的 3583B 也发完:预算让位不丢字节(累计 4095)"); +} + +/* ============ TC-CONLOG-130..135 计数支路的成功路径 / TC 抖动 / 直调 NULL ============ */ +static void tc_counted_paths(void) +{ + printf("-- TC-CONLOG-130..132 DWT 不可用 -> con_tx_counted 也能发全(不只会超时) --\n"); + reset_all(); + con_dwt_ctrl_sticky = 0; /* CYCCNTENA 落不住 */ + con_tick_now = 9000U; /* tick 冻结:证明与本支路无关 */ + dbg_console_write_raw("AB", 2U); + CHECK(s_con_dwt_up == 0U && tx_is(0U, "AB"), + "TC-CONLOG-130 计数支路把整块发完并返回成功(回退路径没退化)"); + CHECK(con_uart_tc_reads == 1U && con_dwt_reads == 3U && con_tick_reads == 0U, + "TC-CONLOG-131 计数支路既不等 tick 也不再读 CYCCNT(零外部时间源依赖)"); + + reset_all(); + con_dwt_ctrl_sticky = 0; + con_uart_tc_stuck = 1; /* 字节能进 DR,TC 永不置位 */ + dbg_console_write_raw("CD", 2U); + CHECK(tx_is(0U, "CD") && con_uart_tc_reads == CON_TX_GUARD_MAX, + "TC-CONLOG-132 TC 卡死时计数支路同样最多自旋 CON_TX_GUARD_MAX 次即放弃"); + + printf("-- TC-CONLOG-133..135 DWT 支路 TC 抖动 / con_append 直调参数校验 --\n"); + reset_all(); + con_dwt_step = 1U; /* 时间走得极慢:不会误判超时 */ + con_uart_tc_after = 2; /* 前两轮 TC 不就绪 */ + dbg_console_write_raw("EF", 2U); + CHECK(tx_is(0U, "EF") && con_uart_tc_reads == 3U, + "TC-CONLOG-133 TC 抖动(未超上界)后仍算整块成功:TC 循环能正常退出"); + reset_all(); + CHECK(con_append(0, 3U) == -1, "TC-CONLOG-134 con_append(NULL) 自己也要挡住(不只靠上层)"); + CHECK(con_append("abc", 0U) == -1, "TC-CONLOG-135 con_append(len=0) 同样拒绝"); +} + +/* ===================== main ===================== */ +int main(void) +{ + /* 先验一次宿主 CRT 与本模块 fputc 的关系(被 tc_params 记成 TC-CONLOG-000) */ + tc_params(); + tc_unattached(); + tc_ring(); + tc_writer_self(); + tc_full_drop(); + tc_tx_fault(); + tc_tx_cool(); + tc_tx_half(); + tc_dwt(); + tc_null_uart(); + tc_fputc(); + tc_budget(); + tc_counted_paths(); + + printf("=== Console Log Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + fflush(stdout); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/console_log/run_unit_test.py b/test/host/console_log/run_unit_test.py new file mode 100644 index 00000000..f919e9a1 --- /dev/null +++ b/test/host/console_log/run_unit_test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""控制台单写者日志(console_log)主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/console_log/console_log_test.c(以 #include 纳入生产源 + App/log/console_log.c),在 PC 上离线验证单写者环形缓冲的全套契约:192B 行截断、 + 256B 分块、跨环尾两段拷贝、写者未注册/ISR 上下文的无锁直发、缓冲满两条上界 + (DWT 周期 / 200 tick)与丢弃计数、[ERR] 告警行不经环(不递归)、TX 僵尸通道软复位 + 自愈 + 故障即止 + 300ms 冷却(含无符号回绕)、DWT 使能失败退回 20000 次指令计数、 + huart1.Instance==NULL 短路、fputc 重定向、drain 250ms 预算。 + 工具链与判定委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只用台架): + 本模块的坑全是"异常时刻 + 节奏":ORE 僵尸通道在软复位后毫秒级重锁、TIM7 停摆让 + HAL 的 tick 超时永不成立(round 8a 台架:唯一喂狗者死等 TXE -> IWDG 误复位)、 + 背靠背块超时把主循环拉到 450~500ms 饿死喂狗(台架实测的停喂误复位)。这些在台架上 + 要么一年一遇、要么现场已被复位冲掉;把 100/250/300ms 三条时间线做成显式量之后, + 299ms 与 300ms 的差别就是一行断言。详见 + docs/子系统设计/控制台单写者日志设计说明.md(G1~G5 / K1~K5 / §12 ORE 僵尸通道)。 + +桩的边界:console_log.h 与 sys_events.h 用真实头(SYS_EVT_CONSOLE_TX 位定义一改即 + 编译期报错);usart.h 是桩但只暴露本模块访问的 huart1 + MX_USART1_UART_Init, + USART SR 位号、DWT/CoreDebug 位掩码、HAL_StatusTypeDef 枚举值均照抄真实头, + 宿主编不动的那条 main.h -> stm32f4xx_hal.h -> core_cm4.h 链由 stub/stm32f4_fake.h + 以普通结构体替代(理由写在该文件头注释里)。 + ⚠ 测试单元刻意不定义任何 syslog_* / DBG_* 符号:生产源哪天恢复"在 drain 里调日志 + 子系统",本套件会以链接失败告警(那是设计上必须挡住的成环)。 + +用法(仓库根目录): + python test/host/console_log/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:docs/子系统设计/控制台单写者日志设计说明.md、docs/测试说明/主机单测覆盖率口径.md §9「P2 落地」清单第 3 条 +创建日期:2026-10-24 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 必须最先(覆盖 usart.h / FreeRTOS.h / task.h / cmsis_os.h / cmsis_os2.h), +# 其余是真实头目录(console_log.h 在 App/log,sys_events.h 在 App/app)。 +BUILD = TestBuild( + name="Console Log Tests", + src=os.path.join(HERE, "console_log_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "log"), + os.path.join(ROOT, "App", "app"), + HERE, + ], + summary_marker="=== Console Log Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/console_log/stub/FreeRTOS.h b/test/host/console_log/stub/FreeRTOS.h new file mode 100644 index 00000000..e5b70e82 --- /dev/null +++ b/test/host/console_log/stub/FreeRTOS.h @@ -0,0 +1,16 @@ +/* + * 模块名称:主机侧单测桩 - FreeRTOS.h(console_log 模块专用) + * 模块功能:TaskHandle_t / TickType_t 与临界区两个宏(真实版来自 FreeRTOS.h + + * portmacro.c 的 vPortEnterCritical/vPortExitCritical)。宿主单线程 -> + * 临界区退化成计数,但**配对与层数照旧可断言**:本模块三处临界区 + * (整行入环、推进 tail、drain 每块一次)的原子性语义靠计数核对。 + * ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef FREERTOS_H +#define FREERTOS_H + +#include +#include "stm32f4_fake.h" + +#endif /* FREERTOS_H */ diff --git a/test/host/console_log/stub/cmsis_os.h b/test/host/console_log/stub/cmsis_os.h new file mode 100644 index 00000000..b9fb8cad --- /dev/null +++ b/test/host/console_log/stub/cmsis_os.h @@ -0,0 +1,13 @@ +/* + * 模块名称:主机侧单测桩 - cmsis_os.h(console_log 模块专用) + * 模块功能:空壳(真实那份是 CMSIS-RTOS v1 兼容层)。console_log.c include 它但 + * 一个 v1 API 都不用;事件组走的是 cmsis_os2.h 的 osEventFlagsSet。 + * ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef CMSIS_OS_H +#define CMSIS_OS_H + +#include + +#endif /* CMSIS_OS_H */ diff --git a/test/host/console_log/stub/cmsis_os2.h b/test/host/console_log/stub/cmsis_os2.h new file mode 100644 index 00000000..8ac1cf80 --- /dev/null +++ b/test/host/console_log/stub/cmsis_os2.h @@ -0,0 +1,18 @@ +/* + * 模块名称:主机侧单测桩 - cmsis_os2.h(console_log 模块专用) + * 模块功能:给真实 App/app/sys_events.h 一个能在宿主上编的最小内核类型面: + * osEventFlagsId_t / osStatus_t / 事件位选项宏。sys_events.h 只需要 + * osEventFlagsId_t 来声明 sys_evtHandle,而 SYS_EVT_* 位定义全是真实值 + * —— 测试断言"入环后必须置 SYS_EVT_CONSOLE_TX(bit6)"用的就是真实宏。 + * osEventFlagsSet 原型与真实头逐字一致,实体在 stm32f4_fake.h 声明、 + * 由测试单元实现(计数 + 记录最后一次掩码)。 + * ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建(真实那份拉 FreeRTOS 内核头)。 + * 创建日期:2026-10-24 + */ +#ifndef CMSIS_OS2_H +#define CMSIS_OS2_H + +#include +#include "stm32f4_fake.h" + +#endif /* CMSIS_OS2_H */ diff --git a/test/host/console_log/stub/stm32f4_fake.h b/test/host/console_log/stub/stm32f4_fake.h new file mode 100644 index 00000000..88b79bd8 --- /dev/null +++ b/test/host/console_log/stub/stm32f4_fake.h @@ -0,0 +1,274 @@ +/* + * 模块名称:主机侧单测桩 - 假 HAL/CMSIS 寄存器面 + 内核入口(console_log 模块专用) + * 模块功能:让 App/log/console_log.c 在 PC 上编起来,并把它依赖的硬件/内核面做成 + * **可脚本控制**的模型: + * ① USART1:TXE/TC 可注入「就绪 / 永远不就绪(僵尸通道)/ 前 N 次不就绪」; + * 写进 DR 的字节被收进一条上线字节日志,并按「一次 TC 就绪 = 一块」记块长。 + * ② DWT CYCCNT:每次访问推进 con_dwt_step 个周期(= 给时间定价), + * CTRL 的 CYCCNTENA 可脚本成「写不进去」以走 con_tx_counted 支路。 + * ③ HAL_GetTick:显式量 + 每次读自动步进。 + * ④ 内核入口:临界区计数、当前任务句柄、vTaskDelay(可挂回调 + 跑飞保险)、 + * __get_IPSR、osEventFlagsSet、NVIC/RCC 宏。 + * ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建(MCU 走真实 stm32f4xx*.h)。 + * + * 为什么必须桩(换机复核依据):真实链是 usart.h -> main.h -> stm32f4xx_hal.h -> + * stm32f4xx.h -> core_cm4.h,里面是 ARMCC 内联汇编与 __IO 寄存器绝对地址 + * (0xE0001000 一类),宿主 MSVC 既编不动也访问不了。本桩把同样的位语义搬到普通 + * 结构体上,字段名 / 位号 / 枚举值一律照真实头抄,生产代码写法一字不改即可编译: + * USART_TypeDef.SR/DR、USART_SR_TXE=bit7、USART_SR_TC=bit6(stm32f4xx_hal_uart.h) + * DWT_Type.CTRL/CYCCNT + DWT_CTRL_CYCCNTENA_Msk=bit0、CoreDebug_Type.DEMCR + + * CoreDebug_DEMCR_TRCENA_Msk=bit24(core_cm4.h) + * HAL_StatusTypeDef{OK=0,ERROR=1,BUSY=2,TIMEOUT=3}(stm32f4xx_hal_def.h) + * HAL_UART_STATE_RESET=0x00、HAL_UART_ERROR_NONE=0x00、USART1_IRQn=7 + * UART_HandleTypeDef 只搬本模块访问的 4 个字段(Instance/gState/RxState/ErrorCode), + * 余下(Init/Lock/...)刻意不搬:抄半份 HAL 结构只会更快与真实头漂移。 + * + * ★ 两个手法(本桩唯一的技巧点,改之前先读懂): + * a) 「读寄存器位」没法用结构体成员钩(u->SR 是成员访问,宏替换不了成员名),但生产 + * 代码的判据写作 (u->SR & USART_SR_TXE),而 **USART_SR_TXE 是本桩的宏** —— 把它 + * 定义成函数调用,于是每次读该位都进 con_uart_read_txe():在这里计数、判跑飞、按 + * 脚本决定就绪与否。SR 成员恒为全 1,就绪完全由钩子返回值决定(返回掩码=就绪, + * 返回 0=不就绪,(全1 & 0)==0 让生产代码继续等)。TC 同理。 + * b) DWT 同理:#define DWT (con_dwt_touch(), &g_con_dwt) —— 每次 DWT-> 访问先走一遍 + * touch()(CYCCNT += step、计数判跑飞、按需让 CYCCNTENA 位落不住),再把地址给出去。 + * 真实硬件 CYCCNT 每周期 +1,这里「每次访问 +step」是同一个模型、只是步长交给测试。 + * 于是 CON_TX_TIMEOUT_CYC = SystemCoreClock/10 这类编译期表达式**不必改生产代码** + * 就能在 1 次自旋内成立(step 取 = 上界值即可)。 + * c) DR 的写不做钩子(写就是写进真成员)。「哪个字节真正上线了」由 a) 的就绪事件推定: + * 生产代码每写一个 DR 之前必然刚读到一次就绪的 TXE(两个发送循环都是这个结构), + * 所以「上次就绪之后写进 DR 的那个字节」= 此刻离开发送寄存器上线的字节 —— + * 与真实「TXE 空 -> 装载 -> 移位」时序一致,不是桩自己发明的规则。 + * 由此还能照出超时路径的真实后果:TXE 卡死时最后那个字节**没有**被写进 DR, + * 也就不会出现在上线日志里(半个块丢失、丢在哪个字节可查)。 + * + * 创建日期:2026-10-24 + */ +#ifndef STM32F4_FAKE_H +#define STM32F4_FAKE_H + +#include +#include +#include +#include + +/* ===== 与真实头同值的位 / 枚举(来源见文件头) ===== */ +#define USART_SR_TXE_Msk (0x0080U) /* USART1->SR bit7 发送寄存器空 */ +#define USART_SR_TC_Msk (0x0040U) /* USART1->SR bit6 发送完成 */ +#define DWT_CTRL_CYCCNTENA_Msk (0x00000001U) +#define CoreDebug_DEMCR_TRCENA_Msk (0x01000000U) +#define USART1_IRQn 7 + +#define HAL_UART_STATE_RESET 0x00U +#define HAL_UART_STATE_READY 0x20U +#define HAL_UART_ERROR_NONE 0x00U + +typedef enum { + HAL_OK = 0x00U, + HAL_ERROR = 0x01U, + HAL_BUSY = 0x02U, + HAL_TIMEOUT = 0x03U +} HAL_StatusTypeDef; + +/* 主机没有真周期钟。CON_TX_TIMEOUT_CYC = SystemCoreClock/10 与 + * CON_APPEND_WAIT_CYC = SystemCoreClock/5 都是编译期表达式,取值原样保留 + * 168 MHz(与 MCU 端 SystemCoreClock 一致),时间快慢由 con_dwt_step 决定。 */ +extern uint32_t SystemCoreClock; + +/* ------------------------------------------------------------------ */ +/* ① 假 USART1 */ +/* ------------------------------------------------------------------ */ +typedef struct { + volatile uint32_t SR; + volatile uint32_t DR; +} USART_TypeDef; + +typedef struct { + USART_TypeDef *Instance; + volatile uint32_t gState; + volatile uint32_t RxState; + volatile uint32_t ErrorCode; +} UART_HandleTypeDef; + +extern USART_TypeDef g_con_uart; /* 寄存器实体(单 TU,非 static) */ + +#define CON_UART_TX_MAX 20000U /* 上线字节日志容量 */ +#define CON_UART_BLOCK_MAX 64U /* 块长记录容量 */ + +extern uint32_t con_uart_tx_n; /* 已上线字节数 */ +extern uint8_t con_uart_tx[CON_UART_TX_MAX]; +extern uint32_t con_uart_block_n; /* 已完成的块数 */ +extern uint32_t con_uart_block_len[CON_UART_BLOCK_MAX]; +extern uint32_t con_uart_cur_len; /* 当前块已上线字节数 */ +extern uint32_t con_uart_txe_reads; +extern uint32_t con_uart_tc_reads; +extern int con_uart_txe_stuck; /* 1 = TXE 永不就绪(僵尸通道) */ +extern int con_uart_txe_after; /* 前 N 次 TXE 读**不**就绪,之后就绪 */ +extern int con_uart_txe_ready_left; /* >=0 = 只准就绪 N 次,之后卡死 */ +extern int con_uart_tc_after; /* 前 N 次 TC 读不就绪,之后就绪 */ +extern int con_uart_tc_stuck; /* 1 = TC 永不就绪 */ +extern int con_uart_pending; /* 1 = 有个字节已进 DR 待收 */ + +void con_uart_reset(void); + +static void con_uart_tx_put(uint8_t b) { + if (con_uart_tx_n + 1U >= CON_UART_TX_MAX) { + printf("[FATAL] fake USART: tx log overflow (>=%u)\n", CON_UART_TX_MAX); + exit(2); + } + con_uart_tx[con_uart_tx_n++] = b; + /* 每次都在尾部补一个 NUL:让 strstr 一类字符串函数不可能读过缓冲末尾 */ + con_uart_tx[con_uart_tx_n < CON_UART_TX_MAX ? con_uart_tx_n : (CON_UART_TX_MAX - 1U)] = 0U; + con_uart_cur_len++; +} + +/* 收走「上次就绪之后写进 DR 的字节」:它此刻才真正上线 */ +static void con_uart_take_byte(void) { + if (con_uart_pending == 0) { + return; + } + con_uart_pending = 0; + con_uart_tx_put((uint8_t)(g_con_uart.DR & 0xFFU)); +} + +static uint32_t con_uart_read_txe(void) { + con_uart_take_byte(); + con_uart_txe_reads++; + if (con_uart_txe_reads > 200000U) { + printf("[FATAL] fake USART: TXE read %lu times (runaway)\n", + (unsigned long)con_uart_txe_reads); + exit(2); + } + if ((con_uart_txe_stuck != 0) || (con_uart_txe_after > 0)) { + if (con_uart_txe_after > 0) { + con_uart_txe_after--; + } + return 0U; + } + if (con_uart_txe_ready_left >= 0) { + if (con_uart_txe_ready_left == 0) { + return 0U; /* 配额用尽 -> 从这一字节起通道卡死 */ + } + con_uart_txe_ready_left--; + } + con_uart_pending = 1; + return USART_SR_TXE_Msk; +} + +static uint32_t con_uart_read_tc(void) { + con_uart_take_byte(); + con_uart_tc_reads++; + if (con_uart_tc_reads > 200000U) { + printf("[FATAL] fake USART: TC read %lu times (runaway)\n", + (unsigned long)con_uart_tc_reads); + exit(2); + } + if (con_uart_tc_stuck != 0) { + return 0U; + } + if (con_uart_tc_after > 0) { /* TC 抖动:等几轮才置位 */ + con_uart_tc_after--; + return 0U; + } + if (con_uart_block_n >= CON_UART_BLOCK_MAX) { + printf("[FATAL] fake USART: block log overflow (>=%u)\n", CON_UART_BLOCK_MAX); + exit(2); + } + con_uart_block_len[con_uart_block_n++] = con_uart_cur_len; + con_uart_cur_len = 0U; + return USART_SR_TC_Msk; +} + +/* ★手法 a):判据里的掩码宏定义成函数调用 -> 每次读该位都进钩子 */ +#define USART_SR_TXE (con_uart_read_txe()) +#define USART_SR_TC (con_uart_read_tc()) + +/* ------------------------------------------------------------------ */ +/* ② 假 DWT / CoreDebug */ +/* ------------------------------------------------------------------ */ +typedef struct { + volatile uint32_t CTRL; + volatile uint32_t CYCCNT; +} DWT_Type; + +typedef struct { + volatile uint32_t DEMCR; +} CoreDebug_Type; + +extern DWT_Type g_con_dwt; +extern CoreDebug_Type g_con_core_debug; +extern uint32_t con_dwt_step; /* 每次 DWT 访问推进的周期数 */ +extern int con_dwt_ctrl_sticky; /* 0 = CYCCNTENA 写不进去 */ +extern uint32_t con_dwt_reads; + +void con_dwt_reset(void); + +/* ★手法 b):每次访问先「花时间」,再决定是否让使能位落得住 */ +static void con_dwt_touch(void) { + con_dwt_reads++; + if (con_dwt_reads > 200000U) { + printf("[FATAL] fake DWT: accessed %lu times (runaway)\n", + (unsigned long)con_dwt_reads); + exit(2); + } + g_con_dwt.CYCCNT += con_dwt_step; + if (con_dwt_ctrl_sticky == 0) { + g_con_dwt.CTRL &= ~DWT_CTRL_CYCCNTENA_Msk; + } +} + +#define DWT (con_dwt_touch(), &g_con_dwt) +#define CoreDebug (&g_con_core_debug) + +/* ------------------------------------------------------------------ */ +/* ③ HAL 时基(真实原型 uint32_t HAL_GetTick(void),stm32f4xx_hal.c) */ +/* ------------------------------------------------------------------ */ +extern uint32_t con_tick_now; +extern uint32_t con_tick_step; /* 每次读之后自动推进的 ms(0 = 不动) */ +extern uint32_t con_tick_reads; +uint32_t HAL_GetTick(void); + +/* ------------------------------------------------------------------ */ +/* ④ 内核入口 */ +/* ------------------------------------------------------------------ */ +typedef void *TaskHandle_t; +typedef uint32_t TickType_t; +typedef long BaseType_t; +typedef unsigned long UBaseType_t; + +extern TaskHandle_t con_task_self; /* xTaskGetCurrentTaskHandle 的返回值 */ +TaskHandle_t xTaskGetCurrentTaskHandle(void); + +extern uint32_t con_delay_calls; /* vTaskDelay 次数 = 真的等过 */ +extern void (*con_delay_hook)(void); /* 每次 delay 跑一遍(可模拟别的任务排空) */ +void vTaskDelay(TickType_t ticks); + +extern uint32_t con_crit_enter; +extern uint32_t con_crit_exit; +void con_crit_push(void); +void con_crit_pop(void); +#define taskENTER_CRITICAL() con_crit_push() +#define taskEXIT_CRITICAL() con_crit_pop() + +/* 真实 __get_IPSR 是 core_cmFunc.h 的静态内联;这里做成宏读一个可控量 */ +extern uint32_t con_ipsr; /* 0 = 线程模式;!=0 = 中断/异常上下文 */ +#define __get_IPSR() (con_ipsr) + +/* 事件组:原型与真实 cmsis_os2.h 逐字一致 */ +typedef void *osEventFlagsId_t; +typedef uint32_t osStatus_t; +#define osFlagsWaitAny 0x00000000u +#define osFlagsNoClear 0x00000001u + +extern uint32_t con_evt_sets; +extern uint32_t con_evt_flags; +osStatus_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags); + +/* NVIC / RCC:真实版是寄存器写。自愈路径的判据 = 重初始化做了几回、次序对不对 */ +extern uint32_t con_nvic_clears; +extern uint32_t con_rcc_disables; +extern uint32_t con_rcc_enables; +#define __NVIC_ClearPendingIRQ(irqn) do { (void)(irqn); con_nvic_clears++; } while (0) +#define __HAL_RCC_USART1_CLK_DISABLE() do { con_rcc_disables++; } while (0) +#define __HAL_RCC_USART1_CLK_ENABLE() do { con_rcc_enables++; } while (0) + +#endif /* STM32F4_FAKE_H */ diff --git a/test/host/console_log/stub/task.h b/test/host/console_log/stub/task.h new file mode 100644 index 00000000..eb9c4a18 --- /dev/null +++ b/test/host/console_log/stub/task.h @@ -0,0 +1,16 @@ +/* + * 模块名称:主机侧单测桩 - task.h(console_log 模块专用) + * 模块功能:本模块用到的两个内核入口,原型与真实 task.h 一致: + * TaskHandle_t xTaskGetCurrentTaskHandle(void); + * void vTaskDelay(const TickType_t xTicksToDelay); + * vTaskDelay 在桩里**不睡**,只计数 + 跑回调(测试用回调模拟"别的任务 + * 把环形缓冲排空"),并带跑飞保险:超过 5000 次直接 exit(2)。 + * ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef TASK_H +#define TASK_H + +#include "FreeRTOS.h" + +#endif /* TASK_H */ diff --git a/test/host/console_log/stub/usart.h b/test/host/console_log/stub/usart.h new file mode 100644 index 00000000..bc22ea7c --- /dev/null +++ b/test/host/console_log/stub/usart.h @@ -0,0 +1,20 @@ +/* + * 模块名称:主机侧单测桩 - usart.h(console_log 模块专用) + * 模块功能:与真实 Inc/usart.h 对本模块可见的部分逐字一致:extern UART_HandleTypeDef + * huart1; + void MX_USART1_UART_Init(void);(真实那份还声明 huart2~5 与 + * MX_USART2~5_UART_Init,本模块只碰 USART1,搬多出来的声明没有意义)。 + * ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建。 + * 真实 Inc/usart.h 的 include 链(main.h -> stm32f4xx_hal.h -> stm32f4xx.h -> + * core_cm4.h)在宿主上编不动,寄存器面由 stm32f4_fake.h 提供(理由见该文件头)。 + * 创建日期:2026-10-24 + */ +#ifndef USART_H +#define USART_H + +#include +#include "stm32f4_fake.h" + +extern UART_HandleTypeDef huart1; +void MX_USART1_UART_Init(void); + +#endif /* USART_H */ diff --git a/test/host/net_socket/net_socket_test.c b/test/host/net_socket/net_socket_test.c new file mode 100644 index 00000000..79e5bf4a --- /dev/null +++ b/test/host/net_socket/net_socket_test.c @@ -0,0 +1,657 @@ +/* + * 模块名称:NET 套接字核心(net_socket)主机侧单元测试 + * 模块功能:在 PC 上离线验证 Drivers/BSP/NET/net_socket.c 的**跨上下文决策面**。 + * 被测对象:真实源文件 #include 原样纳入(同 TU 内可直接驱动它的 static)。 + * + * 分片(同一 TU 内按片组织;一个目录=一个模块名,所以只有一个 runner): +* 分片 1 纯函数与 fd 表:inet_addr/inet_ntoa(E13 段序定案)、hton 与 ntoh 四件套、 + * alloc_socket/net_get_sock/net_sock_raw 的边界、net_socket() 的"新 fd 不得继承"、 + * net_bind 的动态端口与回绕、getsockname/getpeername、setblocking/set_event_cb/ + * sock_state、net_poll 未初始化短路。 + * 分片 2 每任务错误码槽(E-03)+ 队列句柄绑定(adopt 幂等):内核未跑→[0] 槽退化、 + * 不同任务不同槽位互不覆写、槽位用尽回退 [0]、临界区成对且不嵌套泄漏。 + * 分片 3 跨上下文消息面:八个 msg.type 全分派 + 结果经**任务通知值**回传 + + * "登记挂起即不发即时回执"(do_notify=0)+ 失败时把 netTask 自己槽位的精确 + * 错误码搭通知值带回来 + 调用方在自己的槽位里恢复 errno。 +* 分片 4 状态机与闸门:监听门控 net_listener_armed 三支放行/其余挡下、net_can_read、 + * net_poll 的"全局位命中才读本 Socket SINT、永不读全局"(B3/F-NET-7)、 + * 事件计数、每轮全量刷新驱动缓存 + sock_reset 步进(I1/E-02)。 +* 分片 5 读写错误分支:send 分块上限/短写/挂起登记/末块不登记(NET_PENDING 拒收边界)、 + * recv 快路径与 EOF vs WOULDBLOCK、connect 命令被拒的回滚、accept 的 + * closed-with-readable 排空接受、close 的先结算再关芯片。 + * + * ⚠ 断言只钉**真实契约**(AGENTS 模块约定 + docs/网络/网络自愈架构设计.md §4.10 + + * 源码注释里的定案编号),不钉假件规则:凡"因为假件这么做所以断言这么做"的一律不写。 + * + * 入口:int main(void),返回 0=PASS / 1=FAIL。跑法见同目录 run_unit_test.py。 + * + * 创建日期:2026-10-24 + */ + +#include +#include + +#include "net_socket.h" +#include "net_types.h" +#include "net_pending.h" +#include "net_listener.h" +#include "net_events.h" +#include "ch395f.h" + +/* dbg_log 桩的计数定义(桩头只 extern) */ +int g_ns_info_count; +int g_ns_error_count; +int g_ns_debug_count; + +/* —— 假件实现与生产源进同一个 TU(本仓主机单测既有做法)—— */ +#include "ch395f_fake.c" +#include "ns_rtos.c" +#include "ns_peers.c" + +/* —— 被测生产源 —— */ +#include "net_socket.c" + +/* ===================== 断言框架 ===================== */ + +static int s_total; +static int s_passed; +static int s_failed; + +#define CHECK(cond, name) do { s_total++; if (cond) { s_passed++; printf(" PASS %s\n", name); } else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } } while (0) + +/* ===================== 槽位表只读小工具(断言用,不参与被测逻辑) ===================== */ + +static int ns_slot_owner_count(void) +{ + int i; + int n = 0; + + for (i = 0; i < (int)NET_ERRNO_TASK_SLOTS; i++) { + if (s_net_errno_slots[i].owner != NULL) { + n++; + } + } + return n; +} + +static int ns_slot_index_of(TaskHandle_t h) +{ + int i; + + for (i = 0; i < (int)NET_ERRNO_TASK_SLOTS; i++) { + if (s_net_errno_slots[i].owner == (void *)h) { + return i; + } + } + return -1; +} + +/* ===================== 场景复位 ===================== */ +/* 生产模块的 static 在场景之间会残留,每个场景开头必须显式清干净(漏一次红一片) */ +static void reset_all(void) +{ + (void)memset(s_net_socks, 0, sizeof(s_net_socks)); + (void)memset(s_net_errno_slots, 0, sizeof(s_net_errno_slots)); + (void)memset(s_local_ip, 0, sizeof(s_local_ip)); + s_dynamic_port = 49152; + s_net_generation = 0U; + s_net_initialized = 0U; + s_net_msg_queue = NULL; + g_ns_info_count = 0; + g_ns_error_count = 0; + g_ns_debug_count = 0; + ch395f_fake_reset(); + ns_rtos_reset(); + ns_peers_reset(); +} + +/* 把网络层拉到"已初始化 + 队列已绑定"的常态(绝大多数场景的前置) */ +static void bring_up(void) +{ + static int s_fake_queue; /* 句柄只是不透明标识,假内核不解引用 */ + + reset_all(); + (void)net_msg_queue_adopt((osMessageQueueId_t)&s_fake_queue); + /* 装泵:调用方阻塞等待时以 netTask 身份跑一轮消息分派(= 跨上下文那条链) */ + g_ns_pump = net_process_messages; + (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL); + g_peers.pending_capture_calls = 0; /* bring_up 内部的登记动作不该算进场景 */ + g_ns_error_count = 0; + g_ns_info_count = 0; +} + +/* 直接造一个在用的控制块(多数场景不需要走完整的 socket()/bind()) */ +static int mk_sock(int type, net_sock_state_t state) +{ + int fd = net_socket(NET_AF_INET, type); + + if (fd < 0) { + return fd; + } + s_net_socks[fd].state = state; + return fd; +} +/* ===================== 分片:纯函数 / 错误码槽 / 消息面 / 状态机 / 读写分支 ===================== */ +/* 一个目录 = 一个模块名(check_test_coverage 的发现规则),所以只有一个 runner 与一个 + * TU;分片以文件维度拆开,是为了让 1251 行口径的最大块能逐片推进、逐片可查。 */ +#include "ns_s3_msg.c" +#include "ns_s4_state.c" +#include "ns_s5_io.c" +#include "ns_s6_edges.c" + +/* = 分片 1 纯函数面与 fd 表 ===================== */ + +/* TC-NETSOCK-100 点分十进制 ↔ 32 位:钉**当前行为**并显式标注疑点(见报告) + * 现状:net_inet_addr 把首段放在**最高字节**(0xC0A80164),而 net_getsockname 组装 + * s_addr 时把首段放在**最低字节**,net_inet_ntoa 只与后者互为逆——同一文件里 + * 存在两套字节序约定。这里逐条钉住,不替生产"圆场"。 */ +static void tc_100_inet_helpers(void) +{ + char buf[16]; + + reset_all(); + + CHECK(net_inet_addr(NULL) == 0U, "TC-NETSOCK-100a inet_addr(NULL) 返 0(无效)"); + CHECK(net_inet_addr("192.168.1.100") == 0xC0A80164U, + "TC-NETSOCK-100b inet_addr 首段落在最高字节(当前实现)"); + CHECK(net_inet_ntoa(0xC0A80164U, buf) == buf && strcmp(buf, "100.1.168.192") == 0, + "TC-NETSOCK-100c ntoa(同一 32 位值) 与 inet_addr 不互逆(疑点已报,钉当前行为)"); + CHECK(net_inet_ntoa(0x6401A8C0U, buf) == buf && strcmp(buf, "192.168.1.100") == 0, + "TC-NETSOCK-100d ntoa 与 getsockname 的 s_addr 约定互逆(E13 定案的段序:高段在前)"); + CHECK(net_inet_ntoa(0U, NULL) == NULL, "TC-NETSOCK-100e ntoa(NULL 缓冲) 返 NULL 不崩"); + /* 单段/缺分隔的容错:逐段解析,非数字段当 0(钉实现,不钉"应该是多少") */ + CHECK(net_inet_addr("10.0.0.1") == 0x0A000001U, "TC-NETSOCK-100f inet_addr 10.0.0.1"); +} + +static void tc_101_byteorder(void) +{ + reset_all(); + + CHECK(net_htons(0x1234U) == 0x3412U, "TC-NETSOCK-101a htons 换端"); + CHECK(net_ntohs(net_htons(80U)) == 80U, "TC-NETSOCK-101b htons/ntohs 互逆"); + CHECK(net_htonl(0x01020304U) == 0x04030201U, "TC-NETSOCK-101c htonl 换端"); + CHECK(net_ntohl(net_htonl(0xDEADBEEFU)) == 0xDEADBEEFU, "TC-NETSOCK-101d htonl/ntohl 互逆"); +} + +static void tc_102_parse_ip(void) +{ + uint8_t a[4]; + + reset_all(); + + (void)memset(a, 0xEE, sizeof(a)); + parse_ip_string(NULL, a); + CHECK(a[0] == 0xEEU, "TC-NETSOCK-102a parse_ip_string(NULL) 直接返回,不写目标缓冲"); + parse_ip_string("192.168.1.100", a); + CHECK(a[0] == 192U && a[1] == 168U && a[2] == 1U && a[3] == 100U, + "TC-NETSOCK-102b parse_ip_string 段 0 落在 [0](芯片 set_ip_addr 要的顺位)"); + parse_ip_string("10.20.30.40", NULL); /* 只验不崩 */ + CHECK(1, "TC-NETSOCK-102c parse_ip_string 目标为 NULL 不崩"); +} + +static void tc_103_alloc_and_getsock(void) +{ + int i; + int fd; + net_sock_t *p; + + reset_all(); + + CHECK(net_get_sock(-1) == NULL && net_get_sock(NET_MAX_SOCKETS) == NULL, + "TC-NETSOCK-103a net_get_sock 越界返 NULL(含负数)"); + CHECK(net_get_sock(0) == NULL, "TC-NETSOCK-103b 未分配的槽位返 NULL"); + CHECK(net_sock_raw(3) == &s_net_socks[3], + "TC-NETSOCK-103c net_sock_raw 不判 in_use(对账必须看到 CLOSED 槽位)"); + CHECK(net_sock_state(0) == -1, "TC-NETSOCK-103d 未占用 sock_state 返 -1 且不设 errno"); + + /* 逐个占满:alloc_socket 线性扫描首个空闲槽 */ + for (i = 0; i < NET_MAX_SOCKETS; i++) { + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + if (fd != i) { + break; + } + } + CHECK(i == NET_MAX_SOCKETS, "TC-NETSOCK-103e alloc_socket 按序分配 0..7"); + p = net_get_sock(0); + CHECK(p != NULL && p->in_use == 1U, "TC-NETSOCK-103f 分配的槽位 in_use=1"); + + reset_all(); +} + +static void tc_104_socket_no_inherit(void) +{ + int fd; + net_sock_t *p; + + reset_all(); + + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + CHECK(fd == 0, "TC-NETSOCK-104a net_socket 返回首个空闲 fd"); + p = &s_net_socks[fd]; + CHECK(p->ch395_sock == (uint8_t)fd, "TC-NETSOCK-104b 芯片 Socket 号与描述符一一对应"); + CHECK(p->state == NET_SOCK_STATE_CREATED && p->type == NET_SOCK_STREAM, + "TC-NETSOCK-104c 初态 CREATED + 类型记下"); + CHECK(p->send_ready == 1U && p->readable == 0U, + "TC-NETSOCK-104d 初态:发送缓冲视为空闲、无可读"); + /* 三条"新 fd 不得继承"的定案,逐条钉(都是真机上静默出错的形态) */ + CHECK(p->blocking == 1U, "TC-NETSOCK-104e blocking 默认必须显式置 1(BSD 语义)"); + CHECK(p->released_abnormal == 0U, "TC-NETSOCK-104f 不得继承上一条的释放原因(TRAP-NET-18)"); + CHECK(p->session_dead == 0U, "TC-NETSOCK-104g 不得继承上一条的会话结束电平(N-32 P1)"); + CHECK(g_peers.listener_clear_calls == 1 && g_peers.pending_clear_calls == 1 && + g_peers.send_clear_calls == 1, + "TC-NETSOCK-104h 新 fd 必须清监听重试 + 两张挂起表(各一次)"); + + /* 脏槽位复用:把上一条会话的残留写进去,再看新建是否清干净 */ + p->blocking = 0U; + p->released_abnormal = 1U; + p->session_dead = 1U; + p->readable = 1U; + p->send_ready = 0U; + p->event_cb = (net_event_cb_t)0x1234; + /* 这里刻意用 netTask 直调版(net_close_sock)而不是公开 net_close: + * 本场景没绑消息队列,公开入口会先撞上"队列未创建"的 NET_ERR, + * 而那属于分片 3 的断言面(TC-NETSOCK-110h)。close 的清零主路径在 net_close_locked。 */ + CHECK(net_close_sock(&s_net_socks[fd]) == 0, + "TC-NETSOCK-104i net_close_sock 成功(close 的 memset 是清零主路径)"); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + p = &s_net_socks[fd]; + CHECK(fd == 0 && p->blocking == 1U && p->released_abnormal == 0U && + p->session_dead == 0U && p->readable == 0U && p->send_ready == 1U && + p->event_cb == NULL, + "TC-NETSOCK-104j 复用同一槽位:残留字段全部不得带走"); + /* 地址族不支持 → INVAL(在 alloc 之前,不占槽) */ + CHECK(net_socket(NET_AF_INET6, NET_SOCK_STREAM) == -1 && + net_get_errno() == NET_ERR_INVAL && s_net_socks[1].in_use == 0U, + "TC-NETSOCK-104k 非 INET 地址族 → INVAL 且不占槽位"); + + reset_all(); +} + +static void tc_105_bind(void) +{ + struct net_sockaddr_in in; + int fd; + int rc; + + reset_all(); + + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(2404); + in.sin_addr.s_addr = net_inet_addr("192.168.1.100"); + + CHECK(net_bind(-1, (struct net_sockaddr *)&in, sizeof(in)) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-105a 越界 fd → BADF"); + CHECK(net_bind(0, (struct net_sockaddr *)&in, sizeof(in)) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-105b 未创建就 bind → BADF"); + + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + CHECK(net_bind(fd, NULL, sizeof(in)) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-105c addr 为 NULL → INVAL"); + CHECK(net_bind(fd, (struct net_sockaddr *)&in, 1) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-105d addrlen 不足 → INVAL"); + CHECK(net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)) == 0, + "TC-NETSOCK-105e TCP bind 成功"); + CHECK(s_net_socks[fd].local_port == 2404U && + s_net_socks[fd].state == NET_SOCK_STATE_BOUND, + "TC-NETSOCK-105f TCP 只记端口转主机序 + 进 BOUND,不碰芯片"); + CHECK(g_chip.set_port_calls == 0 && g_chip.open_calls == 0, + "TC-NETSOCK-105g TCP bind 期间不得下发源端口/开芯片 Socket(打开留给 listen)"); + + /* port=0 → 动态端口 49152 起,并推进 */ + reset_all(); + (void)memset(&in, 0, sizeof(in)); + in.sin_port = 0U; + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + rc = net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)); + CHECK(rc == 0 && s_net_socks[fd].local_port == 49152U, + "TC-NETSOCK-105h port=0 → 分配 49152"); + CHECK(s_dynamic_port == 49153U, + "TC-NETSOCK-105h2 计数器已推进到 49153(分配即推进,不留重复)"); + (void)net_socket(NET_AF_INET, NET_SOCK_STREAM); /* 只创建不 bind */ + CHECK(s_dynamic_port == 49153U, + "TC-NETSOCK-105i3 创建 socket **不**消耗动态端口(只有 bind 消耗)"); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + rc = net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)); + CHECK(rc == 0 && s_net_socks[fd].local_port == 49153U && s_dynamic_port == 49154U, + "TC-NETSOCK-105i 第二次 bind 拿下一个端口(不会把同一个端口发两次)"); + + /* 回绕:把计数器压到 65535,分配后应回 49152(不是 0——0 会让芯片按"未绑定"处理) */ + s_dynamic_port = 65535U; + reset_all(); + s_dynamic_port = 65535U; + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + rc = net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)); + CHECK(rc == 0 && s_net_socks[fd].local_port == 65535U && s_dynamic_port == 49152U, + "TC-NETSOCK-105j 动态端口越过 65535 回绕到 49152(不回 0)"); +} + +static void tc_106_bind_udp(void) +{ + struct net_sockaddr_in in; + int fd; + + reset_all(); + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(8888); + + /* UDP:bind 就直接开芯片侧(无监听态),且必须配 DesIP=255.255.255.255 收任意源 */ + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + CHECK(net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)) == 0, "TC-NETSOCK-106a UDP bind 成功"); + CHECK(s_net_socks[fd].state == NET_SOCK_STATE_UDP_OPEN, "TC-NETSOCK-106b UDP 直接进 UDP_OPEN"); + CHECK(g_chip.set_proto_calls == 1 && g_chip.last_proto == CH395F_PROTO_TYPE_UDP, + "TC-NETSOCK-106c 协议下发为 UDP(不是 TCP)"); + CHECK(g_chip.set_port_calls == 1 && g_chip.last_port == 8888U, + "TC-NETSOCK-106d 源端口随 bind 下发"); + CHECK(g_fx.des_ip[0][0] == 0xFFU && g_fx.des_ip[0][1] == 0xFFU && + g_fx.des_ip[0][2] == 0xFFU && g_fx.des_ip[0][3] == 0xFFU && + g_fx.des_port[0] == 0U, + "TC-NETSOCK-106e UDP Server 模式:DesIP=全 FF + 目的端口 0(否则只收单一对端)"); + CHECK(g_chip.open_calls == 1, "TC-NETSOCK-106f UDP bind 开一次芯片 Socket"); + + /* 槽位 >=4 必须补设收发缓冲(net_init 已配过,这里是兜底):布局 6 块/槽 */ + reset_all(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + CHECK(fd == 0, "TC-NETSOCK-106g 先拿 fd0(对照:不补缓冲)"); + CHECK(g_fxchip.set_recv_buf_calls == 0, "TC-NETSOCK-106h fd<4 的 UDP bind 不重设缓冲"); + + reset_all(); + for (fd = 0; fd < 4; fd++) { + (void)net_socket(NET_AF_INET, NET_SOCK_DGRAM); + } + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); /* fd4 */ + (void)net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)); + CHECK(g_fx.recv_blk_start[4] == (uint8_t)(4 * 6) && + g_fx.recv_blk_cnt[4] == (uint8_t)CH395F_RECV_BLOCKS && + g_fx.send_blk_start[4] == (uint8_t)(4 * 6 + (int)CH395F_RECV_BLOCKS) && + g_fx.send_blk_cnt[4] == (uint8_t)CH395F_SEND_BLOCKS, + "TC-NETSOCK-106i fd>=4 兜底重设收发缓冲,块布局与 net_init 同构(6 块/槽)"); + + /* open 失败 → BUSY 且留在原状态(不得谎报 UDP_OPEN) */ + reset_all(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + g_fake.open_rc[fd] = CH395F_ERR_USE; + CHECK(net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)) == -1 && + net_get_errno() == NET_ERR_BUSY && + s_net_socks[fd].state != NET_SOCK_STATE_UDP_OPEN && + g_ns_error_count == 1, + "TC-NETSOCK-106j UDP open 失败 → BUSY + 留痕 + 状态不前进"); +} + +static void tc_107_peer_and_localname(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + int addrlen; + int fd; + + reset_all(); + (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL); + + CHECK(net_getsockname(-1, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-107a getsockname 越界 → BADF"); + CHECK(net_getsockname(0, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-107b 未用且 CLOSED 的槽 → BADF"); + + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + (void)memset(&in, 0, sizeof(in)); + in.sin_port = net_htons(7003); + CHECK(net_bind(fd, sa, (int)sizeof(in)) == 0, "TC-NETSOCK-107c bind 前置成功"); + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + CHECK(net_getsockname(fd, sa, &addrlen) == 0, "TC-NETSOCK-107d getsockname 成功"); + CHECK(in.sin_family == NET_AF_INET && net_ntohs(in.sin_port) == 7003U && + addrlen == (int)sizeof(in), + "TC-NETSOCK-107e 回填族/端口(网络序)/长度"); + /* 钉**具体顺位**而不是二选一:getsockname 走的是「首段在最低字节」这一族 + * (与 net_inet_ntoa、net_events 的 remote_ip 同族),所以 192.168.1.100 = + * 0x6401A8C0。net_inet_addr 返回的是另一族(首段在最高字节,0xC0A80164), + * 两族不互逆——已作为疑似缺陷报出,这里刻意不写成「两个都行」。 */ + CHECK(in.sin_addr.s_addr == 0x6401A8C0U, + "TC-NETSOCK-107f 本机 IP 顺位=首段在最低字节(与 ntoa 同族;net_inet_addr 是另一族)"); + + addrlen = 1; + CHECK(net_getsockname(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-107g 长度不足 → INVAL"); + CHECK(net_getsockname(fd, sa, NULL) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-107h addrlen 为 NULL → INVAL"); + + /* getpeername:没有 CONNECT 回读过对端 → NOTCONN(不发 SPI 是它的卖点) */ + (void)memset(&in, 0, sizeof(in)); + addrlen = (int)sizeof(in); + CHECK(net_getpeername(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_NOTCONN, + "TC-NETSOCK-107i 尚无对端 → NOTCONN(remote_port==0 判据)"); + CHECK(g_chip.get_status_calls == 0 && g_fxchip.glob_calls == 0, + "TC-NETSOCK-107j getpeername 纯读缓存,不发任何芯片事务"); + + s_net_socks[fd].remote_port = 51999U; + s_net_socks[fd].remote_ip = 0x0B01A8C0U; + (void)memset(&in, 0, sizeof(in)); + addrlen = (int)sizeof(in); + CHECK(net_getpeername(fd, sa, &addrlen) == 0 && + net_ntohs(in.sin_port) == 51999U && in.sin_addr.s_addr == 0x0B01A8C0U && + in.sin_family == NET_AF_INET, + "TC-NETSOCK-107k getpeername 回填缓存的对端 IP:端口"); + CHECK(net_getpeername(-1, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-107l getpeername 越界 → BADF"); + CHECK(net_getpeername(5, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-107m getpeername 未在用 → BADF"); + addrlen = 2; + CHECK(net_getpeername(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-107n getpeername 长度不足 → INVAL"); + + reset_all(); +} + +static void tc_108_sockprops(void) +{ + int fd; + int arg = 7; + + reset_all(); + CHECK(net_setblocking(-1, 1) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-108a setblocking 越界 → BADF"); + CHECK(net_setblocking(0, 1) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-108b 未占用 → BADF"); + CHECK(net_set_event_cb(-1, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-108c set_event_cb 越界 → BADF"); + CHECK(net_set_event_cb(0, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-108d set_event_cb 未占用 → BADF"); + + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + CHECK(net_setblocking(fd, 0) == 0 && s_net_socks[fd].blocking == 0U, + "TC-NETSOCK-108e setblocking(0) 落属性(模式挂控制块,不是单次参数)"); + CHECK(net_setblocking(fd, 5) == 0 && s_net_socks[fd].blocking == 1U, + "TC-NETSOCK-108f 非 0 一律视为阻塞"); + CHECK(net_set_event_cb(fd, (net_event_cb_t)0x1234, &arg) == 0 && + s_net_socks[fd].event_cb == (net_event_cb_t)0x1234 && + s_net_socks[fd].event_arg == &arg, + "TC-NETSOCK-108g 注册回调与用户参数"); + CHECK(net_set_event_cb(fd, NULL, NULL) == 0 && s_net_socks[fd].event_cb == NULL, + "TC-NETSOCK-108h cb=NULL 取消注册"); + s_net_socks[fd].state = NET_SOCK_STATE_ESTABLISHED; + CHECK(net_sock_state(fd) == (int)NET_SOCK_STATE_ESTABLISHED, + "TC-NETSOCK-108i sock_state 返状态快照"); + CHECK(net_sock_state(NET_MAX_SOCKETS) == -1, "TC-NETSOCK-108j sock_state 越界 -1"); + s_net_socks[fd].in_use = 0U; + CHECK(net_sock_state(fd) == -1, "TC-NETSOCK-108k sock_state 未占用 -1"); + + reset_all(); +} + +static void tc_109_generation(void) +{ + reset_all(); + CHECK(net_get_generation() == 0U, "TC-NETSOCK-109a 初值 0"); + net_generation_inc(); + net_generation_inc(); + CHECK(net_get_generation() == 2U, "TC-NETSOCK-109b 每次网络重建 +1(服务据此重建监听)"); + reset_all(); +} + +static void tc_110_queue_adopt(void) +{ + static int q1; + static int q2; + + reset_all(); + CHECK(net_msg_queue_adopt(NULL) == -1, "TC-NETSOCK-110a 空句柄 → -1"); + CHECK(net_msg_queue_adopt((osMessageQueueId_t)&q1) == 0, "TC-NETSOCK-110b 首次绑定成功"); + CHECK(net_msg_queue_adopt((osMessageQueueId_t)&q2) == -1, + "TC-NETSOCK-110c 重复绑定必须被拒(不静默换句柄)"); + CHECK(s_net_msg_queue == (osMessageQueueId_t)&q1, + "TC-NETSOCK-110d 重复绑定**保留首次**句柄——换掉会让已入队请求永不处理"); + /* 未绑定时所有公开 API 一律 NET_ERR,而不是崩在空句柄上 */ + reset_all(); + CHECK(net_listen(0) == -1 && net_get_errno() == NET_ERR, + "TC-NETSOCK-110e 未绑定队列:net_listen → NET_ERR"); + CHECK(net_recv(0, NULL, 0, 0) == -1 && net_get_errno() == NET_ERR, + "TC-NETSOCK-110f 未绑定队列:net_recv → NET_ERR(越界检查在队检查之前)"); + CHECK(net_send(0, "x", 1, 0) == -1 && net_get_errno() == NET_ERR, + "TC-NETSOCK-110g 未绑定队列:net_send → NET_ERR"); + CHECK(net_close(0) == -1 && net_get_errno() == NET_ERR, + "TC-NETSOCK-110h 未绑定队列:net_close → NET_ERR"); + CHECK(net_drop_conn(0) == -1 && net_get_errno() == NET_ERR, + "TC-NETSOCK-110i 未绑定队列:net_drop_conn → NET_ERR"); + CHECK(net_recv_to(0, NULL, 0, 10U) == -1 && net_get_errno() == NET_ERR, + "TC-NETSOCK-110j 未绑定队列:net_recv_to → NET_ERR"); + CHECK(net_accept(0, NULL, NULL) == -1 && net_get_errno() == NET_ERR, + "TC-NETSOCK-110k 未绑定队列:net_accept → NET_ERR"); + CHECK(net_connect(0, NULL, 0) == -1 && net_get_errno() == NET_ERR, + "TC-NETSOCK-110l 未绑定队列:net_connect → NET_ERR"); + /* 越界优先于"未初始化":调用方拿到的码要能区分"我传错了"和"网还没起来" */ + CHECK(net_send(NET_MAX_SOCKETS, "x", 1, 0) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-110m 越界检查先于队列检查 → BADF"); + reset_all(); +} + +/* ===================== 分片 2 每任务错误码槽(E-03) ===================== */ + +static void tc_120_errno_slot_per_task(void) +{ + reset_all(); + + /* 内核没跑起来:不做槽位分配,一律退到 [0] 槽(中断/内核前上下文) */ + g_ns_kernel_state = osKernelInactive; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + (void)net_set_errno(NET_ERR_BADF); + CHECK(net_get_errno() == NET_ERR_BADF && s_net_errno_slots[0].owner == NULL, + "TC-NETSOCK-120a 内核未运行 → 用 [0] 槽且**不占槽**(不写 owner)"); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; + CHECK(net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-120b 内核未运行时所有任务共享 [0] 槽(退化语义)"); + + /* 内核在跑:每个任务自己的槽位 */ + g_ns_kernel_state = osKernelRunning; + reset_all(); + g_ns_kernel_state = osKernelRunning; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + (void)net_set_errno(NET_ERR_TIMEDOUT); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; + CHECK(net_get_errno() == NET_OK, + "TC-NETSOCK-120c 另一个任务读不到 caller 的码(E-03 的正面表述)"); + (void)net_set_errno(NET_ERR_WOULDBLOCK); + CHECK(net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-120d 本任务读自己的码"); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + CHECK(net_get_errno() == NET_ERR_TIMEDOUT, + "TC-NETSOCK-120e 互不覆写:caller 的码没被 netTask 抢走(旧实现的红线)"); + /* 临界区:两个任务各占一次槽 → 两次进入、两次退出,绝不泄漏嵌套计数 */ + CHECK(g_ns_crit_enter == 2 && g_ns_crit_exit == 2 && ns_os_crit_depth() == 0, + "TC-NETSOCK-120f 每次**首次占槽**进一次临界区且成对退出(无泄漏)"); + + /* 快路径只读不写:已有槽位的任务反复读写都不该再进临界区、也不该再占槽 */ + { + int e0 = g_ns_crit_enter; + int owners0 = ns_slot_owner_count(); + int i_caller = ns_slot_index_of((TaskHandle_t)&g_ns_task_caller); + int i_net = ns_slot_index_of((TaskHandle_t)&g_ns_task_net); + + CHECK(i_caller >= 0 && i_net >= 0 && i_caller != i_net, + "TC-NETSOCK-120g 两个任务落在**不同**槽位(槽位表可查,不是靠约定)"); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + (void)net_set_errno(NET_ERR_BUSY); + (void)net_get_errno(); + (void)net_set_errno(NET_ERR_CONNRESET); + CHECK(g_ns_crit_enter == e0, "TC-NETSOCK-120h 命中已有槽位:全程零临界区(快路径只读)"); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; + CHECK(net_get_errno() == NET_ERR_WOULDBLOCK && + ns_slot_owner_count() == owners0, + "TC-NETSOCK-120i 别的任务反复读也不新增槽位、也读不到 caller 的码"); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + CHECK(net_get_errno() == NET_ERR_CONNRESET, "TC-NETSOCK-120j caller 读到自己最新的码"); + } + + /* 槽位用尽 → 回退 [0](刻意不重试,避免把竞态带回来) */ + reset_all(); + g_ns_kernel_state = osKernelRunning; + { + int i; + static int filler[10]; + for (i = 0; i < (int)NET_ERRNO_TASK_SLOTS; i++) { + g_ns_cur_task = (TaskHandle_t)&filler[i]; + (void)net_set_errno(NET_ERR_ISCONN); + } + g_ns_cur_task = (TaskHandle_t)&filler[0]; + CHECK(net_get_errno() == NET_ERR_ISCONN, "TC-NETSOCK-120k 8 个槽各归各主"); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; /* 第 9 个任务 */ + (void)net_set_errno(NET_ERR_NOMEM); + CHECK(net_get_errno() == NET_ERR_NOMEM, + "TC-NETSOCK-120l 槽位用尽:新任务落 [0] 槽仍读得到自己的码"); + g_ns_cur_task = (TaskHandle_t)&filler[0]; + CHECK(net_get_errno() == NET_ERR_NOMEM, + "TC-NETSOCK-120m 用尽后退化为共享 [0] 槽(只影响排障可读性,不新增共享槽位)"); + } + reset_all(); +} + +static void tc_121_errno_slot_isr(void) +{ + reset_all(); + g_ns_kernel_state = osKernelRunning; + g_ns_cur_task = NULL; /* 中断上下文/无句柄 */ + (void)net_set_errno(NET_ERR_CONNRESET); + CHECK(net_get_errno() == NET_ERR_CONNRESET && s_net_errno_slots[0].owner == NULL, + "TC-NETSOCK-121 句柄为 NULL(中断上下文)→ 退 [0] 槽且不改槽位表"); + reset_all(); +} + +/* ===================== 主入口 ===================== */ + +int main(void) +{ + printf("=== NET Socket Tests (net_socket.c 主机单测) ===\n"); + + printf("[shard1] 纯函数面与 fd 表\n"); + tc_100_inet_helpers(); + tc_101_byteorder(); + tc_102_parse_ip(); + tc_103_alloc_and_getsock(); + tc_104_socket_no_inherit(); + tc_105_bind(); + tc_106_bind_udp(); + tc_107_peer_and_localname(); + tc_108_sockprops(); + tc_109_generation(); + tc_110_queue_adopt(); + + printf("[shard2] 每任务错误码槽 + 队列绑定\n"); + tc_120_errno_slot_per_task(); + tc_121_errno_slot_isr(); + + printf("[shard3] 跨上下文消息与通知值回传\n"); + ns_run_s3(); + + printf("[shard4] 状态机与闸门\n"); + ns_run_s4(); + + printf("[shard5] 建流与读写分支\n"); + ns_run_s5(); + + printf("[shard6] 边界残支与其余出口\n"); + ns_run_s6(); + + printf("=== NET Socket Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/net_socket/ns_peers.c b/test/host/net_socket/ns_peers.c new file mode 100644 index 00000000..fee85cb9 --- /dev/null +++ b/test/host/net_socket/ns_peers.c @@ -0,0 +1,379 @@ +/* + * 模块名称:NET 层兄弟模块桩(net_socket 主机侧单测专用) + * 模块功能:net_socket.c 依赖的三个兄弟模块(net_listener / net_pending / net_events) + * 在这里**按边界契约打桩**,让本套单测只测 net_socket.c 自己那部分决策。 + * + * ⚠ 仅供 test/host/net_socket/ 使用,不参与 MCU 构建。 + * + * 为什么打桩而不是 #include 真实源(而不是"越真越好"): + * 1. 那三个模块**已各有自己的主机单测**(net_listener 34 条、net_events 34 条、 + * net_pending 43 条),本套再跑一遍不增加任何对 net_socket.c 的判别力, + * 只会把"谁的红"变成糊账; + * 2. 本套真正要钉的是**边界契约**——net_socket.c 在什么条件下把什么 reason 登记给 + * 监听层、什么条件下把挂起请求登记给 pending 层、以及"登记了就不能再自己发回执"。 + * 桩把这些变成可断言的记录,比让真实 tick 在背后默默跑更有判别力; + * 3. 原型一律取**真实头**(net_listener.h / net_pending.h / net_events.h), + * 兄弟模块改签名 → 本套编译期就红。 + * + * 桩的取值纪律(不自己发明语义,全部按真实头的函数说明): + * - net_pending_register / net_send_pending_register 返回 NET_PENDING_REGISTERED, + * 与真实实现一致(登记成功=哨兵,供分派跳过即时通知);可注入 -1 测登记被拒; + * - net_pending_tick 支持"下一轮把脚本里的结果当作数据到达,通知登记时的 caller", + * 复刻真实 tick 的"数据到达即通知"契约; + * - 其余全为纯记录(次数 + 最近一次入参),供"不该登记/必须登记"的断言。 + * + * 创建日期:2026-10-24 + */ + +#include +#include +#include + +#include "net_listener.h" +#include "net_pending.h" +#include "net_events.h" +#include "net_socket.h" +#include "ns_peers.h" +#include "ns_rtos.h" + +ns_peers_obs_t g_peers; + +/* 桩内部"完成/取消"要发通知:转给假内核,这样测试能断言通知次数与通知值 */ +void ns_peers_note_notify(TaskHandle_t to, uint32_t v) +{ + ns_rtos_post_notify(to, v); +} + +/* 挂起登记簿(只记一条,够本套断言用) */ +static int s_pend_active[NET_MAX_SOCKETS]; +static void *s_pend_buf[NET_MAX_SOCKETS]; +static int s_pend_len[NET_MAX_SOCKETS]; +static TaskHandle_t s_pend_caller[NET_MAX_SOCKETS]; +static int s_snd_active[NET_MAX_SOCKETS]; +static TaskHandle_t s_snd_caller[NET_MAX_SOCKETS]; + +/* tick 完成脚本:fd -> 结果值(一次生效) */ +static int s_pend_done_armed[NET_MAX_SOCKETS]; +static int s_pend_done_val[NET_MAX_SOCKETS]; +static int s_snd_done_armed[NET_MAX_SOCKETS]; +static int s_snd_done_val[NET_MAX_SOCKETS]; + +void ns_peers_reset(void) +{ + (void)memset(&g_peers, 0, sizeof(g_peers)); + (void)memset(s_pend_active, 0, sizeof(s_pend_active)); + (void)memset(s_pend_buf, 0, sizeof(s_pend_buf)); + (void)memset(s_pend_len, 0, sizeof(s_pend_len)); + (void)memset(s_pend_caller, 0, sizeof(s_pend_caller)); + (void)memset(s_snd_active, 0, sizeof(s_snd_active)); + (void)memset(s_snd_caller, 0, sizeof(s_snd_caller)); + (void)memset(s_pend_done_armed, 0, sizeof(s_pend_done_armed)); + (void)memset(s_pend_done_val, 0, sizeof(s_pend_done_val)); + (void)memset(s_snd_done_armed, 0, sizeof(s_snd_done_armed)); + (void)memset(s_snd_done_val, 0, sizeof(s_snd_done_val)); + g_peers.pend_register_rc = NET_PENDING_REGISTERED; + g_peers.send_register_rc = NET_PENDING_REGISTERED; +} + +void ns_peers_arm_pending_done(int sockfd, int result) +{ + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) { + s_pend_done_armed[sockfd] = 1; + s_pend_done_val[sockfd] = result; + } +} + +void ns_peers_arm_send_pending_done(int sockfd, int result) +{ + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) { + s_snd_done_armed[sockfd] = 1; + s_snd_done_val[sockfd] = result; + } +} + +/* ================= net_listener.h ================= */ + +void net_listener_request(int sockfd, net_listener_req_t reason) +{ + g_peers.listener_req_calls++; + g_peers.last_req_fd = sockfd; + g_peers.last_req_reason = (int)reason; + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) { + g_peers.req_armed[sockfd] = 1; + g_peers.req_reason[sockfd] = (int)reason; + } +} + +void net_listener_clear(int sockfd) +{ + g_peers.listener_clear_calls++; + g_peers.last_clear_fd = sockfd; + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) { + g_peers.req_armed[sockfd] = 0; + } +} + +void net_listener_tick(void) +{ + g_peers.listener_tick_calls++; +} + +void net_listener_settle(int sockfd, net_listener_settle_t level) +{ + g_peers.listener_settle_calls++; + g_peers.last_settle_fd = sockfd; + g_peers.last_settle_level = (int)level; +} + +/* ================= net_pending.h(recv 侧) ================= */ + +void net_pending_capture_owner(void) +{ + g_peers.pending_capture_calls++; +} + +int net_pending_register(int sockfd, void *buf, int len, uint32_t to_ms, + TaskHandle_t caller) +{ + g_peers.pending_reg_calls++; + g_peers.last_pend_fd = sockfd; + g_peers.last_pend_to = to_ms; + g_peers.last_pend_caller = caller; + if (g_peers.pend_register_rc != NET_PENDING_REGISTERED) { + return g_peers.pend_register_rc; /* 测"登记被拒"分支 */ + } + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) { + s_pend_active[sockfd] = 1; + s_pend_buf[sockfd] = buf; + s_pend_len[sockfd] = len; + s_pend_caller[sockfd] = caller; + g_peers.pend_active_now[sockfd] = 1; + } + return NET_PENDING_REGISTERED; +} + +void net_pending_tick(net_sock_t *p_socks) +{ + int i; + + g_peers.pending_tick_calls++; + (void)p_socks; + /* 复刻真实 tick 的主用例:数据到达 → 按登记时的 caller 通知 → 清槽 */ + for (i = 0; i < NET_MAX_SOCKETS; i++) { + if ((s_pend_active[i] != 0) && (s_pend_done_armed[i] != 0)) { + s_pend_done_armed[i] = 0; + s_pend_active[i] = 0; + g_peers.pend_active_now[i] = 0; + g_peers.pending_notified++; + ns_peers_note_notify(s_pend_caller[i], (uint32_t)s_pend_done_val[i]); + } + } +} + +void net_pending_clear(int sockfd) +{ + g_peers.pending_clear_calls++; + g_peers.last_pend_clear_fd = sockfd; + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) { + s_pend_active[sockfd] = 0; + g_peers.pend_active_now[sockfd] = 0; + } +} + +void net_pending_cancel(int sockfd, int err) +{ + g_peers.pending_cancel_calls++; + g_peers.last_pend_cancel_err = err; + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) { + s_pend_active[sockfd] = 0; + g_peers.pend_active_now[sockfd] = 0; + } +} + +void net_pending_cancel_all(int err) +{ + int i; + + g_peers.pending_cancel_all_calls++; + g_peers.last_pend_cancel_err = err; + for (i = 0; i < NET_MAX_SOCKETS; i++) { + if (s_pend_active[i] != 0) { + g_peers.pending_notified++; + ns_peers_note_notify(s_pend_caller[i], (uint32_t)err); + } + s_pend_active[i] = 0; + g_peers.pend_active_now[i] = 0; + } +} + +void net_pending_settle_close(int sockfd, net_sock_t *p_sock) +{ + g_peers.pending_settle_calls++; + g_peers.last_pend_fd = sockfd; + (void)p_sock; + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS) && (s_pend_active[sockfd] != 0)) { + /* 真实实现:有数据先送达,无数据按 CONNRESET 取消——两者都**必通知** */ + s_pend_active[sockfd] = 0; + g_peers.pend_active_now[sockfd] = 0; + g_peers.pending_notified++; + ns_peers_note_notify(s_pend_caller[sockfd], (uint32_t)NET_ERR_CONNRESET); + } +} + +int net_pending_active(int sockfd) +{ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return 0; + } + return s_pend_active[sockfd]; +} + +/* ================= net_pending.h(send 侧) ================= */ + +int net_send_pending_register(int sockfd, const void *buf, int len, int sent, + TaskHandle_t caller) +{ + g_peers.send_reg_calls++; + g_peers.last_snd_fd = sockfd; + g_peers.last_snd_sent = sent; + g_peers.last_snd_len = len; + (void)buf; + if (g_peers.send_register_rc != NET_PENDING_REGISTERED) { + return g_peers.send_register_rc; + } + /* 真实实现的硬前置:sent >= len 视为参数非法拒收(末块边界) */ + if (sent >= len) { + return -1; + } + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) { + s_snd_active[sockfd] = 1; + s_snd_caller[sockfd] = caller; + } + return NET_PENDING_REGISTERED; +} + +void net_send_pending_tick(net_sock_t *p_socks) +{ + int i; + + g_peers.send_tick_calls++; + (void)p_socks; + for (i = 0; i < NET_MAX_SOCKETS; i++) { + if ((s_snd_active[i] != 0) && (s_snd_done_armed[i] != 0)) { + s_snd_done_armed[i] = 0; + s_snd_active[i] = 0; + g_peers.send_notified++; + ns_peers_note_notify(s_snd_caller[i], (uint32_t)s_snd_done_val[i]); + } + } +} + +void net_send_pending_clear(int sockfd) +{ + g_peers.send_clear_calls++; + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS)) { + s_snd_active[sockfd] = 0; + } +} + +void net_send_pending_cancel(int sockfd, int err) +{ + g_peers.send_cancel_calls++; + g_peers.last_snd_cancel_err = err; + if ((sockfd >= 0) && (sockfd < NET_MAX_SOCKETS) && (s_snd_active[sockfd] != 0)) { + s_snd_active[sockfd] = 0; + g_peers.send_notified++; + ns_peers_note_notify(s_snd_caller[sockfd], (uint32_t)err); + } +} + +void net_send_pending_cancel_all(int err) +{ + int i; + + g_peers.send_cancel_all_calls++; + g_peers.last_snd_cancel_err = err; + for (i = 0; i < NET_MAX_SOCKETS; i++) { + if (s_snd_active[i] != 0) { + g_peers.send_notified++; + ns_peers_note_notify(s_snd_caller[i], (uint32_t)err); + } + s_snd_active[i] = 0; + } +} + +int net_send_pending_active(int sockfd) +{ + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return 0; + } + return s_snd_active[sockfd]; +} + +/* ================= net_events.h(事件面与对账) ================= */ + +/* 事件处理桩:按真实头注释的效果改控制块(CONNECT→ESTABLISHED + 记对端等), + * 让 net_poll 的"中断 → 状态机"分派可断言;同时计数。 */ +void handle_connect_event(net_sock_t *p_sock) +{ + g_peers.evt_connect_calls++; + if (p_sock != NULL) { + p_sock->state = NET_SOCK_STATE_ESTABLISHED; + p_sock->readable = 0U; + p_sock->released_abnormal = 0U; + p_sock->session_dead = 0U; + } +} + +void handle_disconnect_event(net_sock_t *p_sock) +{ + g_peers.evt_disconnect_calls++; + if (p_sock != NULL) { + p_sock->last_peer_port = p_sock->remote_port; + p_sock->state = NET_SOCK_STATE_CLOSED; + p_sock->session_dead = 1U; + } +} + +void handle_recv_event(net_sock_t *p_sock) +{ + g_peers.evt_recv_calls++; + if (p_sock != NULL) { + p_sock->readable = 1U; + } +} + +void handle_send_event(net_sock_t *p_sock) +{ + g_peers.evt_send_calls++; + if (p_sock != NULL) { + p_sock->send_ready = 1U; + } +} + +void handle_timeout_event(net_sock_t *p_sock) +{ + g_peers.evt_timeout_calls++; + if (p_sock != NULL) { + p_sock->last_peer_port = p_sock->remote_port; + p_sock->state = NET_SOCK_STATE_CLOSED; + p_sock->released_abnormal = 1U; + p_sock->session_dead = 1U; + } +} + +void net_reconcile_tick(void) +{ + g_peers.reconcile_calls++; +} + +void net_lost_connect_probe(net_sock_t *p_sock) +{ + g_peers.lost_probe_calls++; + g_peers.last_probe_state = (p_sock != NULL) ? (int)p_sock->state : -1; + if ((p_sock != NULL) && (g_peers.lost_probe_hit != 0)) { + /* 脚本:探针命中 → 直接转 ESTABLISHED(复刻补 accept 的净效果) */ + p_sock->state = NET_SOCK_STATE_ESTABLISHED; + p_sock->lost_conn_seen = 0U; + } +} diff --git a/test/host/net_socket/ns_peers.h b/test/host/net_socket/ns_peers.h new file mode 100644 index 00000000..67c9b54c --- /dev/null +++ b/test/host/net_socket/ns_peers.h @@ -0,0 +1,82 @@ +/* + * 模块名称:NET 层兄弟模块桩的观测面(net_socket 主机侧单测专用) + * 模块功能:声明 ns_peers.c 的记录结构与复位入口。原型层面的约束(必须与真实头逐字 + * 一致)在 ns_peers.c 里由"include 真实头 + 直接实现同名函数"保证: + * 签名漂移 = 本套编译期红。 + * ⚠ 仅供 test/host/net_socket/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef NS_PEERS_H +#define NS_PEERS_H + +#include + +#include "FreeRTOS.h" +#include "net_types.h" + +typedef struct { + /* —— net_listener —— */ + int listener_req_calls; + int last_req_fd; + int last_req_reason; + int listener_clear_calls; + int last_clear_fd; + int listener_tick_calls; + int listener_settle_calls; + int last_settle_fd; + int last_settle_level; + int req_armed[NET_MAX_SOCKETS]; /* 登记未清=1(clear/tick 后归 0) */ + int req_reason[NET_MAX_SOCKETS]; + + /* —— net_pending(recv) —— */ + int pending_capture_calls; + int pending_reg_calls; + int pend_register_rc; /* 注入登记结果(默认 NET_PENDING_REGISTERED) */ + int last_pend_fd; + uint32_t last_pend_to; + TaskHandle_t last_pend_caller; + int pending_tick_calls; + int pending_clear_calls; + int last_pend_clear_fd; + int pending_cancel_calls; + int pending_cancel_all_calls; + int last_pend_cancel_err; + int pending_settle_calls; + int pending_notified; /* 经由桩真实发出通知的次数(必通知契约) */ + int pend_active_now[NET_MAX_SOCKETS]; + + /* —— net_pending(send) —— */ + int send_reg_calls; + int send_register_rc; + int last_snd_fd; + int last_snd_sent; + int last_snd_len; + int send_tick_calls; + int send_clear_calls; + int send_cancel_calls; + int send_cancel_all_calls; + int last_snd_cancel_err; + int send_notified; + + /* —— net_events —— */ + int evt_connect_calls; + int evt_disconnect_calls; + int evt_recv_calls; + int evt_send_calls; + int evt_timeout_calls; + int reconcile_calls; + int lost_probe_calls; + int lost_probe_hit; /* 1=让探针命中(补 accept) */ + int last_probe_state; +} ns_peers_obs_t; + +extern ns_peers_obs_t g_peers; + +void ns_peers_reset(void); +void ns_peers_arm_pending_done(int sockfd, int result); +void ns_peers_arm_send_pending_done(int sockfd, int result); + +/* 由 ns_peers.c 调用,把"桩内部完成的通知"接到假内核的通知面上 */ +void ns_peers_note_notify(TaskHandle_t to, uint32_t v); + +#endif /* NS_PEERS_H */ diff --git a/test/host/net_socket/ns_rtos.c b/test/host/net_socket/ns_rtos.c new file mode 100644 index 00000000..d4061ae9 --- /dev/null +++ b/test/host/net_socket/ns_rtos.c @@ -0,0 +1,334 @@ +/* + * 模块名称:假内核实现(net_socket 主机侧单测专用) + * 模块功能:实现 ns_rtos.h 声明的假队列/假通知/假时基。由测试单元 #include 使用。 + * ⚠ 仅供 test/host/net_socket/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ + +#include +#include +#include + +#include "ns_rtos.h" +#include "net_socket.h" + +int g_ns_task_caller; +int g_ns_task_net; + +TaskHandle_t g_ns_cur_task; +ns_pump_fn g_ns_pump; +ns_poll_hook_fn g_ns_on_poll; +int g_ns_pump_calls; +int g_ns_block_waits; +int g_ns_notify_calls; +uint32_t g_ns_notify_last_val; +TaskHandle_t g_ns_notify_last_to; +int g_ns_put_calls; +int g_ns_get_calls; +int g_ns_crit_enter; +int g_ns_crit_exit; +uint32_t g_ns_tick; +osKernelState_t g_ns_kernel_state = osKernelRunning; + +/* 通知槽:按句柄区分(只服务上面两个假任务,超出的句柄一律现场分配不了→报错) */ +typedef struct { + TaskHandle_t h; + uint32_t val; + int pending; +} ns_notify_slot_t; + +static ns_notify_slot_t s_slot[2]; + +/* 假队列:定长环形,存 net_msg_t 按值拷贝(与真队列语义一致) */ +static net_msg_t s_q[NS_Q_CAP]; +static int s_q_head; +static int s_q_count; + +void ns_rtos_reset(void) +{ + (void)memset(s_slot, 0, sizeof(s_slot)); + s_slot[0].h = (TaskHandle_t)&g_ns_task_caller; + s_slot[1].h = (TaskHandle_t)&g_ns_task_net; + (void)memset(s_q, 0, sizeof(s_q)); + s_q_head = 0; + s_q_count = 0; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + g_ns_pump = NULL; + g_ns_on_poll = NULL; + g_ns_pump_calls = 0; + g_ns_block_waits = 0; + g_ns_notify_calls = 0; + g_ns_notify_last_val = 0U; + g_ns_notify_last_to = NULL; + g_ns_put_calls = 0; + g_ns_get_calls = 0; + g_ns_crit_enter = 0; + g_ns_crit_exit = 0; + g_ns_tick = 0U; + g_ns_kernel_state = osKernelRunning; + g_ns_int_pin_set = 1; + g_ns_poll_tick_inc = 1U; + g_ns_poll_calls = 0U; +} + +void ns_rtos_q_flush(void) +{ + s_q_head = 0; + s_q_count = 0; +} + +int ns_rtos_q_count(void) +{ + return s_q_count; +} + +void ns_rtos_q_fill(int n) +{ + int i; + + for (i = 0; (i < n) && ((uint32_t)s_q_count < NS_Q_CAP); i++) { + (void)memset(&s_q[(s_q_head + s_q_count) % (int)NS_Q_CAP], 0, sizeof(net_msg_t)); + s_q_count++; + } +} + +int ns_rtos_q_put_raw(const net_msg_t *msg) +{ + if (msg == NULL || (uint32_t)s_q_count >= NS_Q_CAP) { + return -1; + } + (void)memcpy(&s_q[(s_q_head + s_q_count) % (int)NS_Q_CAP], msg, sizeof(net_msg_t)); + s_q_count++; + return 0; +} + +/* 假内核里的"另一次通知发出":与 xTaskNotify 共用同一套观测计数, + * 这样"谁发了几次通知"在测试里只有一个口径(挂起完成/取消走的就是这条) */ +void ns_rtos_post_notify(TaskHandle_t to, uint32_t v) +{ + int i; + + g_ns_notify_calls++; + g_ns_notify_last_val = v; + g_ns_notify_last_to = to; + for (i = 0; i < 2; i++) { + if (s_slot[i].h == to) { + s_slot[i].val = v; + s_slot[i].pending = 1; + return; + } + } + printf("FATAL: ns_rtos_post_notify 收到未知任务句柄\n"); + exit(2); +} + +static ns_notify_slot_t *slot_of(TaskHandle_t h) +{ + int i; + + for (i = 0; i < 2; i++) { + if (s_slot[i].h == h) { + return &s_slot[i]; + } + } + return NULL; +} + +/* —— CMSIS-RTOS2 侧(原型逐字取自真实 cmsis_os2.h:689/697/298/459)—— */ + +osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr, + uint8_t msg_prio, uint32_t timeout) +{ + (void)mq_id; + (void)msg_prio; + (void)timeout; + + g_ns_put_calls++; + if (msg_ptr == NULL) { + return osErrorParameter; + } + if ((uint32_t)s_q_count >= NS_Q_CAP) { + return osErrorResource; /* 队列满(真机是 osWaitForever 阻塞,测试里用满队列测 BUSY 分支) */ + } + (void)memcpy(&s_q[(s_q_head + s_q_count) % (int)NS_Q_CAP], msg_ptr, sizeof(net_msg_t)); + s_q_count++; + return osOK; +} + +osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr, + uint8_t *msg_prio, uint32_t timeout) +{ + (void)mq_id; + (void)msg_prio; + + g_ns_get_calls++; + if (s_q_count == 0) { + (void)timeout; + return osErrorTimeout; + } + if (msg_ptr != NULL) { + (void)memcpy(msg_ptr, &s_q[s_q_head], sizeof(net_msg_t)); + } + s_q_head = (s_q_head + 1) % (int)NS_Q_CAP; + s_q_count--; + return osOK; +} + +osKernelState_t osKernelGetState(void) +{ + return g_ns_kernel_state; +} + +osStatus_t osDelay(uint32_t ticks) +{ + static int guard; + + g_ns_tick += ticks; + if (++guard > 20000) { + printf("FATAL: osDelay 跑飞(%d 次)—— 等待条件永不满足\n", guard); + exit(2); + } + if (guard > 0) { + /* 每次 osDelay 也让 HAL_GetTick 往前走,600ms 复位上限才量得到 */ + g_ns_tick += 0U; + } + return osOK; +} + +/* —— FreeRTOS 侧(桩 task.h 声明)—— */ + +TaskHandle_t xTaskGetCurrentTaskHandle(void) +{ + return g_ns_cur_task; +} + +BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, + eNotifyAction eAction) +{ + ns_notify_slot_t *p = slot_of(xTaskToNotify); + + (void)eAction; /* 本测试只用 eSetValueWithOverwrite,语义相同 */ + g_ns_notify_calls++; + g_ns_notify_last_val = ulValue; + g_ns_notify_last_to = xTaskToNotify; + if (p == NULL) { + printf("FATAL: xTaskNotify 收到未知任务句柄\n"); + exit(2); + } + p->val = ulValue; + p->pending = 1; + return pdTRUE; +} + +BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, + uint32_t ulBitsToClearOnExit, + uint32_t *pulNotificationValue, + TickType_t xTicksToWait) +{ + ns_notify_slot_t *p = slot_of(g_ns_cur_task); + + if (p == NULL) { + printf("FATAL: xTaskNotifyWait 当前任务句柄未知\n"); + exit(2); + } + if (pulNotificationValue != NULL) { + *pulNotificationValue = p->val & (~ulBitsToClearOnEntry); + } + if (p->pending != 0) { + if (pulNotificationValue != NULL) { + *pulNotificationValue = p->val; + } + p->val = p->val & (~ulBitsToClearOnExit); + p->pending = 0; + return pdTRUE; + } + if (xTicksToWait == 0U) { + return pdFALSE; /* 零等待拾取:没有就是没有 */ + } + + /* 阻塞分支:这就是"睡到 netTask 回执"的调度点 → 驱动一轮 netTask 再拾取 */ + g_ns_block_waits++; + if (g_ns_pump == NULL) { + printf("FATAL: 走到阻塞等待但测试没装 pump 钩子\n"); + exit(2); + } + { + TaskHandle_t save = g_ns_cur_task; + int rounds = 0; + + while (rounds < NS_PUMP_MAX) { + rounds++; + g_ns_pump_calls++; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; /* 以 netTask 身份跑 */ + g_ns_pump(); + g_ns_cur_task = save; + if (p->pending != 0) { + break; + } + } + if (p->pending == 0) { + printf("FATAL: 阻塞等待 %d 轮仍无回执(该通知的没通知 = 真机必挂死)\n", rounds); + exit(2); + } + } + if (pulNotificationValue != NULL) { + *pulNotificationValue = p->val; + } + p->val = p->val & (~ulBitsToClearOnExit); + p->pending = 0; + return pdTRUE; +} + +uint32_t xTaskGetTickCount(void) +{ + return g_ns_tick; +} + +int ns_os_crit_depth(void) +{ + return g_ns_crit_enter - g_ns_crit_exit; +} + +void ns_os_enter_critical(void) +{ + g_ns_crit_enter++; +} + +void ns_os_exit_critical(void) +{ + g_ns_crit_exit++; +} + +/* —— main.h 桩:时基 + INT# 引脚 —— */ + +uint32_t HAL_GetTick(void) +{ + return g_ns_tick; +} + +/* INT# 电平可注入:默认 SET(无中断)——net_poll 默认走"引脚高即退出"的短路分支 */ +int g_ns_int_pin_set = 1; +uint32_t g_ns_poll_tick_inc = 1U; +uint32_t g_ns_poll_calls; + +/* 读 INT# 就是"net_poll 又轮了一轮"的天然计数点: + * - 时基自走 g_ns_poll_tick_inc 毫秒(默认 1ms),NET_SEND_SPIN_MS=2 这种 + * "有界自旋"预算才有可量测的时间轴; + * - 跑飞保险:轮询次数超阈值直接 exit(2)。net_send_locked / net_recvfrom 的 + * 等待环里没有 osDelay,光靠 osDelay 的保险挡不住死循环(实测过一次挂门禁)。 */ +GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) +{ + (void)GPIOx; + (void)GPIO_Pin; + g_ns_poll_calls++; + g_ns_tick += g_ns_poll_tick_inc; + if (g_ns_on_poll != NULL) { + g_ns_on_poll(); /* 并发事件注入点(在时间推进之后,超时判定才量得到) */ + } + if (g_ns_poll_calls > 400000U) { + printf("FATAL: net_poll 轮询 %lu 次仍未收敛(等待条件永不满足)\n", + (unsigned long)g_ns_poll_calls); + exit(2); + } + return (g_ns_int_pin_set != 0) ? GPIO_PIN_SET : GPIO_PIN_RESET; +} diff --git a/test/host/net_socket/ns_s3_msg.c b/test/host/net_socket/ns_s3_msg.c new file mode 100644 index 00000000..5dcb07d5 --- /dev/null +++ b/test/host/net_socket/ns_s3_msg.c @@ -0,0 +1,613 @@ +/* + * 分片 3:跨上下文消息面(net_process_messages 全分派 + 结果经任务通知值回传) + * 前置:net_socket_test.c 已纳入生产源与假内核/假芯片/兄弟模块桩(同一 TU)。 + * + * 这一片是整套里最值钱的部分,钉的是 AGENTS 模块约定的两条硬契约: + * A.「所有 net_* socket 操作经 netMsgQueue 在 netTask 串行执行」 + * B.「net_* 公开 API 的错误码经任务通知值回传:net_msg_t 是按值拷入队列的, + * netTask 改的是自己那份副本,调用方永远看不到 → 错误码搭 xTaskNotify 通知值回来; + * 勿给 net_msg_t 加回传字段(加了只会让调用方读到栈垃圾)」 + * 断言形态因此刻意是:先看**通知值本身**(32 位整体即结果,负值=精确错误码), + * 再看调用方在自己上下文恢复出的 errno——而不是去读队列里的 msg.result + * (那正是旧实现的错觉,也是 B 条明文禁止的方向)。 + * + * 创建日期:2026-10-24 + */ + +/* close 场景的增量快照基准(前面场景也会 clear,必须按增量断言) */ +static int s_close_base_clear; +static int s_close_base_settle; + +/* 造一条已建立的 TCP 会话(多数消息场景的前置) */ +static int mk_established(int type) +{ + int fd = net_socket(NET_AF_INET, type); + net_sock_t *p; + + if (fd < 0) { + return fd; + } + p = &s_net_socks[fd]; + p->state = NET_SOCK_STATE_ESTABLISHED; + p->send_ready = 1U; + p->readable = 0U; + p->local_port = 2404U; + p->remote_port = 51999U; + p->remote_ip = 0x0B01A8C0U; + return fd; +} + +/* TC-NETSOCK-200 未知消息类型:枚举闭合外的值也必须给出**本条消息自身**的确定结果 + * (不许把上一条的回执残留当成这次的结果) */ +static void tc_200_unknown_type(void) +{ + net_msg_t msg; + + bring_up(); + (void)memset(&msg, 0, sizeof(msg)); + msg.type = (net_msg_type_t)999; + msg.sockfd = 0; + msg.caller = (TaskHandle_t)&g_ns_task_caller; + msg.result = 12345; /* 脏值:分派必须覆写它 */ + CHECK(ns_rtos_q_put_raw(&msg) == 0, "TC-NETSOCK-200a 原始消息入队"); + CHECK(g_ns_notify_calls == 0, "TC-NETSOCK-200b 入队本身不产生通知"); + + net_process_messages(); /* 以 netTask 身份跑一轮 */ + + CHECK(g_ns_notify_calls == 1, "TC-NETSOCK-200c 未知 type 仍必发回执(否则调用方永久悬挂)"); + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR, + "TC-NETSOCK-200d 未知 type 的通知值 = 本条的精确错误码 NET_ERR(不是上一条残留)"); + CHECK(ns_rtos_q_count() == 0, "TC-NETSOCK-200e 本轮队列被排空(非阻塞 while 语义)"); + reset_all(); +} + +/* TC-NETSOCK-201 队列为空:四个自愈 tick 必跑,且不产生任何通知 */ +static void tc_201_ticks_run_before_dispatch(void) +{ + bring_up(); + + net_process_messages(); + + CHECK(g_peers.listener_tick_calls == 1 && g_peers.pending_tick_calls == 1 && + g_peers.send_tick_calls == 1 && g_peers.reconcile_calls == 1, + "TC-NETSOCK-201a 空队列也要跑满四个自愈 tick(D10/pending×2/对账)"); + CHECK(g_ns_notify_calls == 0 && g_ns_get_calls == 1, + "TC-NETSOCK-201b 空队列:取一次即退,不发通知"); + reset_all(); +} + +/* TC-NETSOCK-202 队列句柄未绑定:直接返回,连 tick 都不该跑 */ +static void tc_202_no_queue_bound(void) +{ + reset_all(); + g_ns_pump = net_process_messages; + + net_process_messages(); + + CHECK(g_peers.listener_tick_calls == 0 && g_ns_get_calls == 0, + "TC-NETSOCK-202 未绑定队列就调 net_process_messages → 立即返回(不碰芯片也不跑 tick)"); + reset_all(); +} + +/* TC-NETSOCK-210 SEND 快路径:端到端跑通"入队 → 分派 → 写芯片 → 通知值=字节数" */ +static void tc_210_send_end_to_end(void) +{ + char buf[8]; + int fd; + int rc; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + (void)memset(buf, 'A', sizeof(buf)); + + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + rc = net_send(fd, buf, (int)sizeof(buf), 0); + + CHECK(rc == (int)sizeof(buf), "TC-NETSOCK-210a net_send 返回实际写入字节数"); + CHECK(g_chip.write_calls == 1 && g_chip.last_read_len == (uint16_t)sizeof(buf), + "TC-NETSOCK-210b 一次写满(健康链路不拆块、不进挂起)"); + CHECK(s_net_socks[fd].send_bytes == sizeof(buf), "TC-NETSOCK-210c 发送字节统计累计"); + CHECK(g_ns_notify_calls == 1 && g_ns_notify_last_val == (uint32_t)sizeof(buf), + "TC-NETSOCK-210d 结果经通知值回传(32 位整体即结果)"); + CHECK(g_ns_notify_last_to == (TaskHandle_t)&g_ns_task_caller, + "TC-NETSOCK-210e 通知发给**登记的那个 caller**,不是广播"); + CHECK(g_ns_block_waits == 1, + "TC-NETSOCK-210f 零等待没拾到 → 走阻塞分支(两段式拾取的第二段)"); + reset_all(); +} + +/* TC-NETSOCK-211 大帧分块:超过 NET_SEND_CHUNK_MAX 必须按硬件发送缓冲上界切块 + * (TRAP-NET-16:单次写 >1KB 会写穿 1KB 发送 FIFO) */ +static void tc_211_send_chunking(void) +{ + static char buf[3000]; + int fd; + int rc; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + /* 造"每写完一块芯片就报 SENBUF_FREE"的健康链路:让 net_poll 把 send_ready 置回来 */ + g_ns_int_pin_set = 0; + ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); + /* SENBUF_FREE 是**条件位**:发送缓冲空着就还会再报。用假件的电平型掩码而不是 + * 一次性位图,否则第二块起就再也等不到空闲 → 变成测假件的清零时机。 */ + ch395f_fake_set_sint_sticky((uint8_t)fd, CH395F_SINT_STAT_SENBUF_FREE); + + rc = net_send(fd, buf, 2500, 0); + + CHECK(rc == 2500, "TC-NETSOCK-211a 2500 字节全部交付"); + CHECK(g_chip.write_calls == 3, "TC-NETSOCK-211b 按 1024 上界切成 3 块(1024/1024/452)"); + CHECK(g_chip.last_read_len == 452U, "TC-NETSOCK-211c 末块只写剩余量"); + CHECK(NET_SEND_CHUNK_MAX == 1024, + "TC-NETSOCK-211d 分块粒度取芯片发送缓冲(不是假件自定的数)"); + reset_all(); +} + +/* TC-NETSOCK-212 SEND 挂起登记:等发送缓冲超预算 → 登记挂起并**跳过即时回执** */ +static void tc_212_send_pending_defers_notify(void) +{ + static char buf[64]; + net_msg_t msg; + int fd; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].send_ready = 0U; /* 芯片一直不腾缓冲 */ + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_SEND; + msg.sockfd = fd; + msg.buf = buf; + msg.len = (int)sizeof(buf); + msg.caller = (TaskHandle_t)&g_ns_task_caller; + + CHECK(ns_rtos_q_put_raw(&msg) == 0, "TC-NETSOCK-212a SEND 消息入队"); + net_process_messages(); + + CHECK(g_peers.send_reg_calls == 1, + "TC-NETSOCK-212b 超自旋预算后登记挂起 send(而不是继续占着 netTask 忙等)"); + CHECK(g_peers.last_snd_fd == fd && g_peers.last_snd_len == (int)sizeof(buf) && + g_peers.last_snd_sent == 0, + "TC-NETSOCK-212c 登记的进度=已写芯片字节数(此处 0),总长与 caller 一并带走"); + CHECK(g_ns_notify_calls == 0, + "TC-NETSOCK-212d 登记成功**绝不发即时回执**(否则「登记成功」被当成本次结果)"); + CHECK(g_ns_notify_last_to == NULL, + "TC-NETSOCK-212e 一次通知都没发生(0x70000000 哨兵值也没可能漏进通知值)"); + + /* 完成时刻:由挂起 send 的 tick 结束(本套由桩脚本模拟"缓冲腾出来了") */ + ns_peers_arm_send_pending_done(fd, 64); + net_process_messages(); + CHECK(g_ns_notify_calls == 1 && g_ns_notify_last_val == 64U, + "TC-NETSOCK-212f 挂起完成后才发回执,通知值=实际写出字节数"); + reset_all(); +} + +/* TC-NETSOCK-213 SEND 失败:未建立连接 → 通知值带**精确**错误码,不是笼统 -1 */ +static void tc_213_send_error_code_roundtrip(void) +{ + char buf[4]; + net_msg_t msg; + int fd; + int rc; + + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); /* CREATED,未连接 */ + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_SEND; + msg.sockfd = fd; + msg.buf = buf; + msg.len = (int)sizeof(buf); + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_NOTCONN, + "TC-NETSOCK-213a 未连接的 SEND:通知值=NET_ERR_NOTCONN(精确码回传,供调用方分支)"); + + /* 端到端:公开 net_send 的 BADF 形态(fd 越界由**分派侧**再判一次) */ + reset_all(); + bring_up(); + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_SEND; + msg.sockfd = 3; /* 合法范围内但未分配 */ + msg.buf = buf; + msg.len = 4; + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, + "TC-NETSOCK-213b 范围内但未分配的 fd:net_get_sock 判空 → BADF 搭通知值回来"); + + /* 调用方上下文恢复 errno:拿到的必须是"本任务的"精确码 */ + reset_all(); + bring_up(); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + rc = net_send(6, buf, 4, 0); /* 6 未分配 → 分派侧 BADF → 通知值 -2 */ + CHECK(rc == -1, "TC-NETSOCK-213c 对外保持 BSD 语义:失败一律 -1"); + CHECK(net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-213d 调用方在自己的错误码槽里拿到精确码(E-03 + 通知值回传合力)"); + reset_all(); +} + +/* TC-NETSOCK-220 RECV 快路径:芯片缓冲里已有数据 → 当场读出,不登记挂起 */ +static void tc_220_recv_fast_path(void) +{ + char buf[16]; + int fd; + int rc; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].readable = 1U; + ch395f_fake_push_recv_len((uint8_t)fd, 1400U); + + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + rc = net_recv(fd, buf, 10, NET_MSG_DONTWAIT); + + CHECK(rc == 10, "TC-NETSOCK-220a 缓冲 1400B、只给 10B 缓冲 → 读满 10(部分读合法)"); + CHECK(g_chip.read_calls == 1 && g_chip.last_read_len == 10U, + "TC-NETSOCK-220b 只读调用方给的量(不得按芯片可用量越界写)"); + CHECK(s_net_socks[fd].recv_bytes == 10U, "TC-NETSOCK-220c 接收字节统计累计"); + CHECK(s_net_socks[fd].readable == 0U, + "TC-NETSOCK-220d 读过即清 readable(下一次 RECV_OK 中断再置)"); + CHECK(g_peers.pending_reg_calls == 0, "TC-NETSOCK-220e 数据已在手,不该登记挂起"); + reset_all(); +} + +/* TC-NETSOCK-221 RECV 非阻塞无数据 → WOULDBLOCK(且不许登记挂起) */ +static void tc_221_recv_wouldblock(void) +{ + char buf[8]; + int fd; + int rc; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT); + CHECK(rc == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, + "TC-NETSOCK-221a DONTWAIT 无数据 → -1 + WOULDBLOCK"); + CHECK(g_peers.pending_reg_calls == 0, + "TC-NETSOCK-221b 非阻塞绝不登记挂起(否则调用方以为自己在等,实际已被挂住)"); + + /* 模式属性(net_setblocking(0))与单次标志同判据 */ + CHECK(net_setblocking(fd, 0) == 0, "TC-NETSOCK-221c 切非阻塞模式成功"); + rc = net_recv(fd, buf, (int)sizeof(buf), 0); + CHECK(rc == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, + "TC-NETSOCK-221d blocking 属性=0 与 NET_MSG_DONTWAIT 等价生效"); + CHECK(net_recv_nb(fd, buf, (int)sizeof(buf)) == -1 && + net_get_errno() == NET_ERR_WOULDBLOCK, + "TC-NETSOCK-221e net_recv_nb 具名入口同路径"); + reset_all(); +} + +/* TC-NETSOCK-222 RECV EOF:会话已结束 + 无数据 → 返 0 并登记重建(不是 -1) */ +static void tc_222_recv_eof_relisten(void) +{ + char buf[8]; + int fd; + int rc; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; + s_net_socks[fd].auto_relisten = 1U; + + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT); + + CHECK(rc == 0, "TC-NETSOCK-222a CLOSED+无数据 → EOF(0),不是错误"); + CHECK(g_peers.listener_req_calls == 1 && + g_peers.last_req_reason == (int)NET_LISTENER_REQ_EOF, + "TC-NETSOCK-222b EOF 登记 REQ_EOF(重建动作交监听层唯一执行者,本层不动芯片)"); + CHECK(g_chip.open_calls == 0 && g_chip.listen_calls == 0 && g_chip.close_calls == 0, + "TC-NETSOCK-222c 登记路径上**一个芯片事务都不发**(单一所有者不变量 II)"); + + /* 抢先 relisten 后 state 已回 LISTENING,但会话电平仍是"已结束" */ + reset_all(); + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].state = NET_SOCK_STATE_LISTENING; + s_net_socks[fd].session_dead = 1U; + s_net_socks[fd].auto_relisten = 1U; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT); + CHECK(rc == 0, + "TC-NETSOCK-222d 已 relisten 回 LISTENING 仍判 EOF(N-32 P1 电平化:否则 EOF 永久失踪)"); + + /* auto_relisten=0(客户端 fd):EOF 不登记重建,交调用方决定重连 */ + reset_all(); + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; + s_net_socks[fd].session_dead = 1U; + s_net_socks[fd].auto_relisten = 0U; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT); + CHECK(rc == 0 && g_peers.listener_req_calls == 0, + "TC-NETSOCK-222e 非监听 fd 的 EOF 不许替调用方擅自重建"); + reset_all(); +} + +/* TC-NETSOCK-223 RECV 阻塞无数据 → 登记挂起 + 跳过即时回执;到点由 tick 结束 */ +static void tc_223_recv_pending_roundtrip(void) +{ + char buf[16]; + net_msg_t msg; + int fd; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_RECV; + msg.sockfd = fd; + msg.buf = buf; + msg.len = (int)sizeof(buf); + msg.flags = 0; + msg.timeout_ms = 250U; /* 显式预算必须原样带到登记处 */ + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + + CHECK(g_peers.pending_reg_calls == 1 && g_peers.last_pend_fd == fd, + "TC-NETSOCK-223a 阻塞无数据 → 登记挂起 recv(netTask 零占用返回)"); + CHECK(g_peers.last_pend_to == 250U, + "TC-NETSOCK-223b 调用方的显式预算随登记带走(0=无限,这里必须不是 0)"); + CHECK(g_peers.last_pend_caller == (TaskHandle_t)&g_ns_task_caller, + "TC-NETSOCK-223c 完成时要知道通知谁"); + CHECK(g_ns_notify_calls == 0, + "TC-NETSOCK-223d 登记即发回执 = 把「登记成功」当结果,本条钉它没发生"); + + /* 数据到达:由挂起 recv 的 tick 结束(脚本模拟) */ + ns_peers_arm_pending_done(fd, 7); + net_process_messages(); + CHECK(g_ns_notify_calls == 1 && g_ns_notify_last_val == 7U, + "TC-NETSOCK-223e 数据到达后才回执,通知值=字节数"); + reset_all(); +} + +/* TC-NETSOCK-224 net_recv_to:分片超时用独立返回码 -2(不靠 errno,避免跨任务竞态) */ +static void tc_224_recv_to_timeout_code(void) +{ + char buf[16]; + int fd; + int rc; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + + /* 先造一条"已登记"的挂起,再让 tick 判到点 */ + ns_peers_arm_pending_done(fd, NET_ERR_TIMEDOUT); + rc = net_recv_to(fd, buf, (int)sizeof(buf), 30U); + CHECK(rc == -2, "TC-NETSOCK-224a 本片超时 → 独立返回码 -2(与真错误 -1 区分)"); + reset_all(); +} + +/* TC-NETSOCK-225 RECV 参数非法:不碰芯片、不登记 */ +static void tc_225_recv_bad_args(void) +{ + char buf[4]; + int fd; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + + CHECK(net_recv_to(0, buf, 4, 0U) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-225a net_recv_to(timeout=0) 直接拒——0 在 netTask 侧是「无限等待」," + "与本接口「有界占用」的存在理由相反"); + CHECK(g_ns_put_calls == 0, "TC-NETSOCK-225b 拒绝发生在入队之前"); + + CHECK(net_recv(fd, NULL, 4, NET_MSG_DONTWAIT) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-225c buf 为 NULL → INVAL"); + CHECK(net_recv(fd, buf, 0, NET_MSG_DONTWAIT) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-225d len<=0 → INVAL"); + CHECK(net_send(fd, NULL, 4, 0) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-225e send 的 buf 为 NULL → INVAL"); + CHECK(net_send(fd, buf, -1, 0) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-225f send 的 len<0 → INVAL"); + CHECK(g_chip.write_calls == 0 && g_chip.read_calls == 0 && + g_peers.pending_reg_calls == 0 && g_peers.send_reg_calls == 0, + "TC-NETSOCK-225g 参数非法路径上零芯片事务、零挂起登记"); + reset_all(); +} + +/* TC-NETSOCK-230 CLOSE:先结束挂起再关芯片,且 last_peer_port 跨释放保留 */ +static void tc_230_close_order_and_peer(void) +{ + net_msg_t msg; + int fd; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].auto_relisten = 1U; + /* 登记一条挂起 recv,再看 close 是否先把它结算掉(排空/取消都必通知) */ + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_RECV; + msg.sockfd = fd; + msg.buf = (void *)"x"; + msg.len = 1; + msg.caller = (TaskHandle_t)&g_ns_task_net; /* 另一个任务在等这条流 */ + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); /* 走登记路径(不发回执) */ + CHECK(g_peers.pending_reg_calls == 1 && g_ns_notify_calls == 0, + "TC-NETSOCK-230a 前置:已登记挂起且未回执"); + + /* 芯片侧此刻仍是 OPEN:close 才需要真去拆(已 CLOSED 的形态见 TC-NETSOCK-232) */ + g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; + g_ns_notify_calls = 0; + { + s_close_base_clear = g_peers.listener_clear_calls; + s_close_base_settle = g_peers.pending_settle_calls; + } + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_CLOSE; + msg.sockfd = fd; + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + + CHECK(g_peers.pending_settle_calls == s_close_base_settle + 1, + "TC-NETSOCK-230b close 必先结算本 fd 的挂起 recv(否则等待方永久悬挂)"); + CHECK(g_peers.pending_notified == 1, + "TC-NETSOCK-230c 结算必通知(取消也要通知,不是静默清槽)"); + CHECK(g_chip.close_calls >= 1 && g_fxchip.rst_begin_calls == 1, + "TC-NETSOCK-230d 芯片仍 OPEN → 走 sock_reset 确定性排渣把它拆掉"); + CHECK(g_fxchip.glob_calls == 0, + "TC-NETSOCK-230e close 全程不得读全局中断寄存器(B3:唯一读者=net_poll)"); + CHECK(s_net_socks[fd].in_use == 0U && s_net_socks[fd].last_peer_port == 51999U, + "TC-NETSOCK-230f 控制块清零但 last_peer_port 保留(幽灵甄别基准属芯片 Socket 级)"); + CHECK(g_peers.listener_clear_calls == s_close_base_clear + 1, + "TC-NETSOCK-230g close 幂等清理监听重试(关闭即取消待重试,D10)"); + CHECK(g_peers.pend_active_now[fd] == 0, + "TC-NETSOCK-230h close 后本 fd 不再有挂起 recv(两张表都归零)"); + reset_all(); +} + +/* TC-NETSOCK-231 CLOSE 未在用:net_get_sock 判空 → BADF 通知值 */ +static void tc_231_close_badfd(void) +{ + net_msg_t msg; + + bring_up(); + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_CLOSE; + msg.sockfd = 5; + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, + "TC-NETSOCK-231a close 未分配的 fd → BADF 经通知值回传"); + CHECK(g_chip.close_calls == 0, "TC-NETSOCK-231b BADF 不碰芯片"); + + bring_up(); + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_ACCEPT; /* 没有 in_use 的监听 */ + msg.sockfd = 5; + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_INVAL, + "TC-NETSOCK-231c accept 未在用 → INVAL(控制块级校验,不是 fd 越界)"); + reset_all(); +} + +/* TC-NETSOCK-240 DROP_CONN:四类前置各走各的分支(幂等语义是关键) */ +static void tc_240_drop_conn_branches(void) +{ + net_msg_t msg; + int fd; + + /* 非在用 → BADF */ + bring_up(); + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_DROP_CONN; + msg.sockfd = 2; + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, "TC-NETSOCK-240a 未分配 → BADF"); + + /* UDP → INVAL */ + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_DROP_CONN; + msg.sockfd = fd; + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_INVAL, + "TC-NETSOCK-240b 非 TCP 流 → INVAL(本 API 只拆 TCP 会话)"); + + /* 监听态但无活动会话 → 幂等 0,不动芯片 */ + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + s_net_socks[fd].state = NET_SOCK_STATE_LISTENING; + s_net_socks[fd].auto_relisten = 1U; + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_DROP_CONN; + msg.sockfd = fd; + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + CHECK(g_ns_notify_last_val == 0U, "TC-NETSOCK-240c 无活动会话 → 幂等成功 0"); + CHECK(g_chip.close_calls == 0 && g_fxchip.rst_begin_calls == 0, + "TC-NETSOCK-240d 幂等分支不得拆芯片(会把在听的监听毁掉)"); + + /* ESTABLISHED + 监听型 → QUARANTINE 拆除 + 登记重建 */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].auto_relisten = 1U; + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_DROP_CONN; + msg.sockfd = fd; + msg.caller = (TaskHandle_t)&g_ns_task_caller; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + + CHECK(g_peers.pending_settle_calls == 1 && g_peers.send_cancel_calls == 1, + "TC-NETSOCK-240e 拆会话前先结束两张表的挂起(必通知)"); + CHECK(g_fxchip.rst_begin_calls == 1, + "TC-NETSOCK-240f 拆除走 sock_reset 确定性排渣(不是 close+盲等)"); + CHECK(g_peers.listener_req_calls == 1 && + g_peers.last_req_reason == (int)NET_LISTENER_REQ_DROP, + "TC-NETSOCK-240g 监听型 fd 拆完登记 REQ_DROP(重建交监听层,本层不自己 open)"); + CHECK(s_net_socks[fd].state == NET_SOCK_STATE_CLOSED && + s_net_socks[fd].in_use == 1U && s_net_socks[fd].local_port == 2404U && + s_net_socks[fd].auto_relisten == 1U, + "TC-NETSOCK-240h 只收口会话簿记:fd 保留、监听属性与端口都留下"); + CHECK(g_fxchip.glob_calls == 0, + "TC-NETSOCK-240i 拆除路径也只碰本 Socket(F-NET-7:不读全局)"); + reset_all(); +} + +/* TC-NETSOCK-250 入队失败(队列满)→ 调用方 BUSY,不留下"已提交"的假象 */ +static void tc_250_put_fail_busy(void) +{ + int fd; + int rc; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + ns_rtos_q_fill((int)NS_Q_CAP); /* 把队列灌满 */ + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + + rc = net_send(fd, "abc", 3, 0); + CHECK(rc == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-250a 队列满 → net_send 报 BUSY(不是静默丢弃)"); + CHECK(g_chip.write_calls == 0 && g_ns_notify_calls == 0, + "TC-NETSOCK-250b 入队失败时零芯片事务、零通知(调用方不会误等回执)"); + CHECK(net_listen(fd) == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-250c 同一形态对 net_listen 成立"); + CHECK(net_close(fd) == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-250d 同一形态对 net_close 成立"); + CHECK(net_drop_conn(fd) == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-250e 同一形态对 net_drop_conn 成立"); + CHECK(net_recv_to(fd, (void *)"x", 1, 5U) == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-250f 同一形态对 net_recv_to 成立"); + reset_all(); +} + +static void ns_run_s3(void) +{ + tc_200_unknown_type(); + tc_201_ticks_run_before_dispatch(); + tc_202_no_queue_bound(); + tc_210_send_end_to_end(); + tc_211_send_chunking(); + tc_212_send_pending_defers_notify(); + tc_213_send_error_code_roundtrip(); + tc_220_recv_fast_path(); + tc_221_recv_wouldblock(); + tc_222_recv_eof_relisten(); + tc_223_recv_pending_roundtrip(); + tc_224_recv_to_timeout_code(); + tc_225_recv_bad_args(); + tc_230_close_order_and_peer(); + tc_231_close_badfd(); + tc_240_drop_conn_branches(); + tc_250_put_fail_busy(); +} diff --git a/test/host/net_socket/ns_s4_state.c b/test/host/net_socket/ns_s4_state.c new file mode 100644 index 00000000..6ebe7890 --- /dev/null +++ b/test/host/net_socket/ns_s4_state.c @@ -0,0 +1,346 @@ +/* + * 分片 4:net_poll 中断分派、监听/可读闸门、close 与 drop_conn 的拆除序列 + * + * 这一片钉的是 AGENTS 模块约定里**没有文档就无法发现**的那条芯片约束: + * 所有 CH395F 中断在 net_poll() 内经 GET_GLOB_INT_STATUS 集中读取并清除,不得在 + * ISR 或其他地方单独处理。全局寄存器整体读即清、芯片无按位清除手段,而 net_poll + * 只在全局位为 1 时才读该 Socket 的 SINT → 顺手全量清一遍等于把别的在用 Socket 的 + * 门焊死(B3 实证,详见 docs/网络/网络自愈架构设计.md §4.10)。 + * + * 断言手法(刻意不用「读了几次」这种弱口径):GET_SOCK_INT_STATUS 是**读即清**, + * 所以给每个 Socket 预置互不相同的挂起位图,跑一轮 net_poll 后看哪些位图被清掉了 + * ——被清=被读过。这比数调用次数更硬:它直接照出「门有没有被焊死」。 + * + * 创建日期:2026-10-24 + */ + +/* 预置:给每个 Socket 挂一个只有自己才有的 SINT 位图(只用低 6 位,避开保留位) */ +static void prime_sint_masks(void) +{ + int i; + + for (i = 0; i < NET_MAX_SOCKETS; i++) { + g_fake.int_status[i] = (uint8_t)(0x01U << (i % 6)); + } + g_chip.int_status_calls = 0; +} + +/* TC-NETSOCK-300 net_poll 的前置闸门 */ +static void tc_300_poll_gates(void) +{ + /* 1) 未初始化:直接 0,连引脚都不该读 */ + reset_all(); + g_ns_pump = net_process_messages; + CHECK(net_poll() == 0, "TC-NETSOCK-300a 未初始化 → 0 事件"); + CHECK(g_ns_poll_calls == 0U && g_fxchip.glob_calls == 0, + "TC-NETSOCK-300b 未初始化不得读 INT# 引脚、不得读全局中断(省 SPI)"); + + /* 2) 初始化了但 INT# 为高(无中断待处理):不读全局寄存器 */ + bring_up(); + g_ns_int_pin_set = 1; + CHECK(net_poll() == 0, "TC-NETSOCK-300c INT# 高 → 0 事件"); + CHECK(g_fxchip.glob_calls == 0, + "TC-NETSOCK-300d INT# 高时不得发全局中断读取(引脚轮询就是为省这一步)"); + + /* 3) INT# 低但全局状态为 0:读了全局一次,且不进 Socket 循环 */ + bring_up(); + g_ns_int_pin_set = 0; + ch395f_fake_push_glob(0U); + CHECK(net_poll() == 0, "TC-NETSOCK-300e 全局位全 0 → 0 事件"); + CHECK(g_fxchip.glob_calls == 1 && g_chip.int_status_calls == 0, + "TC-NETSOCK-300f 全局只读一次,读到 0 立即收手(不逐个 Socket 扫)"); + reset_all(); +} + +/* TC-NETSOCK-301 门控精度:只读全局位置位的那个 Socket 的 SINT(B3 的正面表述) */ +static void tc_301_poll_only_gated_sockets(void) +{ + int i; + int a; + int b; + + bring_up(); + a = mk_established(NET_SOCK_STREAM); + b = mk_established(NET_SOCK_STREAM); + (void)mk_established(NET_SOCK_STREAM); + CHECK(a == 0 && b == 1, "TC-NETSOCK-301a 前置:三个在用 fd"); + + prime_sint_masks(); + g_ns_int_pin_set = 0; + ch395f_fake_push_glob((uint16_t)(1U << (1 + 4))); /* Socket n 的位 = bit(n+4) */ + + (void)net_poll(); + + CHECK(g_fake.int_status[0] == (uint8_t)(0x01U << 0) && + g_fake.int_status[2] == (uint8_t)(0x01U << 2), + "TC-NETSOCK-301b 未点亮的 Socket 0/2 挂起位图没被清(读即清=读过,没读=还在)"); + CHECK(g_fake.int_status[1] == 0U, "TC-NETSOCK-301c 点亮的 Socket 1 被读走(位图清零)"); + CHECK(g_chip.int_status_calls == 1, "TC-NETSOCK-301d 整轮只发了一次 SINT 读取"); + /* 缓存刷新环是另一回事:它对所有在用 fd 都要刷(单一事实源 I1) */ + CHECK(g_fxchip.rst_poll_calls == 3, + "TC-NETSOCK-301e 每轮对全部 3 个在用 fd 步进 sock_reset(IDLE 槽零 SPI)"); + (void)net_poll(); + CHECK(g_fxchip.rst_poll_calls == 6, + "TC-NETSOCK-301f 再一轮再加 3 次:刷新环不受全局位门控(事件面与状态面分离)"); + reset_all(); + + /* 高位字节:Socket 4~7 的全局位在 bit8~bit11 */ + bring_up(); + for (i = 0; i < 5; i++) { + (void)mk_established(NET_SOCK_STREAM); + } + prime_sint_masks(); + g_ns_int_pin_set = 0; + ch395f_fake_push_glob((uint16_t)(1U << (4 + 4))); + (void)net_poll(); + CHECK(g_fake.int_status[4] == 0U && g_fake.int_status[3] == (uint8_t)(0x01U << 3), + "TC-NETSOCK-301g Socket4~7 用高位字节 bit(n+4)=bit8..11 定位(算错就永远收不到 4~7 的事件)"); + reset_all(); +} + +/* TC-NETSOCK-302 六种 SINT 位各自的分派与事件计数 */ +static void tc_302_poll_event_dispatch(void) +{ + static const uint8_t bits[6] = { + CH395F_SINT_STAT_CONNECT, CH395F_SINT_STAT_DISCONNECT, CH395F_SINT_STAT_RECV_OK, + CH395F_SINT_STAT_SEND_OK, CH395F_SINT_STAT_SOCK_TIMEOUT, CH395F_SINT_STAT_SENBUF_FREE + }; + int k; + int fd; + + for (k = 0; k < 6; k++) { + int expect = (bits[k] == CH395F_SINT_STAT_SENBUF_FREE) ? 0 : 1; + int n; + + reset_all(); + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].send_ready = 0U; + g_ns_int_pin_set = 0; + g_fake.int_status[fd] = bits[k]; + ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); + + n = net_poll(); + CHECK(n == expect, "TC-NETSOCK-302a 事件计数按位型区分(SENBUF_FREE 是状态位不是事件)"); + switch (bits[k]) { + case CH395F_SINT_STAT_CONNECT: + CHECK(g_peers.evt_connect_calls == 1, "TC-NETSOCK-302b CONNECT → handle_connect_event"); + break; + case CH395F_SINT_STAT_DISCONNECT: + CHECK(g_peers.evt_disconnect_calls == 1, "TC-NETSOCK-302c DISCONNECT → handle_disconnect_event"); + break; + case CH395F_SINT_STAT_RECV_OK: + CHECK(g_peers.evt_recv_calls == 1 && s_net_socks[fd].readable == 1U, + "TC-NETSOCK-302d RECV_OK → handle_recv_event 置 readable 电平"); + break; + case CH395F_SINT_STAT_SEND_OK: + CHECK(g_peers.evt_send_calls == 1, "TC-NETSOCK-302e SEND_OK → handle_send_event"); + break; + case CH395F_SINT_STAT_SOCK_TIMEOUT: + CHECK(g_peers.evt_timeout_calls == 1, "TC-NETSOCK-302f TIMEOUT → handle_timeout_event"); + break; + default: + CHECK(s_net_socks[fd].send_ready == 1U && g_peers.evt_send_calls == 0, + "TC-NETSOCK-302g SENBUF_FREE 只置 send_ready(不发事件、不误当 SEND_OK)"); + break; + } + CHECK(g_peers.evt_connect_calls + g_peers.evt_disconnect_calls + + g_peers.evt_recv_calls + g_peers.evt_send_calls + + g_peers.evt_timeout_calls <= 1, + "TC-NETSOCK-302h 一位进来只敲一个 handler(不串味)"); + } + + /* 同一次 SINT 里多个位 → 多个 handler 都被叫到,事件数叠加 */ + reset_all(); + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_ns_int_pin_set = 0; + g_fake.int_status[fd] = (uint8_t)(CH395F_SINT_STAT_RECV_OK | CH395F_SINT_STAT_DISCONNECT); + ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); + CHECK(net_poll() == 2, "TC-NETSOCK-302i 同批多事件位:各计一次(不吞后到的那位)"); + CHECK(g_peers.evt_recv_calls == 1 && g_peers.evt_disconnect_calls == 1, + "TC-NETSOCK-302j 两位各归各的 handler"); + reset_all(); +} + +/* TC-NETSOCK-303 PHY 变更位经驱动 feed 落到链路缓存(F-DRV-13 接线) */ +static void tc_303_phy_feed(void) +{ + bring_up(); + g_ns_int_pin_set = 0; + g_fx.phy_status = CH395F_PHY_DISCONN; /* 下一次 feed 读到断开 */ + ch395f_fake_push_glob(CH395F_GINT_STAT_PHY_CHANGE); + (void)net_poll(); + CHECK(g_fake.link_up == 0U && g_fake.link_change == 1U, + "TC-NETSOCK-303 PHY_CHANGE → 驱动重读 PHY 并计一次链路变更(拔网线不再静默)"); + reset_all(); +} + +/* TC-NETSOCK-310 监听门控 net_listener_armed 的三支放行与全部挡下形态 */ +static void tc_310_listener_armed(void) +{ + int fd; + + reset_all(); + CHECK(net_listener_armed(-1) == 0 && net_listener_armed(NET_MAX_SOCKETS) == 0, + "TC-NETSOCK-310a 越界 fd → 0(纯 RAM 读,不发 RPC)"); + CHECK(net_listener_armed(0) == 0, "TC-NETSOCK-310b 未分配的 fd → 0"); + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + CHECK(net_listener_armed(fd) == 1, + "TC-NETSOCK-310c ESTABLISHED(待 accept 电平)必须放行:i104 台架实证不放行则会话整场饿死"); + s_net_socks[fd].state = NET_SOCK_STATE_LISTENING; + CHECK(net_listener_armed(fd) == 1, + "TC-NETSOCK-310d LISTENING 放行:补 accept 探针在 net_accept_locked 内部,挡住永远到不了"); + + s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; + s_net_socks[fd].auto_relisten = 1U; + s_net_socks[fd].local_port = 7003U; + s_net_socks[fd].readable = 1U; + CHECK(net_listener_armed(fd) == 1, + "TC-NETSOCK-310e CLOSED+auto_relisten+有残留数据 → 放行(保住 closed-with-readable 接受)"); + s_net_socks[fd].readable = 0U; + CHECK(net_listener_armed(fd) == 0, + "TC-NETSOCK-310f CLOSED 但无残留 → 必须挡下(否则整个 CLOSED 窗每轮发 RPC → 喂不上狗)"); + s_net_socks[fd].readable = 1U; + s_net_socks[fd].auto_relisten = 0U; + CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310g 非监听型 fd 的 CLOSED 不放行"); + s_net_socks[fd].auto_relisten = 1U; + s_net_socks[fd].local_port = 0U; + CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310h 还没绑端口的半成品监听不放行"); + s_net_socks[fd].state = NET_SOCK_STATE_CONNECTING; + CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310i CONNECTING 不是监听态 → 0"); + reset_all(); +} + +/* TC-NETSOCK-311 recv 门控 net_can_read:非 ESTABLISHED 一律放行 */ +static void tc_311_can_read(void) +{ + int fd; + + reset_all(); + CHECK(net_can_read(0) == 1, + "TC-NETSOCK-311a 描述符失效 → 1:让 recv 去报 BADF,别把连接不存在藏住"); + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].readable = 0U; + CHECK(net_can_read(fd) == 0, "TC-NETSOCK-311b ESTABLISHED 且无可读 → 0"); + s_net_socks[fd].readable = 1U; + CHECK(net_can_read(fd) == 1, "TC-NETSOCK-311c readable 置位 → 1"); + s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; + s_net_socks[fd].readable = 0U; + CHECK(net_can_read(fd) == 1, + "TC-NETSOCK-311d 连接已非 ESTABLISHED → 必须放行一次 recv 取回 EOF/错误"); + reset_all(); +} + +/* TC-NETSOCK-320 close 的拆除序列:快路径(芯片已 CLOSED)与慢路径(走复位机) */ +static void tc_320_close_two_paths(void) +{ + int fd; + + /* 快路径:芯片已 CLOSED → 只清本 Socket 残留事件,不发复位 */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_fake.cached_open[fd] = CH395F_SOCKET_CLOSED; + CHECK(net_close(fd) == 0, "TC-NETSOCK-320a close 成功(快路径)"); + CHECK(g_fxchip.cmd_done_calls >= 1 && g_fxchip.rst_begin_calls == 0, + "TC-NETSOCK-320b 已 CLOSED → 跳过复位(省掉 100~600ms 的无占用)"); + CHECK(g_fxchip.sock_events_calls >= 1, + "TC-NETSOCK-320c 快路径仍要清本 Socket 残留事件(防下一连接误判刚建立就断开)"); + CHECK(g_fxchip.glob_calls == 0, "TC-NETSOCK-320d 全程零全局读取(F-NET-7)"); + reset_all(); + + /* 慢路径:芯片还 OPEN → 走 sock_reset,且必须是 GRACEFUL */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; + CHECK(net_close(fd) == 0, "TC-NETSOCK-320e close 成功(慢路径)"); + CHECK(g_fxchip.rst_begin_calls == 1 && g_fx.rst_mode[fd] == 0U, + "TC-NETSOCK-320f 慢路径用 GRACEFUL(正常释放 100ms 底),不是 QUARANTINE"); + CHECK(g_fx.rst_state[fd] == (uint8_t)CH395F_FAKE_RST_IDLE, + "TC-NETSOCK-320g 复位机收口回 IDLE(槽位可复用,不搁浅)"); + reset_all(); + + /* 复位启动被拒(槽位已在运行)→ close 仍放行,只留痕 */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; + g_fx.rst_state[fd] = (uint8_t)CH395F_FAKE_RST_DRAINED; + CHECK(net_close(fd) == 0, "TC-NETSOCK-320h 复位启动失败也放行 close(不卡调用方)"); + CHECK(g_fxchip.rst_begin_reject == 1 && g_ns_error_count >= 1, + "TC-NETSOCK-320i 复位没成必须 [ERR] 留痕,交 reconciler/pump 对账接管"); + reset_all(); +} + +/* TC-NETSOCK-321 drop_conn 用 QUARANTINE(异常释放语义),且不动监听簿记 */ +static void tc_321_drop_conn_settle(void) +{ + int fd; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].auto_relisten = 1U; + s_net_socks[fd].readable = 1U; + s_net_socks[fd].lost_conn_seen = 3U; + g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; + + CHECK(net_drop_conn(fd) == 0, "TC-NETSOCK-321a drop_conn 成功"); + CHECK(g_fxchip.rst_begin_calls == 1 && g_fx.rst_mode[fd] == 1U, + "TC-NETSOCK-321b 拆会话用 QUARANTINE 400ms 底(TRAP-NET-18 异常释放,与 close 分级)"); + CHECK(s_net_socks[fd].lost_conn_seen == 0U, + "TC-NETSOCK-321c 新会话起点:事件未收到去抖计数清零(N-30)"); + CHECK(s_net_socks[fd].local_port == 2404U && s_net_socks[fd].auto_relisten == 1U && + s_net_socks[fd].in_use == 1U, + "TC-NETSOCK-321d 端口/监听属性/fd 全部保留:这就是拆连接不拆监听"); + CHECK(g_peers.listener_req_calls == 1 && + g_peers.last_req_reason == (int)NET_LISTENER_REQ_DROP, + "TC-NETSOCK-321e 登记 REQ_DROP,重建动作在 net_listener_tick 一处"); + CHECK(s_net_socks[fd].readable == 0U && s_net_socks[fd].state == NET_SOCK_STATE_CLOSED, + "TC-NETSOCK-321f 收口会话簿记:清 readable + 转 CLOSED"); + reset_all(); +} + +/* TC-NETSOCK-322 close 前必先把挂起结算掉,再动芯片(顺序不可换) */ +static void tc_322_close_before_chip(void) +{ + int fd; + int close_at; + int settle_at; + char rxbuf[4]; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; + /* 先登记一条挂起 recv:走 netTask 直调版 recv(无数据 + 阻塞 → 登记挂起) */ + g_ns_pump = NULL; /* 本场景不靠泵,直接看登记结果 */ + CHECK(net_recv_sock(&s_net_socks[fd], rxbuf, (int)sizeof(rxbuf), 0) + == NET_PENDING_REGISTERED, + "TC-NETSOCK-322p 前置:阻塞 recv 登记了挂起(返回哨兵)"); + ch395f_fake_clear_log(); + settle_at = g_peers.pending_settle_calls; + close_at = g_chip.close_calls; + + CHECK(net_close_sock(&s_net_socks[fd]) == 0, "TC-NETSOCK-322a netTask 直调 close 成功"); + CHECK(g_peers.pending_settle_calls == settle_at + 1, + "TC-NETSOCK-322b close 必先结算本 fd 的挂起 recv"); + CHECK(g_chip.close_calls > close_at || g_fxchip.rst_close_calls > 0, + "TC-NETSOCK-322c 结算之后芯片侧才收到 CLOSE"); + CHECK(g_peers.pending_notified == 1, + "TC-NETSOCK-322d 结算必通知(取消也要通知,不是静默清槽 → 否则等待方永久悬挂)"); + reset_all(); +} + +static void ns_run_s4(void) +{ + tc_300_poll_gates(); + tc_301_poll_only_gated_sockets(); + tc_302_poll_event_dispatch(); + tc_303_phy_feed(); + tc_310_listener_armed(); + tc_311_can_read(); + tc_320_close_two_paths(); + tc_321_drop_conn_settle(); + tc_322_close_before_chip(); +} diff --git a/test/host/net_socket/ns_s5_io.c b/test/host/net_socket/ns_s5_io.c new file mode 100644 index 00000000..b103cecb --- /dev/null +++ b/test/host/net_socket/ns_s5_io.c @@ -0,0 +1,609 @@ +/* + * 分片 5:建流三件套(listen / accept / connect)的锁定版决策 + net_init 下发面 + * + UDP 数据报路径 + netTask 直调入口 + * + * 为什么值得单独一片:这三条是「真机极难按需复现」的决策分支——open 被芯片拒 + * (USE)、LISTEN 命令失败、TCP_CONNECT 被命令级拒绝、CONNECT 事件未收到的补 accept、 + * 对端「发完就 FIN」的 closed-with-readable,在台架上要凑齐对端行为+时序+总线状态 + * 才见得到一次;而每一条的错误形态都是「静默毁掉监听」或「数据被吞」(TRAP-NET-09/ + * -12/-18/-20 与 E14 复核实验的原文结论)。 + * + * 创建日期:2026-10-24 + */ + +/* 断线注入点:第二次轮询时把状态打回 CLOSED(模拟 net_poll 处理 DISCONNECT 之后的形态)。 + * 用假内核现成的「每轮 net_poll 回调」注入并发事件,而不是自己发明时间或事件 API。 */ +static int s_ns_poll_seen; +static void ns_drop_conn_on_second_poll(void) +{ + s_ns_poll_seen++; + if (s_ns_poll_seen == 2) { + s_net_socks[0].state = NET_SOCK_STATE_CLOSED; + s_net_socks[0].session_dead = 1U; + } +} + +/* TC-NETSOCK-400 net_init 的下发面与门控(一次调用钉十条契约) */ +static void tc_400_net_init_downlink(void) +{ + uint8_t mac_zero[6] = {0, 0, 0, 0, 0, 0}; + uint8_t mac_real[6] = {0x02, 0x11, 0x22, 0x33, 0x44, 0x55}; + int i; + + /* 1) 芯片不在位:直接失败,且不得继续下发任何参数 */ + reset_all(); + g_fx.exist_rc = (int)CH395F_STATUS_NOT_DETECTED; + CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == -1, + "TC-NETSOCK-400a 芯片不在位 → -1"); + CHECK(g_fxchip.reset_calls == 0 && g_fxchip.init_calls == 0, + "TC-NETSOCK-400b 不在位就不再复位/初始化(别把错误拖到协议栈里)"); + CHECK(g_ns_error_count == 1, "TC-NETSOCK-400c 芯片不在位必留 [ERR](否则只剩端口不通的现象)"); + + /* 2) 协议栈初始化失败 */ + reset_all(); + g_fx.init_rc = (int)CH395F_STATUS_ERROR; + CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == -1, + "TC-NETSOCK-400d ch395f_init 失败 → -1"); + CHECK(g_fxchip.ping_calls == 0, "TC-NETSOCK-400e 初始化失败后不得继续使能 PING(半配置状态)"); + + /* 3) 版本门控:低于 0x46 必须**拒置** bit3(SOCKET_CLOSE),退回芯片自动关断 */ + reset_all(); + g_fx.version = 0x45U; + CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == 0, + "TC-NETSOCK-400f 版本不足不是致命错:继续起来"); + CHECK(g_fx.fun_para == 0U && g_ns_error_count == 1, + "TC-NETSOCK-400g ver<0x46 → FUN_PARA 实发 0(bit3 拒置)+ [ERR] 留痕,不静默带病运行"); + + /* 4) 版本够新:FUN_PARA=0x08;bit1(TCP_SERVER 多连接) 与 bit0(保留) 都必须为 0 */ + reset_all(); + CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == 0, + "TC-NETSOCK-400h 正常初始化成功"); + /* bit3=SOCKET_CLOSE 要置;bit1=TCP_SERVER 多连接(本项目单连接模型)与 + * bit0=保留位都必须为 0——旧实现把 0x09 当「多 Socket 模式」下发,2026-10-11 + * 定案为 0x08(bit0 是保留位,证据链 TRAP-NET-15)。所以掩码要考 bit0|bit1, + * 拿 0x09 考会把我们正想要的 bit3 一起算进去(第一版就这么写错了)。 */ + CHECK(g_fx.fun_para == CH395F_FUN_PARA_SOCKET_CLOSE && + (g_fx.fun_para & 0x03UL) == 0UL, + "TC-NETSOCK-400i FUN_PARA 只置 bit3,bit0/bit1 为 0(0x09 是历史误值,TRAP-NET-15)"); + CHECK(g_fx.mss == (uint16_t)CH395F_TCP_MSS && g_fx.retrans_cnt == 8U && + g_fx.retrans_period == 200U, + "TC-NETSOCK-400j MSS/重传参数取 net_config.h 真值(8 次 / 200ms,P0-a)"); + CHECK(g_fx.ka_idle == 60000U && g_fx.ka_intvl == 5000U && g_fx.ka_cnt == 3U, + "TC-NETSOCK-400k KeepAlive 60s/5s/3(且必须在 ch395f_init 之前下发)"); + CHECK(g_fx.mss != 0U || g_fx.ka_idle != 0U ? 1 : 0, "TC-NETSOCK-400k2 参数确有下发"); + CHECK(g_chip.log_n == 0 || + ch395f_fake_log_find('n') > ch395f_fake_log_find('Q'), + "TC-NETSOCK-400l MSS/KeepAlive 等 SET 类命令排在 INIT_CH395 之前(手册硬约束)"); + + /* 5) IP/掩码/网关三样都按点分顺位下发,并缓存本机 IP 供 getsockname 用 */ + CHECK(g_fx.ip[0] == 192U && g_fx.ip[1] == 168U && g_fx.ip[2] == 1U && + g_fx.ip[3] == 100U && g_fx.gw[3] == 1U && g_fx.mask[0] == 255U, + "TC-NETSOCK-400m IP/网关/掩码按 parse_ip_string 顺位下发(段 0 在 [0])"); + CHECK(g_fxchip.set_dhcp_calls == 0 && g_fx.dhcp_enable == 0U, + "TC-NETSOCK-400n 指定了静态 IP 就绝不使能 DHCP"); + + /* 6) ip=NULL → 走 DHCP,不下发静态三样 */ + reset_all(); + (void)net_init(NULL, NULL, NULL, NULL); + CHECK(g_fxchip.set_dhcp_calls == 1 && g_fx.dhcp_enable == 1U, + "TC-NETSOCK-400o ip=NULL → 使能 DHCP"); + CHECK(g_fxchip.set_ip_calls == 0 && g_fxchip.set_gw_calls == 0 && + g_fxchip.set_mask_calls == 0, "TC-NETSOCK-400p DHCP 形态不下发静态地址"); + + /* 7) MAC:全零=用芯片内置,不下发;非零必须下发(写 EEPROM,须在 INIT 之前) */ + reset_all(); + (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", mac_zero); + CHECK(g_fxchip.set_mac_calls == 0, "TC-NETSOCK-400q MAC 全零 → 保持芯片内置,不下发"); + reset_all(); + (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", mac_real); + CHECK(g_fxchip.set_mac_calls == 1 && g_fx.mac[0] == 0x02U && g_fx.mac[5] == 0x55U, + "TC-NETSOCK-400r 非零 MAC 下发(设备在线身份与 UI 显示一致)"); + CHECK(ch395f_fake_log_find('M') < ch395f_fake_log_find('n') || + ch395f_fake_log_find('M') < 0, + "TC-NETSOCK-400s SET_MAC 排在 INIT_CH395 **之前**(TRAP-NET-19:顺序反了本次上电不生效)"); + + /* 8) 收发缓冲块布局:6 块/Socket,先配数据 Socket 再配监听 Socket 0(TRAP-NET-01) */ + reset_all(); + (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL); + CHECK(g_fxchip.set_recv_buf_calls == NET_MAX_SOCKETS && + g_fxchip.set_send_buf_calls == NET_MAX_SOCKETS, + "TC-NETSOCK-400t 8 个 Socket 全部显式重分配(上电默认只给 0~3)"); + for (i = 0; i < NET_MAX_SOCKETS; i++) { + if (g_fx.recv_blk_start[i] != (uint8_t)(i * 6) || + g_fx.recv_blk_cnt[i] != (uint8_t)CH395F_RECV_BLOCKS || + g_fx.send_blk_start[i] != (uint8_t)(i * 6 + (int)CH395F_RECV_BLOCKS) || + g_fx.send_blk_cnt[i] != (uint8_t)CH395F_SEND_BLOCKS) { + break; + } + } + CHECK(i == NET_MAX_SOCKETS, + "TC-NETSOCK-400u 块布局 recv=i*6 / send=recv+4,Socket7 恰好用尽 0~47(手册 §5.45)"); + CHECK(g_fx.ping_enable == 1U && g_fxchip.ping_calls == 1, "TC-NETSOCK-400v 使能 PING 响应"); + + /* 9) net_init 的第一件事是取消挂起并通知调用方(顺序错了 = 调用方永久挂死) */ + reset_all(); + (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL); + CHECK(g_peers.pending_cancel_all_calls == 1 && + g_peers.send_cancel_all_calls == 1, + "TC-NETSOCK-400w 重建前两张挂起表都要取消(recv/send 各一张,漏一张就有人永远等)"); + CHECK(g_peers.last_pend_cancel_err == NET_ERR_CONNRESET && + g_peers.last_snd_cancel_err == NET_ERR_CONNRESET, + "TC-NETSOCK-400x 取消原因 = CONNRESET"); + CHECK(g_peers.pending_capture_calls == 1, + "TC-NETSOCK-400y 登记执行任务(挂起登记的自等死锁守卫据此判「谁在执行」)"); + reset_all(); +} + +/* TC-NETSOCK-410 net_listen_locked:open 重试、清事件时序、失败对称关断 */ +static void tc_410_listen_locked(void) +{ + struct net_sockaddr_in in; + int fd; + + /* 非 TCP 流 → INVAL */ + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-410a UDP 不能 listen"); + + /* 状态不对(还没 bind 也没到 CREATED)→ INVAL */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-410b 已建立的 fd 再 listen → INVAL(不许把数据通道打回监听)"); + + /* 成功路径:BOUND → LISTENING,并启用 auto_relisten */ + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + (void)memset(&in, 0, sizeof(in)); + in.sin_port = net_htons(2404); + CHECK(net_bind(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, "TC-NETSOCK-410c bind 前置"); + ch395f_fake_clear_log(); + CHECK(net_listen_locked(fd) == 0, "TC-NETSOCK-410d listen 成功"); + CHECK(s_net_socks[fd].state == NET_SOCK_STATE_LISTENING && + s_net_socks[fd].auto_relisten == 1U && s_net_socks[fd].lost_conn_seen == 0U, + "TC-NETSOCK-410e 进 LISTENING + auto_relisten=1(BSD 监听常驻)+ 去抖计数清零"); + CHECK(g_chip.set_port_calls == 1 && g_chip.last_port == 2404U, + "TC-NETSOCK-410f 源端口取控制块的 local_port(端口不能漂)"); + CHECK(ch395f_fake_log_find('i') >= 0 && + ch395f_fake_log_find('i') < ch395f_fake_log_find('l'), + "TC-NETSOCK-410g 清本 Socket 事件排在 TCP_LISTEN **之前**(E14:反了会吞掉新 CONNECT)"); + CHECK(g_fxchip.glob_calls == 0, + "TC-NETSOCK-410h listen 全程不得读全局中断寄存器(B3 审计掉的正是这个)"); + + /* open 一直失败:重试 3 次,每次都走确定性排渣,最后 BUSY */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + g_fake.open_rc[fd] = CH395F_ERR_USE; + CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-410i open 三次仍失败 → BUSY"); + CHECK(g_chip.open_calls == 3 && g_fxchip.rst_begin_calls == 3, + "TC-NETSOCK-410j 三次 open + 三次 sock_reset(TRAP-NET-09 的重试语义保留,不是盲等 500ms)"); + CHECK(g_chip.listen_calls == 0, "TC-NETSOCK-410k open 没成绝不发 LISTEN 命令"); + + /* LISTEN 命令失败:open 已成的 Socket 必须关回去(U12 对称,否则卡 OPEN 无监听) */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + g_fake.listen_rc[fd] = CH395F_ERR_VAL; + CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-410l TCP_LISTEN 失败 → BUSY"); + CHECK(g_chip.close_calls == 1 && g_chip.open_calls == 1, + "TC-NETSOCK-410m LISTEN 失败必须 close 已开的 Socket(否则下次 open 恒返 USE)"); + CHECK(s_net_socks[fd].state != NET_SOCK_STATE_LISTENING, + "TC-NETSOCK-410n 失败不得谎报 LISTENING"); + reset_all(); +} + +/* TC-NETSOCK-411 公开 net_listen:越界 / 队列 / 结果恢复 */ +static void tc_411_listen_public(void) +{ + struct net_sockaddr_in in; + int fd; + + bring_up(); + CHECK(net_listen(-1) == -1 && net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-411a 越界 → BADF"); + CHECK(net_listen(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-411b 上界越界 → BADF"); + + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + (void)memset(&in, 0, sizeof(in)); + in.sin_port = net_htons(7003); + CHECK(net_bind(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, "TC-NETSOCK-411c bind"); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + CHECK(net_listen(fd) == 0, "TC-NETSOCK-411d 端到端 listen 成功(入队→分派→通知值 0)"); + CHECK(s_net_socks[fd].state == NET_SOCK_STATE_LISTENING, + "TC-NETSOCK-411e 公开入口确实驱动了芯片侧监听"); + CHECK(g_ns_notify_last_val == 0U, + "TC-NETSOCK-411f 成功的 listen 通知值=0(哨兵 0x70000000 不许漏进来)"); + + /* 已监听的 fd 再 listen:netTask 里 INVAL,经通知值回传 */ + CHECK(net_listen(fd) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-411g 重复 listen → -1 且 errno=INVAL(精确码,不是笼统 -1)"); + reset_all(); +} + +/* TC-NETSOCK-420 accept:单连接模型的四种形态 */ +static void tc_420_accept_shapes(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + int addrlen; + int fd; + + /* 1) 已有可接受连接:返回的就是监听 fd 自身(单连接),并回填对端 */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + CHECK(net_accept(fd, sa, &addrlen) == fd, "TC-NETSOCK-420a 单连接:accept 返回监听 fd 自身"); + CHECK(in.sin_family == NET_AF_INET && net_ntohs(in.sin_port) == 51999U && + addrlen == (int)sizeof(in), + "TC-NETSOCK-420b 回填对端地址与长度(lftpd 打印「谁连进来」就靠这个)"); + CHECK(g_peers.lost_probe_calls == 0, + "TC-NETSOCK-420c 非 LISTENING 不触发补 accept 探针(探针是监听态专用自救)"); + + /* 2) LISTENING 且探针命中 → 接受(CONNECT 事件未收到的自愈路径) */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + s_net_socks[fd].state = NET_SOCK_STATE_LISTENING; + s_net_socks[fd].local_port = 2404U; + s_net_socks[fd].remote_port = 55555U; + g_peers.lost_probe_hit = 1; + addrlen = 0; + CHECK(net_accept(fd, NULL, NULL) == fd, + "TC-NETSOCK-420d 探针把监听转 ESTABLISHED → accept 立即成功(N-30 主根因修复)"); + CHECK(g_peers.lost_probe_calls == 1, + "TC-NETSOCK-420e LISTENING 才调探针(每轮 accept 一次,带去抖计数)"); + + /* 3) LISTENING 且无连接 → WOULDBLOCK,且不动监听重建 */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + s_net_socks[fd].state = NET_SOCK_STATE_LISTENING; + addrlen = (int)sizeof(in); + CHECK(net_accept(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, + "TC-NETSOCK-420f 无连接 → WOULDBLOCK(不阻塞 netTask)"); + CHECK(g_peers.listener_req_calls == 0, + "TC-NETSOCK-420g 监听还活着时绝不登记重建(那是 U12 事故的形态)"); + + /* 4) CLOSED + auto_relisten + RX 有残留 → 接受"已关但可读",先排空再 EOF */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; + s_net_socks[fd].auto_relisten = 1U; + s_net_socks[fd].local_port = 21U; + s_net_socks[fd].remote_port = 44444U; + ch395f_fake_push_recv_len((uint8_t)fd, 1027U); + addrlen = (int)sizeof(in); + CHECK(net_accept(fd, sa, &addrlen) == fd, + "TC-NETSOCK-420h closed-with-readable:接受这条连接去排空(N-30 定案,反了就是 425)"); + CHECK(g_peers.listener_req_calls == 0, + "TC-NETSOCK-420i 有残留数据时**不**登记重建(先 relisten 会把已到达的数据销毁)"); + CHECK(g_chip.get_status_calls == 0, + "TC-NETSOCK-420j 排空判据只读 GET_RECV_LEN,不发状态查询"); + + /* 5) CLOSED + auto_relisten + RX 空 → 登记 REQ_ACCEPT 并 WOULDBLOCK */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; + s_net_socks[fd].auto_relisten = 1U; + s_net_socks[fd].local_port = 21U; + CHECK(net_accept(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, + "TC-NETSOCK-420k CLOSED 且无残留 → WOULDBLOCK"); + CHECK(g_peers.listener_req_calls == 1 && + g_peers.last_req_reason == (int)NET_LISTENER_REQ_ACCEPT && + g_peers.last_req_fd == fd, + "TC-NETSOCK-420l 消费者撞空才登记即时重建(N-32 P2:删掉它会拉长 CLOSED 窗)"); + CHECK(g_chip.open_calls == 0 && g_chip.listen_calls == 0, + "TC-NETSOCK-420m 登记路径零芯片事务(重建动作只在 net_listener_tick 一处)"); + + /* 6) 非监听型 CLOSED(客户端)→ 不擅自登记 */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; + s_net_socks[fd].auto_relisten = 0U; + CHECK(net_accept(fd, sa, &addrlen) == -1 && g_peers.listener_req_calls == 0, + "TC-NETSOCK-420n 客户端 fd 的 CLOSED 不登记监听重建"); + reset_all(); +} + +/* TC-NETSOCK-421 accept 的公开入口边界 */ +static void tc_421_accept_public(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + int addrlen; + int fd; + + bring_up(); + CHECK(net_accept(-1, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-421a 越界 → BADF"); + fd = mk_established(NET_SOCK_STREAM); + addrlen = 0; + CHECK(net_accept(fd, sa, NULL) == fd, + "TC-NETSOCK-421b addrlen 为 NULL 仍可 accept(addr 回填被跳过,不崩)"); + addrlen = (int)sizeof(in); + CHECK(net_accept(fd, NULL, &addrlen) == fd, + "TC-NETSOCK-421c addr 为 NULL:只要 fd,不回填地址"); + reset_all(); +} + +/* TC-NETSOCK-430 connect:地址簿记、open 重试、命令被拒回滚 */ +static void tc_430_connect(void) +{ + struct net_sockaddr_in in; + int fd; + + bring_up(); + CHECK(net_connect(-1, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-430a 越界 → BADF"); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + CHECK(net_connect(fd, NULL, (int)sizeof(in)) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-430b addr=NULL → INVAL(入队之前就把参数挡下)"); + CHECK(net_connect(fd, (struct net_sockaddr *)&in, 1) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430c addrlen 不足 → INVAL"); + CHECK(ns_rtos_q_count() == 0, "TC-NETSOCK-430d 参数非法不产生消息(不占 netTask)"); + + /* 成功:地址簿记 + 大端顺位 + 状态 CONNECTING */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(8888); + in.sin_addr.s_addr = net_inet_addr("192.168.1.200"); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, + "TC-NETSOCK-430e connect 命令下发成功(异步,成败由中断裁定)"); + CHECK(s_net_socks[fd].state == NET_SOCK_STATE_CONNECTING, + "TC-NETSOCK-430f 本地状态转 CONNECTING"); + CHECK(s_net_socks[fd].remote_port == 8888U, + "TC-NETSOCK-430g 对端端口转主机序存簿记"); + CHECK(g_fx.des_ip[fd][0] == 192U && g_fx.des_ip[fd][1] == 168U && + g_fx.des_ip[fd][2] == 1U && g_fx.des_ip[fd][3] == 200U, + "TC-NETSOCK-430h 目的 IP 按大端数组下发(set_des_ip 要的顺位,反了就连不上)"); + CHECK(g_chip.set_port_calls == 1 && g_fx.des_port[fd] == 8888U, + "TC-NETSOCK-430i 源端口下发一次(SET_SOUR_PORT),目的端口另走 SET_DES_PORT=8888"); + CHECK(g_fxchip.sock_events_calls == 1 && g_fxchip.glob_calls == 0, + "TC-NETSOCK-430j 清残留中断只清本 Socket(F-NET-7:原「再读清全局」会焊死别人)"); + CHECK(ch395f_fake_log_find('v') < ch395f_fake_log_find('T') || + ch395f_fake_log_find('v') < 0, + "TC-NETSOCK-430k 清事件排在 TCP_CONNECT 之前(TRAP-NET-12/-18 的两级预清)"); + + /* 命令级被拒(非 BUSY):必须回滚,不能停在 CONNECTING 干等 */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + g_fx.connect_rc[fd] = CH395F_ERR_VAL; + CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430l 芯片拒命令 → -1"); + CHECK(g_chip.close_calls == 1, + "TC-NETSOCK-430m 被拒要按手册「再次连接需从 OPEN 重开」关掉 Socket"); + CHECK(s_net_socks[fd].session_dead == 1U && + s_net_socks[fd].state == NET_SOCK_STATE_CLOSED, + "TC-NETSOCK-430n 命令级失败同样是本会话结束:置 session_dead+CLOSED(B2:否则挂起 recv 干等)"); + CHECK(g_ns_error_count >= 1, "TC-NETSOCK-430o 被拒必留 [ERR]"); + + /* BUSY 只是命令仍在执行,不得判失败 */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + g_fx.connect_rc[fd] = CH395F_ERR_BUSY; + CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, + "TC-NETSOCK-430p BUSY=芯片还在执行 → 维持原行为交给中断裁定(慢芯片不该被误杀)"); + CHECK(s_net_socks[fd].state == NET_SOCK_STATE_CONNECTING && g_chip.close_calls == 0, + "TC-NETSOCK-430q BUSY 不回滚、不关 Socket"); + + /* open 三次失败 → BUSY,且不发 CONNECT */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + g_fake.open_rc[fd] = CH395F_ERR_USE; + CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && + net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-430r open 三次失败 → BUSY"); + CHECK(g_chip.open_calls == 3 && g_fxchip.connect_calls == 0, + "TC-NETSOCK-430s open 没成不发 TCP_CONNECT"); + + /* 非 TCP 流 / 未在用 → INVAL(netTask 侧再判一次) */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + CHECK(net_connect_locked(fd, (const struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430t UDP 槽位不走 TCP connect"); + CHECK(net_connect_locked(7, (const struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && + net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-430u 范围内但未在用 → INVAL(in_use 与控制块一起考;BADF 只考越界)"); + CHECK(net_connect_locked(NET_MAX_SOCKETS, (const struct net_sockaddr *)&in, + (int)sizeof(in)) == -1, "TC-NETSOCK-430v 越界 → BADF"); + reset_all(); +} + +/* TC-NETSOCK-440 UDP 数据报:sendto 分块 / recvfrom 剥 8 字节报文头 */ +static void tc_440_udp_datagram(void) +{ + static char big[2100]; + char buf[64]; + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + int addrlen; + int fd; + + bring_up(); + CHECK(net_sendto(-1, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-440a sendto 越界 → BADF"); + CHECK(net_sendto(0, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-440b 未在用 → BADF"); + CHECK(net_recvfrom(0, buf, sizeof(buf), 0, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-440c recvfrom 未在用 → BADF"); + + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + CHECK(net_sendto(fd, NULL, 5, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-440d sendto 参数非法 → INVAL"); + CHECK(net_recvfrom(fd, NULL, 5, NET_MSG_DONTWAIT, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-440e recvfrom buf 为空 → INVAL"); + + /* 带目的地址:先设 DesIP/DesPort 再写数据(不带上限的自旋实现,健康链路一次写完) */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + (void)memset(&in, 0, sizeof(in)); + in.sin_port = net_htons(8888); + in.sin_addr.s_addr = net_inet_addr("192.168.1.9"); + CHECK(net_sendto(fd, "hello", 5, 0, sa, (int)sizeof(in)) == 5, + "TC-NETSOCK-440f sendto 成功返回字节数"); + CHECK(g_fx.des_ip[fd][3] == 9U && g_fx.des_port[fd] == 8888U, + "TC-NETSOCK-440g 目的地址随本次发送下发(UDP 无连接,地址是每包带的)"); + CHECK(g_chip.write_calls == 1 && s_net_socks[fd].send_bytes == 5U, + "TC-NETSOCK-440h 小包一次写完 + 统计"); + + /* 大报分块(仍受 NET_SEND_CHUNK_MAX 上界约束) */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + g_ns_int_pin_set = 0; + ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); + ch395f_fake_set_sint_sticky((uint8_t)fd, CH395F_SINT_STAT_SENBUF_FREE); + CHECK(net_sendto(fd, big, 2100, 0, NULL, 0) == 2100, "TC-NETSOCK-440i 2100B 全部交付"); + CHECK(g_chip.write_calls == 3, "TC-NETSOCK-440j UDP 也按 1024 上界分块(写穿 FIFO 的约束同 TCP)"); + + /* recvfrom:前 8 字节是芯片 UDP 头(长度/srcPort/srcIP),要剥掉再交出去 */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + ch395f_fake_push_recv_len((uint8_t)fd, 8U + 20U); + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen) == 20, + "TC-NETSOCK-440k 返回 20:报文头 8 字节被剥掉,不把头部当数据交给应用"); + CHECK(addrlen == (int)sizeof(in) && in.sin_family == NET_AF_INET, + "TC-NETSOCK-440l 回填源地址结构与长度"); + + /* 无数据 + 非阻塞 → WOULDBLOCK(阻塞形态是自旋无预算,本套刻意不测,见交付说明) */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440m 非阻塞无数据 → WOULDBLOCK"); + s_net_socks[fd].blocking = 0U; + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440n 非阻塞属性同判据生效"); + /* 短于 8 字节的残包:不剥头,整包交出去(钉当前实现的边界,不猜"应该怎么算") */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + ch395f_fake_push_recv_len((uint8_t)fd, 5U); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == 5, + "TC-NETSOCK-440o recv_len<8 时不剥头,整包返回(当前实现)"); + reset_all(); +} + +/* TC-NETSOCK-450 netTask 直调入口的 NULL 守卫与委托 */ +static void tc_450_direct_entries(void) +{ + bring_up(); + CHECK(net_send_sock(NULL, "x", 1) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-450a net_send_sock(NULL) → BADF"); + CHECK(net_recv_sock(NULL, (void *)"x", 1, 0) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-450b net_recv_sock(NULL) → BADF"); + CHECK(net_close_sock(NULL) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-450c net_close_sock(NULL) → BADF"); + + /* netTask 内直调 send:挂起登记会被"执行者自等待"守卫拒 → 返回 -1(BUSY) */ + reset_all(); + bring_up(); + { + int fd = mk_established(NET_SOCK_STREAM); + + s_net_socks[fd].send_ready = 0U; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; + g_peers.send_register_rc = -1; /* 复刻守卫:netTask 自己发的必被拒 */ + CHECK(net_send_sock(&s_net_socks[fd], "hello", 5) == -1, + "TC-NETSOCK-450d netTask 内等不到缓冲时不登记挂起(没人能替它完成)→ -1"); + CHECK(g_peers.send_reg_calls == 1, + "TC-NETSOCK-450e 确实走到了登记调用(被拒的是登记侧,不是本层没试)"); + } + reset_all(); +} + +/* TC-NETSOCK-460 复位收口超时:芯片永远关不掉时上层必须有界退出(不得无限占用 netTask) */ +static void tc_460_reset_sync_timeout_bound(void) +{ + uint32_t t_before; + int fd; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; + g_fx.rst_keep_open[fd] = 1U; /* 复位机怎么努力芯片都不落回 CLOSED */ + t_before = g_ns_tick; + + CHECK(net_close(fd) == 0, + "TC-NETSOCK-460a 芯片关不掉也照样放行 close(fd 侧簿记必须继续收口)"); + CHECK((g_ns_tick - t_before) <= 700U, + "TC-NETSOCK-460b 复位等待有 600ms 硬上限(无上限=netTask 被一颗坏芯片拖死)"); + CHECK(g_ns_error_count >= 1, + "TC-NETSOCK-460c 复位超时必 [ERR]:这条 [ERR] 是唯一能发现它的信号"); + CHECK(s_net_socks[fd].in_use == 0U, + "TC-NETSOCK-460d 超时也照常释放槽位(不因芯片状态卡住本地 fd 表)"); + reset_all(); + + /* drop_conn 同一条超时线 */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].auto_relisten = 1U; + g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; + g_fx.rst_keep_open[fd] = 1U; + t_before = g_ns_tick; + CHECK(net_drop_conn(fd) == 0 && (g_ns_tick - t_before) <= 700U && + g_ns_error_count >= 1, + "TC-NETSOCK-460e drop_conn 也走同一条有界收口并留痕"); + reset_all(); +} + +/* TC-NETSOCK-470 发送途中的并发事件:断线要按短写交出已发量,不能假装全成功 */ +static void tc_470_send_disconnect_midway(void) +{ + static char big[2100]; + int fd; + int rc; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + /* 第一块写得进去,第二块等待期间对端断线(net_poll 处理 DISCONNECT 把状态打回 CLOSED) */ + g_ns_int_pin_set = 0; + ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); + g_fake.int_status[fd] = CH395F_SINT_STAT_DISCONNECT; + g_ns_on_poll = ns_drop_conn_on_second_poll; + s_ns_poll_seen = 0; + + rc = net_send(fd, big, 2100, 0); + + CHECK(rc >= 0 && rc < 2100, + "TC-NETSOCK-470a 断线只截断剩余量:按已发数返回(短写),不是 -1 也不是谎报 2100"); + CHECK((int)s_net_socks[fd].send_bytes == rc, + "TC-NETSOCK-470b 字节统计与实际交付一致(对端已收到的不能回收)"); + CHECK(g_ns_error_count >= 1, + "TC-NETSOCK-470c 短写必 [ERR](静默截断曾让 UI 推流被对端误判为全程无字节)"); + reset_all(); +} + +static void ns_run_s5(void) +{ + tc_400_net_init_downlink(); + tc_410_listen_locked(); + tc_411_listen_public(); + tc_420_accept_shapes(); + tc_421_accept_public(); + tc_430_connect(); + tc_440_udp_datagram(); + tc_450_direct_entries(); + tc_460_reset_sync_timeout_bound(); + tc_470_send_disconnect_midway(); +} diff --git a/test/host/net_socket/ns_s6_edges.c b/test/host/net_socket/ns_s6_edges.c new file mode 100644 index 00000000..9afe7053 --- /dev/null +++ b/test/host/net_socket/ns_s6_edges.c @@ -0,0 +1,395 @@ +/* + * 分片 6:边界残支(公开入口的越界/队满、直调入口的参数支、等待环里的其余出口) + * + * 这一片不为「覆盖率好看」而写:每条都对应一条**能被改坏**的分支——越界检查挪到 + * 入队之后,调用方就会在 netTask 里等到一个没人处理的 BADF;短写留痕被删掉, + * 上层就会像 2026-09-21 那次 UI 推流一样「对端永久等待剩余字节」而日志全白。 + * 唯一一条刻意**不是**真实时序的场景是 TC-NETSOCK-530(一次轮询之间被压了 1s), + * 它在真机上的对应形态是 netTask 被更高优先级任务压住——用大步进时基表达, + * 并在注释里写明它考的是「停滞超时先于自旋预算生效」这条判定顺序。 + * + * 创建日期:2026-10-24 + */ + +/* TC-NETSOCK-500 六个公开入口的越界形态:BADF 必须在**入队之前**由调用方侧挡下 */ +static void tc_500_public_bounds(void) +{ + struct net_sockaddr_in in; + char buf[8]; + int addrlen; + + bring_up(); + (void)memset(&in, 0, sizeof(in)); + addrlen = (int)sizeof(in); + + CHECK(net_send(NET_MAX_SOCKETS, buf, 4, 0) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-500a net_send 越界 → BADF"); + CHECK(net_recv(NET_MAX_SOCKETS, buf, 4, NET_MSG_DONTWAIT) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500b net_recv 越界 → BADF"); + CHECK(net_recv_to(NET_MAX_SOCKETS, buf, 4, 10U) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500c net_recv_to 越界 → BADF"); + CHECK(net_close(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-500d net_close 越界 → BADF"); + CHECK(net_drop_conn(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-500e net_drop_conn 越界 → BADF"); + CHECK(net_accept(NET_MAX_SOCKETS, (struct net_sockaddr *)&in, &addrlen) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500f net_accept 越界 → BADF"); + CHECK(net_connect(NET_MAX_SOCKETS, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500g net_connect 越界 → BADF"); + CHECK(net_sendto(NET_MAX_SOCKETS, buf, 4, 0, NULL, 0) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500h net_sendto 越界 → BADF"); + CHECK(net_recvfrom(NET_MAX_SOCKETS, buf, 4, NET_MSG_DONTWAIT, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500i net_recvfrom 越界 → BADF"); + CHECK(g_ns_put_calls == 0, + "TC-NETSOCK-500j 全部越界形态零入队(入队之后才挡 = 让 netTask 处理一个没主的请求)"); + reset_all(); +} + +/* TC-NETSOCK-501 队满时每个公开入口都要报 BUSY(不能有的报、有的静默) */ +static void tc_501_all_entries_busy_on_full_queue(void) +{ + struct net_sockaddr_in in; + int fd; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + + ns_rtos_q_fill((int)NS_Q_CAP); + CHECK(net_accept(fd, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-501a net_accept 队满 → BUSY"); + ns_rtos_q_fill((int)NS_Q_CAP); + (void)memset(&in, 0, sizeof(in)); + CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && + net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501b net_connect 队满 → BUSY"); + ns_rtos_q_fill((int)NS_Q_CAP); + CHECK(net_recv_to(fd, (void *)"x", 1, 20U) == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-501c net_recv_to 队满 → BUSY"); + ns_rtos_q_fill((int)NS_Q_CAP); + CHECK(net_recv(fd, (void *)"x", 1, NET_MSG_DONTWAIT) == -1 && + net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501d net_recv 队满 → BUSY"); + ns_rtos_q_fill((int)NS_Q_CAP); + CHECK(net_send(fd, "x", 1, 0) == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-501e net_send 队满 → BUSY"); + CHECK(g_ns_notify_calls == 0, "TC-NETSOCK-501f 队满路径零通知(调用方直接拿返回值走人)"); + reset_all(); +} + +/* TC-NETSOCK-502 fd 表用尽:NOMEM 且不影响已建立的会话 */ +static void tc_502_socket_table_full(void) +{ + int i; + int fd; + + bring_up(); + for (i = 0; i < NET_MAX_SOCKETS; i++) { + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + if (fd < 0) { + break; + } + } + CHECK(i == NET_MAX_SOCKETS, "TC-NETSOCK-502a 8 个槽位全部可分配"); + CHECK(net_socket(NET_AF_INET, NET_SOCK_STREAM) == -1 && + net_get_errno() == NET_ERR_NOMEM, "TC-NETSOCK-502b 第 9 个 → NOMEM(不越界写表)"); + s_net_socks[3].state = NET_SOCK_STATE_ESTABLISHED; + CHECK(net_get_sock(3) != NULL && net_listener_armed(3) == 1, + "TC-NETSOCK-502c 表满不影响已在用会话(alloc 失败只回 NOMEM,不动别人的槽位)"); + reset_all(); +} + +/* TC-NETSOCK-503 net_wait_result 的两段式拾取:先零等待,已有通知就不必再睡 */ +static void tc_503_zero_wait_pickup(void) +{ + int fd; + int rc; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + /* 预置一条**已挂起**的通知(真机上=上一条消息的回执还没被读走), + * 公开入口应先零等待拾取它,而不是再睡一次(睡过头就是 实测踩过的坑) */ + ns_rtos_post_notify((TaskHandle_t)&g_ns_task_caller, (uint32_t)(int)4); + g_ns_block_waits = 0; + + rc = net_send(fd, "abcd", 4, 0); + + CHECK(rc == 4, "TC-NETSOCK-503a 零等待就拾到通知值 → 直接返回结果"); + CHECK(g_ns_block_waits == 0, "TC-NETSOCK-503b 已有通知时绝不进阻塞分支"); + CHECK(g_peers.send_reg_calls == 0 && g_chip.write_calls == 0, + "TC-NETSOCK-503c 本次调用没有真的写芯片(拾到的是上一条消息的回执——这就是 实测踩过的形态)"); + reset_all(); +} + +/* TC-NETSOCK-504 直调入口的参数支(这些分支从公开入口不可达,只能直接钉) */ +static void tc_504_locked_entry_branches(void) +{ + struct net_sockaddr_in in; + int fd; + + bring_up(); + (void)memset(&in, 0, sizeof(in)); + in.sin_port = net_htons(80); + in.sin_addr.s_addr = net_inet_addr("10.1.1.1"); + + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + CHECK(net_connect_locked(fd, NULL, (int)sizeof(in)) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504a connect_locked addr=NULL → INVAL"); + CHECK(net_connect_locked(fd, (const struct net_sockaddr *)&in, 1) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504b connect_locked addrlen 不足 → INVAL"); + s_net_socks[fd].in_use = 1U; + CHECK(net_send_locked(&s_net_socks[fd], "x", 1, NULL) == -1 && + net_get_errno() == NET_ERR_NOTCONN, "TC-NETSOCK-504c 未建立的直调 send → NOTCONN"); + CHECK(net_recv_locked(&s_net_socks[fd], (void *)"x", 1, NET_MSG_DONTWAIT, 0U, NULL) == -1 && + net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-504d 直调 recv 无数据非阻塞 → WOULDBLOCK"); + CHECK(net_recv_locked(&s_net_socks[fd], NULL, 1, NET_MSG_DONTWAIT, 0U, NULL) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504e 直调 recv buf 空 → INVAL"); + reset_all(); +} + +/* TC-NETSOCK-505 发送途中缓冲再也不腾出来:内层等待也要转挂起,并且末块不许登记 */ +static void tc_505_inner_wait_and_last_chunk(void) +{ + static char big[2100]; + int fd; + int rc; + + /* 第一块写得进、之后芯片不再报空闲 → 第二块的**内层**等待转挂起 */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_ns_int_pin_set = 0; + ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); + g_fake.int_status[fd] = CH395F_SINT_STAT_SENBUF_FREE; /* 只给一次空闲 */ + rc = net_send_locked(&s_net_socks[fd], big, 2100, + (TaskHandle_t)&g_ns_task_caller); + CHECK(rc == NET_PENDING_REGISTERED, + "TC-NETSOCK-505a 内层等待(写完一块后等发完)超预算也转挂起,不留 in-place 忙等"); + CHECK(g_chip.write_calls == 2 && g_peers.last_snd_sent == 2048, + "TC-NETSOCK-505b 登记的进度=已写进芯片的字节数(1024+1024=2048),已发的绝不重发"); + reset_all(); + + /* 末块:全部写完但芯片不回执 → 按完成返回,**不许**登记挂起 + * (2026-10-14 台架回归 TC-NET-902 正是登记了却无人完成 → 永久挂死) */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + rc = net_send_locked(&s_net_socks[fd], big, 1024, + (TaskHandle_t)&g_ns_task_caller); + CHECK(rc == 1024, "TC-NETSOCK-505c 单块恰好写完 → 返回 1024(完成)"); + CHECK(g_peers.send_reg_calls == 0, + "TC-NETSOCK-505d 末块等待不得登记挂起(sent>=len 的守卫必须挡住)"); + reset_all(); +} + +/* TC-NETSOCK-506 UDP sendto:等待腾出与短写留痕(与 TCP 同一套语义的另一份实现) */ +static void tc_506_sendto_stall_and_short(void) +{ + static char big[2100]; + int fd; + int rc; + + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + s_net_socks[fd].send_ready = 0U; + g_ns_int_pin_set = 0; + ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); + /* 每轮只走 1ms:先命中 NET_SEND_TIMEOUT_MS(1s) 之前不会有别出口 → 短写 */ + g_ns_poll_tick_inc = 500U; + rc = net_sendto(fd, big, 2100, 0, NULL, 0); + CHECK(rc < 2100 && rc >= 0, + "TC-NETSOCK-506a sendto 停滞只拿到部分:按已发数返回(短写语义与 TCP 一致)"); + CHECK(g_ns_error_count >= 2, + "TC-NETSOCK-506b 停滞 [ERR] + 短写 [ERR] 都要留(缺一就退化成静默截断)"); + reset_all(); + + /* 写完一块后等发完的超时出口 */ + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + g_ns_int_pin_set = 0; + ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); + g_fake.int_status[fd] = CH395F_SINT_STAT_SENBUF_FREE; /* 只给一次 */ + g_ns_poll_tick_inc = 500U; + rc = net_sendto(fd, big, 2100, 0, NULL, 0); + CHECK(rc >= 1024 && rc < 2100, + "TC-NETSOCK-506c 第一块已交付、第二块等发完超时 → 返回 1024..2099 之间的实际值"); + reset_all(); +} + +/* TC-NETSOCK-530 停滞超时优先于自旋预算:考判定顺序,不考真实时长 */ +static void tc_530_stall_timeout_before_spin(void) +{ + static char buf[64]; + int fd; + int rc; + + /* 一次轮询之间被压了 2s(真机形态:netTask 被更高优先级任务压住), + * 停滞超时(NET_SEND_TIMEOUT_MS=1s)必须**先于**自旋预算(2ms)命中: + * 生产代码里两条判定的先后顺序就是这条断言的内容。 */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].send_ready = 0U; + g_ns_poll_tick_inc = 2000U; + + rc = net_send_locked(&s_net_socks[fd], buf, (int)sizeof(buf), + (TaskHandle_t)&g_ns_task_caller); + CHECK(rc == 0, + "TC-NETSOCK-530a 停滞超时按已发数 0 返回(短写形态,不是 -1)"); + CHECK(g_peers.send_reg_calls == 0, + "TC-NETSOCK-530b 停滞超时优先:不进挂起登记(判定顺序被换掉的话这里会登记)"); + CHECK(net_get_errno() == NET_ERR_TIMEDOUT, + "TC-NETSOCK-530c errno = TIMEDOUT(供调用方区分停滞与断线)"); + reset_all(); +} + +/* TC-NETSOCK-535 已经写出去一块之后的超时收口(内层等待的另一条出口) */ +static void tc_535_inner_timeout_after_chunk(void) +{ + static char big[2100]; + int fd; + int rc; + + /* 第一块写得进(send_ready 初值 1),第二块的**内层**等待直接撞上停滞超时 + * (判定顺序:先超时后自旋,所以这里收口在超时支而不是登记挂起) */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + g_ns_poll_tick_inc = 2000U; + + rc = net_send_locked(&s_net_socks[fd], big, 2100, + (TaskHandle_t)&g_ns_task_caller); + + CHECK(rc == 1024, "TC-NETSOCK-535a 已交付的第一块照样交出(1024),剩余按短写处理"); + CHECK(g_peers.send_reg_calls == 0, + "TC-NETSOCK-535b 停滞超时收口在登记挂起**之前**(换序就会登记一条永远完不成的挂起)"); + CHECK(g_chip.write_calls == 1, "TC-NETSOCK-535c 只写了一次芯片:不得把已交付的块重发"); + reset_all(); +} + +/* TC-NETSOCK-536 net_recv_to 的三条回传分支(-2 / -1+errno / >=0) */ +static void tc_536_recv_to_three_exits(void) +{ + char buf[16]; + int fd; + + /* >=0:正常拿到数据 */ + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + s_net_socks[fd].readable = 1U; + ch395f_fake_push_recv_len((uint8_t)fd, 12U); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + CHECK(net_recv_to(fd, buf, (int)sizeof(buf), 100U) == 12, + "TC-NETSOCK-536a 有数据 → 返回实际字节数(走 >=0 支)"); + + /* <0 且非 TIMEDOUT:分派侧 BADF → 恢复 errno 后返回 -1 */ + reset_all(); + bring_up(); + (void)net_socket(NET_AF_INET, NET_SOCK_STREAM); /* 占掉 fd0,用未分配的 fd1 */ + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + CHECK(net_recv_to(1, buf, (int)sizeof(buf), 100U) == -1 && + net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-536b 真错误 → -1 且 errno 从通知值恢复(与 -2 严格区分)"); + + /* ==TIMEDOUT:已在 TC-NETSOCK-224 钉过 -2;这里补一条「-2 不改写 errno」 */ + reset_all(); + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + (void)net_set_errno(NET_OK); + ns_peers_arm_pending_done(fd, NET_ERR_TIMEDOUT); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + CHECK(net_recv_to(fd, buf, (int)sizeof(buf), 30U) == -2 && + net_get_errno() == NET_OK, + "TC-NETSOCK-536c 超时只凭返回码判定,**不改写**错误码槽(本分支刻意不设 errno)"); + reset_all(); +} + +/* TC-NETSOCK-537 fire_event:注册了回调才会被叫到(本层唯一的回调触发点在 SEND_OK) */ +static void tc_537_event_callback_path(void) +{ + int fd; + + bring_up(); + fd = mk_established(NET_SOCK_STREAM); + /* handle_send_event 是兄弟模块(net_events)的职责,本层只负责把中断交给它; + * 回调是否触发属 net_events 的断言面(其单测 TC-NETEV 已覆盖),此处只钉 + * 「net_poll 不因回调未注册而跳过事件」。 */ + s_net_socks[fd].event_cb = NULL; + g_ns_int_pin_set = 0; + ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); + g_fake.int_status[fd] = CH395F_SINT_STAT_SEND_OK; + CHECK(net_poll() == 1, "TC-NETSOCK-537 未注册回调也照常计事件(不因空指针跳过分发)"); + reset_all(); +} + +/* TC-NETSOCK-540 close / drop_conn 的失败结果也要经通知值恢复精确 errno */ +static void tc_540_negative_results(void) +{ + net_msg_t msg; + int fd; + + /* close 的负结果支:net_close_locked 自身恒返 0(复位没成也放行,见 TC-NETSOCK-320h), + * 所以 CLOSE 唯一的负值来自**分派侧**的 net_get_sock 判空 → 通知值 -2 → + * 调用方在自己的槽位里恢复成 BADF。范围合法但未分配的 fd 正好走这条。 */ + bring_up(); + fd = 3; /* 范围内、未分配 */ + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + CHECK(net_close(fd) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-540a close 未分配的 fd → -1 且 errno 从通知值恢复成 BADF(不是笼统 NET_ERR)"); + CHECK(net_drop_conn(fd) == -1 && net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-540a2 drop_conn 同一条负值恢复支"); + reset_all(); + + /* drop_conn 同理:用未分配 fd 走分派侧 BADF 后再由调用方恢复 */ + bring_up(); + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_DROP_CONN; + msg.sockfd = 1; + msg.caller = (TaskHandle_t)&g_ns_task_net; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, + "TC-NETSOCK-540b drop_conn 分派侧 BADF 经通知值回传"); + + /* RECV 的 BADF 支(分派侧 net_get_sock 判空) */ + reset_all(); + bring_up(); + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_RECV; + msg.sockfd = 4; + msg.buf = (void *)"x"; + msg.len = 1; + msg.caller = (TaskHandle_t)&g_ns_task_net; + (void)ns_rtos_q_put_raw(&msg); + net_process_messages(); + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, + "TC-NETSOCK-540c recv 分派侧 BADF 同样经通知值回传"); + reset_all(); +} + +/* TC-NETSOCK-545 未绑队列时的 sendto/recvfrom/getpeername 等纯本地入口 */ +static void tc_545_local_entries_without_queue(void) +{ + struct net_sockaddr_in in; + int addrlen = (int)sizeof(in); + + reset_all(); + CHECK(net_getsockname(0, (struct net_sockaddr *)&in, &addrlen) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-545a 未初始化也能安全报 BADF(不崩)"); + CHECK(net_getpeername(0, (struct net_sockaddr *)&in, &addrlen) == -1, + "TC-NETSOCK-545b getpeername 同上"); + CHECK(net_inet_addr("1.2.3.4") != 0U, "TC-NETSOCK-545c 纯函数不依赖初始化"); + reset_all(); +} + +static void ns_run_s6(void) +{ + tc_500_public_bounds(); + tc_501_all_entries_busy_on_full_queue(); + tc_502_socket_table_full(); + tc_503_zero_wait_pickup(); + tc_504_locked_entry_branches(); + tc_505_inner_wait_and_last_chunk(); + tc_506_sendto_stall_and_short(); + tc_530_stall_timeout_before_spin(); + tc_535_inner_timeout_after_chunk(); + tc_536_recv_to_three_exits(); + tc_537_event_callback_path(); + tc_540_negative_results(); + tc_545_local_entries_without_queue(); +} diff --git a/test/host/net_socket/run_unit_test.py b/test/host/net_socket/run_unit_test.py new file mode 100644 index 00000000..357dfa6f --- /dev/null +++ b/test/host/net_socket/run_unit_test.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""NET 套接字核心(net_socket)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/net_socket/net_socket_test.c(以 #include 纳入生产源 + Drivers/BSP/NET/net_socket.c + 共享假芯片 test/host/common/ch395f_fake.c + + 本目录的假内核 ns_rtos.c 与兄弟模块桩 ns_peers.c),在 PC 上离线验证: + 跨上下文消息全分派、结果经任务通知值回传、每任务错误码槽(E-03)、 + 监听门控与 closed-with-readable、net_poll 的中断分派与"只读自己 SINT", + 以及读写路径的短写 / 挂起登记 / EOF / 命令被拒回滚。 + 工具链与判定委托 test/host/common/unit_test_harness.py。 + +先跑常量闸(本 runner 独有的第二道检查,见 check_fake_chip_constants 注释), +不干净直接不编译——编译错误会把"假件值漂移"这件事埋得很深。 + +为什么要主机侧单测(而不是只用台架): + net_socket.c 是本产品**唯一的 SPI 串行化点**:所有 net_* 公开 API 把请求按值拷进 + 消息队列,由 netTask 串行执行,结果只能经 xTaskNotify 的 32 位通知值回来 + (net_msg_t 是按值入队的,netTask 改自己那份副本,调用方永远看不到——AGENTS + 模块约定明写"勿给 net_msg_t 加回传字段")。这条链的正确性取决于两个任务的 + 交错时序,真机上"通知丢了"表现为永久挂死并被看门狗复位,日志里只剩一条 + [ERR];而错误码本身还按任务隔离(E-03),跨任务覆写只会让排障读到别人的码。 + 把队列/通知/时基做成显式量后,"登记了挂起却仍发即时回执"这种缺陷在 PC 上 + 一次就能钉死。 + +桩的边界(每一层为什么这么切): + - 真实头:net_socket.h / net_types.h / net_config.h / net_pending.h / + net_listener.h / net_events.h / cmsis_os2.h(只依赖 stdint+stddef,宿主可编) + → 字段名、枚举取值、队列原型一改即编译期报错; + - 假芯片:test/host/common/ch395f_fake.h/.c(NET 域共享件,本批只做**纯新增**扩展: + net_socket.c 这一层才会调的初始化下发类 + 全局中断 0x19 + sock_reset 复位机 + + cmd_done 判据;既有宏/字段/函数一字未动); + - 假内核:本目录 ns_rtos.c(osMessageQueue 环形队列 + 任务通知 + 时基 + + 临界区计数);**阻塞等待 = 调度点**:xTaskNotifyWait(portMAX_DELAY) 里以 + netTask 身份跑一轮 net_process_messages(),于是单次公开 API 调用能跑完整条 + 入队→分派→通知值回传→调用方恢复 errno 的链; + - 兄弟模块桩:本目录 ns_peers.c(net_listener/net_pending/net_events 的边界契约 + + 调用记录)——这三个模块各有自己的主机单测(34/34/43 条),本套只测 + net_socket.c 自己的决策,并把"该不该登记、登记哪个 reason"变成可断言面; + - main.h / FreeRTOS.h / task.h / dbg_log.h / test_config.h / net_latency_test.h + 是本目录桩;刻意不定义 TEST_LATENCY_PROBE 与 NET_BENCH_CMD_LOG。 + - 不需要 #undef s_addr:本套不 include ff.h(唯一会经 Windows 支路拉进 winsock.h + 并把 s_addr 定义成 S_un.S_addr 的来源),真实 net_types.h 用的是自有的 + struct net_in_addr 镜像类型,不受污染。 + +用法(仓库根目录): + python test/host/net_socket/run_unit_test.py + +退出码:0=PASS / 1=测试失败(含常量闸不过)/ 2=环境错误。 + +设计依据:docs/网络/网络自愈架构设计.md §4.10、docs/测试说明/主机单测覆盖率口径.md §9 +创建日期:2026-10-24 +""" +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 必须最先(覆盖 ch395f.h / FreeRTOS.h / task.h / main.h / dbg_log.h); +# CMSIS_RTOS_V2 提供**真实** cmsis_os2.h;common 提供共享假芯片;NET 目录给真实 +# net_socket.h / net_types.h / net_pending.h / net_listener.h / net_events.h;HERE 放本模块头。 +BUILD = TestBuild( + name="NET Socket Tests", + src=os.path.join(HERE, "net_socket_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "Middlewares", "Third_Party", "FreeRTOS", "Source", + "CMSIS_RTOS_V2"), + os.path.join(ROOT, "test", "host", "common"), + os.path.join(ROOT, "Drivers", "BSP", "NET"), + HERE, + ], + # /wd4244:生产源里 ch395f_set_*(sockfd, ...) 一律传 int fd 给 uint8_t 形参, + # ARMCC 不报、MSVC /W4 每条都报一遍(20+ 条)——那是宿主与原编译器的诊断差异, + # 不是本批要盯的问题,屏蔽以保持门禁输出可读(不改生产代码去"消警")。 + extra_flags=["/wd4244", "/wd4100"], + summary_marker="=== NET Socket Tests:", +) + + +# --------------------------------------------------------------------------- +# 常量闸:真头与假件的**同名** CH395F_* 常量数值必须逐一对齐 +# --------------------------------------------------------------------------- +# +# 为什么必须有这根闸(2026-10-24 实测漂移):假件头自称"值抄自真实头,勿顺手改", +# 实际 CH395F_SOCKET_OPEN 写成 0x01(真值 0x05)、CH395F_TCP_ESTABLISHED 写成 0x05 +# (真值 0x04)。host 编译里生产码看到的也是假宏,宏对宏自洽 → 三套全绿也照不出来。 +# 谁一旦写**数字**断言或从假件抄常量,就拿到了与真芯片不同的值。 +# +# 为什么只能文本核对:两个头在 host 编译里是互斥的(stub/ch395f.h 转发到假件), +# 跨头 _Static_assert 不可能同时看到两份定义 → 编译期闸不存在。 +# +# 只做同名常量的数值比对,且**只比对两边都是纯十六进制/十进制字面量的**: +# 宏链(CH395F_SEND_BUF_SIZE_BYTES 那种引用其它宏的)跳过——它们由结构而不是值约束。 +_DEFINE_RE = re.compile(r"^\s*#\s*define\s+(CH395F_[A-Z0-9_]+)\s+([^\s/]+)") +_NUM_RE = re.compile(r"^(0[xX][0-9a-fA-F]+|[0-9]+)(U|UL|ULL)?$") + + +def parse_consts(path): + """{CH395F_xxx: int},只收字面量取值的宏;文件不存在返回 None。""" + if not os.path.isfile(path): + return None + out = {} + try: + with open(path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + m = _DEFINE_RE.match(line) + if not m: + continue + name, val = m.group(1), m.group(2) + if not _NUM_RE.match(val): + continue + digits = re.sub(r"[uUlL]+$", "", val) + try: + num = int(digits, 0) + except ValueError: + continue + out.setdefault(name, (num, line.rstrip("\n"))) + except OSError: + return None + return out + + +def check_fake_chip_constants(verbose=True): + """返回不一致的 (名字, 真头值, 假件值) 列表;两侧任一头读不到按环境问题报告。""" + real = parse_consts(os.path.join(ROOT, "Drivers", "BSP", "CH395F", "ch395f.h")) + fake = parse_consts(os.path.join(HERE, os.pardir, "common", "ch395f_fake.h")) + if real is None: + return None + if fake is None: + return None + drift = [] + for name, (val, _line) in sorted(real.items()): + if name in fake and fake[name][0] != val: + drift.append((name, val, fake[name][0])) + if verbose and drift: + print("[DRIFT] 假件常量与真实 ch395f.h 不一致(host 编译里宏对宏自洽,照不出来):", + file=sys.stderr) + for name, rv, fv in drift: + print(" {} 真=0x{:02X} 假=0x{:02X}".format(name, rv, fv), file=sys.stderr) + return drift + + +if __name__ == "__main__": + drift = check_fake_chip_constants() + if drift is None: + print("[ENV] 常量闸无法读取真头或假件头(路径不对?)", file=sys.stderr) + sys.exit(2) + if drift: + print("[FAIL] 常量闸拦下 {} 处漂移:修 test/host/common/ch395f_fake.h 到真值" + "(出处见 Drivers/BSP/CH395F/ch395f.h 与 docs/芯片手册/CH395F.md)" + .format(len(drift)), file=sys.stderr) + sys.exit(1) + print("[OK] 常量闸:假件 CH395F_* 常量与真实 ch395f.h 同名项全对齐") + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/net_socket/stub/FreeRTOS.h b/test/host/net_socket/stub/FreeRTOS.h new file mode 100644 index 00000000..dd46b89e --- /dev/null +++ b/test/host/net_socket/stub/FreeRTOS.h @@ -0,0 +1,40 @@ +/* + * 模块名称:主机侧单测桩 - FreeRTOS.h(net_socket 模块专用) + * 模块功能:给宿主一份与 FreeRTOS 语义等价的类型定义。net_socket.c 用到 TaskHandle_t / + * TickType_t / BaseType_t / eNotifyAction / pdTRUE / portMAX_DELAY,真实那份 + * 经 portmacro.h 拉编译器与芯片寄存器,PC 上编不动。 + * ⚠ 枚举成员与取值照抄 Middlewares/.../include/task.h:92-97 与 projdefs.h:44 + * (eSetBits=0 起、pdTRUE=1、pdFALSE=0),改了就是换分母。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef FREERTOS_H +#define FREERTOS_H + +#include +#include + +typedef void *TaskHandle_t; +typedef long BaseType_t; +typedef unsigned long UBaseType_t; +typedef uint32_t TickType_t; + +#define pdFALSE ((BaseType_t)0) +#define pdTRUE ((BaseType_t)1) +#define pdPASS pdTRUE +#define pdFAIL pdFALSE + +#ifndef portMAX_DELAY +#define portMAX_DELAY ((TickType_t)0xFFFFFFFFU) +#endif + +/* 与真实 task.h 同序(eSetBits..eSetValueWithoutOverwrite),取值不可重排 */ +typedef enum { + eNoAction = 0, + eSetBits, + eIncrement, + eSetValueWithOverwrite, + eSetValueWithoutOverwrite +} eNotifyAction; + +#endif /* FREERTOS_H */ diff --git a/test/host/net_socket/stub/ch395f.h b/test/host/net_socket/stub/ch395f.h new file mode 100644 index 00000000..34d09601 --- /dev/null +++ b/test/host/net_socket/stub/ch395f.h @@ -0,0 +1,10 @@ +/* + * 模块名称:主机侧单测桩 - ch395f.h(net_socket 模块入口) + * 模块功能:只做转发,真身在 test/host/common/ch395f_fake.h/.c(NET 域共享假芯片)。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef CH395F_H +#define CH395F_H +#include "ch395f_fake.h" +#endif /* CH395F_H */ diff --git a/test/host/net_socket/stub/cmsis_os.h b/test/host/net_socket/stub/cmsis_os.h new file mode 100644 index 00000000..b2e553b8 --- /dev/null +++ b/test/host/net_socket/stub/cmsis_os.h @@ -0,0 +1,14 @@ +/* + * 模块名称:主机侧单测桩 - cmsis_os.h(net_socket 模块专用) + * 模块功能:net_socket.c include 的是 CMSIS-RTOS **v1** 头,但它实际用的 API + * (osDelay / osMessageQueueGet/Put / osKernelGetState / osOK / osWaitForever) + * 全在 v2 头里。真实 v1 头会拉 FreeRTOS.h + task.h + cmsis_os2.h, + * 前两个由本目录桩接管,cmsis_os2.h 用**真实头**(只依赖 stdint/stddef, + * 宿主可编)——所以本桩就是一次转发,把 v2 的真实类型/枚举/原型接进来。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef CMSIS_OS_H +#define CMSIS_OS_H +#include "cmsis_os2.h" +#endif /* CMSIS_OS_H */ diff --git a/test/host/net_socket/stub/dbg_log.h b/test/host/net_socket/stub/dbg_log.h new file mode 100644 index 00000000..b49c7d4e --- /dev/null +++ b/test/host/net_socket/stub/dbg_log.h @@ -0,0 +1,23 @@ +/* + * 模块名称:主机侧单测桩 - dbg_log.h(net_socket 模块专用) + * 模块功能:DBG_* 换成计数,风格与本目录既有 NET 域桩一致(net_listener / net_events + * 同名文件)。net_socket.c 有 11 条 DBG_ERROR + 6 条 DBG_INFO,其中 + * 「短写必留痕」「版本门控拒置 bit3 必 [ERR]」「open 失败必 [ERR]」 + * 「close 复位没成也放行仍要留痕」这四条本身就是可断言的契约。 + * 本模块在 include 本头之前把 DBG_LINE_BUF_MAX 设为 128U(AGENTS 深调用链纪律), + * 桩不关心缓冲大小,只数条数。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef DBG_LOG_H +#define DBG_LOG_H + +extern int g_ns_info_count; +extern int g_ns_error_count; +extern int g_ns_debug_count; + +#define DBG_INFO(fmt, ...) do { (void)(fmt); g_ns_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_ns_debug_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_ns_error_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/net_socket/stub/main.h b/test/host/net_socket/stub/main.h new file mode 100644 index 00000000..54eb2bc8 --- /dev/null +++ b/test/host/net_socket/stub/main.h @@ -0,0 +1,35 @@ +/* + * 模块名称:主机侧单测桩 - main.h(net_socket 模块专用) + * 模块功能:net_socket.c 从真实 Inc/main.h 只要三样:HAL_GetTick(自旋/超时/挂起预算的 + * 唯一致度源)、HAL_GPIO_ReadPin(轮询 CH395F INT#,低电平有效)、 + * CH395_INT_GPIO_Port / CH395_INT_Pin(Inc/main.h:60-61,PE5)。 + * 引脚名/号按真实头转抄(真值改了就编译期/断言期报错),GPIO_TypeDef 用不透明 + * void*(本层不碰寄存器,只需类型能编)。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef MAIN_H +#define MAIN_H + +#include + +typedef void GPIO_TypeDef; + +typedef enum { + GPIO_PIN_RESET = 0, + GPIO_PIN_SET +} GPIO_PinState; + +/* 转抄 Inc/main.h:60-61 */ +#define CH395_INT_Pin 0x0020U /* GPIO_PIN_5 */ +#define CH395_INT_GPIO_Port ((GPIO_TypeDef *)0) + +uint32_t HAL_GetTick(void); +GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); + +/* 每轮 net_poll 时基自走多少毫秒(默认 1ms:轮询本身就要花时间,自走才量得到 + * NET_SEND_SPIN_MS 这类"有界自旋"预算;测试可置 0 造"时钟冻结"形态) */ +extern uint32_t g_ns_poll_tick_inc; +extern uint32_t g_ns_poll_calls; /* net_poll 经 GPIO 读 INT# 的次数(也是跑飞保险计数) */ + +#endif /* MAIN_H */ diff --git a/test/host/net_socket/stub/net_latency_test.h b/test/host/net_socket/stub/net_latency_test.h new file mode 100644 index 00000000..d8517349 --- /dev/null +++ b/test/host/net_socket/stub/net_latency_test.h @@ -0,0 +1,9 @@ +/* + * 模块名称:主机侧单测桩 - net_latency_test.h(net_socket 模块专用) + * 模块功能:空壳,配合 TEST_LATENCY_PROBE 不定义即把 lat_* 调用全部条件编译掉。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef NET_LATENCY_TEST_H +#define NET_LATENCY_TEST_H +#endif /* NET_LATENCY_TEST_H */ diff --git a/test/host/net_socket/stub/ns_rtos.h b/test/host/net_socket/stub/ns_rtos.h new file mode 100644 index 00000000..a49b82e3 --- /dev/null +++ b/test/host/net_socket/stub/ns_rtos.h @@ -0,0 +1,84 @@ +/* + * 模块名称:假内核(net_socket 主机侧单测专用) + * 模块功能:给 net_socket.c 一份可在 PC 上跑的 FreeRTOS/CMSIS-RTOS2 薄壳—— + * 消息队列(osMessageQueuePut/Get,真实 cmsis_os2.h 的原型)、 + * 任务通知(xTaskNotify / xTaskNotifyWait)、当前任务句柄、时基、临界区计数。 + * + * ⚠ 仅供 test/host/net_socket/ 使用,不参与 MCU 构建。 + * + * 关键设计:**阻塞等待即调度点**。net_wait_result 的第二段 + * xTaskNotifyWait(..., portMAX_DELAY) 在真机上是"睡到 netTask 回执";这里把这一次 + * 等待实现成"调用测试装泵的 pump 钩子(= 以 netTask 身份跑一轮 + * net_process_messages()),再拾取通知值"。于是单次 net_send()/net_recv() 调用就能 + * 把"入队 → 分派 → 通知值回传 → 调用方恢复 errno"整条跨上下文链真实跑完, + * 而不是把中间环节假掉。 + * + * 泵保险:pump 钩子最多跑 NS_PUMP_MAX 轮,超了直接 exit(2)——否则一条"该通知却没通知" + * 的缺陷会让门禁挂死而不是报错(本仓假 osDelay 的跑飞保险同一条纪律)。 + * + * 创建日期:2026-10-24 + */ +#ifndef NS_RTOS_H +#define NS_RTOS_H + +#include "FreeRTOS.h" +#include "cmsis_os2.h" +#include "main.h" /* 桩:HAL_GetTick / HAL_GPIO_ReadPin / CH395_INT_* */ + +#define NS_Q_CAP 8u +#define NS_PUMP_MAX 50 + +/* 两个假任务:调用方任务 + netTask(句柄取地址即唯一) */ +extern int g_ns_task_caller; +extern int g_ns_task_net; + +/* 当前任务句柄(测试直接读写:切换"我是调用方"还是"我是 netTask") */ +extern TaskHandle_t g_ns_cur_task; + +/* 阻塞等待时跑的泵(测试装:通常 = 跑一轮 net_process_messages) */ +typedef void (*ns_pump_fn)(void); +extern ns_pump_fn g_ns_pump; +extern int g_ns_pump_calls; /* 泵被驱动的次数 */ +extern int g_ns_block_waits; /* 真正走到"阻塞分支"的次数 */ + +/* 通知/队列观测面 */ +extern int g_ns_notify_calls; /* xTaskNotify 总次数 */ +extern uint32_t g_ns_notify_last_val; +extern TaskHandle_t g_ns_notify_last_to; +extern int g_ns_put_calls; +extern int g_ns_get_calls; +extern int g_ns_crit_enter; +extern int g_ns_crit_exit; +extern uint32_t g_ns_tick; + +void ns_rtos_reset(void); +void ns_rtos_q_flush(void); +int ns_rtos_q_count(void); +void ns_rtos_q_fill(int n); /* 预灌 n 条占位消息(把队列灌到满,测 BUSY 分支) */ + +/* 测试用的显式通知投递(不经队列,直接给某任务置通知值) */ +void ns_rtos_post_notify(TaskHandle_t to, uint32_t v); + +/* 手工投一条原始消息(绕过公开包装器:造未知 type、造 netTask 自调等异常形态) */ +int ns_rtos_q_put_raw(const net_msg_t *msg); + +/* —— 下面是给桩头对接的实现(原型与真实头一致) —— */ +osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr, + uint8_t msg_prio, uint32_t timeout); +osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr, + uint8_t *msg_prio, uint32_t timeout); +osKernelState_t osKernelGetState(void); +osStatus_t osDelay(uint32_t ticks); + +/* 每轮 net_poll(= 一次 INT# 读取)都调一下测试回调:用来在"等待发送缓冲/等待数据" + * 的循环途中投递并发事件(对端断线、芯片突然可读),而不必自己发明时间 API */ +typedef void (*ns_poll_hook_fn)(void); +extern ns_poll_hook_fn g_ns_on_poll; + +/* CH395F INT# 电平(1=SET 无中断,net_poll 走短路分支) */ +extern int g_ns_int_pin_set; + +/* 内核状态可注入(net_errno_slot 靠它判"内核没跑起来"的退化分支) */ +extern osKernelState_t g_ns_kernel_state; + +#endif /* NS_RTOS_H */ diff --git a/test/host/net_socket/stub/task.h b/test/host/net_socket/stub/task.h new file mode 100644 index 00000000..f097adaf --- /dev/null +++ b/test/host/net_socket/stub/task.h @@ -0,0 +1,32 @@ +/* + * 模块名称:主机侧单测桩 - task.h(net_socket 模块专用) + * 模块功能:声明 net_socket.c 用到的三件套 + 临界区宏。原型与真实 + * Middlewares/.../include/task.h 逐字一致(返回值/参数顺序都不许改), + * 实现在 ns_rtos.c(假内核:任务通知值 + 当前任务句柄可切换)。 + * taskENTER_CRITICAL/EXIT_CRITICAL 记计数——临界区只存在于本层槽位分配, + * 计数能断言"只在占槽时进临界区、且成对退出"。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef TASK_H +#define TASK_H + +#include "FreeRTOS.h" + +TaskHandle_t xTaskGetCurrentTaskHandle(void); +BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, + eNotifyAction eAction); +BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, + uint32_t ulBitsToClearOnExit, + uint32_t *pulNotificationValue, + TickType_t xTicksToWait); +uint32_t xTaskGetTickCount(void); + +int ns_os_crit_depth(void); +void ns_os_enter_critical(void); +void ns_os_exit_critical(void); + +#define taskENTER_CRITICAL() ns_os_enter_critical() +#define taskEXIT_CRITICAL() ns_os_exit_critical() + +#endif /* TASK_H */ diff --git a/test/host/net_socket/stub/test_config.h b/test/host/net_socket/stub/test_config.h new file mode 100644 index 00000000..9913af31 --- /dev/null +++ b/test/host/net_socket/stub/test_config.h @@ -0,0 +1,11 @@ +/* + * 模块名称:主机侧单测桩 - test_config.h(net_socket 模块专用) + * 模块功能:空壳。net_socket.c 只在 TEST_LATENCY_PROBE 打开时才用延迟探针, + * 主机单测刻意**不开**(探针要写真实时间/真实周期计数,不是被测对象); + * 也不定义 NET_BENCH_CMD_LOG(压测命令日志同理,见 net_events 同名说明)。 + * ⚠ 仅供主机侧单测使用,不参与 MCU 构建。 + * 创建日期:2026-10-24 + */ +#ifndef TEST_CONFIG_H +#define TEST_CONFIG_H +#endif /* TEST_CONFIG_H */ diff --git a/test/host/soe_store/run_unit_test.py b/test/host/soe_store/run_unit_test.py new file mode 100644 index 00000000..7248411e --- /dev/null +++ b/test/host/soe_store/run_unit_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +r"""SOE 落盘胶水层(soe_store)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/soe_store/soe_store_test.c(以 #include 纳入**生产源** + App/log/soe_store.c 与 App/log/soe_rec.c,并纳入共享假卷 + test/host/common/fatfs_fake.c),在 PC 上离线跑 post 侧零 I/O 契约、环/队列分流与 + 丢弃计数、drain 侧的卷锁 try / FA_WRITE|FA_OPEN_APPEND / 写回字节数校验 / 容量满 + 重建 / 故障留痕去重,失败即非 0 退出。工具链与判定委托 + test/host/common/unit_test_harness.py。 + +⚠ 与 test/host/soe/ 是两码事:那套测的是 soe_rec.c 的**编解码**(纯逻辑,零 FS); + 本套测 soe_store.c 的**落盘胶水**。目录、模块名、TC 号族都分开 + (TC 模块码用 SOESTORE,SOE 已被存量占用)。 + +为什么要主机侧单测(而不是只用台架): + 1. post 侧"一条 DBG 都不许打"是 protectTask 的**栈预算硬约束**(函数头原话: + 该任务静态最坏链 = 经 s_log_active_edge 的 console 尾链 872 B,post 里加一条 + 日志就 +320 B → stack_report FAIL)。真机上忘这条不会崩,只会某次最坏路径 + 把栈打穿;只有把 DBG_* 换成计数才能机械断言"这条路径 0 日志"。 + 2. 环写失败不推进 tail(不丢)而队列写失败丢已出队那一条——**两条通道不对称**, + 台架要恰好撞上"FTP 正占卷"才看得到一次。 + 3. 短写(FR_OK + 写回不足)、容量满重建、故障留痕去重都要故障注入才成立。 + 4. 陷阱二(名字/参数类错误码严禁升级成卷操作)只有假卷能把 FR_INVALID_NAME 稳定 + 喂进自愈路径,并断言"邻居文件与 /log 目录一根毛都没动"。 + +桩的边界:ff.h / sys_clock.h / vol_lock.h / soe_rec.h / soe_store.h 用**真实头** + (FRESULT、FA_*、sys_clock_get 签名一改即编译期报错);main.h 是桩,只为截断 + sys_clock.h -> sd2506.h -> main.h -> stm32f4xx_hal.h 这条链(真实 main.h 第一行 + 就拉整棵 HAL);cmsis_os.h / dbg_log.h 是本目录桩。FatFs 由 + test/host/common/fatfs_fake.c 给 RAM 实现(**显式纳入,不用目录通配**, + 避免被卷进别人的构建)。sys_clock_* / vol_lock_* / 消息队列强符号桩在本测试单元里。 + +用法(仓库根目录): + & '.venv\Scripts\python.exe' test/host/soe_store/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:docs/架构设计/STM32F4-Base存储架构说明.md §10.1 第 10/11 条、 +docs/算法/继电保护功能复现规划.md §3.4(L4-4/L4-5,B9)、 +docs/测试说明/主机单测覆盖率口径.md §9 P2 第 2 项 +创建日期:2026-10-24 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 必须最先(覆盖 cmsis_os.h / dbg_log.h / main.h), +# 其余全是真实头目录;假卷 fatfs_fake.c 由测试单元 #include 纳入(同 ch395f_fake.c 惯例)。 +BUILD = TestBuild( + name="SOE Store Tests", + src=os.path.join(HERE, "soe_store_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "test", "host", "common"), + os.path.join(ROOT, "App", "log"), + os.path.join(ROOT, "App", "util"), + os.path.join(ROOT, "App", "fs"), + os.path.join(ROOT, "Drivers", "BSP"), # bsp_err.h(sd2506.h 依赖) + os.path.join(ROOT, "Drivers", "BSP", "SD2506"), + os.path.join(ROOT, "Lib", "FatFs"), # 真实 ff.h / ffconf.h + HERE, + ], + summary_marker="=== SOE Store Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/soe_store/soe_store_test.c b/test/host/soe_store/soe_store_test.c new file mode 100644 index 00000000..155d20c8 --- /dev/null +++ b/test/host/soe_store/soe_store_test.c @@ -0,0 +1,708 @@ +/* + * 模块名称:SOE 落盘胶水层(soe_store)主机侧单元测试(L0.5) + * 模块功能:在 PC 上离线验证 App/log/soe_store.c 的四条对外契约与它自己的 + * "照 sys_log_mcu 的台架实证纪律"注释: + * - post 侧**零文件系统、零日志**(protectTask 栈预算 + DoD 硬约束); + * - 环 / 队列两条待发通道的分流与"丢弃只计数"; + * - drain 侧的 vol_lock try、FA_WRITE|FA_OPEN_APPEND、写回字节数校验、 + * 容量满 unlink 重建、故障留痕去重; + * - **名字/参数类错误码绝不升级成卷操作**(存储架构 §10.1 第 10 条)。 + * + * 被测对象:真实源文件(#include 原样纳入,不改生产代码): + * App/log/soe_store.c —— 本模块主体 + * App/log/soe_rec.c —— 64B 记录构建(纯逻辑,零 OS,直接用真件; + * test/host/soe 已单独覆盖其编解码,本套不重测) + * 假件与桩: + * test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明) + * stub/dbg_log.h / stub/cmsis_os.h / stub/main.h —— 本目录桩 + * sys_clock / vol_lock 的可控强符号实现写在本文件里 + * + * 为什么要主机侧单测(而不是只靠台架): + * 1. **post 侧禁 DBG 是栈预算硬约束**(soe_store.c 函数头原话:protectTask 静态最 + * 坏链 = 经 s_log_active_edge 的 console 尾链 872 B,post 内任何日志调用点都会 + * 再垫高一帧,stack_report 实测 +320 B → FAIL)。真机上"忘了这条"不会崩,只会 + * 在某次最坏路径上把栈打穿 → 只有把 DBG_* 换成计数才能机械断言"这条路径 0 日志"。 + * 2. **环 / 队列的丢弃语义不对称**:环写失败**不推进 tail**(下轮重试、条目不丢), + * 队列条目已出队、卷忙就丢那一条。真机上要恰好撞上"FTP 正在占卷"才看得到, + * 而"到底丢不丢"是运维读 SOE 时能不能信任序号连续的前提。 + * 3. **短写 = 失败**(FR_OK + 写回字节数不足,簇分配没有错误码)与**满则重建** + * 这两条只在介质坏块 / 容量见底时发作;假卷可以把"只写回 63 字节"直接喂进来。 + * 4. 写故障留痕的去重(s_fault_reported)决定了长跑时 /log 与串口会不会被同一条 + * 故障刷屏——真机要复现"失败→恢复→再失败"这个序列极慢。 + * + * 覆盖点(TC 号族 = SOESTORE(三位序号);SOE 已被存量 test/host/soe 占用): + * —— post 侧(零 I/O 契约)—— + * TC-SOESTORE-001 post 3 条只写环:零 FatFs 调用、零卷锁调用、零日志 + * TC-SOESTORE-002 墙钟未建立(UINT32_MAX)→ flags 带 IV 且 epoch 字段落 0(不造假时标) + * TC-SOESTORE-003 seq 上电内单调 +0123(丢弃也占号:号不跳 = 假象) + * TC-SOESTORE-004 环满(8 条)→ 第 9 条丢弃 + 计数,不阻塞、不崩、**不打日志** + * TC-SOESTORE-005 内核运行 + 队列已建 → 走队列,环不再写入 + * TC-SOESTORE-006 队列满 → 丢弃 + 计数(与环满同一条可观测出口) + * TC-SOESTORE-007 丢弃计数饱和:UINT32_MAX 不再回绕 + * —— drain 侧:落盘与打开模式 —— + * TC-SOESTORE-010 首次 drain 惰性建队列;1 条落盘 = 64 字节且可被 soe_rec_parse 解回 + * TC-SOESTORE-011 打开模式必须是 FA_WRITE|FA_OPEN_APPEND;对照保真:只给 + * FA_OPEN_APPEND 的句柄 f_write 恒返 FR_DENIED(陷阱一,本仓实证根因) + * TC-SOESTORE-012 多条落盘:一条一次 open/write/close(本模块**没有**批量合并—— + * 与 sys_log 的 N-24 不同),FIFO 顺序、seq 递增 + * TC-SOESTORE-013 open 与 close 严格配对(脏句柄不跨调用残留) + * —— drain 侧:故障注入 —— + * TC-SOESTORE-014 短写 63B → 判失败 → unlink 重建 → 重试成功 → 最终恰好 64 字节 + * TC-SOESTORE-015 连续两次短写 → 返回 -1;留痕恰好一次(去重);恢复后再失败才再留痕 + * TC-SOESTORE-016 卷忙(vol_lock_wait(0)=0)→ 返回 -2;**不碰 FatFs、不 release** + * TC-SOESTORE-017 环条目在写失败时不推进 tail → 下轮重试不丢、且不重复落盘 + * TC-SOESTORE-018 队列条目已出队:卷忙只丢当前一条,剩余留在队列(不对称现状钉住) + * TC-SOESTORE-019 容量满(1024 条)→ unlink 重建 + INFO 留痕,新文件恰好 64 字节 + * TC-SOESTORE-020 丢弃留痕按差值上报一次,不刷屏 + * TC-SOESTORE-021 队列创建失败(返回 NULL)→ 条目滞留环、下轮再试,不崩 + * —— 陷阱二:名字/参数类错误码不得升级成卷操作 —— + * TC-SOESTORE-022 f_open 返 FR_INVALID_NAME → 只留痕返回 -1:**不 unlink**、 + * 不碰 /log 目录、条目全部保留(自愈只针对自己的文件) + * TC-SOESTORE-023 写路径反复失败时 f_unlink 次数有界(每轮 <=1),目录与邻居文件不动 + * TC-SOESTORE-024 假卷严格 8.3 校验的保真自检:".fschk" -> 名字/盘符类拒绝码, + * 而生产路径 SOE_PATH 必过(钉住"改名即静默写失败"的约束) + * + * 覆盖率台账(--module soe_store 自量):App/log/soe_store.c 78/79 行 = 98.7%、 + * 函数 4/4。唯一未覆盖行 soe_store.c:202 = drain 里 for (;;) 的收尾大括号, + * 该循环只能靠 break 退出,不存在"走到行尾再回跳"的机器码,属工具口径缺口。 + * + * 本套照出的现状(只登记、不改生产代码;详见交 Lead 的报告): + * soe_store.c 的函数头第 ④ 条写"打开/写入结构性错误 → unlink 重试一次(同 run.log)", + * 但实现里 **f_open 失败直接 return -1、不 unlink 重试**(只有 f_write 失败才重试)。 + * run.log 侧(sys_log_mcu.c s_append_rec)则对 FR_NO_FILE/FR_NO_PATH/FR_INVALID_NAME + * 等一整套码做 unlink+重开。两套并不一致 —— 是注释夸大了实现,还是实现漏了一半, + * 需生产侧裁定;本套只用 TC-SOESTORE-022 把现状钉成"改了会响"。 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2) + */ + +#include +#include +#include +#include + +#include "ff.h" /* 真实 Lib/FatFs/ff.h:FRESULT / FA_* / FIL 原汁 */ +#include "fatfs_fake.h" /* 假卷:类型与常量都取自上面那份真头 */ +#include "cmsis_os.h" /* 桩:消息队列 + 内核状态(签名照 cmsis_os2.h) */ +#include "dbg_log.h" /* 桩:DBG_* -> 计数 */ +#include "sys_clock.h" /* 真实头(App/util):签名一改即编译期报错 */ +#include "vol_lock.h" /* 真实头(App/fs) */ +#include "soe_rec.h" +#include "soe_store.h" + +/* ===================== 断言框架 ===================== */ + +static int s_total; +static int s_passed; +static int s_failed; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* ===================== 假件与生产源码纳入 ===================== */ + +#include "fatfs_fake.c" /* 假 FatFs 实现 */ +#include "soe_rec.c" /* 真实 64B 记录构建(纯逻辑) */ +#include "soe_store.c" /* 被测主体 */ + +/* ===================== dbg_log 桩计数(stub/dbg_log.h 声明) ===================== */ + +int g_soe_err_count; +int g_soe_info_count; +int g_soe_debug_count; + +/* ===================== 假墙钟 ===================== */ + +static uint32_t s_clock_now = 1760234567u; /* 2025-10-12 前后,CP56 年域内 */ +static int s_clock_valid = 1; + +uint32_t sys_clock_get(void) +{ + return (s_clock_valid != 0) ? s_clock_now : UINT32_MAX; +} + +/* sys_clock.h 里声明、本模块不调用的接口:给出空实现,纯为可链接 */ +int sys_clock_init(void) { return 0; } +int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *p_time) +{ + (void)unix_secs; (void)p_time; return 0; +} +int sys_clock_set_unix(uint32_t unix_secs) { (void)unix_secs; return 0; } +int sys_clock_get_time(sd2506_time_t *p_time) { (void)p_time; return -1; } +uint32_t HAL_GetTick(void) { return 0u; } + +char *sys_clock_get_str(char *buf, size_t buf_size) +{ + (void)buf; (void)buf_size; + return NULL; +} + +/* ===================== 假卷锁(try 语义 + 配对检查) ===================== */ + +static int s_lock_busy_left; /* >0:接下来 N 次 vol_lock_wait 返回"忙"(try 失败) + * 用计数器而不是按序号取值的脚本数组——脚本数组的下标 + * 会被场景里前几次 drain 消耗掉,注入看着设了其实没生效 */ +static int s_lock_waits; +static int s_lock_releases; +static int s_lock_held; +static int s_lock_seq_err; /* 未持锁却 release = 锁纪律被写反 */ + +static void lock_busy_next(int n) +{ + s_lock_busy_left = n; +} + +static void locks_clear(void) +{ + s_lock_busy_left = 0; + s_lock_waits = 0; + s_lock_releases = 0; + s_lock_held = 0; + s_lock_seq_err = 0; +} + +int vol_lock_init(void) { return 0; } + +int vol_lock_wait(uint32_t timeout_ms) +{ + int r; + + (void)timeout_ms; + /* 跑飞保险:调用数越界直接判死,不让死循环拖挂门禁 */ + if (s_lock_waits > 20000) { + (void)fprintf(stderr, "[FATAL] vol_lock_wait call guard tripped\n"); + exit(2); + } + r = (s_lock_busy_left > 0) ? 0 : 1; + if (s_lock_busy_left > 0) { + s_lock_busy_left--; + } + s_lock_waits++; + if (r != 0) { + s_lock_held = 1; + } + return r; +} + +void vol_lock_release(void) +{ + s_lock_releases++; + if (s_lock_held == 0) { + s_lock_seq_err++; /* 没拿到锁就释放:fail-open 语义被写反的证据 */ + } + s_lock_held = 0; +} + +/* ===================== 假消息队列(真 FIFO + 满/空语义) ===================== */ + +#define MQ_SLOTS 16u +#define MQ_MAXMSG 64u /* SOE 记录 64B;syslog 侧条目 12B,同一池够用 */ + +typedef struct { + int used; /* 槽位有无消息 */ + uint8_t data[MQ_MAXMSG]; +} mq_slot_t; + +static mq_slot_t s_q_pool[MQ_SLOTS]; +static uint32_t s_q_head; /* 出队下标 */ +static uint32_t s_q_cnt; +static uint32_t s_q_cap; /* osMessageQueueNew 给的 msg_count */ +static uint32_t s_q_msgsz; +static int s_q_new_calls; +static int s_q_new_fail; /* 1 = osMessageQueueNew 返 NULL */ +static int s_q_get_calls; +static int s_q_put_calls; +static int s_q_create_fail_left; /* 预留:连续失败注入(当前用 0/1) */ + +static void queue_clear(void) +{ + (void)memset(s_q_pool, 0, sizeof(s_q_pool)); + s_q_head = 0u; + s_q_cnt = 0u; + s_q_cap = SOE_QUEUE_MAX; + s_q_msgsz = SOE_REC_SIZE; + s_q_new_calls = 0; + s_q_new_fail = 0; + s_q_create_fail_left = 0; + s_q_get_calls = 0; + s_q_put_calls = 0; +} + +osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size, + const osMessageQueueAttr_t *attr) +{ + (void)attr; + s_q_new_calls++; + if ((s_q_new_fail != 0) || (s_q_create_fail_left > 0)) { + if (s_q_create_fail_left > 0) { + s_q_create_fail_left--; + } + return NULL; + } + s_q_cap = msg_count; + s_q_msgsz = msg_size; + s_q_head = 0u; + s_q_cnt = 0u; + /* 句柄本身没有意义:返回一个稳定非空指针即可 */ + return (osMessageQueueId_t)&s_q_pool[0]; +} + +osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr, + uint8_t msg_prio, uint32_t timeout) +{ + uint32_t tail; + + (void)mq_id; (void)msg_prio; (void)timeout; + if (s_q_put_calls++ > 20000) { + (void)fprintf(stderr, "[FATAL] osMessageQueuePut guard tripped\n"); + exit(2); + } + if ((msg_ptr == NULL) || (s_q_cnt >= s_q_cap) || (s_q_msgsz > MQ_MAXMSG)) { + return osErrorResource; /* 队列满:真实 CMSIS 在 timeout=0 时的返回 */ + } + tail = (s_q_head + s_q_cnt) % MQ_SLOTS; + (void)memcpy(s_q_pool[tail].data, msg_ptr, s_q_msgsz); + s_q_pool[tail].used = 1; + s_q_cnt++; + return osOK; +} + +osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr, + uint8_t *msg_prio, uint32_t timeout) +{ + (void)mq_id; (void)msg_prio; (void)timeout; + if (s_q_get_calls++ > 20000) { + (void)fprintf(stderr, "[FATAL] osMessageQueueGet guard tripped\n"); + exit(2); + } + if ((msg_ptr == NULL) || (s_q_cnt == 0u)) { + return osErrorResource; /* 队列空 */ + } + (void)memcpy(msg_ptr, s_q_pool[s_q_head].data, s_q_msgsz); + s_q_pool[s_q_head].used = 0; + s_q_head = (s_q_head + 1u) % MQ_SLOTS; + s_q_cnt--; + return osOK; +} + +uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id) +{ + (void)mq_id; + return s_q_cnt; +} + +osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id) +{ + (void)mq_id; + s_q_head = 0u; + s_q_cnt = 0u; + return osOK; +} + +/* ===================== 内核状态开关 ===================== */ + +static osKernelState_t s_kernel_state = osKernelInactive; + +osKernelState_t osKernelGetState(void) +{ + return s_kernel_state; +} + +/* ===================== 复位与辅助 ===================== */ + +static void reset_all(void) +{ + ff_fake_reset(); + locks_clear(); + queue_clear(); + s_clock_now = 1760234567u; + s_clock_valid = 1; + s_kernel_state = osKernelInactive; + g_soe_err_count = 0; + g_soe_info_count = 0; + g_soe_debug_count = 0; + /* 生产模块的 static 在场景之间会残留:逐位显式清,漏一次会红一片 + *(test/host/lftpd_io 与 nand_port 同一教训) */ + (void)memset(s_ring, 0, sizeof(s_ring)); + s_ring_head = 0u; + s_ring_tail = 0u; + s_queue = NULL; + s_seq = 0u; + s_drop_cnt = 0u; + s_drop_logged = 0u; + s_fault_reported = 0u; + /* 生产语义前置:/log 由 defaultTask 挂载时建好,本模块假定它在(soe_store.h: + * "由 syslog_drain 在卷就绪门之后挂接") */ + (void)ff_fake_mkdir_direct("/log"); +} + +/* 用假卷 + 真 soe_rec_parse 校验落盘内容:路径常量取自生产宏 SOE_PATH */ +static int read_rec(uint32_t idx, soe_rec_t *p_out) +{ + uint8_t raw[SOE_REC_SIZE]; + + if (ff_fake_fetch(SOE_PATH, raw, idx * SOE_REC_SIZE, SOE_REC_SIZE) != SOE_REC_SIZE) { + return 0; + } + return (soe_rec_parse(raw, p_out) == 0) ? 1 : 0; +} + +/* ===================== TC-SOESTORE-001..007:post 侧 ===================== */ + +static void tc_post_side(void) +{ + soe_rec_t out; + uint32_t i; + int ok; + + printf("-- TC-SOESTORE-001..007 post 侧(零 I/O 契约)--\n"); + + /* 001:post 3 条只写环,绝不碰盘、绝不碰卷锁、绝不打日志 */ + reset_all(); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 0x1111u); + soe_store_post((uint8_t)SOE_T_ELM_RETURN, 2u, 0x2222u); + soe_store_post((uint8_t)SOE_T_PLATE_IN, SOE_ELM_NONE, 1u); + ok = (s_ring_head == 3u) && (s_ring_tail == 0u) && (s_drop_cnt == 0u); + for (i = 0u; i < (uint32_t)FF_FAKE_OP_N; i++) { + if (g_ff_obs.calls[i] != 0) { + ok = 0; + } + } + CHECK(ok && (s_lock_waits == 0) && (g_soe_err_count == 0) && (g_soe_info_count == 0), + "TC-SOESTORE-001 post 零 FatFs / 零卷锁 / 零日志(protectTask 栈预算)"); + + /* 002:墙钟未建立 -> IV + epoch 字段 0(不造假时标) */ + reset_all(); + s_clock_valid = 0; + soe_store_post((uint8_t)SOE_T_SAMPLE_BLOCK, 1u, 0xF0u); + soe_store_drain(); /* 环 -> 卷:不 drain 就还在 RAM 里 */ + ok = read_rec(0u, &out); + CHECK(ok && (out.flags & SOE_FLG_IV) != 0u && (out.epoch_s == 0u) + && (out.cp56[2] & 0x80u) != 0u && (out.value == 0xF0u), + "TC-SOESTORE-002 无墙钟 -> flags.IV + epoch=0 + CP56 IV 位(B9 口径)"); + + /* 003:seq 单调;丢弃的那条也占号(真机丢号 = 主机看到的号不连续是**如实**) */ + reset_all(); + for (i = 0u; i < 9u; i++) { + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i); + } + ok = (s_seq == 9u) && (s_drop_cnt == 1u); + CHECK(ok, "TC-SOESTORE-003 seq 单调递增,丢弃也占号(号不跳才是假象)"); + + /* 004:环满丢弃不阻塞、不崩、**不打日志**(留痕归 drain 侧) */ + reset_all(); + for (i = 0u; i < 9u; i++) { + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i); + } + CHECK((s_ring_head == 8u) && (s_drop_cnt == 1u) + && (g_soe_err_count == 0) && (g_soe_info_count == 0), + "TC-SOESTORE-004 环满丢弃:只计数,post 侧零 DBG(栈尾链 +320B 教训)"); + + /* 005:内核运行 + 队列已建 -> 走队列,环不再写 */ + reset_all(); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); /* 先落一条,drain 会建队列 */ + s_kernel_state = osKernelRunning; + soe_store_drain(); /* 建队列 + 落第 1 条 */ + s_ring_head = 0u; /* 观察用:清环位 */ + s_ring_tail = 0u; + soe_store_post((uint8_t)SOE_T_ELM_RETURN, 1u, 2u); + CHECK((s_ring_head == 0u) && (s_q_cnt == 1u) && (s_q_msgsz == SOE_REC_SIZE), + "TC-SOESTORE-005 队列已建 + 内核运行 -> 条目进队列、环不再写"); + + /* 006:队列满 -> 丢弃计数(与环满同一条可观测出口) */ + reset_all(); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); + s_kernel_state = osKernelRunning; + soe_store_drain(); /* 建队列 */ + (void)osMessageQueueReset(s_queue); + for (i = 0u; i < SOE_QUEUE_MAX + 2u; i++) { + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i); + } + CHECK((s_q_cnt == SOE_QUEUE_MAX) && (s_drop_cnt == 2u), + "TC-SOESTORE-006 队列满 -> 丢弃并计数(soe_store_dropped 同一出口)"); + + /* 007:计数饱和不回绕 */ + reset_all(); + s_drop_cnt = UINT32_MAX; + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); + CHECK((s_drop_cnt == UINT32_MAX) && (soe_store_dropped() == UINT32_MAX), + "TC-SOESTORE-007 丢弃计数饱和:UINT32_MAX 不回绕"); +} + +/* ===================== TC-SOESTORE-010..021:drain 落盘与故障注入 ===================== */ + +static void tc_drain_paths(void) +{ + soe_rec_t out; + uint32_t i; + int ok; + int opens_before; + int writes_before; + + printf("-- TC-SOESTORE-010..021 drain 落盘 / 故障注入 --\n"); + + /* 010:首次 drain 惰性建队列 + 1 条 64B 落盘且能解析回来 */ + reset_all(); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 3u, 0xABCDu); + soe_store_drain(); + ok = (s_q_new_calls == 1) && (s_q_msgsz == SOE_REC_SIZE) + && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE); + CHECK(ok && read_rec(0u, &out) && (out.type == (uint8_t)SOE_T_ELM_ASSERT) + && (out.elm_id == 3u) && (out.value == 0xABCDu) && (out.seq == 0u), + "TC-SOESTORE-010 惰性建队列 + 64B 定长落盘 + 记录可解回"); + + /* 011:打开模式必须带 FA_WRITE —— 陷阱一 */ + reset_all(); + soe_store_post((uint8_t)SOE_T_PLATE_OUT, SOE_ELM_NONE, 0u); + soe_store_drain(); + ok = (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND)); + /* 同一断言的"为什么必须带写位"保真对照:只给 FA_OPEN_APPEND 的句柄 f_write + * 必须拿 FR_DENIED——假件如果哪天放行,这条会红,整套陷阱一断言随之失效 */ + { + static FIL probe; + static uint8_t payload[SOE_REC_SIZE]; + UINT bw = 0u; + FRESULT fr2; + + fr2 = f_open(&probe, SOE_PATH, FA_OPEN_APPEND); + { + FRESULT fr3 = f_write(&probe, payload, SOE_REC_SIZE, &bw); + CHECK(ok && (fr2 == FR_OK) && (fr3 == FR_DENIED) && (bw == 0u), + "TC-SOESTORE-011 模式必为 FA_WRITE|FA_OPEN_APPEND;缺写位 f_write 恒 FR_DENIED"); + } + (void)f_close(&probe); + } + + /* 012:多条 = 一条一次 open/write/close(本模块刻意没有批量合并)+ FIFO */ + reset_all(); + for (i = 0u; i < 4u; i++) { + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)(i + 1u), 100u + i); + } + soe_store_drain(); + ok = (ff_fake_size(SOE_PATH) == 4u * SOE_REC_SIZE) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 4) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 4); + if (ok) { + for (i = 0u; i < 4u; i++) { + if (!read_rec(i, &out) || (out.seq != i) || (out.value != 100u + i)) { + ok = 0; + } + } + } + CHECK(ok, "TC-SOESTORE-012 逐条 open/write/close(无批量合并)+ FIFO 顺序正确"); + + /* 013:open/close 严格配对 */ + reset_all(); + soe_store_post((uint8_t)SOE_T_SAMPLE_RESUME, SOE_ELM_NONE, 0u); + soe_store_drain(); + CHECK(g_ff_obs.calls[FF_FAKE_OP_CLOSE] == g_ff_obs.calls[FF_FAKE_OP_OPEN], + "TC-SOESTORE-013 open/close 配对(脏句柄不跨调用残留)"); + + /* 014:短写判失败 -> unlink 重建 -> 重试成功 -> 恰好 64 字节 */ + reset_all(); + ff_fake_inject_write_short(SOE_PATH, SOE_REC_SIZE - 1u, 1); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 7u, 0x555Au); + soe_store_drain(); + CHECK((ff_fake_size(SOE_PATH) == SOE_REC_SIZE) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] >= 1) + && (g_ff_obs.last_write_bw == SOE_REC_SIZE) + && (g_soe_err_count == 0), + "TC-SOESTORE-014 FR_OK+短写被当失败处理(不校验字节数就会静默收下 63B)"); + + /* 015:连续**两轮** drain 都失败 -> 留痕只许 1 次(一轮 = 首写 + 自愈重试 + * = 2 次 f_write,所以注入 4 次零字节短写才够撑两轮) */ + reset_all(); + ff_fake_inject_write_short(SOE_PATH, 0u, 4); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); + soe_store_drain(); /* 第 1 轮失败:留痕 1 次,环不推进 */ + ok = (g_soe_err_count == 1) && (s_ring_tail == 0u) && (s_fault_reported == 1u); + soe_store_drain(); /* 第 2 轮仍失败:不得再留痕 */ + CHECK(ok && (g_soe_err_count == 1) && (s_ring_tail == 0u), + "TC-SOESTORE-015 写故障留痕去重(防串口与 selftest 被同一条故障刷屏)"); + + /* 015b:成功一次复位去重位,之后再失败才会重新留痕 */ + /* 015b:失败(留痕1) -> 恢复(去重位复位) -> 再失败(必须留痕2)。 + * 少了"成功复位"这一步,一次真机坏块就会让后续所有写故障永久静默。 */ + reset_all(); + ff_fake_inject_write_short(SOE_PATH, 0u, 2); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); + soe_store_drain(); /* 失败 -> 留痕 1 */ + soe_store_drain(); /* 注入耗尽 -> 成功 -> 复位 */ + ok = (g_soe_err_count == 1) && (s_fault_reported == 0u) && (s_ring_tail == 1u); + ff_fake_inject_write_short(SOE_PATH, 0u, 2); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 2u); + soe_store_drain(); /* 又失败 -> 留痕 2 */ + CHECK(ok && (g_soe_err_count == 2) && (s_fault_reported == 1u), + "TC-SOESTORE-015b 恢复后再失败必须重新留痕(漏复位 = 永久静默)"); + + /* 016:卷忙 -> -2:不碰 FatFs、不 release、环不推进 */ + reset_all(); + lock_busy_next(1); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); + soe_store_drain(); + ok = (s_lock_waits == 1) && (s_lock_releases == 0) && (s_lock_seq_err == 0); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0) + && (s_ring_tail == 0u) && (g_soe_err_count == 0), + "TC-SOESTORE-016 卷忙 = 顺延:零 FatFs、零 release、不计故障(喂狗点纪律)"); + + /* 017:写失败时环不推进 tail -> 下轮重试、既不丢也不重复 */ + reset_all(); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 5u, 0x77u); + ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1); + soe_store_drain(); /* open 失败 -> -1,tail 不动 */ + ok = (s_ring_tail == 0u) && (ff_fake_size(SOE_PATH) == 0u || + ff_fake_exists(SOE_PATH) == 0); + soe_store_drain(); /* 下轮成功 */ + CHECK(ok && (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE) + && read_rec(0u, &out) && (out.elm_id == 5u), + "TC-SOESTORE-017 环条目写失败不推进 tail:不丢、下轮也不重复落"); + + /* 018:队列条目已出队 -> 卷忙只丢当前一条,剩余留在队列(现状不对称) */ + reset_all(); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); + s_kernel_state = osKernelRunning; + soe_store_drain(); /* 建队列 */ + (void)osMessageQueueReset(s_queue); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 10u, 10u); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 11u, 11u); + opens_before = g_ff_obs.calls[FF_FAKE_OP_OPEN]; /* 前面那次 drain 已开过盘 */ + writes_before = g_ff_obs.calls[FF_FAKE_OP_WRITE]; + lock_busy_next(1); + soe_store_drain(); + CHECK((s_q_cnt == 1u) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_before) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == writes_before) + && (s_lock_waits >= 1) && (s_lock_seq_err == 0) && (s_q_get_calls >= 2), + "TC-SOESTORE-018 队列路径卷忙:只丢已出队那一条、本轮零 FatFs(与环路径不对称)"); + + /* 019:容量满 -> unlink 重建 + INFO 留痕,新文件恰好 64 字节 */ + reset_all(); + CHECK(ff_fake_mkfile_size(SOE_PATH, (uint32_t)SOE_STORE_MAX * SOE_REC_SIZE) == 0, + "TC-SOESTORE-019 前置:铺 1024 条(64KB)假记录"); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 9u, 0x99u); + soe_store_drain(); + CHECK((g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE) + && (g_soe_info_count == 1) && read_rec(0u, &out) && (out.elm_id == 9u), + "TC-SOESTORE-019 满则删除重建:新文件只剩本次这一条"); + + /* 020:丢弃留痕按差值上报一次,不刷屏 */ + reset_all(); + for (i = 0u; i < SOE_RING_MAX + 3u; i++) { + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i); /* 环 8 + 丢 3 */ + } + soe_store_drain(); /* 差值上报 1 条 */ + soe_store_drain(); /* 无新丢弃 -> 不再报 */ + CHECK((s_drop_cnt == 3u) && (s_drop_logged == 3u) && (g_soe_err_count == 1), + "TC-SOESTORE-020 丢弃留痕按差值上报一次(defaultTask 侧才是日志安全区)"); + + /* 021:队列创建失败 -> 条目滞留环、下轮再试、不崩 */ + /* 021:osMessageQueueNew 返回 NULL —— 生产代码里环在队列**之前**排空, + * 所以条目不丢;留痕一次,内核已运行时后续 post 仍走环(s_queue 为空), + * 下一轮再建队列。真机上 heap_4 见底就是这种形态。 */ + reset_all(); + s_kernel_state = osKernelRunning; + s_q_new_fail = 1; + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 4u, 4u); + soe_store_drain(); + ok = (s_queue == NULL) && (g_soe_err_count == 1) + && (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 5u, 5u); /* 队列没建成 -> 仍进环 */ + CHECK(ok && (s_q_cnt == 0u) && (s_ring_head == 2u), + "TC-SOESTORE-021 队列创建失败:环路径照常落盘不丢条目 + 留痕一次"); + s_q_new_fail = 0; + soe_store_drain(); + CHECK((s_queue != NULL) && (s_ring_tail == 2u) + && (ff_fake_size(SOE_PATH) == 2u * SOE_REC_SIZE) && (g_soe_err_count == 1), + "TC-SOESTORE-021b 堆恢复后队列补建、滞留条目全部落盘"); +} + +/* ===================== TC-SOESTORE-022..024:陷阱二 —— 名字/参数类错误码 ===================== */ + +static void tc_name_error_not_volume(void) +{ + uint32_t i; + int ok; + + printf("-- TC-SOESTORE-022..024 名字/参数类错误码严禁升级成卷操作 --\n"); + + /* 022:FR_INVALID_NAME 只让本条失败,**不得** unlink 别人、不得动目录 */ + reset_all(); + (void)ff_fake_mkfile("/log/run.log", NULL, 0u); /* 邻居文件(同卷) */ + ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 2); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); + soe_store_drain(); /* open 失败 -> -1 */ + ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0) + && (ff_fake_exists("/log") == 2) + && (ff_fake_exists("/log/run.log") == 1) + && (s_ring_tail == 0u) && (g_soe_err_count == 1); + CHECK(ok, "TC-SOESTORE-022 FR_INVALID_NAME -> 只留痕失败;不 unlink、目录与邻居文件不动"); + + /* 023:反复失败的自愈上限:每轮最多 unlink 一次,条目与卷都不被吞 */ + reset_all(); + (void)ff_fake_mkfile("/log/run.log", NULL, 0u); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 2u); + for (i = 0u; i < 5u; i++) { + ff_fake_inject_write_short(SOE_PATH, 0u, 2); /* 每轮两次短写都失败 */ + soe_store_drain(); + } + ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] <= (int)(5 * 1)) + && (ff_fake_exists("/log") == 2) && (ff_fake_exists("/log/run.log") == 1) + && (s_ring_tail == 0u); + ff_fake_inject_write_short(SOE_PATH, 0u, 0); /* 清掉不再需要的注入 */ + (void)ok; + CHECK(ok, "TC-SOESTORE-023 长跑反复失败:unlink 有界(<=1/轮),绝不升级为整卷动作"); + + /* 024:假卷严格 8.3 校验的保真自检 —— 生产路径必过、点开头必被拒 */ + reset_all(); + { + static FIL probe; + FRESULT fr_ok; + FRESULT fr_dot; + FRESULT fr_colon; + + fr_ok = f_open(&probe, SOE_PATH, FA_WRITE | FA_OPEN_APPEND); + (void)f_close(&probe); + fr_dot = f_open(&probe, "/log/.fschk", FA_WRITE | FA_CREATE_ALWAYS); + if (fr_dot == FR_OK) { + (void)f_close(&probe); + } + fr_colon = f_open(&probe, "1:/log/soe.log", FA_WRITE | FA_OPEN_APPEND); + if (fr_colon == FR_OK) { + (void)f_close(&probe); + } + CHECK((fr_ok == FR_OK) + && ((fr_dot == FR_INVALID_NAME) || (fr_dot == FR_INVALID_DRIVE)) + && (fr_colon == FR_INVALID_DRIVE), + "TC-SOESTORE-024 假件保真:/log/soe.log 过 8.3,.fschk 与坏盘符被拒"); + } + /* 生产路径本身合规的正向断言:改名成非 8.3 会让上面那条保真自检变红 */ + reset_all(); + soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); + soe_store_drain(); + CHECK((strcmp(g_ff_obs.last_open_path, "/LOG/SOE.LOG") == 0) + && (g_ff_obs.last_open_rc == (int)FR_OK), + "TC-SOESTORE-024b SOE_PATH 在严格 8.3 卷上确实打得开(改名=静默写失败的防线)"); +} + +/* ===================== 入口 ===================== */ + +int main(void) +{ + printf("=== SOE Store Tests ===\n"); + + tc_post_side(); + tc_drain_paths(); + tc_name_error_not_volume(); + + printf("\n=== SOE Store Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/soe_store/stub/cmsis_os.h b/test/host/soe_store/stub/cmsis_os.h new file mode 100644 index 00000000..c74b1843 --- /dev/null +++ b/test/host/soe_store/stub/cmsis_os.h @@ -0,0 +1,91 @@ +/* + * 模块名称:主机侧单测桩 - cmsis_os.h(soe_store 套件专用) + * 模块功能:给 App/log/soe_store.c 一个主机可编译的 CMSIS-RTOS V2 **消息队列 + + * 内核状态**最小子集。 + * + * ⚠ 仅供 test/host/soe_store/ 使用,不参与 MCU 构建。 + * 真实 Middlewares/.../CMSIS_RTOS_V2/cmsis_os.h 第 126/127/155 行会拉 + * FreeRTOS.h + task.h + cmsis_os2.h(整棵内核 + portmacro),PC 上编不动。 + * + * 保真点(逐条对照真实头,签名一字不改;改动真实头即编译期报错): + * - osMessageQueueId_t = void * (cmsis_os2.h:207) + * - osMessageQueueAttr_t 六字段同序同型 (cmsis_os2.h:272~279) + * ——soe_store.c 用 .name 指定成员初始化,字段名也必须一致 + * - osStatus_t osMessageQueuePut(id, const void *, uint8_t, uint32_t) (:689) + * - osStatus_t osMessageQueueGet (id, void *, uint8_t *, uint32_t) (:697) + * - osMessageQueueId_t osMessageQueueNew(uint32_t, uint32_t, const attr *)(:676) + * - osKernelState_t osKernelGetState(void) (cmsis_os2.h:298) + * - osOK = 0 / osErrorResource = -3 (cmsis_os2.h:177/180) + * - osKernelRunning = 2 (cmsis_os2.h:63) + * 本模块只用到内核状态的两个值,其余按真实枚举原样列出(少列会让"顺手改了 + * 状态判定"这类漂移编不出来)。 + * + * 队列行为由测试单元给**真 FIFO + 满/空语义**的强符号实现(不是恒返 osOK 的空壳): + * soe_store_post 的"队列满就丢弃并计数"、drain 的"取空即停"全靠这两条才测得出。 + * + * 创建日期:2026-10-24 + */ + +#ifndef CMSIS_OS_H +#define CMSIS_OS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 不透明句柄(真实 cmsis_os2.h:195~207 全是 void *) */ +typedef void *osMessageQueueId_t; +typedef void *osThreadId_t; +typedef void *osTimerId_t; +typedef void *osEventFlagsId_t; +typedef void *osMutexId_t; +typedef void *osSemaphoreId_t; + +/* 状态码(cmsis_os2.h:176~186) */ +typedef enum { + osOK = 0, + osError = -1, + osErrorTimeout = -2, + osErrorResource = -3, + osErrorParameter = -4, + osErrorNoMemory = -5, + osErrorISR = -6 +} osStatus_t; + +/* 内核状态(cmsis_os2.h:60~68) */ +typedef enum { + osKernelInactive = 0, + osKernelReady = 1, + osKernelRunning = 2, + osKernelLocked = 3, + osKernelSuspended = 4, + osKernelError = -1 +} osKernelState_t; + +/* 消息队列属性(cmsis_os2.h:272~279,字段名与顺序原样) */ +typedef struct { + const char *name; + uint32_t attr_bits; + void *cb_mem; + uint32_t cb_size; + void *mq_mem; + uint32_t mq_size; +} osMessageQueueAttr_t; + +osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size, + const osMessageQueueAttr_t *attr); +osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr, + uint8_t msg_prio, uint32_t timeout); +osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr, + uint8_t *msg_prio, uint32_t timeout); +uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id); +osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id); +osKernelState_t osKernelGetState(void); + +#ifdef __cplusplus +} +#endif + +#endif /* CMSIS_OS_H */ diff --git a/test/host/soe_store/stub/dbg_log.h b/test/host/soe_store/stub/dbg_log.h new file mode 100644 index 00000000..551a0182 --- /dev/null +++ b/test/host/soe_store/stub/dbg_log.h @@ -0,0 +1,30 @@ +/* + * 模块名称:主机侧单测桩 - dbg_log.h(soe_store 套件专用) + * 模块功能:DBG_* 换成计数,让"post 侧一条日志都不许打、drain 侧按差值只打一次" + * 成为可断言的行为。 + * + * ⚠ 仅供 test/host/soe_store/ 使用,不参与 MCU 构建。 + * + * 为什么这条计数在本模块**尤其**重要(App/log/soe_store.c 的函数头原话): + * soe_store_post() 由 protectTask 调用,该任务的静态最坏链 = 经 s_log_active_edge + * 的 console 尾链(872 B,B5 实测),post 里任何日志调用点都会再垫高一帧 + * (stack_report 实测 +320 B → FAIL)。所以"post 零 DBG"不是风格问题, + * 是栈预算硬约束;真实宏展开成 snprintf + 192 B 行缓冲,只有把宏换成计数 + * 才能在主机上把这条钉住。 + * + * 创建日期:2026-10-24 + */ + +#ifndef DBG_LOG_H +#define DBG_LOG_H + +/* 计数定义在 soe_store_test.c */ +extern int g_soe_err_count; +extern int g_soe_info_count; +extern int g_soe_debug_count; + +#define DBG_INFO(fmt, ...) do { (void)(fmt); g_soe_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_soe_debug_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_soe_err_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/soe_store/stub/main.h b/test/host/soe_store/stub/main.h new file mode 100644 index 00000000..25e84323 --- /dev/null +++ b/test/host/soe_store/stub/main.h @@ -0,0 +1,34 @@ +/* + * 模块名称:主机侧单测桩 - main.h(soe_store 套件专用) + * 模块功能:截断 App/util/sys_clock.h -> Drivers/BSP/SD2506/sd2506.h -> main.h + * -> Inc/main.h -> stm32f4xx_hal.h 这条 include 链。 + * + * ⚠ 仅供 test/host/soe_store/ 使用,不参与 MCU 构建。 + * 真实 Inc/main.h 第一行就 #include "stm32f4xx_hal.h"(整棵 HAL + CMSIS), + * PC 上编不动;本桩与 test/host/sys_clock/stub/main.h 同一招(该套件已验证 + * "真实 sys_clock.h + 真实 sd2506.h + 桩 main.h"这条组合可行),好处是 + * **sys_clock.h / sd2506.h 保持真实**:sys_clock_get() 的签名一改即编译期报错, + * 不必像 test/host/time_sync 那样连 sys_clock.h 一起桩掉。 + * + * 创建日期:2026-10-24 + */ + +#ifndef __MAIN_H +#define __MAIN_H + +#include /* size_t:真实 sys_clock.h 的 get_str 原型要用 */ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* HAL 毫秒时基(MCU 端 = TIM7 中断累加)。本模块不读它,声明在此只为让 + * 真实 main.h 的消费者(sd2506.h / sys_clock.h)在 PC 上也拿得到同一符号。 */ +uint32_t HAL_GetTick(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __MAIN_H */ diff --git a/test/host/sys_log_mcu/run_unit_test.py b/test/host/sys_log_mcu/run_unit_test.py new file mode 100644 index 00000000..3154b9d7 --- /dev/null +++ b/test/host/sys_log_mcu/run_unit_test.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +r"""运行/自检记录落盘后端(sys_log_mcu)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/sys_log_mcu/sys_log_mcu_test.c(以 #include 纳入**生产源** + App/log/sys_log_mcu.c,并纳入共享假卷 test/host/common/fatfs_fake.c),在 PC 上离线 + 跑 64B 定长记录格式、单条/批量两条写路径、syslog_drain 的 preboot+队列排空与 + "失败回投不丢条目"、N-24 的 8 条滑动读窗口 + 写者代数失效、挂载幂等与 + 上电/重启分类、sys_log_evidence 的 wdev.log 取证通道,失败即非 0 退出。 + 工具链与判定委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只用台架): + 1. **FA_OPEN_APPEND 不含 FA_WRITE**(存储架构 §10.1 第 11 条):漏写位时 f_open + 会成功、文件甚至被创建出来,只有 f_write 恒返 FR_DENIED —— 真机表现是 + "/log 写失败 x5/开机",2026-09-20 才定位到根因。假卷按真实 ff.c 判 fp->flag + 就能把这条做成必然失败的断言。 + 2. **f_write 簇分配失败 = FR_OK + 短写**(同一页第 11 条后半):只判 fr != FR_OK + 等于把丢记录当写成功;短写注入只有假卷给得出。 + 3. **挂载幂等**:syslog_mcu_mounted 曾在建 /log 失败后永久 no-op(运行期日志与自检 + 全部滞留),改成幂等 + 重试后又必须防"每 2s 刷一条同样错误" → 要注入 mkdir + 失败才测得到,且必须能数出"到底留痕几次"。 + 4. **读窗口 try 语义 / N-24 读侧**:窗口"命中即零 I/O"与"写者一落地就失效"是一枚硬币两面, + 只做前者会把陈旧读当正确、只做后者会退回逐条 open/seek/close 的旧实现。 + +桩的边界:ff.h / sys_log.h / soe_store.h / sys_clock.h / vol_lock.h 用**真实头**; + main.h 是桩,只为截断 sys_clock.h -> sd2506.h -> main.h -> stm32f4xx_hal.h; + cmsis_os.h / dbg_log.h 本目录桩。soe_store_drain() 用**计数桩**(本套只断言 + "每次 drain 各调一次"这个 B9 挂接点,SOE 落盘本身归 test/host/soe_store/)。 + FatFs 由 test/host/common/fatfs_fake.c 给 RAM 实现(**显式纳入,不用目录通配**)。 + sys_clock_* / vol_lock_* / osMessageQueue* 强符号桩写在本测试单元里。 + +用法(仓库根目录): + & '.venv\\Scripts\\python.exe' test/host/sys_log_mcu/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:docs/子系统设计/运行与自检记录设计说明.md §4.4/§4.5/§5.3、 +docs/架构设计/STM32F4-Base存储架构说明.md §10.1 第 10/11 条、 +docs/测试说明/主机单测覆盖率口径.md §9 P2 第 2 项 +创建日期:2026-10-24 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 必须最先(覆盖 cmsis_os.h / dbg_log.h / main.h),其余全用真实头。 +# 假卷 fatfs_fake.c 由测试单元 #include 纳入(同 ch395f_fake.c 惯例,显式列文件、不通配)。 +BUILD = TestBuild( + name="Sys Log Mcu Tests", + src=os.path.join(HERE, "sys_log_mcu_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "test", "host", "common"), + os.path.join(ROOT, "App", "log"), + os.path.join(ROOT, "App", "util"), + os.path.join(ROOT, "App", "fs"), + os.path.join(ROOT, "Drivers", "BSP"), + os.path.join(ROOT, "Drivers", "BSP", "SD2506"), + os.path.join(ROOT, "Lib", "FatFs"), + HERE, + ], + summary_marker="=== Sys Log Mcu Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/sys_log_mcu/stub/cmsis_os.h b/test/host/sys_log_mcu/stub/cmsis_os.h new file mode 100644 index 00000000..4c0d7272 --- /dev/null +++ b/test/host/sys_log_mcu/stub/cmsis_os.h @@ -0,0 +1,91 @@ +/* + * 模块名称:主机侧单测桩 - cmsis_os.h(sys_log_mcu 套件专用) + * 模块功能:给 App/log/sys_log_mcu.c(与 soe_store.c)一个/g主机可编译的 CMSIS-RTOS V2 **消息队列 + + * 内核状态**最小子集。 + * + * ⚠ 仅供 test/host/sys_log_mcu/ 使用,不参与 MCU 构建。 + * 真实 Middlewares/.../CMSIS_RTOS_V2/cmsis_os.h 第 126/127/155 行会拉 + * FreeRTOS.h + task.h + cmsis_os2.h(整棵内核 + portmacro),PC 上编不动。 + * + * 保真点(逐条对照真实头,签名一字不改;改动真实头即编译期报错): + * - osMessageQueueId_t = void * (cmsis_os2.h:207) + * - osMessageQueueAttr_t 六字段同序同型 (cmsis_os2.h:272~279) + * ——sys_log_mcu.c 用 .name 指定成员初始化,字段名也必须一致 + * - osStatus_t osMessageQueuePut(id, const void *, uint8_t, uint32_t) (:689) + * - osStatus_t osMessageQueueGet (id, void *, uint8_t *, uint32_t) (:697) + * - osMessageQueueId_t osMessageQueueNew(uint32_t, uint32_t, const attr *)(:676) + * - osKernelState_t osKernelGetState(void) (cmsis_os2.h:298) + * - osOK = 0 / osErrorResource = -3 (cmsis_os2.h:177/180) + * - osKernelRunning = 2 (cmsis_os2.h:63) + * 本模块只用到内核状态的两个值,其余按真实枚举原样列出(少列会让"顺手改了 + * 状态判定"这类漂移编不出来)。 + * + * 队列行为由测试单元给**真 FIFO + 满/空语义**的强符号实现(不是恒返 osOK 的空壳): + * soe_store_post 的"队列满就丢弃并计数"、drain 的"取空即停"全靠这两条才测得出。 + * + * 创建日期:2026-10-24 + */ + +#ifndef CMSIS_OS_H +#define CMSIS_OS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 不透明句柄(真实 cmsis_os2.h:195~207 全是 void *) */ +typedef void *osMessageQueueId_t; +typedef void *osThreadId_t; +typedef void *osTimerId_t; +typedef void *osEventFlagsId_t; +typedef void *osMutexId_t; +typedef void *osSemaphoreId_t; + +/* 状态码(cmsis_os2.h:176~186) */ +typedef enum { + osOK = 0, + osError = -1, + osErrorTimeout = -2, + osErrorResource = -3, + osErrorParameter = -4, + osErrorNoMemory = -5, + osErrorISR = -6 +} osStatus_t; + +/* 内核状态(cmsis_os2.h:60~68) */ +typedef enum { + osKernelInactive = 0, + osKernelReady = 1, + osKernelRunning = 2, + osKernelLocked = 3, + osKernelSuspended = 4, + osKernelError = -1 +} osKernelState_t; + +/* 消息队列属性(cmsis_os2.h:272~279,字段名与顺序原样) */ +typedef struct { + const char *name; + uint32_t attr_bits; + void *cb_mem; + uint32_t cb_size; + void *mq_mem; + uint32_t mq_size; +} osMessageQueueAttr_t; + +osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size, + const osMessageQueueAttr_t *attr); +osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr, + uint8_t msg_prio, uint32_t timeout); +osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr, + uint8_t *msg_prio, uint32_t timeout); +uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id); +osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id); +osKernelState_t osKernelGetState(void); + +#ifdef __cplusplus +} +#endif + +#endif /* CMSIS_OS_H */ diff --git a/test/host/sys_log_mcu/stub/dbg_log.h b/test/host/sys_log_mcu/stub/dbg_log.h new file mode 100644 index 00000000..24b724ac --- /dev/null +++ b/test/host/sys_log_mcu/stub/dbg_log.h @@ -0,0 +1,29 @@ +/* + * 模块名称:主机侧单测桩 - dbg_log.h(sys_log_mcu 套件专用) + * 模块功能:DBG_* 换成计数,让"写故障只报第一次(L2 去重)"与"挂载失败重试不刷屏" + * 成为可断言的行为。 + * + * ⚠ 仅供 test/host/sys_log_mcu/ 使用,不参与 MCU 构建。 + * + * 为什么本模块尤其需要计数而不是直通打印:sys_log_mcu.c 的 s_append_rec() 里 + * 有一条刻意设计的静默规则(源码注释)—— + * "L2 错误去重:持续故障只报第一次,恢复前静默(消除每次开机同一错误刷屏)" + * 直通 printf 只能让人眼看,"到底报了几次"必须可数才能钉住;而另一头, + * 挂载幂等里的"建 /log 失败日志去重(成功复位)"同样是次断言。 + * + * 创建日期:2026-10-24 + */ + +#ifndef DBG_LOG_H +#define DBG_LOG_H + +/* 计数定义在 sys_log_mcu_test.c */ +extern int g_sys_err_count; +extern int g_sys_info_count; +extern int g_sys_debug_count; + +#define DBG_INFO(fmt, ...) do { (void)(fmt); g_sys_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_sys_debug_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_sys_err_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/sys_log_mcu/stub/main.h b/test/host/sys_log_mcu/stub/main.h new file mode 100644 index 00000000..b1556178 --- /dev/null +++ b/test/host/sys_log_mcu/stub/main.h @@ -0,0 +1,34 @@ +/* + * 模块名称:主机侧单测桩 - main.h(sys_log_mcu 套件专用) + * 模块功能:截断 App/util/sys_clock.h -> Drivers/BSP/SD2506/sd2506.h -> main.h + * -> Inc/main.h -> stm32f4xx_hal.h 这条 include 链。 + * + * ⚠ 仅供 test/host/sys_log_mcu/ 使用,不参与 MCU 构建。 + * 真实 Inc/main.h 第一行就 #include "stm32f4xx_hal.h"(整棵 HAL + CMSIS), + * PC 上编不动;本桩与 test/host/sys_clock/stub/main.h 同一招(该套件已验证 + * "真实 sys_clock.h + 真实 sd2506.h + 桩 main.h"这条组合可行),好处是 + * **sys_clock.h / sd2506.h 保持真实**:sys_clock_get() 的签名一改即编译期报错, + * 不必像 test/host/time_sync 那样连 sys_clock.h 一起桩掉。 + * + * 创建日期:2026-10-24 + */ + +#ifndef __MAIN_H +#define __MAIN_H + +#include /* size_t:真实 sys_clock.h 的 get_str 原型要用 */ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* HAL 毫秒时基(MCU 端 = TIM7 中断累加)。本模块不读它,声明在此只为让 + * 真实 main.h 的消费者(sd2506.h / sys_clock.h)在 PC 上也拿得到同一符号。 */ +uint32_t HAL_GetTick(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __MAIN_H */ diff --git a/test/host/sys_log_mcu/sys_log_mcu_test.c b/test/host/sys_log_mcu/sys_log_mcu_test.c new file mode 100644 index 00000000..2007825e --- /dev/null +++ b/test/host/sys_log_mcu/sys_log_mcu_test.c @@ -0,0 +1,1534 @@ +/* + * 模块名称:运行/自检记录落盘后端(sys_log_mcu)主机侧单元测试(L0.5) + * 模块功能:在 PC 上离线验证 App/log/sys_log_mcu.c 的四族契约—— + * A 64B 定长记录格式(时间戳 / epoch ASCII / 描述三段 + 截断 + 占位) + * B 写侧(单条 s_append_rec、批量 s_append_many、N-24 的"一批一次 + * open/write/close"、容量轮转、L1 自愈、L2 留痕去重、卷锁 try) + * C 排空(syslog_drain 的 preboot + 队列两条来源、换文件分批、 + * 失败回投不丢条目、-3 退回慢路径) + * D 读侧(N-24 的 8 条滑动窗口 + 写者代数失效 + 越界/忙/短读处置) + * 外加 syslog_mcu_mounted 的 挂载幂等与上电/重启分类、sys_log_evidence + * 的 wdev.log 取证通道(8KB 护栏 + 第二条写必须在释放卷锁之后)。 + * + * 被测对象:真实源文件 App/log/sys_log_mcu.c(#include 原样纳入,不改生产代码)。 + * 假件与桩: + * test/host/common/fatfs_fake.{h,c} —— 假 FatFs 内存卷(真实 ff.h 声明) + * soe_rec.c 不纳入;soe_store_drain() 用**计数桩**(B9 挂接点本身由 + * test/host/soe_store/ 那套测,本套只断言"每次 drain 各调一次") + * stub/{cmsis_os,dbg_log,main}.h 本目录桩;sys_clock_* / vol_lock_* / + * osMessageQueue* 的可控强符号实现写在本文件里 + * + * 为什么要主机侧单测(而不是只用台架)——本文件的四条坑全是"日志看不出来"型的: + * 1. **FA_OPEN_APPEND 不含 FA_WRITE**(§10.1 第 11 条):漏写位时 f_open 会成功 + * (文件甚至被创建出来),只有 f_write 恒返 FR_DENIED(7)。真机表现是 + * "/log 写失败 x5/开机",2026-09-20 才定位到。→ 用假卷把这条做成必然失败的断言。 + * 2. **f_write 簇分配失败 = FR_OK + 短写**:只判 fr != FR_OK 就等于把丢记录当成功。 + * 3. **挂载幂等**:syslog_mcu_mounted 曾在建 /log 失败后永久 no-op(运行期日志与自检 + * 全部滞留),且 2s 重试会刷屏 → 幂等 + 留痕去重都要注入 mkdir 失败才测得到。 + * 4. **读窗口 try 语义 / N-24 读侧**:读窗口"命中即零 I/O"与"写者一落地就必须失效重填"是同一 + * 枚硬币的两面——只做前者会把陈旧读当正确,只做后者会退化成逐条 open/seek/close + * 的旧实现(UI 翻 20 条 = 20 次 FAT 往返)。两者都靠假卷的调用计数才断言得了。 + * + * 覆盖点(TC 号族 SYSLOG,形如 TC-SYSLOG-001,族内按功能分档;缺口在小节末尾如实登记): + * —— A 记录格式 —— + * TC-SYSLOG-001 布局:[0..18] 时间戳、[19]=' '、[20..29]=epoch "%010u"、[30]=' '、 + * [31..] 描述、尾部 NUL 填充到 64B + * TC-SYSLOG-002 描述超长截断到 SYSLOG_DESC_MAX,且不越 64B 边界 + * TC-SYSLOG-003 墙钟未建立:get_str 返 NULL -> 占位 "1970-01-01 08:00:00"; + * get() 返 UINT32_MAX -> epoch 字段落 "0000000000" + * TC-SYSLOG-004 单条路径与批量路径产出的同内容记录**逐字节相同**(N-24 抽出 + * s_format_rec 的全部理由:UI 按定长字段偏移解析) + * —— B 写侧 —— + * TC-SYSLOG-010 单条落盘成功 + 打开模式必须 FA_WRITE|FA_OPEN_APPEND;close 配对 + * TC-SYSLOG-011 只给 FA_OPEN_APPEND 的对照保真:f_write 恒 FR_DENIED(陷阱一) + * TC-SYSLOG-012 卷忙(try 失败)-> 返回 -2、零 FatFs、不计故障、不 release + * TC-SYSLOG-013 FR_OK + 短写 -> 判失败 -> unlink 重建重试 -> 恰好 64 字节 + * TC-SYSLOG-014 容量满(1024 条)-> 先 unlink 再写;新文件只剩本次一条 + * TC-SYSLOG-015 满且 unlink 失败(真错误码)-> 返回 -1 并释放卷锁 + * TC-SYSLOG-016 满且 unlink 返 FR_NO_FILE -> 视为无害,继续写 + * TC-SYSLOG-017 无效 id / 空指针守卫 -> -1 + * TC-SYSLOG-018 写成功 -> 读侧代数 s_rd_gen 自增(窗口失效钩子) + * TC-SYSLOG-019 连续两轮写失败只留痕一次;成功一次复位后再失败才再留痕 + * TC-SYSLOG-020 批量:8 条 = 一次 open + 一次 512B write + 一次 close + * TC-SYSLOG-021 批量入参守卫(id 越界 / n=0 / n>8 / 空指针)-> -1 + * TC-SYSLOG-022 剩余容量装不下一批 -> 返回 -3 且**不 unlink**(轮转只在慢路径) + * TC-SYSLOG-023 批量卷忙 -> -2;批量短写 -> unlink 重试成功 + * TC-SYSLOG-024 批量持续失败 -> -1 + 留痕去重(与单条同一判据) + * —— C 排空 —— + * TC-SYSLOG-030 卷未就绪 -> drain 直接返回,preboot 条目滞留不丢 + * TC-SYSLOG-031 首次 drain 惰性建队列(内核对象在 osKernelStart 之后) + * TC-SYSLOG-032 preboot 8 条同文件 -> 一批 512B 落盘 + preboot 计数清零 + * TC-SYSLOG-033 两个文件混排 -> 按文件分批,顺序不乱、条目不丢 + * TC-SYSLOG-034 换文件那条不消耗(preboot 下标回退 / 队列回投) + * TC-SYSLOG-035 卷忙 -> 整批回投队列,下一轮全部落盘(旧实现 -2 直接丢一条) + * TC-SYSLOG-036 队列满回投失败 -> "hold-over drop" 留痕,不崩 + * TC-SYSLOG-037 -3 -> 逐条慢路径,容量轮转只发生一次 + * TC-SYSLOG-038 慢路径中途写失败 -> 该条丢弃留痕 + 尾部回投 + * TC-SYSLOG-039 批量整体 -1 -> 整批丢弃留痕一次并结束本轮 + * TC-SYSLOG-040 drain 尾部各调一次 soe_store_drain()(B9 挂接点) + * TC-SYSLOG-041 post:内核未运行走 preboot、满则丢弃留痕;内核运行走队列、满则留痕; + * id 越界/NULL 描述静默丢弃 + * —— D 读侧 —— + * TC-SYSLOG-050 count = filesize/64(O(1));文件缺失/未挂载 -> 0 + * TC-SYSLOG-051 窗口命中 -> 第二次读**零 FatFs 调用** + * TC-SYSLOG-052 写者落地 -> 代数变化 -> 窗口重填并读到新内容 + * TC-SYSLOG-053 窗口按 8 条对齐:跨窗读 idx=8 -> 一次 open + lseek(512) + read(64) + * TC-SYSLOG-054 越界 idx -> -1;空文件 -> -1 + * TC-SYSLOG-055 读时卷忙 -> -1(读窗口 try 语义:UI 每 100ms 喂心跳,绝不阻塞) + * TC-SYSLOG-056 短读 -> 作废窗口(s_rd_win_cnt=0),不把半窗当有效命中 + * TC-SYSLOG-057 get_record:缓冲容量守卫 + 描述按 desc_len 截断 + * TC-SYSLOG-058 last_epoch:无记录 -1;有记录解析 10 位 ASCII + * —— E mounted / 上电重启分类 —— + * TC-SYSLOG-060 /log 不存在 -> f_mkdir 成功 + 幂等(第二次不再建目录) + * TC-SYSLOG-061 f_mkdir 失败 -> 不就绪、留痕去重、后续重试成功并复位 + * TC-SYSLOG-062 卷忙 -> 本轮顺延、不置就绪 + * TC-SYSLOG-063 末条 epoch 距 now < 600s -> "reboot";>=600s 或墙钟无效 -> "power on" + * TC-SYSLOG-064 f_stat /log 已存在 -> 不再调 f_mkdir(幂等靠 stat,不是靠状态位) + * —— F 取证通道 sys_log_evidence —— + * TC-SYSLOG-070 写 /log/wdev.log 变长行("ts 描述\\r\\n")+ 随后补一条 run.log + * TC-SYSLOG-071 卷锁顺序硬约束:第二条写在 release **之后**(非递归锁,反了自死锁) + * TC-SYSLOG-072 >8192B -> 先 unlink 再写(防长跑无界增长) + * TC-SYSLOG-073 卷忙 -> 返回 -1 且 wdev.log 零字节(不阻塞喂狗点) + * TC-SYSLOG-074 wdev 写失败 -> 返回 -1,但第二条 run.log 仍写(独立证据语义) + * —— H 补齐分支(自愈 / 锁后二次校验 / 回投 / 占位时标 / 空指针)—— + * TC-SYSLOG-090 批量 open 失败一次 -> unlink 重开 -> 整批落全、不留痕 + * TC-SYSLOG-091 批量 open 持续失败 -> -1 + 留痕一次 + 已放锁 + * TC-SYSLOG-092 取锁后二次 count 失败 -> 放锁返回 -1(漏放 = FTP 永久等不到卷) + * TC-SYSLOG-093 读时 f_open 失败 -> 放锁 -1;下轮恢复后窗口正常重填 + * TC-SYSLOG-094 无墙钟 -> wdev.log 落占位时标(取证行也必须可解析) + * TC-SYSLOG-095 wdev 短写 -> 返回 -1,第二条独立证据仍落 run.log + * TC-SYSLOG-096 队列创建失败 -> 一条都不排(不静默丢),堆恢复后排空并接上 SOE + * TC-SYSLOG-097 队列侧换文件那条原样回投 -> 三条全落、各文件内顺序不乱 + * TC-SYSLOG-098 desc_len=29 时截断分支不可达(守卫取值导致,登记死分支) + * TC-SYSLOG-099 p_rec / p_desc 空指针守卫(守卫在取锁之前) + * TC-SYSLOG-100 last_epoch 读失败 -> -1 且 *p_epoch 清零 + * —— G 陷阱二:名字/参数类错误码严禁升级成卷操作 —— + * TC-SYSLOG-080 短名违规分档保真:体>8 / 扩展>3 / 非法字符 / 以点开头(.fschk) + * 各自落 FR_INVALID_NAME;含 ':' 落 FR_INVALID_DRIVE + * TC-SYSLOG-081 生产路径全过严格 8.3:/log /run.log /selftest.log /wdev.log /soe.log + * TC-SYSLOG-082 FR_INVALID_NAME 反复失败 20 轮:unlink 次数有界、/log 目录与 + * 邻居文件一根毛没动(自愈只针对自己的文件) + * + * 覆盖率台账(--module sys_log_mcu 自量):App/log/sys_log_mcu.c 310/313 行 = + * 99.0%、函数 12/12。未覆盖 3 行:L628(下面第三条死分支)、L727~728 + * (sys_log_evidence 里 snprintf 返回 <=0 的分支——宿主 snprintf 不可能失败, + * 假件也不伪造返回值;为刷数字去伪造等于把断言写在假件规则上,宁可留空)。 + * + * 本套照出的现状(**只钉现状、不改生产代码**;三条都进交 Lead 的报告): + * + * 【疑似缺陷 A:卷忙时整批日志被静默丢弃】sys_log_mcu.c:509~537 的批量路径里 + * done 先被置成 n,只有 rc==-3 与 rc==-1 两个分支会改它;**rc==-2(卷忙)没有 + * 任何分支**,于是紧随其后的 "for (i = done; i < n; i++) osMessageQueuePut(...)" + * 一次都不执行 —— 整批(最多 8 条)既没落盘也没回投,且不计故障、不打日志。 + * 而同一段代码自己的注释写的是"rc == -2(卷忙):整批回投队列,静默顺延", + * 函数头 465~470 行与设计文档 §4.4 又都写明"**条目不丢**(旧实现 -2 卷忙会 + * 直接丢掉那一条)"。⇒ 现状比旧实现更糟:旧实现丢 1 条,现在一次丢 8 条。 + * 触发条件极常见:卷锁 v1.2 之后 ftpTask 传文件期间 defaultTask 每 2s 排空 + * 一次,每次撞上卷忙就吞一批。对照:逐条慢路径(-3 分支)遇 -2 会正确回投 + * 尾部 —— 同一个错误码在两条路径上行为相反(TC-SYSLOG-035 与 035b 成对钉住)。 + * + * 【疑似缺陷 B:rc==-1 的留痕文案与实际行为相反】sys_log_mcu.c:527~529 打 + * DBG_ERROR("drop %lu entries id=%d"),但该分支把 done 置 0 -> 整批**回投队列**, + * 一条都没丢(下一轮重试)。排障时按"日志说丢了"去找会走错方向;而真正会丢的 + * 是上面那个没有任何日志的 -2 分支。另外写故障持续时每轮 drain 都会重投重 + * 试一遍(s_append_many 内部的 L2 去重只压得住它自己那条,压不住这条)。 + * + * 【死分支:syslog_get_record 的截断不可达】sys_log_mcu.c:616 的守卫是 + * desc_len <= SYSLOG_DESC_MAX 直接 -1,而函数里 dl 恒 <= SYSLOG_DESC_MAX, + * 于是 627~628 的 "if (dl >= desc_len) dl = desc_len - 1" 永不成立。 + * 无害(宁可拒绝也不越界),但"传小缓冲就能拿到截断内容"的直觉是错的(TC-SYSLOG-098)。 + * + * 故意没测(写在这里,不留"看起来全覆盖"的错觉): + * - 真跨任务并发:读窗口用"对齐 32 位读写在 ARM 上原子"免锁,主机 x64 + 单线程 + * 编不出真竞态;本套只断言单写者语义与代数失效逻辑本身。 + * - f_close 内部 sync_fs -> disk_ioctl(CTRL_SYNC) -> ftl_flush_cache() 的掉电落盘 + * 链:属 nand_port/GD5F 层,假卷里没有 FTL,"关文件即落盘"要靠真机 + N-22 台账。 + * - snprintf 失败(n<=0)分支:宿主 snprintf 不会失败,且假件不提供注入—— + * 为了刷覆盖率去伪造返回值等于把断言写在假件规则上,宁可留空。 + * - DBG 文本内容:桩只计数(真实宏展开成 snprintf + 192B 行缓冲)。 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-24(P2 主机单测覆盖率批次,task-2) + */ + +#include +#include +#include +#include +#include + +#include "ff.h" +#include "fatfs_fake.h" +#include "cmsis_os.h" +#include "dbg_log.h" +#include "sys_clock.h" +#include "vol_lock.h" +#include "sys_log.h" +#include "soe_store.h" + +/* ===================== 断言框架 ===================== */ + +static int s_total; +static int s_passed; +static int s_failed; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* 路径常量:与生产 s_paths[] / sys_log_evidence() 里的字面量一致(写死在测试里 + * 是为了"改生产路径名必须同时改测试",改名静默失效正是 §10.1 第 10 条的老账) */ +#define P_RUN "/log/run.log" +#define P_SELFTEST "/log/selftest.log" +#define P_WDEV "/log/wdev.log" + +/* ===================== 假件与生产源码纳入 ===================== */ + +#include "fatfs_fake.c" +#include "sys_log_mcu.c" + +/* ===================== dbg_log 桩计数 ===================== */ + +int g_sys_err_count; +int g_sys_info_count; +int g_sys_debug_count; + +/* ===================== soe_store 挂接点计数桩 ===================== */ + +static int s_soe_drain_calls; + +void soe_store_drain(void) +{ + if (s_soe_drain_calls++ > 20000) { + (void)fprintf(stderr, "[FATAL] soe_store_drain guard tripped\n"); + exit(2); + } +} + +void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) +{ + (void)type; (void)elm_id; (void)value; +} + +uint32_t soe_store_dropped(void) +{ + return 0u; +} + +/* ===================== 假墙钟 ===================== */ + +static uint32_t s_now = 1760234567u; +static int s_clock_ok = 1; + +uint32_t sys_clock_get(void) +{ + return (s_clock_ok != 0) ? s_now : UINT32_MAX; +} + +char *sys_clock_get_str(char *buf, size_t buf_size) +{ + struct tm gmt; + time_t t; + size_t need = SYSLOG_TS_LEN + 1u; + + if ((s_clock_ok == 0) || (buf == NULL) || (buf_size < need)) { + return NULL; + } + t = (time_t)s_now; +#if defined(_WIN32) + if (gmtime_s(&gmt, &t) != 0) { + return NULL; + } +#else + gmt = *gmtime(&t); +#endif + (void)strftime(buf, buf_size, "%Y-%m-%d %H:%M:%S", &gmt); + return buf; +} + +int sys_clock_init(void) { return 0; } +int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *p_time) +{ + (void)unix_secs; (void)p_time; return 0; +} +int sys_clock_set_unix(uint32_t unix_secs) { (void)unix_secs; return 0; } +int sys_clock_get_time(sd2506_time_t *p_time) { (void)p_time; return -1; } +uint32_t HAL_GetTick(void) { return 0u; } + +/* ===================== 假卷锁(try 语义 + 配对检查) ===================== */ + +static int s_lock_busy_left; +static int s_lock_busy_skip; /* 先放行 N 次再开始忙(同一次 drain 里多把锁的场景) */ +static int s_lock_waits; +static int s_lock_releases; +static int s_lock_held; +static int s_lock_seq_err; + +static void lock_busy_next(int n) +{ + s_lock_busy_skip = 0; + s_lock_busy_left = n; +} + +/* 前 skip 次 vol_lock_wait 正常放行,接下来 busy 次返回"忙"。 + * 需要它是因为同一次 syslog_drain() 里会连拿两把锁:批量路径那把必须先成功, + * 才谈得上"退回逐条慢路径后第二把忙"。 */ +static void lock_busy_after(int skip, int busy) +{ + s_lock_busy_skip = skip; + s_lock_busy_left = busy; +} + +static void locks_clear(void) +{ + s_lock_busy_left = 0; + s_lock_busy_skip = 0; + s_lock_waits = 0; + s_lock_releases = 0; + s_lock_held = 0; + s_lock_seq_err = 0; +} + +int vol_lock_init(void) { return 0; } + +int vol_lock_wait(uint32_t timeout_ms) +{ + int r; + + (void)timeout_ms; + if (s_lock_waits > 20000) { + (void)fprintf(stderr, "[FATAL] vol_lock_wait guard tripped\n"); + exit(2); + } + if (s_lock_busy_skip > 0) { + s_lock_busy_skip--; + r = 1; + } else { + r = (s_lock_busy_left > 0) ? 0 : 1; + if (s_lock_busy_left > 0) { + s_lock_busy_left--; + } + } + s_lock_waits++; + if (r != 0) { + s_lock_held = 1; + } + return r; +} + +void vol_lock_release(void) +{ + s_lock_releases++; + if (s_lock_held == 0) { + s_lock_seq_err++; + } + s_lock_held = 0; +} + +/* ===================== 假消息队列(真 FIFO + 满/空语义) ===================== */ + +#define MQ_SLOTS 16u +#define MQ_MAXMSG 24u /* sizeof(syslog_entry_t) 在 x64 = 16,留余量 */ + +typedef struct { + int used; + uint8_t data[MQ_MAXMSG]; +} mq_slot_t; + +static mq_slot_t s_q_pool[MQ_SLOTS]; +static uint32_t s_q_head; +static uint32_t s_q_cnt; +static uint32_t s_q_cap; +static uint32_t s_q_msgsz; +static int s_q_new_calls; +static int s_q_new_fail; +static int s_q_get_calls; +static int s_q_put_calls; + +static void queue_clear(void) +{ + (void)memset(s_q_pool, 0, sizeof(s_q_pool)); + s_q_head = 0u; + s_q_cnt = 0u; + s_q_cap = SYSLOG_QUEUE_MAX; + s_q_msgsz = (uint32_t)sizeof(syslog_entry_t); + s_q_new_calls = 0; + s_q_new_fail = 0; + s_q_get_calls = 0; + s_q_put_calls = 0; +} + +osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size, + const osMessageQueueAttr_t *attr) +{ + (void)attr; + s_q_new_calls++; + if (s_q_new_fail != 0) { + return NULL; + } + s_q_cap = msg_count; + s_q_msgsz = msg_size; + s_q_head = 0u; + s_q_cnt = 0u; + return (osMessageQueueId_t)&s_q_pool[0]; +} + +osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr, + uint8_t msg_prio, uint32_t timeout) +{ + uint32_t tail; + + (void)mq_id; (void)msg_prio; (void)timeout; + if (s_q_put_calls++ > 20000) { + (void)fprintf(stderr, "[FATAL] osMessageQueuePut guard tripped\n"); + exit(2); + } + if ((msg_ptr == NULL) || (s_q_cnt >= s_q_cap) || (s_q_msgsz > MQ_MAXMSG)) { + return osErrorResource; + } + tail = (s_q_head + s_q_cnt) % MQ_SLOTS; + (void)memcpy(s_q_pool[tail].data, msg_ptr, s_q_msgsz); + s_q_pool[tail].used = 1; + s_q_cnt++; + return osOK; +} + +osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr, + uint8_t *msg_prio, uint32_t timeout) +{ + (void)mq_id; (void)msg_prio; (void)timeout; + if (s_q_get_calls++ > 20000) { + (void)fprintf(stderr, "[FATAL] osMessageQueueGet guard tripped\n"); + exit(2); + } + if ((msg_ptr == NULL) || (s_q_cnt == 0u)) { + return osErrorResource; + } + (void)memcpy(msg_ptr, s_q_pool[s_q_head].data, s_q_msgsz); + s_q_pool[s_q_head].used = 0; + s_q_head = (s_q_head + 1u) % MQ_SLOTS; + s_q_cnt--; + return osOK; +} + +uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id) +{ + (void)mq_id; + return s_q_cnt; +} + +osStatus_t osMessageQueueReset(osMessageQueueId_t mq_id) +{ + (void)mq_id; + s_q_head = 0u; + s_q_cnt = 0u; + return osOK; +} + +/* ===================== 内核状态开关 ===================== */ + +static osKernelState_t s_kernel_state = osKernelInactive; + +osKernelState_t osKernelGetState(void) +{ + return s_kernel_state; +} + +/* ===================== 复位与铺状态 ===================== */ + +static void reset_all(void) +{ + ff_fake_reset(); + locks_clear(); + queue_clear(); + s_now = 1760234567u; + s_clock_ok = 1; + s_kernel_state = osKernelInactive; + g_sys_err_count = 0; + g_sys_info_count = 0; + g_sys_debug_count = 0; + s_soe_drain_calls = 0; + /* 生产模块的 static 残留:逐位显式清(漏一个就会"上一场景的尾巴"咬下一场景) */ + (void)memset(s_preboot, 0, sizeof(s_preboot)); + s_preboot_cnt = 0u; + (void)memset(s_batch_src, 0, sizeof(s_batch_src)); + s_queue = NULL; + s_fs_ready = 0u; + s_mount_fail_reported = 0u; + (void)memset(s_rd_win, 0, sizeof(s_rd_win)); + s_rd_win_id = -1; + s_rd_win_base = 0u; + s_rd_win_cnt = 0u; + s_rd_win_gen = 0u; + s_rd_gen[SYSLOG_RUN] = 0u; + s_rd_gen[SYSLOG_SELFTEST] = 0u; + /* 卷状态:/log 由 defaultTask 挂载时建好;本套多数场景直接假定已建, + * "没建 -> 补建" 那条路径单独用 TC-SYSLOG-060/061 测 */ + (void)ff_fake_mkdir_direct("/log"); +} + +/* 就绪但**不**走 mounted()(避免上电那条记录混进断言) */ +static void ready_only(void) +{ + s_fs_ready = 1u; +} + +/* 用生产 API 落一条记录(时间戳/epoch 由假墙钟决定) */ +static void put_rec(syslog_id_t id, const char *desc) +{ + ready_only(); + (void)s_append(id, desc); +} + +/* 从假卷直读第 i 条原始记录 */ +static int raw_at(const char *path, uint32_t i, uint8_t *p_out) +{ + return (ff_fake_fetch(path, p_out, i * SYSLOG_REC_SIZE, SYSLOG_REC_SIZE) + == SYSLOG_REC_SIZE) ? 1 : 0; +} + +/* ===================== A 记录格式 ===================== */ + +static void tc_format(void) +{ + uint8_t rec[SYSLOG_REC_SIZE]; + uint8_t b[SYSLOG_REC_SIZE]; + int ok; + uint32_t i; + + printf("-- TC-SYSLOG-001..004 记录格式(64B 定长)--\n"); + + /* 001 布局 */ + reset_all(); + s_now = 1760234567u; + s_clock_ok = 1; + s_format_rec(SYSLOG_RUN, "hello", rec); + ok = (memcmp(rec, "2025-10-12 02:02:47", SYSLOG_TS_LEN) == 0) + && (rec[SYSLOG_TS_LEN] == ' ') + && (memcmp(rec + SYSLOG_EPOCH_OFFS, "1760234567", SYSLOG_EPOCH_LEN) == 0) + && (rec[SYSLOG_EPOCH_OFFS - 1u] == ' ') + && (memcmp(rec + SYSLOG_DESC_OFFS, "hello", 5u) == 0) + && (rec[SYSLOG_DESC_OFFS + 5u] == '\0'); + CHECK(ok, "TC-SYSLOG-001 [0..18]ts / [19]空格 / [20..29]epoch / [30]空格 / [31..]desc"); + ok = 1; + for (i = SYSLOG_DESC_OFFS + 6u; i < SYSLOG_REC_SIZE; i++) { + if (rec[i] != 0u) { + ok = 0; + } + } + CHECK(ok, "TC-SYSLOG-001b 尾部 NUL 填充到 64B(定长解析的地基)"); + + /* 002 描述截断 */ + reset_all(); + { + char big[SYSLOG_DESC_MAX + 16u]; + (void)memset(big, 'A', sizeof(big) - 1u); + big[sizeof(big) - 1u] = '\0'; + s_format_rec(SYSLOG_RUN, big, rec); + ok = 1; + for (i = 0u; i < SYSLOG_DESC_MAX; i++) { + if (rec[SYSLOG_DESC_OFFS + i] != (uint8_t)'A') { + ok = 0; + } + } + CHECK(ok && (rec[SYSLOG_DESC_OFFS + SYSLOG_DESC_MAX] == 0u) + && (rec[SYSLOG_REC_SIZE - 1u] == 0u), + "TC-SYSLOG-002 超长描述截到 28B,不越 64B 边界"); + } + + /* 003 墙钟未建立 */ + reset_all(); + s_clock_ok = 0; + s_format_rec(SYSLOG_RUN, "x", rec); + CHECK((memcmp(rec, "1970-01-01 08:00:00", SYSLOG_TS_LEN) == 0) + && (memcmp(rec + SYSLOG_EPOCH_OFFS, "0000000000", SYSLOG_EPOCH_LEN) == 0), + "TC-SYSLOG-003 无墙钟 -> 占位时间戳 + epoch 全 0(不造假时标)"); + + /* 004 单条 vs 批量逐字节相同 */ + reset_all(); + s_now = 1700000000u; + s_format_rec(SYSLOG_RUN, "same-content", rec); + ready_only(); + (void)memset(b, 0, sizeof(b)); + s_format_rec(SYSLOG_RUN, "same-content", b); + CHECK(memcmp(rec, b, SYSLOG_REC_SIZE) == 0, + "TC-SYSLOG-004 同一输入两次格式化逐字节一致(UI 定长偏移解析的前提)"); + reset_all(); + s_now = 1700000000u; + ready_only(); + s_format_rec(SYSLOG_RUN, "ab", rec); + { + uint8_t batch[SYSLOG_REC_SIZE * 2u]; + (void)memset(batch, 0, sizeof(batch)); + s_format_rec(SYSLOG_RUN, "ab", batch); + s_format_rec(SYSLOG_RUN, "ab", batch + SYSLOG_REC_SIZE); + CHECK((memcmp(rec, batch, SYSLOG_REC_SIZE) == 0) + && (memcmp(rec, batch + SYSLOG_REC_SIZE, SYSLOG_REC_SIZE) == 0), + "TC-SYSLOG-004b 批量路径复用同一 s_format_rec(两条都等于单条)"); + } +} + +/* ===================== B 写侧 ===================== */ + +static void tc_write_single(void) +{ + uint8_t rec[SYSLOG_REC_SIZE]; + int ok; + + printf("-- TC-SYSLOG-010..019 单条写路径 --\n"); + + /* 010 成功 + 打开模式 */ + reset_all(); + ready_only(); + ok = (s_append(SYSLOG_RUN, "boot") == 0) + && (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND)) + && (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 1) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1) + && (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1) + && (s_lock_releases == 1) && (s_lock_seq_err == 0) + && (g_sys_err_count == 0); + CHECK(ok, "TC-SYSLOG-010 单条落盘成功:模式带写位 + open/write/close 各一次 + 放锁"); + + /* 011 陷阱一保真对照 */ + reset_all(); + { + static FIL probe; + UINT bw = 99u; + FRESULT fr1; + FRESULT fr2; + + fr1 = f_open(&probe, P_RUN, FA_OPEN_APPEND); + fr2 = f_write(&probe, rec, SYSLOG_REC_SIZE, &bw); + (void)f_close(&probe); + CHECK((fr1 == FR_OK) && (fr2 == FR_DENIED) && (bw == 0u), + "TC-SYSLOG-011 只给 FA_OPEN_APPEND -> f_write 恒 FR_DENIED(2026-09-20 真根因)"); + } + + /* 012 卷忙 */ + reset_all(); + ready_only(); + lock_busy_next(1); + ok = (s_append(SYSLOG_RUN, "x") == -2); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0) + && (s_lock_releases == 0) && (s_lock_seq_err == 0) && (g_sys_err_count == 0), + "TC-SYSLOG-012 卷忙 = 顺延:不碰盘、不 release、不计故障(喂狗点绝不阻塞)"); + + /* 013 短写 -> 自愈 */ + reset_all(); + ready_only(); + ff_fake_inject_write_short(P_RUN, SYSLOG_REC_SIZE - 1u, 1); + ok = (s_append(SYSLOG_RUN, "short") == 0); + CHECK(ok && (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (g_ff_obs.last_write_bw == SYSLOG_REC_SIZE), + "TC-SYSLOG-013 FR_OK+短写 63B 被判失败 -> unlink 重建 -> 恰好 64 字节"); + + /* 014 容量满 -> 删除重建 */ + reset_all(); + ready_only(); + (void)ff_fake_mkfile_size(P_RUN, (uint32_t)SYSLOG_RUN_MAX * SYSLOG_REC_SIZE); + ok = (s_append(SYSLOG_RUN, "wrap") == 0); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE) + && (syslog_count(SYSLOG_RUN) == 1u), + "TC-SYSLOG-014 满 1024 条 -> 先 unlink 再写,新文件只剩本次一条"); + + /* 015 满 + unlink 真失败 */ + reset_all(); + ready_only(); + (void)ff_fake_mkfile_size(P_RUN, (uint32_t)SYSLOG_RUN_MAX * SYSLOG_REC_SIZE); + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 1); + ok = (s_append(SYSLOG_RUN, "x") == -1); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) + && (s_lock_releases == 1) && (s_lock_seq_err == 0), + "TC-SYSLOG-015 容量轮转时 unlink 失败 -> 返回 -1 且**已释放卷锁**(漏放 = FTP 永久拿不到卷)"); + + /* 016 满 + unlink 返 FR_NO_FILE 无害 */ + reset_all(); + ready_only(); + (void)ff_fake_mkfile_size(P_RUN, (uint32_t)SYSLOG_RUN_MAX * SYSLOG_REC_SIZE); + /* 注入只替换返回码、不真的删条目(真机上 FR_NO_FILE 的含义恰恰是"条目已不在"), + * 所以这里钉的是**处置**:该码不进错误分支、流程继续把这条写下去。 + * 文件大小断言留给 TC-SYSLOG-014(那里 unlink 是真删)。 */ + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_NO_FILE, 1); + ok = (s_append(SYSLOG_RUN, "gone") == 0); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1) && (g_sys_err_count == 0), + "TC-SYSLOG-016 unlink 返 FR_NO_FILE 被当无害:不进错误分支、不留痕、继续写"); + + /* 017 入参守卫 */ + reset_all(); + ready_only(); + ok = (s_append((syslog_id_t)SYSLOG_COUNT, "x") == -1) + && (s_append_rec(SYSLOG_RUN, NULL) == -1) + && (s_append(SYSLOG_RUN, NULL) == -1) + && (s_append_rec((syslog_id_t)9u, rec) == -1); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), + "TC-SYSLOG-017 id 越界 / 空 rec / 空 desc -> -1 且不碰卷锁与盘"); + + /* 018 写成功 -> 读侧代数自增 */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "a"); + ok = (s_rd_gen[SYSLOG_RUN] == 1u) && (s_rd_gen[SYSLOG_SELFTEST] == 0u); + (void)s_append(SYSLOG_SELFTEST, "b"); + CHECK(ok && (s_rd_gen[SYSLOG_SELFTEST] == 1u), + "TC-SYSLOG-018 每次落盘只自增本 id 的代数(另一文件的窗口不被误失效)"); + + /* 019 留痕去重 */ + reset_all(); + ready_only(); + ff_fake_inject_write_short(P_RUN, 0u, 4); + ok = (s_append(SYSLOG_RUN, "e1") == -1); + (void)s_append(SYSLOG_RUN, "e2"); /* 第二轮仍失败 */ + ok = ok && (g_sys_err_count == 1); + (void)s_append(SYSLOG_RUN, "ok"); /* 注入耗尽 -> 成功 -> 复位 */ + ff_fake_inject_write_short(P_RUN, 0u, 2); + (void)s_append(SYSLOG_RUN, "e3"); /* 再失败 -> 再留痕 */ + CHECK(ok && (g_sys_err_count == 2), + "TC-SYSLOG-019 持续故障只报第一次;恢复后再失败必须重新留痕"); +} + +static void tc_write_many(void) +{ + uint8_t batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE]; + uint32_t i; + int ok; + + printf("-- TC-SYSLOG-020..024 批量写路径(N-24)--\n"); + + /* 020 8 条一次 I/O */ + reset_all(); + ready_only(); + (void)memset(batch, 0x5A, sizeof(batch)); + ok = (s_append_many(SYSLOG_RUN, batch, SYSLOG_BATCH_MAX) == 0); + CHECK(ok && (ff_fake_size(P_RUN) == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 1) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1) + && (g_ff_obs.last_write_btw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE) + && (g_ff_obs.last_write_bw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE) + && (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1), + "TC-SYSLOG-020 一批 8 条 = 一次 open + 一次 512B write + 一次 close(旧实现 8 轮)"); + + /* 021 入参守卫 */ + reset_all(); + ready_only(); + ok = (s_append_many(SYSLOG_RUN, batch, 0u) == -1) + && (s_append_many(SYSLOG_RUN, batch, SYSLOG_BATCH_MAX + 1u) == -1) + && (s_append_many(SYSLOG_RUN, NULL, 1u) == -1) + && (s_append_many((syslog_id_t)SYSLOG_COUNT, batch, 1u) == -1); + CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), + "TC-SYSLOG-021 批量入参守卫在**取卷锁之前**(越界 n 不占用卷)"); + + /* 022 容量不足 -> -3 且不 unlink */ + reset_all(); + ready_only(); + (void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 3u) * SYSLOG_REC_SIZE); + ok = (s_append_many(SYSLOG_RUN, batch, SYSLOG_BATCH_MAX) == -3); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) + && (s_lock_releases == 1) && (s_lock_seq_err == 0) + && (ff_fake_size(P_RUN) == ((uint32_t)SYSLOG_RUN_MAX - 3u) * SYSLOG_REC_SIZE), + "TC-SYSLOG-022 装不下一批 -> -3 交回慢路径;批量路径**绝不**自己 unlink 重建"); + + /* 023 批量卷忙 / 短写自愈 */ + reset_all(); + ready_only(); + lock_busy_next(1); + ok = (s_append_many(SYSLOG_RUN, batch, 4u) == -2); + ff_fake_inject_write_short(P_RUN, 100u, 1); /* 本批 4 条 = 256 B,100 才算真短 */ + ok = ok && (s_append_many(SYSLOG_RUN, batch, 4u) == 0); + CHECK(ok && (ff_fake_size(P_RUN) == 4u * SYSLOG_REC_SIZE) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (g_sys_err_count == 0), + "TC-SYSLOG-023 批量卷忙 -> -2;短写(256 只回 100)-> 自愈重建后整批落全"); + + /* 024 批量持续失败 + 去重 */ + reset_all(); + ready_only(); + ff_fake_inject_write_short(P_RUN, 0u, 4); + ok = (s_append_many(SYSLOG_RUN, batch, 2u) == -1); + (void)s_append_many(SYSLOG_RUN, batch, 2u); + CHECK(ok && (g_sys_err_count == 1) && (s_rd_gen[SYSLOG_RUN] == 2u), + "TC-SYSLOG-024 批量持续失败留痕去重;代数照样自增(宁多失效一轮也不读脏窗)"); + (void)i; +} + +/* ===================== C 排空 ===================== */ + +static void tc_drain(void) +{ + uint8_t rec[SYSLOG_REC_SIZE]; + uint32_t i; + int ok; + + printf("-- TC-SYSLOG-030..041 syslog_drain 排空 --\n"); + + /* 030 卷未就绪:条目滞留 preboot,drain 直接返回。 + * 注意两条容易被读漏的事实: + * a) 队列还没建时(s_queue == NULL)即使内核已运行,post 也走 preboot 数组 + * (syslog_post 的条件是 s_queue != NULL && 内核运行,两个条件都要满足); + * b) drain 里"惰性建队列"在 s_fs_ready 判定**之前**,所以卷没就绪也会先把 + * 队列建出来 —— 于是就绪前的条目一律滞留 preboot、就绪后一次排空。 */ + reset_all(); + s_kernel_state = osKernelRunning; + syslog_post(SYSLOG_RUN, "early"); + ok = (s_preboot_cnt == 1u) && (s_q_cnt == 0u) && (s_queue == NULL); + syslog_drain(); + CHECK(ok && (s_q_new_calls == 1) && (s_preboot_cnt == 1u) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_soe_drain_calls == 0), + "TC-SYSLOG-030 卷未就绪 -> 队列先建成、条目滞留 preboot、不落盘也不调 SOE"); + + /* 031 惰性建队列 */ + reset_all(); + ready_only(); + ok = (s_queue == NULL); + syslog_drain(); + CHECK(ok && (s_queue != NULL) && (s_q_new_calls == 1) && (s_q_msgsz == sizeof(syslog_entry_t)), + "TC-SYSLOG-031 队列在**首次 drain**(defaultTask 上下文)才创建(内核前建对象 = 启动失败)"); + + /* 032 preboot 8 条一批落 */ + reset_all(); + ready_only(); + for (i = 0u; i < SYSLOG_QUEUE_MAX; i++) { + syslog_post(SYSLOG_RUN, "pre"); + } + CHECK(s_preboot_cnt == SYSLOG_QUEUE_MAX, "TC-SYSLOG-032a 内核未运行:8 条全进 preboot 数组"); + syslog_drain(); + ok = (s_preboot_cnt == 0u) && (syslog_count(SYSLOG_RUN) == SYSLOG_BATCH_MAX) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1) + && (g_ff_obs.last_write_btw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE); + CHECK(ok, "TC-SYSLOG-032b preboot 8 条同文件 -> 一次 512B 合并写(数组下标看而不删)"); + + /* 033 + 034 两文件混排:换文件那条不消耗 */ + reset_all(); + ready_only(); + syslog_post(SYSLOG_RUN, "r1"); + syslog_post(SYSLOG_RUN, "r2"); + syslog_post(SYSLOG_SELFTEST, "s1"); + syslog_post(SYSLOG_RUN, "r3"); + syslog_drain(); + ok = (syslog_count(SYSLOG_RUN) == 3u) && (syslog_count(SYSLOG_SELFTEST) == 1u); + if (ok) { + ok = raw_at(P_RUN, 0u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "r1", 2u) == 0) + && raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "r2", 2u) == 0) + && raw_at(P_RUN, 2u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "r3", 2u) == 0) + && raw_at(P_SELFTEST, 0u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "s1", 2u) == 0); + } + /* selftest 插在 run 中间,于是 run.log 被切成"r1r2"与"r3"两批: + * 合计 3 次 open/write/close,而不是 2 次 —— 换文件处即分批处。 */ + CHECK(ok && (s_preboot_cnt == 0u) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 3), + "TC-SYSLOG-033/034 换文件即分批(run 被切成两批 = 共 3 轮 I/O)+ 顺序不乱"); + + /* 035【现状桩 = 疑似生产缺陷,见文件头"本套照出的现状"】 + * sys_log_mcu.c:509~537 的批量路径里 done 先被置成 n,只有 -3 / -1 分支会 + * 把它改回 0;**rc == -2(卷忙)没有任何分支**,于是紧随其后的 + * "for (i = done; i < n; i++) osMessageQueuePut(...)" 一次都不执行 —— + * 整批(最多 8 条)**静默丢失**,而同一段代码上方的注释写的是 + * "rc == -2(卷忙):整批回投队列,静默顺延",设计文档 §4.4 也写明 + * "队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时回投队列—— + * 条目不丢(旧实现 -2 卷忙会直接丢掉那一条)"。 + * ⇒ 本条按**当前实现**断言(丢了就是丢了),并显式命名为 DEFECT, + * 生产侧一旦按注释补上 done=0,这条会红,提醒同步翻转成"不丢"的正向断言。 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); /* 建队列 */ + for (i = 0u; i < 3u; i++) { + syslog_post(SYSLOG_RUN, "hold"); + } + lock_busy_next(1); + syslog_drain(); /* 批量 -2 */ + CHECK((syslog_count(SYSLOG_RUN) == 0u) && (s_q_cnt == 0u) + && (g_sys_err_count == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), + "TC-SYSLOG-035[DEFECT 现状] 卷忙 -> 整批 3 条既没落盘也没回投(静默丢失)"); + + /* 035b 对照组:慢路径(-3 分支内)遇到 -2 时**会**把尾部回投 —— 同一卷忙 + * 在两条路径上行为不一致,这就是"批量路径漏了一个分支"最直接的证据 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); + for (i = 0u; i < 3u; i++) { + syslog_post(SYSLOG_RUN, "slow-hold"); + } + /* 让批量路径先返回 -3(容量只剩 1 条),从而进入逐条慢路径,再让卷忙 */ + (void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 1u) * SYSLOG_REC_SIZE); + lock_busy_after(1, 1); /* 批量那把必须拿到,慢路径那把才忙 */ + /* 批量路径先拿锁 -> 发现装不下一批 -> 返回 -3(此时一条都没写);逐条慢路径 + * 的第二把锁遇到卷忙 -> break -> done=0 -> 整批 3 条全部回投队列。 */ + g_ff_obs.calls[FF_FAKE_OP_OPEN] = 0; + syslog_drain(); + CHECK((s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == SYSLOG_RUN_MAX - 1u) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), + "TC-SYSLOG-035b 慢路径遇卷忙 -> 3 条**整批回投**(同一码在两条路径上行为相反)"); + + /* 035c 卷忙丢掉的是"整批"而不是"一条":8 条一批全丢 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); + for (i = 0u; i < SYSLOG_QUEUE_MAX; i++) { + syslog_post(SYSLOG_RUN, "eight"); + } + lock_busy_next(1); + syslog_drain(); + CHECK((s_q_cnt == 0u) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0), + "TC-SYSLOG-035c 卷忙一次吞掉一整批(8 条),不是旧实现的 1 条"); + + /* 036 回投溢出(hold-over drop):批量里混了 preboot 与队列两个来源时, + * 回投的目标队列已经被"没被取走的那些"占着 -> 装不下的部分才真被丢掉。 + * 队列 8 槽占满 + preboot 5 条 = 一批取 8 条(5 preboot + 3 队列), + * 失败后 done=0 整批回投 -> 队列只剩 3 个空位 -> 必然触发 5 次留痕。 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); /* 建队列 */ + for (i = 0u; i < SYSLOG_QUEUE_MAX; i++) { + syslog_post(SYSLOG_RUN, "q8"); /* 队列 8 槽占满 */ + } + s_kernel_state = osKernelInactive; + for (i = 0u; i < 5u; i++) { + syslog_post(SYSLOG_RUN, "p5"); /* preboot 再放 5 条 */ + } + ok = (s_q_cnt == SYSLOG_QUEUE_MAX) && (s_preboot_cnt == 5u); + ff_fake_inject_write_short(P_RUN, 0u, 2); /* 整批写失败 */ + g_sys_err_count = 0; + s_kernel_state = osKernelRunning; + syslog_drain(); + CHECK(ok && (s_q_cnt == SYSLOG_QUEUE_MAX) && (s_preboot_cnt == 0u) + && (g_sys_err_count == 1 + 5 + 1), + "TC-SYSLOG-036 回投装不下 -> 5 次 hold-over drop + drop/wfail 各 1 条留痕"); + + /* 037 -3 退回慢路径:容量轮转只发生一次 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); + for (i = 0u; i < 4u; i++) { + syslog_post(SYSLOG_RUN, "slow"); + } + (void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 2u) * SYSLOG_REC_SIZE); + syslog_drain(); + ok = (syslog_count(SYSLOG_RUN) == 2u) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (g_ff_obs.calls[FF_FAKE_OP_WRITE] >= 4); + CHECK(ok && (s_q_cnt == 0u), + "TC-SYSLOG-037 -3 -> 逐条慢路径:装满剩位 -> 满 -> unlink 重建 -> 继续,全程 unlink 恰一次"); + + /* 038 慢路径中途写失败 -> 该条丢 + 尾部回投 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); + for (i = 0u; i < 3u; i++) { + syslog_post(SYSLOG_RUN, "mid"); + } + (void)ff_fake_mkfile_size(P_RUN, ((uint32_t)SYSLOG_RUN_MAX - 1u) * SYSLOG_REC_SIZE); + ff_fake_inject_write_short(P_RUN, 0u, 2); /* 第一条就失败 */ + g_sys_err_count = 0; + syslog_drain(); + ok = (s_q_cnt == 2u) && (ff_fake_size(P_RUN) == 0u) && (g_sys_err_count >= 2); + syslog_drain(); /* 之后成功 */ + CHECK(ok && (s_q_cnt == 0u) && (syslog_count(SYSLOG_RUN) == 2u), + "TC-SYSLOG-038 慢路径写失败:只丢**当前那一条**,尾部 2 条下轮补齐"); + + /* 039【现状桩 = 第二个不一致】rc == -1 分支把 done 置 0 并打 + * DBG_ERROR("drop %lu entries id=%d"),但 done=0 意味着紧随其后的循环把 + * **整批全部回投队列** —— 日志说"丢了多少条",实际一条没丢(下轮还会重试)。 + * 措辞与行为相反,排障时会把人往错方向带;本条按现状钉住。 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); + for (i = 0u; i < 3u; i++) { + syslog_post(SYSLOG_RUN, "wfail"); + } + ff_fake_inject_write_short(P_RUN, 0u, 2); + g_sys_err_count = 0; + syslog_drain(); + CHECK((s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == 0u) && (g_sys_err_count == 2), + "TC-SYSLOG-039[DEFECT 现状] 批量 -1:日志报 drop,条目却整批回投(3 条仍在队列)"); + syslog_drain(); /* 注入耗尽 -> 这次真的落下去 */ + CHECK((syslog_count(SYSLOG_RUN) == 3u) && (s_q_cnt == 0u), + "TC-SYSLOG-039b 回投的 3 条下一轮补齐(与 035 的卷忙整批丢失形成对照)"); + + /* 040 B9 挂接:每次 drain 尾部各调一次 soe_store_drain */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); + ok = (s_soe_drain_calls == 1); + syslog_drain(); + CHECK(ok && (s_soe_drain_calls == 2), + "TC-SYSLOG-040 drain 尾部必调 soe_store_drain(同写者、同卷就绪门)"); + + /* 041 post 分流与守卫 */ + reset_all(); + ready_only(); + syslog_post((syslog_id_t)SYSLOG_COUNT, "bad-id"); + syslog_post(SYSLOG_RUN, NULL); + CHECK((s_preboot_cnt == 0u) && (s_q_cnt == 0u), + "TC-SYSLOG-041a id 越界 / NULL 描述 -> 静默丢弃(不占待发位)"); + reset_all(); + ready_only(); + for (i = 0u; i < SYSLOG_QUEUE_MAX + 2u; i++) { + syslog_post(SYSLOG_RUN, "pf"); + } + CHECK((s_preboot_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 2), + "TC-SYSLOG-041b preboot 满(8 槽)-> 丢弃 + 每条一次留痕(日志不得反压业务)"); + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + s_q_cap = 2u; /* 造一个很短的队列 */ + s_q_msgsz = (uint32_t)sizeof(syslog_entry_t); + s_queue = (osMessageQueueId_t)&s_q_pool[0]; + syslog_post(SYSLOG_RUN, "q1"); + syslog_post(SYSLOG_RUN, "q2"); + syslog_post(SYSLOG_RUN, "q3"); + CHECK((s_q_cnt == 2u) && (g_sys_err_count == 1), + "TC-SYSLOG-041c 内核运行 -> 走队列;队列满丢弃 + 留痕"); +} + +/* ===================== D 读侧 ===================== */ + +static void tc_read(void) +{ + char ts[SYSLOG_TS_LEN + 1u]; + char desc[SYSLOG_DESC_MAX + 1u]; + uint32_t epoch; + uint32_t i; + int ok; + + printf("-- TC-SYSLOG-050..058 读侧(N-24 滑动窗口)--\n"); + + /* 050 count */ + reset_all(); + ready_only(); + CHECK(syslog_count(SYSLOG_RUN) == 0u, "TC-SYSLOG-050a 文件缺失 -> count 0"); + for (i = 0u; i < 3u; i++) { + (void)s_append(SYSLOG_RUN, "c"); + } + ok = (syslog_count(SYSLOG_RUN) == 3u) && (syslog_count((syslog_id_t)7u) == 0u); + CHECK(ok, "TC-SYSLOG-050b count = filesize/64(O(1),不读盘内容);id 越界 -> 0"); + + /* 051 窗口命中零 I/O */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "w0"); + (void)s_append(SYSLOG_RUN, "w1"); + { + int opens_base = g_ff_obs.calls[FF_FAKE_OP_OPEN]; + int reads_base = g_ff_obs.calls[FF_FAKE_OP_READ]; + int opens_after; + ok = (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == 0); + opens_after = g_ff_obs.calls[FF_FAKE_OP_OPEN]; + ok = ok && (strcmp(desc, "w1") == 0) + && (opens_after == opens_base + 1) /* 填窗这一次 I/O */ + && (g_ff_obs.calls[FF_FAKE_OP_READ] == reads_base + 1); + ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0) + && (strcmp(desc, "w0") == 0) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_after) /* 命中 -> 零 I/O */ + && (g_ff_obs.calls[FF_FAKE_OP_READ] == reads_base + 1); + } + CHECK(ok, "TC-SYSLOG-051 窗口命中 -> 第二次读**零 FatFs 调用**(UI 翻页不再逐条开合)"); + + /* 052 写者代数失效 */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "old"); + ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0) + && (strcmp(desc, "old") == 0); + (void)s_append(SYSLOG_RUN, "new"); /* 代数 +1 -> 窗口必须重填 */ + ok = ok && (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == 0) + && (strcmp(desc, "new") == 0); + CHECK(ok, "TC-SYSLOG-052 写者一落地代数就变 -> 窗口重填,绝不返回陈旧内容"); + + /* 053 跨窗口 */ + reset_all(); + ready_only(); + for (i = 0u; i < 9u; i++) { + (void)s_append(SYSLOG_RUN, "x"); + } + (void)syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)); + { + int opens_before = g_ff_obs.calls[FF_FAKE_OP_OPEN]; + ok = (syslog_get_record(SYSLOG_RUN, 8u, ts, sizeof(ts), desc, sizeof(desc)) == 0); + ok = ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_before + 1) + && (g_ff_obs.last_lseek_ofs == (FSIZE_t)(SYSLOG_RD_WINDOW * SYSLOG_REC_SIZE)) + && (g_ff_obs.last_read_btr == SYSLOG_REC_SIZE); + } + CHECK(ok, "TC-SYSLOG-053 idx=8 跨窗 -> 一次 open + lseek(8*64) + read(1 条)(窗口按 8 对齐)"); + + /* 054 越界 */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "only"); + ok = (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == -1) + && (s_read_raw(SYSLOG_RUN, 5u, (uint8_t *)desc) == -1); + reset_all(); + ready_only(); + ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1); + CHECK(ok, "TC-SYSLOG-054 越界 idx / 空文件 -> -1(等锁前后各校验一次 count)"); + + /* 055 读时卷忙 */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "busy"); + (void)syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)); /* 填窗 */ + /* 换到另一 id 迫使真读盘,再让卷忙 */ + (void)s_append(SYSLOG_SELFTEST, "s"); + lock_busy_next(1); + ok = (syslog_get_record(SYSLOG_SELFTEST, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] >= 1) && (s_lock_seq_err == 0); + CHECK(ok, "TC-SYSLOG-055 读时卷忙 -> -1(读窗口 try 语义:uiTask 也绝不阻塞等锁,下轮刷新重读)"); + + /* 056 短读作废窗口 */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "half"); + (void)s_append(SYSLOG_RUN, "half2"); + s_rd_win_id = -1; + s_rd_win_cnt = 0u; + ff_fake_inject_read_short(P_RUN, 1u, 1); + ok = (s_read_raw(SYSLOG_RUN, 0u, (uint8_t *)desc) == -1) + && (s_rd_win_cnt == 0u) && (s_rd_win_id == -1); + CHECK(ok, "TC-SYSLOG-056 短读 -> 窗口作废(半窗当有效命中 = UI 显示半条脏记录)"); + + /* 057 get_record 守卫与截断 */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "01234567890123456789012345678901234567"); + ok = (syslog_get_record(SYSLOG_RUN, 0u, NULL, 32u, desc, sizeof(desc)) == -1) + && (syslog_get_record(SYSLOG_RUN, 0u, ts, SYSLOG_TS_LEN, desc, sizeof(desc)) == -1) + && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), NULL, sizeof(desc)) == -1) + /* 守卫是 desc_len <= SYSLOG_DESC_MAX(不是 0),所以 28 及以下整条被拒 */ + && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, SYSLOG_DESC_MAX) == -1) + && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, SYSLOG_DESC_MAX + 1u) == 0) + /* 唯一"能读"的边界是 desc_len = 29:这时函数里 dl >= desc_len 那条截断 + * 永远不成立(dl 由记录内容决定,恒 <= SYSLOG_DESC_MAX = 28),所以拿到的是 + * 整 28 字符而不是截断结果 —— 即"传刚够的缓冲会被拒、传更小的也被拒, + * 从来没有截断"。登记为死分支,见文件头"本套照出的现状"。 */ + && (strlen(desc) == (size_t)SYSLOG_DESC_MAX); + CHECK(ok, "TC-SYSLOG-057 守卫:ts_len<=19 或 desc_len<=28 一律 -1;29 才读得到整 28 字符"); + + /* 058 last_epoch */ + reset_all(); + ready_only(); + ok = (syslog_last_epoch(SYSLOG_RUN, &epoch) == -1) && (epoch == 0u); + s_now = 1700000123u; + (void)s_append(SYSLOG_RUN, "e"); + ok = ok && (syslog_last_epoch(SYSLOG_RUN, &epoch) == 0) && (epoch == 1700000123u); + CHECK(ok, "TC-SYSLOG-058 last_epoch:无记录 -1;有记录解析 10 位 ASCII"); +} + +/* ===================== E mounted / 上电重启分类 ===================== */ + +static void tc_mounted(void) +{ + uint8_t rec[SYSLOG_REC_SIZE]; + int ok; + + printf("-- TC-SYSLOG-060..064 挂载后处理(挂载幂等 + 上电/重启分类)--\n"); + + /* 060 /log 不在 -> 建目录 + 写上电记录;第二次调用完全静默 */ + reset_all(); + s_fs_ready = 0u; + s_kernel_state = osKernelInactive; + ok = (ff_fake_exists("/log") == 2); + syslog_post(SYSLOG_RUN, "pre-mount"); /* 就绪前排队的条目也应当被带出来 */ + syslog_mcu_mounted(); + ok = ok && (s_fs_ready == 1u) && (syslog_count(SYSLOG_RUN) == 1u); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0) && (g_sys_err_count == 0), + "TC-SYSLOG-060a /log 已存在 -> 只写上电/重启记录一条"); + /* 060b 幂等:再来一次既不建目录也不写第二条 */ + reset_all(); + s_fs_ready = 0u; + syslog_mcu_mounted(); /* 第一次:写一条上电/重启记录 */ + { + uint32_t cnt1 = syslog_count(SYSLOG_RUN); + int mkdirs1 = g_ff_obs.calls[FF_FAKE_OP_MKDIR]; + syslog_mcu_mounted(); /* 第二次:应完全静默 */ + CHECK((s_fs_ready == 1u) && (syslog_count(SYSLOG_RUN) == cnt1) + && (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == mkdirs1), + "TC-SYSLOG-060b 幂等再调用:不再建目录、不再写上电记录(2s 复查不刷屏)"); + } + (void)rec; + + /* 061 建目录失败:不就绪 + 留痕去重 + 后续重试成功 */ + reset_all(); + s_fs_ready = 0u; + ff_fake_inject_rc("/log", FF_FAKE_OP_STAT, FR_DISK_ERR, 3); /* 逼它每次都走 mkdir 分支 */ + ff_fake_inject_rc("/log", FF_FAKE_OP_MKDIR, FR_DENIED, 2); + g_sys_err_count = 0; + syslog_mcu_mounted(); /* 失败 1 -> 留痕 */ + ok = (s_fs_ready == 0u) && (g_sys_err_count == 1); + syslog_mcu_mounted(); /* 失败 2 -> 不再留痕 */ + ok = ok && (g_sys_err_count == 1) && (s_lock_releases == 2) && (s_lock_seq_err == 0); + syslog_mcu_mounted(); /* 注入耗尽 -> 成功 */ + CHECK(ok && (s_fs_ready == 1u) && (s_mount_fail_reported == 0u), + "TC-SYSLOG-061 mkdir 失败:不就绪、留痕一次(防 2s 刷屏)、第三轮自愈并复位标志"); + + /* 062 卷忙:本轮顺延,不置就绪、不碰盘 */ + reset_all(); + s_fs_ready = 0u; + lock_busy_next(1); + syslog_mcu_mounted(); + CHECK((s_fs_ready == 0u) && (g_ff_obs.calls[FF_FAKE_OP_STAT] == 0) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 0), + "TC-SYSLOG-062 卷忙 -> 直接返回(try 失败不 release,下轮 2s 后再试)"); + + /* 063 上电 / 重启分类:阈值 SYSLOG_REBOOT_WINDOW_S 两侧 + 墙钟无效 */ + reset_all(); + s_fs_ready = 0u; + s_now = 1760234567u; + (void)ff_fake_mkfile_size(P_RUN, SYSLOG_REC_SIZE); /* 有一条末条记录可解析 */ + /* 末条 epoch 造在 5 分钟前 -> reboot */ + { + uint8_t one[SYSLOG_REC_SIZE]; + (void)memset(one, 0, sizeof(one)); + (void)memcpy(one, "2025-10-12 01:57:47", SYSLOG_TS_LEN); + one[SYSLOG_TS_LEN] = ' '; + one[SYSLOG_EPOCH_OFFS - 1u] = ' '; + (void)memcpy(one + SYSLOG_EPOCH_OFFS, "1760234267", SYSLOG_EPOCH_LEN); /* -300s */ + (void)ff_fake_mkfile(P_RUN, one, SYSLOG_REC_SIZE); + } + syslog_mcu_mounted(); + ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "reboot", 6u) == 0) + && (rec[SYSLOG_DESC_OFFS + 6u] == '\0'); + CHECK(ok, "TC-SYSLOG-063a 末条 epoch 距 now 300s(<600) -> 描述落 \"reboot\""); + + reset_all(); + s_fs_ready = 0u; + { + uint8_t one[SYSLOG_REC_SIZE]; + (void)memset(one, 0, sizeof(one)); + (void)memcpy(one, "2025-10-12 08:00:00", SYSLOG_TS_LEN); + one[SYSLOG_TS_LEN] = ' '; + one[SYSLOG_EPOCH_OFFS - 1u] = ' '; + (void)memcpy(one + SYSLOG_EPOCH_OFFS, "1760200000", SYSLOG_EPOCH_LEN); /* -9.5h */ + (void)ff_fake_mkfile(P_RUN, one, SYSLOG_REC_SIZE); + } + syslog_mcu_mounted(); + ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "power on", 8u) == 0); + CHECK(ok, "TC-SYSLOG-063b 间隔 >=600s -> \"power on\"(掉电超过 10 分钟算重新上电)"); + + reset_all(); + s_fs_ready = 0u; + s_clock_ok = 0; + { + uint8_t one[SYSLOG_REC_SIZE]; + (void)memset(one, 0, sizeof(one)); + (void)memcpy(one + SYSLOG_EPOCH_OFFS, "1760234560", SYSLOG_EPOCH_LEN); /* 距 now 7s */ + (void)ff_fake_mkfile(P_RUN, one, SYSLOG_REC_SIZE); + } + syslog_mcu_mounted(); + ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "power on", 8u) == 0); + CHECK(ok, "TC-SYSLOG-063c 墙钟无效(UINT32_MAX)-> 保守判 \"power on\",不拿假差值判重启"); + + /* 064 幂等靠 stat,不靠状态位:/log 已在就不再 mkdir */ + reset_all(); + s_fs_ready = 0u; + syslog_mcu_mounted(); + ok = (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0) && (g_ff_obs.calls[FF_FAKE_OP_STAT] >= 1); + CHECK(ok, "TC-SYSLOG-064 /log 已存在时靠 f_stat 短路,f_mkdir 一次都不调"); +} + +/* ===================== F 取证通道 sys_log_evidence ===================== */ + +static void tc_evidence(void) +{ + uint8_t raw[8u * SYSLOG_REC_SIZE]; + int ok; + + printf("-- TC-SYSLOG-070..074 sys_log_evidence(wdev.log 取证通道)--\n"); + + /* 070 变长行 + 第二条 run.log */ + reset_all(); + s_now = 1760234567u; + ok = (sys_log_evidence("task stall declared") == 0); + { + uint32_t n = ff_fake_fetch(P_WDEV, raw, 0u, sizeof(raw)); + ok = ok && (n > 0u) + && (memcmp(raw, "2025-10-12 02:02:47 task stall declared\r\n", 41u) == 0) + && (syslog_count(SYSLOG_RUN) == 1u) + && raw_at(P_RUN, 0u, raw) + && (memcmp(raw + SYSLOG_DESC_OFFS, "wd stall declared", 17u) == 0); + } + CHECK(ok, "TC-SYSLOG-070 wdev.log 写变长行(带 CRLF),并补一条 run.log 独立证据"); + + /* 071 卷锁顺序:第二条写必须在释放之后(非递归锁,反了 = 自死锁) */ + reset_all(); + (void)sys_log_evidence("order probe"); + CHECK((s_lock_waits == 2) && (s_lock_releases == 2) && (s_lock_seq_err == 0) + && (s_lock_held == 0), + "TC-SYSLOG-071 取锁两次各放一次(在持锁状态下调 s_append 会永远拿不到锁)"); + + /* 072 >8192B 护栏 */ + reset_all(); + (void)ff_fake_mkfile_size(P_WDEV, 8193u); + ok = (sys_log_evidence("after rotate") == 0); + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (ff_fake_size(P_WDEV) > 0u) && (ff_fake_size(P_WDEV) < 200u), + "TC-SYSLOG-072 超过 8KB -> 先 unlink 再写(长跑不被无界撑爆)"); + + /* 073 卷忙 */ + reset_all(); + lock_busy_next(1); + ok = (sys_log_evidence("busy probe") == -1); + CHECK(ok && (ff_fake_exists(P_WDEV) == 0) && (syslog_count(SYSLOG_RUN) == 0u) + && (s_lock_releases == 0) && (s_lock_seq_err == 0), + "TC-SYSLOG-073 卷忙 -> -1 且**两条写都没发生**(喂狗点绝不阻塞等锁)"); + + /* 074 wdev 写失败:返回 -1,但第二条仍写("独立证据"语义) */ + reset_all(); + ff_fake_inject_rc(P_WDEV, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1); + ok = (sys_log_evidence("second write still") == -1); + CHECK(ok && (syslog_count(SYSLOG_RUN) == 1u) && (g_sys_err_count == 0), + "TC-SYSLOG-074 wdev 写失败不阻断第二条 run.log(多一条独立证据的原始意图)"); +} + +/* ===================== G 陷阱二:名字/参数类错误码严禁升级成卷操作 ===================== */ + +static void tc_name_rules(void) +{ + static FIL probe; + FRESULT fr_body; + FRESULT fr_ext; + FRESULT fr_bad; + FRESULT fr_dot; + FRESULT fr_colon; + int ok; + uint32_t i; + + printf("-- TC-SYSLOG-080..082 陷阱二分档与「绝不升级」 --\n"); + + /* 080 短名违规**分档**:每条都落到 FR_INVALID_NAME;':' 落到 FR_INVALID_DRIVE */ + reset_all(); + fr_body = f_open(&probe, "/log/selftestt.log", FA_WRITE | FA_CREATE_ALWAYS); + if (fr_body == FR_OK) { (void)f_close(&probe); } + fr_ext = f_open(&probe, "/log/run.logbak", FA_WRITE | FA_CREATE_ALWAYS); + if (fr_ext == FR_OK) { (void)f_close(&probe); } + fr_bad = f_open(&probe, "/log/ru*n.log", FA_WRITE | FA_CREATE_ALWAYS); + if (fr_bad == FR_OK) { (void)f_close(&probe); } + fr_dot = f_open(&probe, "/log/.fschk", FA_WRITE | FA_CREATE_ALWAYS); + if (fr_dot == FR_OK) { (void)f_close(&probe); } + fr_colon = f_open(&probe, "1:/log/run.log", FA_WRITE | FA_OPEN_APPEND); + if (fr_colon == FR_OK) { (void)f_close(&probe); } + CHECK((fr_body == FR_INVALID_NAME) && (fr_ext == FR_INVALID_NAME) + && (fr_bad == FR_INVALID_NAME) && (fr_dot == FR_INVALID_NAME) + && (fr_colon == FR_INVALID_DRIVE), + "TC-SYSLOG-080 分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME;':' -> FR_INVALID_DRIVE"); + + /* 081 生产实际用的每个名字都必须过严格 8.3(改名 = 静默写失败的第一道防线) */ + reset_all(); + ok = (f_open(&probe, P_RUN, FA_WRITE | FA_OPEN_APPEND) == FR_OK) && (f_close(&probe) == FR_OK); + ok = ok && (f_open(&probe, P_SELFTEST, FA_WRITE | FA_OPEN_APPEND) == FR_OK) + && (f_close(&probe) == FR_OK); + ok = ok && (f_open(&probe, P_WDEV, FA_WRITE | FA_OPEN_APPEND) == FR_OK) + && (f_close(&probe) == FR_OK); + ok = ok && (f_stat("/log", NULL) == FR_OK); + CHECK(ok, "TC-SYSLOG-081 生产路径 /log、run.log、selftest.log、wdev.log 全部 8.3 合规"); + + /* 082 名字类错误码不得升级成卷操作:反复失败也只动自己的文件 */ + reset_all(); + ready_only(); + (void)ff_fake_mkfile(P_SELFTEST, NULL, SYSLOG_REC_SIZE); /* 邻居文件 */ + (void)ff_fake_mkfile("/config.ini", NULL, 12u); /* 同卷别的东西 */ + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 200); + for (i = 0u; i < 20u; i++) { + (void)s_append(SYSLOG_RUN, "bad-name"); + } + ok = (ff_fake_exists("/log") == 2) + && (ff_fake_size(P_SELFTEST) == SYSLOG_REC_SIZE) + && (ff_fake_size("/config.ini") == 12u) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] <= (int)(2u * 20u)) + && (g_sys_err_count == 1); + CHECK(ok, "TC-SYSLOG-082 反复拿 FR_INVALID_NAME 也只 unlink 自己的文件:目录/邻居不动、留痕一次"); +} + +/* ===================== H 补齐分支(自愈 / 二次校验 / 回投 / 占位时标) ===================== */ + +static void tc_extra_branches(void) +{ + char ts[SYSLOG_TS_LEN + 1u]; + char desc[SYSLOG_DESC_MAX + 1u]; + uint8_t raw[SYSLOG_REC_SIZE]; + uint32_t epoch; + uint32_t i; + int ok; + + printf("-- TC-SYSLOG-090..100 补齐分支 --\n"); + + /* 090 批量路径的 L1 自愈:open 失败一次 -> unlink + 重开 -> 整批照常落盘 + * (这条钉的是 s_append_many 里那段"和单条同一份自愈表";漏了它, + * FTP 侧删过文件之后的第一批日志就会整批丢) */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "seed"); /* 先让文件存在 */ + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1); + { + static uint8_t batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE]; + (void)memset(batch, 0x33, sizeof(batch)); + ok = (s_append_many(SYSLOG_RUN, batch, 2u) == 0); + } + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3) /* 单条 1 + 批量首开 1 + 自愈重开 1 */ + && (ff_fake_size(P_RUN) == 2u * SYSLOG_REC_SIZE) && (g_sys_err_count == 0), + "TC-SYSLOG-090 批量 open 失败一次 -> unlink 重开 -> 整批 2 条落全、不留痕"); + + /* 091 批量 open 永久失败 -> -1(走的是"打开失败"那条 else,不是写失败) */ + reset_all(); + ready_only(); + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 99); + { + static uint8_t batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE]; + ok = (s_append_many(SYSLOG_RUN, batch, 3u) == -1); + } + CHECK(ok && (ff_fake_exists(P_RUN) == 0) && (g_sys_err_count == 1) + && (s_lock_releases == 1) && (s_lock_seq_err == 0), + "TC-SYSLOG-091 批量打开持续失败 -> -1 + 留痕一次 + 已释放卷锁"); + + /* 092 取锁后二次校验:等锁期间写者可能已把文件删掉(容量轮转/取证重建) + * -> 释放锁再返回 -1,绝不能拿锁前的旧 total 去 lseek */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "one"); + ff_fake_inject_rc_skip(P_RUN, FF_FAKE_OP_STAT, FR_DISK_ERR, 1, 1); + /* 前面那次 s_append 已经取放锁各一次,所以这里的基线是 1,不是 0 */ + ok = (s_lock_releases == 1); + ok = ok && (s_read_raw(SYSLOG_RUN, 0u, raw) == -1) + && (s_lock_releases == 2) && (s_lock_seq_err == 0); + CHECK(ok && (s_rd_win_cnt == 0u), + "TC-SYSLOG-092 锁后二次 count 失败 -> 放锁返回 -1(漏放 = FTP 永久等不到卷)"); + + /* 093 读时 f_open 失败 -> 放锁返回 -1(另一条提前退出路径) */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "one"); + s_rd_win_id = -1; + s_rd_win_cnt = 0u; + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1); + ok = (s_read_raw(SYSLOG_RUN, 0u, raw) == -1) + && (s_lock_releases == 2) && (s_lock_seq_err == 0); + /* 第二次读(注入耗尽)应成功,证明窗口没被留在半初始化状态 */ + ok = ok && (s_read_raw(SYSLOG_RUN, 0u, raw) == 0) && (s_rd_win_cnt == 1u); + CHECK(ok, "TC-SYSLOG-093 读打开失败 -> 放锁 -1;下轮恢复后窗口正常重填"); + + /* 094 墙钟未建立时 evidence 用占位时标(不得写半截或未初始化缓冲) */ + reset_all(); + s_clock_ok = 0; + ok = (sys_log_evidence("no clock") == 0); + if (ok) { + (void)ff_fake_fetch(P_WDEV, raw, 0u, SYSLOG_TS_LEN); + ok = (memcmp(raw, "1970-01-01 08:00:00", SYSLOG_TS_LEN) == 0); + } + CHECK(ok, "TC-SYSLOG-094 无墙钟 -> wdev.log 落占位时标(取证行也必须可解析)"); + + /* 095 wdev 短写:FR_OK + 字节数不足 -> -1,但第二条 run.log 照写 */ + reset_all(); + ff_fake_inject_write_short(P_WDEV, 3u, 1); + ok = (sys_log_evidence("short line") == -1); + CHECK(ok && (ff_fake_size(P_WDEV) == 3u) && (syslog_count(SYSLOG_RUN) == 1u) + && (g_sys_err_count == 0), + "TC-SYSLOG-095 wdev 短写判失败返回 -1,第二条独立证据仍落 run.log"); + + /* 096 队列创建失败:条目滞留 preboot、留痕一次、下轮建成后排空 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + s_q_new_fail = 1; + syslog_post(SYSLOG_RUN, "nq"); + syslog_drain(); + ok = (s_queue == NULL) && (g_sys_err_count == 1) && (s_preboot_cnt == 1u) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_soe_drain_calls == 0); + s_q_new_fail = 0; + syslog_drain(); + CHECK(ok && (s_queue != NULL) && (syslog_count(SYSLOG_RUN) == 1u) + && (s_preboot_cnt == 0u) && (s_soe_drain_calls == 1), + "TC-SYSLOG-096 队列没建成 -> 一条都不排(不静默丢),堆恢复后排空并接上 SOE"); + + /* 097 换文件那条来自队列 -> 原样回投队列(不是 preboot 下标回退) + * 队列里 RUN/SELFTEST/RUN 交替:三条必须全落且各自文件内顺序不乱 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); /* 建队列 */ + syslog_post(SYSLOG_RUN, "qa"); + syslog_post(SYSLOG_SELFTEST, "qb"); + syslog_post(SYSLOG_RUN, "qc"); + ok = (s_q_cnt == 3u); + syslog_drain(); + if (ok) { + ok = (syslog_count(SYSLOG_RUN) == 2u) && (syslog_count(SYSLOG_SELFTEST) == 1u) + && raw_at(P_RUN, 0u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "qa", 2u) == 0) + && raw_at(P_RUN, 1u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "qc", 2u) == 0) + && raw_at(P_SELFTEST, 0u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "qb", 2u) == 0) + && (s_q_cnt == 0u); + } + CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3), + "TC-SYSLOG-097 队列侧换文件回投:三条全落、各文件内顺序不乱(回投不是丢弃)"); + /* 098 dl >= desc_len 的截断分支:守卫已挡掉 desc_len <= SYSLOG_DESC_MAX, + * 而 dl 由记录内容决定、恒 <= SYSLOG_DESC_MAX -> 该分支在**当前守卫取值下 + * 永不可达**。这条断言把"不可达"这件事本身钉住:一旦有人把守卫放宽成 + * desc_len == 0,本条就会因为读到截断结果而红,提醒补截断用例。 */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "abcdefghijklmnopqrstuvwxyz0123"); /* 34 字符 -> 截到 28 */ + ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, SYSLOG_DESC_MAX + 1u) == 0) + && (strlen(desc) == (size_t)SYSLOG_DESC_MAX) + && (desc[SYSLOG_DESC_MAX] == '\0'); + CHECK(ok, "TC-SYSLOG-098 desc_len=29 时截断分支不可达(拿到整 28 字符,非 28-1)"); + + /* 099 空指针守卫:s_read_raw 的 p_rec 与 evidence 的 p_desc */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "g"); + ok = (s_read_raw(SYSLOG_RUN, 0u, NULL) == -1) + && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0) + && (sys_log_evidence(NULL) == -1); + CHECK(ok && (s_lock_seq_err == 0), + "TC-SYSLOG-099 p_rec / p_desc 空指针守卫(守卫顺序在取锁之前,不放锁)"); + + /* 100 last_epoch:有记录但读失败 -> -1 且把 *p_epoch 清 0 + * (调用方 syslog_mcu_mounted 直接拿这个值判上电/重启,留旧值就会误判) */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "e"); + s_rd_win_id = -1; + s_rd_win_cnt = 0u; + epoch = 0xA5A5A5A5u; + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1); + ok = (syslog_last_epoch(SYSLOG_RUN, &epoch) == -1) && (epoch == 0u); + CHECK(ok, "TC-SYSLOG-100 last_epoch 读失败 -> -1 且 *p_epoch 清零(不留调用方旧值)"); + (void)i; +} + +/* ===================== 入口 ===================== */ + +int main(void) +{ + printf("=== Sys Log Mcu Tests ===\n"); + + tc_format(); + tc_write_single(); + tc_write_many(); + tc_drain(); + tc_read(); + tc_mounted(); + tc_evidence(); + tc_name_rules(); + tc_extra_branches(); + + printf("\n=== Sys Log Mcu Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} From 5a806d8a26b0a65fb7947ec4292c7681a56e8218 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sat, 24 Oct 2026 19:43:56 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(log,net):=20=E5=A4=84=E7=BD=AE=E4=B8=BB?= =?UTF-8?q?=E6=9C=BA=E5=8D=95=E6=B5=8B=E7=85=A7=E5=87=BA=E7=9A=84=2010=20?= =?UTF-8?q?=E6=9D=A1=E7=8E=B0=E7=8A=B6=EF=BC=8C=E6=92=A4=E9=94=80=201=20?= =?UTF-8?q?=E6=9D=A1=E8=AF=AF=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户裁定「全部修」。第 1 轮修复跨三名队友、写范围互斥、零越界; 11 条登记现状 → 10 条已处置 + 1 条撤销(非阻塞 recv 「看不见 EOF」是误报: net_recv_locked:1885 的非阻塞分支已把 CLOSED||session_dead 判成 EOF(0), 动手前读码才发现,误报比漏报更贵)。 存储与日志(App/log/sys_log_mcu.c、soe_store.c、cfg_file_store_mcu.c、sys_log.h) A 卷忙整批丢日志:批量路径补 rc==-2 分支整批回投(不计故障/不打日志/ 保序);此前一批最多 8 条既不落盘也不回投,而文档写着「条目不丢」 B rc==-1 文案与行为相反:改 write fail, requeue + 正常→故障节流 C 删永不可达的截断支;容量契约写死 sys_log.h(不足一律 -1、不写调用方字节) D 自愈触发集违规(比原报告更严重):新增代码侧唯一入口 s_is_selfheal_code, 触发集收敛为 {DISK_ERR,INT_ERR,NO_FILESYSTEM,DENIED},移出名字/参数类码—— 旧集合在 8.3 同名同因下每轮删一次自己的历史日志且永不自愈,正是存储架构 §10.1 第 10 条严禁的归集;soe_store 侧同源取更保守子集(打开失败绝不 unlink) E 注释里的 FR_NO_SYSTEM 不存在,改 FR_NOT_READY / FR_NO_FILESYSTEM F syslog_post 满态留痕节流(A 修好后「队列常驻满」成必然场景,不修等于用 新刷屏换掉静默丢);丢弃语义一字未改 NET(net_socket.c/.h、net_types.h、net_socket_sim.c、lftpd.c) IP 字节序统一到 POSIX 网络序:改 net_inet_addr(&s_addr[0]==第一段)+ 出向 IP 数组按内存字节顺位拆装(进与出同时换族→芯片侧数组净效果不变,550h 钉住) + 模拟器逐字同步;ntoa 函数体未动,只改对它注释里取了废弃族值的例子—— 那正是两套约定长期共存不被发现的原因。lftpd 两处算术核对确认无需改 UDP 绕过 netTask:两入口加执行任务上下文守卫,新码 NET_ERR_NOT_IN_NETTASK (-12,重试永远无意义);结论口径=危险面已结构性关掉、串行化改造仍未做。 附带关掉第二条更隐蔽的路:非 netTask 自旋 recvfrom 期间控制块被 close 并 把同一 sockfd 分给新会话→旧自旋者消费新会话数据报 控制台(console_log.c/.h):冷却改独立 s_con_cooling 标志(旧写法在 tick==0 那 拍冷却等于没记);con_append 的判空位+推进 head+整行拷贝收进同一临界区(旧 为区外 check-then-act,环 >95% 满时可静默覆盖且既不打 [ERR] 也不计 dropped); Instance==NULL 与僵尸通道分诊(未初始化绝不 reinit);三本账分离 + 新读口 dbg_console_tx_timeout();[ERR] 行改为发成功才销账 测试:net_socket 452、console_log 127、sys_log_mcu 104、soe_store 27、 cfg_file_store_mcu 34;回归 net_listener 34 / net_events 34 / net_pending 43 / net_select 22 / lftpd 35 / lftpd_inet 28 / lftpd_io 33 / iec104_serv 28 / nand_port 149 / vol_lock 33 / soe 32 / cfg_file 151 全绿。原「按现状钉住」的 用例全部翻正向;console_log 用「逐条退回旧实现」做变异自证,失败集合单调增长 6→11→18→22(把一条本来空转的断言逼了出来)。 游标抬升:TC-NETSOCK-545→560、TC-SYSLOG-100→107、TC-CONLOG-135→167。 文档:控制台设计说明 §4.1 口径与 §13-O2 销项 + v1.5;运行与自检记录 §4.4/§4.5 四条;存储架构 §10.1 第 10 条推论(指向 s_is_selfheal_code);TRAP-NET-21 后续 条目;BSD 指南新增 §9.0 s_addr 唯一约定;覆盖率口径 §9「第 1 轮修复处置」表 + 后续项 5 条 + 台架复验 7 条 + 新坑 8 条 + 变更记录。 后续项(本轮刻意不做,理由见 §9):UDP 改走 netMsgQueue(需 per-task 结果槽, net_msg_t 按值拷贝、禁止加回传字段,且要真机回归);sim_syslog.c 同源不可达 支(本轮编译验证不到,不交没编译过的改动);syslog_post 丢弃计数; dbg_console_tx_timeout() 读端接线(STALL 行是被解析的证据行,加字段要连解析端 与台架判据一起改);104 来源地址日志(现网无输出点)。 gate: @build 0 errors 0 warnings(Code=213720 RO=18860 RW=1204 ZI=131500)。 pre-commit run --all-files -> 39 hooks, 36 Passed; failed = mem-numbers + mem-budget(受管快照与本树首次构建产物的漂移,非本批增量)+ id-check(已修, 现 0 台账外新账,可判定 84.63% > 基线 82.6%)。实测水位 RW_IRAM1 57.4%、 RW_CCM 87.9%,均低于 92% 红线;RAM/Flash 本批增量为数个 static 变量。 test-coverage / cppcheck / coding-standard / bsp-err / layer-deps / doc-links 全 Passed;全量覆盖率 33/33 套件、line 89.3%、func 92.3%、逐文件下降 0。 按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。 --- App/cfg/cfg_file_store_mcu.c | 14 +- App/log/console_log.c | 131 ++++-- App/log/console_log.h | 10 +- App/log/soe_store.c | 56 ++- App/log/sys_log.h | 33 +- App/log/sys_log_mcu.c | 191 +++++++- Drivers/BSP/NET/lftpd/lftpd.c | 4 + Drivers/BSP/NET/net_socket.c | 126 ++++-- Drivers/BSP/NET/net_socket.h | 43 +- Drivers/BSP/NET/net_types.h | 9 + docs/子系统设计/控制台单写者日志设计说明.md | 5 +- docs/子系统设计/运行与自检记录设计说明.md | 8 +- docs/架构设计/STM32F4-Base存储架构说明.md | 3 +- docs/测试说明/主机单测覆盖率口径.md | 56 ++- docs/网络/BSD_Socket_API_使用指南.md | 20 + docs/问题记录/CH395F_Trap_Records.md | 1 + scripts/id_registry.json | 6 +- scripts/test_coverage_baseline.json | 62 +-- simulator/sim/net_socket_sim.c | 26 +- .../cfg_file_store_mcu_test.c | 16 +- test/host/common/ch395f_fake.c | 25 +- test/host/common/ch395f_fake.h | 7 + test/host/console_log/console_log_test.c | 341 +++++++++++++-- test/host/console_log/stub/stm32f4_fake.h | 32 ++ test/host/net_socket/net_socket_test.c | 81 +++- test/host/net_socket/ns_s5_io.c | 12 +- test/host/net_socket/ns_s6_edges.c | 185 +++++++- test/host/soe_store/soe_store_test.c | 58 ++- test/host/sys_log_mcu/sys_log_mcu_test.c | 407 +++++++++++++++--- 29 files changed, 1690 insertions(+), 278 deletions(-) diff --git a/App/cfg/cfg_file_store_mcu.c b/App/cfg/cfg_file_store_mcu.c index b908fdd4..cc07ab90 100644 --- a/App/cfg/cfg_file_store_mcu.c +++ b/App/cfg/cfg_file_store_mcu.c @@ -18,6 +18,10 @@ * dhara journal 竞争实证);busy 返回 -1 由调用方 * 按既有失败语义处理(poll 保留 RAM/保存待重试), * 本任务 = 看门狗喂狗点,绝不阻塞等锁 + * v1.5 2026-10-24 王建锋 仅注释更正:cfg_store_read 的"限定条件"引用了 + * FRESULT 里不存在的 FR_NO_SYSTEM,改成实际会返回的 + * FR_NOT_READY / FR_NO_FILESYSTEM 并写明"非 FR_OK 一律 + * 转 -1,不要枚举错误码集合"(无行为改动) * v1.4 2026-10-05 王建锋 原子写临时文件由 p_path 推导(P3-2):与目标同目录的 * 固定 8.3 名 config.TMP,不再硬编码卷根;目标恒为 * "/config.ini" 时推导结果与旧值逐字节一致 @@ -47,7 +51,15 @@ static const char s_path_tmp[] = "/config.TMP"; * cap - 缓冲容量 size_t * p_nread - 实际读字节数输出(不可为 NULL) size_t * * 返回值:0 - 成功;-1 - 文件缺失/读失败 int - * 限定条件:卷已挂载(未挂载时 f_open 返回 FR_NO_SYSTEM,转 -1) + * 限定条件:卷已挂载。未挂载时本函数一律转 -1,但**别照着不存在的错误码去写 + * 判据**:Lib/FatFs/ff.h 的 FRESULT 共 20 个码(FR_OK..FR_INVALID_PARAMETER), + * 没有 FR_NO_SYSTEM(旧注释此处笔误,2026-10-24 更正)。本移植里卷未挂载 + * 实际返回 FR_NOT_READY(3)(物理驱动器不可用)或 FF_VOLUMES 内无已挂载卷时 + * 由 mount_volume 直接给错;卷上无有效 FAT 则返回 FR_NO_FILESYSTEM(13)。 + * 本函数(以及 write_atomic / stat / remove)对**任何**非 FR_OK 一律转 -1, + * 所以这些码的差异不影响对外语义——也正因如此,谁都不该去枚举错误码集合, + * 除了自愈触发集那种"必须区分能不能靠删文件修好"的场合(见 sys_log_mcu.c + * 的 s_is_selfheal_code)。 * 函数说明:FIL 为**函数级 static**而非局部——本 FatFs 移植 FF_FS_TINY=0, * `FIL` 内含 `buf[FF_MAX_SS]`=512 B(Lib/FatFs/ff.h),一个局部对象就吃掉 * 528 B 栈,是配置/syslog 调用链栈深的主要来源(2026-10-10 栈预算整改)。 diff --git a/App/log/console_log.c b/App/log/console_log.c index 994a9c43..d93a0711 100644 --- a/App/log/console_log.c +++ b/App/log/console_log.c @@ -31,6 +31,17 @@ * adcTask(B1)等周期任务此时落日志会与 defaultTask 的存储初始化 * 日志字节级交错(台架实测 [ADC_TASK] 首帧行与 [NAND_FTL] 行交错)。 * 提供就绪查询供周期任务门控自身日志,不改直发路径本身。 + * 2026-10-24 王建锋 主机单测(test/host/console_log)照出的四条一并修复: + * 冷却改判独立标志 s_con_cooling(旧写法拿 tick 值当"是否在冷却"哨兵, + * 故障恰好落在 HAL_GetTick()==0 那一拍时冷却等于没记); + * con_append 的"复核空位 + 整行拷贝 + 推进 head"收进同一临界区(旧写法 + * 判据在区外,多生产者可在环将满时同时过判据 -> head 越过容量把未发出 + * 的旧行静默覆盖,既不报错也不计丢弃); + * drain 失败分诊:Instance==NULL(未初始化)不再走外设软复位自愈(那会 + * 在 HAL 未就绪时重配 RCC/GPIO),改记 dropped;僵尸通道仍自愈; + * 三本账分开(docs §13-O2):新增 dbg_console_tx_timeout(),dropped 只记 + * 真丢的,tx_fault 仍只记软复位自愈次数;[ERR] dropped 行改为"发成功才 + * 销账",通道不可用期间不再把丢弃证据一起冲掉。 */ #include "console_log.h" @@ -89,10 +100,12 @@ static uint8_t s_con_ring[DBG_CON_RING_SIZE]; static volatile uint16_t s_con_head; /* 生产者写入位置 */ static volatile uint16_t s_con_tail; /* 写者读取位置 */ -static volatile uint32_t s_con_drop; /* 因缓冲满丢弃的行数 */ +static volatile uint32_t s_con_drop; /* 真没落地的丢弃数(一次一行/一块) */ static volatile uint32_t s_con_tx_fault; /* TX 失败(僵尸通道)次数,均经外设软复位自愈 */ static volatile uint8_t s_con_tx_fault_log; /* TX 故障留痕待上报(由写者任务在 drain 之外补记 run.log) */ -static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(0 = 无冷却) */ +static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(只存时刻,不再兼作哨兵) */ +static volatile uint8_t s_con_cooling; /* 1 = 正在故障冷却(独立标志:tick 恰好等于 0 的那一拍也必须算在冷却) */ +static volatile uint32_t s_con_tx_timeout; /* 发送块超时次数(写者排空所见,与丢弃账分开) */ static volatile uint8_t s_con_draining; /* 1 = 处于 drain 中(重入保护) */ static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */ static volatile uint8_t s_con_dwt_up; /* 1 = DWT CYCCNT 已使能(硬上界基准可用) */ @@ -179,7 +192,7 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) { con_dwt_ensure(); if (huart1.Instance == NULL) { - return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位 */ + return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位(不是超时,不记超时账) */ } if (s_con_dwt_up == 0U) { return con_tx_counted(huart1.Instance, p_buf, len); @@ -190,14 +203,14 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) { for (i = 0U; i < len; i++) { while ((u->SR & USART_SR_TXE) == 0U) { if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) { - return HAL_TIMEOUT; /* 有界:本块放弃(调用方自愈) */ + return HAL_TIMEOUT; /* 有界:本块放弃(记账由调用方按返回值做) */ } } u->DR = (uint16_t)p_buf[i]; } while ((u->SR & USART_SR_TC) == 0U) { if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) { - return HAL_TIMEOUT; + return HAL_TIMEOUT; /* TC 超时:字节已交给发送器,调用方分账 */ } } return HAL_OK; @@ -281,13 +294,16 @@ void dbg_console_drain(void) { /* 故障冷却:僵尸通道(ORE 重锁)秒内不自愈,冷却期内不发送—— * 本轮 drain 直接让位主循环,保住喂狗节奏(gen-288 停喂误复位的 - * 直接诱因 = 背靠连发的块超时链)。无符号差值跨 49.7 天回绕安全 */ - if (s_con_cool_until != 0U) { + * 直接诱因 = 背靠连发的块超时链)。是否在冷却看 s_con_cooling 独立标志, + * **不再拿 tick 值当哨兵**(2026-10-24 实测:故障恰好落在 tick==0 那一拍时,旧写法 + * 记出的 cool_until==0 等于没记冷却,下一轮立刻再耗一次 100ms 块超时)。 + * 无符号差值跨 49.7 天回绕安全 */ + if (s_con_cooling != 0U) { if ((uint32_t)(t0 - s_con_cool_until) < DBG_CON_FAULT_COOL_MS) { s_con_draining = 0U; return; } - s_con_cool_until = 0U; + s_con_cooling = 0U; } while ((used = con_used()) != 0U) { @@ -309,12 +325,28 @@ void dbg_console_drain(void) { taskEXIT_CRITICAL(); if (st != HAL_OK) { + if (st == HAL_ERROR) { + /* 2026-10-24 修复:USART1 **未初始化**(huart1.Instance == NULL, + * 只可能出现在 HAL_Init 早期或断言/故障上报路径)与"僵尸通道"是两回事: + * 这里**绝不做外设重初始化**——那会在 HAL 尚未就绪时跑 MspInit 重配 + * RCC/GPIO;也不计 tx_fault(那本账的语义是"软复位自愈次数")。 + * 本块字节确实一个都没上线 -> 计入 dropped(只记真丢的), + * 末尾的 [ERR] dropped 行负责留痕;本轮到此为止(失败即止)。 */ + s_con_drop++; + break; + } /* 僵尸通道:软复位自愈 + 进入发送冷却(ORE 毫秒级重锁, * 继续背靠连发只会把主循环耗在块超时链上);本块丢弃 - * (行已出环,不可重放) */ + * (行已出环,不可重放),本块在超时账与丢弃账上各记一笔 */ + s_con_tx_timeout++; /* 新增账:块超时次数(TXE 与 TC 两条硬上界 + * 都算,口径 = 写者排空时观察到的块超时) */ con_uart_reinit(); s_con_tx_fault++; + s_con_drop++; /* 确认不了的块算真丢(一次丢弃事件), + * 旧写法只涨 tx_fault -> 账上看不出日志在丢, + * docs §13-O2 记的正是这条 */ s_con_cool_until = (uint32_t)HAL_GetTick(); + s_con_cooling = 1U; if (s_con_tx_fault == 1U) { static const char note[] = "\r\n[WARN] CONSOLE tx fault: UART reinit (self-healed)\r\n"; @@ -344,14 +376,21 @@ void dbg_console_drain(void) { uint32_t d = s_con_drop; int n; - s_con_drop = 0U; n = snprintf(note, sizeof(note), "\r\n[ERR] CONSOLE dropped %lu lines\r\n", (unsigned long)d); if (n > 0) { if (n > (int)sizeof(note) - 1) { n = (int)sizeof(note) - 1; } - dbg_console_write_raw(note, (unsigned)n); + /* 只有**真发出去了才销账**(2026-10-24 一并修正):旧写法先把计数清零 + * 再裸发,通道不可用/僵尸时那一发也失败 -> 丢弃证据被一起冲掉,[ERR] 行 + * 永远出不来("永不静默"变成"永不显示")。留到下一轮或串口恢复后补发。 */ + if (con_tx_bounded((const uint8_t *)note, (uint16_t)n) == HAL_OK) { + /* 按"本次报出去的这条"扣账而不是清零:期间生产者新丢的行留下轮报。 + * 递减只发生在这一处(单写者 + s_con_draining 重入保护),生产者只增, + * 故 s_con_drop >= d 恒成立,无需再写下溢分支(写了也永远测不到)。 */ + s_con_drop -= d; + } } } } @@ -360,6 +399,8 @@ void dbg_console_drain(void) { * 函数功能:把一段文本放入控制台(写者已注册)/直接发出(未注册或中断上下文) * 返回值:0 - 已入缓冲或已直发;-1 - 缓冲满丢弃(计入 dbg_console_dropped) * 函数说明:生产者不持锁等待过久:最多 DBG_CON_WAIT_MS,仍满则丢弃该行; + * "复核空位 -> 整行拷贝 -> 推进 head"是一个原子的临界区动作; + * 环满等待(vTaskDelay / DWT 判据)一律在临界区之外,区内绝不睡眠。 * 写者自身记录日志时先排空缓冲,避免自等。 * 等待上界优先用 DWT 真实时间(CON_APPEND_WAIT_CYC,时间源无关)—— * tick 停摆时 vTaskDelay 本身也会停,故不能用它作唯一判据(round 8b)。 @@ -392,10 +433,37 @@ static int con_append(const char *buf, uint16_t len) { } for (;;) { - uint16_t used = con_used(); - uint16_t free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - used); + uint16_t free_space; + int fitted = 0; + /* 复核空位 + 整行拷贝 + 推进 head **必须在同一个临界区内**(2026-10-24 修复): + * 旧写法先出区判 free_space >= len 再进区写,两个生产者可在环 >95% 满时同时过 + * 判据,head 合计越过容量 -> con_used() 回绕变小 -> 后写的行覆盖尚未发出的旧行, + * 且既不报错也不计丢弃(静默损坏)。本区最长拷 192B(168MHz 亚微秒级),相对 + * 115200 波特下一行约 16ms 可忽略;等待(vTaskDelay/DWT 判据)刻意留在区外。 */ + taskENTER_CRITICAL(); + free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - con_used()); if (free_space >= len) { + uint16_t head = s_con_head; + uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head); + uint16_t i; + + if (first > len) { + first = len; + } + for (i = 0U; i < first; i++) { + s_con_ring[head + i] = (uint8_t)buf[i]; + } + for (i = first; i < len; i++) { + s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] = + (uint8_t)buf[i]; + } + s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U)); + fitted = 1; + } + taskEXIT_CRITICAL(); + + if (fitted != 0) { break; } if (have_dwt != 0U) { @@ -411,26 +479,6 @@ static int con_append(const char *buf, uint16_t len) { waited++; } - taskENTER_CRITICAL(); - { - uint16_t head = s_con_head; - uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head); - uint16_t i; - - if (first > len) { - first = len; - } - for (i = 0U; i < first; i++) { - s_con_ring[head + i] = (uint8_t)buf[i]; - } - for (i = first; i < len; i++) { - s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] = - (uint8_t)buf[i]; - } - s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U)); - } - taskEXIT_CRITICAL(); - if (sys_evtHandle != NULL) { (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CONSOLE_TX); /* 唤醒写者 */ } @@ -472,6 +520,23 @@ uint32_t dbg_console_tx_fault(void) { return s_con_tx_fault; } +/* + * 函数功能:已发生的发送块超时次数(诊断用) + * 返回值:块超时累计次数 uint32_t + * 限定条件:无(任意上下文可读,含 ISR) + * 函数说明:三本账分开(docs/子系统设计/控制台单写者日志设计说明.md §13-O2)—— + * dbg_console_dropped() = **真没落地**的丢弃数(缓冲满丢的一行,或一个 + * 没能确认送达而出环的块;[ERR] 行报的就是它); + * dbg_console_tx_fault() = 外设软复位**自愈次数**(语义不变); + * dbg_console_tx_timeout() = 本账:写者排空时观察到的**块超时**次数 + * (TXE 与 TC 两条硬上界都算)。 + * 裸发路径(内核前 / ISR / 两条告警行自身)的超时不记本账——那些上下文里 + * 没有"块"的概念,也只可能发生在启动极早期,混进来只会让运维读数失去意义。 + * UART 物理失效但字节已发出的情形只涨本账,不污染另两本。 */ +uint32_t dbg_console_tx_timeout(void) { + return s_con_tx_timeout; +} + /* * 函数功能:取走"控制台 TX 故障待上报"标志(读后清) * 返回值:1 - 需要补记一条 run.log 证据,0 - 无 diff --git a/App/log/console_log.h b/App/log/console_log.h index 1566ae9e..b73e60ef 100644 --- a/App/log/console_log.h +++ b/App/log/console_log.h @@ -39,8 +39,13 @@ extern "C" { * (仅假设 defaultTask 在日志),其余任务此时落日志会与 * defaultTask 的存储初始化日志字节级交错(2026-10-10 台架实测) * dbg_console_drain - 写者任务排空缓冲并发送(单次有界;TX 失败自动软复位自愈) - * dbg_console_dropped - 因缓冲满丢弃的行数(诊断) - * dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断) + * dbg_console_dropped - 真没落地的丢弃数:缓冲满丢的一行,或一个没能确认送达 + * 而出环的块(诊断;[ERR] CONSOLE dropped 行报的就是它) + * dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断,语义不变) + * dbg_console_tx_timeout - 写者排空时观察到的块超时次数(TXE / TC 两条硬上界都算)。 + * 三本账分开:看日志有没有在丢 -> dropped;看外设自愈了几次 + * -> tx_fault;看 UART 物理失效(含"字节已发出但 TC 不置位") + * -> tx_timeout。别把三本账混成一个数(docs §13-O2 ) * dbg_console_tx_fault_log_take * - 取走"TX 故障待上报"标志(读后清)。**必须**由写者任务在 * dbg_console_drain() 之外调用后自行 syslog_post——本模块 @@ -55,6 +60,7 @@ uint8_t dbg_console_writer_ready(void); void dbg_console_drain(void); uint32_t dbg_console_dropped(void); uint32_t dbg_console_tx_fault(void); +uint32_t dbg_console_tx_timeout(void); uint8_t dbg_console_tx_fault_log_take(void); /* diff --git a/App/log/soe_store.c b/App/log/soe_store.c index 719b0b64..77b65864 100644 --- a/App/log/soe_store.c +++ b/App/log/soe_store.c @@ -3,6 +3,16 @@ * 模块功能:实现 soe_store.h——protectTask 投递(取时标 + soe_rec_build + * SPSC 待发环 / 消息队列,零 FS);defaultTask(syslog_drain 尾部) * 排空落 /log/soe.log(64B 定长,容量满删除重建)。 + * 修改记录: + * v1.0 2026-10-12 王建锋 创建初始版本(B9,设计见 docs/算法/继电保护功能复现规划.md §3.4) + * v1.1 2026-10-24 王建锋 仅注释/文档更正(D 项,无行为改动):s_soe_append + * 函数头原写"打开/写入结构性错误 → unlink 重试一次 + * (同 run.log)",与实现不符且方向错误——实现里 + * f_open 失败直接返回 -1、不 unlink,这是**有意**的: + * SOE 是事件序列,打开失败就删文件等于销毁已发生的 + * 事件。改为如实描述"仅写失败才重试一次",并补上 + * 与 sys_log_mcu.c s_is_selfheal_code 同源的逐码表 + * (名字/参数类错误码两侧都绝不 unlink) * 创建日期:2026-10-12(B9) */ @@ -88,6 +98,30 @@ void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) { } } +/* + * ── 自愈触发集逐码依据(与 sys_log_mcu.c 的同名资料块同源;2026-10-24 逐条核对 + * Lib/FatFs/ff.c)───────────────────────────────────────────────────────── + * 进触发集(run.log 侧允许 unlink 重建一次):介质/结构类 FR_DISK_ERR(1) / + * FR_INT_ERR(2) / FR_NO_FILESYSTEM(13) / FR_DENIED(7)。本模块**只在写失败路径** + * 上碰自己的文件——打开失败连这些码也不 unlink。 + * 不算自愈:FR_NO_FILE(4)——带 FA_OPEN_APPEND 打开时文件会被直接建出来;返回本 + * 码的唯一现实场景是同名对象是目录(ff.c:3854),unlink 修不好它。 + * 两侧都绝不 unlink:名字/参数类 FR_INVALID_NAME(6) / FR_NO_PATH(5) / + * FR_INVALID_OBJECT(9),以及 FR_NOT_READY(3) / FR_NOT_ENABLED(12) / + * FR_TIMEOUT(15)——同名同因,删了也修不好,反而每轮丢一份历史(§10.1 第 10 条 + * 严禁的正是这种归集)。 + * 写失败路径不接这套判定:f_write 的返回码里不存在名字/参数类(能走到那里说明 + * 已经打开成功),而簇分配失败是 FR_OK + 短写、根本没有错误码可判,只能无条件 + * 重试一次。 + * 为什么 SOE 比 run.log 保守(两侧资料块各写一份、互指,改动必须同步):run.log + * 是滚动运行日志,坏了重建损失有限;SOE 是**事件序列**,文件里是一条条已经发生 + * 过的保护事件,"打开失败就删整个历史文件"等于**销毁事件证据**——SOE 的存在就 + * 是为了留这些事件,删了比写不进去更糟。正确出路是等 syslog_mcu_mounted() 每 2s + * 重试补目录 / 等卷恢复,条目留在待发环与队列里,不丢。 + * (run.log 侧的同一张表 + 同一理由见 App/log/sys_log_mcu.c 的同名资料块,测试 + * 侧两侧对齐由 TC-SYSLOG-101/102 与 TC-SOESTORE-022/022b 分别钉住。) + */ + /* * 函数功能:追加一条 64B SOE 记录到 /log/soe.log * 入口参数:rec - 完整记录(SOE_REC_SIZE 字节) const uint8_t * @@ -98,11 +132,20 @@ void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) { * ② FA_WRITE|FA_OPEN_APPEND(本移植 OPEN_APPEND 不含 WRITE, * 缺写位 f_write 恒 FR_DENIED,2026-09-20 实证); * ③ 判失败必须校验写回字节数(FR_OK+短写 = 失败,簇分配无错误码); - * ④ 容量满 unlink 重建;打开/写入结构性错误 → unlink 重试一次 - * (L1 文件级自愈,同 run.log)。 + * ④ 容量满 unlink 重建;**仅写入失败(含 FR_OK+短写)才 unlink 重试 + * 一次**——f_open 失败一律直接返回 -1、不 unlink。 + * (2026-10-24 定案:本条旧注释写的是"打开/写入结构性错误都 unlink + * 重试一次(同 run.log)",与实际实现不符,也与本模块的语义相反: + * run.log 是**滚动运行日志**,坏了重建损失有限;SOE 是**事件序列**, + * 文件里是已经发生的事件,打开失败就把整个历史文件删掉等于销毁 + * 证据,方向是错的。正确出路是让 syslog_mcu_mounted() 每 2s 重试 + * 补目录/等卷恢复,条目留在待发环与队列里,不丢。) + * ⑤ 自愈触发集与 sys_log_mcu.c 的 s_is_selfheal_code() 同源,但本模块取 + * 更保守的子集:打开失败**任何错误码都不 unlink**,只有写失败才重试 + * 一次。逐码依据与两侧差异见上方同名资料块。 * ⚠ 本模块**没有**任何"卷损坏"归类/重格式化路径:unlink 失败、 * 名字/参数类错误码一律只留痕(存储架构 §10.1 教训),条目丢弃 - * 计数,绝不升级成卷操作。 + * 计数,绝不升级成卷操作(全模块无 f_mount/f_mkfs/f_opendir)。 */ static int s_soe_append(const uint8_t *rec) { static FIL f; /* static:FF_FS_TINY=0 → FIL=528B 栈; @@ -129,8 +172,9 @@ static int s_soe_append(const uint8_t *rec) { fr = f_write(&f, rec, SOE_REC_SIZE, &bw); (void)f_close(&f); if ((fr != FR_OK) || (bw != SOE_REC_SIZE)) { - /* L1 文件级自愈:陈旧/损坏条目 → 删除重建、重试一次(同 run.log; - * unlink 返回值刻意不检查——失败也只是重试失败 → 留痕,不升级) */ + /* L1 文件级自愈:**只有写失败**才删除重建、重试一次(打开失败走下面 + * 的 else,不删——见函数头 ④)。unlink 返回值刻意不检查:失败也只是 + * 重试失败 → 留痕计数,绝不升级成卷操作。 */ (void)f_unlink(SOE_PATH); bw = 0u; fr = f_open(&f, SOE_PATH, FA_WRITE | FA_OPEN_APPEND); @@ -143,6 +187,8 @@ static int s_soe_append(const uint8_t *rec) { } } } else { + /* 打开失败:只留痕、**不 unlink**(SOE 是事件序列,删文件 = 销毁历史; + * 名字/参数类错误码 unlink 也修不好,见函数头的逐码表) */ rc = -1; } if (rc == 0) { diff --git a/App/log/sys_log.h b/App/log/sys_log.h index a8c81691..3d7c588b 100644 --- a/App/log/sys_log.h +++ b/App/log/sys_log.h @@ -7,6 +7,13 @@ * 创建日期:2026-09-14 * 修改记录: * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4) + * v1.1 2026-10-24 王建锋 仅契约注释更正,与 App/log/sys_log_mcu.c 的 C/F 两项 + * 整改同步(无签名、无行为改动): + * syslog_get_record 写死容量要求(ts_len > SYSLOG_TS_LEN、 + * desc_len > SYSLOG_DESC_MAX)并明确"容量不足一律 -1、 + * 不截断、不写调用方缓冲"——旧注释像是"给少了会被截断", + * 实际是整条读不到;syslog_post 写明必须假设丢弃存在、 + * 且丢弃留痕已按通道节流(节流后无丢弃计数) */ #ifndef SYS_LOG_H @@ -61,6 +68,14 @@ typedef enum { * desc - 描述串(UTF-8,≤SYSLOG_DESC_MAX 字节;超长截断) const char * * 返回值:无 * 限定条件:MCU 端 FreeRTOS 启动前调用走静态待发数组(由 defaultTask 挂载后落盘) + * 函数说明:调用方**必须假设投递可能被丢弃**——MCU 后端(sys_log_mcu.c)队列 8 槽、 + * 启动前待发数组 8 槽,卷被 ftpTask 长期占用时 defaultTask 会把整批回投 + * 队列(队列因此可长时间处于满态),此时本函数直接丢弃该条,并只在 + * "转入故障"的那一次留痕(丢弃留痕按正常→故障节流,F 项整改 2026-10-24; + * 节流后**丢弃总数没有计数**,要计量得另加计数器)。必须留住的证据走 + * sys_log_evidence(),不要靠 syslog_post 重试。 + * 模拟器后端(simulator/sim/sim_syslog.c)是单线程直写宿主文件、**没有 + * 队列与丢弃**,因此上面这段满态语义只约束 MCU 端。 */ void syslog_post(syslog_id_t id, const char *desc); @@ -75,11 +90,19 @@ uint32_t syslog_count(syslog_id_t id); * 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾) * 入口参数:id - 日志 ID syslog_id_t * idx - 记录序号(0 起) uint32_t - * p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节;不可为 NULL) char * - * ts_len- p_ts 容量 size_t - * p_desc- 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节;不可为 NULL) char * - * desc_len - p_desc 容量 size_t - * 返回值:0 - 成功;-1 - 越界/读失败 int + * p_ts - 时间戳输出缓冲(不可为 NULL) char * + * ts_len- p_ts 容量,必须 > SYSLOG_TS_LEN(即 ≥ SYSLOG_TS_LEN+1 = 20 字节) size_t + * p_desc- 描述输出缓冲(不可为 NULL) char * + * desc_len - p_desc 容量,必须 > SYSLOG_DESC_MAX(即 ≥ 29 字节) size_t + * 返回值:0 - 成功;-1 - 越界 / 读失败 / **缓冲容量不足** int + * 限定条件:容量不足一律返回 -1,**不做静默截断、也不写调用方一个字节** + * (C 项整改 2026-10-24;旧注释只写"≥…+1 字节"像是"给少了会被截断", + * 照它给 28 字节缓冲的结果是整条读不到而不是读到 27 字符)。 + * 描述字段在写入侧已截到 SYSLOG_DESC_MAX,所以按上面的最小容量给缓冲 + * 就一定装得下,读侧不需要第二道截断。 + * MCU 后端与模拟器后端(sim_syslog.c)在这条守卫上取值相同,行为一致; + * 模拟器侧那份同样不可达的截断支还留在文件里(未列入本轮改动范围)。 + * 调用方:uiTask 的日志页(行缓冲 32B)与 SYSLOG 取证打印。 */ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len); diff --git a/App/log/sys_log_mcu.c b/App/log/sys_log_mcu.c index 31ec693e..d379b1ff 100644 --- a/App/log/sys_log_mcu.c +++ b/App/log/sys_log_mcu.c @@ -16,6 +16,24 @@ * v1.3 2026-10-02 王建锋 新增 sys_log_evidence():把看门狗停喂现场 * 证据落 /log/wdev.log(掉电不丢)+ flush 诱导 * (round 7 纯取证,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录) + * v1.5 2026-10-24 王建锋 主机单测批次照出的缺陷一起修,本文件内 5 条 + * (A/B/C/D/F;E 在 App/cfg/cfg_file_store_mcu.c): + * A 卷忙整批静默丢日志——syslog_drain 批量路径补 + * rc==-2 分支把 done 置 0,整批回投队列(不计故障、 + * 不打日志),与 -3 慢路径遇 -2 的处置对齐; + * B 同分支文案 "drop N entries" 与行为相反(其实全 + * 部回投),改成如实描述并按"正常→故障"节流一条; + * C 删掉 syslog_get_record 里永不可达的截断支,容量 + * 要求写进函数头与 sys_log.h(宁可拒绝不静默截断); + * D 自愈触发集按语义拆类(新增 s_is_selfheal_code), + * 把 FR_INVALID_NAME / FR_NO_PATH / FR_NO_FILE / + * FR_INVALID_OBJECT 移出 unlink 重建集合——旧集合在 + * 名字/参数类错误下每轮删一次自己的历史日志且永不 + * 自愈,违反存储架构 §10.1 第 10 条 + * F syslog_post 的丢弃留痕按通道节流(队列满 / 启动前 + * 数组满各立 volatile 抑制位,与 B 的 drain 侧那位分 + * 开):卷被长期占用 + A 项回投让队列常驻满时,串口 + * 不再按投递频率刷 "queue full, drop" * v1.4 2026-10-04 王建锋 P2-9:syslog_mcu_mounted() 改为幂等 + 卷锁保护; * defaultTask 每 2s 复查,建 /log 曾失败时自动重试 * (原"仅调用一次"→ 失败后 drain 永久 no-op)。 @@ -65,6 +83,22 @@ static osMessageQueueId_t s_queue; /* 运行期消息队列(s static const osMessageQueueAttr_t s_queue_attr = { .name = "syslog" }; static volatile uint8_t s_fs_ready; /* 卷挂载 + /log 目录就绪后置位 */ static volatile uint8_t s_mount_fail_reported; /* P2-9:建目录失败日志去重(成功复位) */ +/* B 项整改(2026-10-24):syslog_drain 批量写失败"整批回投"这条留痕的节流位—— + * 只在"从正常转入故障"的那一次打一条,成功一轮后复位。与 s_append_many 内部的 + * s_batch_fault 各管一层:那位压的是"这个文件写坏了",本位压的是"这批条目回投了"。 + * 唯一写者 defaultTask,免锁。 */ +static uint8_t s_requeue_reported[SYSLOG_COUNT]; + +/* F 项整改(2026-10-24,Lead 批准并入本轮):syslog_post 的丢弃留痕节流。 + * 场景:卷被 ftpTask 长期占用 -> A 项修复后整批回投 -> 队列常驻满 -> 此后每一条 + * 日志投递都会"队列满 + 打一条 DBG_ERROR",串口按业务投递频率刷屏(日志本身 + * 变成负载)。与上面 s_requeue_reported **分开**是两个必然:那位归 defaultTask + * (唯一写者,免锁),本位由**任意上下文**的投递者读写,且恢复时机是"下一次投递 + * 成功"而不是"下一批落盘成功"。两个位各管一个通道:队列 / 启动前待发数组。 + * volatile + 单字节:Cortex-M 上对齐字节读写原子;跨任务竞态的最坏后果是"同一轮 + * 故障多打一行"或"少打一行",不会漏丢条目、也不会长时间静默(下一次成功会复位)。 */ +static volatile uint8_t s_post_qfull_reported; /* 队列满丢弃:已留痕? */ +static volatile uint8_t s_post_pboot_full_reported;/* 启动前数组满丢弃:已留痕? */ /* N-24(2026-10-15,报告 N-16 读侧):8 条滑动读窗口 + 写者代数。 * 窗口 512 B 走静态(不吃任务栈;defaultTask/uiTask 最坏链见 stack_report.py, @@ -80,6 +114,72 @@ static volatile uint32_t s_rd_gen[SYSLOG_COUNT]; /* 每次成功落盘 / unli /* 函数定义区 */ +/* + * ── 自愈触发集逐码依据(配套下方 s_is_selfheal_code;2026-10-24 逐条核对 + * Lib/FatFs/ff.c,行号即该文件)──────────────────────────────────────── + * 旧实现把 FR_INVALID_NAME / FR_NO_PATH / FR_NO_FILE / FR_INVALID_OBJECT 一并 + * 放进触发集,违反《STM32F4-Base 存储架构说明》§10.1 第 10 条:名字/参数类错误 + * 码严禁归入"卷损坏"处置集——这类码下 unlink 自己的文件必然同样失败(同名同因), + * 后果是每轮排空删一次自己的历史日志、且永远修不好(run.log 1024 条 / + * selftest.log 全是被删对象)。 + * + * 进触发集(删了这个文件真能修好): + * FR_DISK_ERR(1) 低层 I/O 失败(ff.c:1683/1739/1785 等)——陈旧簇链或坏 + * 目录项,unlink 后重建换新链有效; + * FR_INT_ERR(2) 内部一致性断言失败(ff.c:1379/1460/3869)——本文件的目录 + * 项/簇号已不自洽,只能废弃重建; + * FR_NO_FILESYSTEM(13) 卷上找不到有效 FAT(ff.c:3483 起)——本模块**不**做任何 + * 卷级动作(全模块无 f_mount/f_mkfs),只对本文件废弃重建 + * 一次并留痕,卷怎么办归挂载门; + * FR_DENIED(7) 两条真实成因都能被 unlink 治好:目标被置只读属性 + * (ff.c:3808/3859),以及目录区无空目录项(ff.c:1850 由 + * FR_NO_FILE 改写而来)——后者删掉自己这个文件正好腾出一 + * 个目录项。陷阱一那类"缺写位"的 FR_DENIED 出现在 f_write + * 上,不走本判定。 + * + * 不进触发集(一律立即失败,不 unlink、不重开、不计卷损坏): + * FR_INVALID_NAME(6) 8.3 短名违规 / 非法字符 / 以点开头(非 LFN 配置下的 + * create_name)——unlink 同名同因失败,永不自愈; + * FR_NO_PATH(5) 中间路径段缺失(ff.c:3144/4357)= /log 目录不在:重建 + * 文件不解决目录,补目录归 syslog_mcu_mounted() 的每 2s 重试 + * (见该函数注释里的幂等改造说明),两侧各管一层; + * FR_NO_FILE(4) 本模块用 FA_WRITE|FA_OPEN_APPEND(含 FA_OPEN_ALWAYS), + * "文件不存在"会被直接创建出来;返回本码的唯一现实场景是 + * 同名对象是目录(ff.c:3854),unlink 一个目录修不好它; + * FR_INVALID_OBJECT(9) 仅 ff.c:3774 的 !fp 分支,即句柄指针为 NULL——本模块用 + * 函数级 static FIL,地址恒非 0,属不可达码; + * FR_NOT_READY(3) / FR_NOT_ENABLED(12) / FR_TIMEOUT(15)——卷未挂载、无工作区、 + * FF_FS_REENTRANT 取卷超时,属挂载门与并发范畴,与"这个 + * 文件坏了"无关。 + * + * 两点边界,免得后人当成漏改: + * 1) f_write 之后那条"失败/短写 -> unlink 重建重试一次"**不接**本判定:其前提 + * 是已经打开成功,名字/参数类错误到不了那里;而簇分配失败是 FR_OK + 短写 + * (根本没有错误码可判),必须无条件重试。 + * 2) App/log/soe_store.c 对同一张表取**更保守的子集**:那边**打开失败连介质类 + * 码也不 unlink**。原因不是它漏改,而是两个文件的对象不同——run.log 是滚动 + * 运行日志,文件坏了重建损失有限;soe.log 是**事件序列**,里面是一条条已经 + * 发生过的保护事件,"打开失败就删掉整个文件"等于**销毁事件证据**(SOE 的存 + * 在即目的,删了比写不进去更糟)。所以 SOE 侧的出路是等卷/目录恢复(条目留 + * 在待发环与队列里,不丢),只有"已经打开成功之后写失败"才允许废弃重建。 + * 两侧资料块必须同步修改、互指(彼处在 App/log/soe_store.c 的同名资料块)。 + */ +/* + * 函数功能:判定一个 f_open 返回码是否属于"把自己的日志文件删掉重建一次"能修好 + * 的那一类,即 L1 文件级自愈的触发集(D 项整改,逐码依据见上方资料块) + * 入口参数:fr - f_open 的返回码 FRESULT + * 返回值:1 - 属触发集,可 unlink 后重开一次;0 - 不属,一律立即失败、不 unlink、 + * 不计卷损坏 int + * 限定条件:纯分类、无任何副作用;调用点只有 s_append_rec() 与 s_append_many() + * 的"打开失败"分支 + * 函数说明:拆成函数是为了两处调用点共用同一张表,避免历史上"改一处漏一处" + */ +static int s_is_selfheal_code(FRESULT fr) { + return ((fr == FR_DISK_ERR) || (fr == FR_INT_ERR) || + (fr == FR_NO_FILESYSTEM) || (fr == FR_DENIED)) ? 1 : 0; +} + + /* * 函数功能:格式化一条 64 B 定长记录(时间戳 + 空格 + epoch + 空格 + 描述) * 入口参数:id - 日志 ID(当前不参与编码,仅为接口对称) syslog_id_t @@ -155,10 +255,9 @@ static int s_append_rec(syslog_id_t id, const uint8_t *p_rec) { * FA_OPEN_APPEND(0x30) 不含 FA_WRITE(0x02),不带写位打开的文件 f_write * 恒返回 FR_DENIED(7)——2026-09-20 台架实测 /log 写失败 ×5/开机 真根因 */ fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND); - if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT || - fr == FR_INT_ERR || fr == FR_DISK_ERR || - fr == FR_NO_FILE || fr == FR_NO_PATH || - fr == FR_INVALID_NAME || fr == FR_NO_FILESYSTEM)) { + /* D 项整改(2026-10-24):只有"删了这个文件能修好"的介质/结构类错误码才 + * 进这里;名字/参数类一律不 unlink —— 判据见 s_is_selfheal_code() */ + if ((fr != FR_OK) && (s_is_selfheal_code(fr) != 0)) { (void)f_unlink(s_paths[id]); fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND); } @@ -255,10 +354,9 @@ static int s_append_many(syslog_id_t id, const uint8_t *p_buf, uint32_t n) { total = (UINT)(n * SYSLOG_REC_SIZE); fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND); - if (fr != FR_OK && (fr == FR_DENIED || fr == FR_INVALID_OBJECT || - fr == FR_INT_ERR || fr == FR_DISK_ERR || - fr == FR_NO_FILE || fr == FR_NO_PATH || - fr == FR_INVALID_NAME || fr == FR_NO_FILESYSTEM)) { + /* D 项整改(2026-10-24):只有"删了这个文件能修好"的介质/结构类错误码才 + * 进这里;名字/参数类一律不 unlink —— 判据见 s_is_selfheal_code() */ + if ((fr != FR_OK) && (s_is_selfheal_code(fr) != 0)) { (void)f_unlink(s_paths[id]); fr = f_open(&f, s_paths[id], FA_WRITE | FA_OPEN_APPEND); } @@ -524,19 +622,36 @@ void syslog_drain(void) { break; /* 写失败:该条已丢,尾部回投 */ } } - } else if (rc == -1) { + } else if (rc == -2) { + /* A 项整改(2026-10-24,主机单测照出):卷忙 = **整批回投队列**, + * 静默顺延——不计故障、不打日志(本函数每 2s 排空一次,一打就刷屏), + * 与上面 -3 慢路径遇 -2 的处置对齐。旧实现在这里没有分支:done 保持 n + * → 下面的回投循环空转 → 最多 8 条既不落盘也不回投,静默丢失。 */ done = 0u; - DBG_ERROR("drop %lu entries id=%d", (unsigned long)n, (int)bid); + } else if (rc == -1) { + /* B 项整改(2026-10-24):旧文案写 "drop %lu entries",但本分支 + * done = 0 → 整批其实**全部回投**、一条没丢(下轮重试),文案与行为 + * 相反会把排障方向带错;改成如实描述,并加"正常→故障"节流(持续故障 + * 时每 2s 一条会自己刷成新一轮刷屏,真丢另有下面 hold-over drop 兜底)。 */ + done = 0u; + if (s_requeue_reported[bid] == 0u) { + s_requeue_reported[bid] = 1u; + DBG_ERROR("write fail, requeue %lu entries id=%d", + (unsigned long)n, (int)bid); + } } - /* rc == -2(卷忙):整批回投队列,静默顺延,不计故障 */ for (i = done; i < n; i++) { if (osMessageQueuePut(s_queue, &s_batch_src[i], 0u, 0u) != osOK) { DBG_ERROR("hold-over drop (queue full): id=%d", (int)s_batch_src[i].id); } } - /* 本批消耗的 preboot 条目:已落盘的删除;未落盘的已转投队列,同样删除 - * (否则会重复落盘) */ + if (rc == 0) { + s_requeue_reported[bid] = 0u; /* 恢复正常:允许下一次故障重新留痕 */ + } + /* 本批消耗的 preboot 条目:已落盘的删除;未落盘的(-1/-2/-3 尾部)已在 + * 上面转投队列,同样从数组删除——**由队列接手重投**,不是丢弃 + * (A 项整改前 -2 走不到回投,这里删掉就等于吞掉) */ if (n_pre > 0u) { memmove(&s_preboot[0], &s_preboot[n_pre], sizeof(syslog_entry_t) * (size_t)(s_preboot_cnt - n_pre)); @@ -558,6 +673,12 @@ void syslog_drain(void) { * desc - 描述串(UTF-8,静态存储期,超长截断) const char * * 返回值:无 * 限定条件:FreeRTOS 启动前走静态待发数组;desc 必须为静态/常量串 + * 函数说明:丢弃留痕按"正常→故障"节流(F 项整改 2026-10-24):每个通道只在 + * 转入故障的那一次打一条,恢复(下一次成功收下)后复位。 + * **丢弃语义本身没变**(队列满/数组满仍然是丢),变的只有留痕次数—— + * 卷被 FTP 长期占用时 A 项修复让整批回投、队列常驻满,若这里不节流, + * 串口就按业务投递频率刷"queue full, drop",日志自己变成负载。 + * 抑制位是 volatile 单字节、由任意上下文读写:竞态最坏只多打/少打一行。 */ void syslog_post(syslog_id_t id, const char *desc) { syslog_entry_t e; @@ -569,15 +690,33 @@ void syslog_post(syslog_id_t id, const char *desc) { e.desc = desc; if (s_queue != NULL && osKernelGetState() == osKernelRunning) { if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) { - DBG_ERROR("queue full, drop: %s", desc); + /* F 项:只报"从正常转入故障"那一次;期间条目照常丢弃(丢弃语义不变, + * 变的只是留痕次数)。下一次投递成功即复位。 */ + if (s_post_qfull_reported == 0u) { + s_post_qfull_reported = 1u; + DBG_ERROR("queue full, drop: %s", desc); + } + } else { + /* 恢复正常:清抑制位,下一次故障重新留痕。这里刻意无条件写 + * (曾试成"仅置位时才清",被 cppcheck duplicateConditionalAssign + * 判成等价赋值并拦提交闸;本函数上方已做过一次条目拷贝,多一条 + * STR 可以忽略,别为它加 inline-suppr)。 */ + s_post_qfull_reported = 0u; } } else { uint8_t n = s_preboot_cnt; if (n < SYSLOG_QUEUE_MAX) { s_preboot[n] = e; s_preboot_cnt = n + 1u; - } else { + s_post_pboot_full_reported = 0u; /* 恢复正常:同上,无条件复位 */ + } else if (s_post_pboot_full_reported == 0u) { + /* 同一纪律的另一通道:启动前数组只有 SYSLOG_QUEUE_MAX 槽,挂载慢 + + * 开机事件密集时会在整个"未就绪窗口"里每条投递打一行(比队列通道更早 + * 发生、也更密集)。数组重新收下第一条即视为恢复。 */ + s_post_pboot_full_reported = 1u; DBG_ERROR("preboot full, drop: %s", desc); + } else { + /* 故障期间静默,条目照常丢弃 */ } } } @@ -603,11 +742,14 @@ uint32_t syslog_count(syslog_id_t id) { * 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾) * 入口参数:id - 日志 ID syslog_id_t * idx - 记录序号(0 起) uint32_t - * p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char * - * ts_len - p_ts 容量 size_t - * p_desc - 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节,不可为 NULL) char * - * desc_len - p_desc 容量 size_t - * 返回值:0 - 成功;-1 - 越界/读失败 int + * p_ts - 时间戳输出缓冲(不可为 NULL) char * + * ts_len - p_ts 容量,**必须 > SYSLOG_TS_LEN**(即至少 20 字节) size_t + * p_desc - 描述输出缓冲(不可为 NULL) char * + * desc_len - p_desc 容量,**必须 > SYSLOG_DESC_MAX**(即至少 29 字节) size_t + * 返回值:0 - 成功;-1 - 越界/读失败/缓冲容量不足(不截断、不写入调用方缓冲) int + * 限定条件:容量不足一律拒绝而不是截断(C 项整改 2026-10-24;理由见函数体注释)。 + * 记录里的描述字段本身已在写入侧截到 SYSLOG_DESC_MAX,所以按上面 minimum + * 给缓冲就一定装得下,读侧无需第二道截断。 */ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len) { uint8_t rec[SYSLOG_REC_SIZE]; @@ -624,9 +766,12 @@ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, c while (dl < SYSLOG_DESC_MAX && rec[SYSLOG_DESC_OFFS + dl] != '\0') { dl++; } - if (dl >= desc_len) { - dl = desc_len - 1u; - } + /* C 项整改(2026-10-24):这里原来还有一句 "if (dl >= desc_len) dl = desc_len + * - 1u"——**永不可达**:上面的守卫已把 desc_len <= SYSLOG_DESC_MAX 全部挡掉, + * 而 dl 由定长记录的内容决定、恒 <= SYSLOG_DESC_MAX。删掉它,容量要求改由 + * 函数头与 sys_log.h 写死:**宁可整条拒绝,也不做静默截断**(UI 拿到截断串 + * 会显示成一条被删短过的日志,比读不到更难查)。desc_len 的校验仍保留, + * 它是"调用方给够缓冲"这条契约的执行点,不是截断开关。 */ memcpy(p_desc, rec + SYSLOG_DESC_OFFS, dl); p_desc[dl] = '\0'; return 0; diff --git a/Drivers/BSP/NET/lftpd/lftpd.c b/Drivers/BSP/NET/lftpd/lftpd.c index ffec9da4..66dbdb8a 100644 --- a/Drivers/BSP/NET/lftpd/lftpd.c +++ b/Drivers/BSP/NET/lftpd/lftpd.c @@ -545,6 +545,10 @@ static int cmd_pasv(lftpd_client_t *client, const char *arg) return -1; } + /* s_addr 是网络序整数(本层唯一约定,见 net_inet_addr 的声明处),这里 + * 先转主机序再按高字节到低字节拆 = 线序顺位,PASV 227 的 IP 文本因此正确 + * (TRAP-NET-21 已定案"双重反转恰好正确,勿误判";2026-10-24 字节序统一后复核: + * 本行不属于被统一掉的那一族,语义未变,勿"顺手改对"成直接拆 s_addr) */ uint32_t ip = net_htonl(local_addr.sin_addr.s_addr); send_simple_response(client->socket, 227, STATUS_227, (ip >> 24) & 0xff, diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 7c060932..303dbdc7 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -78,6 +78,14 @@ static volatile uint32_t s_net_generation = 0; /* 网络代次号:网络 */ static osMessageQueueId_t s_net_msg_queue; +/* + * 本层执行任务句柄(= net_init 的调用者,NET 层为 netTask、硬件层为 testTask) + * 与 net_pending_capture_owner() 同一时刻、同一语义捕获,判据也一致。 + * 用途见 net_ctx_is_owner_task():给"绕过消息队列直调芯片"的接口把危险面关掉。 + * 捕获前恒 NULL(net_init 未跑)——那时任何 socket 都不存在,判空放行即可。 + */ +static TaskHandle_t s_net_owner_task; + /* * 函数功能:绑定消息队列句柄(本模块是该句柄的唯一使用者) * 入口参数:queue_id - 已创建的消息队列句柄 @@ -87,6 +95,30 @@ static osMessageQueueId_t s_net_msg_queue; * 真出现两处绑定时,说明有人新加了调用点,此时保留先绑的那个并把后来者 * 拒掉,比静默换句柄(已入队的请求会留在旧队列里永不处理)更容易暴露。 */ +/* + * 函数功能:判当前上下文是否本层执行任务(netTask / 硬件层的 testTask) + * 入口参数:无 + * 出口参数:无 + * 返回值:1 - 是本层执行任务(或未捕获,见下);0 - 明确不是(应拒绝直调芯片) + * 限定条件:net_init 之后判据才有效 + * 函数说明:给 net_sendto / net_recvfrom 这类**绕过消息队列、在调用方上下文里 + * 直接 net_poll + 读写芯片缓冲**的接口把危险面结构性关掉:非执行任务 + * 调用会与本层的 net_poll 抢同一根 SPI2(TRAP-NET-14 的根因形态), + * 而 SPI 事务没有跨任务互斥(ch395f_spi_begin/end 只是片选与分频)。 + * 刻意**不做**成"排队改写":recvfrom 要把源地址交回调用方,按 AGENTS + * 不能塞进按值拷贝的 net_msg_t,真要串行化得设计 per-task 结果槽, + * 那是需要真机回归的独立工程(2026-10-24 定案:本轮先把危险面关掉,串行化留后续)。 + * 未捕获(net_init 未跑)时返回 1:那时任何 socket 都不存在, + * 调用只会先在参数检查上拿到 BADF,不构成绕过。 + */ +static uint8_t net_ctx_is_owner_task(void) { + if (s_net_owner_task == NULL) { + return 1U; + } + /* 内核未启动/中断上下文里句柄为 NULL:与执行任务不等 → 判不是(不得碰 SPI) */ + return (xTaskGetCurrentTaskHandle() == s_net_owner_task) ? 1U : 0U; +} + int net_msg_queue_adopt(osMessageQueueId_t queue_id) { if (queue_id == NULL) { return -1; @@ -253,6 +285,9 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { net_set_errno(NET_OK); s_net_initialized = 0; net_pending_capture_owner(); /* 执行任务 = net_init 调用者(NET 层 = netTask / 硬件层 = testTask) */ + /* 同一时刻捕获本层自己的执行任务句柄(net_ctx_is_owner_task 的判据; + * 必须与上面同源,否则两张守卫会各认一个"谁是 netTask") */ + s_net_owner_task = xTaskGetCurrentTaskHandle(); /* 检查 CH395F 芯片 */ if (ch395f_check_exist() != CH395F_STATUS_OK) { @@ -1561,14 +1596,16 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a p_sock->remote_ip = p_addr_in->sin_addr.s_addr; p_sock->remote_port = net_ntohs(p_addr_in->sin_port); - /* 转换 IP 为数组格式(大端/网络字节序,与 ch395f set_des_ip 期望一致, - * 同 Phase2 phase2_tc201 的 test_parse_ip 结果 [192,168,1,2]) */ - p_sock->remote_ip_arr[0] = (uint8_t)(p_sock->remote_ip >> 24); - /* 最高字节 → [0](大端序) */ - p_sock->remote_ip_arr[1] = (uint8_t)(p_sock->remote_ip >> 16); - p_sock->remote_ip_arr[2] = (uint8_t)(p_sock->remote_ip >> 8); - /* 最低字节 → [3] */ - p_sock->remote_ip_arr[3] = (uint8_t)(p_sock->remote_ip); + /* 转换 IP 为数组格式(芯片要的是**线序**数组:[0]=第一段,与 ch395f_set_des_ip + * 期望一致,同 Phase2 phase2_tc201 的 test_parse_ip 结果 [192,168,1,2])。 + * s_addr 是网络序整数,其**内存字节顺位**就是线序(2026-10-24 定案:全仓 s_addr 只此一族), + * 所以这里按字节顺位取、不做数值右移:旧写法 remote_ip_arr[0] = remote_ip >> 24 + * 取的是数值的最高字节,只配得上当时那套「第一段在最高字节」的 inet_addr; + * inet_addr 已改成 POSIX 网络序后,右移写法会把地址拆成 [100,1,168,192] 反序, + * 表现为「连谁都不知去哪了」。用 memcpy 取顺位与 net_events.c 填 remote_ip_arr + * 的写法同一形制(那边一直就是线序顺位),两端不再各说一套。 */ + (void)memcpy(p_sock->remote_ip_arr, &p_sock->remote_ip, + sizeof(p_sock->remote_ip_arr)); /* 配置 CH395F Socket:先 OPEN 再写源/目的端口(避免 open 复位寄存器) */ /* 设置协议类型为 TCP */ @@ -2429,27 +2466,34 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, return NET_ERR_RETURN_1(NET_ERR_BADF); } - /* 缓冲区无效 → INVAL */ + /* 缓冲区无效 → INVAL(本块必须留在守卫之前:参数错误先报参数错误) */ if (buf == NULL || len <= 0) { return NET_ERR_RETURN_1(NET_ERR_INVAL); } + /* netTask 上下文守卫:本接口在本任务上下文里直接 net_poll + 读写芯片缓冲, + * 非执行任务调用会与本层 net_poll 抢同一根 SPI2(TRAP-NET-14)。放在参数 + * 校验**之后**:越界/未在用/参数非法这些正常错误路径照旧先报,守卫不吞它们。 + * 判据与留痕见 net_ctx_is_owner_task()。 */ + if (net_ctx_is_owner_task() == 0U) { + DBG_ERROR("sendto from non-exec task: refused (SPI race, use netTask)"); + return NET_ERR_RETURN_1(NET_ERR_NOT_IN_NETTASK); + } + /* UDP 模式:设置目标地址 */ if (dest_addr != NULL && addrlen >= (int)sizeof(struct net_sockaddr_in)) { /* 强转为 IPv4 地址结构体 */ p_addr_in = (const struct net_sockaddr_in *)dest_addr; - /* 转换 IP 为大端数组(同 TCP 客户端,set_des_ip 期望大端) */ - /* 大端 IP 数组 */ + /* 转换 IP 为线序数组(同 TCP 客户端:set_des_ip 要 [0]=第一段)。 + * s_addr 的内存字节顺位即线序(2026-10-24 定案:全仓 s_addr 只此一族),按顺位取, + * 不再按数值右移取最高字节——理由与 net_connect_locked 内同一处相同 */ + /* 线序 IP 数组 */ uint8_t ip_arr[4]; /* 目的 IP(网络序 32 位) */ uint32_t ip = p_addr_in->sin_addr.s_addr; - /* 最高字节 → [0](大端序) */ - ip_arr[0] = (uint8_t)(ip >> 24); - ip_arr[1] = (uint8_t)(ip >> 16); - ip_arr[2] = (uint8_t)(ip >> 8); - /* 最低字节 → [3] */ - ip_arr[3] = (uint8_t)(ip); + /* 字节顺位拷贝:ip_arr[i] = s_addr 的第 i 个内存字节 */ + (void)memcpy(ip_arr, &ip, sizeof(ip_arr)); /* 设置目的 IP */ ch395f_set_des_ip(sockfd, ip_arr); @@ -2557,6 +2601,13 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, return NET_ERR_RETURN_1(NET_ERR_INVAL); } + /* netTask 上下文守卫:本接口自旋 net_poll + 读芯片缓冲,同样只允许执行任务调; + * 位置与 net_sendto 同一原则(参数校验之后、任何芯片事务之前) */ + if (net_ctx_is_owner_task() == 0U) { + DBG_ERROR("recvfrom from non-exec task: refused (SPI race, use netTask)"); + return NET_ERR_RETURN_1(NET_ERR_NOT_IN_NETTASK); + } + /* 轮询直到:有数据 / 非阻塞(阻塞 = 无限等待,无默认预算) */ while (1) { /* 轮询一次:更新状态机与可读标志 */ @@ -2586,7 +2637,10 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, /* CH395F UDP 头 [0-1] 数据长度(LE) [2-3] src_port(LE) [4-7] src_ip(§9.2.4 表) */ /* 源端口(小端 2 字节 → 主机序 → 网络序) */ p_addr_in->sin_port = net_htons((uint16_t)(header[2] | (header[3] << 8))); - /* 源 IP(小端 4 字节组装为网络序) */ + /* 源 IP:芯片给的是线序字节([4]=第一段),按"首段落在最低 + * 字节"组装即得网络序整数——与本层 s_addr 唯一约定同族 + * (2026-10-24 定案:全仓 s_addr 只此一族),所以这里**不能再加一次换端**。 + * 源 IP(小端 4 字节组装为网络序) */ p_addr_in->sin_addr.s_addr = ((uint32_t)header[4]) | ((uint32_t)header[5] << 8) | ((uint32_t)header[6] << 16) | @@ -2819,11 +2873,20 @@ int net_sock_state(int sockfd) { * 出口参数:无 * 返回值:网络字节序的 32 位 IP 地址(0 表示参数无效) uint32_t * 限定条件:无 - * 函数说明:1. 逐段解析十进制数与并小端组装为 32 位值 - * 2. 转换结果经 NET_HTONL 转为网络字节序 + * 函数说明:1. 逐段解析并按「第一段在主机序最高字节」累加,再经 NET_HTONL 转网络序, + * 使结果满足 POSIX in_addr_t 的不变量:&s_addr 的第 0 个字节 == 点分第一段。 + * 本文件内 s_addr 只有这一族(2026-10-24:两套 IP 字节序约定统一为 POSIX 网络序):net_inet_ntoa 的 + * 「先 NTOHL、再高段在前」正是本函数的解析逆运算;net_getsockname、 + * net_getpeername、net_recvfrom 填的 s_addr 也同族,故三者可直接相等比较。 + * 2. 小端主机(STM32F407 与 PC 模拟器均是)上 "192.168.1.100" 的数值为 + * 0x6401A8C0(内存字节 C0 A8 01 64)。旧实现在小端累加后又多做一次 + * HTONL,得到 0xC0A80164,与 ntoa 不互逆——那是同一文件里的第二套约定, + * 已连同其消费者(connect/sendto 的 des_ip 拆装)一起改为本族。 + * 3. 段值超过 255 时按低 8 位截断(旧实现会把溢出位溅进相邻段,更不可预期); + * 非法字符仍按 0 处理,失败返回值维持 0,不改 NET_INADDR_NONE 的语义。 */ uint32_t net_inet_addr(const char *cp) { - /* 组装出的 32 位地址(主机序,末转网络序) */ + /* 主机序累加值(第一段最终落在最高字节) */ uint32_t addr = 0; /* 段循环索引 */ int i; @@ -2842,15 +2905,15 @@ uint32_t net_inet_addr(const char *cp) { val = val * 10 + (*cp - '0'); cp++; } - /* 本段放入对应字节位(段 0 = 最低字节,小端组装) */ - addr |= (val << (i * 8)); + /* 高位先入:第 0 段左移三次后落在最高字节 */ + addr = (addr << 8) | (val & 0xFFU); /* 跳过分隔符点(末段可无点) */ if (*cp == '.') { cp++; } } - /* 转网络字节序后返回 */ + /* 转网络字节序后返回(此时最低地址字节 = 第一段) */ return NET_HTONL(addr); } @@ -2863,8 +2926,15 @@ uint32_t net_inet_addr(const char *cp) { * 限定条件:buf 非空且长度足够(>= 16 字节) * 函数说明:1. 先经 NET_NTOHL 转为主机字节序再格式化输出 * 2. ⚠ 段序(E13 定案 修复):NTOHL 后主机序数值的高字节才是 - * 点分第一段(192.168.1.100 → 0xC0A80164 → "192.168.1.100")。 - * 原实现低段在前,显示成字节反转的 "100.1.168.192" + * 点分第一段。**入参必须是网络序整数**(&addr 的第 0 字节 = 第一段, + * 小端上 192.168.1.100 = 0x6401A8C0,见 net_inet_addr 的约定说明), + * 于是 0x6401A8C0 → NTOHL → 0xC0A80164 → 高字节 0xC0 = 192 首段 ✓。 + * 原实现低段在前,同一入参会显示成字节反转的 "100.1.168.192"。 + * 3. ⚠ 本注释旧版举的例子写成「192.168.1.100 → 0xC0A80164 → 同串」, + * 那个 0xC0A80164 取的是**已废弃的另一族**(当时 net_inet_addr 的返回 + * 值),按本函数的真实算法它打印出来恰是 "100.1.168.192"。两套 s_addr + * 约定能在同一文件里长期共存而不被发现,源头就是这行错例(2026-10-24 + * 2026-10-24 修正;函数行为未变,只把例子改对) */ char *net_inet_ntoa(uint32_t addr, char *buf) { /* 网络序 → 主机序 */ @@ -3076,7 +3146,9 @@ int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) { p_addr_in->sin_family = NET_AF_INET; /* 本地端口(主机序 → 网络序) */ p_addr_in->sin_port = net_htons(p_sock->local_port); - /* 本地 IP:由 s_local_ip 数组小端组装为 32 位网络序值 */ + /* 本地 IP:s_local_ip[] 是线序数组([0]=第一段),按"首段落在最低字节" + * 组装即得网络序整数 —— 与 net_inet_addr / net_inet_ntoa 同族(2026-10-24 定案), + * 故 getsockname 的结果可直接与 inet_addr 的返回值相等比较 */ p_addr_in->sin_addr.s_addr = ((uint32_t)s_local_ip[0]) | ((uint32_t)s_local_ip[1] << 8) | ((uint32_t)s_local_ip[2] << 16) | diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index 37a2c086..d085cd7e 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -324,6 +324,8 @@ int net_setblocking(int sockfd, int on); * 3. 用本接口把等待切成 ≤timeout_ms 的片,调用方自行累计总预算:总超时语义不变、单次占用有界 * 4. 超时用独立返回码 -2(而非靠 errno 区分)→ 无跨任务 errno 竞态 * 5. ⚠ IWDG required_mask 内任务必须走本接口或 net_select(见 net_recv 契约) + * 6. 本接口**走消息队列**、任意任务可调用(与 net_recv 同一条路径), + * 与 net_recvfrom(仅限执行任务、直调芯片)不是一回事,勿混 */ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); @@ -335,9 +337,26 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); * flags - 通常为 0 * dest_addr - 目标地址 * addrlen - 地址长度 - * 返回值:实际发送字节数,失败返回 -1 - * 限定条件:socket 已创建,指针非空 - * 函数说明:设置目标地址后调用 CH395F 发送 + * 返回值:实际发送字节数 int > 0 + * -1 失败,精确码经本任务错误码槽给出(net_get_errno()): + * NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_TIMEDOUT / + * NET_ERR_NOT_IN_NETTASK(调用上下文不对,见「限定条件」) + * 限定条件:**仅限本层执行任务(net_init 的调用者,NET 层 = netTask)调用**; + * socket 已创建且为 DGRAM;指针非空 + * 函数说明:1. 设置目标地址后调用 CH395F 发送 + * 2. ⚠ 本接口**不走消息队列**:它在调用方上下文里直接 net_poll + + * 写芯片缓冲,与 net_send/net_recv/net_connect 那一族(入队交 netTask + * 串行执行)不是同一条路径。别的任务调用它 = 与本层的 net_poll 抢 + * 同一根 SPI2(TRAP-NET-14 的根因形态),故加上下文守卫直接拒掉, + * 回 NET_ERR_NOT_IN_NETTASK。**该码重试永远无意义**(既非忙也非超时): + * 要么把调用挪进执行任务,要么改用走队列的 net_send——没有第三条。 + * 3. 「把它改成走消息队列」是已知后续项,未做的原因:UDP 的目的地址 + * 与源地址回传都要穿过 net_msg_t,而 net_msg_t 按值拷入队列、 + * 错误码只搭任务通知值回来(AGENTS 明文「勿给 net_msg_t 加回传字段」), + * 真要串行化得先设计 per-task 结果槽并配真机回归(2026-10-24 定案: + * 本轮只把危险面关掉,串行化仍未做)。 + * 4. 全仓当前**无生产调用者**(只有 test/board 与模拟器),正因如此 + * 这条守卫要在有人开始用之前就在。 */ int net_sendto(int sockfd, const void *buf, int len, int flags, const struct net_sockaddr *dest_addr, int addrlen); @@ -350,8 +369,12 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, * flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞 * src_addr - 输出:发送方地址 * addrlen - 输入输出:地址长度 - * 返回值:实际接收字节数,失败返回 -1 - * 限定条件:socket 已创建,指针非空 + * 返回值:实际接收字节数(0 = 无数据可报的边界形态见说明) int >= 0 + * -1 失败,精确码经本任务错误码槽给出(net_get_errno()): + * NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_WOULDBLOCK / + * NET_ERR_NOT_IN_NETTASK(调用上下文不对,见「限定条件」) + * 限定条件:**仅限本层执行任务(net_init 的调用者,NET 层 = netTask)调用**; + * socket 已创建且为 DGRAM;指针非空 * 函数说明:1. 读取 CH395F 接收缓冲区,获取对端地址 * 2. 阻塞语义与 net_recv 同:blocking 属性下无数据 * 无限等待,DONTWAIT / 非阻塞属性立即 NET_ERR_WOULDBLOCK; @@ -438,6 +461,16 @@ int net_sock_state(int sockfd); * 返回值:网络字节序 IP,失败返回 0 * 限定条件:cp 指针非空 * 函数说明:不支持域名解析 + * + * s_addr 的**唯一约定**(2026-10-24 定案:两套 IP 字节序约定统一为 POSIX 网络序;全仓只此一族,勿再引入第二套): + * 网络字节序整数 —— &s_addr 的第 0 个内存字节 == 点分第一段。 + * 小端主机(STM32F407 与 PC 模拟器都是)上 "192.168.1.100" 的数值 = 0x6401A8C0。 + * 与 net_inet_ntoa 互为逆运算;net_getsockname / net_getpeername / net_recvfrom + * 填出来的 s_addr、以及 net_bind / net_connect / net_sendto 收进去的 s_addr + * 全在同一族,故可直接做相等比较(旧实现里 inet_addr 与其余四个不互逆, + * 曾造成「解析→存储→打印」链路输出反序 IP,见 TRAP-NET-21 与本函数实现注释)。 + * 另:本层 inet_addr 语义与 BSD 一致——失败回 0,故 "0.0.0.0" 与非法串同值; + * 需要区分时用 net_inet_addr 的返回值再判,勿拿 NET_INADDR_NONE 当失败哨兵。 */ uint32_t net_inet_addr(const char *cp); diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 24c36b8f..8aa4bbf5 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -108,6 +108,15 @@ struct net_sockaddr_in { #define NET_ERR_TIMEDOUT -9 /* 操作超时 */ #define NET_ERR_NOSPACE -10 /* 发送缓冲区无空间 */ #define NET_ERR_BUSY -11 /* 操作未完成(CH395F BUSY) */ +/* + * 调用方**上下文**不对(不是运行时故障):该接口只允许在 net_init 的执行任务 + * (NET 层 = netTask / 硬件层 = testTask)里调用,其它任务调用会与本层 net_poll + * 抢同一根 SPI(TRAP-NET-14)。与上面各码的区别:BUSY/TIMEDOUT/NOSPACE 换个时刻 + * 重试有意义,本码**重试永远无意义**——要么改到执行任务里调,要么把动作交回 + * 消息队列(netMsgQueue),没有第三种。所以它不参与任何"可重试"判定, + * 上层见到它应当直接当编程错误处理(详见 net_socket.h net_sendto/net_recvfrom)。 + */ +#define NET_ERR_NOT_IN_NETTASK -12 /* 非执行任务上下文调用(重试无用) */ /* * Socket 状态枚举(**本层会话投影**,非芯片状态事实源) diff --git a/docs/子系统设计/控制台单写者日志设计说明.md b/docs/子系统设计/控制台单写者日志设计说明.md index da95515b..17f1c69b 100644 --- a/docs/子系统设计/控制台单写者日志设计说明.md +++ b/docs/子系统设计/控制台单写者日志设计说明.md @@ -123,7 +123,7 @@ flowchart LR | 参数 | 值 | 理由 | |------|-----|------| | 容量 `DBG_CON_RING_SIZE` | 4096B | 2 的幂 → `&(SIZE-1)` 回绕免除法;≈21 整行(192B/行)余量 | -| head/tail | `uint16` + `volatile`,单生产者/单写者 | head = 生产者写位置,tail = 写者读位置 | +| head/tail | `uint16` + `volatile`;**多生产者安全 / 单写者**(2026-10-24 更正:本表旧写「单生产者」,与本文 §3.1「生产者 = 全部任务 + 内核前主线程 + ISR」互相矛盾,现由代码侧统一——**复核空位 + 推进 head + 整行拷贝收进同一个临界区**,最长 192 B 拷贝在 168 MHz 下约亚微秒;`vTaskDelay` 与 DWT/tick 上界判据一律留在区外,区内绝不睡眠) | head = 生产者写位置(临界区内推进),tail = 写者读位置 | | 已用字节 | `(head - tail) & (SIZE-1)` | 无符号差自然回绕,免锁可读 | | 单行可接收上限 | `SIZE/2 = 2048B` | 保证"一行恒可入环";正常路径受 `DBG_CON_LINE_MAX=192B` 约束,不触发 | | 位置 | BSS(SRAM1,非 CCM) | CCM = FreeRTOS 堆,放 BSS 不占堆;轮询发送不依赖 DMA 可达性(CCM 的 DMA 不可达约束不适用) | @@ -331,7 +331,7 @@ sequenceDiagram | # | 项 | 说明 | |---|----|------| | O1 | 未注册期裸发无互斥 | 启动窗口(内核启动 → attach)`dbg_console_write_raw` 无互斥 → 多任务并发会**逐字节交错**(非旧 `HAL_BUSY` 丢整行)。2026-10-10 已固化"周期任务用 `dbg_console_writer_ready()` 门控"纪律;若未来开机日志密度上升,可升级为"内核前静态待发行 + attach 后统一补发"或给裸发加轻量互斥 | -| O2 | `HAL_UART_Transmit` 超时未计数 | 超时块(UART 物理失效)丢弃但不入 `s_con_drop`;v2 可加独立超时计数 | +| O2 | `HAL_UART_Transmit` 超时未计数 —— **已处置(2026-10-24,v1.5)** | 三本账已分离:`dbg_console_dropped()` = **真没落地**的数(缓冲满丢的一行,或没能确认送达而出环的一个块);`dbg_console_tx_fault()` = 外设**软复位自愈次数**(语义不变);新增 `dbg_console_tx_timeout()` = 写者排空时看到的**块超时次数**(TXE 与 TC 两条硬上界都算)。**口径边界**:裸发路径(内核前 / ISR / 两条告警行自身)的超时**不进**超时账——裸发没有「块」的概念,混进来读数失去意义。**尚未接线**:本读口暂无生产调用者;候选位置 `App/util/task_watchdog.c` 的 `STALL` 行是**被解析的证据行**,加字段要连解析端与台架判据一起改 → 列为后续项 | | O3 | `printf` 逐字节开销 | 遗留 `printf` 路径 = 逐字节临界区 + 逐字节事件置位;遗留调用点全部切 `DBG_*` 宏后开销自然消失(不改变机制) | | O4 | **ORE 僵尸未根治** | §12:现仅有缓解(250ms 预算 / 300ms 冷却 / DWT 超时),机制根源是 USART1 使能了 RX。根治需 `.ioc` 改 TX-only + CubeMX 重生成 → 已登记 `docs/任务规划/登记簿.md` **S-3** | @@ -351,4 +351,5 @@ sequenceDiagram | v1.1 | 2026-09-25 | 文件拆分同步:写者任务体迁至 `App/task/default_task.c`(`Src/freertos.c` 只留 CubeMX 接线与崩溃钩子);§3.2 文件清单 / §4.3 / §6 / §11 位置引用更新 | | v1.2 | 2026-10-02 | 新增 §12 ORE 僵尸通道(台架 gen-288 实证:节奏性喂狗饥饿,非死等;缓解已做、TX-only 根治待办)+ §11 时间线两条 10-02 记录;§13 新增 O4;原 §12/§13 顺延为 §13/§14 | | v1.3 | 2026-10-10 | 栈链整改(**未抬栈**)三处:① 裸发改为纯寄存器轮询——DWT 不可用时走 `con_tx_counted`(计数上界),**不再回退 `HAL_UART_Transmit`**(tick 依赖不可信 + 其调用链污染所有任务最坏栈链);② 切断 `drain → syslog_post → write_line` **调用图成环**,改为置标志 + 写者任务在 drain 之外补记(新 API `dbg_console_tx_fault_log_take`);③ 行缓冲按模块可收紧(`dbg_log.h` 新增 `DBG_LINE_BUF_MAX`,深链模块 128)+ 时间戳缓冲并入行缓冲(32 → 24)。§4.4 / §5 / §6 / §7 / §8 同步;实测 defaultTask 静态最坏链 5312 → 2776 B | +| v1.5 | 2026-10-24 | **主机单测批次照出的四条现状全部处置**(生产码在 `App/log/console_log.c`,用例在 `test/host/console_log/`):① 冷却判据改用独立 `s_con_cooling` 标志,不再拿 `HAL_GetTick()` 的 0 值当哨兵——旧写法在 tick==0 那一拍(开机头 1 ms,或 49.7 天回绕到 0)冷却等于没记;② `con_append` 的空位判据 / 推进 head / 整行拷贝收进同一临界区——旧实现是临界区外 check-then-act,多生产者可在环 >95% 满时双双过判据把 head 推过容量 → 静默覆盖且**既不打 [ERR] 也不计 dropped**;③ `Instance == NULL`(未初始化)与僵尸通道**分诊**,未初始化绝不 reinit(旧路径会在 HAL 未就绪时经 MspInit 重配 RCC/GPIO);④ 三本账,见本文 §13-O2。**dropped 的单位口径变了**:一次 = 一行(缓冲满)或一个块(没能确认送达 / 通道不可用),故 `[ERR] CONSOLE dropped N lines` 的 N **不恒等于行数**(为兼容 `@test` 标记匹配,文案未改词)。本文 §3.1 与 §4.1 的「单生产者 vs 全上下文生产者」矛盾一并消除 | | v1.4 | 2026-10-10 | 新增 `dbg_console_writer_ready()`(写者就绪查询)并固化"未注册期只有 defaultTask 记日志、周期任务用就绪门控"纪律;更正 §3.4 / §7 / §9-D5 / §13-O1 的"只丢不交错"旧结论(裸发已改寄存器级轮询、无 `gState` 互斥) | diff --git a/docs/子系统设计/运行与自检记录设计说明.md b/docs/子系统设计/运行与自检记录设计说明.md index 6ba2a4f9..eceda58c 100644 --- a/docs/子系统设计/运行与自检记录设计说明.md +++ b/docs/子系统设计/运行与自检记录设计说明.md @@ -131,9 +131,12 @@ defaultTask(唯一写者,挂载成功后): - **N-24(2026-10-15,报告 N-16 写侧):一次排空合并成"每文件一次 open/write/close"**。旧实现每条记录一轮,而 `f_close` 要回写目录项、`f_write` 要对目标扇区做读-改-写 → **每条日志 2~4 次 NAND 页读写**。现在同一文件的一批(≤8 条 = 512 B 静态缓冲 `s_batch`)合并成一次追加写: - **掉电语义不变**:批次仍在**同一次 `syslog_drain()` 调用内**写完并 `f_close`;`f_close` 内部 `sync_fs` → `disk_ioctl(CTRL_SYNC)` 会 `ftl_flush_cache()` 把 FTL 的单页写回缓存刷进 NAND(`Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` 的 `CTRL_SYNC` 分支)。落盘时机与旧实现每条 close 等价,**批次不跨调用驻留 RAM**:"写完即关"的路径关文件即落盘,残余只到"单次写事务进行中掉电/复位"为止(`../任务规划/登记簿.md` N-22:无空闲/掉电前周期 flush,2026-10-22 用户裁定暂不做)。 - - **条目不丢**:启动前数组按下标"看而不删",整批成功才删;队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时**回投队列**——旧实现 -2 会直接丢掉那一条。 + - **条目不丢**:启动前数组按下标"看而不删",整批成功才删;队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时**回投队列**。 + ⚠ **2026-10-24 更正本条**:旧实现里 -2 只在**按条慢路径**被正确回投,**批量路径根本没有 -2 分支**(`done` 保持 `n` → 回投循环空转)→ 卷忙时一批(≤8 条)既不落盘也不回投、静默丢失,而本节长期写着"条目不丢"替它背书。现已补 `rc == -2` 分支整批回投(不计故障、不打日志、保序),由 `test/host/sys_log_mcu/` 的 TC-SYSLOG-035 组逐条钉住。**真机必验**:FTP 传输期间持续投日志,传输结束后条数 == 投递条数、顺序不乱。 - **容量轮转仍只在按条慢路径做**:批量路径发现剩余容量装不下一批时返回 -3,退回逐条 `s_append()`("满了 unlink 重建"只保留一处实现,避免一批跨多轮重建)。 - 自愈重试(L1 丢弃重建 + 重试一次)与错误去重(L2 只报第一次)判据与单条路径一致。 + - **自愈触发集分类(2026-10-24,代码侧唯一入口 `s_is_selfheal_code()`)**:只有"删了这个文件真能修好"的介质/结构类码才触发 unlink 重建——`FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`;**名字/参数类严禁归入**(`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`,另显式排除 `FR_NOT_READY` / `FR_NOT_ENABLED` / `FR_TIMEOUT`)。同名同因下 unlink 必然同样失败,旧集合的真实后果是**每轮排空删一次自己的历史日志且永远修不好**,违反 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条。`f_write` 之后的"失败/短写 → unlink 重试一次"**不接**本判定(能走到那里说明已打开成功;而簇分配失败是 `FR_OK` + 短写、根本没有码可判,必须无条件重试)。钉住:TC-SYSLOG-101 / 101b / 102 / 103;`App/log/soe_store.c` 侧同源但取**更保守子集**(TC-SOESTORE-022 / 022b)。 + - **留痕节流分两层、各自独立抑制位**:drain 侧"整批回投"只在正常→故障那一次报(`write fail, requeue %lu entries`,落盘成功一轮复位;旧文案写作 "drop N entries" 与实际行为相反——整批其实一条没丢);post 侧 `queue full, drop` / `preboot full, drop` 各一位(**丢弃语义不变,满则丢照旧**,只压留痕次数)。节流之后**没有丢弃计数**——要计数属新增对外可见信息,另列后续项。 - 读路径与写路径并发安全:UI 页在 uiTask 经 `syslog_get_record()` 读记录(内部走 `s_read_raw()`,见 §4.5);读写两侧都取卷锁 try 语义(P2-10),FatFS 系统锁保护单次调用。 - **存储未就绪/挂载失败**:待发条目滞留 RAM(下次上电丢失),串口打印告警——存储故障时本就无法持久化,属预期行为。 @@ -142,12 +145,13 @@ defaultTask(唯一写者,挂载成功后): ```c uint32_t syslog_count(syslog_id_t id); /* = filesize/64 */ int syslog_get_record(syslog_id_t id, uint32_t idx, char *ts, size_t ts_len, - char *desc, size_t desc_len); /* 越界返回 -1 */ + char *desc, size_t desc_len); /* 越界 / 读失败 / 缓冲容量不足 均返回 -1 */ ``` - MCU:**8 条滑动读窗口**(N-24,报告 N-16 读侧)——`s_read_raw()` 先查窗口(同 id + 代数未变 + 落在区间内 → **零 I/O** 直接从 512 B 静态窗口返回);未命中才 `f_open(FA_READ) → f_lseek(窗口首条×64) → f_read(≤8×64B) → f_close` **一次取回连续 8 条**,再从中切出请求的那条。旧实现每读一条都要 open/lseek/read/close 一轮,UI 翻 20 条记录 = 20 次目录项/FAT 往返。 - 失效机制:写者每次落盘后把 `s_rd_gen[id]` 自增,读者比对填充时记录的代数,不匹配就重填。对齐 32 位在 ARM 上原子,跨任务(uiTask 填、defaultTask 增)无需临界区;最坏情况多读一轮,不会读错。 - **API 与调用方一律不变**(`syslog_get_record()` 签名照旧,`ui_page_runlog` / `ui_page_selftest` 零改动)。 +- **容量契约(2026-10-24 写死在 `App/log/sys_log.h`)**:`ts_len > SYSLOG_TS_LEN`、`desc_len > SYSLOG_DESC_MAX`,**不足一律 -1,既不静默截断也不写调用方一个字节**(描述在写入侧已截到 `SYSLOG_DESC_MAX`,按最小容量给就一定装得下,读侧不需要第二道截断)。旧实现里那句 `if (dl >= desc_len) dl = desc_len - 1` 由上面的守卫保证**永不可达**,已删除;它的存在还会误导调用方"给 28 字节缓冲能读到 27 字符"——实际是整条读不到。UI 侧现用 32 B 列缓冲,走允许路径、行为不变。钉住:TC-SYSLOG-057、057b(0x7A 哨兵逐字节回比)、098。 - 模拟器:宿主文件同语义(UI 页双端零差异)。 - UI 页每帧仅读当前窗口 ≤8 条记录(每次重绘 8×64B SPI 读,用户翻页触发,频率极低)。 diff --git a/docs/架构设计/STM32F4-Base存储架构说明.md b/docs/架构设计/STM32F4-Base存储架构说明.md index 77746cf1..ad95d3b3 100644 --- a/docs/架构设计/STM32F4-Base存储架构说明.md +++ b/docs/架构设计/STM32F4-Base存储架构说明.md @@ -579,7 +579,8 @@ if (nand_ftl_format() == 0) { 7. **单写者规则** — 存储由 **defaultTask 独占**:挂载 / `f_mkfs` / 卷健康探针 / `net_cfg_load` / syslog 落盘 / 配置引擎全收敛于它。netTask / uiTask **不得**直接触碰配置引擎或 FatFS。启用存储测试套件(`TEST_SUITE_STORAGE`)时存储所有权整体移交 testTask,defaultTask 必须**完全跳过**存储 bring-up(`App/task/default_task.c` 中 `#if !defined(TEST_SUITE_STORAGE)` 包裹,2026-09-25 自 `Src/freertos.c` 迁出)——否则后台文件写与测试的 `nand_ftl_deinit` 掉电模拟并发,互相踩坏 FTL/FatFs 状态(实测:mkfs 半途冻结 + BBT 被误写)。 8. **内存布局与 DMA/CCM 约束** — FreeRTOS 堆(含全部任务栈)已迁至 **CCM `0x10000000`**(64KB,直连 M4 零等待),但 **DMA1/DMA2 物理不可达 CCM**。GD5F 驱动 `GD5F_DMA_THRESHOLD = 1024`:栈上的小缓冲(dhara meta 读 132B、`dhara_nand_is_free` 64B)一律走 **CPU 轮询**、绝不经 DMA;页级大块(2048)才走 DMA 且源/目的**全部是文件作用域静态缓冲**(落 SRAM)。新增任何 DMA 收发缓冲必须①文件作用域静态②大小≥阈值③确认落 SRAM。违反任一条 = 静默数据损坏(CCM 首轮扩容实验实证)。换料注意:克隆片可能缺 SRAM3(CCM),必须重测。 9. **LVGL 绘制缓冲越界护栏** — LVGL 9.3.0 对 I1(1bpp) 整帧渲染会在其算出的 `stride*h`(160×160/8=3200) 之后**多写 1 个字节**,紧邻 `g_ui_draw_buf` 之后的 `.bss`(如 `ui_menu_view` 的 `s_frame_box`)指针最低字节被打成 `0xFF` → 野指针 → HardFault(`PC=lv_obj_get_scroll_y`)。`ui_port.c` 的 `g_ui_draw_buf` 已预留 `UI_DRAW_BUF_GUARD=32` 字节护栏吸收该越界写(逻辑尺寸仍按 `UI_FB_SIZE` 上报,渲染数学不变)。**勿删护栏、勿把缓冲缩到与邻接 `.bss` 紧邻**。Lib/LVGL 为第三方不改。 -10. **FatFs 严格 8.3 短名** — 本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**:主名 ≤8 字符、扩展 ≤3 字符、**不得以点开头**(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)。`.fschk` 会被 `ff.c` 的 `create_short` 把「点之后」当扩展域解析而超限 → `f_open` 恒返 `FR_INVALID_NAME`(6);这是**纯 RAM 里的名字判定,不碰盘**。`ff.c` `create_short` 全部拒绝点的地方已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`——非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入"卷损坏"触发集(整卷重格式化不可逆)。补充:含冒号 `:` 的名字被 FatFs 先解析为**盘符前缀** → `f_open` 返回 `FR_INVALID_DRIVE`(11) 而非 `FR_INVALID_NAME`(6);两者都属合法拒绝(测试用例须同时接受)。 +10. **FatFs 严格 8.3 短名** — 本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**:主名 ≤8 字符、扩展 ≤3 字符、**不得以点开头**(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)。 + - **推论(2026-10-24 补齐,代码侧唯一入口 `s_is_selfheal_code()`)**:既然名字/参数类错误码在 8.3 配置下**必然重现**(同名同因),它们就**严禁**被归进任何"卷/文件坏了"的自愈触发集——否则自愈会退化成反复删自己的数据。本仓落点:`App/log/sys_log_mcu.c` 的触发集只收 `FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`,`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT` 一律立即失败(逐码依据 + `Lib/FatFs/ff.c` 行号写在该函数上方资料块;`App/log/soe_store.c` 侧同源取更保守子集并互指)。**本条拦下的真事**:旧集合含名字类码,后果是路径/卷异常期间**每轮排空删一次自己的历史日志且永远修不好**,由主机单测照出(TC-SYSLOG-101 / 102、TC-SOESTORE-022b),详见 `docs/测试说明/主机单测覆盖率口径.md` §9「第 1 轮修复处置」。。`.fschk` 会被 `ff.c` 的 `create_short` 把「点之后」当扩展域解析而超限 → `f_open` 恒返 `FR_INVALID_NAME`(6);这是**纯 RAM 里的名字判定,不碰盘**。`ff.c` `create_short` 全部拒绝点的地方已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`——非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入"卷损坏"触发集(整卷重格式化不可逆)。补充:含冒号 `:` 的名字被 FatFs 先解析为**盘符前缀** → `f_open` 返回 `FR_INVALID_DRIVE`(11) 而非 `FR_INVALID_NAME`(6);两者都属合法拒绝(测试用例须同时接受)。 11. **FA_OPEN_APPEND 陷阱 / 短写** — `FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(这是当年 `/log` 写失败的真根因);追加写必须写 `FA_WRITE | FA_OPEN_APPEND`(2026-09-20 台架实测,`Lib/FatFs` 移植版)。该移植的 `f_write` 在簇分配失败时返回 `FR_OK` + 短写(**没有错误码**)→ 判失败必须校验写回的字节数。标志位为**旧式编号**:`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(`ff.c` 内部定义),与上游 FatFs R0.14 不同,**勿照抄上游代码示例**。 12. **崩溃可观测性(勿屏蔽)** — 已开启 `configCHECK_FOR_STACK_OVERFLOW=2` + `configUSE_MALLOC_FAILED_HOOK` + HardFault 现场报告(`Src/stm32f4xx_it.c`:裸 UART 打印 PC/LR/xPSR + CFSR/HFSR/BFAR/MMFAR 后复位,对照 `.map` 定位符号)。defaultTask **1024** words、testTask **2048** words 是**实测下限,勿降**。开机日志出现 `[ERR] STACK OVERFLOW task=` / `[ERR] HARDFAULT ...` / `MALLOC FAILED` 是精确定位线索,禁止屏蔽或降级。 13. **BBT 恢复模式** — 运行期坏块会随 TC-STORAGE-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。 diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index f07eff21..6a858451 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -483,7 +483,7 @@ uint16_t ch395f_link_change_count(void); `FF_USE_LFN=0` 下 `.fschk` 走的正是"扩展域 5 字符超限"这条真实路径;自愈只动自己的文件 (反复失败 20 轮后 `/log` 目录与邻居文件未被触碰)。 -**本批照出的现状(只登记、未改生产码;逐条已上报用户待裁定)**: +**本批照出的 11 条现状(缺陷原文保留在此,处置结论见紧随其后的「第 1 轮修复处置」)**: | # | 位置 | 现象与影响 | 钉住它的用例 | |---|---|---|---| @@ -498,8 +498,61 @@ uint16_t ch395f_link_change_count(void); | 9 | `App/log/console_log.c:394~412` | `con_append` 的**空位判据在 `taskENTER_CRITICAL` 之外**(check-then-act)。生产者按 `docs/子系统设计/控制台单写者日志设计说明.md` §3.1 是"全部任务 + 内核前主线程 + ISR",而同文档 §4.1 的 head/tail 行写作"单生产者/单写者"——**两处口径互斥,后者需要更正**。多生产者可在环 >95% 满时同时过判据 → `head` 各自推进后合计越过容量 → `con_used()` 回绕变小 → 写者少发一段,被覆盖的行**既不打 `[ERR]` 也不进 `dropped`**(静默损坏;索引恒取模,不会越界)。**这条没有用例、只报推断**:单线程宿主构造不出真抢占点(把注入点挂在 `taskENTER_CRITICAL` 上已在区内,不算抢占点) | —(无用例,仅登记) | | 10 | `App/log/console_log.c:181~183` + `:306~331` | `huart1.Instance == NULL`(HAL 早期 / 断言-故障上报路径的专用短路,返回 `HAL_ERROR`)在 drain 里被当成**僵尸通道**走 `con_uart_reinit()` → `MX_USART1_UART_Init()`(含 MspInit 的 RCC/GPIO 重配)。当前接线顺序(`MX_USART1_UART_Init` 早于 `dbg_console_writer_attach`)挡住了,但"未初始化"与"通道卡死"共用一条自愈支路,attach 时机一旦被提前就会在 HAL 未就绪时重配外设 | TC-CONLOG-103(钉现状) | +### 第 1 轮修复处置(2026-10-24,同日;生产码改动 + 用例翻正向) + +用户裁定「全部修」。三名队友按域并行改生产码(写范围互斥、零越界),Lead 统一 `@build` 与跑闸。 +**`@build` 0 错误 0 警告**(本工作树首次全量构建):`Code=213720 RO-data=18860 RW-data=1204 ZI-data=131500`。 +全量闸:**33/33 套件全绿**、`line=5605/6275(89.3%)`、`func=396/429(92.3%)`、未触及仍 29 个 `.c`; +与修复前逐文件比对**下降文件数 = 0**(改过生产码的三个文件覆盖随代码同步增长: +`console_log.c` 188→199、`sys_log_mcu.c` 310→324、`net_socket.c` 881→887)。 + +| 原表条目 | 处置 | 钉住它的正向断言 | +|---|---|---| +| 1 卷忙整批丢日志 | **已修**:`syslog_drain` 批量路径补 `rc == -2` 分支把 `done` 置 0 → 整批回投队列;三条纪律逐条有断言(不计故障 / 不打日志 / 保序),慢路径(参照物)一字未动 | TC-SYSLOG-035、035a、035c、035d、104 | +| 2 文案与行为相反 | **已修**:文案改 `write fail, requeue %lu entries`,并加「正常→故障」节流(`s_requeue_reported[]`,落盘成功一轮复位);真丢仍由 `hold-over drop` 兜底 | TC-SYSLOG-039、039b | +| 3 截断死分支 | **已修**:删掉永不可达的截断支,容量契约写死在 `sys_log.h`(`ts_len > SYSLOG_TS_LEN`、`desc_len > SYSLOG_DESC_MAX`,不足**一律 -1 且不写调用方一个字节**);API 签名与返回码集合不变 | TC-SYSLOG-057、057b(0x7A 哨兵逐字节回比)、098 | +| 6 soe_store 注释夸大实现 | **已修(改注释,实现不动)**:打开失败**任何错误码都不 unlink** 是有意的——soe.log 是事件序列,「打开失败就删文件」等于销毁事件证据;两侧资料块各写一份理由并**互指对方文件与测试号**,要求同步修改 | TC-SOESTORE-022 | +| 7 FR_NO_SYSTEM 不存在 | **已修**:改成实际会返回的 `FR_NOT_READY(3)` / `FR_NO_FILESYSTEM(13)`,并写明「本层对任何非 FR_OK 一律转 -1,别去枚举错误码集合」,唯一例外指向自愈触发集 | 注释面(无行为改动) | +| 4 IP 两套字节序 | **已修(统一到 POSIX 网络序)**:改 `net_inet_addr`(`&s_addr[0] == 第一段`,小端上 192.168.1.100 = `0x6401A8C0`)+ `net_connect_locked`/`net_sendto` 的 IP 数组改按内存字节顺位拆装(**进与出同时换族 → 芯片侧数组净效果不变**,由 550h 直接钉住)+ 模拟器 `net_socket_sim.c` 逐字同语义;`net_inet_ntoa` **函数体一字未动**,只把它注释里那个取了废弃族值的例子改对(就是那条隐蔽原因)。lftpd 两处经算术核对**确认无需改**(PASV 227 的双重反转仍成立),只加「勿顺手改对」的注释 | TC-NETSOCK-100 组(新增往返不变量 100g/100h、边界 100i/100j、截断 100k)、550h | +| 5 UDP 绕过 netTask | **危险面已结构性关掉,串行化改造仍未做**(按 Lead 定案,未自升级为改队列):两个 UDP 入口加**执行任务上下文守卫**,非 netTask 调用回新码 `NET_ERR_NOT_IN_NETTASK = -12`(住 `net_types.h`,与 BUSY/TIMEDOUT 的本质差别 = **重试永远无意义**,不参与任何可重试判定);守卫位于参数校验之后、任何芯片事务之前;模拟器**刻意不加**并标注为有意的不对称(单线程 + Winsock 自身线程安全、没有那根 SPI)。**附带关掉第二条更隐蔽的危险**:非 netTask 在 recvfrom 里自旋期间,netTask 可经 close 清零控制块并把同一 sockfd 分给新会话 → 旧自旋者消费新会话数据报,这条路现在断了 | TC-NETSOCK-560 组(含「零芯片事务」与「守卫不吞正常错误路径」) | +| 8 冷却哨兵与 tick 撞值 | **已修**:新增独立 `s_con_cooling` 判定「是否在冷却」,`s_con_cool_until` 只存截止时刻(无符号差跨回绕的写法保留) | TC-CONLOG-080~084、107、108 | +| 9 环空位判据在临界区外 | **已修**:「复核空位 + 整行拷贝 + 推进 head」收进同一个临界区(最长 192 B、168 MHz 下亚微秒;`vTaskDelay` 与 DWT/tick 判据一律留区外,区内绝不睡眠)→ 多生产者结构性安全,写者仍唯一。**原「这条主机测不出来」的结论本轮作废**:桩里记临界区嵌套深度并把第二生产者精确塞进第一个生产者的临界区,再把环形缓冲的宏名成函数调用使每一次碰环都可断言 | TC-CONLOG-160~167(**167 是正控**,证明 160~166 不是空转) | +| 10 未初始化被误当僵尸通道 | **已修**:drain 的失败处理先分诊——`Instance == NULL` 视为通道不可用(不 reinit、不计 tx_fault、不进冷却、只记 dropped);只有确认超时/僵尸通道才走软复位自愈 + 冷却 | TC-CONLOG-103~106 | +| (撤销)非阻塞 recv 看不见 EOF | **误报,未改任何码**:`net_recv_locked:1885` 的非阻塞分支里 `CLOSED || session_dead` 已判 EOF(0),只有会话存活才 WOULDBLOCK。顺带核实 `net_recvfrom` **不存在** EOF 误判面(UDP 无连接,无数据只有 WOULDBLOCK 或继续自旋两条出口) | —(记一次「描述得很合理」的误报:修之前先读码,误报比漏报更贵) | + +**修复过程中顺带照出的两条(原表没有,一并修掉)**: + +- `syslog_post` 的 `queue full, drop` / `preboot full, drop` **无节流**——1 号修好之后「整批回投 → 队列常驻满」成为必然场景,不修等于**用一个新刷屏换掉一个静默丢**。已按同一「正常→故障」纪律加两个独立抑制位(**丢弃语义一字未改**,满则丢照旧)。TC-SYSLOG-041b/041d/041e/041f、105、106、107、107b、107c。 +- 自愈触发集里混着 `FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`——**比原报告的 6 号严重**:这类码下 unlink 自己的文件必然同样失败(同名同因),后果是**每轮排空删一次自己的历史日志且永远修不好**,正是 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条严禁的归集。已新增代码侧唯一入口 `s_is_selfheal_code()`:触发集收敛为 `{FR_DISK_ERR, FR_INT_ERR, FR_NO_FILESYSTEM, FR_DENIED}`,逐码依据(含 `Lib/FatFs/ff.c` 行号)与显式排除清单写在资料块里;`soe_store.c` 侧同源但**取更保守子集**。TC-SYSLOG-101/101b/102/103/103b、TC-SOESTORE-022b。 + +**本轮刻意没做(后续项,含理由)**:① UDP 改走 `netMsgQueue`(需先设计 per-task 结果槽:`net_msg_t` 按值拷入队列、错误码只搭任务通知值回传,AGENTS 明文禁止给 `net_msg_t` 加回传字段,且必须配真机回归); +② `simulator/sim/sim_syslog.c:171` 那条与 3 号**同源的不可达截断支**(本轮编译验证不到它——模拟器走 CMake+VS,不在 `@build` 覆盖内,不交没编译过的改动);③ `syslog_post` 的**丢弃计数**(属新增对外可见信息,本轮没有实测频率支撑阈值); +④ `dbg_console_tx_timeout()` 的**读端接线**(现无生产调用者;候选位置 `task_watchdog.c` 的 `STALL snap=… req=… h=… mg=… cf=… seg=…` 是**被解析的证据行**,加字段要连解析端与台架判据一起改,本批不碰台架);⑤ 给 104 加来源地址日志(现网 `iec104_serv.c` 根本不打印来源 IP、无 `getpeername/ntoa`,**没有输出点可看**)。 + +**必须上台架复验(本轮按指示未碰台架;主机测覆盖不到的部分逐条列出)**: + +1. TCP 出向连接(唯一真正穿过被改的 `des_ip` 拆装路径):板级 `net_test_task` Phase 7(TC-NET-701/703/704,PC 侧起 server),判据 connect 能建立、PC 侧看到的源端口正确;Phase 2 的 `test_parse_ip` 期望 `[192,168,1,2]` 不变。 +2. FTP 被动模式取一个文件并抓 227 响应:括号内前 4 元组 == 设备实际 IP、后 2 元组解出的端口 == 实际拨的端口(验 lftpd 那处双重反转没被改坏)。 +3. FTP 对端日志 `connection received from :`:ip 必须是 PC 地址且正序(验 `getpeername` → `ntoa` 同族)。 +4. UI 网络配置页改 IP 保存 → 回读一致、按新 IP 通、104/FTP 仍可连(代次重建链路;UI 显示取自配置字符串、不经 `s_addr`,列进来是兜住「配置字符串→芯片下发→回读」这条主机测不到的路)。 +5. FTP 传输期间持续投日志:传输结束后 `run.log`/`selftest.log` 条数 == 投递条数、顺序不乱、全程无 `write fail, requeue`;同窗口 `queue full, drop` 只应出现**一行**。 +6. 制造持续写故障(坏块/写保护):串口只在**转入故障那一刻**出一条,恢复后再故障要再出一条。 +7. 控制台灌高频日志把环逼到将满:不再出现半行/串行,且 `[ERR] CONSOLE dropped N lines` 与实际缺失数一致(**单位口径已变**:N 是「行或块」的计数,不恒等于行数);拔/短接 TX 造一次僵尸通道,核对 `dropped` / `tx_fault` / `tx_timeout` 各 +1。 + +**第 1 轮新踩到的坑(都有复现路径)**: + +1. **`edit` 替换块会吞掉相邻代码**:在 `net_sendto` 插守卫时 `new_string` 漏写了紧随其后的 `buf == NULL → INVAL` 检查,那块被整段删掉,现场只表现为「sendto 空指针不报 INVAL」——**是上一节刚落的存量断言 440d 当场抓到的**。教训:`old_string` 覆盖多个语义块时 `new_string` 必须逐块回读,别信「我只加了一段」。 +2. **`read` 全文回写会截断大文件**:`console_log_test.c` 到约 54 KB 后用「read 全文 → 替换 → write」连续两次把尾部(含 `main()`)吃掉,报 `C1075` 才发现。**超过约 40 KB 的文件只能定点改**(`edit` 或直接 fs 读写),不要整文件回写。 +3. **块注释里替换把结尾的注释闭合符提前引出**:给 `console_log.h` 的 API 清单加说明时,替换文本自带了注释闭合符 → 注释中途关闭、后面中文裸露成代码,报 `C3872` / `C4138` 且**错误落在远处中文字符上**,第一眼完全看不出根因。改注释类替换宁可让 old/new 都不含行尾的注释闭合符。 +4. **假时钟不要只挂在单一计数点**:假内核的时基挂在 `net_poll` 必读的 GPIO 上,而 `net_init` 未跑时 `net_poll` 直接短路 → tick 永不前进 → 停滞超时永不命中,**连 40 万次跑飞保险也挂在同一计数点、同样失效**(进程真挂死)。凡「可能短路返回」的路径都要能推进时间或独立触发保险;那条用例最后改用 `recvfrom(DONTWAIT)` 测同一分支。 +5. **被 kill 的测试进程会占住 `tempfile\u003csuite\u003e.exe`**:下一个套件表现为 `LNK1169` / `LNK1104 无法打开文件`,看着像构建配置坏了,其实先 `taskkill` 那个 exe——这是「逐套 runner 固定共享路径」的又一处连带代价(与上面第 12 条同源)。 +6. **cppcheck 会把「仅置位时才清」的抑制位写法判成 `duplicateConditionalAssign`(style,拦提交闸)**:为省一次 volatile 写而写成「判非零再清零」会被判等价赋值。处置:**回退成无条件写 + 注释写明别再「优化」回去**,不要为一条 style 告警加 inline 抑制。 +7. **新增断言要用「逐条退回生产码」验一次牙**(变异自证):把四条修复逐条退回旧实现,失败集合单调增长 6 → 11 → 18 → 22 条、每步都是上一步的超集,改回即 127/127 全绿——**没有这一步,一条空转断言就要被交上去**。 +8. **任务书里给的临时缺陷标签会被编号门禁当自造编号拦**——本批 Lead 在任务书里用的两个「单字母+连字符+序号」临时标签被原样抄进生产注释,一次冒出 8 类台账外错误(连本文档举这个反例时把字面量写出来,都会被同一根闸再拦一次,故此处只能描述形态、不能给样例)。纪律:**Lead 给的内部标签不得写进代码**,注释只写行为(如「两套 IP 字节序统一为 POSIX 网络序」),要指用例就指已登记的 `TC-*` 号。 + **给下一批的可复用手法与坑**(完整清单写在各套件 `.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`**(测试输出不会被吞进环形缓冲)。 @@ -606,3 +659,4 @@ P2 5 个 2,226 行、P3 29 个 4,814 行明确不做**(四条对得上:13+5+ (重点:`sys_log_mcu.c:509~537` 卷忙整批丢日志、`net_socket.c` 同文件两套 IP 字节序、UDP 两个公开 API 绕过 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 | diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index 7ee9270d..99e1607c 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -723,6 +723,26 @@ void event_callback_example(void) 网络协议使用**大端序**(网络字节序),而 STM32 使用**小端序**(主机字节序)。发送和接收数据时需要进行字节序转换。 +### 9.0 `s_addr` 的唯一约定(2026-10-24 统一;全仓只此一族,勿再引入第二套) + +**定义**:`sin_addr.s_addr` 一律存**网络字节序整数**,判据是 `&s_addr` 的**第 0 个内存字节 == 点分第一段**。 +小端主机(STM32F407 与 PC 模拟器都是)上 `"192.168.1.100"` 的数值 = **`0x6401A8C0`**(内存里是 `C0 A8 01 64`)。 + +| 方向 | 用哪个 API | 要点 | +|---|---|---| +| 字符串 → `s_addr` | `net_inet_addr()` | 失败返回 **0**(与 BSD 一致),故 `"0.0.0.0"` 与非法串同值;**别拿 `NET_INADDR_NONE` 当失败哨兵** | +| `s_addr` → 字符串 | `net_inet_ntoa()` | 内部先 `NET_NTOHL` 再**高段在前**打印(TRAP-NET-21 定案) | + +**不变量(有单测钉住)**:`net_inet_ntoa(net_inet_addr(s), buf)` 还原 `s`;`net_inet_addr(net_inet_ntoa(v, buf))` 还原 `v`。 +`net_getsockname` / `net_getpeername` / `net_recvfrom` 填出来的 `s_addr`,与喂进 `net_bind` / `net_connect` / `net_sendto` +的 `s_addr` **同族、可直接相等比较**。历史背景(同一文件曾并存两套约定、`ntoa` 注释里的例子取了废弃族的值) +见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-21 的 2026-10-24 后续条目。 + +**UDP 直调限制**:`net_sendto` / `net_recvfrom` **不走消息队列**——它们在调用方上下文里直接 `net_poll()` + 读写芯片缓冲, +因此**仅限本层执行任务(NET 层 = netTask)调用**;别的任务调用回 `NET_ERR_NOT_IN_NETTASK`(-12)。该码**不是运行时故障, +重试永远无意义**:要么把调用挪进执行任务,要么改用走队列的 `net_send` / `net_recv`,没有第三条路。 +「把这两个接口改成经 `netMsgQueue` 串行化」是已知后续项,未做的理由见 `Drivers/BSP/NET/net_socket.h` 同名函数头。 + ### 9.1 端口号转换 ```c diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 7b74f375..24b8e809 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -233,6 +233,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 - **根因(定案)**:① `net_inet_ntoa` 段序反转——`NET_NTOHL` 后主机数字的 **MSB 才是点分首段**,原实现低段在前;② lftpd 打印 `net_getsockname` = 本机监听地址(端口恒 21),对端地址应读 `net_getpeername`。PASV 227 消费点做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判**。 - **修复**:`net_inet_ntoa` 段序改 MSB 首(MCU+模拟器同步);新增 `net_getpeername`(读 CONNECT 事件缓存,零 SPI);lftpd 改打印**对端** IP:port(台架判据:日志 `connection received from 192.168.1.x:<高位端口>`)。 - **教训**:显示层字节序 bug 不影响协议本身(FTP 功能全程正常),但会系统性误导排障——**日志里的地址也要当被测对象核对**。 +- **后续(2026-10-24):同一文件里的`s_addr` 两套约定已统一**。修完段序之后,本条只解决了显示侧——当时 `net_inet_addr` 仍属**另一族**(小端累加后又 `NET_HTONL`,返回 `0xC0A80164`),而 `net_inet_ntoa` / `net_getsockname` 属 POSIX 网络序那族(`0x6401A8C0`),两者**不互逆**;按前者写的消费者(`net_connect_locked` 用 `>>24` 取首段)恰好自洽,所以一直没人撞上。现已统一到 **POSIX 网络序一族**(判据 `&s_addr[0] == 第一段`;小端上 192.168.1.100 = `0x6401A8C0`):改 `net_inet_addr` + 出向 IP 数组改按内存字节顺位拆装(**进与出同时换族 → 芯片侧数组净效果不变**)+ 模拟器 `simulator/sim/net_socket_sim.c` 逐字同步;`net_inet_ntoa` 函数体一字未动。**为什么它能长期不被发现**:本条"修复"行下方 `ntoa` 自己注释里的例子(`192.168.1.100 → 0xC0A80164`)**取的是废弃族的值**——注释替错误背书,比没有注释更糟。规范定义见 `docs/网络/BSD_Socket_API_使用指南.md` §9.0;往返不变量由 TC-NETSOCK-100g / 100h 钉住。 --- diff --git a/scripts/id_registry.json b/scripts/id_registry.json index c9a60b6f..f2467ed8 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1563,7 +1563,7 @@ ], "scope": "global", "frozen": false, - "cursor": "TC-NETSOCK-545", + "cursor": "TC-NETSOCK-560", "cursor_owner_only": true, "check_dangling": true, "allow_orphans": true, @@ -1585,7 +1585,7 @@ ], "scope": "global", "frozen": false, - "cursor": "TC-SYSLOG-100", + "cursor": "TC-SYSLOG-107", "cursor_owner_only": true, "check_dangling": true, "allow_orphans": true, @@ -1651,7 +1651,7 @@ ], "scope": "global", "frozen": false, - "cursor": "TC-CONLOG-135", + "cursor": "TC-CONLOG-167", "cursor_owner_only": true, "check_dangling": true, "allow_orphans": true, diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index 6d54c06b..18fd88c5 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -90,11 +90,11 @@ }, "App/log/console_log.c": { "func_rate": 1.0, - "funcs_covered": 15, - "funcs_valid": 15, - "line_rate": 0.9947, - "lines_covered": 188, - "lines_valid": 189 + "funcs_covered": 16, + "funcs_valid": 16, + "line_rate": 0.995, + "lines_covered": 199, + "lines_valid": 200 }, "App/log/soe_rec.c": { "func_rate": 1.0, @@ -114,11 +114,11 @@ }, "App/log/sys_log_mcu.c": { "func_rate": 1.0, - "funcs_covered": 12, - "funcs_valid": 12, - "line_rate": 0.9904, - "lines_covered": 310, - "lines_valid": 313 + "funcs_covered": 13, + "funcs_valid": 13, + "line_rate": 0.9939, + "lines_covered": 324, + "lines_valid": 326 }, "App/protocol/iec104/iec104_asdu.c": { "func_rate": 1.0, @@ -274,11 +274,11 @@ }, "Drivers/BSP/NET/net_socket.c": { "func_rate": 1.0, - "funcs_covered": 52, - "funcs_valid": 52, - "line_rate": 0.9977, - "lines_covered": 881, - "lines_valid": 883 + "funcs_covered": 53, + "funcs_valid": 53, + "line_rate": 0.9978, + "lines_covered": 887, + "lines_valid": 889 }, "Drivers/BSP/NETCFG/ftp_cfg.c": { "func_rate": 1.0, @@ -329,9 +329,9 @@ }, "console_log": { "files": { - "App/log/console_log.c": 189 + "App/log/console_log.c": 200 }, - "lines_total": 189, + "lines_total": 200, "ok": true }, "freq": { @@ -446,9 +446,9 @@ }, "net_socket": { "files": { - "Drivers/BSP/NET/net_socket.c": 883 + "Drivers/BSP/NET/net_socket.c": 889 }, - "lines_total": 883, + "lines_total": 889, "ok": true }, "phasor": { @@ -517,9 +517,9 @@ }, "sys_log_mcu": { "files": { - "App/log/sys_log_mcu.c": 313 + "App/log/sys_log_mcu.c": 326 }, - "lines_total": 313, + "lines_total": 326, "ok": true }, "task_prof": { @@ -596,12 +596,12 @@ "files_generated_excluded": 5, "files_tested": 37, "files_untested": 107, - "func_rate": 0.9225, - "funcs_covered": 393, - "funcs_valid": 426, - "line_rate": 0.8926, - "lines_covered": 5574, - "lines_valid": 6245, + "func_rate": 0.9231, + "funcs_covered": 396, + "funcs_valid": 429, + "line_rate": 0.8932, + "lines_covered": 5605, + "lines_valid": 6275, "modules_ok": 33, "modules_total": 33 }, @@ -671,7 +671,7 @@ "path": "App/task/net_task.c" }, { - "code_lines": 100, + "code_lines": 101, "path": "Drivers/BSP/NET/net_types.h" }, { @@ -848,11 +848,11 @@ }, { "code_lines": 20, - "path": "Drivers/BSP/UI/ui_page_dc.c" + "path": "App/log/console_log.h" }, { - "code_lines": 19, - "path": "App/log/console_log.h" + "code_lines": 20, + "path": "Drivers/BSP/UI/ui_page_dc.c" }, { "code_lines": 19, diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c index b92b2eca..f48d712e 100644 --- a/simulator/sim/net_socket_sim.c +++ b/simulator/sim/net_socket_sim.c @@ -600,8 +600,16 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { * dest_addr - 目标地址 * addrlen - 地址长度 * 出口参数:无 - * 返回值:实际发送字节数,-1 失败 + * 返回值:实际发送字节数,-1 失败(精确码见 net_get_errno) * 限定条件:socket 已创建,buf/dest_addr 非空 + * 函数说明:⚠ **MCU 端本接口另有上下文约束**:它不经消息队列、在调用方上下文里 + * 直接 net_poll + 写芯片缓冲,故 MCU 侧只允许 net_init 的执行任务调用, + * 其他任务调用回 NET_ERR_NOT_IN_NETTASK(2026-10-24 定案,理由与 + * "为何不是改成走队列"见 net_socket.h 同名声明处)。 + * 本后端刻意**不加**同样的守卫:模拟器是单线程 + Winsock(自身线程安全), + * 没有"与 net_poll 抢同一根 SPI"这条危险面,也没有 RTOS 任务句柄可比。 + * 所以这是**有意的不对称**,不是漏实现;若将来模拟器引入多线程, + * 必须连同这条约束一起补上(按线程 ID 比 net_init 的调用线程)。 */ int net_sendto(int sockfd, const void *buf, int len, int flags, const struct net_sockaddr *dest_addr, int addrlen) { @@ -643,8 +651,10 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, * src_addr - 输出:发送方地址(可为 NULL) * addrlen - 输入输出:源地址长度(可为 NULL) * 出口参数:buf/src_addr/addrlen - 填充 - * 返回值:实际接收字节数,-1 失败 + * 返回值:实际接收字节数,-1 失败(精确码见 net_get_errno) * 限定条件:socket 已创建,buf 非空 + * 函数说明:MCU 端有"仅限执行任务调用"的上下文守卫(NET_ERR_NOT_IN_NETTASK), + * 本后端按 net_sendto 处同一条理由不加——有意的不对称,勿当成漏实现 */ int net_recvfrom(int sockfd, void *buf, int len, int flags, struct net_sockaddr *src_addr, int *addrlen) { @@ -803,7 +813,14 @@ int net_get_errno(void) { * 出口参数:无 * 返回值:网络字节序 IP(参数无效返回 0) * 限定条件:无 - * 函数说明:逐段解析后按 NET_HTONL 转网络字节序(与 MCU 端实现一致) + * 函数说明:与 MCU 端 net_inet_addr **逐字同语义**(2026-10-24:两套 IP 字节序约定统一为 POSIX 网络序): + * 按「第一段在主机序最高字节」累加,再经 NET_HTONL 转网络序,结果满足 + * POSIX/Winsock 的 in_addr 不变量——&s_addr 的第 0 个内存字节 == 第一段。 + * 本模拟器的 to_ws_addr()/from_ws_addr() 是把 net_sockaddr_in **整块 memcpy** + * 进 sockaddr_in 的,所以只有两边同族时才成立:旧实现(小端累加后再换端) + * 交给 Winsock 的是反序地址,只有从不走 inet_addr 的路径(bind ANY、 + * getpeername)才没暴露。改完后本函数与 net_inet_ntoa 互逆、与 Winsock 直拷兼容。 + * 段值超 255 按低 8 位截断;失败返回 0(不引入 NET_INADDR_NONE 判定)。 */ uint32_t net_inet_addr(const char *cp) { uint32_t addr = 0; @@ -819,7 +836,8 @@ uint32_t net_inet_addr(const char *cp) { val = val * 10 + (uint32_t)(*cp - '0'); cp++; } - addr |= (val << (i * 8)); + /* 高位先入:第 0 段左移三次后落在最高字节 */ + addr = (addr << 8) | (val & 0xFFU); if (*cp == '.') { cp++; } diff --git a/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c b/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c index 70d5f4b9..694b0770 100644 --- a/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c +++ b/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c @@ -74,13 +74,15 @@ * TC-CFGFST-052 cfg_store_free(NULL) 安全(不计数、不崩) * TC-CFGFST-053 heap_free 原样转发 xPortGetFreeHeapSize * - * 本套照出的现状(只登记、不改生产代码;进交 Lead 的报告): - * cfg_file_store_mcu.c:50 的函数头写"卷已挂载(未挂载时 f_open 返回 - * FR_NO_SYSTEM,转 -1)"——**FRESULT 里根本没有 FR_NO_SYSTEM 这个码** - * (Lib/FatFs/ff.h 的枚举是 FR_OK..FR_INVALID_PARAMETER 共 20 个,未挂载卷 - * 在本移植里实际返回 FR_NOT_READY / FR_NO_FILESYSTEM)。这条只写在注释里、 - * 不参与编译,所以一直没暴露;危害是后人照它写判据时会写出一个永不成立的 - * 分支(本模块实际也没写:它把所有非 FR_OK 一律转 -1,因此运行结果正确)。 + * 本套照出的现状 —— E 项已在 task-4(2026-10-24)修掉(纯注释更正,无行为改动): + * cfg_file_store_mcu.c 的 cfg_store_read "限定条件"原写"未挂载时 f_open 返回 + * FR_NO_SYSTEM"——FRESULT 里没有这个码(Lib/FatFs/ff.h 枚举共 20 个: + * FR_OK..FR_INVALID_PARAMETER)。实际未挂载返回 FR_NOT_READY(3),卷上无有效 FAT + * 返回 FR_NO_FILESYSTEM(13)。本模块对任何非 FR_OK 一律转 -1,所以运行结果一直 + * 是对的,危害在"后人照它写判据会写出永不成立的分支"。现已改成实际码,并写明 + * "不要枚举错误码集合"——唯一的例外是必须区分"删文件能不能修好"的自愈触发集 + * (App/log/sys_log_mcu.c 的 s_is_selfheal_code)。 + * 本套不新增断言:这条是注释层的事实错误,测试无从观测(生产行为未变)。 * * 故意没测(不留"看起来全覆盖"的错觉): * - **真掉电**:unlink 与 rename 之间断电图象只能靠"目标缺失 -> 上层按首启重建" diff --git a/test/host/common/ch395f_fake.c b/test/host/common/ch395f_fake.c index e74e0c60..1b2c11f5 100644 --- a/test/host/common/ch395f_fake.c +++ b/test/host/common/ch395f_fake.c @@ -194,10 +194,33 @@ void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len) g_chip.last_read_len = len; fake_log('r'); if ((p_data != NULL) && (len > 0U)) { - (void)memset(p_data, 0, len); /* 交出去的是零,测试不依赖内容 */ + if (g_fx.recv_bytes_n > 0U) { + /* 已预置内容:按预置交付,不足部分补零(默认分支行为不变) */ + uint16_t n = (len < g_fx.recv_bytes_n) ? len : g_fx.recv_bytes_n; + + (void)memcpy(p_data, g_fx.recv_bytes, n); + if (n < len) { + (void)memset((uint8_t *)p_data + n, 0, (size_t)(len - n)); + } + } else { + (void)memset(p_data, 0, len); /* 交出去的是零,测试不依赖内容 */ + } } } +void ch395f_fake_set_recv_bytes(const uint8_t *p, uint16_t n) +{ + uint16_t i; + + if (n > (uint16_t)sizeof(g_fx.recv_bytes)) { + n = (uint16_t)sizeof(g_fx.recv_bytes); + } + for (i = 0U; i < n; i++) { + g_fx.recv_bytes[i] = (p != NULL) ? p[i] : (uint8_t)0xA5U; + } + g_fx.recv_bytes_n = n; +} + void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len) { (void)p_data; diff --git a/test/host/common/ch395f_fake.h b/test/host/common/ch395f_fake.h index 41bf996c..c33a99f2 100644 --- a/test/host/common/ch395f_fake.h +++ b/test/host/common/ch395f_fake.h @@ -255,6 +255,11 @@ typedef struct { /* 1=复位机**不**替芯片把 cached_open 落回 CLOSED(造"永远关不掉"的芯片形态, * 用来量上层的复位超时收口)。默认 0 → 与既有行为一致。 */ uint8_t rst_keep_open[CH395F_FAKE_SOCK_MAX]; + /* GET_RECV_BUF 的**脚本化内容**(默认长度 0 → 与既有"全零交付"逐字相同)。 + * UDP 报文头(长度/源端口/源 IP)只有内容可脚本化才能被断言:net_recvfrom 要 + * 从芯片头里拆出对端地址,全零内容下"拆对了"与"拆反了"看起来一模一样。 */ + uint8_t recv_bytes[128]; + uint16_t recv_bytes_n; /* SINT 的**电平型**来源(可脚本化):读即清之后立刻把这个掩码放回 int_status。 * 默认 0 → 与既有假件行为逐字相同(读一次就没了)。真实芯片的 RECV_OK / * SENBUF_FREE 本就是"条件位":只要条件还在(RX 仍非空 / 发送缓冲仍空),下一轮 @@ -303,6 +308,8 @@ extern ch395f_fake_ext_obs_t g_fxchip; void ch395f_fake_ext_reset(void); void ch395f_fake_push_glob(uint16_t v); /* 预置全局中断返回序列 */ void ch395f_fake_set_sint_sticky(uint8_t sock, uint8_t mask); +/* 预置 GET_RECV_BUF 交付的字节(n=0 撤销预置);p 可为 NULL(表示填 n 个 0xA5) */ +void ch395f_fake_set_recv_bytes(const uint8_t *p, uint16_t n); /* 顺序断言辅助:清空调用日志 / 找某个短名第一次出现的位置(-1=没调过)。 * 刻意提供"清零日志",否则一次长轮询(如 sock_reset 的几十轮 'x')会把 96 格 * 环形外的顺序信息挤掉,让后续场景的顺序断言失真。 */ diff --git a/test/host/console_log/console_log_test.c b/test/host/console_log/console_log_test.c index 8f7d05e1..35a34227 100644 --- a/test/host/console_log/console_log_test.c +++ b/test/host/console_log/console_log_test.c @@ -23,7 +23,7 @@ * 留痕"改回直接调日志子系统,链接就失败——那正是必须挡住的(write_line → * con_append → drain → syslog_post → write_line 成环,静态栈深在每个任务上虚高)。 * - * 覆盖点(编号族 TC-CONLOG-*,属主登记处 = 本文件;实占 104 号 / 104 条断言): + * 覆盖点(编号族 TC-CONLOG-*,属主登记处 = 本文件;实占 127 号 / 127 条断言): * 000..006 宿主 printf 与本模块 fputc 的隔离 + 参数校验与 192B 截断 * 010..018 写者未注册 = 无锁直发(不入环、不置事件位、零临界区、不等待、256B 有界) * 020..038 注册后走环形:入环不发送、按 256B 分块、多生产者不交错、跨环尾两段拷贝、 @@ -32,20 +32,41 @@ * 050..062 缓冲满:DWT 硬上界丢弃、不覆盖旧行、等待中出现空间、无 DWT 时 200 tick * 上界、[ERR] dropped 直发不经环、读后即清 * 070..088 TX 僵尸通道:软复位自愈序列、故障即止、WARN 只一次、tx_fault_log_take - * 读后清、300ms 冷却两端、无符号回绕、tick==0 哨兵现状、半块/已上线仍判失败 - * 090..104 DWT 惰性使能三处寄存器写、使能失败退回 20000 次指令计数、超时判据不读 - * HAL_GetTick(round 8b 动机)、huart1.Instance == NULL 短路两路 + * 读后清、300ms 冷却两端、tick==0 也进冷却(哨兵撞值修好后为正向断言)、 + * 半块 / 已上线仍判失败 + * 090..106 DWT 惰性使能三处寄存器写、使能失败退回 20000 次指令计数、超时判据不读 + * HAL_GetTick(round 8b 动机)、huart1.Instance == NULL 短路两路 + + * "未初始化绝不 reinit"(误自愈修好后为正向断言)+ 证据不蒸发 * 110..117 fputc 重定向(未注册直发 / 注册逐字节入环 / ISR 上下文裸发 / 环满时 ISR * 仍不等待) * 120..122 drain 单次 250ms 预算与下轮续排(让位喂狗但不丢字节) * 130..135 计数支路也能把整块发完(不只会超时)+ 其 TC 计数上界 + DWT 支路 TC 抖动 * 后正常收尾 + con_append 直调时的自有参数校验 + * 107..108 冷却截止 tick 跨 49.7 天回绕两端(无符号差值),改用独立标志后仍成立 + * 140..150 三本账分开(docs §13-O2):dropped 只记真没落地的、tx_fault 只记 + * 软复位自愈、dbg_console_tx_timeout() 记写者所见块超时;裸发路径超时不进 + * 本账(口径写进函数头);通道不可用期间 [ERR] 计数不蒸发、恢复后补发 + * 160..167 结构性断言(环空位竞态):把第二个生产者塞进第一个生产者的临界区里,钉住 + * "复核空位 + 整行拷贝 + 推进 head"同处一个临界区、环用量不越容量、胜者行 + * 完整、败者行不留半截;再加环访问探针两条(166 逐字节命中数含上界、 + * 167 生产者路径零次越区)——167 是正控(证明上一组不是空转) * * 实测覆盖率(scripts/check_test_coverage.py --module console_log,/Od + /Zi 口径): - * App/log/console_log.c **188/189 行 = 99.5%,函数 15/15 = 100%**。 + * App/log/console_log.c **199/200 行 = 99.5%,函数 16/16 = 100%**(第 1 轮修复加了 + * 11 个有效行与 1 个新函数 dbg_console_tx_timeout,覆盖率口径同步上移)。 + * + * 这批用例的"有牙"是自证的(不是自我感觉):把四处生产码逐个退回修复前的写法 + * (scripts/check_test_coverage 无关,本地临时脚本做的变异测试),失败集合单调增长: + * 退回"判据在临界区外"的旧写法 -> 红 6 条(TC-CONLOG-161..166); + * 再退回"拿 tick 值当冷却哨兵" -> 红 11 条(再红 TC-CONLOG-080/082/083/084/108); + * 再退回"未初始化也自愈" -> 红 18 条(再红 TC-CONLOG-103..106/144/148/149); + * 四条全退回 -> 红 22 条(再红 TC-CONLOG-140/141/143/145)。 + * 改回修复版即 127/127 全绿;每一步的失败集合都是上一步的超集,说明四组断言各自 + * 咬住自己的那条语义,没有互相顶包。 + * 也就是说:这四组断言不是"写在假件规则上"的自证,而是真能咬住生产语义的。 * * 故意没测(逐行说明见交付报告): - * - console_log.c:352 唯一未覆盖行:snprintf 溢出后的钳位 `n = sizeof(note)-1`。 + * - console_log.c:383 唯一未覆盖行:snprintf 溢出后的钳位 `n = sizeof(note)-1`。 * note[64],前缀 24B + "%lu"(unsigned long 最多 10 位)+ 后缀 8B = 最长 42B * < 63B,任何输入都到不了这一行 —— 纯防御,不是漏测。 * - 真实周期时间量级:主机没有真 DWT CYCCNT 也没有 TIM7 的 uwTick,只能钉"判据在 @@ -54,6 +75,10 @@ * - osEventFlagsWait 唤醒写者与 50ms 兜底:写者任务体在 App/task/default_task.c。 * - MicroLIB 真实 retarget 链接语义:宿主 CRT 有自己的 fputc(实测与本模块符号 * 冲突 -> LNK2005),故本 TU 把被测 fputc 改名纳入,只验函数体不验链接钩子。 + * - 多生产者真并发:宿主单线程造不出两个执行流,故环空位竞态改钉**结构性事实**—— + * 桩里记临界区嵌套深度,并把生产的 s_con_ring 宏名成 (*con_ring_cell()), + * 于是"每一次碰环形缓冲"都能断言它是否发生在临界区内(详见 stub 头注释)。 + * 这不是"模拟并发",是把"判据与推进之间不出区"这句话本身变成可执行断言。 * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL;跑飞保险 exit(2)。 @@ -111,6 +136,13 @@ uint32_t con_crit_exit; uint32_t con_crit_max_depth; static uint32_t s_crit_depth; +/* 环形缓冲实体 + 探针(钩子本体在 stub/stm32f4_fake.h,判据说明也在那里) */ +uint8_t con_ring_store[4096U]; +int con_ring_probe_on; +uint32_t con_ring_probe_hits; +uint32_t con_ring_zero_depth_hits; +int con_ring_draining; + uint32_t con_ipsr; osEventFlagsId_t sys_evtHandle; /* 真实 sys_events.h 声明的全局句柄 */ @@ -159,6 +191,8 @@ void vTaskDelay(TickType_t ticks) } } +void (*con_crit_hook)(void); + void con_crit_push(void) { con_crit_enter++; @@ -166,6 +200,14 @@ void con_crit_push(void) if (s_crit_depth > con_crit_max_depth) { con_crit_max_depth = s_crit_depth; } + if (con_crit_hook != 0) { + con_crit_hook(); /* 深度已记好,注入体看到的是"已在区内" */ + } +} + +uint32_t con_crit_depth(void) +{ + return s_crit_depth; } void con_crit_pop(void) @@ -259,6 +301,8 @@ static void reset_all(void) s_con_tx_fault = 0U; s_con_tx_fault_log = 0U; s_con_cool_until = 0U; + s_con_cooling = 0U; + s_con_tx_timeout = 0U; s_con_draining = 0U; s_con_writer_up = 0U; s_con_dwt_up = 0U; @@ -274,12 +318,19 @@ static void reset_all(void) con_task_self = (TaskHandle_t)&s_task_other; con_delay_calls = 0U; con_delay_hook = 0; + con_crit_hook = 0; con_crit_enter = 0U; con_crit_exit = 0U; con_crit_max_depth = 0U; s_crit_depth = 0U; + memset(con_ring_store, 0, sizeof(con_ring_store)); + con_ring_probe_on = 0; + con_ring_probe_hits = 0U; + con_ring_zero_depth_hits = 0U; + con_ring_draining = 0; + con_ipsr = 0U; sys_evtHandle = (osEventFlagsId_t)&s_evt_obj; @@ -319,6 +370,37 @@ static int tx_is(uint32_t skip, const char *s) return memcmp(tx_at(skip), s, n) == 0; } +/* 断言 [0, n) 全部是同一个字符(回绕场景下按字符比对,不依赖 NUL 结尾) */ +static int tx_prefix_all(char c, uint32_t n) +{ + uint32_t i; + + if (con_uart_tx_n < n) { + return 0; + } + for (i = 0U; i < n; i++) { + if (con_uart_tx[i] != (uint8_t)c) { + return 0; + } + } + return 1; +} + +static int tx_at_is(char c, uint32_t skip, uint32_t n) +{ + uint32_t i; + + if (con_uart_tx_n < skip + n) { + return 0; + } + for (i = 0U; i < n; i++) { + if (con_uart_tx[skip + i] != (uint8_t)c) { + return 0; + } + } + return 1; +} + /* 在已上线字节里找一段(tx 是二进制缓冲,不依赖 NUL 结尾) */ static int tx_contains(const char *needle) { @@ -357,14 +439,18 @@ static uint32_t tx_count(const char *needle) static void writer_drain(void) { con_task_self = (TaskHandle_t)&s_task_writer; + con_ring_draining++; /* 此刻读环的是写者,不计入生产者违规 */ dbg_console_drain(); + con_ring_draining--; con_task_self = (TaskHandle_t)&s_task_other; } /* 后台排空(不改当前任务句柄):模拟"生产者等一轮,写者腾出了空间" */ static void bg_drain_hook(void) { + con_ring_draining++; dbg_console_drain(); + con_ring_draining--; } /* 把环填到只剩 want_free 字节可用:走真实入环路径(顺带踩到回绕)。 @@ -518,9 +604,9 @@ static void tc_ring(void) (void)dbg_console_write_line("0123456789ABCDEF", 16U); CHECK(s_con_head == 10U && used_now() == 16U, "TC-CONLOG-033 跨环尾入环后 head 回绕到 10(4090+16-4096)"); - CHECK(memcmp(&s_con_ring[4090], "012345", 6) == 0 - && s_con_ring[0] == (uint8_t)'6' && s_con_ring[9] == (uint8_t)'F' - && s_con_ring[10] == 0U, + CHECK(memcmp(&con_ring_store[4090], "012345", 6) == 0 + && con_ring_store[0] == (uint8_t)'6' && con_ring_store[9] == (uint8_t)'F' + && con_ring_store[10] == 0U, "TC-CONLOG-034 回绕两段拷贝位置正确(前 6B 填到环尾、余 10B 从 0 起)"); writer_drain(); CHECK(con_uart_block_n == 2U && con_uart_block_len[0] == 6U @@ -678,7 +764,7 @@ static void tc_tx_fault(void) static void tc_tx_cool(void) { - printf("-- TC-CONLOG-078..084 300ms 冷却两端 + 回绕 + tick==0 现状 --\n"); + printf("-- TC-CONLOG-078..084 + 107..108 cooldown both ends / tick==0 / wrap --\n"); reset_all(); fault_setup(7000U, 1); /* 治好的那次故障 */ CHECK(s_con_cool_until == 7000U && used_now() == 3839U, @@ -691,30 +777,41 @@ static void tc_tx_cool(void) "TC-CONLOG-079 差 1ms 仍在冷却:环不动、不重复自愈(冷却即节奏保护)"); con_tick_now = 7300U; /* 到点 */ writer_drain(); - CHECK(s_con_cool_until == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U, - "TC-CONLOG-080 到点即清冷却、放行本轮排空,且没有新增故障"); + CHECK(s_con_cooling == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U, + "TC-CONLOG-080 到点即清冷却标志、放行本轮排空,且没有新增故障" + "(cool_until 只存时刻,不再被拿来当哨兵清零)"); + /* 修复后的正向断言:冷却与否看独立标志,不再拿 tick 值当哨兵 */ reset_all(); - fault_setup(0U, 0); - CHECK(s_con_cool_until == 0U && dbg_console_tx_fault() == 1U, - "TC-CONLOG-081 现状:tick==0 时记出的 cool_until 撞上 0 哨兵 = 等于没冷却"); + fault_setup(0U, 0); /* 故障恰好落在 HAL_GetTick()==0 那一拍 */ + CHECK(s_con_cooling == 1U && dbg_console_tx_fault() == 1U, + "TC-CONLOG-081 tick==0 那拍的故障照样进入冷却(独立标志,不撞 0 哨兵)"); + writer_drain(); /* tick 仍是 0 */ + CHECK(dbg_console_tx_fault() == 1U && s_mx_init_calls == 1U + && used_now() == (DBG_CON_RING_SIZE - 1U) - DBG_CON_CHUNK_MAX, + "TC-CONLOG-082 同一 tick 上再来一轮被冷却挡住:不再白耗一次块超时"); + con_tick_now = DBG_CON_FAULT_COOL_MS - 1U; writer_drain(); - CHECK(dbg_console_tx_fault() == 2U - && used_now() == (DBG_CON_RING_SIZE - 1U) - 2U * DBG_CON_CHUNK_MAX, - "TC-CONLOG-082 上一条的后果:下一轮立刻再耗一次块超时(节奏保护在这一 tick 失效)"); + CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-083 冷却 299ms 仍未到点"); + con_uart_txe_stuck = 0; + con_tick_now = DBG_CON_FAULT_COOL_MS; /* 到点(且回绕起点正好是 0) */ + writer_drain(); + CHECK(s_con_cooling == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U, + "TC-CONLOG-084 到点后清冷却并放行:全程没有第二次故障"); reset_all(); attach_writer(); fill_ring(0U); - s_con_cool_until = 0xFFFFFFFFU; /* 上轮故障记在回绕点 */ + s_con_cooling = 1U; /* 在不在冷却只由这个标志说 */ + s_con_cool_until = 0xFFFFFFFFU; /* 截止时刻记在回绕点前 */ con_tick_now = 100U; /* 现在刚回绕到 100 */ writer_drain(); - CHECK(s_con_cool_until == 0xFFFFFFFFU && used_now() == 4095U, - "TC-CONLOG-083 跨 49.7 天回绕仍判在冷却内(无符号差值 101 < 300)"); + CHECK(s_con_cooling == 1U && used_now() == 4095U, + "TC-CONLOG-107 wrap-around deadline still cools (unsigned delta 101 < 300)"); con_tick_now = 401U; /* 无符号差值 402 >= 300 */ writer_drain(); - CHECK(s_con_cool_until == 0U && used_now() == 0U, - "TC-CONLOG-084 回绕后到点 -> 清冷却并把 4095B 全排空"); + CHECK(s_con_cooling == 0U && used_now() == 0U, + "TC-CONLOG-108 past the wrapped deadline the flag clears and the ring empties"); } static void tc_tx_half(void) @@ -782,10 +879,10 @@ static void tc_dwt(void) "TC-CONLOG-099 DWT 支路第一次自旋就判超时(上界真的起作用,不会拖满 20000 次)"); } -/* ============ TC-CONLOG-100..104 huart1.Instance == NULL 短路 ============ */ +/* ============ TC-CONLOG-100..106 huart1.Instance == NULL 短路 + 未初始化分诊 ============ */ static void tc_null_uart(void) { - printf("-- TC-CONLOG-100..104 USART1 未初始化:安全让位,不再触发二次故障 --\n"); + printf("-- TC-CONLOG-100..106 USART1 not initialised: yield, never reinit, keep proof --\n"); reset_all(); huart1.Instance = 0; dbg_console_write_raw("boom", 5U); @@ -799,17 +896,25 @@ static void tc_null_uart(void) CHECK(dbg_console_write_line("early", 5U) == 5, "TC-CONLOG-102 未注册路径(内核前)撞上无串口也不崩、仍返回行长"); + /* 修复后的正向断言:未初始化 != 僵尸通道,绝不重配外设 */ reset_all(); attach_writer(); (void)dbg_console_write_line("abc", 3U); - huart1.Instance = 0; /* 写者起来了但串口被毁 */ + huart1.Instance = 0; /* 写者起来了但串口还没初始化 */ s_mx_repair = 1; writer_drain(); - CHECK(dbg_console_tx_fault() == 1U && s_mx_init_calls == 1U && huart1.Instance != 0, - "TC-CONLOG-103 未初始化也被当作僵尸通道自愈:重初始化把句柄装回来"); - con_dwt_step = SystemCoreClock / 4U; - writer_drain(); /* 仍在冷却 -> 让位 */ - CHECK(used_now() == 0U, "TC-CONLOG-104 自愈那一轮已把出环字节丢掉,没留下脏数据"); + CHECK(s_mx_init_calls == 0U && con_rcc_disables == 0U && con_nvic_clears == 0U, + "TC-CONLOG-103 Instance==NULL never triggers a peripheral soft reset (no RCC/GPIO reconfig before HAL is ready)"); + CHECK(dbg_console_tx_fault() == 0U && s_con_cooling == 0U, + "TC-CONLOG-104 not treated as a zombie channel: tx_fault and cooldown stay untouched"); + CHECK(dbg_console_dropped() == 1U && used_now() == 0U, + "TC-CONLOG-105 the block that left the ring but never hit the wire counts as dropped"); + + huart1.Instance = &g_con_uart; /* 串口随后初始化好 */ + con_uart_tx_n = 0U; + writer_drain(); + CHECK(tx_contains("[ERR] CONSOLE dropped 1 lines") && dbg_console_dropped() == 0U, + "TC-CONLOG-106 drop evidence survives the outage and is flushed once the UART is back"); } /* ============ TC-CONLOG-110..115 fputc 重定向 ============ */ @@ -824,7 +929,7 @@ static void tc_fputc(void) reset_all(); attach_writer(); (void)con_fputc_under_test('R', stdout); - CHECK(con_uart_tx_n == 0U && used_now() == 1U && s_con_ring[0] == (uint8_t)'R', + CHECK(con_uart_tx_n == 0U && used_now() == 1U && con_ring_store[0] == (uint8_t)'R', "TC-CONLOG-112 注册后 fputc 也收敛到单写者路径(逐字节入环)"); writer_drain(); CHECK(tx_is(0U, "R"), "TC-CONLOG-113 写者把 printf 字节发出"); @@ -897,10 +1002,178 @@ static void tc_counted_paths(void) CHECK(con_append("abc", 0U) == -1, "TC-CONLOG-135 con_append(len=0) 同样拒绝"); } + +/* ============ TC-CONLOG-140..150 三本账分开(docs §13-O2) ============ */ +static void tc_three_ledgers(void) +{ + uint32_t f0; + uint32_t v0; + + printf("-- TC-CONLOG-140..150 dropped / tx_fault / tx_timeout: three separate books --\n"); + + /* TC 卡死:8 个字节都交给了发送器 -> 只涨超时账 + 自愈账,丢弃账不动 */ + reset_all(); + attach_writer(); + (void)dbg_console_write_line("ABCDEFGH", 8U); + con_uart_tc_stuck = 1; + con_dwt_step = SystemCoreClock / 4U; + writer_drain(); + CHECK(dbg_console_tx_timeout() == 1U, + "TC-CONLOG-140 TC timeout is booked on the new timeout ledger"); + CHECK(dbg_console_dropped() == 1U && dbg_console_tx_fault() == 1U, + "TC-CONLOG-141 timed-out block is no longer silently swallowed: dropped and self-heal each move by one"); + + /* 缓冲满丢行:只算丢弃,与 UART 无关 */ + reset_all(); + attach_writer(); + fill_ring(10U); + con_dwt_step = SystemCoreClock / 4U; + (void)dbg_console_write_line("0123456789ABCDEFGHIJ", 20U); + CHECK(dbg_console_dropped() == 1U && dbg_console_tx_timeout() == 0U + && dbg_console_tx_fault() == 0U, + "TC-CONLOG-142 buffer-full drop touches only the dropped ledger (no UART involved)"); + + /* 口径:只有写者排空看到的块才进超时账;裸发路径(内核前 / ISR / 告警行) + * 没有"块"的概念,它的超时不混进来(否则运维读数失去意义)。 + * 两条硬上界(DWT 与指令计数)都算,这一点用同一断言在两处覆盖。 */ + reset_all(); + con_dwt_ctrl_sticky = 0; + con_uart_txe_stuck = 1; + dbg_console_write_raw("Q", 1U); + CHECK(dbg_console_tx_timeout() == 0U, + "TC-CONLOG-143 raw-path timeout stays out of the block-timeout ledger (documented scope)"); + reset_all(); + attach_writer(); + con_dwt_ctrl_sticky = 0; /* DWT 用不了 -> 计数上界支路 */ + con_uart_txe_stuck = 1; + (void)dbg_console_write_line("Q", 1U); + writer_drain(); + CHECK(dbg_console_tx_timeout() == 1U, + "TC-CONLOG-150 the counted-guard upper bound is booked on the same ledger as the DWT one"); + + /* 通道不可用:既不是超时也不是自愈,只算丢弃(分诊与分账的交点) */ + reset_all(); + attach_writer(); + (void)dbg_console_write_line("xyz", 3U); + huart1.Instance = 0; + writer_drain(); + CHECK(dbg_console_dropped() == 1U && dbg_console_tx_timeout() == 0U + && dbg_console_tx_fault() == 0U, + "TC-CONLOG-144 dead channel (not initialised): only the dropped ledger moves"); + + /* 145:两轮都坏 -> 超时账与自愈账各自累计(互不吞并) */ + reset_all(); + attach_writer(); + fill_ring(0U); + s_con_tx_fault = 1U; /* 预置:让首轮那条 WARN 留痕不发, + * 否则通道没治好时它自己也吃一次超时 */ + con_uart_txe_stuck = 1; + con_dwt_step = SystemCoreClock / 4U; + s_mx_repair = 0; + f0 = dbg_console_tx_fault(); + v0 = dbg_console_tx_timeout(); + writer_drain(); + con_tick_now = 400U; /* 出冷却,再来一轮 */ + writer_drain(); + CHECK((dbg_console_tx_timeout() - v0) == 2U, + "TC-CONLOG-145 timeout ledger grows once per timed-out block across rounds"); + CHECK((dbg_console_tx_fault() - f0) == 2U, + "TC-CONLOG-146 self-heal ledger grows on its own, independent of the timeout ledger"); + CHECK(dbg_console_tx_timeout() == dbg_console_tx_timeout(), + "TC-CONLOG-147 the timeout getter is a pure read (no clear-on-read semantics)"); + + /* 148/149:通道不可用期间 [ERR] 告警不能蒸发(丢的证据必须留得住) */ + reset_all(); + attach_writer(); + fill_ring(10U); + con_dwt_step = SystemCoreClock / 4U; + (void)dbg_console_write_line("0123456789ABCDEFGHIJ", 20U); /* 缓冲满丢 1 行 */ + con_dwt_step = 0U; + huart1.Instance = 0; /* 告警那一发也发不出去 */ + writer_drain(); + CHECK(dbg_console_dropped() >= 1U, + "TC-CONLOG-148 drop count survives while the channel is unavailable (evidence no longer washed away)"); + huart1.Instance = &g_con_uart; + con_uart_tx_n = 0U; + writer_drain(); + CHECK(tx_contains("[ERR] CONSOLE dropped") && dbg_console_dropped() == 0U, + "TC-CONLOG-149 once the UART is back the pending drop count is reported and then cleared"); +} + +/* ============ TC-CONLOG-160..165 check-then-act 的结构性钉法(环空位竞态) ========= */ +static char s_inject_buf[64]; +static uint32_t s_inject_left; +static uint32_t s_inject_hits; + +/* 第二个生产者:被塞进"第一个生产者的临界区"里执行。旧写法里判据在区外, + * 第一个生产者复核时看不到这次写入 -> head 合计越过容量;新写法复核在区内, + * 一定看得到 -> 第一个生产者改为等待/丢弃,绝不覆盖未发出的旧行。 */ +static void crit_second_producer(void) +{ + if (s_inject_left == 0U) { + return; + } + s_inject_left--; + s_inject_hits++; + (void)con_append(s_inject_buf, 60U); +} + +static void tc_check_then_act(void) +{ + char first[64]; + uint32_t used_after; + int rc; + + printf("-- TC-CONLOG-160..165 re-check must stay inside the critical section --\n"); + reset_all(); + attach_writer(); + fill_ring(100U); /* 只剩 100B 可用:两个 60B 塞不下 */ + memset(first, 'X', sizeof(first)); + memset(s_inject_buf, 'Y', sizeof(s_inject_buf)); + s_inject_left = 1U; + s_inject_hits = 0U; + con_ring_probe_hits = 0U; + con_ring_zero_depth_hits = 0U; + con_ring_probe_on = 1; /* 探针只在本次调用期间计数 */ + con_crit_hook = crit_second_producer; + con_dwt_step = SystemCoreClock / 4U; /* 复核不过就直接判上界,不空等 */ + rc = dbg_console_write_line(first, 60U); + con_crit_hook = 0; + con_ring_probe_on = 0; + con_dwt_step = 0U; + used_after = used_now(); + + CHECK(s_inject_hits == 1U && con_crit_max_depth == 2U, + "TC-CONLOG-160 the second producer ran with the region already held (depth two nesting)"); + CHECK(rc == -1 && dbg_console_dropped() == 1U, + "TC-CONLOG-161 the first producer re-checked inside the region, found space gone and dropped instead of overwriting"); + CHECK(used_after == (DBG_CON_RING_SIZE - 1U) - 40U, + "TC-CONLOG-162 ring usage stayed within capacity: 3995B + one 60B line, not 4115B wrapped past the end"); + + writer_drain(); /* 末尾还会跟一条 [ERR] dropped */ + CHECK(con_uart_tx_n >= used_after && tx_contains("[ERR] CONSOLE dropped 1"), + "TC-CONLOG-163 everything that survived the race is delivered, plus the drop warning"); + CHECK(tx_prefix_all('Z', (DBG_CON_RING_SIZE - 1U) - 100U) + && tx_at_is('Y', (DBG_CON_RING_SIZE - 1U) - 100U, 60U), + "TC-CONLOG-164 old 3995 bytes intact then the winner 60B line intact, loser absent (no silent overwrite)"); + CHECK(!tx_contains("XXXXXX"), + "TC-CONLOG-165 the dropped line never made it onto the wire half-written"); + + /* 环访问探针(把生产的 s_con_ring 名成 (*con_ring_cell()),每次碰环都进钩子, + * 钩子本体见 stub 头注释)。167 是纯正控:证明探针真数到了逐字节访问、不是零命中 + * 空转。166 的下界是正控、上界则是判据——旧写法里"复核"发生在区外,败者那一份 60B + * 也会真写进环(合计约 120 次),所以本条在退回旧写法时同样会红。 */ + /* 期望次数 = 胜者那一行的逐字节写入(60B);败者复核失败后一个字节都没写, + * 这正是"不覆盖未发出旧行"的直接体现,所以命中数恰好只有 60 多几次。 */ + CHECK(con_ring_probe_hits >= 60U && con_ring_probe_hits < 120U, + "TC-CONLOG-166 ring probe counted exactly the winner line byte writes (loser wrote nothing)"); + CHECK(con_ring_zero_depth_hits == 0U, + "TC-CONLOG-167 no producer-side ring access ever happened outside a critical section"); +} + /* ===================== main ===================== */ int main(void) { - /* 先验一次宿主 CRT 与本模块 fputc 的关系(被 tc_params 记成 TC-CONLOG-000) */ tc_params(); tc_unattached(); tc_ring(); @@ -914,6 +1187,8 @@ int main(void) tc_fputc(); tc_budget(); tc_counted_paths(); + tc_three_ledgers(); + tc_check_then_act(); printf("=== Console Log Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); diff --git a/test/host/console_log/stub/stm32f4_fake.h b/test/host/console_log/stub/stm32f4_fake.h index 88b79bd8..2fb6b672 100644 --- a/test/host/console_log/stub/stm32f4_fake.h +++ b/test/host/console_log/stub/stm32f4_fake.h @@ -246,9 +246,41 @@ extern uint32_t con_crit_enter; extern uint32_t con_crit_exit; void con_crit_push(void); void con_crit_pop(void); +uint32_t con_crit_depth(void); +/* 进区回调:环空位竞态那条正向用例靠它把"第二个生产者"精确塞进第一个生产者的临界区里 + * (旧写法里判据在区外,塞进去的写入不会被复核到;新写法复核在区内,必然看到)。 + * 只在 con_crit_push 记完深度之后调用,不改任何深度值。 */ +extern void (*con_crit_hook)(void); #define taskENTER_CRITICAL() con_crit_push() #define taskEXIT_CRITICAL() con_crit_pop() +/* 环形缓冲实体改由测试持有:生产代码里的 s_con_ring 被宏名成 (*con_ring_cell()), + * 于是**每一次对环的取址访问都进这个钩子** —— 这是本轮唯一能把"整行拷贝发生在 + * 临界区内、而读环发送发生在临界区外"这件事直接断言出来的办法(生产侧的空位判据 + * 与拷贝之间没有任何函数调用可插桩,靠调用深度而不是靠时序观察来钉结构性事实)。 + * 钩子只记事实不改行为:返回的就是那块 4096B 存储。 */ +/* 注意:**不要**在这里定义 DBG_CON_RING_SIZE(那是生产 console_log.c 的宏, + * 重复定义会撞 C4005);环形存储用字面量 4096U,与生产的数组维度一致即兼容。 */ +extern uint8_t con_ring_store[4096U]; +extern int con_ring_probe_on; /* 只在被测那一次调用期间开启 */ +extern uint32_t con_ring_probe_hits; /* 探针开启期间被索引取址的次数(证明真碰了环) */ +extern uint32_t con_ring_zero_depth_hits;/* 其中发生在临界区之外(深度 0)的次数 */ +/* 写者读环发本来就在临界区外(正确行为),探针不该把它算成违规:由测试在进入 + * drain 的前后翻转这个标志(等价于把生产侧的 s_con_draining 递进来) */ +extern int con_ring_draining; + +/* 用老式无原型定义,与生产那行 static 声明展开后的形态一致(避免 C4255/C4273) */ +static uint8_t (*con_ring_cell())[4096U] { + if (con_ring_probe_on != 0) { + con_ring_probe_hits++; + if ((con_crit_depth() == 0U) && (con_ring_draining == 0)) { + con_ring_zero_depth_hits++; /* 生产者绕过临界区碰环 = 竞态回来了 */ + } + } + return &con_ring_store; +} +#define s_con_ring (*con_ring_cell()) + /* 真实 __get_IPSR 是 core_cmFunc.h 的静态内联;这里做成宏读一个可控量 */ extern uint32_t con_ipsr; /* 0 = 线程模式;!=0 = 中断/异常上下文 */ #define __get_IPSR() (con_ipsr) diff --git a/test/host/net_socket/net_socket_test.c b/test/host/net_socket/net_socket_test.c index 79e5bf4a..8a2107d6 100644 --- a/test/host/net_socket/net_socket_test.c +++ b/test/host/net_socket/net_socket_test.c @@ -97,6 +97,9 @@ static void reset_all(void) s_net_generation = 0U; s_net_initialized = 0U; s_net_msg_queue = NULL; + /* UDP 上下文守卫的判据也是生产 static:不清就会把上一场景的"执行任务"带进本场景 + * (表现为守卫判错上下文,且与任务句柄复用后的地址撞车) */ + s_net_owner_task = NULL; g_ns_info_count = 0; g_ns_error_count = 0; g_ns_debug_count = 0; @@ -114,12 +117,26 @@ static void bring_up(void) (void)net_msg_queue_adopt((osMessageQueueId_t)&s_fake_queue); /* 装泵:调用方阻塞等待时以 netTask 身份跑一轮消息分派(= 跨上下文那条链) */ g_ns_pump = net_process_messages; + /* net_init 以 netTask 身份跑:生产里它就是 net_stack_bring_up() 在 netTask 内 + * 调用的(App/task/net_task.c:92),而本层执行任务句柄(UDP 上下文守卫的判据) + * 正是在这里捕获——这里若不换身份,整个 UDP 守卫就只能测到"自己判自己" */ + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; g_peers.pending_capture_calls = 0; /* bring_up 内部的登记动作不该算进场景 */ g_ns_error_count = 0; g_ns_info_count = 0; } +/* UDP 两接口的场景前置:net_sendto / net_recvfrom 有 netTask 上下文守卫, + * 合法用法只能在 netTask 里调 —— 这里把"换身份"收进一个入口,避免每个场景各写 + * 一遍换/还原而漏掉一次(漏了就是"守卫把正常路径判成拒绝"的假故障)。 */ +static void bring_up_netctx(void) +{ + bring_up(); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; +} + /* 直接造一个在用的控制块(多数场景不需要走完整的 socket()/bind()) */ static int mk_sock(int type, net_sock_state_t state) { @@ -141,26 +158,64 @@ static int mk_sock(int type, net_sock_state_t state) /* = 分片 1 纯函数面与 fd 表 ===================== */ -/* TC-NETSOCK-100 点分十进制 ↔ 32 位:钉**当前行为**并显式标注疑点(见报告) - * 现状:net_inet_addr 把首段放在**最高字节**(0xC0A80164),而 net_getsockname 组装 - * s_addr 时把首段放在**最低字节**,net_inet_ntoa 只与后者互为逆——同一文件里 - * 存在两套字节序约定。这里逐条钉住,不替生产"圆场"。 */ +/* TC-NETSOCK-100 点分十进制 ↔ 32 位:**正向断言**(字节序统一修复后) + * 全仓唯一约定:s_addr 是网络字节序整数,&s_addr 的第 0 个内存字节 == 点分第一段; + * 小端主机上 "192.168.1.100" 的数值 = 0x6401A8C0。本组断言的落点是「互逆」与 + * 「同族可比」,不是某个中间数值——数值只是这条约定的一个投影。 */ static void tc_100_inet_helpers(void) { + /* 往返不变量的样本集:两个全零/全一边界 + 三个非回文地址(非回文才能考出顺位) */ + static const char *const k_ips[5] = { + "0.0.0.0", "255.255.255.255", "1.1.1.1", "192.168.1.100", "10.0.0.0" + }; char buf[16]; + int i; reset_all(); - CHECK(net_inet_addr(NULL) == 0U, "TC-NETSOCK-100a inet_addr(NULL) 返 0(无效)"); - CHECK(net_inet_addr("192.168.1.100") == 0xC0A80164U, - "TC-NETSOCK-100b inet_addr 首段落在最高字节(当前实现)"); - CHECK(net_inet_ntoa(0xC0A80164U, buf) == buf && strcmp(buf, "100.1.168.192") == 0, - "TC-NETSOCK-100c ntoa(同一 32 位值) 与 inet_addr 不互逆(疑点已报,钉当前行为)"); + CHECK(net_inet_addr(NULL) == 0U, "TC-NETSOCK-100a inet_addr(NULL) returns 0 (invalid)"); + CHECK(net_inet_addr("192.168.1.100") == 0x6401A8C0U, + "TC-NETSOCK-100b inet_addr returns network-order int (LE value 0x6401A8C0 = bytes C0 A8 01 64)"); + CHECK(net_inet_ntoa(net_inet_addr("192.168.1.100"), buf) == buf && + strcmp(buf, "192.168.1.100") == 0, + "TC-NETSOCK-100c ntoa(inet_addr(s)) == s: the two are exact inverses"); CHECK(net_inet_ntoa(0x6401A8C0U, buf) == buf && strcmp(buf, "192.168.1.100") == 0, - "TC-NETSOCK-100d ntoa 与 getsockname 的 s_addr 约定互逆(E13 定案的段序:高段在前)"); - CHECK(net_inet_ntoa(0U, NULL) == NULL, "TC-NETSOCK-100e ntoa(NULL 缓冲) 返 NULL 不崩"); - /* 单段/缺分隔的容错:逐段解析,非数字段当 0(钉实现,不钉"应该是多少") */ - CHECK(net_inet_addr("10.0.0.1") == 0x0A000001U, "TC-NETSOCK-100f inet_addr 10.0.0.1"); + "TC-NETSOCK-100d ntoa segment order is MSB-first (fixed by E13/TRAP-NET-21, unchanged here)"); + CHECK(net_inet_ntoa(0U, NULL) == NULL, "TC-NETSOCK-100e ntoa(NULL buf) returns NULL without crashing"); + CHECK(net_inet_addr("10.0.0.1") == 0x0100000AU, + "TC-NETSOCK-100f non-palindrome address: 10.0.0.1 -> 0x0100000A (first octet in low byte)"); + + /* 正向断言 1:字符串 → 整数 → 字符串 必须回到原串(含 0.0.0.0 与 255.255.255.255) */ + for (i = 0; i < 5; i++) { + (void)memset(buf, 0, sizeof(buf)); + if (net_inet_ntoa(net_inet_addr(k_ips[i]), buf) == NULL || strcmp(buf, k_ips[i]) != 0) { + break; + } + } + CHECK(i == 5, "TC-NETSOCK-100g round trip ntoa(inet_addr(s)) == s over 5 addrs incl. 0.0.0.0 and 255.255.255.255"); + + /* 正向断言 2:整数 → 字符串 → 整数 必须回到原值(反方向也闭合) */ + for (i = 0; i < 5; i++) { + uint32_t v = net_inet_addr(k_ips[i]); + + (void)memset(buf, 0, sizeof(buf)); + (void)net_inet_ntoa(v, buf); + if (net_inet_addr(buf) != v) { + break; + } + } + CHECK(i == 5, "TC-NETSOCK-100h round trip inet_addr(ntoa(v)) == v (inverse direction closes too)"); + + /* 边界语义别改坏:255.255.255.255 是**合法地址**,不得被当失败; + * 失败只有"参数为 NULL"一种,返回值 0 同时是 0.0.0.0 —— 这是 BSD inet_addr + * 的既有歧义,本轮刻意不改(改它要动全部调用者的判错方式,风险大于收益)。 */ + CHECK(net_inet_addr("255.255.255.255") == 0xFFFFFFFFU, + "TC-NETSOCK-100i 255.255.255.255 parses to a valid value, never demoted to failure"); + CHECK(net_inet_addr("0.0.0.0") == 0U, + "TC-NETSOCK-100j 0.0.0.0 -> 0 (BSD inet_addr ambiguity, not introduced by this fix)"); + /* 段值溢出:按低 8 位截断(旧实现会把溢出位溅进相邻段,得到不可预期的混合值) */ + CHECK(net_inet_addr("300.1.1.1") == net_inet_addr("44.1.1.1"), + "TC-NETSOCK-100k segment 300 truncates to 44 (300 and 0xFF) with no spill into neighbours"); } static void tc_101_byteorder(void) diff --git a/test/host/net_socket/ns_s5_io.c b/test/host/net_socket/ns_s5_io.c index b103cecb..fb02382d 100644 --- a/test/host/net_socket/ns_s5_io.c +++ b/test/host/net_socket/ns_s5_io.c @@ -435,7 +435,7 @@ static void tc_440_udp_datagram(void) int addrlen; int fd; - bring_up(); + bring_up_netctx(); CHECK(net_sendto(-1, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-440a sendto 越界 → BADF"); CHECK(net_sendto(0, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF, @@ -451,7 +451,7 @@ static void tc_440_udp_datagram(void) /* 带目的地址:先设 DesIP/DesPort 再写数据(不带上限的自旋实现,健康链路一次写完) */ reset_all(); - bring_up(); + bring_up_netctx(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); (void)memset(&in, 0, sizeof(in)); in.sin_port = net_htons(8888); @@ -465,7 +465,7 @@ static void tc_440_udp_datagram(void) /* 大报分块(仍受 NET_SEND_CHUNK_MAX 上界约束) */ reset_all(); - bring_up(); + bring_up_netctx(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); g_ns_int_pin_set = 0; ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); @@ -475,7 +475,7 @@ static void tc_440_udp_datagram(void) /* recvfrom:前 8 字节是芯片 UDP 头(长度/srcPort/srcIP),要剥掉再交出去 */ reset_all(); - bring_up(); + bring_up_netctx(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); ch395f_fake_push_recv_len((uint8_t)fd, 8U + 20U); addrlen = (int)sizeof(in); @@ -487,7 +487,7 @@ static void tc_440_udp_datagram(void) /* 无数据 + 非阻塞 → WOULDBLOCK(阻塞形态是自旋无预算,本套刻意不测,见交付说明) */ reset_all(); - bring_up(); + bring_up_netctx(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440m 非阻塞无数据 → WOULDBLOCK"); @@ -496,7 +496,7 @@ static void tc_440_udp_datagram(void) net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440n 非阻塞属性同判据生效"); /* 短于 8 字节的残包:不剥头,整包交出去(钉当前实现的边界,不猜"应该怎么算") */ reset_all(); - bring_up(); + bring_up_netctx(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); ch395f_fake_push_recv_len((uint8_t)fd, 5U); CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == 5, diff --git a/test/host/net_socket/ns_s6_edges.c b/test/host/net_socket/ns_s6_edges.c index 9afe7053..4f8f86ea 100644 --- a/test/host/net_socket/ns_s6_edges.c +++ b/test/host/net_socket/ns_s6_edges.c @@ -186,7 +186,7 @@ static void tc_506_sendto_stall_and_short(void) int fd; int rc; - bring_up(); + bring_up_netctx(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); s_net_socks[fd].send_ready = 0U; g_ns_int_pin_set = 0; @@ -201,7 +201,7 @@ static void tc_506_sendto_stall_and_short(void) reset_all(); /* 写完一块后等发完的超时出口 */ - bring_up(); + bring_up_netctx(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); g_ns_int_pin_set = 0; ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); @@ -377,6 +377,184 @@ static void tc_545_local_entries_without_queue(void) reset_all(); } + +/* ============= TC-NETSOCK-550 s_addr 同族一致性(字节序统一后的收口断言) ============= + * + * 这次字节序统一的实质不是「给 inet_addr 换个数值」,而是「全仓只剩一套 s_addr 约定」。 + * 所以这组断言全部写在**跨函数的接缝**上:解析→存储→芯片下发→回读→打印, + * 每一环都拿同一句点分十进制做基准。任何一处又引入第二套顺位,这里必红。 + */ + +/* TC-NETSOCK-550 解析 → 存储 → 打印 闭合(getsockname / getpeername 同族可比对) */ +static void tc_550_family_roundtrip(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + char txt[16]; + int addrlen; + int fd; + + bring_up(); + + /* 1) getsockname 的 s_addr 与 inet_addr 解析结果**直接相等**(旧实现差一次换端) */ + (void)mk_established(NET_SOCK_STREAM); /* getsockname 要 in_use 的描述符 */ + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + CHECK(net_getsockname(0, sa, &addrlen) == 0, + "TC-NETSOCK-550a getsockname precondition ok"); + CHECK(in.sin_addr.s_addr == net_inet_addr("192.168.1.100"), + "TC-NETSOCK-550b getsockname s_addr is the same family as inet_addr (direct equality)"); + CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "192.168.1.100") == 0, + "TC-NETSOCK-550c local address closes parse-store-print (old code printed 100.1.168.192)"); + + /* 2) connect 的对端簿记同样闭合(lftpd:864 与 104 显示走的就是这条链) */ + fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(2404U); + in.sin_addr.s_addr = net_inet_addr("192.168.1.200"); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; + CHECK(net_connect_locked(fd, sa, (int)sizeof(in)) == 0, + "TC-NETSOCK-550d connect precondition ok"); + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + CHECK(net_getpeername(fd, sa, &addrlen) == 0, + "TC-NETSOCK-550e getpeername ok (reads event cache, zero SPI)"); + CHECK(in.sin_addr.s_addr == net_inet_addr("192.168.1.200"), + "TC-NETSOCK-550f getpeername peer equals the s_addr fed into net_connect"); + CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "192.168.1.200") == 0, + "TC-NETSOCK-550g peer address prints right (lftpd connection-received log depends on it)"); + CHECK(s_net_socks[fd].remote_ip_arr[0] == 192U && + s_net_socks[fd].remote_ip_arr[3] == 200U, + "TC-NETSOCK-550h chip des_ip array stays wire order [192,168,1,200]: on-wire behaviour unchanged"); + reset_all(); +} + +/* TC-NETSOCK-555 UDP 报文头里的源地址拆装(同族的第四个生产者:net_recvfrom) */ +static void tc_555_udp_src_addr(void) +{ + /* CH395F UDP 头(§9.2.4):[0-1] 总长(LE) [2-3] 源端口(LE) [4-7] 源 IP(线序) */ + static const uint8_t hdr[8] = { + 11U, 0U, /* 总长 = 8 头 + 3 数据 */ + 0x1FU, 0xCBU, /* 源端口 51999(小端 1F CB) */ + 239U, 1U, 2U, 3U /* 源 IP 线序:239.1.2.3 */ + }; + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + char buf[16]; + char txt[16]; + int addrlen; + int fd; + + bring_up_netctx(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + ch395f_fake_push_recv_len((uint8_t)fd, (uint16_t)(8U + 3U)); + ch395f_fake_set_recv_bytes(hdr, (uint16_t)sizeof(hdr)); + + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &addrlen) == 3, + "TC-NETSOCK-555a strips the 8-byte chip header and delivers only the 3-byte payload"); + CHECK(in.sin_addr.s_addr == net_inet_addr("239.1.2.3"), + "TC-NETSOCK-555b source IP unpack matches the inet_addr family"); + CHECK(in.sin_port == net_htons(51999U), + "TC-NETSOCK-555c source port read chip little-endian then stored in network order"); + CHECK(addrlen == (int)sizeof(in) && net_ntohs(in.sin_port) == 51999U, + "TC-NETSOCK-555d addrlen filled and port round-trips to 51999"); + CHECK(strcmp(net_inet_ntoa(in.sin_addr.s_addr, txt), "239.1.2.3") == 0, + "TC-NETSOCK-555e multicast source address prints correctly"); + reset_all(); +} + + +/* TC-NETSOCK-560 UDP 上下文守卫:非执行任务不得直调芯片 + * 三条一起才成立,缺一条都不算把危险面关掉: + * ① 执行任务内照常成功(守卫不是"把 UDP 判死") + * ② 非执行任务被拒且**一次芯片事务都没有**(拒了却已经抢过 SPI = 白拒) + * ③ 正常错误路径不被守卫吞掉,且**走消息队列的接口完全不受影响** + * (那些接口生来就是给非 netTask 用的,误伤它们才是大事故) + */ +static void tc_560_udp_context_guard(void) +{ + char buf[8]; + int fd; + + /* ① 执行任务上下文:与修复前逐字相同 */ + bring_up_netctx(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == 3, + "TC-NETSOCK-560a net_sendto unchanged inside the executing task"); + reset_all(); + + /* ② 非执行任务:拒绝 + 零芯片事务(bring_up 里 net_init 是以 netTask 身份跑的, + * 此刻 g_ns_cur_task 已还原成 caller,正是"别的任务") */ + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + g_chip.write_calls = 0; + g_chip.read_calls = 0; + g_fxchip.set_des_ip_calls = 0; + g_fxchip.glob_calls = 0; + g_ns_poll_calls = 0; + g_ns_error_count = 0; + + CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == -1 && + net_get_errno() == NET_ERR_NOT_IN_NETTASK, + "TC-NETSOCK-560b non-executing task gets NET_ERR_NOT_IN_NETTASK (exact code, not -1)"); + CHECK(g_chip.write_calls == 0 && g_fxchip.set_des_ip_calls == 0 && + g_ns_poll_calls == 0, + "TC-NETSOCK-560c clean refusal: zero writes, zero set_des_ip, zero net_poll rounds"); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_NOT_IN_NETTASK, + "TC-NETSOCK-560d net_recvfrom guarded the same way"); + CHECK(g_chip.read_calls == 0 && g_fxchip.glob_calls == 0 && g_ns_poll_calls == 0, + "TC-NETSOCK-560e recvfrom touched no chip and no global interrupt register either"); + CHECK(g_ns_error_count == 2, + "TC-NETSOCK-560f each refusal logs one ERR line (silence hides the wrong task)"); + + /* ③ 正常错误路径优先:守卫排在参数校验之后 */ + CHECK(net_sendto(-1, "abc", 3, 0, NULL, 0) == -1 && + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-560g out-of-range fd still reports BADF first"); + CHECK(net_sendto(fd, NULL, 3, 0, NULL, 0) == -1 && + net_get_errno() == NET_ERR_INVAL, + "TC-NETSOCK-560h NULL buf still reports INVAL, the guard does not swallow it"); + CHECK(net_recvfrom(fd, NULL, 3, NET_MSG_DONTWAIT, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-560i recvfrom keeps the same precedence"); + + /* ④ 走消息队列的接口不受守卫影响:同一个非 netTask 上下文照常能把活干完 */ + CHECK(net_close(fd) == 0, + "TC-NETSOCK-560j net_close from a non-executing task still works (queue path untouched)"); + /* 执行完的证据用"收到回执 + 槽位释放",不看 close_calls:芯片此刻已是 + * CLOSED,走的是快路径(不再补发 CLOSE,见 TC-NETSOCK-320a/b),拿它当证据 + * 会把一条正确的快路径判成失败 */ + CHECK(s_net_socks[fd].in_use == 0U && g_ns_notify_calls >= 1, + "TC-NETSOCK-560k queue path really completed (receipt seen and slot released)"); + reset_all(); + + /* ⑤ 未捕获执行任务(net_init 没跑):守卫不抢先,仍由参数/状态检查给确定错误 */ + reset_all(); + g_ns_pump = net_process_messages; + CHECK(net_sendto(0, "abc", 3, 0, NULL, 0) == -1 && + net_get_errno() == NET_ERR_BADF, + "TC-NETSOCK-560l before net_init sendto reports BADF, no invented context error"); + + /* 未捕获执行任务、但 socket 确实在用时,守卫**选择放行**:宁可不做判定,也不给 + * 一个无从判定的场景编造错误码。放行后走的是完全正常的执行路径(这里 DONTWAIT + * 无数据 → WOULDBLOCK),证明守卫不是"没初始化就一律拒绝"的粗暴实现。 + * 刻意用 recvfrom 而不是 sendto:未初始化时 net_poll 直接短路返回、时间基准 + * 不前进,sendto 的等待环永远等不到超时(真机上不可达:net_socket 之后还有 + * NET_READY 屏障,但这条差异值得记一笔,见交付报告)。 */ + reset_all(); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; /* net_init 没跑:判据恒 NULL */ + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + g_fxchip.glob_calls = 0; + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_WOULDBLOCK, + "TC-NETSOCK-560m guard stays open when the executing task was never captured"); + CHECK(net_get_errno() != NET_ERR_NOT_IN_NETTASK && g_fxchip.glob_calls == 0, + "TC-NETSOCK-560n no context error invented and no global register read"); + reset_all(); +} + static void ns_run_s6(void) { tc_500_public_bounds(); @@ -392,4 +570,7 @@ static void ns_run_s6(void) tc_537_event_callback_path(); tc_540_negative_results(); tc_545_local_entries_without_queue(); + tc_550_family_roundtrip(); + tc_555_udp_src_addr(); + tc_560_udp_context_guard(); } diff --git a/test/host/soe_store/soe_store_test.c b/test/host/soe_store/soe_store_test.c index 155d20c8..780c5714 100644 --- a/test/host/soe_store/soe_store_test.c +++ b/test/host/soe_store/soe_store_test.c @@ -56,8 +56,11 @@ * TC-SOESTORE-020 丢弃留痕按差值上报一次,不刷屏 * TC-SOESTORE-021 队列创建失败(返回 NULL)→ 条目滞留环、下轮再试,不崩 * —— 陷阱二:名字/参数类错误码不得升级成卷操作 —— - * TC-SOESTORE-022 f_open 返 FR_INVALID_NAME → 只留痕返回 -1:**不 unlink**、 - * 不碰 /log 目录、条目全部保留(自愈只针对自己的文件) + * TC-SOESTORE-022 f_open 失败(FR_INVALID_NAME)-> 只留痕返回 -1:**不 unlink**、 + * 不碰 /log 目录、已有 3 条历史一条不少、条目留在待发环 + * TC-SOESTORE-022b 逐码对齐(8 个码含介质类):打开失败**任何码都不 unlink**—— + * SOE 侧比 run.log 侧更保守,是刻意差异(事件序列不可销毁), + * 与 test/host/sys_log_mcu/ 的 TC-SYSLOG-101/102 同一张表 * TC-SOESTORE-023 写路径反复失败时 f_unlink 次数有界(每轮 <=1),目录与邻居文件不动 * TC-SOESTORE-024 假卷严格 8.3 校验的保真自检:".fschk" -> 名字/盘符类拒绝码, * 而生产路径 SOE_PATH 必过(钉住"改名即静默写失败"的约束) @@ -66,12 +69,13 @@ * 函数 4/4。唯一未覆盖行 soe_store.c:202 = drain 里 for (;;) 的收尾大括号, * 该循环只能靠 break 退出,不存在"走到行尾再回跳"的机器码,属工具口径缺口。 * - * 本套照出的现状(只登记、不改生产代码;详见交 Lead 的报告): - * soe_store.c 的函数头第 ④ 条写"打开/写入结构性错误 → unlink 重试一次(同 run.log)", - * 但实现里 **f_open 失败直接 return -1、不 unlink 重试**(只有 f_write 失败才重试)。 - * run.log 侧(sys_log_mcu.c s_append_rec)则对 FR_NO_FILE/FR_NO_PATH/FR_INVALID_NAME - * 等一整套码做 unlink+重开。两套并不一致 —— 是注释夸大了实现,还是实现漏了一半, - * 需生产侧裁定;本套只用 TC-SOESTORE-022 把现状钉成"改了会响"。 + * 本轮(第 1 轮缺陷修复批次 task-4)已修,本套相应断言已从"钉现状"翻成正向: + * D 项:soe_store.c 函数头 ④ 旧写"打开/写入结构性错误都 unlink 重试一次(同 + * run.log)",与实现不符且方向错误——实现里 f_open 失败只 return -1、不 unlink。 + * 定案是**实现不动、改注释**:SOE 是事件序列,打开失败就删文件等于销毁已发生的 + * 事件;run.log 是滚动运行日志,坏了重建损失有限。现在 TC-SOESTORE-022 正向钉 + * "打开失败不得 unlink、历史一条不少",TC-SOESTORE-022b 对 8 个码逐个钉同一件事 + * (含介质类码——SOE 侧比 run.log 侧更保守,两处注释同源,改动必须同步)。 * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 @@ -633,17 +637,47 @@ static void tc_name_error_not_volume(void) printf("-- TC-SOESTORE-022..024 名字/参数类错误码严禁升级成卷操作 --\n"); - /* 022:FR_INVALID_NAME 只让本条失败,**不得** unlink 别人、不得动目录 */ + /* 022【D 项已修 2026-10-24:正向断言】f_open 失败(名字/参数类码) + * -> 只留痕返回 -1:**绝不 unlink**、不动 /log 目录、已有记录一条不少。 + * 生产侧的定案是"打开失败不重试也不删",理由写在 s_soe_append 函数头: + * SOE 是事件序列,删文件 = 销毁已发生的事件。 */ reset_all(); - (void)ff_fake_mkfile("/log/run.log", NULL, 0u); /* 邻居文件(同卷) */ + (void)ff_fake_mkfile(SOE_PATH, NULL, 3u * SOE_REC_SIZE); /* 已有 3 条历史 */ + (void)ff_fake_mkfile("/log/run.log", NULL, 0u); /* 邻居文件(同卷) */ ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 2); soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); - soe_store_drain(); /* open 失败 -> -1 */ + soe_store_drain(); /* open 失败 */ ok = (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0) && (ff_fake_exists("/log") == 2) + && (ff_fake_size(SOE_PATH) == 3u * SOE_REC_SIZE) /* 历史原封不动 */ && (ff_fake_exists("/log/run.log") == 1) && (s_ring_tail == 0u) && (g_soe_err_count == 1); - CHECK(ok, "TC-SOESTORE-022 FR_INVALID_NAME -> 只留痕失败;不 unlink、目录与邻居文件不动"); + CHECK(ok, "TC-SOESTORE-022 open failure keeps history: no unlink, entry stays queued"); + + /* 022b 逐码对齐(与 test/host/sys_log_mcu 的 TC-SYSLOG-101/102 同一张表): + * SOE 侧对**所有**打开失败码都不 unlink,是 run.log 侧触发集的保守子集 + * ——介质类码在 run.log 侧会 unlink 重建,在 SOE 侧不会,这是刻意的差异 + * (事件序列不可销毁),两处注释同源、改动必须同步。 */ + { + static const FRESULT codes[] = { FR_INVALID_NAME, FR_NO_PATH, FR_NO_FILE, + FR_INVALID_OBJECT, FR_DISK_ERR, FR_INT_ERR, + FR_NO_FILESYSTEM, FR_DENIED }; + uint32_t c; + int all_ok = 1; + for (c = 0u; c < (uint32_t)(sizeof(codes) / sizeof(codes[0])); c++) { + reset_all(); + (void)ff_fake_mkfile(SOE_PATH, NULL, SOE_REC_SIZE); + ff_fake_inject_rc(SOE_PATH, FF_FAKE_OP_OPEN, codes[c], 1); + soe_store_post((uint8_t)SOE_T_ELM_RETURN, (uint8_t)c, c); + soe_store_drain(); + if ((g_ff_obs.calls[FF_FAKE_OP_UNLINK] != 0) + || (ff_fake_size(SOE_PATH) != SOE_REC_SIZE) + || (s_ring_tail != 0u)) { + all_ok = 0; + } + } + CHECK(all_ok, "TC-SOESTORE-022b no open-failure code ever unlinks soe.log (8 codes)"); + } /* 023:反复失败的自愈上限:每轮最多 unlink 一次,条目与卷都不被吞 */ reset_all(); diff --git a/test/host/sys_log_mcu/sys_log_mcu_test.c b/test/host/sys_log_mcu/sys_log_mcu_test.c index 2007825e..efea41e1 100644 --- a/test/host/sys_log_mcu/sys_log_mcu_test.c +++ b/test/host/sys_log_mcu/sys_log_mcu_test.c @@ -60,7 +60,11 @@ * TC-SYSLOG-032 preboot 8 条同文件 -> 一批 512B 落盘 + preboot 计数清零 * TC-SYSLOG-033 两个文件混排 -> 按文件分批,顺序不乱、条目不丢 * TC-SYSLOG-034 换文件那条不消耗(preboot 下标回退 / 队列回投) - * TC-SYSLOG-035 卷忙 -> 整批回投队列,下一轮全部落盘(旧实现 -2 直接丢一条) + * TC-SYSLOG-035 卷忙 -> 整批回投队列,一条不丢(A 项修复后的正向断言) + * TC-SYSLOG-035a 回投后再排空:顺序与投递顺序一致,全程静默 + * TC-SYSLOG-035b 慢路径遇卷忙 -> 尾部整批回投(参照物,A 项就是向它对齐) + * TC-SYSLOG-035c 满批 8 条在卷忙时全部保住 + * TC-SYSLOG-035d preboot 来源的条目卷忙时转投队列(不丢也不重复落盘) * TC-SYSLOG-036 队列满回投失败 -> "hold-over drop" 留痕,不崩 * TC-SYSLOG-037 -3 -> 逐条慢路径,容量轮转只发生一次 * TC-SYSLOG-038 慢路径中途写失败 -> 该条丢弃留痕 + 尾部回投 @@ -76,7 +80,8 @@ * TC-SYSLOG-054 越界 idx -> -1;空文件 -> -1 * TC-SYSLOG-055 读时卷忙 -> -1(读窗口 try 语义:UI 每 100ms 喂心跳,绝不阻塞) * TC-SYSLOG-056 短读 -> 作废窗口(s_rd_win_cnt=0),不把半窗当有效命中 - * TC-SYSLOG-057 get_record:缓冲容量守卫 + 描述按 desc_len 截断 + * TC-SYSLOG-057 get_record 容量守卫:ts_len<=19 或 desc_len<=28 一律 -1 + * TC-SYSLOG-057b 被拒的调用一个字节都不写调用方缓冲(C 项"宁可拒绝不截断") * TC-SYSLOG-058 last_epoch:无记录 -1;有记录解析 10 位 ASCII * —— E mounted / 上电重启分类 —— * TC-SYSLOG-060 /log 不存在 -> f_mkdir 成功 + 幂等(第二次不再建目录) @@ -99,9 +104,25 @@ * TC-SYSLOG-095 wdev 短写 -> 返回 -1,第二条独立证据仍落 run.log * TC-SYSLOG-096 队列创建失败 -> 一条都不排(不静默丢),堆恢复后排空并接上 SOE * TC-SYSLOG-097 队列侧换文件那条原样回投 -> 三条全落、各文件内顺序不乱 - * TC-SYSLOG-098 desc_len=29 时截断分支不可达(守卫取值导致,登记死分支) + * TC-SYSLOG-098 读侧不再截断(C 项已删死分支):拿到整 28 字符 + 唯一 NUL * TC-SYSLOG-099 p_rec / p_desc 空指针守卫(守卫在取锁之前) * TC-SYSLOG-100 last_epoch 读失败 -> -1 且 *p_epoch 清零 + * —— I D 项:自愈触发集逐码(与 soe_store 侧同一张表)—— + * TC-SYSLOG-101/101b 介质/结构类码(FR_DISK_ERR/INT_ERR/NO_FILESYSTEM/DENIED) + * -> 单条与批量路径各 unlink 恰好一次 + 重开一次并落全(自愈成功 + * 不留痕;文件被删是设计代价,所以 SOE 侧不允许这么做) + * TC-SYSLOG-102 名字/参数类码(INVALID_NAME/NO_PATH/NO_FILE/INVALID_OBJECT/ + * NOT_READY/NOT_ENABLED/TIMEOUT)连续 20 轮 -> 零 unlink、零重开、 + * 邻居文件与 /log 目录不动、读侧代数照常自增 + * TC-SYSLOG-103/103b s_is_selfheal_code() 分类表本身逐码钉死 + * TC-SYSLOG-104 卷忙不计故障:恢复后第一次真失败仍立刻留痕(反证 -2 没置故障位) + * —— J F 项:投递侧丢弃留痕节流(队列通道)—— + * TC-SYSLOG-105 队列满后连投 12 条 -> 只报第一次(A 项回投让队列常驻满, + * 不节流就按投递频率刷串口) + * TC-SYSLOG-106 同一故障窗口内再投 5 条 -> 完全静默(丢弃语义不变,照丢) + * TC-SYSLOG-107 复位点必须在 syslog_post 的成功分支:drain 只落盘、不复位 + * TC-SYSLOG-107b 恢复后再灌满 -> 第二次故障重新留痕 + * TC-SYSLOG-107c 队列通道与启动前数组通道的抑制位互不干扰 * —— G 陷阱二:名字/参数类错误码严禁升级成卷操作 —— * TC-SYSLOG-080 短名违规分档保真:体>8 / 扩展>3 / 非法字符 / 以点开头(.fschk) * 各自落 FR_INVALID_NAME;含 ':' 落 FR_INVALID_DRIVE @@ -114,29 +135,41 @@ * (sys_log_evidence 里 snprintf 返回 <=0 的分支——宿主 snprintf 不可能失败, * 假件也不伪造返回值;为刷数字去伪造等于把断言写在假件规则上,宁可留空)。 * - * 本套照出的现状(**只钉现状、不改生产代码**;三条都进交 Lead 的报告): + * 本套照出的四条现状 —— 已在 task-4(2026-10-24)全部处置,相应断言已翻成正向: * - * 【疑似缺陷 A:卷忙时整批日志被静默丢弃】sys_log_mcu.c:509~537 的批量路径里 - * done 先被置成 n,只有 rc==-3 与 rc==-1 两个分支会改它;**rc==-2(卷忙)没有 - * 任何分支**,于是紧随其后的 "for (i = done; i < n; i++) osMessageQueuePut(...)" - * 一次都不执行 —— 整批(最多 8 条)既没落盘也没回投,且不计故障、不打日志。 - * 而同一段代码自己的注释写的是"rc == -2(卷忙):整批回投队列,静默顺延", - * 函数头 465~470 行与设计文档 §4.4 又都写明"**条目不丢**(旧实现 -2 卷忙会 - * 直接丢掉那一条)"。⇒ 现状比旧实现更糟:旧实现丢 1 条,现在一次丢 8 条。 - * 触发条件极常见:卷锁 v1.2 之后 ftpTask 传文件期间 defaultTask 每 2s 排空 - * 一次,每次撞上卷忙就吞一批。对照:逐条慢路径(-3 分支)遇 -2 会正确回投 - * 尾部 —— 同一个错误码在两条路径上行为相反(TC-SYSLOG-035 与 035b 成对钉住)。 + * A【已修】卷忙整批静默丢日志:syslog_drain 批量路径 done = n 后缺 rc==-2 分支 + * → 回投循环空转,最多丢 8 条。修法 = 补 else if (rc == -2) { done = 0u; }, + * 与 -3 慢路径遇 -2 的处置对齐(慢路径是参照物,没动它)。三条语义纪律 + * ①不计故障 ②不打日志 ③回投保序 分别由 TC-SYSLOG-035 / 035a / 035d 钉住。 + * B【已修】rc==-1 的留痕文案 "drop %lu entries" 与行为相反(done=0 → 其实整批 + * 回投、一条没丢)。改成 "write fail, requeue N entries",并按"正常→故障" + * 节流(新增文件级 s_requeue_reported[],成功一轮复位)——旧版持续故障时每 + * 2s 刷一条。TC-SYSLOG-039 / 039b 钉住"节流一次"与"恢复后再失败要再报"。 + * C【已修】syslog_get_record 的截断支永不可达:守卫已挡掉 desc_len <= + * SYSLOG_DESC_MAX,而 dl 恒 <= 28。删掉死分支,容量要求写进函数头与 + * sys_log.h(**宁可整条拒绝也不静默截断**:UI 拿到截断串会显示成一条被删短 + * 过的日志,比读不到更难查)。TC-SYSLOG-057 / 057b / 098 钉住。 + * D【已修】自愈触发集违反 §10.1 第 10 条:FR_INVALID_NAME / FR_NO_PATH / + * FR_NO_FILE / FR_INVALID_OBJECT 都曾在 unlink 重建集合里,后果是名字/参数类 + * 错误下**每轮删一次自己的历史日志且永远修不好**。现在按语义拆类,新增 + * s_is_selfheal_code():触发集只留 FR_DISK_ERR / FR_INT_ERR / FR_NO_FILESYSTEM / + * FR_DENIED,逐码依据(含 ff.c 行号)写在该函数头。TC-SYSLOG-101~103 逐码钉, + * 与 test/host/soe_store/ 的 TC-SOESTORE-022b 是同一张表的两侧。 * - * 【疑似缺陷 B:rc==-1 的留痕文案与实际行为相反】sys_log_mcu.c:527~529 打 - * DBG_ERROR("drop %lu entries id=%d"),但该分支把 done 置 0 -> 整批**回投队列**, - * 一条都没丢(下一轮重试)。排障时按"日志说丢了"去找会走错方向;而真正会丢的 - * 是上面那个没有任何日志的 -2 分支。另外写故障持续时每轮 drain 都会重投重 - * 试一遍(s_append_many 内部的 L2 去重只压得住它自己那条,压不住这条)。 + * F【已修】投递侧丢弃留痕无节流:syslog_post 的两个丢弃分支(队列满 / 启动前数组 + * 满)原来每条被丢的投递各打一行 DBG_ERROR。A 项修复让卷忙时整批回投 -> 队列 + * 可长时间常驻满态 -> 此后每条业务投递都打一行,日志自己变成串口负载(比原 + * 缺陷更常触发)。修法 = 与 B 同一套"正常→故障"节流纪律,但**另立两个抑制位** + * (s_post_qfull_reported / s_post_pboot_full_reported,volatile 单字节,任意 + * 上下文读写)——不复用 s_requeue_reported:那是 defaultTask 的 drain 侧"整批 + * 回投",本处是任意任务的"这一条被丢了",恢复时机也不同(下一次投递成功)。 + * **丢弃语义一个字没改**(满则丢仍照旧),变的只有留痕次数。 + * TC-SYSLOG-041b/041d/041e/041f(数组通道)+ 105/106/107/107b/107c(队列通道)。 * - * 【死分支:syslog_get_record 的截断不可达】sys_log_mcu.c:616 的守卫是 - * desc_len <= SYSLOG_DESC_MAX 直接 -1,而函数里 dl 恒 <= SYSLOG_DESC_MAX, - * 于是 627~628 的 "if (dl >= desc_len) dl = desc_len - 1" 永不成立。 - * 无害(宁可拒绝也不越界),但"传小缓冲就能拿到截断内容"的直觉是错的(TC-SYSLOG-098)。 + * 顺手记一条容易误读的事实(不是缺陷):自愈成功 = **删掉整个日志文件重建**, + * 所以一次打开失败会连带丢掉既有历史(TC-SYSLOG-101 明确断言文件只剩本次一条)。 + * run.log 是滚动运行日志,这个代价可接受;soe.log 是事件序列,同样的动作被禁止 + * (TC-SOESTORE-022 / 022b),两侧的差异是刻意的,注释也互指。 * * 故意没测(写在这里,不留"看起来全覆盖"的错觉): * - 真跨任务并发:读窗口用"对齐 32 位读写在 ARM 上原子"免锁,主机 x64 + 单线程 @@ -459,6 +492,11 @@ static void reset_all(void) s_queue = NULL; s_fs_ready = 0u; s_mount_fail_reported = 0u; + /* B / F 项整改新增的留痕节流位:跨场景必须显式清(否则第二个故障场景看不到留痕) */ + s_requeue_reported[SYSLOG_RUN] = 0u; + s_requeue_reported[SYSLOG_SELFTEST] = 0u; + s_post_qfull_reported = 0u; + s_post_pboot_full_reported = 0u; (void)memset(s_rd_win, 0, sizeof(s_rd_win)); s_rd_win_id = -1; s_rd_win_base = 0u; @@ -828,16 +866,10 @@ static void tc_drain(void) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 3), "TC-SYSLOG-033/034 换文件即分批(run 被切成两批 = 共 3 轮 I/O)+ 顺序不乱"); - /* 035【现状桩 = 疑似生产缺陷,见文件头"本套照出的现状"】 - * sys_log_mcu.c:509~537 的批量路径里 done 先被置成 n,只有 -3 / -1 分支会 - * 把它改回 0;**rc == -2(卷忙)没有任何分支**,于是紧随其后的 - * "for (i = done; i < n; i++) osMessageQueuePut(...)" 一次都不执行 —— - * 整批(最多 8 条)**静默丢失**,而同一段代码上方的注释写的是 - * "rc == -2(卷忙):整批回投队列,静默顺延",设计文档 §4.4 也写明 - * "队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时回投队列—— - * 条目不丢(旧实现 -2 卷忙会直接丢掉那一条)"。 - * ⇒ 本条按**当前实现**断言(丢了就是丢了),并显式命名为 DEFECT, - * 生产侧一旦按注释补上 done=0,这条会红,提醒同步翻转成"不丢"的正向断言。 */ + /* 035【A 项已修 2026-10-24:正向断言】卷忙 -> **整批回投队列**,一条不丢。 + * 生产侧修法:syslog_drain 批量路径补 else if (rc == -2) { done = 0u; }, + * 与 -3 慢路径遇 -2 的处置对齐。三条语义纪律各有一条断言: + * ① 不计故障(无 DBG_ERROR) ② 不打日志(同一判据) ③ 回投保持原顺序 */ reset_all(); ready_only(); s_kernel_state = osKernelRunning; @@ -846,10 +878,22 @@ static void tc_drain(void) syslog_post(SYSLOG_RUN, "hold"); } lock_busy_next(1); - syslog_drain(); /* 批量 -2 */ - CHECK((syslog_count(SYSLOG_RUN) == 0u) && (s_q_cnt == 0u) - && (g_sys_err_count == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), - "TC-SYSLOG-035[DEFECT 现状] 卷忙 -> 整批 3 条既没落盘也没回投(静默丢失)"); + syslog_drain(); /* 批量 -2 -> 整批回投 */ + ok = (syslog_count(SYSLOG_RUN) == 0u) && (s_q_cnt == 3u) + && (g_sys_err_count == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0); + CHECK(ok, "TC-SYSLOG-035 volume busy -> whole batch requeued, none lost"); + /* ③ 顺序:回投后再排空,落盘顺序必须与投递顺序一致 */ + syslog_drain(); + ok = ok && (syslog_count(SYSLOG_RUN) == 3u) && (s_q_cnt == 0u); + if (ok) { + for (i = 0u; i < 3u; i++) { + if (!raw_at(P_RUN, i, rec) || (memcmp(rec + SYSLOG_DESC_OFFS, "hold", 4u) != 0)) { + ok = 0; + } + } + } + CHECK(ok && (g_sys_err_count == 0), + "TC-SYSLOG-035a requeued batch keeps original order and stays silent"); /* 035b 对照组:慢路径(-3 分支内)遇到 -2 时**会**把尾部回投 —— 同一卷忙 * 在两条路径上行为不一致,这就是"批量路径漏了一个分支"最直接的证据 */ @@ -871,7 +915,7 @@ static void tc_drain(void) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), "TC-SYSLOG-035b 慢路径遇卷忙 -> 3 条**整批回投**(同一码在两条路径上行为相反)"); - /* 035c 卷忙丢掉的是"整批"而不是"一条":8 条一批全丢 */ + /* 035c 一整批(8 条)在卷忙时全部回投,不是只保住一条 */ reset_all(); ready_only(); s_kernel_state = osKernelRunning; @@ -881,8 +925,24 @@ static void tc_drain(void) } lock_busy_next(1); syslog_drain(); - CHECK((s_q_cnt == 0u) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0), - "TC-SYSLOG-035c 卷忙一次吞掉一整批(8 条),不是旧实现的 1 条"); + CHECK((s_q_cnt == SYSLOG_QUEUE_MAX) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0) + && (g_sys_err_count == 0), + "TC-SYSLOG-035c all 8 entries of a full batch survive a busy volume"); + + /* 035d preboot 来源的条目同样不丢:卷忙时它们从数组转投队列(数组侧删掉、 + * 队列接手),既不是留在数组里被重复落盘、也不是被 memmove 吞掉 */ + reset_all(); + ready_only(); + for (i = 0u; i < 4u; i++) { + syslog_post(SYSLOG_RUN, "pb"); /* 内核未运行 -> preboot */ + } + s_kernel_state = osKernelRunning; + lock_busy_next(1); + syslog_drain(); + ok = (s_preboot_cnt == 0u) && (s_q_cnt == 4u) && (syslog_count(SYSLOG_RUN) == 0u); + syslog_drain(); + CHECK(ok && (syslog_count(SYSLOG_RUN) == 4u) && (s_q_cnt == 0u), + "TC-SYSLOG-035d preboot entries move to queue on busy volume (no loss, no dup)"); /* 036 回投溢出(hold-over drop):批量里混了 preboot 与队列两个来源时, * 回投的目标队列已经被"没被取走的那些"占着 -> 装不下的部分才真被丢掉。 @@ -941,10 +1001,10 @@ static void tc_drain(void) CHECK(ok && (s_q_cnt == 0u) && (syslog_count(SYSLOG_RUN) == 2u), "TC-SYSLOG-038 慢路径写失败:只丢**当前那一条**,尾部 2 条下轮补齐"); - /* 039【现状桩 = 第二个不一致】rc == -1 分支把 done 置 0 并打 - * DBG_ERROR("drop %lu entries id=%d"),但 done=0 意味着紧随其后的循环把 - * **整批全部回投队列** —— 日志说"丢了多少条",实际一条没丢(下轮还会重试)。 - * 措辞与行为相反,排障时会把人往错方向带;本条按现状钉住。 */ + /* 039【B 项已修 2026-10-24:正向断言】rc == -1:整批回投 + 留痕文案如实 + * (旧版这里打 "drop %lu entries" 但一条没丢,文案与行为相反)。 + * 一轮失败 = s_append_many 自己那条写故障留痕 + drain 这条回投留痕 = 2; + * 第二轮仍失败时两条都必须被各自的去重位压住(旧版 drain 那条每 2s 刷一次)。 */ reset_all(); ready_only(); s_kernel_state = osKernelRunning; @@ -952,14 +1012,23 @@ static void tc_drain(void) for (i = 0u; i < 3u; i++) { syslog_post(SYSLOG_RUN, "wfail"); } - ff_fake_inject_write_short(P_RUN, 0u, 2); + ff_fake_inject_write_short(P_RUN, 0u, 4); /* 撑住两轮失败(每轮 2 次写) */ g_sys_err_count = 0; syslog_drain(); - CHECK((s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == 0u) && (g_sys_err_count == 2), - "TC-SYSLOG-039[DEFECT 现状] 批量 -1:日志报 drop,条目却整批回投(3 条仍在队列)"); - syslog_drain(); /* 注入耗尽 -> 这次真的落下去 */ - CHECK((syslog_count(SYSLOG_RUN) == 3u) && (s_q_cnt == 0u), - "TC-SYSLOG-039b 回投的 3 条下一轮补齐(与 035 的卷忙整批丢失形成对照)"); + ok = (s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == 0u) && (g_sys_err_count == 2); + syslog_drain(); /* 第二轮:仍失败,必须静默 */ + ok = ok && (s_q_cnt == 3u) && (g_sys_err_count == 2) && (s_requeue_reported[SYSLOG_RUN] == 1u); + CHECK(ok, "TC-SYSLOG-039 write failure requeues whole batch; log throttled to first round"); + ff_fake_inject_write_short(P_RUN, 0u, 0); /* no-op:清不掉已耗尽的注入 */ + syslog_drain(); /* 注入耗尽 -> 成功 -> 复位 */ + ok = ok && (syslog_count(SYSLOG_RUN) == 3u) && (s_q_cnt == 0u) + && (s_requeue_reported[SYSLOG_RUN] == 0u); + ff_fake_inject_write_short(P_RUN, 0u, 2); + syslog_post(SYSLOG_RUN, "again"); + g_sys_err_count = 0; + syslog_drain(); /* 恢复后再失败 -> 必须重新留痕 */ + CHECK(ok && (g_sys_err_count == 2), + "TC-SYSLOG-039b recovery resets the throttle; a new fault is logged again"); /* 040 B9 挂接:每次 drain 尾部各调一次 soe_store_drain */ reset_all(); @@ -983,8 +1052,31 @@ static void tc_drain(void) for (i = 0u; i < SYSLOG_QUEUE_MAX + 2u; i++) { syslog_post(SYSLOG_RUN, "pf"); } - CHECK((s_preboot_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 2), - "TC-SYSLOG-041b preboot 满(8 槽)-> 丢弃 + 每条一次留痕(日志不得反压业务)"); + /* F 项已修(2026-10-24):旧实现每条被丢的投递都打一行,数组满 + 挂载慢的窗口 + * 里按开机事件密度刷屏。现在只报转入故障那一次;**丢弃语义不变**(照样丢 2 条)。 */ + CHECK((s_preboot_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1), + "TC-SYSLOG-041b preboot full: 2 entries dropped but logged only once"); + /* 节流复位靠"数组重新收得下第一条":先排空、再投一条成功的,抑制位才归零。 + * 注意排空本身不复位它(复位点在 syslog_post 的成功分支里)。 */ + syslog_drain(); + ok = (s_preboot_cnt == 0u) && (s_post_pboot_full_reported == 1u); + s_kernel_state = osKernelInactive; /* 让 post 走数组而不是队列 */ + syslog_post(SYSLOG_RUN, "recovered"); /* 成功收下第一条 -> 复位 */ + ok = ok && (s_preboot_cnt == 1u) && (s_post_pboot_full_reported == 0u); + CHECK(ok, "TC-SYSLOG-041d recovery (one accepted entry) clears the throttle bit"); + /* 重新灌满:8 槽 + 2 条溢出 -> 只报一次(证明抑制位真的重新武装了) */ + g_sys_err_count = 0; + for (i = 0u; i < SYSLOG_QUEUE_MAX + 1u; i++) { + syslog_post(SYSLOG_RUN, "pf2"); + } + CHECK((s_preboot_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1), + "TC-SYSLOG-041e second fault is logged again, then stays silent"); + g_sys_err_count = 0; + for (i = 0u; i < 3u; i++) { + syslog_post(SYSLOG_RUN, "pf3"); /* 故障期间继续静默 */ + } + CHECK((g_sys_err_count == 0) && (s_post_pboot_full_reported == 1u), + "TC-SYSLOG-041f drops inside the same fault window log nothing"); reset_all(); ready_only(); s_kernel_state = osKernelRunning; @@ -1107,6 +1199,9 @@ static void tc_read(void) reset_all(); ready_only(); (void)s_append(SYSLOG_RUN, "01234567890123456789012345678901234567"); + /* 拒绝路径必须一个字节都不写调用方缓冲:先填满 0x7A 当哨兵 */ + (void)memset(desc, 0x7A, sizeof(desc)); + (void)memset(ts, 0x7A, sizeof(ts)); ok = (syslog_get_record(SYSLOG_RUN, 0u, NULL, 32u, desc, sizeof(desc)) == -1) && (syslog_get_record(SYSLOG_RUN, 0u, ts, SYSLOG_TS_LEN, desc, sizeof(desc)) == -1) && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), NULL, sizeof(desc)) == -1) @@ -1119,6 +1214,27 @@ static void tc_read(void) * 从来没有截断"。登记为死分支,见文件头"本套照出的现状"。 */ && (strlen(desc) == (size_t)SYSLOG_DESC_MAX); CHECK(ok, "TC-SYSLOG-057 守卫:ts_len<=19 或 desc_len<=28 一律 -1;29 才读得到整 28 字符"); + /* C 项整改(2026-10-24):容量不足一律拒绝,且**不得**碰调用方缓冲—— + * 上面每次被拒的调用都用了同一对缓冲,哨兵还在就说明没写进去 */ + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "sentinel-check"); + (void)memset(desc, 0x7A, sizeof(desc)); + (void)memset(ts, 0x7A, sizeof(ts)); + ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, SYSLOG_TS_LEN, desc, sizeof(desc)) == -1) + && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, SYSLOG_DESC_MAX) == -1); + { + uint32_t k; + int untouched = 1; + for (k = 0u; k < sizeof(desc); k++) { + if (desc[k] != (char)0x7A) { untouched = 0; } + } + for (k = 0u; k < sizeof(ts); k++) { + if (ts[k] != (char)0x7A) { untouched = 0; } + } + CHECK(ok && (untouched != 0), + "TC-SYSLOG-057b rejected call writes nothing into caller buffers"); + } /* 058 last_epoch */ reset_all(); @@ -1343,6 +1459,8 @@ static void tc_name_rules(void) ready_only(); (void)ff_fake_mkfile(P_SELFTEST, NULL, SYSLOG_REC_SIZE); /* 邻居文件 */ (void)ff_fake_mkfile("/config.ini", NULL, 12u); /* 同卷别的东西 */ + /* D 项整改后:名字/参数类码**一次都不许 unlink**(旧实现每轮删一次自己的 + * 历史日志、且永远修不好)。留痕仍受 L2 去重,只 1 条。 */ ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_INVALID_NAME, 200); for (i = 0u; i < 20u; i++) { (void)s_append(SYSLOG_RUN, "bad-name"); @@ -1350,9 +1468,10 @@ static void tc_name_rules(void) ok = (ff_fake_exists("/log") == 2) && (ff_fake_size(P_SELFTEST) == SYSLOG_REC_SIZE) && (ff_fake_size("/config.ini") == 12u) - && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] <= (int)(2u * 20u)) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 20) /* 也没有"重开一次" */ && (g_sys_err_count == 1); - CHECK(ok, "TC-SYSLOG-082 反复拿 FR_INVALID_NAME 也只 unlink 自己的文件:目录/邻居不动、留痕一次"); + CHECK(ok, "TC-SYSLOG-082 20 rounds of FR_INVALID_NAME: zero unlink, zero reopen, one log"); } /* ===================== H 补齐分支(自愈 / 二次校验 / 回投 / 占位时标) ===================== */ @@ -1476,17 +1595,19 @@ static void tc_extra_branches(void) } CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3), "TC-SYSLOG-097 队列侧换文件回投:三条全落、各文件内顺序不乱(回投不是丢弃)"); - /* 098 dl >= desc_len 的截断分支:守卫已挡掉 desc_len <= SYSLOG_DESC_MAX, - * 而 dl 由记录内容决定、恒 <= SYSLOG_DESC_MAX -> 该分支在**当前守卫取值下 - * 永不可达**。这条断言把"不可达"这件事本身钉住:一旦有人把守卫放宽成 - * desc_len == 0,本条就会因为读到截断结果而红,提醒补截断用例。 */ + /* 098【C 项已修 2026-10-24】原来的 "if (dl >= desc_len) dl = desc_len - 1" + * 是永不可达的死分支(守卫已挡掉 desc_len <= SYSLOG_DESC_MAX,而 dl 恒 + * <= SYSLOG_DESC_MAX),已从生产码删除。本条改为正向钉住"读侧不再截断": + * desc_len = 29 时拿到的是整 28 字符,且 desc[28] 是唯一的 NUL。 + * 若有人把守卫放宽(例如改成 desc_len == 0),本条会因拿到 27 字符而红。 */ reset_all(); ready_only(); (void)s_append(SYSLOG_RUN, "abcdefghijklmnopqrstuvwxyz0123"); /* 34 字符 -> 截到 28 */ ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, SYSLOG_DESC_MAX + 1u) == 0) && (strlen(desc) == (size_t)SYSLOG_DESC_MAX) && (desc[SYSLOG_DESC_MAX] == '\0'); - CHECK(ok, "TC-SYSLOG-098 desc_len=29 时截断分支不可达(拿到整 28 字符,非 28-1)"); + CHECK(ok && (desc[SYSLOG_DESC_MAX - 1u] != '\0') && (desc[SYSLOG_DESC_MAX] == '\0'), + "TC-SYSLOG-098 no truncation on read side: full 28 chars with one NUL"); /* 099 空指针守卫:s_read_raw 的 p_rec 与 evidence 的 p_desc */ reset_all(); @@ -1512,6 +1633,172 @@ static void tc_extra_branches(void) (void)i; } +/* ===================== I D 项整改:自愈触发集逐码 ===================== */ + +/* 打开失败时"该不该 unlink 重建"逐码判定。断言的是**语义归属**,不是假件规则: + * 介质/结构类(能靠删文件修好)→ 必须恰好 unlink 一次并再开一次; + * 名字/参数类(删了也修不好)→ 必须零 unlink、零重开。 */ +static void tc_selfheal_codes(void) +{ + static const FRESULT heal_codes[] = { FR_DISK_ERR, FR_INT_ERR, + FR_NO_FILESYSTEM, FR_DENIED }; + /* FR_NO_FILE 也在"不进"这一列:本模块用 FA_OPEN_APPEND(含 FA_OPEN_ALWAYS), + * 文件不存在会被直接建出来;真返回 FR_NO_FILE 只可能是同名对象是目录 + *(ff.c:3854),unlink 一个目录修不好它,反而违反"自愈只针对自己的文件"。 */ + static const FRESULT noheal_codes[] = { FR_INVALID_NAME, FR_NO_PATH, FR_NO_FILE, + FR_INVALID_OBJECT, FR_NOT_READY, + FR_NOT_ENABLED, FR_TIMEOUT }; + static uint8_t one[SYSLOG_REC_SIZE]; + uint32_t c; + uint32_t round; + uint32_t gen_seed; + int ok; + + (void)memset(one, 0x11, sizeof(one)); + printf("-- TC-SYSLOG-101..104 self-heal trigger set, per code (fix D) --\n"); + + /* 101 介质/结构类:单条与批量路径都必须"unlink 恰好一次 + 重开一次", + * 并最终把这条/这批落下去,且不留痕(自愈成功不该报故障) */ + for (c = 0u; c < (uint32_t)(sizeof(heal_codes) / sizeof(heal_codes[0])); c++) { + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "seed"); /* 先让文件存在 */ + { + int opens_seed = g_ff_obs.calls[FF_FAKE_OP_OPEN]; + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, heal_codes[c], 1); + ok = (s_append(SYSLOG_RUN, "single") == 0) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_seed + 2) + /* 自愈的前提就是删文件:seed 那条随之消失,文件只剩本次这一条。 + * (SOE 侧绝不这么做,理由见 TC-SOESTORE-022:事件序列不能销毁) */ + && (ff_fake_size(P_RUN) == 1u * SYSLOG_REC_SIZE) + && (g_sys_err_count == 0) && (s_lock_seq_err == 0); + } + CHECK(ok, "TC-SYSLOG-101 single path heals one media/structure code"); + + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "seed"); + { + int opens_seed = g_ff_obs.calls[FF_FAKE_OP_OPEN]; + static uint8_t batch[2u * SYSLOG_REC_SIZE]; + (void)memset(batch, 0x22, sizeof(batch)); + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, heal_codes[c], 1); + ok = (s_append_many(SYSLOG_RUN, batch, 2u) == 0) + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_seed + 2) + && (ff_fake_size(P_RUN) == 2u * SYSLOG_REC_SIZE) /* 旧的 seed 被删掉 */ + && (g_sys_err_count == 0) && (s_lock_seq_err == 0); + } + CHECK(ok, "TC-SYSLOG-101b batch path heals the same code once"); + } + + /* 102 名字/参数类:连续 20 轮都必须零 unlink、零重开、只留痕一次 */ + for (c = 0u; c < (uint32_t)(sizeof(noheal_codes) / sizeof(noheal_codes[0])); c++) { + reset_all(); + ready_only(); + (void)s_append(SYSLOG_RUN, "seed"); + (void)ff_fake_mkfile(P_SELFTEST, NULL, SYSLOG_REC_SIZE); /* 邻居 */ + { + int opens_seed = g_ff_obs.calls[FF_FAKE_OP_OPEN]; + gen_seed = s_rd_gen[SYSLOG_RUN]; + ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, noheal_codes[c], 200); + for (round = 0u; round < 20u; round++) { + ok = 1; + if (s_append(SYSLOG_RUN, "no-heal") != -1) { ok = 0; } + /* 批量路径也必须走同一条判定:给它一整条 64B 记录的缓冲 */ + if (s_append_many(SYSLOG_RUN, one, 1u) != -1) { ok = 0; } + } + /* 代数每轮 +2(单条与批量各自都失效一次读窗口):宁多失效也不读脏窗 */ + CHECK(ok + && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0) + && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_seed + 40) + && (ff_fake_exists(P_SELFTEST) == 1) + && (ff_fake_exists("/log") == 2) + && (s_rd_gen[SYSLOG_RUN] == gen_seed + 40u) + && (s_lock_seq_err == 0), + "TC-SYSLOG-102 name/param codes never unlink or reopen (20 rounds)"); + } + } + + /* 103 s_is_selfheal_code() 直测:把分类表本身钉死(写死的期望表,不读生产逻辑) */ + ok = (s_is_selfheal_code(FR_DISK_ERR) == 1) && (s_is_selfheal_code(FR_INT_ERR) == 1) + && (s_is_selfheal_code(FR_NO_FILESYSTEM) == 1) && (s_is_selfheal_code(FR_DENIED) == 1); + CHECK(ok, "TC-SYSLOG-103 trigger set = {DISK_ERR, INT_ERR, NO_FILESYSTEM, DENIED}"); + ok = (s_is_selfheal_code(FR_INVALID_NAME) == 0) && (s_is_selfheal_code(FR_NO_PATH) == 0) + && (s_is_selfheal_code(FR_NO_FILE) == 0) && (s_is_selfheal_code(FR_INVALID_OBJECT) == 0) + && (s_is_selfheal_code(FR_NOT_READY) == 0) && (s_is_selfheal_code(FR_NOT_ENABLED) == 0) + && (s_is_selfheal_code(FR_TIMEOUT) == 0) && (s_is_selfheal_code(FR_OK) == 0) + && (s_is_selfheal_code(FR_EXIST) == 0) && (s_is_selfheal_code(FR_WRITE_PROTECTED) == 0); + CHECK(ok, "TC-SYSLOG-103b name/param/volume-gate codes all excluded"); + + /* 104 卷忙不计故障这条纪律(A 项要求①)在写路径同样成立:-2 不产生任何留痕, + * 也不自增故障位(s_fault_reported 是 s_append_rec 的函数级 static, + * 测不到,只能从"恢复后第一次真失败仍立刻留痕"反证它没被置位) */ + reset_all(); + ready_only(); + /* s_append_rec 的 L2 去重位是函数级 static,外部清不了:先用一次成功写复位 */ + ok = (s_append(SYSLOG_RUN, "reset-dedup") == 0); + g_sys_err_count = 0; + lock_busy_next(1); + ok = ok && (s_append(SYSLOG_RUN, "busy") == -2) && (g_sys_err_count == 0); + ff_fake_inject_write_short(P_RUN, 0u, 2); + ok = ok && (s_append(SYSLOG_RUN, "real-fault") == -1); + CHECK(ok && (g_sys_err_count == 1), + "TC-SYSLOG-104 busy is not a fault: first real failure still logs immediately"); +} + +/* ===================== J F 项:投递侧丢弃留痕节流(队列通道) ===================== */ + +static void tc_post_throttle(void) +{ + uint32_t i; + int ok; + + printf("-- TC-SYSLOG-105..107 post-side drop-log throttle (queue channel) --\n"); + + /* 105 队列灌满后持续投递 -> 只报第一次。这正是 A 项修复引出的新场景: + * 卷被 ftpTask 长期占用 -> drain 把整批回投 -> 队列常驻满 -> 每条业务 + * 投递都会"队列满 + 打一行",串口按投递频率刷屏。 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); /* 建队列(8 槽) */ + g_sys_err_count = 0; + for (i = 0u; i < SYSLOG_QUEUE_MAX + 4u; i++) { + syslog_post(SYSLOG_RUN, "flood"); + } + CHECK((s_q_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1) + && (s_post_qfull_reported == 1u), + "TC-SYSLOG-105 queue full: 4 drops but only the first one logged"); + + /* 106 同一故障窗口内继续投递 -> 完全静默(丢弃语义不变,仍照丢) */ + g_sys_err_count = 0; + for (i = 0u; i < 5u; i++) { + syslog_post(SYSLOG_RUN, "flood-more"); + } + CHECK((g_sys_err_count == 0) && (s_q_cnt == SYSLOG_QUEUE_MAX), + "TC-SYSLOG-106 later drops in the same window log nothing"); + + /* 107 恢复(排空腾出槽位 + 下一条被收下)-> 抑制位复位;再灌满必须重新报一次。 + * 复位点必须在 syslog_post 的成功分支,不是 drain 里——drain 只负责落盘。 */ + ok = (s_post_qfull_reported == 1u); + syslog_drain(); /* 队列清空、条目落盘 */ + ok = ok && (s_q_cnt == 0u) && (s_post_qfull_reported == 1u); /* drain 不该复位 */ + syslog_post(SYSLOG_RUN, "recovered"); + ok = ok && (s_q_cnt == 1u) && (s_post_qfull_reported == 0u); + CHECK(ok, "TC-SYSLOG-107 recovery via a successful post clears the bit (drain does not)"); + g_sys_err_count = 0; + for (i = 0u; i < SYSLOG_QUEUE_MAX + 1u; i++) { + syslog_post(SYSLOG_RUN, "again"); + } + CHECK((s_q_cnt == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1), + "TC-SYSLOG-107b a second fault logs again (throttle re-armed)"); + /* 顺带确认两个通道的抑制位互不干扰:数组通道此前未触发过 */ + CHECK(s_post_pboot_full_reported == 0u, + "TC-SYSLOG-107c the two channels keep independent throttle bits"); +} + /* ===================== 入口 ===================== */ int main(void) @@ -1527,6 +1814,8 @@ int main(void) tc_evidence(); tc_name_rules(); tc_extra_branches(); + tc_selfheal_codes(); + tc_post_throttle(); printf("\n=== Sys Log Mcu Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); From 924ac3d4287d20ad3e7620b9e134ca3131fe7146 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sat, 24 Oct 2026 20:57:46 +0800 Subject: [PATCH 3/4] =?UTF-8?q?test(host):=20=E6=96=AD=E8=A8=80=E5=AD=97?= =?UTF-8?q?=E9=9D=A2=E9=87=8F=E7=BB=9F=E4=B8=80=E7=BA=AF=20ASCII=EF=BC=8C3?= =?UTF-8?q?0=20=E6=96=87=E4=BB=B6=201535=20=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户裁定「统一」。口径就是 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 回填留待主工作树合并后。 --- docs/测试说明/主机单测覆盖率口径.md | 92 ++++++- test/host/adc_input/adc_input_test.c | 178 ++++++------- .../cfg_file_store_mcu_test.c | 78 +++--- test/host/console_log/console_log_test.c | 248 +++++++++--------- test/host/ftp_auth/ftp_auth_test.c | 58 ++-- test/host/ftp_cfg/ftp_cfg_test.c | 32 +-- test/host/iec104/iec104_test.c | 188 ++++++------- test/host/iec104_serv/iec104_serv_test.c | 58 ++-- test/host/lftpd/lftpd_test.c | 72 ++--- test/host/lftpd_inet/lftpd_inet_test.c | 58 ++-- test/host/lftpd_io/lftpd_io_test.c | 66 ++--- test/host/net_cfg/net_cfg_test.c | 84 +++--- test/host/net_events/net_events_test.c | 68 ++--- test/host/net_listener/net_listener_test.c | 74 +++--- test/host/net_pending/net_pending_test.c | 86 +++--- test/host/net_select/net_select_test.c | 44 ++-- test/host/net_socket/net_socket_test.c | 214 +++++++-------- test/host/net_socket/ns_rtos.c | 14 +- test/host/net_socket/ns_s3_msg.c | 166 ++++++------ test/host/net_socket/ns_s4_state.c | 114 ++++---- test/host/net_socket/ns_s5_io.c | 226 ++++++++-------- test/host/net_socket/ns_s6_edges.c | 102 +++---- .../host/prot_set_store/prot_set_store_test.c | 32 +-- test/host/sha256/sha256_test.c | 26 +- test/host/soe_store/soe_store_test.c | 56 ++-- test/host/sys_clock/sys_clock_test.c | 200 +++++++------- test/host/sys_log_mcu/sys_log_mcu_test.c | 154 +++++------ test/host/task_prof/task_prof_test.c | 54 ++-- test/host/time_sync/time_sync_test.c | 162 ++++++------ test/host/vol_lock/vol_lock_test.c | 66 ++--- test/host/watchdog/watchdog_test.c | 98 +++---- 31 files changed, 1626 insertions(+), 1542 deletions(-) diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index 6a858451..22615d1d 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -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 "** 并返回 -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 轮」小节 | diff --git a/test/host/adc_input/adc_input_test.c b/test/host/adc_input/adc_input_test.c index a1784267..8a1a9944 100644 --- a/test/host/adc_input/adc_input_test.c +++ b/test/host/adc_input/adc_input_test.c @@ -213,20 +213,20 @@ static void tc_init(void) { (void)adc_input_poll(); /* 单拍无新帧 = 相位伪影,不计故障 */ (void)adc_input_poll(); /* 连续第二拍无新帧 = 真停摆 */ - CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-001 连续两拍无新帧 -> 停摆计数 = 1"); + CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-001 two rounds in a row with no new frame -> stall count = 1"); CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 4321), - "TC-ADCIN-002 poll 后 UA 可取数且值正确"); + "TC-ADCIN-002 after poll UA is readable and the value is right"); - CHECK(adc_input_init() == 0, "TC-ADCIN-003 adc_input_init 返回 0(映射自检通过)"); + CHECK(adc_input_init() == 0, "TC-ADCIN-003 adc_input_init returns 0 (mapping self-check passed)"); CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN, - "TC-ADCIN-004 init 后 STM32 状态码 = NOT_RUN"); - CHECK(s_start_dma_calls == 1, "TC-ADCIN-003B init 挂载 DMA 恰好一次"); - CHECK(s_dma_len == (2u * ADC_INPUT_STM32_CH_NUM), "TC-ADCIN-003C DMA 长度 = 2 x 9(乒乓双缓冲)"); - CHECK(s_dma_buf != NULL, "TC-ADCIN-003D DMA 目的缓冲地址已交下来"); - CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-005B init 后 DMA 完成计数 = 0"); - CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-005 init 后 STM32 失败计数归零"); + "TC-ADCIN-004 after init the STM32 status code = NOT_RUN"); + CHECK(s_start_dma_calls == 1, "TC-ADCIN-003B init arms DMA exactly once"); + CHECK(s_dma_len == (2u * ADC_INPUT_STM32_CH_NUM), "TC-ADCIN-003C DMA length = 2 x 9 (ping-pong double buffer)"); + CHECK(s_dma_buf != NULL, "TC-ADCIN-003D the DMA destination buffer address was handed down"); + CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-005B after init the DMA completion counter = 0"); + CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-005 after init the STM32 failure counter is zeroed"); CHECK((adc_input_raw(ADC_SEM_UA, &v) == -2) && (adc_input_raw(ADC_SEM_UB, &v) == -2), - "TC-ADCIN-006 init 后 TPAFE / STM32 两侧均无有效数据"); + "TC-ADCIN-006 after init neither TPAFE nor STM32 side holds valid data"); } /* ===================== TC-ADCIN-010..01A:desc 映射表 ===================== */ @@ -262,8 +262,8 @@ static void tc_desc(void) { CHECK((d != NULL) && (d->source == ADC_SRC_STM32) && (d->index == 5u), "TC-ADCIN-018 TEMP -> STM32 CH5"); - CHECK(adc_input_desc(ADC_SEM_COUNT) == NULL, "TC-ADCIN-019 越界(ADC_SEM_COUNT) -> NULL"); - CHECK(adc_input_desc((adc_semantic_t)(-1)) == NULL, "TC-ADCIN-01A 负数语义 -> NULL"); + CHECK(adc_input_desc(ADC_SEM_COUNT) == NULL, "TC-ADCIN-019 out of range (ADC_SEM_COUNT) -> NULL"); + CHECK(adc_input_desc((adc_semantic_t)(-1)) == NULL, "TC-ADCIN-01A negative index -> NULL"); } /* ===================== TC-ADCIN-020:map_check ===================== */ @@ -272,7 +272,7 @@ static void tc_map_check(void) { /* 真实映射表自洽:TPAFE 下标 < 8 且每个 STM32 通道号都出现在扫描 Rank 内。 * 反向路径(表不自洽 -> -1)需在运行时改表,而 k_adc_map / k_stm32_rank_ch 均为 * 编译期 static const、无运行时输入可使其不自洽 -> 结构上不可达(见文件末说明)。 */ - CHECK(map_check() == 0, "TC-ADCIN-020 映射表自洽(TPAFE 下标与 STM32 Rank 均在范围内)"); + CHECK(map_check() == 0, "TC-ADCIN-020 mapping table is self-consistent (TPAFE index and STM32 Rank both in range)"); } /* ===================== TC-ADCIN-021..02C:stm32_scan 成功路径 ===================== */ @@ -284,22 +284,22 @@ static void tc_take_ok(void) { (void)adc_input_init(); dma_beat(100); /* 前半区一帧:Rank i -> 100+i */ - CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-021 stm32_take 取到新帧 -> 0"); - CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-022 状态码 = RC_OK"); - CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-023 成功不计入失败"); - CHECK(s_start_dma_calls == 1, "TC-ADCIN-024 DMA 只在 init 挂一次(每拍不重启)"); - CHECK(adc_input_stm32_seq() == 1u, "TC-ADCIN-025 DMA 完成计数 = 1"); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-021 stm32_take got a new frame -> 0"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-022 status code = RC_OK"); + CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-023 a success is not counted as a failure"); + CHECK(s_start_dma_calls == 1, "TC-ADCIN-024 DMA is armed once at init (never restarted per round)"); + CHECK(adc_input_stm32_seq() == 1u, "TC-ADCIN-025 DMA completion counter = 1"); /* Rank -> 通道号映射:第 i 个 Rank = 100+i。 * k_stm32_rank_ch = [CH0..CH7, CH14],故 CH3->103 / CH5->105 / CH6->106 / CH14->108。 */ - CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-ADCIN-026 UB 取到 CH14(rank9)=108"); - CHECK((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 103), "TC-ADCIN-027 UC 取到 CH3(rank4)=103"); - CHECK((adc_input_raw(ADC_SEM_IB, &v) == 0) && (v == 106), "TC-ADCIN-028 IB 取到 CH6(rank7)=106"); - CHECK((adc_input_raw(ADC_SEM_IC, &v) == 0) && (v == 101), "TC-ADCIN-029 IC 取到 CH1(rank2)=101"); - CHECK((adc_input_raw(ADC_SEM_IN, &v) == 0) && (v == 102), "TC-ADCIN-02A IN 取到 CH2(rank3)=102"); - CHECK((adc_input_raw(ADC_SEM_TEMP, &v) == 0) && (v == 105), "TC-ADCIN-02B TEMP 取到 CH5(rank6)=105"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-ADCIN-026 UB reads CH14(rank9)=108"); + CHECK((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 103), "TC-ADCIN-027 UC reads CH3(rank4)=103"); + CHECK((adc_input_raw(ADC_SEM_IB, &v) == 0) && (v == 106), "TC-ADCIN-028 IB reads CH6(rank7)=106"); + CHECK((adc_input_raw(ADC_SEM_IC, &v) == 0) && (v == 101), "TC-ADCIN-029 IC reads CH1(rank2)=101"); + CHECK((adc_input_raw(ADC_SEM_IN, &v) == 0) && (v == 102), "TC-ADCIN-02A IN reads CH2(rank3)=102"); + CHECK((adc_input_raw(ADC_SEM_TEMP, &v) == 0) && (v == 105), "TC-ADCIN-02B TEMP reads CH5(rank6)=105"); - CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-02C stm32_take 不置 TPAFE 有效标志"); + CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-02C stm32_take does not set the TPAFE valid flag"); } /* ============ TC-ADCIN-080..085:乒乓双缓冲(读刚完成半区,不读正在写的半区) ============ @@ -314,27 +314,27 @@ static void tc_dma_pingpong(void) { dma_beat(100); /* 第 1 帧 -> 前半区(半传输回调) */ dma_fill(1u, 900u); /* 模拟 DMA 正在写后半区(值 900 系) */ - CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-080 第一拍取帧成功"); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-080 first round fetch succeeded"); CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), - "TC-ADCIN-081 读的是刚完成的前半区(108),不是正在写的后半区"); + "TC-ADCIN-081 reads the just completed half (108), not the half being written"); dma_beat(200); /* 第 2 帧 -> 后半区(传输完成回调) */ dma_fill(0u, 800u); /* 模拟 DMA 正在写前半区(值 800 系) */ - CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-082 第二拍取帧成功"); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-082 second round fetch succeeded"); CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208), - "TC-ADCIN-083 第二拍读到后半区(208)"); - CHECK(adc_input_stm32_seq() == 2u, "TC-ADCIN-084 DMA 完成计数 = 2"); + "TC-ADCIN-083 second round reads the later half (208)"); + CHECK(adc_input_stm32_seq() == 2u, "TC-ADCIN-084 DMA completion counter = 2"); /* 半区奇偶口径:seq=1 -> 前半区,seq=2 -> 后半区(与 dma_beat 独立推导,互相证伪) */ - CHECK(((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 203)), "TC-ADCIN-085 UC 同帧内一致(203)"); + CHECK(((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 203)), "TC-ADCIN-085 UC stays consistent within the frame (203)"); /* 消费者超拍:两拍都没取 -> 中间那帧已被乒乓缓冲覆盖 -> 记 1 点丢帧 */ dma_beat(300); dma_beat(400); - CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-086 超拍后仍取到最新帧"); - CHECK(adc_input_stm32_gaps() == 1u, "TC-ADCIN-087 seq 前进 2 -> 记 1 点丢帧"); - CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 408), "TC-ADCIN-088 取到的是最新帧(408)"); - CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-089 丢帧不计入停摆(两类故障分开)"); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-086 after skipping rounds the newest frame is still fetched"); + CHECK(adc_input_stm32_gaps() == 1u, "TC-ADCIN-087 seq advanced by 2 -> records 1 dropped frame"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 408), "TC-ADCIN-088 what came back is the newest frame (408)"); + CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-089 a drop is not counted as a stall (two fault classes stay apart)"); /* 第二档:上面只跨了 1 帧(前进 2 记 1 点),"次数"与"点数"数值相同、钉不出单位。 * 这里连投三帧不消费 = 一次跨 2 帧,必须记 2 点(累计 1+2=3),把 2026-10-21 改的 @@ -355,18 +355,18 @@ static void tc_hw_errs(void) { HAL_ADC_ErrorCallback(&hadc1); HAL_ADC_ErrorCallback(&hadc1); - CHECK(adc_input_stm32_hw_errs() == 2u, "TC-ADCIN-090 错误回调累计 = 2"); + CHECK(adc_input_stm32_hw_errs() == 2u, "TC-ADCIN-090 error callback total = 2"); HAL_ADC_ErrorCallback(&hadc_other); - CHECK(adc_input_stm32_hw_errs() == 2u, "TC-ADCIN-091 别的 ADC 句柄不得计入(hadc 守卫)"); + CHECK(adc_input_stm32_hw_errs() == 2u, "TC-ADCIN-091 other ADC handles must not count (hadc guard)"); HAL_ADC_ConvHalfCpltCallback(&hadc_other); HAL_ADC_ConvCpltCallback(&hadc_other); - CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-092 别的句柄的完成回调不得推进 seq"); + CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-092 a completion callback from another handle must not advance seq"); HAL_ADC_ErrorCallback(&hadc1); - CHECK(adc_input_stm32_hw_errs() == 3u, "TC-ADCIN-093 hw_errs 与 errs 分开计数、互不干扰"); - CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-09A hw_errs 不污染 errs"); + CHECK(adc_input_stm32_hw_errs() == 3u, "TC-ADCIN-093 hw_errs and errs count separately and never interfere"); + CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-09A hw_errs does not pollute errs"); } /* ===================== TC-ADCIN-030..034:DMA 挂载失败 ===================== */ @@ -377,10 +377,10 @@ static void tc_init_dma_fail(void) { fakes_reset(); s_start_dma_rc = HAL_ERROR; - CHECK(adc_input_init() == -2, "TC-ADCIN-030 HAL_ADC_Start_DMA 失败 -> init 返回 -2"); - CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_START, "TC-ADCIN-031 状态码 = RC_START"); - CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-032 失败计数 +1"); - CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-034 挂载失败后 STM32 侧无有效数据"); + CHECK(adc_input_init() == -2, "TC-ADCIN-030 HAL_ADC_Start_DMA failure -> init returns -2"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_START, "TC-ADCIN-031 status code = RC_START"); + CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-032 failure counter +1"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-034 after an arming failure the STM32 side holds no valid data"); } /* ===================== TC-ADCIN-035..03A:本拍无新帧(停摆) ===================== */ @@ -392,21 +392,21 @@ static void tc_take_stall(void) { (void)adc_input_init(); /* 没有任何 DMA 完成回调 = TIM3 未启动 / ADC 停摆 / DMA 出错 */ - CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-ADCIN-035 无新帧 -> RC_STALL(-2)"); - CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-036 状态码 = RC_STALL"); - CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-037 单拍无新帧不计故障(相位伪影)"); - CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-038 停摆期间 seq 不前进"); - CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-039 停摆拍 STM32 侧无有效数据"); + CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-ADCIN-035 no new frame -> RC_STALL(-2)"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-036 status code = RC_STALL"); + CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-037 a single round without a new frame is not a fault (phase artefact)"); + CHECK(adc_input_stm32_seq() == 0u, "TC-ADCIN-038 seq does not advance while stalled"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-039 a stalled round leaves the STM32 side without valid data"); (void)stm32_take(); /* 连续第二拍无新帧 = 真停摆 */ - CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-03A 连续两拍无新帧 -> 停摆计数 = 1"); + CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-03A two rounds in a row with no new frame -> stall count = 1"); (void)stm32_take(); /* 第三拍仍无新帧 */ - CHECK(adc_input_stm32_errs() == 2u, "TC-ADCIN-03B 停摆持续 -> 每拍累计 = 2"); - CHECK(adc_input_stm32_gaps() == 0u, "TC-ADCIN-03C 停摆不计入丢帧(两类故障分开)"); + CHECK(adc_input_stm32_errs() == 2u, "TC-ADCIN-03B an ongoing stall accumulates every round = 2"); + CHECK(adc_input_stm32_gaps() == 0u, "TC-ADCIN-03C a stall is not counted as a drop (two fault classes stay apart)"); dma_beat(100); /* 恢复 */ - CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-03D 恢复后取帧成功"); - CHECK(adc_input_stm32_gaps() == 0u, "TC-ADCIN-03E 恢复拍前进 1 -> 不算丢帧"); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-03D after recovery the fetch succeeds"); + CHECK(adc_input_stm32_gaps() == 0u, "TC-ADCIN-03E the recovery round advances by 1 -> not a drop"); } /* ============ TC-ADCIN-070..078:停摆拍不得沿用上一拍旧值(陈旧帧) ============ @@ -424,24 +424,24 @@ static void tc_take_stale(void) { fakes_reset(); (void)adc_input_init(); dma_beat(100); - CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-070 前置:首拍成功"); - CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-ADCIN-071 前置:UB 取到 108(有效态)"); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-070 precondition: first round succeeded"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-ADCIN-071 precondition: UB reads 108 (valid state)"); /* 第二拍没有任何 DMA 完成(TIM3 停 / ADC 停摆 / DMA 出错) */ - CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-ADCIN-072 第二拍无新帧"); - CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-073 停摆拍不得沿用上一拍值 -> -2"); - CHECK(adc_input_raw(ADC_SEM_UC, &v) == -2, "TC-ADCIN-074 停摆拍连其它 STM32 通道也不得出数"); - CHECK(adc_input_raw(ADC_SEM_TEMP, &v) == -2, "TC-ADCIN-074B 同上(TEMP 亦 -2)"); + CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-ADCIN-072 second round has no new frame"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-073 a stalled round must not reuse the previous value -> -2"); + CHECK(adc_input_raw(ADC_SEM_UC, &v) == -2, "TC-ADCIN-074 a stalled round must not emit any other STM32 channel either"); + CHECK(adc_input_raw(ADC_SEM_TEMP, &v) == -2, "TC-ADCIN-074B same as above (TEMP also -2)"); dma_beat(200); - CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-075 第三拍恢复成功"); - CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208), "TC-ADCIN-076 恢复拍重新可取数(自愈)"); + CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-ADCIN-075 third round recovers successfully"); + CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208), "TC-ADCIN-076 the recovery round can be read again (self-healing)"); /* --- 挂载失败支路:同样不得沿用(init 失败后整层无有效数据) --- */ fakes_reset(); s_start_dma_rc = HAL_ERROR; (void)adc_input_init(); - CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-078 挂载失败后不得取到任何值 -> -2"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-078 after an arming failure no value may be read -> -2"); } /* ===================== TC-ADCIN-040..045:raw 参数守卫与无数据 ===================== */ @@ -453,11 +453,11 @@ static void tc_raw_guards(void) { (void)adc_input_init(); CHECK(adc_input_raw(ADC_SEM_UA, NULL) == -1, "TC-ADCIN-040 out=NULL -> -1"); - CHECK(adc_input_raw(ADC_SEM_COUNT, &v) == -1, "TC-ADCIN-041 ch 越界 -> -1"); - CHECK(adc_input_raw((adc_semantic_t)(-1), &v) == -1, "TC-ADCIN-042 ch 负数 -> -1"); - CHECK(v == 0, "TC-ADCIN-043 参数错时不写 out"); - CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-044 TPAFE 无有效帧 -> -2"); - CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-045 STM32 无有效扫描 -> -2"); + CHECK(adc_input_raw(ADC_SEM_COUNT, &v) == -1, "TC-ADCIN-041 ch out of range -> -1"); + CHECK(adc_input_raw((adc_semantic_t)(-1), &v) == -1, "TC-ADCIN-042 ch negative -> -1"); + CHECK(v == 0, "TC-ADCIN-043 on an argument error out is not written"); + CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-044 TPAFE has no valid frame -> -2"); + CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-ADCIN-045 STM32 has no valid scan -> -2"); } /* ===================== TC-ADCIN-046..04B:raw TPAFE 取数与补码 ===================== */ @@ -478,13 +478,13 @@ static void tc_raw_tpafe(void) { s_fake_tpafe[7] = 1007; s_snapshot_rc = 1; - CHECK(adc_input_poll() == 1, "TC-ADCIN-046 poll 透传 TPAFE 快照码 1"); - CHECK(s_snapshot_count == (uint8_t)ADC_INPUT_TPAFE_CH_NUM, "TC-ADCIN-047 快照请求 8 通道"); - CHECK(s_snapshot_dst != NULL, "TC-ADCIN-048 快照目的缓冲非空"); + CHECK(adc_input_poll() == 1, "TC-ADCIN-046 poll passes through the TPAFE snapshot code 1"); + CHECK(s_snapshot_count == (uint8_t)ADC_INPUT_TPAFE_CH_NUM, "TC-ADCIN-047 snapshot requests 8 channels"); + CHECK(s_snapshot_dst != NULL, "TC-ADCIN-048 the snapshot destination buffer is not NULL"); CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 3006), "TC-ADCIN-049 UA -> buf[6]=3006"); CHECK((adc_input_raw(ADC_SEM_UN, &v) == 0) && (v == 2005), "TC-ADCIN-04A UN -> buf[5]=2005"); CHECK((adc_input_raw(ADC_SEM_IA, &v) == 0) && (v == -1234), - "TC-ADCIN-04B IA -> buf[2]=-1234(补码负值原样保留)"); + "TC-ADCIN-04B IA -> buf[2]=-1234 (two's-complement negative kept as is)"); } /* ===================== TC-ADCIN-050..058:poll 契约 ===================== */ @@ -497,16 +497,16 @@ static void tc_poll(void) { (void)adc_input_init(); s_snapshot_rc = 0; dma_beat(100); - CHECK(adc_input_poll() == 0, "TC-ADCIN-050 快照无新帧 -> poll 返回 0"); - CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-051 无新帧时 TPAFE 侧无效"); - CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-052 同拍 STM32 取帧照常执行"); + CHECK(adc_input_poll() == 0, "TC-ADCIN-050 snapshot with no new frame -> poll returns 0"); + CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-051 without a new frame the TPAFE side is invalid"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-052 the STM32 fetch in the same round still runs"); /* --- 参数错(-1):返回 -1,TPAFE 无效 --- */ fakes_reset(); (void)adc_input_init(); s_snapshot_rc = -1; - CHECK(adc_input_poll() == -1, "TC-ADCIN-053 快照参数错 -> poll 返回 -1"); - CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-054 参数错时 TPAFE 侧无效"); + CHECK(adc_input_poll() == -1, "TC-ADCIN-053 snapshot argument error -> poll returns -1"); + CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-ADCIN-054 on an argument error the TPAFE side is invalid"); /* --- 已有有效帧后再"无新帧":保持上次有效值(不清零) --- */ fakes_reset(); @@ -517,7 +517,7 @@ static void tc_poll(void) { s_snapshot_rc = 0; (void)adc_input_poll(); CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 777), - "TC-ADCIN-055 已有有效帧后无新帧不清零"); + "TC-ADCIN-055 an existing valid frame is not zeroed when no new frame arrives"); /* --- STM32 失败不阻断 TPAFE、不影响 poll 返回值 --- */ fakes_reset(); @@ -525,10 +525,10 @@ static void tc_poll(void) { s_fake_tpafe[2] = 55; s_snapshot_rc = 1; /* 不注入任何 DMA 完成 = STM32 侧停摆 */ - CHECK(adc_input_poll() == 1, "TC-ADCIN-056 STM32 停摆不影响 poll 返回值(仍为 1)"); + CHECK(adc_input_poll() == 1, "TC-ADCIN-056 an STM32 stall does not change the poll return value (still 1)"); CHECK((adc_input_raw(ADC_SEM_IA, &v) == 0) && (v == 55), - "TC-ADCIN-057 STM32 停摆不影响 TPAFE 取数"); - CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-058 STM32 停摆另由状态码暴露"); + "TC-ADCIN-057 an STM32 stall does not affect TPAFE data"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-058 an STM32 stall is exposed through the status code instead"); } /* ===================== TC-ADCIN-060..066:rc / errs 观测 ===================== */ @@ -537,24 +537,24 @@ static void tc_rc_errs(void) { fakes_reset(); (void)adc_input_init(); - CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN, "TC-ADCIN-060 init 后未扫描 = NOT_RUN(-3)"); - CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-061 init 后失败计数 = 0"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN, "TC-ADCIN-060 after init, before any scan = NOT_RUN(-3)"); + CHECK(adc_input_stm32_errs() == 0u, "TC-ADCIN-061 after init the failure counter = 0"); dma_beat(100); (void)stm32_take(); /* 成功 */ CHECK((adc_input_stm32_rc() == ADC_INPUT_RC_OK) && (adc_input_stm32_errs() == 0u), - "TC-ADCIN-062 成功: rc=OK 且 errs 不变"); + "TC-ADCIN-062 success: rc=OK and errs unchanged"); (void)stm32_take(); /* 停摆 1(单拍 = 相位伪影,不计) */ (void)stm32_take(); /* 停摆 2 -> 计 1 */ - CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-063 连续两拍停摆累计 = 1"); - CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-064 rc = 最近一次(STALL)"); + CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-063 two consecutive stalled rounds accumulate = 1"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-ADCIN-064 rc = the most recent result (STALL)"); dma_beat(200); (void)stm32_take(); /* 恢复成功 */ - CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-065 恢复成功后 rc=OK"); - CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-066 errs 单调递增(不因成功回落)"); - CHECK(adc_input_stm32_seq() == 2u, "TC-ADCIN-067 seq 只由 DMA 完成回调推进"); + CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-ADCIN-065 after a successful recovery rc=OK"); + CHECK(adc_input_stm32_errs() == 1u, "TC-ADCIN-066 errs increases monotonically (a success never lowers it)"); + CHECK(adc_input_stm32_seq() == 2u, "TC-ADCIN-067 seq is advanced only by the DMA completion callback"); } /* ===================== main ===================== */ diff --git a/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c b/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c index 694b0770..5259ae0f 100644 --- a/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c +++ b/test/host/cfg_file_store_mcu/cfg_file_store_mcu_test.c @@ -260,7 +260,7 @@ static void tc_read(void) size_t n = 999u; int ok; - printf("-- TC-CFGFST-001..007 读 --\n"); + printf("-- TC-CFGFST-001..007 read path --\n"); /* 001 成功 + 只读模式 */ reset_all(); @@ -269,14 +269,14 @@ static void tc_read(void) && (memcmp(buf, "net ip=192.168.1.100\n", 20u) == 0) && (g_ff_obs.last_open_mode == (BYTE)FA_READ) && (s_lock_waits == 1) && (s_lock_releases == 1) && (s_lock_seq_err == 0); - CHECK(ok, "TC-CFGFST-001 读成功:内容与长度正确,打开模式就是 FA_READ(不带写位)"); + CHECK(ok, "TC-CFGFST-001 read ok: content and length correct, opened with FA_READ only (no write bit)"); /* 002 空指针守卫在取锁之前 */ reset_all(); ok = (cfg_store_read(P_CFG, NULL, sizeof(buf), &n) == -1) && (cfg_store_read(P_CFG, buf, sizeof(buf), NULL) == -1); CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), - "TC-CFGFST-002 空指针 -> -1 且不取卷锁(守卫顺序正确)"); + "TC-CFGFST-002 NULL pointer -> -1 and no volume lock taken (guard order correct)"); /* 003 卷忙 */ reset_all(); @@ -284,13 +284,13 @@ static void tc_read(void) ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1); CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 0) && (s_lock_seq_err == 0), - "TC-CFGFST-003 卷忙 -> -1:零 FatFs、不 release(没拿到锁不放锁)"); + "TC-CFGFST-003 volume busy -> -1: zero FatFs calls, no release (never release a lock we do not hold)"); /* 004 文件缺失:必须已放锁 */ reset_all(); ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1); CHECK(ok && (s_lock_releases == 1) && (s_lock_seq_err == 0), - "TC-CFGFST-004 文件缺失 -> -1 **且已释放卷锁**(漏放 = 之后所有卷操作全瘫)"); + "TC-CFGFST-004 missing file -> -1 **and the volume lock is released** (leaking it paralyses every later volume call)"); /* 005 cap > 文件大小 */ reset_all(); @@ -298,7 +298,7 @@ static void tc_read(void) n = 999u; ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 3u) && (g_ff_obs.last_read_btr == 64u) && (g_ff_obs.last_read_br == 3u); - CHECK(ok, "TC-CFGFST-005 要 64 只给 3 -> nread=3 且仍算成功(短读不是错误)"); + CHECK(ok, "TC-CFGFST-005 asked 64 got 3 -> nread=3 and still success (a short read is not an error)"); /* 006 f_read 失败 */ reset_all(); @@ -307,7 +307,7 @@ static void tc_read(void) ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == -1); CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1) && (s_lock_releases == 1) && (s_lock_seq_err == 0), - "TC-CFGFST-006 读失败 -> -1 + close 已配对 + 放锁"); + "TC-CFGFST-006 read failure -> -1, close paired, lock released"); /* 007 介质短读:现状是"返回 0 + nread 如实变小",调用方靠 nread 定长度 */ reset_all(); @@ -315,7 +315,7 @@ static void tc_read(void) ff_fake_inject_read_short(P_CFG, 4u, 1); n = 999u; ok = (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 4u); - CHECK(ok, "TC-CFGFST-007 注入短读 -> 返回 0、nread=4(上层按 nread 截断使用)"); + CHECK(ok, "TC-CFGFST-007 injected short read -> returns 0 with nread=4 (caller truncates by nread)"); } /* ===================== 原子写 ===================== */ @@ -326,7 +326,7 @@ static void tc_write_atomic(void) size_t n; int ok; - printf("-- TC-CFGFST-010..020 原子写(E-01 核心)--\n"); + printf("-- TC-CFGFST-010..020 atomic write (core of E-01) --\n"); /* 010 首启写(目标不存在) */ reset_all(); @@ -337,7 +337,7 @@ static void tc_write_atomic(void) && (strcmp(g_ff_obs.last_rename_old, "/CONFIG.TMP") == 0) && (strcmp(g_ff_obs.last_rename_new, "/CONFIG.INI") == 0) && (s_lock_releases == 1) && (s_lock_seq_err == 0), - "TC-CFGFST-010 首启写:TMP -> rename -> 目标 2 字节、TMP 不留痕"); + "TC-CFGFST-010 first write: TMP -> rename -> target 2 bytes, no TMP left behind"); /* 011 覆盖写:假卷的 f_rename 对已存在目标返回 FR_EXIST —— 这就是 E-01 */ reset_all(); @@ -349,7 +349,7 @@ static void tc_write_atomic(void) && (ff_fake_fetch(P_CFG, buf, 0u, sizeof(buf)) == 3u) && (memcmp(buf, "new", 3u) == 0) && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlinks_before + 1), - "TC-CFGFST-011 覆盖写:unlink 目标恰一次再 rename -> 新内容落地(E-01)"); + "TC-CFGFST-011 overwrite: unlink target exactly once then rename -> new content lands (E-01)"); } /* 同一条的"反面":直接 rename 到已存在目标必败(证明上一步的 unlink 不是多余的) */ { @@ -359,7 +359,7 @@ static void tc_write_atomic(void) (void)ff_fake_mkfile(P_TMP, (const uint8_t *)"x", 1u); fr = f_rename(P_TMP, P_CFG); CHECK(fr == FR_EXIST, - "TC-CFGFST-011b 假件保真:目标已存在时 f_rename 返回 FR_EXIST、不覆盖"); + "TC-CFGFST-011b fake fidelity: f_rename returns FR_EXIST when the target exists, no overwrite"); if (fr == FR_OK) { (void)f_close(&probe); } @@ -374,7 +374,7 @@ static void tc_write_atomic(void) * TMP 一定被清掉、锁一定释放、绝不留半成品 */ CHECK(ok && (ff_fake_exists(P_TMP) == 0) && (ff_fake_exists(P_CFG) == 0) && (s_lock_releases == 2) && (s_lock_seq_err == 0), - "TC-CFGFST-012 rename 失败 -> -1 + TMP 清理 + 放锁( unlink→rename 窗口如实存在)"); + "TC-CFGFST-012 rename failure -> -1, TMP cleaned, lock released (the unlink->rename window really exists)"); /* 013 TMP 打不开 */ reset_all(); @@ -384,7 +384,7 @@ static void tc_write_atomic(void) CHECK(ok && (ff_fake_size(P_CFG) == 4u) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0) && (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 0) && (s_lock_releases == 1) && (s_lock_seq_err == 0), - "TC-CFGFST-013 TMP 打开失败 -> -1 且**目标原内容一个字节没动**"); + "TC-CFGFST-013 TMP open failure -> -1 and **not one byte of the target changed**"); /* 014 短写 = 失败:TMP 被删、目标不动、不 rename */ reset_all(); @@ -398,7 +398,7 @@ static void tc_write_atomic(void) && (ff_fake_fetch(P_CFG, buf, 0u, sizeof(buf)) == 11u) && (memcmp(buf, "good-config", 11u) == 0) && (s_lock_seq_err == 0), - "TC-CFGFST-014 TMP 短写判失败 -> 删 TMP、不 rename、目标仍是旧内容"); + "TC-CFGFST-014 TMP short write is a failure -> TMP deleted, no rename, target keeps old content"); /* 015 len = 0 合法(清空配置) */ reset_all(); @@ -407,7 +407,7 @@ static void tc_write_atomic(void) n = 999u; CHECK(ok && (ff_fake_size(P_CFG) == 0u) && (cfg_store_read(P_CFG, buf, sizeof(buf), &n) == 0) && (n == 0u), - "TC-CFGFST-015 写 0 字节成功(清空配置),回读 nread=0"); + "TC-CFGFST-015 writing 0 bytes succeeds (config cleared), read-back gives nread=0"); /* 016 卷忙:整条链零 I/O */ reset_all(); @@ -418,13 +418,13 @@ static void tc_write_atomic(void) && (g_ff_obs.calls[FF_FAKE_OP_RENAME] == 0) && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0) && (ff_fake_size(P_CFG) == 4u) && (s_lock_releases == 0), - "TC-CFGFST-016 卷忙 -> -1:不建 TMP、不动目标、不 release"); + "TC-CFGFST-016 volume busy -> -1: no TMP created, target untouched, no release"); /* 017 p_buf NULL */ reset_all(); ok = (cfg_store_write_atomic(P_CFG, NULL, 3u) == -1); CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), - "TC-CFGFST-017 p_buf NULL -> -1 且不取锁"); + "TC-CFGFST-017 p_buf NULL -> -1 and no lock taken"); /* 017b 目标路径本身是 NULL:s_build_tmp_path 的空指针分支(读函数有守卫, * 写函数**没有** p_path 的显式守卫,全靠推导函数把它挡在取锁之前) */ @@ -433,7 +433,7 @@ static void tc_write_atomic(void) ok = (cfg_store_write_atomic(NULL, (const uint8_t *)"x", 1u) == -1); CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (ff_fake_size(P_CFG) == 4u), - "TC-CFGFST-017b p_path NULL -> -1:不取锁、不开文件、目标不动"); + "TC-CFGFST-017b p_path NULL -> -1: no lock, no file open, target untouched"); /* 018 临时名由目标推导(同目录) */ reset_all(); @@ -443,22 +443,22 @@ static void tc_write_atomic(void) && (ff_fake_exists("/cfg/config.TMP") == 0) /* 已被 rename 走,不是留档 */ && (strcmp(g_ff_obs.last_rename_new, "/CFG/A.INI") == 0) && (ff_fake_size("/cfg/a.ini") == 1u), - "TC-CFGFST-018 目标是 /cfg/a.ini -> 临时文件落在同目录 /cfg/config.TMP"); + "TC-CFGFST-018 target /cfg/a.ini -> temp file lands in the same directory /cfg/config.TMP"); CHECK(strcmp(cfg_store_tmp_path(), P_TMP) == 0, - "TC-CFGFST-018b getter 仍是卷根 /config.TMP(与 s_path_tmp 同源)"); + "TC-CFGFST-018b getter still reports the volume root /config.TMP (same source as s_path_tmp)"); /* 019 目录太长 -> 推导失败,且发生在取锁之前 */ reset_all(); ok = (cfg_store_write_atomic("/this/is/a/very/deep/dir/name/x.ini", (const uint8_t *)"y", 1u) == -1); CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), - "TC-CFGFST-019 临时名缓冲放不下 -> -1 且不取锁(不拿半截路径去开文件)"); + "TC-CFGFST-019 temp name does not fit the buffer -> -1, no lock (never open a file with a truncated path)"); /* 020 陷阱一在本模块同样成立 */ reset_all(); (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"m", 1u); CHECK((g_ff_obs.last_open_mode & FA_WRITE) != 0u, - "TC-CFGFST-020 写 TMP 的句柄必带 FA_WRITE(这里用 CREATE_ALWAYS,不是 APPEND)"); + "TC-CFGFST-020 the TMP handle must carry FA_WRITE (CREATE_ALWAYS here, not APPEND)"); reset_all(); { static FIL probe; @@ -470,7 +470,7 @@ static void tc_write_atomic(void) fr2 = f_write(&probe, "abc", 3u, &bw); (void)f_close(&probe); CHECK((fr1 == FR_OK) && (fr2 == FR_DENIED) && (bw == 0u), - "TC-CFGFST-020b 只给 FA_CREATE_ALWAYS -> 打开成功但 f_write 恒 FR_DENIED"); + "TC-CFGFST-020b FA_CREATE_ALWAYS alone -> open succeeds but f_write is always FR_DENIED"); } } @@ -484,7 +484,7 @@ static void tc_stat_remove(void) int rel; int ok; - printf("-- TC-CFGFST-030..033 状态令牌与删除 --\n"); + printf("-- TC-CFGFST-030..033 stat token and remove --\n"); /* 030 size + 令牌 */ reset_all(); @@ -498,7 +498,7 @@ static void tc_stat_remove(void) ok = ok && (cfg_store_stat(P_CFG, &st) == 0) && (st.mtime == tok2) && (tok1 != tok2); CHECK(ok && (s_lock_seq_err == 0) && (s_lock_releases == s_lock_waits), - "TC-CFGFST-030 令牌 = (fdate<<16)|ftime,日期进位一定换令牌"); + "TC-CFGFST-030 token = (fdate<<16)|ftime, a date rollover must change the token"); /* 031 盲区:同一分钟、同长度重写 -> 令牌不变 */ reset_all(); @@ -510,7 +510,7 @@ static void tc_stat_remove(void) (void)cfg_store_write_atomic(P_CFG, (const uint8_t *)"bbbb", 4u); (void)cfg_store_stat(P_CFG, &st); CHECK(ok && (st.size == 4u) && (st.mtime == before), - "TC-CFGFST-031 同分钟同长度重写 -> 令牌不变(poll 检不出,设计已接受)"); + "TC-CFGFST-031 rewrite in the same minute with the same length -> token unchanged (poll cannot see it, accepted by design)"); } /* 032 stat 三种 -1 */ @@ -526,7 +526,7 @@ static void tc_stat_remove(void) ff_fake_inject_rc(P_CFG, FF_FAKE_OP_STAT, FR_DISK_ERR, 1); ok = ok && (cfg_store_stat(P_CFG, &st) == -1) && (s_lock_releases == rel + 1) && (s_lock_seq_err == 0); - CHECK(ok, "TC-CFGFST-032 stat:NULL 守卫/缺失/卷忙/读错误四种出口的放锁配对"); + CHECK(ok, "TC-CFGFST-032 stat: lock pairing on all four exits (NULL guard / missing / busy / read error)"); /* 033 remove */ reset_all(); @@ -539,7 +539,7 @@ static void tc_stat_remove(void) lock_busy_next(1); ok = ok && (cfg_store_remove(P_CFG) == -1) && (s_lock_releases == rel + 2) && (s_lock_seq_err == 0); - CHECK(ok, "TC-CFGFST-033 remove:成功 / 已不存在的 -1 / 卷忙的 -1 与放锁配对"); + CHECK(ok, "TC-CFGFST-033 remove: success / -1 when already gone / -1 when busy, each with lock pairing"); } /* ===================== 路径与 8.3(陷阱二) ===================== */ @@ -559,7 +559,7 @@ static void tc_names(void) FRESULT fr_colon; int ok; - printf("-- TC-CFGFST-040..042 路径合规与「名字错不升级成卷操作」 --\n"); + printf("-- TC-CFGFST-040..042 path legality, name errors never escalate to volume ops --\n"); /* 040 生产路径真的可用(改名即静默失败的第一道防线) */ reset_all(); @@ -568,7 +568,7 @@ static void tc_names(void) && (f_close(&probe) == FR_OK) && (f_open(&probe, cfg_store_tmp_path(), FA_WRITE | FA_CREATE_ALWAYS) == FR_OK) && (f_close(&probe) == FR_OK); - CHECK(ok, "TC-CFGFST-040 /config.ini 与 /config.TMP 都是合法 8.3,真能在卷上建出来"); + CHECK(ok, "TC-CFGFST-040 /config.ini and /config.TMP are valid 8.3 names and really get created on the volume"); /* 041 短名违规分档 */ reset_all(); @@ -585,7 +585,7 @@ static void tc_names(void) CHECK((fr_body == FR_INVALID_NAME) && (fr_ext == FR_INVALID_NAME) && (fr_bad == FR_INVALID_NAME) && (fr_dot == FR_INVALID_NAME) && (fr_colon == FR_INVALID_DRIVE), - "TC-CFGFST-041 分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME;':' -> FR_INVALID_DRIVE"); + "TC-CFGFST-041 classes: body>8 / ext>3 / bad char / leading dot -> FR_INVALID_NAME; ':' -> FR_INVALID_DRIVE"); /* 042 名字类错误码绝不升级成卷操作:非法目标名走一遍原子写 */ reset_all(); @@ -599,7 +599,7 @@ static void tc_names(void) CHECK(ok && ((uint32_t)ff_fake_count_entries() == ents_before) && (ff_fake_size(P_CFG) == 4u) && (s_lock_seq_err == 0) && (s_lock_releases == rel + 3), - "TC-CFGFST-042 非法名只让本次调用失败:条目数不变、已有配置不动、锁把把放过"); + "TC-CFGFST-042 an illegal name fails only this call: entry count unchanged, existing config intact, lock always released"); /* 非法目标名进原子写:推导出的 TMP 也非法 -> 第一步开文件就失败, * 绝不能把已经存在的目标删掉 */ reset_all(); @@ -608,7 +608,7 @@ static void tc_names(void) ok = (cfg_store_write_atomic("/badname-tooooo-long.ini", (const uint8_t *)"x", 1u) == -1); CHECK(ok && (ff_fake_size(P_CFG) == 4u) && ((uint32_t)ff_fake_count_entries() == ents_before), - "TC-CFGFST-042b 非法目标名的原子写整条失败,**不碰已有配置文件**"); + "TC-CFGFST-042b an atomic write to an illegal target fails whole, **never touching the live config file**"); } /* ===================== 堆 ===================== */ @@ -618,7 +618,7 @@ static void tc_heap(void) void *p; void *q; - printf("-- TC-CFGFST-050..053 分配后端(优化调研 P0 第 1 条)--\n"); + printf("-- TC-CFGFST-050..053 allocation backend (P0 item 1 of the optimisation study) --\n"); /* 050 成对归还 */ reset_all(); @@ -626,7 +626,7 @@ static void tc_heap(void) (void)cfg_store_free(p); CHECK((p != NULL) && (s_heap_alloc_calls == 1) && (s_heap_free_calls == 1) && (s_heap_used == 0u) && (s_heap_free_err == 0), - "TC-CFGFST-050 alloc/free 走 FreeRTOS 堆且成对归还(借多少还多少)"); + "TC-CFGFST-050 alloc/free go through the FreeRTOS heap and return in pairs (give back what was taken)"); /* 051 分配失败如实返回 NULL */ reset_all(); @@ -635,19 +635,19 @@ static void tc_heap(void) q = cfg_store_alloc(8u); cfg_store_free(p); /* NULL 必须安全 */ CHECK((p == NULL) && (q == NULL) && (s_heap_free_calls == 0), - "TC-CFGFST-051 堆见底 -> cfg_store_alloc 返回 NULL(MicroLIB 512B 的真形态)"); + "TC-CFGFST-051 heap exhausted -> cfg_store_alloc returns NULL (the real MicroLIB 512B shape)"); /* 052 free(NULL) 安全 + 不计数 */ reset_all(); cfg_store_free(NULL); CHECK((s_heap_free_calls == 0) && (s_heap_free_err == 0), - "TC-CFGFST-052 cfg_store_free(NULL) 不触碰分配器"); + "TC-CFGFST-052 cfg_store_free(NULL) does not touch the allocator"); /* 053 heap_free 转发 */ reset_all(); (void)cfg_store_alloc(100u); CHECK(cfg_store_heap_free() == (FAKE_HEAP_CAP - 100u), - "TC-CFGFST-053 cfg_store_heap_free 原样转发 xPortGetFreeHeapSize"); + "TC-CFGFST-053 cfg_store_heap_free forwards xPortGetFreeHeapSize verbatim"); } /* ===================== 入口 ===================== */ diff --git a/test/host/console_log/console_log_test.c b/test/host/console_log/console_log_test.c index 35a34227..47c75589 100644 --- a/test/host/console_log/console_log_test.c +++ b/test/host/console_log/console_log_test.c @@ -493,27 +493,27 @@ static void tc_params(void) { char buf[DBG_CON_LINE_MAX + 128U]; - printf("-- TC-CONLOG-000..006 参数校验与截断 --\n"); + printf("-- TC-CONLOG-000..006 argument checks and truncation --\n"); reset_all(); - printf("(probe) 这行走宿主 CRT\n"); + printf("(probe) this line goes through the host CRT\n"); fflush(stdout); CHECK(con_uart_tx_n == 0U && used_now() == 0U, - "TC-CONLOG-000 宿主 printf 不经过本模块 fputc(否则测试输出会被吞进环)"); + "TC-CONLOG-000 host printf does not pass through this module fputc (else test output would be swallowed into the ring)"); reset_all(); con_uart_txe_stuck = 1; /* 通道卡死:任何字节都不该上线 */ - CHECK(dbg_console_write_line(0, 5U) == -1, "TC-CONLOG-001 write_line(NULL) 返回 -1"); - CHECK(dbg_console_write_line("abc", 0U) == -1, "TC-CONLOG-002 write_line(len=0) 返回 -1"); + CHECK(dbg_console_write_line(0, 5U) == -1, "TC-CONLOG-001 write_line(NULL) returns -1"); + CHECK(dbg_console_write_line("abc", 0U) == -1, "TC-CONLOG-002 write_line(len=0) returns -1"); dbg_console_write_raw(0, 5U); dbg_console_write_raw("abc", 0U); - CHECK(con_uart_tx_n == 0U, "TC-CONLOG-003 两路 NULL/0 都不碰 UART"); - CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-004 参数非法不计入丢弃"); + CHECK(con_uart_tx_n == 0U, "TC-CONLOG-003 both NULL and len=0 never touch the UART"); + CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-004 invalid arguments are not counted as drops"); reset_all(); memset(buf, 'A', sizeof(buf)); CHECK(dbg_console_write_line(buf, sizeof(buf)) == (int)DBG_CON_LINE_MAX, - "TC-CONLOG-005 超过 DBG_CON_LINE_MAX 的行按 192 截断(返回值即交付字节数)"); - CHECK(con_uart_tx_n == DBG_CON_LINE_MAX, "TC-CONLOG-006 截断后恰好上线 192 字节"); + "TC-CONLOG-005 a line longer than DBG_CON_LINE_MAX is truncated to 192 (return value is the delivered byte count)"); + CHECK(con_uart_tx_n == DBG_CON_LINE_MAX, "TC-CONLOG-006 after truncation exactly 192 bytes reach the wire"); } /* ============ TC-CONLOG-010..018 写者未注册 = 无锁直发 ============ */ @@ -521,26 +521,26 @@ static void tc_unattached(void) { char big[DBG_CON_CHUNK_MAX + 64U]; - printf("-- TC-CONLOG-010..018 写者未注册:无锁直发 --\n"); + printf("-- TC-CONLOG-010..018 writer not attached: lock-free direct send --\n"); reset_all(); - CHECK(dbg_console_writer_ready() == 0U, "TC-CONLOG-010 attach 之前 writer_ready() == 0"); - CHECK(dbg_console_write_line("hello", 5U) == 5, "TC-CONLOG-011 未注册时整行直发,返回行长"); - CHECK(tx_is(0U, "hello"), "TC-CONLOG-012 直发内容原样上线(没经过环形缓冲)"); - CHECK(used_now() == 0U, "TC-CONLOG-013 直发不入环(head == tail)"); - CHECK(con_evt_sets == 0U, "TC-CONLOG-014 直发不置 SYS_EVT_CONSOLE_TX(还没写者可唤醒)"); + CHECK(dbg_console_writer_ready() == 0U, "TC-CONLOG-010 writer_ready() == 0 before attach"); + CHECK(dbg_console_write_line("hello", 5U) == 5, "TC-CONLOG-011 before attach the whole line is sent directly, returning its length"); + CHECK(tx_is(0U, "hello"), "TC-CONLOG-012 direct send reaches the wire verbatim (never went through the ring)"); + CHECK(used_now() == 0U, "TC-CONLOG-013 direct send does not enter the ring (head == tail)"); + CHECK(con_evt_sets == 0U, "TC-CONLOG-014 direct send does not set SYS_EVT_CONSOLE_TX (no writer to wake yet)"); CHECK(con_crit_enter == 0U, - "TC-CONLOG-015 直发零临界区 = 2026-10-10 逐字节交错的根因(不是丢整行)"); + "TC-CONLOG-015 direct send uses zero critical sections = the 2026-10-10 byte-interleaving root cause (not whole-line loss)"); reset_all(); memset(big, 'B', sizeof(big)); dbg_console_write_raw(big, sizeof(big)); - CHECK(con_uart_tx_n == DBG_CON_CHUNK_MAX, "TC-CONLOG-016 write_raw 有界:只发 256 字节"); + CHECK(con_uart_tx_n == DBG_CON_CHUNK_MAX, "TC-CONLOG-016 write_raw is bounded: only 256 bytes are sent"); reset_all(); con_uart_txe_after = 3; /* 前 3 次不就绪(TXE 抖动,未超上界) */ - CHECK(dbg_console_write_line("CD", 2U) == 2, "TC-CONLOG-017 TXE 抖动后仍发全"); + CHECK(dbg_console_write_line("CD", 2U) == 2, "TC-CONLOG-017 after TXE jitter the whole line still goes out"); CHECK(tx_is(0U, "CD") && con_delay_calls == 0U, - "TC-CONLOG-018 直发既不等 tick 也不入环(ISR/崩溃钩子可用的前提)"); + "TC-CONLOG-018 direct send neither waits on tick nor enters the ring (premise for ISR and crash hooks)"); } /* ============ TC-CONLOG-020..037 注册后走环形缓冲 ============ */ @@ -550,24 +550,24 @@ static void tc_ring(void) static char oversize[3000]; /* 超长入环的源(>= 钳位后的 2048B) */ uint32_t i; - printf("-- TC-CONLOG-020..027 注册后:生产者入环、写者发送 --\n"); + printf("-- TC-CONLOG-020..027 after attach: producers enter the ring, writer sends --\n"); reset_all(); attach_writer(); - CHECK(dbg_console_writer_ready() == 1U, "TC-CONLOG-020 attach 之后 writer_ready() == 1"); + CHECK(dbg_console_writer_ready() == 1U, "TC-CONLOG-020 writer_ready() == 1 after attach"); CHECK(s_con_writer_task == (TaskHandle_t)&s_task_writer, - "TC-CONLOG-021 attach 记下的正是调用方的任务句柄(自等保护的依据)"); - CHECK(dbg_console_write_line("netTask up\r\n", 12U) == 12, "TC-CONLOG-022 入环成功返回行长"); - CHECK(con_uart_tx_n == 0U, "TC-CONLOG-023 生产者绝不碰 UART"); - CHECK(used_now() == 12U, "TC-CONLOG-024 整行一次入环(used = 12)"); + "TC-CONLOG-021 attach records exactly the caller task handle (basis of the self-wait guard)"); + CHECK(dbg_console_write_line("netTask up\r\n", 12U) == 12, "TC-CONLOG-022 a successful ring entry returns the line length"); + CHECK(con_uart_tx_n == 0U, "TC-CONLOG-023 producers never touch the UART"); + CHECK(used_now() == 12U, "TC-CONLOG-024 the whole line enters the ring at once (used = 12)"); CHECK(con_evt_sets == 1U && con_evt_flags == SYS_EVT_CONSOLE_TX, - "TC-CONLOG-025 入环后置真实事件位 SYS_EVT_CONSOLE_TX(bit6)唤醒写者"); + "TC-CONLOG-025 after entering the ring the real event bit SYS_EVT_CONSOLE_TX (bit6) wakes the writer"); CHECK(con_crit_enter == 1U && con_crit_exit == 1U && con_crit_max_depth == 1U, - "TC-CONLOG-026 整行入环恰好一层临界区(行级原子的实现依据)"); + "TC-CONLOG-026 one line takes exactly one critical-section level (implementation basis of line atomicity)"); writer_drain(); - CHECK(tx_is(0U, "netTask up\r\n"), "TC-CONLOG-027 写者排空:上线字节与入环内容逐字节相同"); + CHECK(tx_is(0U, "netTask up\r\n"), "TC-CONLOG-027 writer drain: wire bytes are byte-identical to what entered the ring"); - printf("-- TC-CONLOG-028..032 分块 256B 与多生产者不交错 --\n"); + printf("-- TC-CONLOG-028..032 256B chunking and no interleaving between producers --\n"); reset_all(); attach_writer(); memset(line, 'C', sizeof(line)); @@ -575,12 +575,12 @@ static void tc_ring(void) (void)dbg_console_write_line(line, sizeof(line)); } writer_drain(); - CHECK(con_uart_block_n == 4U, "TC-CONLOG-028 960B 按 DBG_CON_CHUNK_MAX 切成 4 块"); + CHECK(con_uart_block_n == 4U, "TC-CONLOG-028 960B is split into 4 blocks by DBG_CON_CHUNK_MAX"); CHECK(con_uart_block_len[0] == 256U && con_uart_block_len[1] == 256U && con_uart_block_len[2] == 256U && con_uart_block_len[3] == 192U, - "TC-CONLOG-029 块长 256/256/256/192(单次发送上限真被执行)"); + "TC-CONLOG-029 block sizes 256/256/256/192 (the single-send ceiling is really enforced)"); CHECK(con_uart_tx_n == 960U && con_uart_txe_reads == 960U, - "TC-CONLOG-030 每字节恰好装载一次 DR(就绪即写,没有重复发)"); + "TC-CONLOG-030 every byte is loaded into DR exactly once (write when ready, no resend)"); reset_all(); attach_writer(); @@ -592,36 +592,36 @@ static void tc_ring(void) (void)dbg_console_write_line("[C] 33333\r\n", 11U); writer_drain(); CHECK(tx_is(0U, "[A] 11111\r\n[B] 22222\r\n[C] 33333\r\n"), - "TC-CONLOG-031 三个生产者各写一行 -> 上线仍是三行连续原文(G1 行级原子)"); + "TC-CONLOG-031 three producers, one line each -> the wire still shows three intact lines (G1 line atomicity)"); CHECK(strstr((const char *)&con_uart_tx[5], "[B] 22222") != 0, - "TC-CONLOG-032 行边界没被打断(没有任何一行被插进另一行中间)"); + "TC-CONLOG-032 no line boundary was broken (no line spliced into another)"); - printf("-- TC-CONLOG-033..037 跨环尾:两段拷贝 + 按连续段分块 --\n"); + printf("-- TC-CONLOG-033..037 ring wrap: two-segment copy + chunking by contiguous run --\n"); reset_all(); attach_writer(); s_con_head = 4090U; s_con_tail = 4090U; (void)dbg_console_write_line("0123456789ABCDEF", 16U); CHECK(s_con_head == 10U && used_now() == 16U, - "TC-CONLOG-033 跨环尾入环后 head 回绕到 10(4090+16-4096)"); + "TC-CONLOG-033 a wrapped line leaves head at 10 (4090+16-4096)"); CHECK(memcmp(&con_ring_store[4090], "012345", 6) == 0 && con_ring_store[0] == (uint8_t)'6' && con_ring_store[9] == (uint8_t)'F' && con_ring_store[10] == 0U, - "TC-CONLOG-034 回绕两段拷贝位置正确(前 6B 填到环尾、余 10B 从 0 起)"); + "TC-CONLOG-034 both wrap segments land correctly (first 6B fill to the ring end, remaining 10B start at 0)"); writer_drain(); CHECK(con_uart_block_n == 2U && con_uart_block_len[0] == 6U && con_uart_block_len[1] == 10U, - "TC-CONLOG-035 drain 按连续段分块:6 + 10(绝不把环中间当连续段读越界)"); - CHECK(tx_is(0U, "0123456789ABCDEF"), "TC-CONLOG-036 回绕行的上线内容与顺序都对"); + "TC-CONLOG-035 drain chunks by contiguous run: 6 + 10 (never reads past the ring end)"); + CHECK(tx_is(0U, "0123456789ABCDEF"), "TC-CONLOG-036 the wrapped line reaches the wire with correct content and order"); reset_all(); attach_writer(); /* 源缓冲必须给到 3000B:con_append 只钳长度,不会钳读取范围, * 拿 192B 的栈数组当 2048B 源就是越界读(插桩跑直接 0xC0000005) */ memset(oversize, 'D', sizeof(oversize)); - CHECK(con_append(oversize, (uint16_t)3000U) == 0, "TC-CONLOG-037 直调 con_append 超长仍成功"); + CHECK(con_append(oversize, (uint16_t)3000U) == 0, "TC-CONLOG-037 calling con_append directly with an over-long buffer still succeeds"); CHECK(used_now() == (DBG_CON_RING_SIZE / 2U), - "TC-CONLOG-038 入环长度钳在 RING/2=2048(保证任何一行都恒可入环)"); + "TC-CONLOG-038 ring entry length is clamped to RING/2=2048 (so any line can always eventually enter)"); } /* ============ TC-CONLOG-040..042 写者自记录 / 重入 / 未注册 drain ============ */ @@ -630,7 +630,7 @@ static void tc_writer_self(void) char line[DBG_CON_LINE_MAX]; uint32_t i; - printf("-- TC-CONLOG-040..042 写者自记录先排空 / drain 重入保护 --\n"); + printf("-- TC-CONLOG-040..042 writer self-logging drains first / drain re-entry guard --\n"); reset_all(); attach_writer(); memset(line, 'E', sizeof(line)); @@ -638,11 +638,11 @@ static void tc_writer_self(void) (void)dbg_console_write_line(line, sizeof(line)); } con_task_self = (TaskHandle_t)&s_task_writer; /* 写者自己记一行 */ - CHECK(dbg_console_write_line("F", 1U) == 1, "TC-CONLOG-040 写者自记录不丢行"); + CHECK(dbg_console_write_line("F", 1U) == 1, "TC-CONLOG-040 a writer logging about itself loses no line"); CHECK(con_delay_calls == 0U, - "TC-CONLOG-041 写者自记录先排空再入环:绝不进等待(否则就是等自己 -> 死锁)"); + "TC-CONLOG-041 writer self-logging drains before entering: it never waits (waiting for itself would deadlock)"); CHECK(con_uart_tx_n == 4032U && used_now() == 1U && dbg_console_dropped() == 0U, - "TC-CONLOG-042 老行全部上线、新行入环,零丢弃"); + "TC-CONLOG-042 old lines all reach the wire, the new line enters the ring, zero drops"); reset_all(); attach_writer(); @@ -650,56 +650,56 @@ static void tc_writer_self(void) s_con_draining = 1U; /* 模拟正处于 drain 之内 */ writer_drain(); CHECK(con_uart_tx_n == 0U && used_now() == 4U, - "TC-CONLOG-043 drain 重入保护:嵌套调用直接让位(写者自记录不触发递归 drain)"); + "TC-CONLOG-043 drain re-entry guard: a nested call yields at once (writer self-logging causes no recursive drain)"); reset_all(); (void)dbg_console_write_line("early\r\n", 7U); /* 未注册 -> 已直发 */ con_uart_tx_n = 0U; writer_drain(); - CHECK(con_uart_tx_n == 0U, "TC-CONLOG-044 写者未注册时 drain 不做任何事"); + CHECK(con_uart_tx_n == 0U, "TC-CONLOG-044 with no writer attached drain does nothing"); } /* ============ TC-CONLOG-050..062 缓冲满 / 丢弃 / 可观测 ============ */ static void tc_full_drop(void) { - printf("-- TC-CONLOG-050..053 缓冲满:DWT 硬上界丢弃且不覆盖旧行 --\n"); + printf("-- TC-CONLOG-050..053 ring full: DWT hard ceiling drops without overwriting old lines --\n"); reset_all(); attach_writer(); fill_ring(10U); /* 只剩 10B 可用 */ con_dwt_step = SystemCoreClock / 4U; /* 一次判据 = 250ms > 200ms 上界 */ CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == -1, - "TC-CONLOG-050 环满 + DWT 上界到 -> 丢这一行(返回 -1)"); - CHECK(dbg_console_dropped() == 1U, "TC-CONLOG-051 丢弃计数 +1"); + "TC-CONLOG-050 ring full + DWT ceiling reached -> this line is dropped (returns -1)"); + CHECK(dbg_console_dropped() == 1U, "TC-CONLOG-051 drop counter +1"); CHECK(con_delay_calls == 0U, - "TC-CONLOG-052 有 DWT 时用周期判据,不必先烧 200 个 tick(K4)"); + "TC-CONLOG-052 with DWT the cycle criterion is used, no need to burn 200 ticks first (K4)"); CHECK(s_con_head == 4085U && used_now() == 4085U && con_uart_tx_n == 0U, - "TC-CONLOG-053 丢弃不推进 head、也不生产者自己发(没半写、更没覆盖未发出的旧行)"); + "TC-CONLOG-053 a drop does not advance head and the producer does not send itself (no half write, no overwrite of pending lines)"); - printf("-- TC-CONLOG-054..056 等待中出现空间 -> 入环成功 --\n"); + printf("-- TC-CONLOG-054..056 space appears while waiting -> line enters the ring --\n"); reset_all(); attach_writer(); fill_ring(10U); con_dwt_step = 0U; /* 时间静止:只能靠写者腾地方 */ con_delay_hook = bg_drain_hook; /* 每等一轮 = 写者排空一次 */ CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == 20, - "TC-CONLOG-054 等一轮后腾出空间 -> 整行入环成功"); + "TC-CONLOG-054 after one wait round space frees up -> the whole line enters the ring"); CHECK(con_delay_calls == 1U && dbg_console_dropped() == 0U, - "TC-CONLOG-055 只等了一轮,且不算丢弃"); + "TC-CONLOG-055 only one wait round happened and it is not counted as a drop"); con_delay_hook = 0; - CHECK(con_uart_tx_n == 4085U, "TC-CONLOG-056 腾地方的正是写者(老行已上线)"); + CHECK(con_uart_tx_n == 4085U, "TC-CONLOG-056 it really was the writer that freed the space (old lines reached the wire)"); - printf("-- TC-CONLOG-057..059 无 DWT 支路:等待上界 = DBG_CON_WAIT_MS 个 tick --\n"); + printf("-- TC-CONLOG-057..059 no-DWT branch: wait ceiling = DBG_CON_WAIT_MS ticks --\n"); reset_all(); con_dwt_ctrl_sticky = 0; /* 调试单元被关 -> DWT 使能失败 */ attach_writer(); fill_ring(10U); - CHECK(s_con_dwt_up == 0U, "TC-CONLOG-057 DWT 使能失败时 s_con_dwt_up 保持 0"); + CHECK(s_con_dwt_up == 0U, "TC-CONLOG-057 s_con_dwt_up stays 0 when DWT enable fails"); CHECK(dbg_console_write_line("0123456789ABCDEFGHIJ", 20U) == -1, - "TC-CONLOG-058 无 DWT 时仍会丢(判据退回 tick 计数,不会无限等)"); + "TC-CONLOG-058 without DWT a drop still happens (criterion falls back to tick counting, never waits forever)"); CHECK(con_delay_calls == DBG_CON_WAIT_MS && dbg_console_dropped() == 1U, - "TC-CONLOG-059 无 DWT 的等待上界恰为 DBG_CON_WAIT_MS(200) 次 vTaskDelay"); + "TC-CONLOG-059 the no-DWT wait ceiling is exactly DBG_CON_WAIT_MS(200) vTaskDelay calls"); - printf("-- TC-CONLOG-060..062 丢弃可观测:[ERR] 行直发不经环 --\n"); + printf("-- TC-CONLOG-060..062 drop observability: the [ERR] line is sent raw, not through the ring --\n"); reset_all(); attach_writer(); fill_ring(10U); @@ -708,10 +708,10 @@ static void tc_full_drop(void) con_dwt_step = 0U; writer_drain(); CHECK(tx_contains("[ERR] CONSOLE dropped 1 lines"), - "TC-CONLOG-060 排空末尾刷出 [ERR] CONSOLE dropped 1 lines(丢行永不静默,@test 判 FAIL)"); - CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-061 刷完即清零(下一轮不重复告警)"); + "TC-CONLOG-060 the drain tail flushes [ERR] CONSOLE dropped 1 lines (loss is never silent, @test marks FAIL)"); + CHECK(dbg_console_dropped() == 0U, "TC-CONLOG-061 the counter is cleared once flushed (next round does not repeat the warning)"); CHECK(used_now() == 0U, - "TC-CONLOG-062 告警行走裸发:不入环 -> 不会 con_append->drain 递归"); + "TC-CONLOG-062 the warning goes out raw: not through the ring -> no con_append->drain recursion"); } /* ============ TC-CONLOG-070..081 TX 僵尸通道:自愈 / 冷却 / 半块 ============ */ @@ -729,21 +729,21 @@ static void fault_setup(uint32_t tick0, int repair) static void tc_tx_fault(void) { - printf("-- TC-CONLOG-070..074 软复位没治好:故障即止 + 留痕只能靠标志 --\n"); + printf("-- TC-CONLOG-070..074 soft reset did not heal: stop at the fault + trace only via flag --\n"); reset_all(); fault_setup(5000U, 0); - CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-070 发送超时 -> tx_fault 计数 +1"); + CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-070 send timeout -> tx_fault counter +1"); CHECK(s_mx_init_calls == 1U && con_nvic_clears == 1U && con_rcc_disables == 1U && con_rcc_enables == 1U, - "TC-CONLOG-071 自愈完整一遍:清 pending -> 断时钟 -> 供时钟 -> 重初始化"); + "TC-CONLOG-071 full self-heal sequence: clear pending -> gate off clock -> gate on clock -> re-init"); CHECK(used_now() == (DBG_CON_RING_SIZE - 1U) - DBG_CON_CHUNK_MAX, - "TC-CONLOG-072 故障即止:只丢当前 256B 块,其余留环里等下轮(让位喂狗)"); + "TC-CONLOG-072 stop at the fault: only the current 256B block is lost, the rest waits in the ring (yields to the watchdog)"); CHECK(con_uart_tx_n == 0U, - "TC-CONLOG-073 通道没治好时连那条 WARN 也上不了线(裸发同样有界、不重试)"); + "TC-CONLOG-073 while the channel is unhealed even that WARN cannot get out (raw send is bounded too, no retry)"); CHECK(dbg_console_tx_fault_log_take() == 1U && dbg_console_tx_fault_log_take() == 0U, - "TC-CONLOG-074 留痕标志读后清:写者据此在 drain 之外补记 run.log(断调用图成环)"); + "TC-CONLOG-074 the trace flag is read-then-clear: the writer uses it to append run.log outside drain (breaks the call graph cycle)"); - printf("-- TC-CONLOG-075..077 软复位治好了:WARN 上线且只一次 --\n"); + printf("-- TC-CONLOG-075..077 soft reset healed: the WARN goes out exactly once --\n"); reset_all(); con_tick_now = 7000U; attach_writer(); @@ -752,14 +752,14 @@ static void tc_tx_fault(void) con_dwt_step = SystemCoreClock / 4U; s_mx_repair = 1; /* 重初始化把通道治好 */ writer_drain(); - CHECK(tx_contains("[WARN] CONSOLE tx fault"), "TC-CONLOG-075 首次故障的 WARN 行成功上线"); + CHECK(tx_contains("[WARN] CONSOLE tx fault"), "TC-CONLOG-075 the first fault WARN line reaches the wire"); CHECK(used_now() == 0U && s_con_cool_until == 7000U && dbg_console_tx_fault() == 1U, - "TC-CONLOG-076 故障块丢弃(行已出环不可重放)+ 冷却截止记为当时 tick"); + "TC-CONLOG-076 the faulted block is dropped (already out of the ring, not replayable) + cooldown deadline is the tick then"); con_uart_txe_ready_left = -1; con_dwt_step = 0U; writer_drain(); /* tick 未推进 -> 仍在冷却 */ CHECK(s_con_cool_until == 7000U && s_mx_init_calls == 1U, - "TC-CONLOG-077 冷却期内 drain 直接让位:不重试、不自愈第二次"); + "TC-CONLOG-077 during cooldown drain yields at once: no retry, no second self-heal"); } static void tc_tx_cool(void) @@ -768,36 +768,36 @@ static void tc_tx_cool(void) reset_all(); fault_setup(7000U, 1); /* 治好的那次故障 */ CHECK(s_con_cool_until == 7000U && used_now() == 3839U, - "TC-CONLOG-078 故障后剩 3839B 在环里(本块被丢)"); + "TC-CONLOG-078 3839B remain in the ring after the fault (this block was dropped)"); con_dwt_step = 0U; con_uart_txe_stuck = 0; con_tick_now = 7299U; /* 差 1ms */ writer_drain(); CHECK(s_con_cool_until == 7000U && used_now() == 3839U && s_mx_init_calls == 1U, - "TC-CONLOG-079 差 1ms 仍在冷却:环不动、不重复自愈(冷却即节奏保护)"); + "TC-CONLOG-079 1ms short of the deadline: the ring does not move and no repeated self-heal (cooldown is the pacing guard)"); con_tick_now = 7300U; /* 到点 */ writer_drain(); CHECK(s_con_cooling == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U, - "TC-CONLOG-080 到点即清冷却标志、放行本轮排空,且没有新增故障" - "(cool_until 只存时刻,不再被拿来当哨兵清零)"); + "TC-CONLOG-080 at the deadline the flag clears, this round drains, and no new fault appears" + " (cool_until only stores a deadline and is no longer used as a clear-on-zero sentinel)"); /* 修复后的正向断言:冷却与否看独立标志,不再拿 tick 值当哨兵 */ reset_all(); fault_setup(0U, 0); /* 故障恰好落在 HAL_GetTick()==0 那一拍 */ CHECK(s_con_cooling == 1U && dbg_console_tx_fault() == 1U, - "TC-CONLOG-081 tick==0 那拍的故障照样进入冷却(独立标志,不撞 0 哨兵)"); + "TC-CONLOG-081 a fault in the tick==0 round still enters cooldown (own flag, no zero sentinel collision)"); writer_drain(); /* tick 仍是 0 */ CHECK(dbg_console_tx_fault() == 1U && s_mx_init_calls == 1U && used_now() == (DBG_CON_RING_SIZE - 1U) - DBG_CON_CHUNK_MAX, - "TC-CONLOG-082 同一 tick 上再来一轮被冷却挡住:不再白耗一次块超时"); + "TC-CONLOG-082 another round on the same tick is blocked by cooldown: no second wasted block timeout"); con_tick_now = DBG_CON_FAULT_COOL_MS - 1U; writer_drain(); - CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-083 冷却 299ms 仍未到点"); + CHECK(dbg_console_tx_fault() == 1U, "TC-CONLOG-083 299ms is still inside the cooldown"); con_uart_txe_stuck = 0; con_tick_now = DBG_CON_FAULT_COOL_MS; /* 到点(且回绕起点正好是 0) */ writer_drain(); CHECK(s_con_cooling == 0U && used_now() == 0U && dbg_console_tx_fault() == 1U, - "TC-CONLOG-084 到点后清冷却并放行:全程没有第二次故障"); + "TC-CONLOG-084 after the deadline the cooldown clears and passes through: no second fault anywhere"); reset_all(); attach_writer(); @@ -816,16 +816,16 @@ static void tc_tx_cool(void) static void tc_tx_half(void) { - printf("-- TC-CONLOG-085..088 超时判据的真实后果:半块 / 已上线仍判失败 --\n"); + printf("-- TC-CONLOG-085..088 real consequences of the timeout criterion: half block / already sent yet failed --\n"); reset_all(); attach_writer(); (void)dbg_console_write_line("ABCDEFGH", 8U); con_uart_tc_stuck = 1; /* 字节都进了 DR,只有 TC 不置位 */ con_dwt_step = SystemCoreClock / 4U; writer_drain(); - CHECK(tx_is(0U, "ABCDEFGH"), "TC-CONLOG-085 TC 卡死:8 字节都已上线"); + CHECK(tx_is(0U, "ABCDEFGH"), "TC-CONLOG-085 TC stuck: all 8 bytes were handed to the shifter"); CHECK(dbg_console_tx_fault() == 1U && con_uart_block_n == 0U, - "TC-CONLOG-086 已上线仍整块判失败(docs §13-O2:丢弃/故障两本账没分开的现状)"); + "TC-CONLOG-086 already on the wire but the whole block still counts as failed (docs section 13-O2: drop and fault books were not separated)"); reset_all(); attach_writer(); @@ -833,50 +833,50 @@ static void tc_tx_half(void) con_uart_txe_ready_left = 3; /* 只挤得出 3 个字节 */ con_dwt_step = SystemCoreClock / 4U; writer_drain(); - CHECK(tx_is(0U, "ABC"), "TC-CONLOG-087 TXE 中途卡死 -> 只有已写进 DR 的 3 字节上线"); + CHECK(tx_is(0U, "ABC"), "TC-CONLOG-087 TXE stuck mid-block -> only the 3 bytes already written to DR reach the wire"); CHECK(dbg_console_tx_fault() == 1U && used_now() == 0U, - "TC-CONLOG-088 半块同样走自愈分支,整块出环(尾部 5 字节随块作废)"); + "TC-CONLOG-088 a half block takes the same self-heal branch and leaves the ring (trailing 5 bytes die with the block)"); } /* ============ TC-CONLOG-090..099 DWT 使能 / 指令计数上界 ============ */ static void tc_dwt(void) { - printf("-- TC-CONLOG-090..093 DWT 惰性使能与就绪即走 --\n"); + printf("-- TC-CONLOG-090..093 lazy DWT enable and skip-when-ready --\n"); reset_all(); (void)dbg_console_write_line("ok\r\n", 4U); /* 走一次直发 -> 顺带使能 DWT */ - CHECK(s_con_dwt_up == 1U, "TC-CONLOG-090 con_dwt_ensure 成功使能后置 s_con_dwt_up"); + CHECK(s_con_dwt_up == 1U, "TC-CONLOG-090 con_dwt_ensure sets s_con_dwt_up after a successful enable"); CHECK((g_con_core_debug.DEMCR & CoreDebug_DEMCR_TRCENA_Msk) != 0U, - "TC-CONLOG-091 真的写了 DEMCR.TRCENA(位号与 core_cm4.h 一致)"); + "TC-CONLOG-091 DEMCR.TRCENA really gets written (bit number matches core_cm4.h)"); CHECK((g_con_dwt.CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0U, - "TC-CONLOG-092 真的写了 DWT.CTRL.CYCCNTENA"); + "TC-CONLOG-092 DWT.CTRL.CYCCNTENA really gets written"); CHECK(tx_is(0U, "ok\r\n") && con_dwt_reads == 4U, - "TC-CONLOG-093 通道就绪时发送循环没再读 CYCCNT(3 次 ensure + 1 次取基准 t0)"); + "TC-CONLOG-093 with the channel ready the send loop reads no further CYCCNT (3 from ensure + 1 for t0)"); - printf("-- TC-CONLOG-094..097 DWT 使能失败 -> con_tx_counted 计数上界 --\n"); + printf("-- TC-CONLOG-094..097 DWT enable failure -> con_tx_counted instruction ceiling --\n"); reset_all(); con_dwt_ctrl_sticky = 0; /* CYCCNTENA 写不进去 */ con_uart_txe_stuck = 1; dbg_console_write_raw("Q", 1U); - CHECK(s_con_dwt_up == 0U, "TC-CONLOG-094 DWT 使能失败 -> 走计数上界支路"); + CHECK(s_con_dwt_up == 0U, "TC-CONLOG-094 DWT enable failure -> the counted-guard branch is taken"); CHECK(con_uart_txe_reads == CON_TX_GUARD_MAX, - "TC-CONLOG-095 每字节最多自旋 CON_TX_GUARD_MAX(20000) 次即放弃(有界,非死等)"); - CHECK(con_uart_tx_n == 0U, "TC-CONLOG-096 计数上界内没发出去就整块放弃"); + "TC-CONLOG-095 each byte spins at most CON_TX_GUARD_MAX(20000) times then gives up (bounded, not a dead wait)"); + CHECK(con_uart_tx_n == 0U, "TC-CONLOG-096 if the block is not out within the counted ceiling the whole block is abandoned"); CHECK(con_dwt_reads == 3U, - "TC-CONLOG-097 计数支路一次都不读 CYCCNT(不依赖 DWT,也就不依赖时间源)"); + "TC-CONLOG-097 the counted branch never reads CYCCNT (independent of DWT, hence of any time source)"); - printf("-- TC-CONLOG-098..099 超时判据不依赖 HAL_GetTick(round 8b 的动机) --\n"); + printf("-- TC-CONLOG-098..099 the timeout criterion does not use HAL_GetTick (round 8b motivation) --\n"); reset_all(); con_uart_txe_stuck = 1; /* tick 冻结在 0,模拟 TIM7 停摆 */ con_dwt_step = SystemCoreClock / 10U; /* 一次判据正好 = 100ms 上界 */ dbg_console_write_raw("Q", 1U); CHECK(con_tick_reads == 0U && con_uart_tx_n == 0U, - "TC-CONLOG-098 tick 完全不走也能退出:裸发一次都没读 HAL_GetTick"); + "TC-CONLOG-098 it exits even when tick never advances: one raw send reads HAL_GetTick zero times"); reset_all(); con_uart_txe_stuck = 1; con_dwt_step = SystemCoreClock / 4U; dbg_console_write_raw("Z", 1U); CHECK(con_uart_txe_reads == 1U, - "TC-CONLOG-099 DWT 支路第一次自旋就判超时(上界真的起作用,不会拖满 20000 次)"); + "TC-CONLOG-099 the DWT branch declares timeout on the first spin (the ceiling really works, never burns 20000)"); } /* ============ TC-CONLOG-100..106 huart1.Instance == NULL 短路 + 未初始化分诊 ============ */ @@ -887,14 +887,14 @@ static void tc_null_uart(void) huart1.Instance = 0; dbg_console_write_raw("boom", 5U); CHECK(con_uart_tx_n == 0U && con_uart_txe_reads == 0U, - "TC-CONLOG-100 Instance==NULL 时一次寄存器都不碰(不解引用地址 0)"); + "TC-CONLOG-100 with Instance==NULL not one register is touched (no dereference of address 0)"); CHECK(con_dwt_reads == 3U, - "TC-CONLOG-101 仍然先惰性使能 DWT 再短路(ensure 在判空之前,顺序照旧)"); + "TC-CONLOG-101 DWT is still lazily enabled before the short circuit (ensure stays ahead of the NULL check)"); reset_all(); huart1.Instance = 0; CHECK(dbg_console_write_line("early", 5U) == 5, - "TC-CONLOG-102 未注册路径(内核前)撞上无串口也不崩、仍返回行长"); + "TC-CONLOG-102 the unattached path (pre-kernel) survives a missing UART and still returns the line length"); /* 修复后的正向断言:未初始化 != 僵尸通道,绝不重配外设 */ reset_all(); @@ -920,26 +920,26 @@ static void tc_null_uart(void) /* ============ TC-CONLOG-110..115 fputc 重定向 ============ */ static void tc_fputc(void) { - printf("-- TC-CONLOG-110..115 fputc 重定向(printf 遗留路径) --\n"); + printf("-- TC-CONLOG-110..115 fputc retarget (legacy printf path) --\n"); reset_all(); - CHECK(con_fputc_under_test('Q', stdout) == 'Q', "TC-CONLOG-110 fputc 原样返回字符(C 库约定)"); + CHECK(con_fputc_under_test('Q', stdout) == 'Q', "TC-CONLOG-110 fputc returns the character unchanged (C library convention)"); CHECK(tx_is(0U, "Q") && used_now() == 0U, - "TC-CONLOG-111 未注册写者:printf 的字节直发(与旧行为一致)"); + "TC-CONLOG-111 writer not attached: printf bytes go out directly (same as the old behaviour)"); reset_all(); attach_writer(); (void)con_fputc_under_test('R', stdout); CHECK(con_uart_tx_n == 0U && used_now() == 1U && con_ring_store[0] == (uint8_t)'R', - "TC-CONLOG-112 注册后 fputc 也收敛到单写者路径(逐字节入环)"); + "TC-CONLOG-112 after attach fputc also funnels into the single-writer path (byte by byte into the ring)"); writer_drain(); - CHECK(tx_is(0U, "R"), "TC-CONLOG-113 写者把 printf 字节发出"); + CHECK(tx_is(0U, "R"), "TC-CONLOG-113 the writer sends those printf bytes"); reset_all(); attach_writer(); con_ipsr = 12U; /* 假异常号:任意非 0 */ - CHECK(dbg_console_write_line("ISR", 3U) == 3, "TC-CONLOG-114 中断上下文写整行返回行长"); + CHECK(dbg_console_write_line("ISR", 3U) == 3, "TC-CONLOG-114 writing a whole line from interrupt context returns the line length"); CHECK(tx_is(0U, "ISR") && used_now() == 0U && con_delay_calls == 0U, - "TC-CONLOG-115 中断上下文一律裸发:不入环、不等待(否则在 ISR 里睡死)"); + "TC-CONLOG-115 interrupt context always sends raw: no ring entry, no waiting (else it sleeps inside the ISR)"); reset_all(); attach_writer(); @@ -947,59 +947,59 @@ static void tc_fputc(void) con_ipsr = 5U; con_dwt_step = SystemCoreClock / 4U; CHECK(dbg_console_write_line("0123456789", 10U) == 10, - "TC-CONLOG-116 环全满 + 中断上下文:仍直发成功(ISR 绝不进丢弃等待)"); + "TC-CONLOG-116 ring completely full + interrupt context: still sends directly (an ISR never enters the drop wait)"); CHECK(dbg_console_dropped() == 0U && con_delay_calls == 0U, - "TC-CONLOG-117 这条路径既不丢行也不记丢弃(K1:内核前/ISR 不依赖任何内核对象)"); + "TC-CONLOG-117 this path loses no line and records no drop (K1: pre-kernel and ISR depend on no kernel object)"); } /* ============ TC-CONLOG-120..122 drain 250ms 预算 ============ */ static void tc_budget(void) { - printf("-- TC-CONLOG-120..122 单次 drain 预算 250ms(让位喂狗轮询) --\n"); + printf("-- TC-CONLOG-120..122 single drain budget 250ms (yields to the watchdog poll) --\n"); reset_all(); attach_writer(); fill_ring(0U); /* 4095B = 16 块 */ con_tick_step = 130U; /* 每次读 tick 走 130ms */ writer_drain(); - CHECK(con_uart_block_n == 2U, "TC-CONLOG-120 一轮只发 2 块就因预算让位(不是发完)"); + CHECK(con_uart_block_n == 2U, "TC-CONLOG-120 one round sends only 2 blocks then yields on budget (not because it finished)"); CHECK(used_now() == (DBG_CON_RING_SIZE - 1U) - (2U * DBG_CON_CHUNK_MAX), - "TC-CONLOG-121 让位时余下 3583B 留在环里(下轮续排,不丢)"); + "TC-CONLOG-121 when it yields, 3583B remain in the ring (next round continues, nothing lost)"); con_tick_step = 0U; writer_drain(); /* 预算内发完余下 3583B */ CHECK(used_now() == 0U && con_uart_tx_n == (DBG_CON_RING_SIZE - 1U), - "TC-CONLOG-122 下轮把余下的 3583B 也发完:预算让位不丢字节(累计 4095)"); + "TC-CONLOG-122 the next round sends the remaining 3583B too: yielding on budget loses no bytes (4095 total)"); } /* ============ TC-CONLOG-130..135 计数支路的成功路径 / TC 抖动 / 直调 NULL ============ */ static void tc_counted_paths(void) { - printf("-- TC-CONLOG-130..132 DWT 不可用 -> con_tx_counted 也能发全(不只会超时) --\n"); + printf("-- TC-CONLOG-130..132 DWT unavailable -> con_tx_counted can also finish a block (not only time out) --\n"); reset_all(); con_dwt_ctrl_sticky = 0; /* CYCCNTENA 落不住 */ con_tick_now = 9000U; /* tick 冻结:证明与本支路无关 */ dbg_console_write_raw("AB", 2U); CHECK(s_con_dwt_up == 0U && tx_is(0U, "AB"), - "TC-CONLOG-130 计数支路把整块发完并返回成功(回退路径没退化)"); + "TC-CONLOG-130 the counted branch sends the whole block and returns success (fallback path did not regress)"); CHECK(con_uart_tc_reads == 1U && con_dwt_reads == 3U && con_tick_reads == 0U, - "TC-CONLOG-131 计数支路既不等 tick 也不再读 CYCCNT(零外部时间源依赖)"); + "TC-CONLOG-131 the counted branch waits no tick and reads no CYCCNT (zero external time source)"); reset_all(); con_dwt_ctrl_sticky = 0; con_uart_tc_stuck = 1; /* 字节能进 DR,TC 永不置位 */ dbg_console_write_raw("CD", 2U); CHECK(tx_is(0U, "CD") && con_uart_tc_reads == CON_TX_GUARD_MAX, - "TC-CONLOG-132 TC 卡死时计数支路同样最多自旋 CON_TX_GUARD_MAX 次即放弃"); + "TC-CONLOG-132 with TC stuck the counted branch also gives up after at most CON_TX_GUARD_MAX spins"); - printf("-- TC-CONLOG-133..135 DWT 支路 TC 抖动 / con_append 直调参数校验 --\n"); + printf("-- TC-CONLOG-133..135 DWT branch TC jitter / con_append direct-call argument checks --\n"); reset_all(); con_dwt_step = 1U; /* 时间走得极慢:不会误判超时 */ con_uart_tc_after = 2; /* 前两轮 TC 不就绪 */ dbg_console_write_raw("EF", 2U); CHECK(tx_is(0U, "EF") && con_uart_tc_reads == 3U, - "TC-CONLOG-133 TC 抖动(未超上界)后仍算整块成功:TC 循环能正常退出"); + "TC-CONLOG-133 TC jitter (below the ceiling) still ends as a successful block: the TC loop exits normally"); reset_all(); - CHECK(con_append(0, 3U) == -1, "TC-CONLOG-134 con_append(NULL) 自己也要挡住(不只靠上层)"); - CHECK(con_append("abc", 0U) == -1, "TC-CONLOG-135 con_append(len=0) 同样拒绝"); + CHECK(con_append(0, 3U) == -1, "TC-CONLOG-134 con_append(NULL) must block it itself (not only relying on the caller)"); + CHECK(con_append("abc", 0U) == -1, "TC-CONLOG-135 con_append(len=0) is rejected the same way"); } diff --git a/test/host/ftp_auth/ftp_auth_test.c b/test/host/ftp_auth/ftp_auth_test.c index 0f91f267..69f44c9b 100644 --- a/test/host/ftp_auth/ftp_auth_test.c +++ b/test/host/ftp_auth/ftp_auth_test.c @@ -58,11 +58,11 @@ static const ftp_auth_creds_t s_creds = { "admin", DEF_HASH, 1u }; static void tc_user_match(void) { - CHECK(ftp_auth_user_match("admin", "admin") == 1, "TC-FTPAUTH-001a 相等匹配"); - CHECK(ftp_auth_user_match("admin", "ADMIN") == 1, "TC-FTPAUTH-001b 大小写不敏感"); - CHECK(ftp_auth_user_match("admin", "root") == 0, "TC-FTPAUTH-001c 不等拒绝"); - CHECK(ftp_auth_user_match("admin", "admin2") == 0, "TC-FTPAUTH-001d 前缀不算匹配"); - CHECK(ftp_auth_user_match("admin", NULL) == 0, "TC-FTPAUTH-001e NULL 拒绝"); + CHECK(ftp_auth_user_match("admin", "admin") == 1, "TC-FTPAUTH-001a equal strings match"); + CHECK(ftp_auth_user_match("admin", "ADMIN") == 1, "TC-FTPAUTH-001b case-insensitive"); + CHECK(ftp_auth_user_match("admin", "root") == 0, "TC-FTPAUTH-001c mismatch rejected"); + CHECK(ftp_auth_user_match("admin", "admin2") == 0, "TC-FTPAUTH-001d prefix does not count as match"); + CHECK(ftp_auth_user_match("admin", NULL) == 0, "TC-FTPAUTH-001e NULL rejected"); } /* ===================== TC-FTPAUTH-002..004:基本判定 ===================== */ @@ -73,17 +73,17 @@ static void tc_basic(void) (void)memset(&st, 0, sizeof(st)); CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 1000u) == FTP_AUTH_OK, - "TC-FTPAUTH-002 对用户名+对口令 → OK"); - CHECK(st.fail_count == 0u, "TC-FTPAUTH-002b 成功后计数为 0"); + "TC-FTPAUTH-002 right user + right password -> OK"); + CHECK(st.fail_count == 0u, "TC-FTPAUTH-002b counter is 0 after success"); (void)memset(&st, 0, sizeof(st)); CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, 1000u) == FTP_AUTH_FAIL, - "TC-FTPAUTH-003 错口令 → FAIL"); - CHECK(st.fail_count == 1u, "TC-FTPAUTH-003b 失败计数 1"); + "TC-FTPAUTH-003 wrong password -> FAIL"); + CHECK(st.fail_count == 1u, "TC-FTPAUTH-003b fail count = 1"); (void)memset(&st, 0, sizeof(st)); CHECK(ftp_auth_check(&s_creds, 0u, "admin", &st, 1000u) == FTP_AUTH_FAIL, - "TC-FTPAUTH-004 错用户名+对口令 → FAIL(哈希路径已走)"); + "TC-FTPAUTH-004 wrong user + right password -> FAIL (hash path taken)"); } /* ===================== TC-FTPAUTH-005..006:锁定与解锁 ===================== */ @@ -96,22 +96,22 @@ static void tc_lock(void) (void)memset(&st, 0, sizeof(st)); for (int i = 0; i < 5; i++) { CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL, - "TC-FTPAUTH-005 第 1..5 次失败 → FAIL"); + "TC-FTPAUTH-005 attempts 1..5 -> FAIL"); } - CHECK(st.fail_count == 5u, "TC-FTPAUTH-005b 计数到达 FAIL_MAX"); + CHECK(st.fail_count == 5u, "TC-FTPAUTH-005b counter reaches FAIL_MAX"); /* 锁定期内:对口令也 LOCKED,且不再计数 */ CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t + 1u) == FTP_AUTH_LOCKED, - "TC-FTPAUTH-005c 锁定期内对口令 → LOCKED"); - CHECK(st.fail_count == 5u, "TC-FTPAUTH-005d LOCKED 不改变计数"); + "TC-FTPAUTH-005c right password during lock window -> LOCKED"); + CHECK(st.fail_count == 5u, "TC-FTPAUTH-005d LOCKED does not change the counter"); /* 59999 tick:仍在窗内(差值 < 60000) */ CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, st.lock_until_tick - 1u) == FTP_AUTH_LOCKED, - "TC-FTPAUTH-005e 窗内最后 1 tick 仍 LOCKED"); + "TC-FTPAUTH-005e last tick inside window still LOCKED"); /* 期满:恰好到达 lock_until → 放行 */ CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, st.lock_until_tick) == FTP_AUTH_OK, - "TC-FTPAUTH-006 期满对口令 → OK"); - CHECK(st.fail_count == 0u, "TC-FTPAUTH-006b 解锁成功后计数清零"); + "TC-FTPAUTH-006 right password after lock expiry -> OK"); + CHECK(st.fail_count == 0u, "TC-FTPAUTH-006b counter cleared after unlock then success"); } /* ===================== TC-FTPAUTH-007:成功清零计数 ===================== */ @@ -126,16 +126,16 @@ static void tc_reset(void) (void)ftp_auth_check(&s_creds, 1u, "wrong", &st, t); } CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t) == FTP_AUTH_OK, - "TC-FTPAUTH-007a 3 败后成功 → OK"); - CHECK(st.fail_count == 0u, "TC-FTPAUTH-007b 成功清零"); + "TC-FTPAUTH-007a success after 3 failures -> OK"); + CHECK(st.fail_count == 0u, "TC-FTPAUTH-007b success clears the counter"); for (int i = 0; i < 4; i++) { CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL, - "TC-FTPAUTH-007c 清零后再 4 败仍 FAIL(未锁)"); + "TC-FTPAUTH-007c after clear, 4 more failures still FAIL (not locked)"); } CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL, - "TC-FTPAUTH-007d 第 5 败 FAIL 并上锁"); + "TC-FTPAUTH-007d 5th failure FAILs and arms the lock"); CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t + 1u) == FTP_AUTH_LOCKED, - "TC-FTPAUTH-007e 连续 5 败后 LOCKED"); + "TC-FTPAUTH-007e LOCKED after 5 consecutive failures"); } /* ===================== TC-FTPAUTH-008:tick 32 位回绕 ===================== */ @@ -150,10 +150,10 @@ static void tc_wraparound(void) st.lock_until_tick = 0xFFFFFFF0u; /* 回绕前 240 tick:仍在窗内 */ CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 0xFFFFFF00u) == FTP_AUTH_LOCKED, - "TC-FTPAUTH-008a 回绕前窗内 → LOCKED"); + "TC-FTPAUTH-008a inside window before wraparound -> LOCKED"); /* 回绕后 32 tick:now=0x10,(int32_t)(0x10-0xFFFFFFF0)=+32 → 期满 */ CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 0x00000010u) == FTP_AUTH_OK, - "TC-FTPAUTH-008b 回绕后期满 → OK(有符号差抗回绕)"); + "TC-FTPAUTH-008b after wraparound window elapsed -> OK (signed diff resists wraparound)"); } /* ===================== TC-FTPAUTH-009..010:fail-closed 边界 ===================== */ @@ -165,17 +165,17 @@ static void tc_fail_closed(void) (void)memset(&st, 0, sizeof(st)); CHECK(ftp_auth_check(&bad_creds, 1u, "admin", &st, 1000u) == FTP_AUTH_FAIL, - "TC-FTPAUTH-009 非法 hex 配置 → 恒 FAIL(无人能登录,fail-closed)"); + "TC-FTPAUTH-009 invalid hex config -> always FAIL (nobody can log in, fail-closed)"); (void)memset(&st, 0, sizeof(st)); CHECK(ftp_auth_check(&s_creds, 1u, "", &st, 1000u) == FTP_AUTH_FAIL, - "TC-FTPAUTH-010a 空口令 → FAIL"); + "TC-FTPAUTH-010a empty password -> FAIL"); CHECK(ftp_auth_check(&s_creds, 1u, NULL, &st, 1000u) == FTP_AUTH_FAIL, - "TC-FTPAUTH-010b NULL 口令 → FAIL"); + "TC-FTPAUTH-010b NULL password -> FAIL"); CHECK(ftp_auth_check(NULL, 1u, "admin", &st, 1000u) == FTP_AUTH_FAIL, - "TC-FTPAUTH-010c NULL creds → FAIL"); + "TC-FTPAUTH-010c NULL creds -> FAIL"); CHECK(ftp_auth_check(&s_creds, 1u, "admin", NULL, 1000u) == FTP_AUTH_FAIL, - "TC-FTPAUTH-010d NULL state → FAIL"); + "TC-FTPAUTH-010d NULL state -> FAIL"); } /* ===================== main ===================== */ diff --git a/test/host/ftp_cfg/ftp_cfg_test.c b/test/host/ftp_cfg/ftp_cfg_test.c index 57d369ea..0e4277bc 100644 --- a/test/host/ftp_cfg/ftp_cfg_test.c +++ b/test/host/ftp_cfg/ftp_cfg_test.c @@ -148,9 +148,9 @@ static void tc_register(void) printf("-- TC-FTPCFG-003..005 register --\n"); fake_reset(); ftp_cfg_register(); - CHECK(g_reg_n == 1, "TC-FTPCFG-003a 首次注册一次"); + CHECK(g_reg_n == 1, "TC-FTPCFG-003a first register adds exactly one entry"); ftp_cfg_register(); - CHECK(g_reg_n == 1, "TC-FTPCFG-003 重复 register 不重复登记(幂等)"); + CHECK(g_reg_n == 1, "TC-FTPCFG-003 repeated register never duplicates the entry (idempotent)"); CHECK((strcmp(g_reg.name, "ftp") == 0) && (g_reg.data_size == sizeof(ftp_cfg_t)) && @@ -158,7 +158,7 @@ static void tc_register(void) (g_reg.p_defaults != NULL) && (g_reg.p_data == (const void *)&s_cfg) && (g_reg.p_cb == NULL), - "TC-FTPCFG-004 段名/尺寸/键数/默认值/**p_cb 必须为 NULL**"); + "TC-FTPCFG-004 section name/size/key count/defaults/**p_cb must be NULL**"); CHECK((strcmp(g_reg.p_keys[0].name, "user") == 0) && (g_reg.p_keys[0].type == CFG_VAL_STR) && @@ -170,7 +170,7 @@ static void tc_register(void) (strcmp(g_reg.p_keys[2].name, "write") == 0) && (g_reg.p_keys[2].type == CFG_VAL_UINT) && (g_reg.p_keys[2].len == 1u), - "TC-FTPCFG-005 键表逐项:user(STR,16)/pwd_hash(STR,65)/write(UINT,1)"); + "TC-FTPCFG-005 key table item by item: user(STR,16)/pwd_hash(STR,65)/write(UINT,1)"); } /* ===================== 出厂默认与只读访问 ===================== */ @@ -182,17 +182,17 @@ static void tc_defaults(void) printf("-- TC-FTPCFG-001..002,014 defaults --\n"); /* 契约就是"恒非空"(静态常驻),单独一条断言;随后的显式守卫既让 * test-cppcheck 不再报"冗余或空指针二选一",也让真出问题时报在 001a 上 */ - CHECK(p != NULL, "TC-FTPCFG-001a ftp_cfg_get 恒非空(静态常驻)"); + CHECK(p != NULL, "TC-FTPCFG-001a ftp_cfg_get never NULL (statically resident)"); if (p == NULL) { return; } - CHECK(strcmp(p->user, "admin") == 0, "TC-FTPCFG-001 未注册也可 get,user = admin"); + CHECK(strcmp(p->user, "admin") == 0, "TC-FTPCFG-001 get works even unregistered, user = admin"); CHECK((strcmp(p->pwd_hash, FTP_CFG_DEF_HASH) == 0) && (p->write_en == 1u), - "TC-FTPCFG-002 初值 = 出厂哈希 + write_en=1(维护工作流依赖)"); + "TC-FTPCFG-002 initial value = factory hash + write_en=1 (maintenance workflow relies on it)"); CHECK((FTP_CFG_USER_MAX == 16u) && (FTP_CFG_HASH_MAX == 65u) && (sizeof(p->user) == FTP_CFG_USER_MAX) && (sizeof(p->pwd_hash) == FTP_CFG_HASH_MAX), - "TC-FTPCFG-014 缓冲尺寸契约:user 16 / hash 65(64 hex + NUL)"); + "TC-FTPCFG-014 buffer size contract: user 16 / hash 65 (64 hex + NUL)"); } /* ===================== 默认口令判定(启动告警的依据) ===================== */ @@ -200,17 +200,17 @@ static void tc_defaults(void) static void tc_default_pwd(void) { printf("-- TC-FTPCFG-006..009 default-password detection --\n"); - CHECK(ftp_cfg_is_default_pwd() == 1, "TC-FTPCFG-006 出厂态判为默认口令"); + CHECK(ftp_cfg_is_default_pwd() == 1, "TC-FTPCFG-006 factory state judged as default password"); set_hash("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); - CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-007 改了口令 → 不再默认"); + CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-007 password changed -> no longer default"); set_hash_upper(); CHECK(ftp_cfg_is_default_pwd() == 1, - "TC-FTPCFG-008 大写 hex 仍判默认(比较必须大小写不敏感)"); + "TC-FTPCFG-008 uppercase hex still judged default (comparison must be case-insensitive)"); set_hash(""); - CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-009 空哈希不是默认口令"); + CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-009 empty hash is not the default password"); } /* ===================== 静态比较函数(NULL / 折叠 / 前缀) ===================== */ @@ -219,12 +219,12 @@ static void tc_ci_eq(void) { printf("-- TC-FTPCFG-010..013 case-insensitive compare --\n"); CHECK(s_str_ci_eq("A_b1!Z", "a_b1!z") == 1, - "TC-FTPCFG-010 只折叠 A-Z,数字/下划线/符号原样比较"); - CHECK(s_str_ci_eq("abc", "abcd") == 0, "TC-FTPCFG-011 前缀关系不算相等"); + "TC-FTPCFG-010 only folds A-Z; digits/underscore/symbols compared as-is"); + CHECK(s_str_ci_eq("abc", "abcd") == 0, "TC-FTPCFG-011 prefix relation is not equality"); CHECK((s_str_ci_eq(NULL, "x") == 0) && (s_str_ci_eq("x", NULL) == 0), - "TC-FTPCFG-012 任一为 NULL 判不等"); + "TC-FTPCFG-012 either side NULL -> not equal"); CHECK(s_str_ci_eq(NULL, NULL) == 0, - "TC-FTPCFG-013 两个 NULL 也判不等(fail-closed)"); + "TC-FTPCFG-013 both NULL also judged not equal (fail-closed)"); } /* ===================== 入口 ===================== */ diff --git a/test/host/iec104/iec104_test.c b/test/host/iec104/iec104_test.c index f162a1b3..bfc8b97e 100644 --- a/test/host/iec104/iec104_test.c +++ b/test/host/iec104/iec104_test.c @@ -315,17 +315,17 @@ static void tc_cp56(void) iec104_epoch_ms_to_cp56(e1, b); rc = iec104_cp56_to_epoch_ms(b, &back); - CHECK((rc == 0) && (back == e1), "TC-001 CP56 往返 2000-01-01"); + CHECK((rc == 0) && (back == e1), "TC-001 CP56 round trip on 2000-01-01"); iec104_epoch_ms_to_cp56(e2, b); back = 0u; rc = iec104_cp56_to_epoch_ms(b, &back); - CHECK((rc == 0) && (back == e2), "TC-001 CP56 往返闰日 2024-02-29"); + CHECK((rc == 0) && (back == e2), "TC-001 CP56 round trip on the leap day 2024-02-29"); b[2] |= 0x80u; - CHECK(iec104_cp56_to_epoch_ms(b, &back) == -2, "TC-001 IV → -2"); + CHECK(iec104_cp56_to_epoch_ms(b, &back) == -2, "TC-001 IV -> -2"); iec104_epoch_ms_to_cp56(0u, b); /* 早于 2000 → 钳位 */ back = 0u; (void)iec104_cp56_to_epoch_ms(b, &back); - CHECK(back == e1, "TC-001 早于 2000 钳位"); + CHECK(back == e1, "TC-001 a value before 2000 is clamped"); } static void tc_ioa_float(void) @@ -334,12 +334,12 @@ static void tc_ioa_float(void) iec104_put_ioa(b, 0x123456u); CHECK((iec104_get_ioa(b) == 0x123456u) && (b[0] == 0x56u) && (b[1] == 0x34u) - && (b[2] == 0x12u), "TC-002 IOA 3B 小端"); + && (b[2] == 0x12u), "TC-002 IOA is 3 bytes little endian"); iec104_put_float(b, 1.0f); CHECK((b[0] == 0x00u) && (b[1] == 0x00u) && (b[2] == 0x80u) - && (b[3] == 0x3Fu), "TC-002 float IEEE754 小端"); + && (b[3] == 0x3Fu), "TC-002 float in IEEE754 little endian"); iec104_put_float(b, -12.5f); - CHECK(iec104_get_float(b) == -12.5f, "TC-002 float 往返"); + CHECK(iec104_get_float(b) == -12.5f, "TC-002 float round trip"); } static void tc_apci(void) @@ -351,9 +351,9 @@ static void tc_apci(void) static const uint8_t b[6] = { 0x68u, 4u, 0x07u, 0u, 0u, 0u }; iec104_link_on_rx(&g_lk, b, 3u, now); - CHECK(cap_u_count(0x0Bu) == 0, "TC-003 半帧不触发"); + CHECK(cap_u_count(0x0Bu) == 0, "TC-003 a half frame triggers nothing"); iec104_link_on_rx(&g_lk, &b[3], 3u, now); - CHECK(cap_u_count(0x0Bu) == 1, "TC-003 半帧重组 → con"); + CHECK(cap_u_count(0x0Bu) == 1, "TC-003 a half frame is reassembled then con"); } { static const uint8_t b[12] = { @@ -364,21 +364,21 @@ static void tc_apci(void) iec104_link_on_rx(&g_lk, b, 12u, now); CHECK((cap_u_count(0x83u) == 1) && (iec104_link_state(&g_lk) == IEC104_ST_STOPPED), - "TC-003 粘帧逐处 + STOPDT 生效"); + "TC-003 a coalesced frame is handled one by one and STOPDT takes effect"); } reset_all(now); { static const uint8_t b[6] = { 0x69u, 4u, 0x07u, 0u, 0u, 0u }; iec104_link_on_rx(&g_lk, b, 6u, now); - CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 坏起始符 → IDLE"); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 a bad start byte -> IDLE"); } reset_all(now); { static const uint8_t b[6] = { 0x68u, 3u, 0x07u, 0u, 0u, 0u }; iec104_link_on_rx(&g_lk, b, 6u, now); - CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 坏长度 → IDLE"); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 a bad length -> IDLE"); } } @@ -387,10 +387,10 @@ static void tc_startdt(void) uint32_t now = 1000u; reset_all(now); - CHECK(iec104_link_state(&g_lk) == IEC104_ST_OPEN, "TC-004 open → OPEN"); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_OPEN, "TC-004 open -> OPEN"); handshake(now); CHECK((cap_u_count(0x0Bu) == 1) - && (iec104_link_state(&g_lk) == IEC104_ST_RUN), "TC-004 STARTDT → RUN"); + && (iec104_link_state(&g_lk) == IEC104_ST_RUN), "TC-004 STARTDT -> RUN"); } static void tc_open_ignore(void) @@ -403,11 +403,11 @@ static void tc_open_ignore(void) n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u); feed_i(a, n, now); CHECK((cap_i_count() == 0) && (iec104_link_state(&g_lk) == IEC104_ST_OPEN), - "TC-005 OPEN 态 I 帧忽略"); + "TC-005 an I frame in OPEN state is ignored"); handshake(now); m_ns = 0u; /* OPEN 忽略不消费序号 → 主站仍从 0 起 */ feed_i(a, n, now); /* ns=0 仍有效(序号未被消费) */ - CHECK(cap_i_count() == 1, "TC-005 RUN 后同 ns 正常应答"); + CHECK(cap_i_count() == 1, "TC-005 after RUN the same ns is answered normally"); } static void tc_interrogation(void) @@ -426,34 +426,34 @@ static void tc_interrogation(void) n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u); feed_i(a, n, now); CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 100u) - && (c.cot == IEC104_COT_ACTCON), "TC-006 actcon 即发 COT7"); + && (c.cot == IEC104_COT_ACTCON), "TC-006 actcon is sent right away, COT7"); iec104_link_poll(&g_lk, now); - CHECK(cap_i_count() == 5, "TC-006 首 poll 发满预算 4 分片"); + CHECK(cap_i_count() == 5, "TC-006 the first poll uses the whole budget of 4 frames"); CHECK((cap_i(1, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT) && (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 4u)) && (c.ioa == 1u), - "TC-006 YX 双点组 首召 COT=4"); + "TC-006 YX double-point group, first call COT=4"); CHECK((cap_i(2, &c) == 0) && (c.type == 1u) && (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 29u)) && (c.ioa == 5u), - "TC-006 YX 单点组 29 点"); + "TC-006 YX single-point group with 29 points"); CHECK((cap_i(3, &c) == 0) && (c.type == 13u) && (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 8u)) && (c.ioa == 0x4011u) - && (c.obj_len == 40u), "TC-006 YC 组 8 点 40B"); + && (c.obj_len == 40u), "TC-006 YC group of 8 points, 40 bytes"); if (cap_i(3, &c) == 0) { CHECK((iec104_get_float(c.obj) == 111.0f) && (c.obj[4] == 0u), - "TC-006 YC 值 111.0 QDS=0"); - CHECK(c.obj[9] == IEC104_QDS_IV, "TC-006 无效通道 QDS=IV"); + "TC-006 YC value 111.0 with QDS=0"); + CHECK(c.obj[9] == IEC104_QDS_IV, "TC-006 an invalid channel sets QDS=IV"); } CHECK((cap_i(4, &c) == 0) && (c.ioa == 0x401Du) && (c.vsq == 1u), - "TC-006 频率单点 SQ=0"); + "TC-006 frequency as a single point with SQ=0"); iec104_link_poll(&g_lk, now + 100u); CHECK((cap_i_count() == 6) && (cap_i(5, &c) == 0) && (c.type == 100u) - && (c.cot == IEC104_COT_ACTTERM), "TC-006 actterm 收尾"); + && (c.cot == IEC104_COT_ACTTERM), "TC-006 actterm closes the sequence"); /* 二召:init 已清 → COT=2 */ n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u); feed_i(a, n, now + 200u); iec104_link_poll(&g_lk, now + 200u); CHECK((cap_i(7, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_BG_SCAN), - "TC-006 二召 COT=2"); + "TC-006 second call COT=2"); /* RBT 位 → COT=4(actcon3 是第 12 个未确认帧 → 先 S 确认窗才开) */ n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, IEC104_COT_BIT_RBT, 1u, 0u, body, 1u); @@ -461,7 +461,7 @@ static void tc_interrogation(void) feed_s(12u, now + 300u); iec104_link_poll(&g_lk, now + 300u); CHECK((cap_i(12, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT), - "TC-006 RBT → COT=4"); + "TC-006 RBT -> COT=4"); } static void tc_cov_yx(void) @@ -473,33 +473,33 @@ static void tc_cov_yx(void) reset_all(now); handshake(now); iec104_link_poll(&g_lk, now); /* 建快照 */ - CHECK(cap_i_count() == 0, "TC-007 首 poll 无自发"); + CHECK(cap_i_count() == 0, "TC-007 the first poll sends nothing spontaneous"); s_f_bits |= (1uLL << 15); /* YX16(idx15)动作 */ iec104_link_poll(&g_lk, now + 100u); /* B9:单点 YX 自发 = M_SP_TB_1(30)带 CP56Time2a 时标(SOE 上送路径) */ CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 30u) && (c.cot == IEC104_COT_SPONT) && (c.ioa == 16u) - && (c.obj_len == 8u) && (c.obj[0] == 1u), "TC-007 YX16 变位 COT3 TB"); + && (c.obj_len == 8u) && (c.obj[0] == 1u), "TC-007 a YX16 change of state sends COT3 with TB"); if ((cap_i(0, &c) == 0) && (c.obj_len == 8u)) { CHECK(iec104_cp56_to_epoch_ms(&c.obj[1], &back) == 0, - "TC-007 CP56 可解"); - CHECK(back == s_epoch_val, "TC-007 时标 = epoch_ms 源"); + "TC-007 the CP56 time tag decodes"); + CHECK(back == s_epoch_val, "TC-007 the time tag equals the epoch_ms source"); } s_f_bits &= ~(1uLL << 15); /* 复归 */ iec104_link_poll(&g_lk, now + 200u); CHECK((cap_i_count() == 2) && (cap_i(1, &c) == 0) && (c.type == 30u) - && (c.ioa == 16u) && (c.obj[0] == 0u), "TC-007 YX16 复归再变"); + && (c.ioa == 16u) && (c.obj[0] == 0u), "TC-007 YX16 is reset and changes again"); /* B9 IV 路径:时标源无效 → CP56 IV 位如实置位 */ s_epoch_val = UINT64_MAX; s_f_bits |= (1uLL << 16); /* YX17(idx16)动作 */ iec104_link_poll(&g_lk, now + 300u); CHECK((cap_i_count() == 3) && (cap_i(2, &c) == 0) && (c.type == 30u) && (c.ioa == 17u) && ((c.obj[3] & 0x80u) != 0u), - "TC-007 时钟无效 → CP56 IV 位"); + "TC-007 an invalid clock sets the CP56 IV bit"); s_epoch_val = 1760234567000ULL; s_f_bits |= (1uLL << 0); /* idx0 无效位 → 不触发 */ iec104_link_poll(&g_lk, now + 400u); - CHECK(cap_i_count() == 3, "TC-007 无效位变位不触发"); + CHECK(cap_i_count() == 3, "TC-007 a change while the IV bit is set triggers nothing"); } static void tc_cov_yc(void) @@ -513,12 +513,12 @@ static void tc_cov_yc(void) iec104_link_poll(&g_lk, now); /* 快照 = 100 */ s_f_yc[0] = 105.0f; /* 死区 10 内 */ iec104_link_poll(&g_lk, now + 100u); - CHECK(cap_i_count() == 0, "TC-008 死区内不上送"); + CHECK(cap_i_count() == 0, "TC-008 inside the deadband nothing is uploaded"); s_f_yc[0] = 130.0f; /* 130-105=25 > 10 */ iec104_link_poll(&g_lk, now + 200u); CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_SPONT) && (c.ioa == 0x4011u) - && (iec104_get_float(c.obj) == 130.0f), "TC-008 越死区上送"); + && (iec104_get_float(c.obj) == 130.0f), "TC-008 crossing the deadband uploads"); } static void feed_rd(uint32_t now) @@ -540,12 +540,12 @@ static void tc_k_window(void) for (i = 0; i < 12; i++) { feed_rd(now); } - CHECK(cap_i_count() == 12, "TC-009 k=12 封顶"); + CHECK(cap_i_count() == 12, "TC-009 the k=12 window caps the outstanding frames"); feed_rd(now); - CHECK(cap_i_count() == 12, "TC-009 第 13 个被 k 窗抑制"); + CHECK(cap_i_count() == 12, "TC-009 the 13th frame is suppressed by the k window"); feed_s(12u, now); /* 全确认 → 窗开 */ feed_rd(now); - CHECK(cap_i_count() == 13, "TC-009 S 确认后恢复发送"); + CHECK(cap_i_count() == 13, "TC-009 sending resumes after the S acknowledgement"); } static void tc_w_thresh(void) @@ -559,9 +559,9 @@ static void tc_w_thresh(void) for (i = 0; i < 7; i++) { feed_rd(now); } - CHECK(cap_has_s(&nr) == 0, "TC-010 收 7 未发 S"); + CHECK(cap_has_s(&nr) == 0, "TC-010 after 7 received frames no S has been sent yet"); feed_rd(now); /* 第 8 个 → w 阈 */ - CHECK((cap_has_s(&nr) == 1) && (nr == 8u), "TC-010 w=8 → S nr=8"); + CHECK((cap_has_s(&nr) == 1) && (nr == 8u), "TC-010 w=8 -> S with nr=8"); } static void tc_t2(void) @@ -573,9 +573,9 @@ static void tc_t2(void) handshake(now); feed_rd(now); /* rx_unacked=1,t2 起爆 */ iec104_link_poll(&g_lk, now + 9999u); - CHECK(cap_has_s(&nr) == 0, "TC-011 t2 未到期不发 S"); + CHECK(cap_has_s(&nr) == 0, "TC-011 no S before t2 expires"); iec104_link_poll(&g_lk, now + 10001u); - CHECK((cap_has_s(&nr) == 1) && (nr == 1u), "TC-011 t2 到期发 S"); + CHECK((cap_has_s(&nr) == 1) && (nr == 1u), "TC-011 S is sent once t2 expires"); } static void tc_t3_t1(void) @@ -585,19 +585,19 @@ static void tc_t3_t1(void) reset_all(now); handshake(now); iec104_link_poll(&g_lk, now + 19999u); - CHECK(cap_u_count(0x43u) == 0, "TC-012 t3 未到期无 TESTFR"); + CHECK(cap_u_count(0x43u) == 0, "TC-012 no TESTFR before t3 expires"); iec104_link_poll(&g_lk, now + 20001u); - CHECK(cap_u_count(0x43u) == 1, "TC-012 t3 到期发 TESTFR act"); + CHECK(cap_u_count(0x43u) == 1, "TC-012 TESTFR act is sent once t3 expires"); iec104_link_poll(&g_lk, now + 20001u + 15001u); - CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-012 TESTFR 未答 t1 → IDLE"); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-012 an unanswered TESTFR hits t1 -> IDLE"); /* con 清挂起 → 不误杀 */ reset_all(now); handshake(now); iec104_link_poll(&g_lk, now + 20001u); - CHECK(cap_u_count(0x43u) == 1, "TC-012 重连 t3 再触发"); + CHECK(cap_u_count(0x43u) == 1, "TC-012 after reconnect t3 fires again"); feed_u(0x83u, now + 20100u); /* TESTFR con */ iec104_link_poll(&g_lk, now + 35000u); - CHECK(iec104_link_state(&g_lk) == IEC104_ST_RUN, "TC-012 con 清挂起后 t1 不误杀"); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_RUN, "TC-012 once con clears the pending frame t1 does not kill it by mistake"); } static void tc_dc(void) @@ -614,13 +614,13 @@ static void tc_dc(void) n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u); feed_i(a, n, now); CHECK((cap_i(0, &c) == 0) && (c.type == 52u) && (c.cot == IEC104_COT_ACTCON) - && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-013 选择段 actcon"); - CHECK((s_f_yk_sel == 1u) && (s_f_yk_ioa == 0x600Au), "TC-013 选择段转发源"); + && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-013 the select phase answers actcon"); + CHECK((s_f_yk_sel == 1u) && (s_f_yk_ioa == 0x600Au), "TC-013 the select phase forwards the source"); body[0] = 0x02u; /* 执行 ON */ n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u); feed_i(a, n, now); CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u) - && (s_f_yk_sel == 0u), "TC-013 执行段 actcon"); + && (s_f_yk_sel == 0u), "TC-013 the execute phase answers actcon"); /* 失配:选 ON 执 OFF */ body[0] = 0x82u; n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u); @@ -629,13 +629,13 @@ static void tc_dc(void) n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u); feed_i(a, n, now); CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), - "TC-013 执行失配 PN"); + "TC-013 a mismatching execute phase returns PN"); /* 拒绝类(YK1):选择段即 PN */ body[0] = 0x82u; n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x6000u, body, 1u); feed_i(a, n, now); CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), - "TC-013 无出口硬件 YK 选择段 PN"); + "TC-013 a YK without output hardware returns PN in the select phase"); } /* TC-024(B7):YK12 全局软压板两段式转真(经真实板端源 + 原子封存) */ @@ -649,21 +649,21 @@ static void tc_dc_yk12(void) reset_all(now); handshake(now); - CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 软压板默认投入"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 soft plates are engaged by default"); body[0] = 0x81u; /* S=1, DPC=OFF:选择段 */ n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); feed_i(a, n, now); CHECK((cap_i(0, &c) == 0) && (c.cot == IEC104_COT_ACTCON) - && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-024 YK12 选择段 actcon"); - CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 选择段不动作"); + && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-024 the YK12 select phase answers actcon"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 the select phase performs no action"); body[0] = 0x01u; /* 执行 OFF */ n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); feed_i(a, n, now); CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), - "TC-024 YK12 执行段 actcon"); - CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-024 执行段软压板转真退出"); - CHECK(prot_set_selfcheck() == 0, "TC-024 set_sealed 原子封存 → selfcheck 通过"); - CHECK(prot_set_dirty_peek() == 1u, "TC-024 置脏待持久化"); + "TC-024 the YK12 execute phase answers actcon"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-024 the execute phase drives the soft plate to the released state"); + CHECK(prot_set_selfcheck() == 0, "TC-024 set_sealed seals atomically and selfcheck passes"); + CHECK(prot_set_dirty_peek() == 1u, "TC-024 the dirty flag is set for persistence"); body[0] = 0x82u; /* 再投入:选择 ON */ n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); feed_i(a, n, now); @@ -671,13 +671,13 @@ static void tc_dc_yk12(void) n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); feed_i(a, n, now); CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), - "TC-024 投入执行 actcon"); - CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 软压板转真投入"); + "TC-024 the engage command executes with actcon"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 the soft plate reaches the engaged state"); body[0] = 0x80u; /* DPC=00 非法:选择段即 PN */ n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); feed_i(a, n, now); CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), - "TC-024 非法 DPC 选择段 PN"); + "TC-024 an invalid DPC returns PN in the select phase"); } static void tc_se(void) @@ -691,27 +691,27 @@ static void tc_se(void) reset_all(now); handshake(now); - CHECK(addr == 0x8858u, "TC-014 OC1 电流定值通讯地址"); + CHECK(addr == 0x8858u, "TC-014 the OC1 current setting communication address"); iec104_put_float(body, 5.0f); body[4] = IEC104_QDC_S; /* 选择段 */ n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u); feed_i(a, n, now); CHECK((cap_i(0, &c) == 0) && (c.type == 50u) - && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-014 定值选择 actcon"); + && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-014 selecting a setting answers actcon"); body[4] = 0u; /* 执行段 */ n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u); feed_i(a, n, now); CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), - "TC-014 定值执行 actcon"); - CHECK(prot_set_get_f(PS_OC1_CUR) == 5.0f, "TC-014 定值已写入(真实 prot_set)"); - CHECK(prot_set_selfcheck() == 0, "TC-014 执行后 seal → selfcheck 通过"); - CHECK(prot_set_dirty_peek() == 1u, "TC-014 执行段置脏(B7 待落盘)"); + "TC-014 executing a setting answers actcon"); + CHECK(prot_set_get_f(PS_OC1_CUR) == 5.0f, "TC-014 the setting really was written (real prot_set)"); + CHECK(prot_set_selfcheck() == 0, "TC-014 sealing after execute keeps selfcheck passing"); + CHECK(prot_set_dirty_peek() == 1u, "TC-014 the execute phase sets dirty (B7, waiting to be persisted)"); iec104_put_float(body, 100000.0f); /* 越上限 */ body[4] = IEC104_QDC_S; n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u); feed_i(a, n, now); CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), - "TC-014 越界选择段 PN"); + "TC-014 an out-of-range select phase returns PN"); } static void tc_rd(void) @@ -727,20 +727,20 @@ static void tc_rd(void) n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u); feed_i(a, n, now); CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_REQ) - && (iec104_get_float(c.obj) == 222.0f), "TC-015 C_RD YC 命中"); + && (iec104_get_float(c.obj) == 222.0f), "TC-015 C_RD hits a YC group"); n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x8858u, NULL, 0u); feed_i(a, n, now); CHECK((cap_i(1, &c) == 0) && (c.type == 13u) && (c.ioa == 0x8858u), - "TC-015 C_RD 定值命中"); + "TC-015 C_RD hits a setting"); n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x9999u, NULL, 0u); feed_i(a, n, now); CHECK((cap_i(2, &c) == 0) && (c.type == 102u) && (c.cot == IEC104_COT_UNK_IOA) && ((c.oa & 0x80u) != 0u), - "TC-015 未知 IOA COT14+EVI"); + "TC-015 an unknown IOA answers COT14 plus EVI"); n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0u, NULL, 0u); feed_i(a, n, now); CHECK((cap_i(3, &c) == 0) && (c.cot == IEC104_COT_UNK_IOA), - "TC-015 本地定值项(comm_addr=0)不可寻址(B7)"); + "TC-015 a local setting item (comm_addr=0) is not addressable (B7)"); } static void tc_cs(void) @@ -759,18 +759,18 @@ static void tc_cs(void) feed_i(a, n, now); CHECK((cap_i(0, &c) == 0) && (c.type == 103u) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u) && (s_clock_ms == e), - "TC-016 C_CS 对时成功 epoch 精确"); + "TC-016 C_CS syncs the clock with an exact epoch"); cp[2] |= 0x80u; /* IV */ n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u); feed_i(a, n, now); CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), - "TC-016 CP56 IV → PN"); + "TC-016 CP56 IV -> PN"); (void)iec104_epoch_ms_to_cp56(e, cp); s_clock_rc = -1; /* 系统侧拒绝 */ n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u); feed_i(a, n, now); CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), - "TC-016 clock_sync 失败 → PN"); + "TC-016 a clock_sync failure -> PN"); } static void tc_err_resp(void) @@ -785,13 +785,13 @@ static void tc_err_resp(void) n = mk_asdu(a, 200u, 1u, IEC104_COT_ACT, 0u, 1u, 0u, NULL, 0u); feed_i(a, n, now); CHECK((cap_i(0, &c) == 0) && (c.type == 200u) && (c.cot == IEC104_COT_UNK_COT) - && ((c.oa & 0x80u) != 0u), "TC-017 未知类型 COT12+EVI"); + && ((c.oa & 0x80u) != 0u), "TC-017 an unknown type answers COT12 plus EVI"); m = iec104_asdu_put_hdr(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 2u); a[m] = IEC104_RQT_GROUP1; m++; feed_i(a, m, now); /* ca=2 ≠ 1 */ CHECK((cap_i(1, &c) == 0) && (c.cot == IEC104_COT_UNK_CA) - && ((c.oa & 0x80u) != 0u), "TC-017 错公共地址 COT13+EVI"); + && ((c.oa & 0x80u) != 0u), "TC-017 a wrong common address answers COT13 plus EVI"); } static void tc_stopdt(void) @@ -809,9 +809,9 @@ static void tc_stopdt(void) body[0] = IEC104_RQT_GROUP1; n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u); feed_i(a, n, now); - CHECK(cap_i_count() == 0, "TC-018 STOPPED 态数据服务关"); + CHECK(cap_i_count() == 0, "TC-018 in STOPPED state the data service is off"); feed_u(0x43u, now); - CHECK(cap_u_count(0x83u) == 1, "TC-018 STOPPED 态 TESTFR 仍应答"); + CHECK(cap_u_count(0x83u) == 1, "TC-018 in STOPPED state TESTFR is still answered"); } static void tc_seq_err(void) @@ -825,7 +825,7 @@ static void tc_seq_err(void) m_ns = 5u; /* 期望 0,投 5 */ n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u); feed_i(a, n, now); - CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-019 序号错 → IDLE"); + CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-019 a sequence error -> IDLE"); } static void tc_pts(void) @@ -836,23 +836,23 @@ static void tc_pts(void) reset_all(1000u); CHECK((iec104_pts_yc_count() == 9u) && (iec104_pts_yx_count() == 33u) - && (iec104_pts_yc_ioa(8u) == 0x401Du), "TC-020 表尺寸/IOA"); + && (iec104_pts_yc_ioa(8u) == 0x401Du), "TC-020 table size and IOA layout"); s_f_yc[0] = 7.5f; CHECK((iec104_pts_yc_read(0u, &v, &q) == 0) && (v == 7.5f) && (q == 0u), - "TC-020 有效 YC"); + "TC-020 a valid YC"); s_f_yc_rc[1] = -1; (void)iec104_pts_yc_read(1u, &v, &q); - CHECK((v == 0.0f) && (q == IEC104_QDS_IV), "TC-020 无效通道 IV"); + CHECK((v == 0.0f) && (q == IEC104_QDS_IV), "TC-020 an invalid channel gives IV"); s_f_yc_rc[2] = -2; (void)iec104_pts_yc_read(2u, &v, &q); - CHECK(q == IEC104_QDS_NT, "TC-020 未就绪 NT"); + CHECK(q == IEC104_QDS_NT, "TC-020 not ready gives NT"); iec104_pts_yx_read(0u, &d); - CHECK(d == IEC104_DIQ_IV, "TC-020 无效遥信 IV"); + CHECK(d == IEC104_DIQ_IV, "TC-020 an invalid telemetry point gives IV"); CHECK((iec104_pts_yx_is_dp(0u) == 1u) && (iec104_pts_yx_is_dp(4u) == 0u), - "TC-020 is_dp 分界"); + "TC-020 the is_dp boundary"); s_f_bits = (1uLL << 15); iec104_pts_yx_read(15u, &d); - CHECK(d == 1u, "TC-020 有效遥信 ON"); + CHECK(d == 1u, "TC-020 a valid telemetry point gives ON"); } static void tc_ts_rp(void) @@ -884,10 +884,10 @@ static void tc_periodic(void) reset_all(now); handshake(now); iec104_link_poll(&g_lk, now + 60001u); - CHECK(cap_i_count() == 2, "TC-022 周期上送 2 分片"); + CHECK(cap_i_count() == 2, "TC-022 the cyclic upload takes 2 frames"); CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_PERIODIC) - && (c.ioa == 0x4011u), "TC-022 周期 YC 组 COT=1"); - CHECK((cap_i(1, &c) == 0) && (c.ioa == 0x401Du), "TC-022 周期频率点"); + && (c.ioa == 0x4011u), "TC-022 the cyclic YC group uses COT=1"); + CHECK((cap_i(1, &c) == 0) && (c.ioa == 0x401Du), "TC-022 the cyclic frequency point"); } static void tc_cov_kfull(void) @@ -904,11 +904,11 @@ static void tc_cov_kfull(void) } s_f_bits |= (1uLL << 14); /* YX15 变位 */ iec104_link_poll(&g_lk, now + 100u); - CHECK(cap_i_count() == 12, "TC-023 k 满时自发发不出"); + CHECK(cap_i_count() == 12, "TC-023 with k full nothing spontaneous can go out"); feed_s(12u, now + 200u); /* 窗开 */ iec104_link_poll(&g_lk, now + 300u); CHECK((cap_i_count() == 13) && (cap_i(12, &c) == 0) && (c.ioa == 15u) - && (c.cot == IEC104_COT_SPONT), "TC-023 窗开后补发不丢事件"); + && (c.cot == IEC104_COT_SPONT), "TC-023 once the window opens the backlogged event is sent, none lost"); } int main(void) diff --git a/test/host/iec104_serv/iec104_serv_test.c b/test/host/iec104_serv/iec104_serv_test.c index 3746c0e8..b23bcc5d 100644 --- a/test/host/iec104_serv/iec104_serv_test.c +++ b/test/host/iec104_serv/iec104_serv_test.c @@ -221,7 +221,7 @@ void osDelay(uint32_t ticks) (void)ticks; g_n.delay_calls++; if (g_n.delay_calls > 500) { - printf(" ABORT 跑飞(>500 轮 osDelay)\n"); + printf(" ABORT runaway (>500 osDelay rounds)\n"); fflush(stdout); exit(2); } @@ -278,22 +278,22 @@ static void tc_bind_listen(void) (g_n.last_bind_family == NET_AF_INET) && (g_n.last_bind_port_net == net_htons(IEC104_SERV_PORT)) && (g_n.last_bind_addr == NET_INADDR_ANY), - "TC-I104SRV-001 建监听:2404 端口过 htons、绑任意地址、记下监听号"); + "TC-I104SRV-001 build the listener: port 2404 through htons, bind to any address, remember the listen handle"); reset_all(); g_n.socket_rc = -1; CHECK((s_bind_listen() == -1) && (g_n.bind_calls == 0) && (g_n.close_calls == 0), - "TC-I104SRV-002 建不出 socket 就返回,不去 bind 也不关不存在的口"); + "TC-I104SRV-002 if no socket can be created return at once: no bind and no close of a handle that does not exist"); reset_all(); g_n.bind_rc = -1; CHECK((s_bind_listen() == -1) && (g_n.close_calls == 1) && (g_n.listen_calls == 0), - "TC-I104SRV-003 bind 失败要把刚建的口关掉(否则每次重试漏一个 socket 号)"); + "TC-I104SRV-003 a bind failure must close the socket just created (otherwise every retry leaks a handle)"); reset_all(); g_n.listen_rc = -1; CHECK((s_bind_listen() == -1) && (g_n.close_calls == 1) && (s_listen == -1), - "TC-I104SRV-004 listen 失败同样关口且不留下半个监听"); + "TC-I104SRV-004 a listen failure also closes it and leaves no half-built listener"); } /* ===================== 发送与连续短写判死 ===================== */ @@ -307,25 +307,25 @@ static void tc_send(void) s_client = -1; s_send_apdu(NULL, apdu, 4u); CHECK((g_n.send_rc == 0) && (g_n.close_calls == 0) && (g_i4_error_count == 0), - "TC-I104SRV-010 没连接时直接返回(不发 net_send、不报错)"); + "TC-I104SRV-010 with no connection it returns directly (no net_send, no error)"); reset_all(); s_client = 7; g_n.send_rc = 4; s_send_apdu(NULL, apdu, 4u); CHECK((s_send_fail_run == 0u) && (s_conn_dead == 0u), - "TC-I104SRV-011 完整发出把连败计数清零"); + "TC-I104SRV-011 a fully sent frame clears the consecutive-failure counter"); reset_all(); s_client = 7; g_n.send_rc = 2; /* 短写:只发了 2/4 */ s_send_apdu(NULL, apdu, 4u); CHECK((s_send_fail_run == 1u) && (s_conn_dead == 0u) && (g_i4_error_count == 1), - "TC-I104SRV-012 第一次短写:留痕但不判死(拥塞很正常)"); + "TC-I104SRV-012 the first short write is logged but does not declare death (congestion is normal)"); s_send_apdu(NULL, apdu, 4u); s_send_apdu(NULL, apdu, 4u); CHECK((s_send_fail_run == 3u) && (s_conn_dead == 1u), - "TC-I104SRV-012b 连到 3 次才判死(IEC104_SEND_FAIL_MAX)"); + "TC-I104SRV-012b only the 3rd failure in a row declares death (IEC104_SEND_FAIL_MAX)"); reset_all(); s_client = 7; @@ -338,7 +338,7 @@ static void tc_send(void) s_send_apdu(NULL, apdu, 4u); s_send_apdu(NULL, apdu, 4u); CHECK((s_conn_dead == 0u) && (s_send_fail_run == 2u), - "TC-I104SRV-013 **成功即复位**:偶发拥塞不会被累计成误杀"); + "TC-I104SRV-013 success resets the counter: occasional congestion never adds up into a false kill"); } /* ===================== 时钟桥接边界 ===================== */ @@ -348,22 +348,22 @@ static void tc_clock(void) printf("-- TC-I104SRV-020..023 clock bridge --\n"); reset_all(); CHECK(s_clock_sync(NULL, 0x100000000ULL * 1000ULL) == -1, - "TC-I104SRV-020 秒数超过 32 位一律拒绝(截断写进 RTC 就是 2106 年后的乱值)"); - CHECK(g_clock_set_calls == 0, "TC-I104SRV-020b 拒绝时根本不该碰时钟"); + "TC-I104SRV-020 any second count beyond 32 bits is rejected (truncating it into the RTC means garbage past 2106)"); + CHECK(g_clock_set_calls == 0, "TC-I104SRV-020b when rejected the clock must not be touched at all"); reset_all(); (void)s_clock_sync(NULL, 1700000123000ULL); CHECK((g_clock_set_calls == 1) && (g_clock_last_secs == 1700000123u), - "TC-I104SRV-021 毫秒按整除换秒后写时钟"); + "TC-I104SRV-021 milliseconds become seconds by integer division before the clock is written"); reset_all(); g_clock_val = UINT32_MAX; /* 未对时的哨兵值 */ CHECK(s_epoch_ms(NULL) == UINT64_MAX, - "TC-I104SRV-022 未对时回 UINT64_MAX 哨兵(绝不能把 1970 当成设备时间)"); + "TC-I104SRV-022 without time sync return the UINT64_MAX sentinel (1970 must never pass for device time)"); reset_all(); g_clock_val = 1700000123u; - CHECK(s_epoch_ms(NULL) == 1700000123000ULL, "TC-I104SRV-023 正常时对时给出毫秒"); + CHECK(s_epoch_ms(NULL) == 1700000123000ULL, "TC-I104SRV-023 with a synced clock the answer is in milliseconds"); } /* ===================== 代次重建与 accept 门控 ===================== */ @@ -379,13 +379,13 @@ static void tc_generation(void) CHECK((g_n.close_calls == 2) && (g_n.last_closed_fd == 5) && (s_client == -1) && (s_liveness_ctr == 0u) && (s_conn_dead == 0u) && (g_lk_close == 1) && (s_gen == 101u) && (g_n.bind_calls == 1), - "TC-I104SRV-030 代次一变就拆掉旧连接与旧监听并重建(新 IP 上端口必须继续服务)"); + "TC-I104SRV-030 a generation change tears down the old connection and listener and rebuilds them (the port must keep serving on the new IP)"); reset_all(); s_listen = -1; s_serv_poll(); CHECK((g_n.bind_calls == 1) && (s_listen == 5) && (g_n.accept_calls == 0), - "TC-I104SRV-031 监听丢了就补建,这一轮不去 accept"); + "TC-I104SRV-031 a lost listener is rebuilt and this round does not accept"); reset_all(); s_listen = 5; @@ -393,7 +393,7 @@ static void tc_generation(void) g_n.sock_exists = 1; s_serv_poll(); CHECK((g_n.accept_calls == 0) && (s_listen == 5), - "TC-I104SRV-032 监听门未开时不 accept(否则对着未就绪的号空转)"); + "TC-I104SRV-032 no accept while the listener gate is down (otherwise it spins on a handle that is not ready)"); reset_all(); s_listen = 5; @@ -401,7 +401,7 @@ static void tc_generation(void) g_n.sock_exists = 0; /* 表项已不存在 */ s_serv_poll(); CHECK((s_listen == -1) && (g_n.accept_calls == 0), - "TC-I104SRV-032b 表项已失效时把监听号丢掉,下一轮重建"); + "TC-I104SRV-032b once the slot is invalid the listen handle is dropped and rebuilt next round"); reset_all(); s_listen = 5; @@ -413,7 +413,7 @@ static void tc_generation(void) CHECK((s_client == 9) && (g_lk_open == 1) && (g_lk_last_now == 5000u) && (s_send_fail_run == 0u) && (s_conn_dead == 0u) && (s_liveness_ctr == 0u) && (g_i4_info_count == 1), - "TC-I104SRV-033 新连接:打开链路、三个失败计数全部复位"); + "TC-I104SRV-033 a new connection opens the link and resets all three failure counters"); } /* ===================== 活性分频、对端 FIN、错误丢弃 ===================== */ @@ -428,7 +428,7 @@ static void tc_liveness(void) s_liveness_ctr = 0u; s_serv_poll(); CHECK((g_n.recv_calls == 0) && (g_lk_poll == 1) && (s_liveness_ctr == 1u), - "TC-I104SRV-034 无可读数据时按 32 分频探活,这一轮不收包(省 CPU 预算)"); + "TC-I104SRV-034 with nothing readable the liveness probe runs on a divide-by-32 tick and this round receives nothing (saves CPU budget)"); reset_all(); s_listen = 5; @@ -441,7 +441,7 @@ static void tc_liveness(void) s_liveness_ctr = 31u; /* 下一轮正好到分频点 */ s_serv_poll(); CHECK((g_n.recv_calls == 1) && (g_lk_poll == 1) && (s_client == 9), - "TC-I104SRV-035 分频到点才去收一次(TESTFR 等活性帧靠它发出去)"); + "TC-I104SRV-035 only when the divider fires does it receive once (liveness frames such as TESTFR depend on it going out)"); reset_all(); s_listen = 5; @@ -451,7 +451,7 @@ static void tc_liveness(void) s_serv_poll(); CHECK((g_n.last_closed_fd == 9) && (s_client == -1) && (g_lk_close == 1) && (g_n.close_calls == 1) && (s_listen == 5) && (g_n.bind_calls == 0), - "TC-I104SRV-036 对端正常断开:只关数据口、**监听必须留着**(单连接 socket 关错就是端口失联)"); + "TC-I104SRV-036 a normal peer disconnect closes only the data socket: the listener must stay (on a single-socket chip closing the wrong one kills the port)"); reset_all(); s_listen = 5; @@ -460,7 +460,7 @@ static void tc_liveness(void) g_n.recv_rc = 0; s_serv_poll(); CHECK((g_n.close_calls == 0) && (s_client == -1) && (g_lk_close == 1), - "TC-I104SRV-036b 数据号与监听号同号时**不许关**(否则把监听一起毁掉)"); + "TC-I104SRV-036b when the data handle equals the listen handle it must NOT be closed (that would destroy the listener too)"); reset_all(); s_listen = 5; @@ -470,7 +470,7 @@ static void tc_liveness(void) g_n.errno_val = NET_ERR_WOULDBLOCK; /* 空转:没数据可读 */ s_serv_poll(); CHECK((s_client == 9) && (g_lk_close == 0) && (g_i4_error_count == 0), - "TC-I104SRV-037 WOULDBLOCK 是正常空转,不能当成错误丢连接"); + "TC-I104SRV-037 WOULDBLOCK is normal idling and must not drop the connection as an error"); reset_all(); s_listen = 5; @@ -481,7 +481,7 @@ static void tc_liveness(void) s_serv_poll(); CHECK((s_client == -1) && (s_listen == 5) && (g_i4_error_count == 1) && (g_lk_close == 1) && (s_liveness_ctr == 0u), - "TC-I104SRV-038 真错误:丢连接并**当场重建监听**(不留空窗)"); + "TC-I104SRV-038 a real error drops the connection and rebuilds the listener on the spot (no gap in service)"); reset_all(); s_listen = 5; @@ -489,7 +489,7 @@ static void tc_liveness(void) s_conn_dead = 1u; s_service_link(1000u); CHECK((s_client == -1) && (g_n.close_calls == 1) && (g_i4_error_count == 1), - "TC-I104SRV-039 判死后由 service_link 收口(发送失败线索单独成一条)"); + "TC-I104SRV-039 after death is declared service_link does the teardown (the send-failure path is a separate case)"); reset_all(); s_listen = 5; @@ -497,7 +497,7 @@ static void tc_liveness(void) g_lk_state = IEC104_ST_IDLE; /* 链路层自己回到了未建立 */ s_service_link(1000u); CHECK((s_client == -1) && (g_lk_close >= 1) && (s_listen == 5), - "TC-I104SRV-040 协议状态掉回 IDLE 也要丢并重生(否则服务挂着但永不服务)"); + "TC-I104SRV-040 falling back to IDLE also drops and respawns the session (otherwise the service stays up but never serves)"); reset_all(); s_listen = 9; @@ -508,7 +508,7 @@ static void tc_liveness(void) g_n.recv_rc = 0; s_serv_poll(); CHECK((s_client == -1) && (s_listen == -1) && (g_n.close_calls == 0), - "TC-I104SRV-041 表项没了又重建失败:丢掉假监听号,下一轮继续重试(数据号与监听同号时不许关)"); + "TC-I104SRV-041 slot gone and the rebuild failed: drop the stale listen handle and retry next round (never close it while it equals the data handle)"); } int main(void) diff --git a/test/host/lftpd/lftpd_test.c b/test/host/lftpd/lftpd_test.c index c22922cd..9f94eecf 100644 --- a/test/host/lftpd/lftpd_test.c +++ b/test/host/lftpd/lftpd_test.c @@ -334,7 +334,7 @@ void vTaskDelay(TickType_t t) (void)t; g_n.delay_calls++; if (g_n.delay_calls > 200) { - printf(" ABORT 循环跑飞(>200 轮)\n"); + printf(" ABORT loop runaway (>200 rounds)\n"); fflush(stdout); exit(2); } @@ -450,46 +450,46 @@ static void tc_gates(void) (void)handle_control_channel(&g_cli); CHECK((rep_has("220 ") == 1) && (rep_has("530 ") == 3) && (g_n.accept_calls == 0), - "TC-LFTPD-001 未登录只放行 USER/PASS/QUIT/FEAT/SYST/NOOP,其它一律 530"); + "TC-LFTPD-001 before login only USER/PASS/QUIT/FEAT/SYST/NOOP pass, everything else gets 530"); reset_all(); feed("ABCDEFG", "NOOP", NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((rep_has("500 ") == 1) && (rep_has("200 ") == 1), - "TC-LFTPD-002 命令名 >=5 字符先拒(command_tmp 只有 5 格,防 memcpy 越界)"); + "TC-LFTPD-002 a command name of 5 characters or more is rejected first (command_tmp has only 5 slots, this keeps memcpy in bounds)"); reset_all(); g_cli.auth_logged_in = 1; /* 否则会被认证白名单先以 530 拦掉,测不到分派 */ feed("FOO bar", NULL, NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((rep_has("502 ") == 1) && (rep_has("530 ") == 0), - "TC-LFTPD-003 认不出的命令回 502 而不是断开"); + "TC-LFTPD-003 an unknown command answers 502 instead of dropping the connection"); reset_all(); feed("QUIT", "NOOP", NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((rep_has("221 ") == 1) && (rep_has("200 ") == 0) && (g_n.line_i == 1), - "TC-LFTPD-004 QUIT 之后不再读下一条(会话就此结束)"); + "TC-LFTPD-004 after QUIT no further command is read (the session ends here)"); reset_all(); g_n.string_rc = -1; /* 220 都送不出去 */ feed("NOOP", NULL, NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((g_n.line_i == 0) && (g_lg_info_count >= 1), - "TC-LFTPD-005 欢迎未送达(对端已走)就直接收工,不空转读命令(该留痕按真实分级算 INFO)"); + "TC-LFTPD-005 if the welcome never got through (peer already gone) finish straight away instead of spinning on reads; that trace really is INFO"); reset_all(); g_cli.data_socket = 33; /* 被动模式已开但客户端直接跑了 */ feed("QUIT", NULL, NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((g_n.close_calls == 1) && (g_cli.data_socket == -1), - "TC-LFTPD-006 退出时必须释放还开着的**数据监听**(单连接 socket 不能留残口)"); + "TC-LFTPD-006 on exit the still-open data listener must be released (a single-socket chip must not be left with a dangling socket)"); reset_all(); g_cli.auth_logged_in = 1; feed("noop", NULL, NULL, NULL); (void)handle_control_channel(&g_cli); - CHECK(rep_has("200 ") == 1, "TC-LFTPD-007 命令名大小写不敏感"); + CHECK(rep_has("200 ") == 1, "TC-LFTPD-007 command names are case insensitive"); reset_all(); g_cli.auth_logged_in = 1; @@ -499,7 +499,7 @@ static void tc_gates(void) feed("LIST", NULL, NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((rep_has("425 ") == 1) && (g_n.accept_calls == 0), - "TC-LFTPD-010 没先开被动模式的数据命令一律 425,且不去 accept"); + "TC-LFTPD-010 a data command without passive mode set first gets 425 and never reaches accept"); } /* ===================== 被动模式与数据通道 ===================== */ @@ -514,7 +514,7 @@ static void tc_passive(void) (void)cmd_pasv(&g_cli, NULL); CHECK((strcmp(rep(0), "227 Entering Passive Mode (192,168,1,1,19,136).\r\n") == 0) && (g_cli.data_socket == 20) && (g_n.listen_calls == 1), - "TC-LFTPD-011 227 六元组:IP 经 htonl 后按高字节到低字节(点分序)、端口高/低,并记下被动监听号"); + "TC-LFTPD-011 the 227 tuple: ip through htonl then high byte to low byte (dotted order), port high/low, and the passive listen handle is recorded"); reset_all(); g_cli.auth_logged_in = 1; @@ -523,14 +523,14 @@ static void tc_passive(void) (void)handle_control_channel(&g_cli); CHECK((rep_has("425 ") == 1) && (g_n.close_calls == 1) && (g_cli.data_socket == -1), - "TC-LFTPD-012 拿不到本机 IP 时要把刚建的监听关掉(别留半个被动模式)"); + "TC-LFTPD-012 when the local ip is unavailable the listener just created must be closed (no half-passive mode left behind)"); reset_all(); g_cli.auth_logged_in = 1; feed("EPSV", NULL, NULL, NULL); (void)handle_control_channel(&g_cli); CHECK(strcmp(rep(1), "229 Entering Extended Passive Mode (|||5000|).\r\n") == 0, - "TC-LFTPD-013 EPSV 只报端口(IPv6 客户端自己沿用控制连接地址)"); + "TC-LFTPD-013 EPSV reports only the port (an IPv6 client reuses the control connection address)"); reset_all(); g_cli.auth_logged_in = 1; @@ -540,7 +540,7 @@ static void tc_passive(void) (void)handle_control_channel(&g_cli); CHECK((rep_has("150 ") == 1) && (rep_has("425 ") == 1) && (g_cli.data_socket == -1) && (g_n.close_calls == 1), - "TC-LFTPD-014 数据 accept 失败:报 425 并**关掉监听复位**,不留给下一条命令"); + "TC-LFTPD-014 a failed data accept reports 425 and closes and resets the listener instead of leaving it to the next command"); reset_all(); g_cli.auth_logged_in = 1; @@ -550,14 +550,14 @@ static void tc_passive(void) feed("PASV", "LIST", NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((rep_has("226 ") == 1) && (g_n.data_write_rc == 0), - "TC-LFTPD-015 LIST 成功:先 150、数据口写目录、再 226 收尾"); + "TC-LFTPD-015 LIST success: 150 first, the directory goes out on the data socket, then 226 closes it"); reset_all(); g_cli.auth_logged_in = 1; g_io.opendir_rc = 1; /* 目录打不开 */ feed("PASV", "LIST", NULL, NULL); (void)handle_control_channel(&g_cli); - CHECK(rep_has("550 ") == 1, "TC-LFTPD-016 目录打不开时 LIST 回 550(不是 226)"); + CHECK(rep_has("550 ") == 1, "TC-LFTPD-016 when the directory cannot be opened LIST answers 550 (not 226)"); } /* ===================== 传输与写开关 ===================== */ @@ -571,7 +571,7 @@ static void tc_transfer(void) feed("PASV", "RETR a.txt", NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((rep_has("226 ") == 1) && (g_lg_info_count >= 1) && (g_wd_beats >= 1), - "TC-LFTPD-017 RETR 成功:226 收尾、留 DOWNLOAD 记录,且传输循环有喂狗"); + "TC-LFTPD-017 RETR success: 226 at the end, a DOWNLOAD record is kept and the transfer loop feeds the watchdog"); reset_all(); g_cli.auth_logged_in = 1; @@ -579,7 +579,7 @@ static void tc_transfer(void) feed("PASV", "RETR gone.txt", NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((rep_has("450 ") == 1) && (g_lg_error_count >= 1), - "TC-LFTPD-018 文件打不开回 450(传输失败与命令不合法要分开报)"); + "TC-LFTPD-018 a file that cannot be opened answers 450 (a transfer failure and an invalid command must be reported apart)"); reset_all(); g_cli.auth_logged_in = 1; @@ -588,7 +588,7 @@ static void tc_transfer(void) feed("PASV", "STOR up.bin", NULL, NULL); (void)handle_control_channel(&g_cli); CHECK((g_io.unlink_calls == 1) && (rep_has("450 ") == 1), - "TC-LFTPD-019 STOR 中途失败必须**删掉半成品**(否则 Flash 上永久留个坏文件)"); + "TC-LFTPD-019 a STOR failing halfway must delete the partial file (otherwise a broken file stays on flash forever)"); reset_all(); g_cli.auth_logged_in = 1; @@ -598,27 +598,27 @@ static void tc_transfer(void) (void)handle_control_channel(&g_cli); CHECK((rep_has("550 ") == 1) && (g_n.accept_calls == 0) && (g_io.open_write_rc == 0) && (g_io.unlink_calls == 0), - "TC-LFTPD-020 写开关关闭时 STOR 在碰文件之前就拒绝,且不开数据通道"); + "TC-LFTPD-020 with writes disabled STOR is refused before the file is touched and no data channel is opened"); reset_all(); g_cli.auth_logged_in = 1; g_cfg.write_en = 0u; (void)cmd_dele(&g_cli, "a.txt"); - CHECK(rep_has("550 ") == 1, "TC-LFTPD-021 写开关同样挡住 DELE"); + CHECK(rep_has("550 ") == 1, "TC-LFTPD-021 the write switch blocks DELE as well"); reset_all(); g_cli.auth_logged_in = 1; g_io.is_dir = 1; (void)cmd_dele(&g_cli, "sub"); CHECK((rep_has("550 ") == 1) && (g_io.unlink_calls == 0), - "TC-LFTPD-022 DELE 不许删目录(本实现只支持删文件)"); + "TC-LFTPD-022 DELE must not delete directories (this implementation only deletes files)"); reset_all(); g_cli.auth_logged_in = 1; g_io.is_dir = 0; (void)cmd_dele(&g_cli, "a.txt"); CHECK((g_io.unlink_calls == 1) && (rep_has("250 ") == 1), - "TC-LFTPD-022b 删普通文件走 unlink 并回 250"); + "TC-LFTPD-022b deleting a regular file goes through unlink and answers 250"); } /* ===================== 目录与元信息 ===================== */ @@ -629,58 +629,58 @@ static void tc_meta(void) reset_all(); (void)cmd_cwd(&g_cli, ""); CHECK((rep_has("550 ") == 1) && (strcmp(g_cli.directory, "/") == 0), - "TC-LFTPD-023 CWD 空参数拒绝,当前目录不变"); + "TC-LFTPD-023 CWD with an empty argument is refused and the current directory stays"); reset_all(); g_io.is_dir = 0; (void)cmd_cwd(&g_cli, "a.txt"); - CHECK(rep_has("550 ") == 1, "TC-LFTPD-023b CWD 到普通文件被拒(is_dir 必须真为目录)"); + CHECK(rep_has("550 ") == 1, "TC-LFTPD-023b CWD onto a regular file is refused (is_dir must really be a directory)"); reset_all(); g_io.is_dir = 1; (void)cmd_cwd(&g_cli, "logs"); CHECK((rep_has("250 ") == 1) && (strcmp(g_cli.directory, "//logs") == 0) && (g_free_calls == 1), - "TC-LFTPD-024 CWD 成功后更新当前目录并释放路径缓冲(不泄漏堆)"); + "TC-LFTPD-024 a successful CWD updates the current directory and frees the path buffer (no heap leak)"); reset_all(); (void)cmd_cwd(&g_cli, "/"); CHECK((rep_has("250 ") == 1) && (g_io.stat_rc == 0), - "TC-LFTPD-024b 回到根目录免 stat 直接放行(根一定存在)"); + "TC-LFTPD-024b going back to the root skips stat and succeeds directly (the root always exists)"); reset_all(); g_io.is_dir = 0; g_io.size = 12345u; (void)cmd_size(&g_cli, "up.bin"); - CHECK(strcmp(rep(0), "213 12345\r\n") == 0, "TC-LFTPD-025 SIZE 报字节数"); + CHECK(strcmp(rep(0), "213 12345\r\n") == 0, "TC-LFTPD-025 SIZE reports the byte count"); reset_all(); g_io.stat_rc = -1; (void)cmd_size(&g_cli, "nope"); - CHECK(rep_has("550 ") == 1, "TC-LFTPD-025b 文件不存在时 SIZE 回 550"); + CHECK(rep_has("550 ") == 1, "TC-LFTPD-025b when the file is missing SIZE answers 550"); reset_all(); (void)strcpy(g_cli.directory, "/logs"); (void)cmd_pwd(&g_cli, NULL); CHECK(strcmp(rep(0), "257 \"/logs\"\r\n") == 0, - "TC-LFTPD-026 PWD 用双引号包住目录(RFC 959 要求)"); + "TC-LFTPD-026 PWD wraps the directory in double quotes (required by RFC 959)"); reset_all(); (void)cmd_syst(&g_cli, NULL); CHECK(strcmp(rep(0), "215 UNIX Type: L8\r\n") == 0, - "TC-LFTPD-027 SYST 固定报 UNIX Type: L8(客户端据此决定模式)"); + "TC-LFTPD-027 SYST always reports UNIX Type: L8 (clients pick their mode from it)"); reset_all(); (void)cmd_feat(&g_cli, NULL); CHECK((strncmp(rep(0), "211-", 4) == 0) && (strcmp(rep(1), "EPSV\r\n") == 0) && (strcmp(rep(2), "PASV\r\n") == 0) && (strncmp(rep(5), "211 ", 4) == 0) && (g_n.reply_n == 6), - "TC-LFTPD-028 FEAT 多行:首行 211-、四条能力无状态码、末行 211 结束"); + "TC-LFTPD-028 FEAT multiline: first line 211-, four capability lines without a status code, last line 211 ends it"); reset_all(); (void)cmd_type(&g_cli, "I"); (void)cmd_noop(&g_cli, NULL); - CHECK(rep_has("200 ") == 2, "TC-LFTPD-030 TYPE 与 NOOP 都回 200"); + CHECK(rep_has("200 ") == 2, "TC-LFTPD-030 both TYPE and NOOP answer 200"); } /* ===================== 登录序列 ===================== */ @@ -693,7 +693,7 @@ static void tc_login(void) (void)handle_control_channel(&g_cli); CHECK((rep_has("331 ") == 1) && (rep_has("230 ") == 1) && (g_cli.auth_logged_in == 1u) && (strcmp(g_cli.auth_user, "admin") == 0), - "TC-LFTPD-031 USER 记下用户名与匹配位,PASS 过了就转已登录"); + "TC-LFTPD-031 USER records the user name and the match bit; a passing PASS switches to logged in"); reset_all(); g_auth_res = FTP_AUTH_FAIL; @@ -701,7 +701,7 @@ static void tc_login(void) (void)handle_control_channel(&g_cli); CHECK((rep_has("530 ") == 1) && (g_cli.auth_logged_in == 0u) && (g_lg_info_count >= 1), - "TC-LFTPD-032 口令错:530 且**必须重新走 USER**(auth_user_ok 已清零)"); + "TC-LFTPD-032 a wrong password gives 530 and USER must be repeated (auth_user_ok was cleared)"); reset_all(); g_auth_res = FTP_AUTH_LOCKED; @@ -709,7 +709,7 @@ static void tc_login(void) feed("USER admin", "PASS bad", NULL, NULL); (void)handle_control_channel(&g_cli); CHECK(rep_has("530 ") == 1, - "TC-LFTPD-033 连错被锁时同样只回 530(不给攻击者区分提示)"); + "TC-LFTPD-033 while locked out after repeated failures it also answers only 530 (no distinguishing hint for an attacker)"); reset_all(); g_cfg_present = 0; /* 配置读不到 */ @@ -717,7 +717,7 @@ static void tc_login(void) (void)handle_control_channel(&g_cli); CHECK((rep_has("331 ") == 1) && (rep_has("530 ") == 1) && (g_cli.auth_user_ok == 0u), - "TC-LFTPD-034 配置缺失时 USER 无法匹配、PASS 直接 530(fail-closed)"); + "TC-LFTPD-034 with missing configuration USER cannot match and PASS answers 530 straight away (fail-closed)"); } int main(void) diff --git a/test/host/lftpd_inet/lftpd_inet_test.c b/test/host/lftpd_inet/lftpd_inet_test.c index 17687bb5..c2d81cde 100644 --- a/test/host/lftpd_inet/lftpd_inet_test.c +++ b/test/host/lftpd_inet/lftpd_inet_test.c @@ -241,7 +241,7 @@ void vTaskDelay(TickType_t xTicksToDelay) if (g_net.delay_calls > 100) { /* 跑飞保险:本模块每条用例都必须在有限轮内退出(accept 序列要么成功、 * 要么撤防、要么代次变化)。真跑飞就退出,绝不把挂死留给 pre-commit。 */ - printf(" ABORT accept/等待循环跑飞(>100 轮未退出)\n"); + printf(" ABORT accept/wait loop runaway (>100 rounds without exit)\n"); fflush(stdout); exit(2); } @@ -262,32 +262,32 @@ static void tc_listen(void) net_reset(); logs_clear(); g_net.socket_rc = -1; CHECK((lftpd_inet_listen(21) == -1) && (g_net.bind_calls == 0) && - (g_lfi_error_count == 1), "TC-LFTINET-001 建 socket 失败即返回,不再 bind"); + (g_lfi_error_count == 1), "TC-LFTINET-001 a socket creation failure returns at once, no bind attempt"); net_reset(); logs_clear(); g_net.bind_rc = -1; CHECK((lftpd_inet_listen(21) == -1) && (g_net.close_calls == 1) && - (g_net.listen_calls == 0), "TC-LFTINET-002 bind 失败要把 socket 关掉"); + (g_net.listen_calls == 0), "TC-LFTINET-002 a bind failure must close the socket"); net_reset(); logs_clear(); g_net.listen_rc = -1; CHECK((lftpd_inet_listen(21) == -1) && (g_net.close_calls == 1) && - (g_lfi_error_count == 1), "TC-LFTINET-003 listen 失败同样关socket 并留痕"); + (g_lfi_error_count == 1), "TC-LFTINET-003 a listen failure also closes the socket and logs it"); net_reset(); logs_clear(); CHECK((lftpd_inet_listen(21) == 3) && (g_net.last_domain == NET_AF_INET) && (g_net.last_type == NET_SOCK_STREAM) && (g_net.last_bind_port_net == net_htons(21u)) && (g_net.listen_calls == 1), - "TC-LFTINET-004 流式监听口,且**端口过了 htons**(sin_port 是网络序)"); + "TC-LFTINET-004 a stream listener whose port went through htons (sin_port is network order)"); net_reset(); logs_clear(); g_net.getsock_rc = -1; CHECK((lftpd_inet_get_socket_port(3) == -1) && (g_lfi_error_count == 1), - "TC-LFTINET-005 问不到名字就报 -1(不拿脏端口当结果)"); + "TC-LFTINET-005 a failing getsockname returns -1 (never report a stale port as the result)"); net_reset(); logs_clear(); g_net.getsock_port_net = net_htons(8888u); CHECK(lftpd_inet_get_socket_port(3) == 8888, - "TC-LFTINET-006 端口回来要 ntohs 还原成宿主序"); + "TC-LFTINET-006 the port comes back through ntohs into host order"); } /* ===================== accept:喂狗、代次、撤防 ===================== */ @@ -298,14 +298,14 @@ static void tc_accept(void) net_reset(); logs_clear(); g_net.accept_seq[0] = 9; g_net.accept_n = 1; CHECK((lftpd_inet_accept(3) == 9) && (g_net.delay_calls == 0), - "TC-LFTINET-010 一击即中不睡不等"); + "TC-LFTINET-010 an immediate hit neither sleeps nor waits"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_seq[1] = -1; g_net.accept_seq[2] = 9; g_net.accept_n = 3; CHECK((lftpd_inet_accept(3) == 9) && (g_net.delay_calls == 2) && (g_lfi_info_count == 0), - "TC-LFTINET-011 每轮失败都让出一次 CPU(同段代码里也喂狗),直到成功"); + "TC-LFTINET-011 every failed round yields the CPU (and feeds the watchdog in the same block) until it succeeds"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_seq[1] = -1; g_net.accept_seq[2] = -1; @@ -313,25 +313,25 @@ static void tc_accept(void) g_net.gen_flip_after = 3; /* 第 3 轮时网络重建 */ CHECK((lftpd_inet_accept(3) == -1) && (g_lfi_info_count == 1) && (g_net.accept_calls == 3), - "TC-LFTINET-012 **代次一变立刻撤掉 accept**(否则新 IP 生效后监听还挂在旧代次)"); + "TC-LFTINET-012 a generation change aborts accept immediately (otherwise the listener stays on the old generation after the new IP takes effect)"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_seq[1] = 9; g_net.accept_n = 2; CHECK((lftpd_inet_accept_timeout(3, 5000) == 9) && (g_net.delay_calls == 1), - "TC-LFTINET-020 超时窗内等到数据通道"); + "TC-LFTINET-020 the data channel is accepted inside the timeout window"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_seq[1] = -1; g_net.accept_n = 2; g_net.armed = 0; /* 监听门被撤(会话已放弃) */ CHECK((lftpd_inet_accept_timeout(3, 5000) == -1) && (g_net.accept_calls == 1) && (g_lfi_error_count == 1), - "TC-LFTINET-021 监听门撤防立即放弃,不对着死监听耗满 5 s"); + "TC-LFTINET-021 lowering the listener gate gives up at once instead of burning the full 5 s on a dead listener"); net_reset(); logs_clear(); g_net.accept_seq[0] = -1; g_net.accept_n = 1; CHECK((lftpd_inet_accept_timeout(3, 300) == -1) && (g_net.delay_calls == 3) && (g_lfi_info_count == 1), - "TC-LFTINET-022 对端一直不开数据通道:按 100ms 步进等满后 INFO 收场"); + "TC-LFTINET-022 the peer never opens the data channel: step by 100ms to the limit and finish as INFO"); } /* ===================== read_line:分片、截断、预算 ===================== */ @@ -347,38 +347,38 @@ static void tc_read_line(void) g_net.recv_seq[0] = 12; g_net.recv_n = 1; rc = lftpd_inet_read_line(3, b, sizeof(b)); CHECK((rc == 0) && (strcmp(b, "USER admin") == 0), - "TC-LFTINET-030 一次到位:CRLF 就地截成结束符(FTP 命令行的边界)"); + "TC-LFTINET-030 one shot: CRLF is cut into the terminator in place (the FTP command line boundary)"); net_reset(); logs_clear(); (void)strcpy(g_net.rx, "USER admin\r\n"); g_net.recv_seq[0] = 4; g_net.recv_seq[1] = 8; g_net.recv_n = 2; rc = lftpd_inet_read_line(3, b, sizeof(b)); CHECK((rc == 0) && (strcmp(b, "USER admin") == 0), - "TC-LFTINET-031 分两片到达也要拼出整行(跨包命令行)"); + "TC-LFTINET-031 two arriving halves are stitched into one line (a command line spanning packets)"); net_reset(); logs_clear(); g_net.recv_seq[0] = 0; g_net.recv_n = 1; CHECK(lftpd_inet_read_line(3, b, sizeof(b)) == -1, - "TC-LFTINET-032 读到 0(对端关闭)立刻失败,不进死循环"); + "TC-LFTINET-032 reading 0 (peer closed) fails at once and must not spin"); net_reset(); logs_clear(); (void)strcpy(g_net.rx, "NOOP\r\n"); g_net.recv_seq[0] = -2; g_net.recv_seq[1] = 6; g_net.recv_n = 2; rc = lftpd_inet_read_line(3, b, sizeof(b)); CHECK((rc == 0) && (strcmp(b, "NOOP") == 0), - "TC-LFTINET-033 -2 表示这一片超时,继续等而不是当错误"); + "TC-LFTINET-033 -2 means this slice timed out: keep waiting instead of treating it as an error"); net_reset(); logs_clear(); g_net.recv_seq[0] = -5; g_net.recv_n = 1; CHECK(lftpd_inet_read_line(3, b, sizeof(b)) == -5, - "TC-LFTINET-034 其它负值**原样返回**(错误码不许被吞成 -1)"); + "TC-LFTINET-034 any other negative value is returned verbatim (an error code must not be swallowed into -1)"); net_reset(); logs_clear(); g_net.recv_seq[0] = -2; g_net.recv_n = 1; /* 一直超时,每轮耗 200ms */ rc = lftpd_inet_read_line(3, b, sizeof(b)); CHECK((rc == -1) && (g_net.recv_calls == 150) && (g_net.last_recv_timeout <= 200u), - "TC-LFTINET-035 30 s 总预算按 200ms 分片累加,到点退出(饿不到看门狗)"); + "TC-LFTINET-035 the 30 s total budget accumulates in 200ms slices and exits on time (the watchdog never starves)"); net_reset(); logs_clear(); (void)memset(g_net.rx, 'A', 40); /* 没有 CRLF 的长行 */ @@ -386,7 +386,7 @@ static void tc_read_line(void) (void)memset(b, 0, sizeof(b)); rc = lftpd_inet_read_line(3, b, 8); CHECK((rc == -1) && (b[7] == '\0'), - "TC-LFTINET-036 缓冲填满仍无行尾:失败且**不越界写**(留最后一格给结束符)"); + "TC-LFTINET-036 buffer full with no line end: fail without writing out of bounds (the last byte is reserved for the terminator)"); } /* ===================== 写与读 ===================== */ @@ -400,45 +400,45 @@ static void tc_write_read(void) net_reset(); logs_clear(); g_net.send_seq[0] = 12; g_net.send_n = 1; CHECK((lftpd_inet_write_string(3, msg) == 0) && (g_net.send_calls == 1), - "TC-LFTINET-040 一次写完"); + "TC-LFTINET-040 everything written in a single call"); net_reset(); logs_clear(); g_net.send_seq[0] = 5; g_net.send_seq[1] = 7; g_net.send_n = 2; CHECK((lftpd_inet_write_string(3, msg) == 0) && (g_net.send_calls == 2) && (g_net.send_off[1] == 5), - "TC-LFTINET-041 短写后续传从**正确的偏移**接着写(少半条响应就是协议错乱)"); + "TC-LFTINET-041 after a short write the continuation resumes at the correct offset (half a response is a protocol break)"); net_reset(); logs_clear(); g_net.send_seq[0] = 0; g_net.send_n = 1; CHECK((lftpd_inet_write_string(3, msg) == -1) && (g_lfi_error_count == 1), - "TC-LFTINET-042 写 0 字节按失败处理(不是当成成功写了 0 字节)"); + "TC-LFTINET-042 writing 0 bytes counts as a failure (not a successful zero-byte write)"); net_reset(); logs_clear(); g_net.send_seq[0] = -1; g_net.send_n = 1; - CHECK(lftpd_inet_write(3, msg, 12) == -1, "TC-LFTINET-043 写失败原样回 -1"); + CHECK(lftpd_inet_write(3, msg, 12) == -1, "TC-LFTINET-043 a write failure returns -1 as is"); net_reset(); logs_clear(); g_net.send_seq[0] = 4; g_net.send_seq[1] = 6; g_net.send_n = 2; CHECK((lftpd_inet_write(3, msg, 10) == 10) && (g_net.send_calls == 2), - "TC-LFTINET-050 二进制写也走短写续传,成功时回**请求长度**"); + "TC-LFTINET-050 binary writes also resume after short writes and return the requested length on success"); net_reset(); logs_clear(); (void)strcpy(g_net.rx, "STOR\r\n"); g_net.recv_seq[0] = 4; g_net.recv_n = 1; - CHECK(lftpd_inet_read(3, b, sizeof(b)) == 4, "TC-LFTINET-060 读到数据立刻交还"); + CHECK(lftpd_inet_read(3, b, sizeof(b)) == 4, "TC-LFTINET-060 data read is handed back immediately"); net_reset(); logs_clear(); g_net.recv_seq[0] = -1; g_net.recv_n = 1; - CHECK(lftpd_inet_read(3, b, sizeof(b)) == -1, "TC-LFTINET-061 -1 直接失败返回"); + CHECK(lftpd_inet_read(3, b, sizeof(b)) == -1, "TC-LFTINET-061 -1 fails and returns directly"); net_reset(); logs_clear(); g_net.recv_seq[0] = -3; g_net.recv_n = 1; CHECK((lftpd_inet_read(3, b, sizeof(b)) == -1) && (g_net.recv_calls > 1), - "TC-LFTINET-062 其它负值继续等,直到 30 s 预算耗尽"); + "TC-LFTINET-062 other negative values keep waiting until the 30 s budget is exhausted"); net_reset(); logs_clear(); CHECK((lftpd_inet_close(3) == 0) && (g_net.close_calls == 1), - "TC-LFTINET-070 close 就是转 net_close"); + "TC-LFTINET-070 close only forwards to net_close"); } int main(void) diff --git a/test/host/lftpd_io/lftpd_io_test.c b/test/host/lftpd_io/lftpd_io_test.c index 40d3c8ba..9e24ae0f 100644 --- a/test/host/lftpd_io/lftpd_io_test.c +++ b/test/host/lftpd_io/lftpd_io_test.c @@ -276,11 +276,11 @@ static void tc_fatfs_path(void) { printf("-- TC-LFTPIO-001..004 to_fatfs_path --\n"); CHECK((strcmp(to_fatfs_path(NULL), "0:") == 0) && - (strcmp(to_fatfs_path(""), "0:") == 0), "TC-LFTPIO-001 NULL/空串 -> 0:"); - CHECK(strcmp(to_fatfs_path("/"), "0:") == 0, "TC-LFTPIO-002 根 -> 0:"); + (strcmp(to_fatfs_path(""), "0:") == 0), "TC-LFTPIO-001 NULL/empty string -> 0:"); + CHECK(strcmp(to_fatfs_path("/"), "0:") == 0, "TC-LFTPIO-002 root -> 0:"); CHECK(strcmp(to_fatfs_path("/a/b.txt"), "a/b.txt") == 0, - "TC-LFTPIO-003 去掉前导斜杠"); - CHECK(strcmp(to_fatfs_path("a.txt"), "a.txt") == 0, "TC-LFTPIO-004 原样返回"); + "TC-LFTPIO-003 the leading slash is stripped"); + CHECK(strcmp(to_fatfs_path("a.txt"), "a.txt") == 0, "TC-LFTPIO-004 returned unchanged"); } /* ===================== 路径规范化 ===================== */ @@ -289,32 +289,32 @@ static void tc_canon(void) { printf("-- TC-LFTPIO-010..022 canonicalize --\n"); reset_all(); - CHECK(strcmp(canon("/dir", "f.txt"), "/dir/f.txt") == 0, "TC-LFTPIO-010 相对拼接"); - CHECK(strcmp(canon("/dir", "/abs.txt"), "/abs.txt") == 0, "TC-LFTPIO-011 绝对忽略 base"); - CHECK(strcmp(canon("/a/b", "../c"), "/a/c") == 0, "TC-LFTPIO-012 上跳一级"); + CHECK(strcmp(canon("/dir", "f.txt"), "/dir/f.txt") == 0, "TC-LFTPIO-010 relative join"); + CHECK(strcmp(canon("/dir", "/abs.txt"), "/abs.txt") == 0, "TC-LFTPIO-011 an absolute name ignores base"); + CHECK(strcmp(canon("/a/b", "../c"), "/a/c") == 0, "TC-LFTPIO-012 go up one level"); CHECK(strcmp(canon("/a", "../../x"), "/x") == 0, - "TC-LFTPIO-013 上跳过头仍被钳在根内(安全属性)"); - CHECK(strcmp(canon("/a", "."), "/a") == 0, "TC-LFTPIO-014 丢弃 . 段"); - CHECK(strcmp(canon("/a", ".."), "/") == 0, "TC-LFTPIO-015 回退到根"); - CHECK(strcmp(canon("/a//", "b"), "/a/b") == 0, "TC-LFTPIO-016 段间多余斜杠被折叠"); + "TC-LFTPIO-013 over-shooting .. is still clamped inside the root (safety property)"); + CHECK(strcmp(canon("/a", "."), "/a") == 0, "TC-LFTPIO-014 a . segment is dropped"); + CHECK(strcmp(canon("/a", ".."), "/") == 0, "TC-LFTPIO-015 .. at the top falls back to the root"); + CHECK(strcmp(canon("/a//", "b"), "/a/b") == 0, "TC-LFTPIO-016 redundant slashes between segments are collapsed"); CHECK(strcmp(canon("/dir", "//x"), "/x") == 0, - "TC-LFTPIO-016b name 以斜杠开头按**绝对路径**处理(忽略 base,仍钳在根内)"); - CHECK(strcmp(canon(NULL, "x"), "/x") == 0, "TC-LFTPIO-017 base NULL 视作空串"); - CHECK(strcmp(canon("/a", NULL), "/a") == 0, "TC-LFTPIO-018 name NULL 视作空串"); - CHECK(strcmp(canon(NULL, NULL), "/") == 0, "TC-LFTPIO-019 两个 NULL -> /"); - CHECK(strcmp(canon("/", ".."), "/") == 0, "TC-LFTPIO-020 根上加 .. 仍是根"); + "TC-LFTPIO-016b a name starting with a slash is treated as an absolute path (base ignored, still clamped inside the root)"); + CHECK(strcmp(canon(NULL, "x"), "/x") == 0, "TC-LFTPIO-017 a NULL base counts as an empty string"); + CHECK(strcmp(canon("/a", NULL), "/a") == 0, "TC-LFTPIO-018 a NULL name counts as an empty string"); + CHECK(strcmp(canon(NULL, NULL), "/") == 0, "TC-LFTPIO-019 both NULL -> /"); + CHECK(strcmp(canon("/", ".."), "/") == 0, "TC-LFTPIO-020 .. on the root is still the root"); reset_all(); g_fail_at = 1; CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL, - "TC-LFTPIO-021 首块分配失败返回 NULL"); - CHECK(g_alloc_calls == 1, "TC-LFTPIO-021b 失败后不再分配第二块"); + "TC-LFTPIO-021 the first allocation failing returns NULL"); + CHECK(g_alloc_calls == 1, "TC-LFTPIO-021b no second allocation is attempted after that failure"); reset_all(); g_fail_at = 2; CHECK(lftpd_io_canonicalize_path("/a", "b") == NULL, - "TC-LFTPIO-022 结果缓冲分配失败返回 NULL"); - CHECK(g_free_calls == 1, "TC-LFTPIO-022b 第一块已释放(不泄漏堆)"); + "TC-LFTPIO-022 the result buffer allocation failing returns NULL"); + CHECK(g_free_calls == 1, "TC-LFTPIO-022b the first block was freed (no heap leak)"); } /* ===================== 打开文件:守卫 / 卷锁 / 错误通道 ===================== */ @@ -329,24 +329,24 @@ static void tc_open(void) rc = lftpd_io_open_read("/a/b.txt"); CHECK((rc == 0) && (g_open_mode == FA_READ) && (strcmp(g_open_path, "a/b.txt") == 0), - "TC-LFTPIO-030 成功:FA_READ + 映射后的路径"); + "TC-LFTPIO-030 success: FA_READ plus the mapped path"); reset_all(); s_file_open = 1; rc = lftpd_io_open_read("/x"); CHECK((rc == -1) && (g_open_calls == 0) && (g_wait_calls == 0), - "TC-LFTPIO-031 单文件槽守卫:已打开时不碰卷锁与 FatFs"); + "TC-LFTPIO-031 single-file slot guard: while a file is open it touches neither the volume lock nor FatFs"); reset_all(); g_open_rc = FR_NO_FILE; rc = lftpd_io_open_read("/x"); CHECK((rc == -1) && (g_release_calls == 1) && (g_lfio_error_count == 1), - "TC-LFTPIO-032 打开失败:返回 -1 + 留痕 + 释放卷锁"); + "TC-LFTPIO-032 open failure: return -1 + log + release the volume lock"); reset_all(); rc = lftpd_io_open_read("/x"); CHECK((rc == 0) && (g_release_calls == 0) && (s_file_open == 1), - "TC-LFTPIO-033 成功后卷锁不释放(传输期独占)"); + "TC-LFTPIO-033 the volume lock stays held after success (exclusive for the whole transfer)"); reset_all(); g_wait_seq[0] = 0; /* 第一轮:卷被 defaultTask 占着 */ @@ -354,22 +354,22 @@ static void tc_open(void) g_wait_seq[2] = 1; /* 第三轮:拿到 */ rc = lftpd_io_open_read("/x"); CHECK((rc == 0) && (g_beat_calls == 2) && (g_wait_calls == 3), - "TC-LFTPIO-034 等锁循环每轮喂看门狗(漏喂 = 整机复位)"); + "TC-LFTPIO-034 the lock-wait loop feeds the watchdog every round (missing a feed resets the whole device)"); reset_all(); rc = lftpd_io_open_write("/up.bin"); CHECK((rc == 0) && (g_open_mode == (BYTE)(FA_WRITE | FA_CREATE_ALWAYS)), - "TC-LFTPIO-035 open_write 用覆盖写模式"); + "TC-LFTPIO-035 open_write uses overwrite mode"); reset_all(); s_file_open = 1; rc = lftpd_io_open_write("/up.bin"); - CHECK((rc == -1) && (g_open_calls == 0), "TC-LFTPIO-036 open_write 同样有槽守卫"); + CHECK((rc == -1) && (g_open_calls == 0), "TC-LFTPIO-036 open_write has the same slot guard"); reset_all(); g_open_rc = FR_DISK_ERR; rc = lftpd_io_open_write("/up.bin"); - CHECK((rc == -1) && (g_release_calls == 1), "TC-LFTPIO-037 open_write 失败也释放卷锁"); + CHECK((rc == -1) && (g_release_calls == 1), "TC-LFTPIO-037 an open_write failure also releases the volume lock"); } /* ===================== trim ===================== */ @@ -386,19 +386,19 @@ static void tc_trim(void) (void)strcpy(buf, " PASS x"); r = lftpd_string_ltrim(buf); CHECK((r != buf) && (strcmp(r, "PASS x") == 0) && (buf[0] == ' '), - "TC-LFTSTR-001 行首空白靠返回指针跳过,原缓冲不搬移"); + "TC-LFTSTR-001 leading blanks are skipped via the returned pointer, the buffer itself is not moved"); (void)strcpy(buf, "NOOP"); r = lftpd_string_ltrim(buf); - CHECK((r == buf) && (strcmp(buf, "NOOP") == 0), "TC-LFTSTR-002 无前导空白返回同一指针"); + CHECK((r == buf) && (strcmp(buf, "NOOP") == 0), "TC-LFTSTR-002 with no leading blanks the same pointer comes back"); (void)strcpy(buf, " USER admin "); r = lftpd_string_ltrim(buf); - CHECK(strcmp(r, "USER admin ") == 0, "TC-LFTSTR-003 词中间空白保留"); + CHECK(strcmp(r, "USER admin ") == 0, "TC-LFTSTR-003 blanks inside the text are kept"); (void)strcpy(buf, " \t\r\n"); r = lftpd_string_ltrim(buf); - CHECK(r[0] == '\0', "TC-LFTSTR-004 全空白 -> 空串"); + CHECK(r[0] == '\0', "TC-LFTSTR-004 all blanks -> empty string"); /* 现状桩(不是期望行为):尾部循环从 strlen(p) 起步,那里是 '\0', * isspace(0)=0 立刻退出 —— **尾部空白/CRLF 实际从未被裁**。今天无害是因为 @@ -411,7 +411,7 @@ static void tc_trim(void) (void)strcpy(buf, "LIST "); r = lftpd_string_ltrim(buf); CHECK(strcmp(r, "LIST ") == 0, - "TC-LFTSTR-005 尾随空格必须保留(ltrim 语义,RFC 959 参数本体)"); + "TC-LFTSTR-005 trailing spaces must survive (ltrim semantics, they belong to the RFC 959 argument)"); } /* ===================== 入口 ===================== */ diff --git a/test/host/net_cfg/net_cfg_test.c b/test/host/net_cfg/net_cfg_test.c index a53f0557..5143bd91 100644 --- a/test/host/net_cfg/net_cfg_test.c +++ b/test/host/net_cfg/net_cfg_test.c @@ -266,7 +266,7 @@ static void tc_defaults(void) CHECK(bytes_eq(p->ip, def_ip, 4u), "TC-NETCFG-001 ip = 192.168.1.100"); CHECK(bytes_eq(p->mask, def_mask, 4u), "TC-NETCFG-002 mask = 255.255.255.0"); CHECK(bytes_eq(p->gw, def_gw, 4u), "TC-NETCFG-003 gw = 192.168.1.1"); - CHECK(mac_eq(p->mac, zero), "TC-NETCFG-004 mac = 全零(芯片内置哨兵)"); + CHECK(mac_eq(p->mac, zero), "TC-NETCFG-004 mac = all zero (chip built-in sentinel)"); } /* ===================== 注册与 load 契约 ===================== */ @@ -279,13 +279,13 @@ static void tc_register_load(void) fake_reset(); rc = net_cfg_load(); CHECK((rc == 0) && (g_reg_n == 1) && (strcmp(g_regs[0].name, "network") == 0), - "TC-NETCFG-005 首次 load 注册 [network] 恰好一次"); + "TC-NETCFG-005 first load registers [network] exactly once"); CHECK((g_ftp_reg_calls == 1) && (g_load_calls == 1), - "TC-NETCFG-006 一并注册 [ftp],且注册在 load 之前"); + "TC-NETCFG-006 also registers [ftp], registered before load"); rc = net_cfg_load(); CHECK((rc == 0) && (g_reg_n == 1) && (g_ftp_reg_calls == 2), - "TC-NETCFG-007 重复 load 不重复注册 [network]"); + "TC-NETCFG-007 repeated load does not re-register [network]"); CHECK((strcmp(g_regs[0].name, "network") == 0) && (g_regs[0].data_size == sizeof(net_cfg_t)) && @@ -293,18 +293,18 @@ static void tc_register_load(void) (g_regs[0].p_defaults != NULL) && (g_regs[0].p_cb != NULL) && (g_regs[0].p_data == (const void *)&s_cfg), - "TC-NETCFG-008 注册内容:尺寸/键数/默认值/回调/数据指针=RAM 副本"); + "TC-NETCFG-008 registration content: size/key count/defaults/callback/data pointer = RAM copy"); fake_reset(); rc = net_cfg_load(); CHECK((rc == 0) && (g_netcfg_info_count == 1) && (g_netcfg_error_count == 0), - "TC-NETCFG-009 load 成功:返回 0 + 一条 INFO"); + "TC-NETCFG-009 load success: returns 0 + one INFO line"); fake_reset(); g_load_rc = -1; rc = net_cfg_load(); - CHECK(rc == 0, "TC-NETCFG-010 引擎 -1 时仍返回 0(RAM 恒有效契约)"); - CHECK(g_netcfg_error_count == 1, "TC-NETCFG-011 load 失败必须留 ERROR"); + CHECK(rc == 0, "TC-NETCFG-010 engine -1 still returns 0 (RAM copy is always valid, by contract)"); + CHECK(g_netcfg_error_count == 1, "TC-NETCFG-011 load failure must leave an ERROR line"); } /* ===================== RAM 副本读写 ===================== */ @@ -321,10 +321,10 @@ static void tc_set_get(void) memcpy(c.mac, mac, 6u); net_cfg_set(&c); CHECK(bytes_eq(net_cfg_get()->ip, ip, 4u) && - mac_eq(net_cfg_get()->mac, mac), "TC-NETCFG-012 set 被 get 读回"); + mac_eq(net_cfg_get()->mac, mac), "TC-NETCFG-012 set is read back by get"); net_cfg_set(NULL); - CHECK(bytes_eq(net_cfg_get()->ip, ip, 4u), "TC-NETCFG-013 set(NULL) 不改写"); + CHECK(bytes_eq(net_cfg_get()->ip, ip, 4u), "TC-NETCFG-013 set(NULL) rewrites nothing"); } /* ===================== 直接保存 ===================== */ @@ -336,13 +336,13 @@ static void tc_save_direct(void) printf("-- TC-NETCFG-014..015 save --\n"); fake_reset(); rc = net_cfg_save(); - CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-014 写盘成功返回 0"); + CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-014 write success returns 0"); fake_reset(); g_save_rc = -1; rc = net_cfg_save(); CHECK((rc == -1) && (g_netcfg_error_count == 1), - "TC-NETCFG-015 写盘失败返回 -1 并留痕"); + "TC-NETCFG-015 write failure returns -1 and leaves a trace"); } /* ===================== 跨任务保存交接状态机 ===================== */ @@ -357,40 +357,40 @@ static void tc_save_handoff(void) net_cfg_set_save_req_cb(NULL); s_save_pending = 0u; /* 白盒复位:上一组的 save() 不碰 pending/result,此处 */ s_save_result = 0u; /* 只是防止将来有人调整后漏出脏态 */ - CHECK(net_cfg_save_state() == NET_CFG_SAVE_ST_IDLE, "TC-NETCFG-020 初始 IDLE"); + CHECK(net_cfg_save_state() == NET_CFG_SAVE_ST_IDLE, "TC-NETCFG-020 initial state IDLE"); CHECK((net_cfg_save_request() == 0) && - (net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT), "TC-NETCFG-021 投递后 WAIT"); + (net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT), "TC-NETCFG-021 WAIT after posting"); net_cfg_set_save_req_cb(req_cb); g_req_cb_calls = 0; (void)net_cfg_save_request(); - CHECK(g_req_cb_calls == 1, "TC-NETCFG-022 投递经回调唤醒属主任务"); + CHECK(g_req_cb_calls == 1, "TC-NETCFG-022 posting wakes the owner task through the callback"); net_cfg_set_save_req_cb(NULL); (void)net_cfg_save_request(); CHECK((g_req_cb_calls == 1) && (net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT), - "TC-NETCFG-023 未注册回调时空操作,请求照样挂起"); + "TC-NETCFG-023 no callback registered -> no-op, request still pending"); g_save_rc = 0; g_save_calls = 0; rc = net_cfg_save_run(); - CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-024a 有请求时确实写盘一次"); + CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-024a with a pending request it really writes once"); g_save_calls = 0; rc = net_cfg_save_run(); CHECK((rc == 0) && (g_save_calls == 0), - "TC-NETCFG-024 无请求时空醒不写盘(2s 轮询不刷 Flash)"); + "TC-NETCFG-024 no request: empty wake does not write (2 s polling never flushes Flash)"); (void)net_cfg_save_request(); net_cfg_set_apply_cb(apply_cb); g_apply_calls = 0; g_save_rc = 0; rc = net_cfg_save_run(); - CHECK((rc == 0) && (s_save_pending == 0u), "TC-NETCFG-025 成功后 pending 清零"); + CHECK((rc == 0) && (s_save_pending == 0u), "TC-NETCFG-025 pending cleared after success"); CHECK((net_cfg_save_state() == NET_CFG_SAVE_ST_OK) && (s_save_busy == 0u), - "TC-NETCFG-026 成功后状态 OK 且写盘标志已清"); - CHECK(g_apply_calls == 1, "TC-NETCFG-027 成功后触发一次立即应用"); + "TC-NETCFG-026 state OK after success and the write flag is cleared"); + CHECK(g_apply_calls == 1, "TC-NETCFG-027 success triggers one immediate apply"); (void)net_cfg_save_request(); g_save_hook = save_hook_probe; @@ -399,7 +399,7 @@ static void tc_save_handoff(void) g_save_rc = 0; rc = net_cfg_save_run(); CHECK((rc == 0) && (g_state_during_save == (int)NET_CFG_SAVE_ST_WAIT), - "TC-NETCFG-028 写盘进行中状态仍是 WAIT"); + "TC-NETCFG-028 state stays WAIT while the write is in progress"); /* 写盘期间再投递一次:本轮不得把它当已处理,下轮必须真的再写 */ g_requeue_during_save = 1; @@ -407,12 +407,12 @@ static void tc_save_handoff(void) g_save_calls = 0; rc = net_cfg_save_run(); CHECK((rc == 0) && (net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT), - "TC-NETCFG-029a 写盘期间的新请求没丢"); + "TC-NETCFG-029a a new request during the write is not lost"); g_requeue_during_save = 0; g_save_hook = NULL; g_save_calls = 0; rc = net_cfg_save_run(); - CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-029b 下一轮真的再写一次"); + CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-029b the next round really writes once more"); /* 写盘失败:置 FAIL 且**不得**触发应用(内存值没落盘,网络不能重建) */ s_save_pending = 0u; @@ -423,20 +423,20 @@ static void tc_save_handoff(void) rc = net_cfg_save_run(); CHECK((rc == -1) && (net_cfg_save_state() == NET_CFG_SAVE_ST_FAIL) && (s_save_busy == 0u), - "TC-NETCFG-030 失败置 FAIL 且写盘标志已清(否则 UI 永远卡 WAIT)"); - CHECK(g_apply_calls == 0, "TC-NETCFG-031 写盘失败不得触发立即应用"); + "TC-NETCFG-030 failure sets FAIL and clears the write flag (else UI sticks in WAIT forever)"); + CHECK(g_apply_calls == 0, "TC-NETCFG-031 write failure must not trigger an immediate apply"); net_cfg_set_apply_cb(NULL); g_apply_calls = 0; net_cfg_request_apply(); - CHECK(g_apply_calls == 0, "TC-NETCFG-032 未注册应用回调时是空操作"); + CHECK(g_apply_calls == 0, "TC-NETCFG-032 apply is a no-op while no callback is registered"); net_cfg_set_apply_cb(apply_cb); net_cfg_request_apply(); - CHECK(g_apply_calls == 1, "TC-NETCFG-033a 注册后可被触发"); + CHECK(g_apply_calls == 1, "TC-NETCFG-033a after registering it can be triggered"); net_cfg_set_apply_cb(NULL); net_cfg_request_apply(); - CHECK(g_apply_calls == 1, "TC-NETCFG-033b 注销后不再被触发"); + CHECK(g_apply_calls == 1, "TC-NETCFG-033b after unregistering it is no longer triggered"); } /* ===================== MAC 三态优先级 ===================== */ @@ -457,30 +457,30 @@ static void tc_mac_priority(void) net_cfg_set_active_mac(shadow); memset(out, 0xFF, sizeof(out)); net_cfg_get_mac(out); - CHECK(mac_eq(out, stored), "TC-NETCFG-040 存储非零优先于芯片影子"); + CHECK(mac_eq(out, stored), "TC-NETCFG-040 stored non-zero takes precedence over the chip shadow"); c = *net_cfg_get(); memcpy(c.mac, zero, 6u); net_cfg_set(&c); memset(out, 0xFF, sizeof(out)); net_cfg_get_mac(out); - CHECK(mac_eq(out, shadow), "TC-NETCFG-041 存储全零则用芯片回写影子"); + CHECK(mac_eq(out, shadow), "TC-NETCFG-041 stored all zero -> use the chip shadow write-back"); s_active_valid = 0u; /* 白盒:回到"芯片还没回写"的上电初态 */ memset(out, 0xFF, sizeof(out)); net_cfg_get_mac(out); - CHECK(mac_eq(out, zero), "TC-NETCFG-042 两者皆无 → 全零"); + CHECK(mac_eq(out, zero), "TC-NETCFG-042 neither available -> all zero"); net_cfg_set_active_mac(NULL); - CHECK(s_active_valid == 0u, "TC-NETCFG-043 NULL 回写不得把影子置有效"); + CHECK(s_active_valid == 0u, "TC-NETCFG-043 NULL write-back must not mark the shadow valid"); net_cfg_set_active_mac(shadow); memset(out, 0xFF, sizeof(out)); net_cfg_get_mac(out); - CHECK(mac_eq(out, shadow), "TC-NETCFG-044 正常回写后影子生效"); + CHECK(mac_eq(out, shadow), "TC-NETCFG-044 shadow becomes active after a normal write-back"); net_cfg_get_mac(NULL); - CHECK(1, "TC-NETCFG-045 get_mac(NULL) 安全返回"); + CHECK(1, "TC-NETCFG-045 get_mac(NULL) returns safely"); } /* ===================== IPv4 定长格式化 ===================== */ @@ -500,33 +500,33 @@ static void tc_fmt_ipv4(void) /* 定长 = 4 段 × 3 位 + 3 个点 = 15(曾有误记为 11 的注释,已同日更正) * 钉死这个长度是 UI 光标偏移(段 i 起点 4i、点号 3/7/11)的前提 */ CHECK((strcmp(buf, "192.168.001.100") == 0) && (strlen(buf) == 15u), - "TC-NETCFG-050 定长 15 字符且每段补零"); + "TC-NETCFG-050 fixed 15 chars with zero padding in every segment"); memset(buf, 0x7A, sizeof(buf)); net_cfg_fmt_ipv4(ip0, buf, sizeof(buf)); - CHECK(strcmp(buf, "000.000.000.000") == 0, "TC-NETCFG-051 全零也占满 15 字符"); + CHECK(strcmp(buf, "000.000.000.000") == 0, "TC-NETCFG-051 all zero still fills 15 chars"); memset(buf, 0x7A, sizeof(buf)); net_cfg_fmt_ipv4(ipm, buf, sizeof(buf)); - CHECK(strcmp(buf, "255.255.255.255") == 0, "TC-NETCFG-052 255 边界"); + CHECK(strcmp(buf, "255.255.255.255") == 0, "TC-NETCFG-052 255 boundary"); memset(guard, 0x7A, sizeof(guard)); memcpy(buf, guard, sizeof(buf)); net_cfg_fmt_ipv4(NULL, buf, sizeof(buf)); - CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-053 p_ip NULL 不写缓冲"); + CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-053 p_ip NULL does not touch the buffer"); net_cfg_fmt_ipv4(ip1, NULL, 16u); - CHECK(1, "TC-NETCFG-054 p_buf NULL 不崩"); + CHECK(1, "TC-NETCFG-054 p_buf NULL does not crash"); memset(guard, 0x7A, sizeof(guard)); memcpy(buf, guard, sizeof(buf)); net_cfg_fmt_ipv4(ip1, buf, 0u); - CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-055 len 0 不写缓冲"); + CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-055 len 0 does not touch the buffer"); memset(buf, 0x7A, sizeof(buf)); net_cfg_fmt_ipv4(ip1, buf, 5u); CHECK((strlen(buf) == 4u) && (buf[4] == '\0') && (buf[5] == (char)0x7A), - "TC-NETCFG-056 len 不足:截断但必须 NUL 结尾且不越界"); + "TC-NETCFG-056 short len: truncate but must stay NUL-terminated and in bounds"); } /* ===================== 入口 ===================== */ diff --git a/test/host/net_events/net_events_test.c b/test/host/net_events/net_events_test.c index 48231fa3..5eef16b2 100644 --- a/test/host/net_events/net_events_test.c +++ b/test/host/net_events/net_events_test.c @@ -217,16 +217,16 @@ static void tc_connect(void) CHECK((p->remote_ip_arr[0] == 192u) && (p->remote_ip_arr[3] == 50u) && (p->remote_ip == ((192u) | (168u << 8) | (1u << 16) | (50u << 24))) && (p->remote_port == 51234u), - "TC-NETEV-001 对端六元组解析:ip 低字节在前拼 32 位、端口取第 5/6 字节"); + "TC-NETEV-001 peer tuple parsing: ip assembled little-endian into 32 bits, port from bytes 5/6"); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (p->session_dead == 0u) && (p->lost_conn_seen == 0u) && (p->send_ready == 1u), - "TC-NETEV-002 监听口自接后转已连接并立刻可写"); + "TC-NETEV-002 a listened socket becomes established after self-accept and is writable at once"); CHECK((g_chip.keepalive_calls == 1) && (g_chip.last_keepalive == 1u) && (g_chip.int_status_calls == 1), - "TC-NETEV-003 建链即开 keepalive 并清一次挂起位(读即清)"); + "TC-NETEV-003 on connect, enable keepalive and clear the pending bit once (read clears)"); CHECK((g_ev_n == 1) && (g_ev_kind[0] == (int)NET_EVENT_CONNECTED) && (g_ev_fd[0] == 0) && (g_ev_arg[0] == (void *)&g_ev_token), - "TC-NETEV-004 事件带 fd 与调用方 arg 回调给应用"); + "TC-NETEV-004 the event calls back into the app with fd and the caller arg"); reset_all(); p = mk(0, NET_SOCK_STATE_CONNECTING, 0u); @@ -234,13 +234,13 @@ static void tc_connect(void) handle_connect_event(p); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (p->send_ready == 0u) && (g_chip.keepalive_calls == 1) && (g_ev_n == 1), - "TC-NETEV-005 主动连接分支同样转已连接,但**不动 send_ready**(与自接有别)"); + "TC-NETEV-005 the active-connect branch also becomes established but does NOT touch send_ready (differs from self-accept)"); reset_all(); p = mk(0, NET_SOCK_STATE_CLOSED, 2404u); handle_connect_event(p); CHECK((g_ev_n == 0) && (p->state == NET_SOCK_STATE_CLOSED), - "TC-NETEV-006 其它状态收到 CONNECT 中断一律忽略(不许凭空造会话)"); + "TC-NETEV-006 a CONNECT interrupt in any other state is ignored (never invent a session)"); } /* ===================== DISCONNECT ===================== */ @@ -257,7 +257,7 @@ static void tc_disconnect(void) handle_disconnect_event(p); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (g_ev_n == 0) && (g_nev_error_count == 1), - "TC-NETEV-010 芯片说还在连接:忽略这个陈旧 DISCONNECT(不许拆活会话)"); + "TC-NETEV-010 the chip still says connecting: ignore this stale DISCONNECT (never tear down a live session)"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); @@ -267,7 +267,7 @@ static void tc_disconnect(void) handle_disconnect_event(p); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (p->lost_conn_seen == 1u) && (g_ev_n == 0) && (g_settle_n == 0), - "TC-NETEV-011 疑似误报(一条数据都没收就断):延后交给对账器,不立即拆"); + "TC-NETEV-011 suspected false alarm (dropped before any data): defer to the reconciler instead of tearing down"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); @@ -280,7 +280,7 @@ static void tc_disconnect(void) (p->last_peer_port == 51234u) && (p->state == NET_SOCK_STATE_CLOSED) && (g_ev_kind[0] == (int)NET_EVENT_DISCONNECTED), - "TC-NETEV-012 正常断开:优雅沉降 + 会话标死 + 记旧端口 + 发事件"); + "TC-NETEV-012 normal disconnect: graceful settle + mark session dead + remember old port + post event"); reset_all(); p = mk(0, NET_SOCK_STATE_LISTENING, 2404u); @@ -289,20 +289,20 @@ static void tc_disconnect(void) handle_disconnect_event(p); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (g_ev_n == 0) && (g_nev_info_count == 1) && (g_nev_error_count == 0), - "TC-NETEV-013 监听还在:陈旧监听 DISCONNECT 只留痕,不报 ERROR"); + "TC-NETEV-013 listen still alive: a stale listen DISCONNECT is only logged, not reported as ERROR"); reset_all(); p = mk(0, NET_SOCK_STATE_LISTENING, 2404u); g_fake.status_byte1[0] = CH395F_TCP_CLOSED; /* 芯片说不在 LISTEN */ handle_disconnect_event(p); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (g_ev_n == 0) && (g_req_n == 0), - "TC-NETEV-013b 监听丢了也只留痕:重开交给对账器,事件层不越权"); + "TC-NETEV-013b even a lost listener is only logged: reopening belongs to the reconciler, the event layer must not overreach"); reset_all(); p = mk(0, NET_SOCK_STATE_CONNECTING, 0u); handle_disconnect_event(p); CHECK((g_nev_error_count == 1) && (g_ev_kind[0] == (int)NET_EVENT_DISCONNECTED), - "TC-NETEV-014 非常规状态收到 DISCONNECT:报 ERROR 但仍要通知应用"); + "TC-NETEV-014 DISCONNECT in an unusual state: report ERROR but still notify the application"); } /* ===================== RECV / SEND / TIMEOUT ===================== */ @@ -316,21 +316,21 @@ static void tc_recv_send_timeout(void) p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); handle_recv_event(p); CHECK((p->readable == 1u) && (g_ev_kind[0] == (int)NET_EVENT_DATA_RECEIVED), - "TC-NETEV-020 RECV 事件置可读并通知应用"); + "TC-NETEV-020 a RECV event sets readable and notifies the application"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); p->send_ready = 1u; handle_send_event(p); CHECK((g_ev_kind[0] == (int)NET_EVENT_SEND_COMPLETE) && (p->send_ready == 1u), - "TC-NETEV-021 SEND 完成只发事件,字段留给写路径自己管"); + "TC-NETEV-021 SEND completion only posts the event; the fields stay owned by the write path"); reset_all(); p = mk(0, NET_SOCK_STATE_CONNECTING, 0u); handle_timeout_event(p); CHECK((p->state == NET_SOCK_STATE_CREATED) && (g_nev_error_count == 1) && (g_ev_kind[0] == (int)NET_EVENT_TIMEOUT), - "TC-NETEV-030 连接超时:退回 CREATED 并通知应用"); + "TC-NETEV-030 connect timeout: fall back to CREATED and notify the application"); reset_all(); p = mk(0, NET_SOCK_STATE_ESTABLISHED, 2404u); @@ -339,14 +339,14 @@ static void tc_recv_send_timeout(void) CHECK((g_settle_level == (int)NET_LISTENER_SETTLE_QUARANTINE) && (p->released_abnormal == 1u) && (p->session_dead == 1u) && (p->last_peer_port == 777u) && (p->state == NET_SOCK_STATE_CLOSED), - "TC-NETEV-031 会话超时:隔离级沉降 + 标异常释放(net_pending 收尾据此报 CONNRESET)"); + "TC-NETEV-031 session timeout: quarantine-level settle + mark released abnormally (net_pending reports CONNRESET from it)"); reset_all(); p = mk(0, NET_SOCK_STATE_LISTENING, 2404u); handle_timeout_event(p); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (g_nev_error_count == 1) && (g_ev_kind[0] == (int)NET_EVENT_TIMEOUT), - "TC-NETEV-032 监听超时只留痕:不改状态,但仍要通知等待方"); + "TC-NETEV-032 listen timeout is only logged: state unchanged but waiters are still notified"); } /* ===================== 掉线自愈 ===================== */ @@ -362,7 +362,7 @@ static void tc_lost_probe(void) g_fake.cached_tcp[0] = CH395F_TCP_CLOSED; net_lost_connect_probe(p); CHECK((p->lost_conn_seen == 0u) && (g_ev_n == 0), - "TC-NETEV-040 芯片不在已连接态:只清标记,不自作主张建会话"); + "TC-NETEV-040 the chip is not established: only clear the flag, do not create a session on our own"); reset_all(); p = mk(0, NET_SOCK_STATE_CLOSED, 2404u); @@ -372,7 +372,7 @@ static void tc_lost_probe(void) ch395f_fake_set_peer(0, 10u, 0u, 0u, 9u, 51234u); net_lost_connect_probe(p); CHECK((p->lost_conn_seen == 0u) && (g_ev_n == 0), - "TC-NETEV-041 对端口与上次相同:认定是自己没看到 CONNECT,不自接第二遍"); + "TC-NETEV-041 peer port identical to last time: assume we missed CONNECT, do not accept a second time"); /* 自愈的适用面是软件还写着 LISTENING、芯片其实已经连上——accept 分支只认 * LISTENING/CONNECTING。若软件态已被别处改成 CLOSED,本函数打完 INFO 后什么都 @@ -387,7 +387,7 @@ static void tc_lost_probe(void) CHECK((g_ev_n == 1) && (g_ev_kind[0] == (int)NET_EVENT_CONNECTED) && (p->state == NET_SOCK_STATE_ESTABLISHED) && (p->remote_port == 55555u) && (p->lost_conn_seen == 0u), - "TC-NETEV-042 丢了 CONNECT 中断时自愈:补一次 accept,会话不至于永久失联"); + "TC-NETEV-042 self-heal after a lost CONNECT interrupt: make up one accept so the session is not stranded forever"); reset_all(); p = mk(0, NET_SOCK_STATE_CLOSED, 2404u); @@ -395,7 +395,7 @@ static void tc_lost_probe(void) ch395f_fake_set_peer(0, 10u, 0u, 0u, 9u, 55555u); net_lost_connect_probe(p); CHECK((g_ev_n == 0) && (p->state == NET_SOCK_STATE_CLOSED), - "TC-NETEV-042b 现状:软件已 CLOSED 时自愈接不上(只留痕,不凭空造会话)"); + "TC-NETEV-042b current state: self-heal cannot accept once the software side is CLOSED (logged only, no invented session)"); } /* ===================== 周期对账 ===================== */ @@ -419,10 +419,10 @@ static void tc_reconcile(void) infos = g_nev_info_count; recon_step(1000u); /* priming:链路计数记基线 */ CHECK(g_nev_info_count == infos, - "TC-NETEV-051a 上电第一趟不因链路计数打日志(0xFFFF 哨兵只记基线)"); + "TC-NETEV-051a the first pass after power-up logs nothing for the link counter (the 0xFFFF sentinel only records the baseline)"); c = g_chip.cached_calls; recon_step(1000u); /* 同一时刻再调一次 */ - CHECK(g_chip.cached_calls == c, "TC-NETEV-050 100ms 周期闸门:同一时刻不重复查芯片"); + CHECK(g_chip.cached_calls == c, "TC-NETEV-050 100ms period gate: do not query the chip twice at the same instant"); reset_all(); p = mk(1, NET_SOCK_STATE_ESTABLISHED, 2404u); @@ -434,17 +434,17 @@ static void tc_reconcile(void) recon_step(1300u); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (g_ev_n == 0) && (g_nev_info_count == infos), - "TC-NETEV-052 会话死亡要**连观 4 次**才动手:前三次保持原状"); + "TC-NETEV-052 a dead session needs 4 consecutive observations before acting: the first three change nothing"); recon_step(1400u); CHECK((p->state == NET_SOCK_STATE_CLOSED) && (p->session_dead == 1u) && (g_ev_kind[0] == (int)NET_EVENT_DISCONNECTED), - "TC-NETEV-052b 第 4 次稳定后走断开收尾"); + "TC-NETEV-052b once the 4th observation is stable, run the disconnect teardown"); p->state = NET_SOCK_STATE_ESTABLISHED; /* 假装又活了,验证冷却 */ recon_step(1500u); recon_step(1600u); recon_step(1700u); recon_step(1800u); - CHECK(g_ev_n == 1, "TC-NETEV-052c 动作后 1000ms 冷却内不再重复折腾(哪怕稳定计数够了)"); + CHECK(g_ev_n == 1, "TC-NETEV-052c no repeat action inside the 1000ms cooldown after acting (even when the stable count is met)"); reset_all(); p = mk(2, NET_SOCK_STATE_ESTABLISHED, 2404u); @@ -452,11 +452,11 @@ static void tc_reconcile(void) g_fake.cached_tcp[2] = CH395F_TCP_LISTEN; /* 芯片自己重开监听了,却没发 DISCONNECT */ recon_step(2900u); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (g_pcan_n == 0), - "TC-NETEV-053 芯片偷偷重开监听:需要 2 次稳定,第一次只累计"); + "TC-NETEV-053 the chip reopened the listener behind our back: needs 2 stable passes, the first one only accumulates"); recon_step(3000u); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (g_pcan_n == 1) && (g_scan_n == 1) && (g_pcan_err == NET_ERR_CONNRESET), - "TC-NETEV-053b 第 2 次稳定:重同步成监听并**取消两条挂起**(否则任务永远等不到回)"); + "TC-NETEV-053b second stable pass: resync to listen and cancel both pending waits (otherwise a task would wait forever)"); reset_all(); p = mk(3, NET_SOCK_STATE_LISTENING, 2404u); @@ -466,7 +466,7 @@ static void tc_reconcile(void) recon_step(4100u); CHECK((p->state == NET_SOCK_STATE_ESTABLISHED) && (p->remote_port == 40000u) && (g_ev_kind[0] == (int)NET_EVENT_CONNECTED), - "TC-NETEV-054 连接完成但中断丢了:1 次即补 accept"); + "TC-NETEV-054 connect completed but the interrupt was lost: a single pass is enough to make up the accept"); reset_all(); p = mk(4, NET_SOCK_STATE_LISTENING, 2404u); @@ -476,7 +476,7 @@ static void tc_reconcile(void) recon_step(5300u); CHECK((g_req_n == 1) && (g_req_sock == 4) && (g_req_reason == (int)NET_LISTENER_REQ_RECON), - "TC-NETEV-055 监听丢了:2 次稳定后请求重开监听(不自已在事件层里开)"); + "TC-NETEV-055 listener lost: request a reopen after 2 stable passes (the event layer does not open it itself)"); reset_all(); p = mk(5, NET_SOCK_STATE_CLOSED, 2404u); @@ -487,7 +487,7 @@ static void tc_reconcile(void) recon_step(6400u); CHECK((p->state == NET_SOCK_STATE_LISTENING) && (p->last_peer_port == 999u) && (p->lost_conn_seen == 0u) && (g_req_n == 0), - "TC-NETEV-056 U11 重同步:软件 CLOSED 而芯片在 LISTEN -> 直接跟芯片,不多余重开"); + "TC-NETEV-056 U11 resync: software CLOSED while the chip is LISTEN -> follow the chip, no redundant reopen"); reset_all(); p = mk(6, NET_SOCK_STATE_CLOSED, 2404u); @@ -495,13 +495,13 @@ static void tc_reconcile(void) recon_step(7500u); recon_step(7600u); CHECK((g_req_n == 1) && (g_req_sock == 6), - "TC-NETEV-057 软件 CLOSED 且芯片也不在监听:请求重开(重建交回监听层)"); + "TC-NETEV-057 software CLOSED and the chip is not listening either: request a reopen (the listener layer rebuilds it)"); reset_all(); (void)memset(&g_tbl[7], 0, sizeof(g_tbl[7])); /* 槽位不在用 */ infos = g_nev_info_count; recon_step(8700u); - CHECK(g_nev_info_count == infos, "TC-NETEV-058 不在用的槽位直接跳过(不产生任何动作)"); + CHECK(g_nev_info_count == infos, "TC-NETEV-058 unused slots are skipped outright (no action at all)"); /* 链路日志:s_link_last 初值 0xFFFF 是哨兵——第一次看到计数只记基线、不打日志 * (否则每次上电都会冒一条"链路 up"的假事件);此后计数一变就必须留痕。 @@ -511,7 +511,7 @@ static void tc_reconcile(void) infos = g_nev_info_count; recon_step(9800u); CHECK(g_nev_info_count == (infos + 1), - "TC-NETEV-051b 链路变更计数一变就留痕(基线那趟已由 051a 验证不打)"); + "TC-NETEV-051b any change of the link-change counter is logged (the baseline pass is already covered by 051a)"); } int main(void) diff --git a/test/host/net_listener/net_listener_test.c b/test/host/net_listener/net_listener_test.c index 30af09af..3d240509 100644 --- a/test/host/net_listener/net_listener_test.c +++ b/test/host/net_listener/net_listener_test.c @@ -194,14 +194,14 @@ static void tc_bounds(void) net_listener_tick(); CHECK((g_chip.open_calls == 0) && (g_chip.close_calls == 0) && (g_nlis_error_count == 0), - "TC-NETLIS-001 越界 fd 静默返回,不打扰芯片"); + "TC-NETLIS-001 an out-of-range fd returns silently without touching the chip"); reset_all(); mk_closed_listen(0, 2404); net_listener_request(0, NET_LISTENER_REQ_ACCEPT); net_listener_request(0, NET_LISTENER_REQ_DROP); /* 应被忽略 */ CHECK(s_relisten_req[0] == (uint8_t)(NET_LISTENER_REQ_ACCEPT + 1U), - "TC-NETLIS-019 请求去重:后来的 reason 不覆盖首个"); + "TC-NETLIS-019 request de-duplication: a later reason must not overwrite the first one"); } /* ===================== 该不该重建 ===================== */ @@ -215,12 +215,12 @@ static void tc_should_or_not(void) net_listener_request(0, NET_LISTENER_REQ_ACCEPT); net_listener_tick(); CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] == 0u), - "TC-NETLIS-010 监听仍活着就不重建,且请求已 clear"); + "TC-NETLIS-010 while the listener is alive nothing is rebuilt and the request is cleared"); g_fake.cached_open[0] = CH395F_SOCKET_CLOSED; /* 之后才真死了 */ net_listener_request(0, NET_LISTENER_REQ_RECON); net_listener_tick(); CHECK(g_chip.open_calls == 1, - "TC-NETLIS-010b 上一步确实清掉了请求(否则这里会被去重吞掉)"); + "TC-NETLIS-010b the previous step really cleared the request (otherwise de-duplication would swallow it here)"); reset_all(); mk_closed_listen(0, 2404); @@ -228,23 +228,23 @@ static void tc_should_or_not(void) net_listener_request(0, NET_LISTENER_REQ_EOF); net_listener_tick(); CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] != 0u), - "TC-NETLIS-011 挂起 recv 未结束时让路但保留请求"); + "TC-NETLIS-011 a pending recv yields the way but keeps the request"); g_pending[0] = 0; g_send_pending[0] = 1; net_listener_tick(); CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] != 0u), - "TC-NETLIS-011b 挂起 send 同样挡路"); + "TC-NETLIS-011b a pending send blocks the same way"); g_send_pending[0] = 0; net_listener_tick(); CHECK((g_chip.open_calls == 1) && (s_relisten_req[0] == 0u), - "TC-NETLIS-011c 挂起结束后自动补上重建"); + "TC-NETLIS-011c once the pending op ends the rebuild happens automatically"); reset_all(); mk_closed_listen(0, 2404); net_listener_request(0, NET_LISTENER_REQ_RECON); net_listener_tick(); net_listener_tick(); - CHECK(g_chip.open_calls == 1, "TC-NETLIS-013 重建成功后不重复重建"); + CHECK(g_chip.open_calls == 1, "TC-NETLIS-013 a successful rebuild is not repeated"); } static void tc_giveup_conditions(void) @@ -254,7 +254,7 @@ static void tc_giveup_conditions(void) int cleared[5]; int errs[5]; - printf("-- TC-NETLIS-012 五种放弃条件 --\n"); + printf("-- TC-NETLIS-012 the five give-up conditions --\n"); reset_all(); for (i = 0; i < 5; i++) { uint8_t before_open = (uint8_t)g_chip.open_calls; @@ -276,19 +276,19 @@ static void tc_giveup_conditions(void) } CHECK((opened[0] == 0) && (opened[1] == 0) && (opened[2] == 0) && (opened[3] == 0) && (opened[4] == 0), - "TC-NETLIS-012 五种条件下都不碰芯片"); + "TC-NETLIS-012 none of the five conditions touches the chip"); CHECK((cleared[0] == 1) && (cleared[1] == 1) && (cleared[2] == 1) && (cleared[3] == 1) && (cleared[4] == 1), - "TC-NETLIS-012b 放弃即清请求(不留在表里反复白试)"); + "TC-NETLIS-012b giving up also clears the request (no repeated futile attempts left in the table)"); CHECK((errs[0] == 0) && (errs[2] == 0) && (errs[4] == 0), - "TC-NETLIS-012c 放弃是静默的(不该报 ERROR,那是对账器的活)"); + "TC-NETLIS-012c giving up is silent (no ERROR here, that is the reconciler's job)"); reset_all(); g_sock_valid[0] = 0u; /* 连表项都没有 */ net_listener_request(0, NET_LISTENER_REQ_RECON); net_listener_tick(); CHECK((g_chip.open_calls == 0) && (s_relisten_req[0] == 0u), - "TC-NETLIS-026 表项不存在由 tick 直接放弃"); + "TC-NETLIS-026 a missing table entry is given up directly by the tick"); /* 注:net_relisten_locked() 里的 NET_ERR_BADF 分支因此从 tick **不可达** * (net_listener.c:112 那段只在 p_sock 非空时才可能被调到)——留痕:它是 * 内部兜底,不是活路径;将来若新增调用点,要重新审视这条。 */ @@ -308,13 +308,13 @@ static void tc_backoff_and_budget(void) CHECK((g_chip.open_calls == 1) && (g_chip.close_calls == 1) && (g_errno_last == NET_ERR_BUSY) && (s_relisten_req[0] != 0u) && (s_relisten_next[0] == 1030u) && (s_relisten_dead[0] == 3000u), - "TC-NETLIS-014 open 失败:关回去 + BUSY + 30ms 退避 + 首轮建 2000ms 预算"); + "TC-NETLIS-014 open failure: close back + BUSY + 30ms backoff + a 2000ms budget armed on the first round"); g_now = 1020u; net_listener_tick(); - CHECK(g_chip.open_calls == 1, "TC-NETLIS-014b 退避未到不重试(不空转打芯片)"); + CHECK(g_chip.open_calls == 1, "TC-NETLIS-014b no retry before the backoff expires (no spinning on the chip)"); g_now = 1030u; net_listener_tick(); - CHECK(g_chip.open_calls == 2, "TC-NETLIS-014c 退避到点再试一次"); + CHECK(g_chip.open_calls == 2, "TC-NETLIS-014c once the backoff expires it tries exactly once more"); reset_all(); mk_closed_listen(0, 2404); @@ -323,7 +323,7 @@ static void tc_backoff_and_budget(void) net_listener_tick(); CHECK((g_chip.listen_calls == 1) && (g_chip.close_calls == 1) && (s_relisten_req[0] != 0u), - "TC-NETLIS-015 listen 失败也要关断(不留下开了一半的 socket)"); + "TC-NETLIS-015 a listen failure must also close (no half-open socket left behind)"); reset_all(); mk_closed_listen(0, 2404); @@ -336,7 +336,7 @@ static void tc_backoff_and_budget(void) net_listener_tick(); CHECK((g_nlis_error_count == 1) && (s_relisten_req[0] == 0u) && (g_chip.open_calls == 1), - "TC-NETLIS-018 预算耗尽:报一次 ERROR 交对账器,之后不再自己试"); + "TC-NETLIS-018 budget exhausted: report ERROR once and hand over to the reconciler, never retry on its own"); } /* ===================== 成功重建的簿记 ===================== */ @@ -363,22 +363,22 @@ static void tc_bookkeeping(void) CHECK((p->state == NET_SOCK_STATE_LISTENING) && (p->send_ready == 0u) && (p->recv_bytes == 0u) && (p->remote_port == 0u) && (p->remote_ip == 0u), - "TC-NETLIS-016 重建后回到干净监听态"); + "TC-NETLIS-016 after the rebuild the state is a clean listener again"); CHECK((p->last_peer_port == 1234u) && (p->lost_conn_seen == 0u) && (p->released_abnormal == 0u) && (p->remote_ip_arr[0] == 0u) && (p->remote_ip_arr[3] == 0u), - "TC-NETLIS-016b 旧对端端口进 last_peer_port,其余对端痕迹清零"); + "TC-NETLIS-016b the old peer port goes into last_peer_port and the other peer traces are zeroed"); CHECK((g_chip.set_port_calls == 1) && (g_chip.last_port == 2404u) && (g_chip.set_proto_calls == 1) && (g_chip.last_proto == CH395F_PROTO_TYPE_TCP), - "TC-NETLIS-017 源端口与协议取自表项本身(监听端口不能漂)"); + "TC-NETLIS-017 the source port and protocol come from the table entry itself (the listening port must not drift)"); } /* ===================== 沉降窗:隔离 400 / 优雅 100 ===================== */ static void tc_settle_windows(void) { - printf("-- TC-NETLIS-020/021 沉降窗 --\n"); + printf("-- TC-NETLIS-020/021 settle windows --\n"); reset_all(); mk_closed_listen(0, 2404); g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 芯片关不掉 */ @@ -390,11 +390,11 @@ static void tc_settle_windows(void) CHECK((g_chip.open_calls == 0) && (g_nlis_info_count == 1) && (s_relisten_settle_start[0] == 1000u) && (s_relisten_settle_dead[0] == 1800u) && (g_errno_last == NET_ERR_BUSY), - "TC-NETLIS-020 关不掉就等:首次记 start 并给 800ms 留痕线,返回 BUSY"); + "TC-NETLIS-020 if it will not close, wait: the first pass records start plus an 800ms logging line and returns BUSY"); g_now = 1030u; net_listener_tick(); CHECK(g_nlis_info_count == 1, - "TC-NETLIS-020b 等待期间**只在第一次**打日志(否则每 30ms 刷一条)"); + "TC-NETLIS-020b while waiting the log is written only the first time (otherwise one line every 30ms)"); /* 边界必须在**独立场景**里测:放行判定与 30ms 退避共用 s_relisten_next, * 上一轮等待会把 next 设成"当次 + 30",于是同一条链里"恰好 400ms"那一次 tick @@ -411,7 +411,7 @@ static void tc_settle_windows(void) g_now = 1399u; net_listener_tick(); CHECK((g_chip.open_calls == 0) && (s_relisten_quarantine[0] != 0u), - "TC-NETLIS-021 隔离级 hold=400ms:第 399ms 仍不放行"); + "TC-NETLIS-021 quarantine level hold=400ms: at 399ms it still does not proceed"); reset_all(); mk_closed_listen(0, 2404); @@ -424,7 +424,7 @@ static void tc_settle_windows(void) g_now = 1400u; net_listener_tick(); CHECK((g_chip.open_calls == 1) && (s_relisten_quarantine[0] == 0u), - "TC-NETLIS-021b 第 400ms 放行并清隔离(独立场景,不受退避槽遮挡)"); + "TC-NETLIS-021b at 400ms it proceeds and clears the quarantine (standalone scenario, not masked by a backoff slot)"); reset_all(); mk_closed_listen(0, 2404); @@ -433,10 +433,10 @@ static void tc_settle_windows(void) g_now = 1000u; net_listener_tick(); CHECK((g_chip.open_calls == 0) && (s_relisten_settle_start[0] == 1000u), - "TC-NETLIS-021c 优雅级同样先等(0 < 100ms)"); + "TC-NETLIS-021c the graceful level also waits first (0 < 100ms)"); g_now = 1099u; net_listener_tick(); - CHECK(g_chip.open_calls == 0, "TC-NETLIS-021c 优雅级 hold=100ms:第 99ms 不放行"); + CHECK(g_chip.open_calls == 0, "TC-NETLIS-021c graceful level hold=100ms: at 99ms it does not proceed"); reset_all(); mk_closed_listen(0, 2404); @@ -446,12 +446,12 @@ static void tc_settle_windows(void) net_listener_tick(); g_now = 1100u; net_listener_tick(); - CHECK(g_chip.open_calls == 1, "TC-NETLIS-021d 优雅级第 100ms 放行(比隔离级快 4 倍)"); + CHECK(g_chip.open_calls == 1, "TC-NETLIS-021d the graceful level proceeds at 100ms (4x faster than the quarantine level)"); } static void tc_cap_is_trace_only(void) { - printf("-- TC-NETLIS-022 800ms 上限只留痕 --\n"); + printf("-- TC-NETLIS-022 the 800ms ceiling only logs --\n"); reset_all(); mk_closed_listen(0, 2404); g_fake.status_byte0[0] = CH395F_SOCKET_OPEN; /* 永远关不掉 */ @@ -464,15 +464,15 @@ static void tc_cap_is_trace_only(void) net_listener_tick(); CHECK((s_relisten_capped[0] == 1u) && (g_nlis_info_count == 2) && (g_chip.open_calls == 0) && (s_relisten_req[0] != 0u), - "TC-NETLIS-022 到 800ms 只**留痕一次**,既不重建也**不放弃请求**"); + "TC-NETLIS-022 reaching 800ms logs exactly once, rebuilds nothing and does not give up the request"); g_now = 1830u; net_listener_tick(); CHECK(g_nlis_info_count == 2, - "TC-NETLIS-022b 留痕只打一次(之后继续默默等,不刷屏)"); + "TC-NETLIS-022b the log line is emitted only once (it keeps waiting quietly afterwards, no flood)"); g_now = 3000u; net_listener_tick(); CHECK((g_nlis_error_count == 1) && (s_relisten_req[0] == 0u), - "TC-NETLIS-022c 最终由 2000ms 预算接手:报 ERROR 并交出请求"); + "TC-NETLIS-022c the 2000ms budget takes over in the end: report ERROR and release the request"); } /* ===================== 芯片交互:清理、顺序、读即清 ===================== */ @@ -488,7 +488,7 @@ static void tc_chip_interaction(void) g_now = 1000u; net_listener_tick(); CHECK((g_chip.read_calls == 16) && (g_chip.last_read_len == 256u), - "TC-NETLIS-023 清接收区:单次钳到 256 字节、最多 16 轮(有守卫不死循环)"); + "TC-NETLIS-023 clearing the receive area: clamped to 256 bytes per call and at most 16 rounds (a guard prevents an infinite loop)"); reset_all(); mk_closed_listen(0, 2404); @@ -499,7 +499,7 @@ static void tc_chip_interaction(void) g_now = 1000u; net_listener_tick(); CHECK((g_chip.read_calls == 1) && (g_chip.last_read_len == 5u), - "TC-NETLIS-023b 读到空立刻停手(不硬凑 16 轮)"); + "TC-NETLIS-023b an empty read stops it immediately (never padding up to 16 rounds)"); reset_all(); mk_closed_listen(0, 2404); @@ -510,7 +510,7 @@ static void tc_chip_interaction(void) net_listener_tick(); CHECK((strcmp(g_chip.log, "Rcis") == 0) && (g_chip.int_status_calls == 1) && (g_chip.get_status_calls == 1), - "TC-NETLIS-024 顺序:清接收 → 关断 → 读即清 → 读状态(不可换)"); + "TC-NETLIS-024 order: clear recv -> close -> read-to-clear -> read status (fixed, must not be reordered)"); reset_all(); mk_closed_listen(0, 2404); @@ -521,7 +521,7 @@ static void tc_chip_interaction(void) g_now = 1000u; net_listener_tick(); CHECK((g_chip.int_status_calls >= 1) && (g_fake.int_status[0] == 0u), - "TC-NETLIS-025 读即清真的清零(复刻芯片无按位清除的怪癖)"); + "TC-NETLIS-025 read-to-clear really clears everything (mirrors the chip having no per-bit clear)"); } /* ===================== 入口 ===================== */ diff --git a/test/host/net_pending/net_pending_test.c b/test/host/net_pending/net_pending_test.c index eef73a0c..42d1a3a2 100644 --- a/test/host/net_pending/net_pending_test.c +++ b/test/host/net_pending/net_pending_test.c @@ -168,24 +168,24 @@ static void tc_recv_register(void) printf("-- TC-NETPEND-001..006 recv register --\n"); reset_all(); rc = net_pending_register(-1, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj); - CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001 下越界 fd 报 BADF"); + CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001 an fd below range reports BADF"); rc = net_pending_register(NET_MAX_SOCKETS, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj); - CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001b 上越界同样拦下"); + CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-001b an fd above range is rejected the same way"); reset_all(); rc = net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj); - CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-005 注册成功回 0x70000000 标记"); + CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-005 a successful registration returns the 0x70000000 marker"); rc = net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_caller_obj); CHECK((rc == -1) && (g_errno_last == NET_ERR_BUSY), - "TC-NETPEND-002 同 fd 已有挂起就拒绝(一条 socket 只挂一个 recv)"); + "TC-NETPEND-002 a pending op on the same fd is refused (one recv per socket at most)"); reset_all(); rc = net_pending_register(0, g_rbuf, 10, 100u, NULL); CHECK((rc == -1) && (g_errno_last == NET_ERR_BUSY), - "TC-NETPEND-003 caller 为空拒绝(没有回传目标就别挂起)"); + "TC-NETPEND-003 a NULL caller is refused (nothing to report back to, so do not suspend)"); rc = net_pending_register(0, g_rbuf, 10, 100u, (TaskHandle_t)&g_owner_obj); CHECK((rc == -1) && (g_errno_last == NET_ERR_BUSY), - "TC-NETPEND-004 属主任务自己发起被拒(自己等自己 = 死锁 netTask)"); + "TC-NETPEND-004 the owning task starting it is refused (waiting for itself would deadlock netTask)"); reset_all(); /* 必须先把槽位置成"在用且已建立":否则 tick 第一条兜底(in_use == 0 → @@ -193,11 +193,11 @@ static void tc_recv_register(void) mk_sock(0, NET_SOCK_STATE_ESTABLISHED); (void)net_pending_register(0, g_rbuf, 10, 0u, (TaskHandle_t)&g_caller_obj); CHECK((s_net_pending[0].infinite == 1u) && (s_net_pending[0].deadline == 0u), - "TC-NETPEND-006 to_ms=0 记为无限等(不置 deadline)"); + "TC-NETPEND-006 to_ms=0 is recorded as an infinite wait (no deadline armed)"); g_now = 999999u; net_pending_tick(g_socks); CHECK((s_net_pending[0].active == 1u) && (g_notify_n == 0), - "TC-NETPEND-006b 无限等不被时间掐掉(也不会偷偷发通知)"); + "TC-NETPEND-006b an infinite wait is never cut off by time (and no notification is sent behind the scenes)"); } static void tc_recv_paths(void) @@ -213,7 +213,7 @@ static void tc_recv_paths(void) (g_notify_target[0] == (void *)&g_caller_obj) && (g_socks[0].recv_bytes == 5u) && (g_socks[0].readable == 0u) && (s_net_pending[0].active == 0u), - "TC-NETPEND-007 数据到达:字节数经通知值回传给发起任务"); + "TC-NETPEND-007 data arrived: the byte count is returned to the calling task through the notification value"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -222,7 +222,7 @@ static void tc_recv_paths(void) ch395f_fake_push_recv_len(0, 100u); net_pending_tick(g_socks); CHECK((g_notify_value[0] == 10u) && (g_chip.last_read_len == 10u), - "TC-NETPEND-008 读量钳在调用方缓冲内(绝不越界写别人的 buf)"); + "TC-NETPEND-008 the read amount is clamped into the caller buffer (never writing past someone else's buf)"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -231,7 +231,7 @@ static void tc_recv_paths(void) net_pending_tick(g_socks); CHECK((g_notify_n == 0) && (g_socks[0].readable == 0u) && (s_net_pending[0].active == 1u), - "TC-NETPEND-009 空 readable:清标志继续等,不误交 0 字节"); + "TC-NETPEND-009 an empty readable: clear the flag and keep waiting instead of handing over 0 bytes"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -240,14 +240,14 @@ static void tc_recv_paths(void) net_pending_tick(g_socks); CHECK((g_notify_value[0] == (uint32_t)NET_ERR_TIMEDOUT) && (g_errno_last == NET_ERR_TIMEDOUT) && (s_net_pending[0].active == 0u), - "TC-NETPEND-010 超时:错误码同时进通知值与 errno(跨上下文拿得到)"); + "TC-NETPEND-010 timeout: the error code goes into both the notification value and errno (available across contexts)"); reset_all(); (void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj); g_socks[0].in_use = 0u; net_pending_tick(g_socks); CHECK(g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET, - "TC-NETPEND-011 槽位消失报 CONNRESET(不能让任务熬到超时才发现)"); + "TC-NETPEND-011 a vanished slot reports CONNRESET (the task must not have to survive until the timeout to notice)"); reset_all(); mk_sock(0, NET_SOCK_STATE_CLOSED); @@ -256,7 +256,7 @@ static void tc_recv_paths(void) net_pending_tick(g_socks); CHECK((g_notify_value[0] == 12u) && (g_socks[0].recv_bytes == 12u) && (g_lis_req_n == 0), - "TC-NETPEND-012 关闭但有残留:先把最后一包读走再收尾(不丢尾包)"); + "TC-NETPEND-012 closed with leftovers: drain the last packet before finishing (no tail data lost)"); reset_all(); mk_sock(0, NET_SOCK_STATE_CLOSED); @@ -264,7 +264,7 @@ static void tc_recv_paths(void) net_pending_tick(g_socks); CHECK((g_notify_value[0] == 0u) && (g_lis_req_n == 1) && (g_lis_req_sock == 0) && (g_lis_req_reason == (int)NET_LISTENER_REQ_PENDING), - "TC-NETPEND-013 干净结束:报 0 并请求重开监听(单连接 socket 必须回 LISTEN)"); + "TC-NETPEND-013 a clean end reports 0 and requests a listener reopen (a single-connection socket must return to LISTEN)"); reset_all(); mk_sock(0, NET_SOCK_STATE_CLOSED); @@ -272,7 +272,7 @@ static void tc_recv_paths(void) (void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj); net_pending_tick(g_socks); CHECK((g_notify_value[0] == 0u) && (g_lis_req_n == 0), - "TC-NETPEND-013b 不自动重开的 socket 不会被偷偷要求重建监听"); + "TC-NETPEND-013b a socket with auto-relisten off is never asked to rebuild the listener behind its back"); reset_all(); mk_sock(0, NET_SOCK_STATE_CLOSED); @@ -280,7 +280,7 @@ static void tc_recv_paths(void) (void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj); net_pending_tick(g_socks); CHECK(g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET, - "TC-NETPEND-014 异常释放报 CONNRESET 而不是 0(调用方要能分辨好坏收尾)"); + "TC-NETPEND-014 an abnormal release reports CONNRESET instead of 0 (the caller must tell a good end from a bad one)"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -288,7 +288,7 @@ static void tc_recv_paths(void) (void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj); net_pending_tick(g_socks); CHECK((g_notify_n == 1) && (g_lis_req_n == 1), - "TC-NETPEND-015 session_dead 与 CLOSED 同路(状态滞后时仍能收口)"); + "TC-NETPEND-015 session_dead takes the same path as CLOSED (a stale state still gets a proper close)"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -296,9 +296,9 @@ static void tc_recv_paths(void) net_pending_cancel(0, NET_ERR_CONNRESET); CHECK((g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET) && (g_errno_last == NET_ERR_CONNRESET) && (s_net_pending[0].active == 0u), - "TC-NETPEND-016 取消:错误码经通知值送达并解除挂起"); + "TC-NETPEND-016 cancel: the error code is delivered through the notification value and the suspension is released"); net_pending_cancel(0, NET_ERR_CONNRESET); - CHECK(g_notify_n == 1, "TC-NETPEND-016b 重复取消幂等(不给同一任务发两次通知)"); + CHECK(g_notify_n == 1, "TC-NETPEND-016b a repeated cancel is idempotent (the same task is never notified twice)"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -308,14 +308,14 @@ static void tc_recv_paths(void) net_pending_cancel_all(NET_ERR_CONNRESET); CHECK((g_notify_n == 2) && (s_net_pending[0].active == 0u) && (s_net_pending[2].active == 0u), - "TC-NETPEND-017 cancel_all 只通知挂着的那两个(网络整体复位时用)"); + "TC-NETPEND-017 cancel_all notifies only the two suspended ops (used on a full network reset)"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); (void)net_pending_register(0, g_rbuf, 10, 500u, (TaskHandle_t)&g_caller_obj); net_pending_clear(0); CHECK((s_net_pending[0].active == 0u) && (g_notify_n == 0), - "TC-NETPEND-018 clear 只解除挂起、不发通知(不给调用方假答复)"); + "TC-NETPEND-018 clear only releases the suspension and sends no notification (the caller gets no fake answer)"); } static void tc_settle_close(void) @@ -328,7 +328,7 @@ static void tc_settle_close(void) before = g_chip.read_calls + g_chip.get_status_calls; net_pending_settle_close(0, &g_socks[0]); CHECK((g_chip.read_calls + g_chip.get_status_calls) == before, - "TC-NETPEND-019 没挂起时直接返回(不打扰芯片)"); + "TC-NETPEND-019 with nothing suspended it returns directly (the chip is not disturbed)"); reset_all(); mk_sock(0, NET_SOCK_STATE_CLOSED); @@ -337,14 +337,14 @@ static void tc_settle_close(void) net_pending_settle_close(0, &g_socks[0]); CHECK((g_notify_value[0] == 7u) && (g_socks[0].recv_bytes == 7u) && (g_socks[0].readable == 0u), - "TC-NETPEND-020 收尾时把剩余字节读走并交还字节数(收口不丢数据)"); + "TC-NETPEND-020 the teardown drains the remaining bytes and returns their count (closing loses no data)"); reset_all(); mk_sock(0, NET_SOCK_STATE_CLOSED); (void)net_pending_register(0, g_rbuf, 20, 500u, (TaskHandle_t)&g_caller_obj); net_pending_settle_close(0, &g_socks[0]); CHECK(g_notify_value[0] == (uint32_t)NET_ERR_CONNRESET, - "TC-NETPEND-021 无残留则报 CONNRESET(关闭收尾不该被当成读到 0 字节)"); + "TC-NETPEND-021 with nothing left it reports CONNRESET (a close teardown must not look like a zero-byte read)"); } static void tc_send_paths(void) @@ -354,31 +354,31 @@ static void tc_send_paths(void) printf("-- TC-NETPEND-030..040 send pending --\n"); reset_all(); rc = net_send_pending_register(-1, g_wbuf, 100, 0, (TaskHandle_t)&g_caller_obj); - CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-030 越界 fd 报 BADF"); + CHECK((rc == -1) && (g_errno_last == NET_ERR_BADF), "TC-NETPEND-030 an out-of-range fd reports BADF"); rc = net_send_pending_register(0, NULL, 100, 0, (TaskHandle_t)&g_caller_obj); - CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030b buf 为空报 INVAL"); + CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030b a NULL buffer reports INVAL"); rc = net_send_pending_register(0, g_wbuf, 0, 0, (TaskHandle_t)&g_caller_obj); - CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030c len 非正报 INVAL"); + CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030c a non-positive length reports INVAL"); rc = net_send_pending_register(0, g_wbuf, 100, -1, (TaskHandle_t)&g_caller_obj); - CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030d sent 为负报 INVAL"); + CHECK(g_errno_last == NET_ERR_INVAL, "TC-NETPEND-030d a negative sent count reports INVAL"); rc = net_send_pending_register(0, g_wbuf, 100, 100, (TaskHandle_t)&g_caller_obj); CHECK((g_errno_last == NET_ERR_INVAL) && (s_net_send[0].active == 0u), - "TC-NETPEND-030e sent>=len 报 INVAL(已发完的不必挂)"); + "TC-NETPEND-030e sent >= len reports INVAL (nothing left to send, no need to suspend)"); rc = net_send_pending_register(0, g_wbuf, 100, 50, NULL); - CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031 caller 为空报 BUSY"); + CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031 a NULL caller reports BUSY"); rc = net_send_pending_register(0, g_wbuf, 100, 50, (TaskHandle_t)&g_owner_obj); - CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031b 属主自己发报 BUSY"); + CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031b the owning task sending it itself reports BUSY"); rc = net_send_pending_register(0, g_wbuf, 100, 50, (TaskHandle_t)&g_caller_obj); - CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-032 挂起发送注册成功"); + CHECK(rc == NET_PENDING_REGISTERED, "TC-NETPEND-032 a suspended send registers successfully"); rc = net_send_pending_register(0, g_wbuf, 100, 50, (TaskHandle_t)&g_caller_obj); - CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031c 同 fd 只允许一条挂起发送"); + CHECK(g_errno_last == NET_ERR_BUSY, "TC-NETPEND-031c only one suspended send per fd is allowed"); reset_all(); (void)net_send_pending_register(0, g_wbuf, 3000, 700, (TaskHandle_t)&g_caller_obj); g_socks[0].in_use = 0u; net_send_pending_tick(g_socks); CHECK((g_notify_n == 1) && (g_notify_value[0] == 700u), - "TC-NETPEND-033 槽位消失回**已发字节数**(不是错误码,调用方据此算进度)"); + "TC-NETPEND-033 a vanished slot returns the number of bytes already sent (not an error code; the caller computes progress from it)"); reset_all(); mk_sock(0, NET_SOCK_STATE_CLOSED); @@ -386,7 +386,7 @@ static void tc_send_paths(void) net_send_pending_tick(g_socks); CHECK((g_npd_error_count == 1) && (g_errno_last == NET_ERR_CONNRESET) && (g_notify_value[0] == 700u), - "TC-NETPEND-034 会话塌了:留 ERROR + 报 CONNRESET,但仍把已发字节交回去"); + "TC-NETPEND-034 a collapsed session logs ERROR + reports CONNRESET yet still hands back the bytes already sent"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -397,7 +397,7 @@ static void tc_send_paths(void) (g_chip.last_read_len == (uint16_t)NET_SEND_CHUNK_MAX) && (g_socks[0].send_bytes == (uint32_t)NET_SEND_CHUNK_MAX) && (g_socks[0].send_ready == 0u) && (s_net_send[0].active == 1u), - "TC-NETPEND-035 单次写入钳在 NET_SEND_CHUNK_MAX(芯片发送缓冲上界)"); + "TC-NETPEND-035 a single write is clamped to NET_SEND_CHUNK_MAX (the chip send buffer upper bound)"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -410,7 +410,7 @@ static void tc_send_paths(void) net_send_pending_tick(g_socks); CHECK((g_notify_value[0] == 3000u) && (s_net_send[0].active == 0u) && (g_chip.write_calls == 3), - "TC-NETPEND-036 分三块发完 3000 字节后交还总长度"); + "TC-NETPEND-036 3000 bytes finish in three chunks and the total length is returned"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -423,7 +423,7 @@ static void tc_send_paths(void) net_send_pending_tick(g_socks); CHECK((g_npd_error_count == 0) && (s_net_send[0].active == 1u) && (g_chip.write_calls == 2), - "TC-NETPEND-038 每写一轮续期 deadline:还在推进就不该被 1000ms 掐死"); + "TC-NETPEND-038 every written round renews the deadline: while it progresses it must not be killed at 1000ms"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -432,7 +432,7 @@ static void tc_send_paths(void) net_send_pending_tick(g_socks); CHECK((g_npd_error_count == 1) && (g_errno_last == NET_ERR_TIMEDOUT) && (g_notify_value[0] == 0u), - "TC-NETPEND-037 停滞超时报 TIMEDOUT(对端不收时不能永久占着发送通道)"); + "TC-NETPEND-037 a stalled wait times out with TIMEDOUT (a peer that never reads must not hold the send path forever)"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); @@ -440,16 +440,16 @@ static void tc_send_paths(void) net_send_pending_cancel(0, NET_ERR_CONNRESET); CHECK((g_npd_error_count == 1) && (g_errno_last == NET_ERR_CONNRESET) && (g_notify_value[0] == 40u) && (s_net_send[0].active == 0u), - "TC-NETPEND-039 取消挂起发送:留痕 + 交还已发字节"); + "TC-NETPEND-039 cancelling a suspended send logs it and hands back the bytes already sent"); net_send_pending_cancel(0, NET_ERR_CONNRESET); - CHECK(g_notify_n == 1, "TC-NETPEND-039b 重复取消幂等"); + CHECK(g_notify_n == 1, "TC-NETPEND-039b a repeated cancel is idempotent"); reset_all(); mk_sock(0, NET_SOCK_STATE_ESTABLISHED); (void)net_send_pending_register(0, g_wbuf, 100, 40, (TaskHandle_t)&g_caller_obj); net_send_pending_clear(0); CHECK((s_net_send[0].active == 0u) && (g_notify_n == 0), - "TC-NETPEND-040 clear 不发通知(与 recv 侧同一条纪律)"); + "TC-NETPEND-040 clear sends no notification (the same discipline as the recv side)"); } int main(void) diff --git a/test/host/net_select/net_select_test.c b/test/host/net_select/net_select_test.c index 907f2214..81b14b89 100644 --- a/test/host/net_select/net_select_test.c +++ b/test/host/net_select/net_select_test.c @@ -106,7 +106,7 @@ void osDelay(uint32_t ticks) /* 保险:本模块每条用例都必须在有限轮内退出(要么就绪、要么超时、要么由 * g_release_after 造事件)。跑飞就说明用例或实现出了问题——直接退出, * 绝不能把挂死留给 pre-commit / CI(上一次就是这么卡住 exe 文件名的)。 */ - printf(" ABORT 轮询跑飞(>500 轮未退出)\n"); + printf(" ABORT poll runaway (>500 rounds without exit)\n"); fflush(stdout); exit(2); } @@ -147,9 +147,9 @@ static void tc_bounds(void) reset_all(); NET_FD_ZERO(&r); CHECK(net_select(0, &r, NULL, NULL, NULL) == -1, "TC-NETSEL-001 nfds=0 -> -1"); - CHECK(net_select(-1, &r, NULL, NULL, NULL) == -1, "TC-NETSEL-002 nfds 为负 -> -1"); + CHECK(net_select(-1, &r, NULL, NULL, NULL) == -1, "TC-NETSEL-002 negative nfds -> -1"); CHECK(net_select(NET_MAX_SOCKETS + 1, &r, NULL, NULL, NULL) == -1, - "TC-NETSEL-003 nfds 越界 -> -1(不许扫出表外)"); + "TC-NETSEL-003 nfds out of range -> -1 (must not scan past the table)"); } /* ===================== 就绪位与集合回写 ===================== */ @@ -169,14 +169,14 @@ static void tc_ready_sets(void) NET_FD_SET(1, &r); rc = net_select(4, &r, NULL, NULL, NULL); CHECK((rc == 1) && NET_FD_ISSET(1, &r) && !NET_FD_ISSET(0, &r), - "TC-NETSEL-004 回写只保留就绪位,未就绪的 fd0 被清掉"); + "TC-NETSEL-004 write-back keeps only the ready bits; not-ready fd0 is cleared"); reset_all(); make_sock(2, NET_SOCK_STATE_ESTABLISHED, 1u, 0u); NET_FD_ZERO(&r); NET_FD_SET(2, &r); rc = net_select(3, &r, NULL, NULL, NULL); /* write/except 传 NULL */ - CHECK((rc == 1) && (NET_FD_ISSET(2, &r)), "TC-NETSEL-005 其余集合可为 NULL"); + CHECK((rc == 1) && (NET_FD_ISSET(2, &r)), "TC-NETSEL-005 the other sets may be NULL"); /* 注意:timeout 传 NULL 时本函数会一直等到有事件(见 008 的现状说明), * 所以这类用例必须让被请求的 fd 真的就绪,否则就是用例自己写死的死循环。 */ @@ -187,7 +187,7 @@ static void tc_ready_sets(void) NET_FD_SET(2, &w); rc = net_select(3, NULL, &w, NULL, NULL); CHECK((rc == 1) && NET_FD_ISSET(2, &w) && (r.fd_bits == 0u), - "TC-NETSEL-005b 只回写调用方真正请求过的集合"); + "TC-NETSEL-005b only the sets the caller actually requested get written back"); } /* ===================== 超时算术 ===================== */ @@ -205,7 +205,7 @@ static void tc_timeout(void) NET_FD_SET(0, &r); rc = net_select(1, &r, NULL, NULL, NULL); /* timeout = NULL:无限等 */ CHECK((rc == 1) && (g_delay_calls == 3) && NET_FD_ISSET(0, &r), - "TC-NETSEL-006 无限等被对端事件唤醒(第 3 轮)"); + "TC-NETSEL-006 infinite wait wakes on a peer event (round 3)"); reset_all(); make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 0u); @@ -215,7 +215,7 @@ static void tc_timeout(void) tv.tv_usec = 1999; /* 应为 1ms,不是 2ms */ rc = net_select(1, &r, NULL, NULL, &tv); CHECK((rc == 0) && (g_delay_calls == 0), - "TC-NETSEL-007 tv_usec 整除截断:1999us 当 1ms,首轮即退"); + "TC-NETSEL-007 tv_usec integer truncation: 1999us counts as 1ms, exits on the first round"); /* 已修(2026-10-23):原来这里写的是 (timeout_ms > 0) && (已用时 >= timeout_ms), * timeout = {0,0} 换算出 0 后**永不超时**,只能干等事件——而 POSIX 语义是 @@ -229,7 +229,7 @@ static void tc_timeout(void) tv.tv_usec = 0; rc = net_select(1, &r, NULL, NULL, &tv); CHECK((rc == 0) && (g_delay_calls == 0) && !NET_FD_ISSET(0, &r), - "TC-NETSEL-008 timeout={0,0} 只查一轮立即返回(与 poll 的 0 对齐)"); + "TC-NETSEL-008 timeout={0,0} checks one round and returns at once (aligned with poll timeout 0)"); } /* ===================== 状态准入 ===================== */ @@ -250,7 +250,7 @@ static void tc_state_admission(void) tv.tv_usec = 1000; rc = net_select(1, &r, NULL, NULL, &tv); CHECK((rc == 0) && !NET_FD_ISSET(0, &r), - "TC-NETSEL-009 监听态不可读(accept 语义不在 select 这层)"); + "TC-NETSEL-009 listen state is not readable (accept semantics are not in the select layer)"); reset_all(); make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 1u); @@ -258,7 +258,7 @@ static void tc_state_admission(void) NET_FD_SET(0, &w); rc = net_select(1, NULL, &w, NULL, &tv); CHECK((rc == 1) && NET_FD_ISSET(0, &w), - "TC-NETSEL-010 已建立 + send_ready 才可写"); + "TC-NETSEL-010 established + send_ready is what makes it writable"); reset_all(); make_sock(0, NET_SOCK_STATE_CONNECTING, 0u, 1u); @@ -266,7 +266,7 @@ static void tc_state_admission(void) NET_FD_SET(0, &w); rc = net_select(1, NULL, &w, NULL, &tv); CHECK((rc == 0) && !NET_FD_ISSET(0, &w), - "TC-NETSEL-011 连接中不可写(send_ready 不作数)"); + "TC-NETSEL-011 connecting state is not writable (send_ready does not count)"); /* 已修(2026-10-23):异常判定原先排在 p_sock == NULL → continue 之后,而 * check_socket_error() 只有"表项为空 / fd 越界"才返回 1、越界的 i 又不在本循环 @@ -279,14 +279,14 @@ static void tc_state_admission(void) NET_FD_SET(0, &r); rc = net_select(1, NULL, NULL, &r, &tv); CHECK((rc == 1) && NET_FD_ISSET(0, &r), - "TC-NETSEL-012 表项不存在的 fd 计入异常集合"); + "TC-NETSEL-012 an fd with no table entry counts into the exception set"); reset_all(); g_sock_valid[0] = 0u; NET_FD_ZERO(&r); NET_FD_SET(0, &r); /* 只请求可读 */ rc = net_select(1, &r, NULL, NULL, &tv); CHECK((rc == 0) && !NET_FD_ISSET(0, &r) && (g_delay_calls == 0), - "TC-NETSEL-012b 未请求异常集时不因缺失 fd 计数(不误伤只查可读的调用方)"); + "TC-NETSEL-012b a missing fd is not counted when the exception set was not requested"); reset_all(); g_sock_valid[0] = 0u; NET_FD_ZERO(&r); @@ -295,7 +295,7 @@ static void tc_state_admission(void) NET_FD_SET(0, &w); /* 请求异常:应该置位 */ rc = net_select(1, &r, NULL, &w, &tv); CHECK((rc == 1) && NET_FD_ISSET(0, &w) && !NET_FD_ISSET(0, &r), - "TC-NETSEL-012c 异常位只落到被请求的那个集合,不污染可读集合"); + "TC-NETSEL-012c the exception bit lands only in the requested set, never polluting the read set"); } /* ===================== poll 语义 ===================== */ @@ -308,7 +308,7 @@ static void tc_poll(void) printf("-- TC-NETSEL-013..019 poll --\n"); reset_all(); CHECK((net_poll_events(NULL, 1, 0) == -1) && (net_poll_events(p, 0, 0) == -1), - "TC-NETSEL-013 poll 参数非法 -> -1"); + "TC-NETSEL-013 invalid poll arguments -> -1"); reset_all(); (void)memset(p, 0, sizeof(p)); @@ -319,7 +319,7 @@ static void tc_poll(void) rc = net_poll_events(p, 2, 0); CHECK((rc == 2) && ((p[0].revents & NET_POLLNVAL) != 0) && ((p[1].revents & NET_POLLNVAL) != 0), - "TC-NETSEL-014 负 fd 与越界 fd 都置 POLLNVAL 并计入 ready"); + "TC-NETSEL-014 negative and out-of-range fd both set POLLNVAL and count as ready"); reset_all(); make_sock(0, NET_SOCK_STATE_CLOSED, 0u, 0u); @@ -328,7 +328,7 @@ static void tc_poll(void) p[0].events = NET_POLLIN; rc = net_poll_events(p, 1, 0); CHECK((rc == 1) && ((p[0].revents & NET_POLLHUP) != 0), - "TC-NETSEL-015 CLOSED 置 POLLHUP"); + "TC-NETSEL-015 CLOSED sets POLLHUP"); reset_all(); make_sock(0, NET_SOCK_STATE_ESTABLISHED, 1u, 0u); @@ -338,7 +338,7 @@ static void tc_poll(void) rc = net_poll_events(p, 1, 0); CHECK((rc == 1) && ((p[0].revents & NET_POLLIN) != 0) && ((p[0].revents & NET_POLLHUP) == 0), - "TC-NETSEL-016 可读置 POLLIN 且不误置 HUP"); + "TC-NETSEL-016 readable sets POLLIN without falsely setting HUP"); reset_all(); make_sock(0, NET_SOCK_STATE_ESTABLISHED, 1u, 1u); @@ -347,7 +347,7 @@ static void tc_poll(void) p[0].events = 0; /* 什么都不请求 */ (void)net_poll_events(p, 1, 0); CHECK((p[0].revents & NET_POLLERR) == 0, - "TC-NETSEL-017 现状:POLLERR 实际永不为真(表项为空已被 POLLNVAL 抢先)"); + "TC-NETSEL-017 current state: POLLERR is never true (an empty entry is caught earlier by POLLNVAL)"); reset_all(); make_sock(0, NET_SOCK_STATE_ESTABLISHED, 0u, 0u); @@ -356,7 +356,7 @@ static void tc_poll(void) p[0].events = NET_POLLIN; rc = net_poll_events(p, 1, 0); /* timeout = 0 */ CHECK((rc == 0) && (g_delay_calls == 0), - "TC-NETSEL-018 poll 的 timeout=0 立即返回(与 select 那条形成对照)"); + "TC-NETSEL-018 poll timeout=0 returns at once (contrast with the matching select case)"); reset_all(); make_sock(0, NET_SOCK_STATE_ESTABLISHED, 1u, 0u); @@ -365,7 +365,7 @@ static void tc_poll(void) p[0].events = NET_POLLIN; p[0].revents = NET_POLLOUT; /* 脏的上一轮残留 */ (void)net_poll_events(p, 1, 0); - CHECK((p[0].revents & NET_POLLOUT) == 0, "TC-NETSEL-019 每轮先清 revents"); + CHECK((p[0].revents & NET_POLLOUT) == 0, "TC-NETSEL-019 revents is cleared at the start of every round"); } /* ===================== 入口 ===================== */ diff --git a/test/host/net_socket/net_socket_test.c b/test/host/net_socket/net_socket_test.c index 8a2107d6..a16ccbf9 100644 --- a/test/host/net_socket/net_socket_test.c +++ b/test/host/net_socket/net_socket_test.c @@ -222,10 +222,10 @@ static void tc_101_byteorder(void) { reset_all(); - CHECK(net_htons(0x1234U) == 0x3412U, "TC-NETSOCK-101a htons 换端"); - CHECK(net_ntohs(net_htons(80U)) == 80U, "TC-NETSOCK-101b htons/ntohs 互逆"); - CHECK(net_htonl(0x01020304U) == 0x04030201U, "TC-NETSOCK-101c htonl 换端"); - CHECK(net_ntohl(net_htonl(0xDEADBEEFU)) == 0xDEADBEEFU, "TC-NETSOCK-101d htonl/ntohl 互逆"); + CHECK(net_htons(0x1234U) == 0x3412U, "TC-NETSOCK-101a htons byte swap"); + CHECK(net_ntohs(net_htons(80U)) == 80U, "TC-NETSOCK-101b htons/ntohs are inverses"); + CHECK(net_htonl(0x01020304U) == 0x04030201U, "TC-NETSOCK-101c htonl byte swap"); + CHECK(net_ntohl(net_htonl(0xDEADBEEFU)) == 0xDEADBEEFU, "TC-NETSOCK-101d htonl/ntohl are inverses"); } static void tc_102_parse_ip(void) @@ -236,12 +236,12 @@ static void tc_102_parse_ip(void) (void)memset(a, 0xEE, sizeof(a)); parse_ip_string(NULL, a); - CHECK(a[0] == 0xEEU, "TC-NETSOCK-102a parse_ip_string(NULL) 直接返回,不写目标缓冲"); + CHECK(a[0] == 0xEEU, "TC-NETSOCK-102a parse_ip_string(NULL) returns at once, target buffer untouched"); parse_ip_string("192.168.1.100", a); CHECK(a[0] == 192U && a[1] == 168U && a[2] == 1U && a[3] == 100U, - "TC-NETSOCK-102b parse_ip_string 段 0 落在 [0](芯片 set_ip_addr 要的顺位)"); + "TC-NETSOCK-102b parse_ip_string puts segment 0 at [0] (the order chip set_ip_addr wants)"); parse_ip_string("10.20.30.40", NULL); /* 只验不崩 */ - CHECK(1, "TC-NETSOCK-102c parse_ip_string 目标为 NULL 不崩"); + CHECK(1, "TC-NETSOCK-102c parse_ip_string with NULL target does not crash"); } static void tc_103_alloc_and_getsock(void) @@ -253,11 +253,11 @@ static void tc_103_alloc_and_getsock(void) reset_all(); CHECK(net_get_sock(-1) == NULL && net_get_sock(NET_MAX_SOCKETS) == NULL, - "TC-NETSOCK-103a net_get_sock 越界返 NULL(含负数)"); - CHECK(net_get_sock(0) == NULL, "TC-NETSOCK-103b 未分配的槽位返 NULL"); + "TC-NETSOCK-103a net_get_sock out of range returns NULL (incl. negative)"); + CHECK(net_get_sock(0) == NULL, "TC-NETSOCK-103b unallocated slot returns NULL"); CHECK(net_sock_raw(3) == &s_net_socks[3], - "TC-NETSOCK-103c net_sock_raw 不判 in_use(对账必须看到 CLOSED 槽位)"); - CHECK(net_sock_state(0) == -1, "TC-NETSOCK-103d 未占用 sock_state 返 -1 且不设 errno"); + "TC-NETSOCK-103c net_sock_raw skips the in_use check (reconciler must see CLOSED slots)"); + CHECK(net_sock_state(0) == -1, "TC-NETSOCK-103d unused sock_state returns -1 and leaves errno alone"); /* 逐个占满:alloc_socket 线性扫描首个空闲槽 */ for (i = 0; i < NET_MAX_SOCKETS; i++) { @@ -266,9 +266,9 @@ static void tc_103_alloc_and_getsock(void) break; } } - CHECK(i == NET_MAX_SOCKETS, "TC-NETSOCK-103e alloc_socket 按序分配 0..7"); + CHECK(i == NET_MAX_SOCKETS, "TC-NETSOCK-103e alloc_socket hands out 0..7 in order"); p = net_get_sock(0); - CHECK(p != NULL && p->in_use == 1U, "TC-NETSOCK-103f 分配的槽位 in_use=1"); + CHECK(p != NULL && p->in_use == 1U, "TC-NETSOCK-103f allocated slot has in_use=1"); reset_all(); } @@ -281,20 +281,20 @@ static void tc_104_socket_no_inherit(void) reset_all(); fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); - CHECK(fd == 0, "TC-NETSOCK-104a net_socket 返回首个空闲 fd"); + CHECK(fd == 0, "TC-NETSOCK-104a net_socket returns the first free fd"); p = &s_net_socks[fd]; - CHECK(p->ch395_sock == (uint8_t)fd, "TC-NETSOCK-104b 芯片 Socket 号与描述符一一对应"); + CHECK(p->ch395_sock == (uint8_t)fd, "TC-NETSOCK-104b chip socket number maps one-to-one to the descriptor"); CHECK(p->state == NET_SOCK_STATE_CREATED && p->type == NET_SOCK_STREAM, - "TC-NETSOCK-104c 初态 CREATED + 类型记下"); + "TC-NETSOCK-104c initial state CREATED with type recorded"); CHECK(p->send_ready == 1U && p->readable == 0U, - "TC-NETSOCK-104d 初态:发送缓冲视为空闲、无可读"); + "TC-NETSOCK-104d initial state: send buffer seen as free, nothing readable"); /* 三条"新 fd 不得继承"的定案,逐条钉(都是真机上静默出错的形态) */ - CHECK(p->blocking == 1U, "TC-NETSOCK-104e blocking 默认必须显式置 1(BSD 语义)"); - CHECK(p->released_abnormal == 0U, "TC-NETSOCK-104f 不得继承上一条的释放原因(TRAP-NET-18)"); - CHECK(p->session_dead == 0U, "TC-NETSOCK-104g 不得继承上一条的会话结束电平(N-32 P1)"); + CHECK(p->blocking == 1U, "TC-NETSOCK-104e blocking must default to explicitly 1 (BSD semantics)"); + CHECK(p->released_abnormal == 0U, "TC-NETSOCK-104f must not inherit the previous release reason (TRAP-NET-18)"); + CHECK(p->session_dead == 0U, "TC-NETSOCK-104g must not inherit the previous session-dead level (N-32 P1)"); CHECK(g_peers.listener_clear_calls == 1 && g_peers.pending_clear_calls == 1 && g_peers.send_clear_calls == 1, - "TC-NETSOCK-104h 新 fd 必须清监听重试 + 两张挂起表(各一次)"); + "TC-NETSOCK-104h new fd clears listener retry and both pending tables (once each)"); /* 脏槽位复用:把上一条会话的残留写进去,再看新建是否清干净 */ p->blocking = 0U; @@ -307,17 +307,17 @@ static void tc_104_socket_no_inherit(void) * 本场景没绑消息队列,公开入口会先撞上"队列未创建"的 NET_ERR, * 而那属于分片 3 的断言面(TC-NETSOCK-110h)。close 的清零主路径在 net_close_locked。 */ CHECK(net_close_sock(&s_net_socks[fd]) == 0, - "TC-NETSOCK-104i net_close_sock 成功(close 的 memset 是清零主路径)"); + "TC-NETSOCK-104i net_close_sock succeeds (its memset is the main reset path)"); fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); p = &s_net_socks[fd]; CHECK(fd == 0 && p->blocking == 1U && p->released_abnormal == 0U && p->session_dead == 0U && p->readable == 0U && p->send_ready == 1U && p->event_cb == NULL, - "TC-NETSOCK-104j 复用同一槽位:残留字段全部不得带走"); + "TC-NETSOCK-104j reusing the slot: no leftover field may carry over"); /* 地址族不支持 → INVAL(在 alloc 之前,不占槽) */ CHECK(net_socket(NET_AF_INET6, NET_SOCK_STREAM) == -1 && net_get_errno() == NET_ERR_INVAL && s_net_socks[1].in_use == 0U, - "TC-NETSOCK-104k 非 INET 地址族 → INVAL 且不占槽位"); + "TC-NETSOCK-104k non-INET address family -> INVAL and no slot taken"); reset_all(); } @@ -336,22 +336,22 @@ static void tc_105_bind(void) in.sin_addr.s_addr = net_inet_addr("192.168.1.100"); CHECK(net_bind(-1, (struct net_sockaddr *)&in, sizeof(in)) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-105a 越界 fd → BADF"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-105a out of range fd -> BADF"); CHECK(net_bind(0, (struct net_sockaddr *)&in, sizeof(in)) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-105b 未创建就 bind → BADF"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-105b bind before socket -> BADF"); fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); CHECK(net_bind(fd, NULL, sizeof(in)) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-105c addr 为 NULL → INVAL"); + "TC-NETSOCK-105c addr NULL -> INVAL"); CHECK(net_bind(fd, (struct net_sockaddr *)&in, 1) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-105d addrlen 不足 → INVAL"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-105d addrlen too small -> INVAL"); CHECK(net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)) == 0, - "TC-NETSOCK-105e TCP bind 成功"); + "TC-NETSOCK-105e TCP bind succeeds"); CHECK(s_net_socks[fd].local_port == 2404U && s_net_socks[fd].state == NET_SOCK_STATE_BOUND, - "TC-NETSOCK-105f TCP 只记端口转主机序 + 进 BOUND,不碰芯片"); + "TC-NETSOCK-105f TCP bind only stores host-order port and goes BOUND, no chip touch"); CHECK(g_chip.set_port_calls == 0 && g_chip.open_calls == 0, - "TC-NETSOCK-105g TCP bind 期间不得下发源端口/开芯片 Socket(打开留给 listen)"); + "TC-NETSOCK-105g bind must not set source port or open the chip socket (open belongs to listen)"); /* port=0 → 动态端口 49152 起,并推进 */ reset_all(); @@ -360,16 +360,16 @@ static void tc_105_bind(void) fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); rc = net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)); CHECK(rc == 0 && s_net_socks[fd].local_port == 49152U, - "TC-NETSOCK-105h port=0 → 分配 49152"); + "TC-NETSOCK-105h port=0 allocates 49152"); CHECK(s_dynamic_port == 49153U, - "TC-NETSOCK-105h2 计数器已推进到 49153(分配即推进,不留重复)"); + "TC-NETSOCK-105h2 counter already advanced to 49153 (advance on allocate, never reuse)"); (void)net_socket(NET_AF_INET, NET_SOCK_STREAM); /* 只创建不 bind */ CHECK(s_dynamic_port == 49153U, - "TC-NETSOCK-105i3 创建 socket **不**消耗动态端口(只有 bind 消耗)"); + "TC-NETSOCK-105i3 creating a socket does NOT consume a dynamic port (only bind does)"); fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); rc = net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)); CHECK(rc == 0 && s_net_socks[fd].local_port == 49153U && s_dynamic_port == 49154U, - "TC-NETSOCK-105i 第二次 bind 拿下一个端口(不会把同一个端口发两次)"); + "TC-NETSOCK-105i second bind gets the next port (same port never issued twice)"); /* 回绕:把计数器压到 65535,分配后应回 49152(不是 0——0 会让芯片按"未绑定"处理) */ s_dynamic_port = 65535U; @@ -378,7 +378,7 @@ static void tc_105_bind(void) fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); rc = net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)); CHECK(rc == 0 && s_net_socks[fd].local_port == 65535U && s_dynamic_port == 49152U, - "TC-NETSOCK-105j 动态端口越过 65535 回绕到 49152(不回 0)"); + "TC-NETSOCK-105j dynamic port wraps from 65535 back to 49152 (never to 0)"); } static void tc_106_bind_udp(void) @@ -393,23 +393,23 @@ static void tc_106_bind_udp(void) /* UDP:bind 就直接开芯片侧(无监听态),且必须配 DesIP=255.255.255.255 收任意源 */ fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); - CHECK(net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)) == 0, "TC-NETSOCK-106a UDP bind 成功"); - CHECK(s_net_socks[fd].state == NET_SOCK_STATE_UDP_OPEN, "TC-NETSOCK-106b UDP 直接进 UDP_OPEN"); + CHECK(net_bind(fd, (struct net_sockaddr *)&in, sizeof(in)) == 0, "TC-NETSOCK-106a UDP bind succeeds"); + CHECK(s_net_socks[fd].state == NET_SOCK_STATE_UDP_OPEN, "TC-NETSOCK-106b UDP goes straight to UDP_OPEN"); CHECK(g_chip.set_proto_calls == 1 && g_chip.last_proto == CH395F_PROTO_TYPE_UDP, - "TC-NETSOCK-106c 协议下发为 UDP(不是 TCP)"); + "TC-NETSOCK-106c protocol set to UDP (not TCP)"); CHECK(g_chip.set_port_calls == 1 && g_chip.last_port == 8888U, - "TC-NETSOCK-106d 源端口随 bind 下发"); + "TC-NETSOCK-106d source port delivered with bind"); CHECK(g_fx.des_ip[0][0] == 0xFFU && g_fx.des_ip[0][1] == 0xFFU && g_fx.des_ip[0][2] == 0xFFU && g_fx.des_ip[0][3] == 0xFFU && g_fx.des_port[0] == 0U, - "TC-NETSOCK-106e UDP Server 模式:DesIP=全 FF + 目的端口 0(否则只收单一对端)"); - CHECK(g_chip.open_calls == 1, "TC-NETSOCK-106f UDP bind 开一次芯片 Socket"); + "TC-NETSOCK-106e UDP server mode: DesIP all FF and dest port 0 (else only one peer is received)"); + CHECK(g_chip.open_calls == 1, "TC-NETSOCK-106f UDP bind opens the chip socket once"); /* 槽位 >=4 必须补设收发缓冲(net_init 已配过,这里是兜底):布局 6 块/槽 */ reset_all(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); - CHECK(fd == 0, "TC-NETSOCK-106g 先拿 fd0(对照:不补缓冲)"); - CHECK(g_fxchip.set_recv_buf_calls == 0, "TC-NETSOCK-106h fd<4 的 UDP bind 不重设缓冲"); + CHECK(fd == 0, "TC-NETSOCK-106g take fd0 first (control case: no buffer reconfig)"); + CHECK(g_fxchip.set_recv_buf_calls == 0, "TC-NETSOCK-106h UDP bind on fd<4 does not reconfigure buffers"); reset_all(); for (fd = 0; fd < 4; fd++) { @@ -421,7 +421,7 @@ static void tc_106_bind_udp(void) g_fx.recv_blk_cnt[4] == (uint8_t)CH395F_RECV_BLOCKS && g_fx.send_blk_start[4] == (uint8_t)(4 * 6 + (int)CH395F_RECV_BLOCKS) && g_fx.send_blk_cnt[4] == (uint8_t)CH395F_SEND_BLOCKS, - "TC-NETSOCK-106i fd>=4 兜底重设收发缓冲,块布局与 net_init 同构(6 块/槽)"); + "TC-NETSOCK-106i fd>=4 falls back to reconfiguring rx/tx buffers, same 6-block layout as net_init"); /* open 失败 → BUSY 且留在原状态(不得谎报 UDP_OPEN) */ reset_all(); @@ -431,7 +431,7 @@ static void tc_106_bind_udp(void) net_get_errno() == NET_ERR_BUSY && s_net_socks[fd].state != NET_SOCK_STATE_UDP_OPEN && g_ns_error_count == 1, - "TC-NETSOCK-106j UDP open 失败 → BUSY + 留痕 + 状态不前进"); + "TC-NETSOCK-106j UDP open failure -> BUSY plus error log and state does not advance"); } static void tc_107_peer_and_localname(void) @@ -445,40 +445,40 @@ static void tc_107_peer_and_localname(void) (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL); CHECK(net_getsockname(-1, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-107a getsockname 越界 → BADF"); + "TC-NETSOCK-107a getsockname out of range -> BADF"); CHECK(net_getsockname(0, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-107b 未用且 CLOSED 的槽 → BADF"); + "TC-NETSOCK-107b unused CLOSED slot -> BADF"); fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); (void)memset(&in, 0, sizeof(in)); in.sin_port = net_htons(7003); - CHECK(net_bind(fd, sa, (int)sizeof(in)) == 0, "TC-NETSOCK-107c bind 前置成功"); + CHECK(net_bind(fd, sa, (int)sizeof(in)) == 0, "TC-NETSOCK-107c bind precondition ok"); addrlen = (int)sizeof(in); (void)memset(&in, 0, sizeof(in)); - CHECK(net_getsockname(fd, sa, &addrlen) == 0, "TC-NETSOCK-107d getsockname 成功"); + CHECK(net_getsockname(fd, sa, &addrlen) == 0, "TC-NETSOCK-107d getsockname succeeds"); CHECK(in.sin_family == NET_AF_INET && net_ntohs(in.sin_port) == 7003U && addrlen == (int)sizeof(in), - "TC-NETSOCK-107e 回填族/端口(网络序)/长度"); + "TC-NETSOCK-107e fills family, port (network order) and length"); /* 钉**具体顺位**而不是二选一:getsockname 走的是「首段在最低字节」这一族 * (与 net_inet_ntoa、net_events 的 remote_ip 同族),所以 192.168.1.100 = * 0x6401A8C0。net_inet_addr 返回的是另一族(首段在最高字节,0xC0A80164), * 两族不互逆——已作为疑似缺陷报出,这里刻意不写成「两个都行」。 */ CHECK(in.sin_addr.s_addr == 0x6401A8C0U, - "TC-NETSOCK-107f 本机 IP 顺位=首段在最低字节(与 ntoa 同族;net_inet_addr 是另一族)"); + "TC-NETSOCK-107f local IP byte order = first octet in low byte (ntoa family)"); addrlen = 1; CHECK(net_getsockname(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-107g 长度不足 → INVAL"); + "TC-NETSOCK-107g length too small -> INVAL"); CHECK(net_getsockname(fd, sa, NULL) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-107h addrlen 为 NULL → INVAL"); + "TC-NETSOCK-107h addrlen NULL -> INVAL"); /* getpeername:没有 CONNECT 回读过对端 → NOTCONN(不发 SPI 是它的卖点) */ (void)memset(&in, 0, sizeof(in)); addrlen = (int)sizeof(in); CHECK(net_getpeername(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_NOTCONN, - "TC-NETSOCK-107i 尚无对端 → NOTCONN(remote_port==0 判据)"); + "TC-NETSOCK-107i no peer yet -> NOTCONN (judged by remote_port==0)"); CHECK(g_chip.get_status_calls == 0 && g_fxchip.glob_calls == 0, - "TC-NETSOCK-107j getpeername 纯读缓存,不发任何芯片事务"); + "TC-NETSOCK-107j getpeername only reads the cache, issues no chip transactions"); s_net_socks[fd].remote_port = 51999U; s_net_socks[fd].remote_ip = 0x0B01A8C0U; @@ -487,14 +487,14 @@ static void tc_107_peer_and_localname(void) CHECK(net_getpeername(fd, sa, &addrlen) == 0 && net_ntohs(in.sin_port) == 51999U && in.sin_addr.s_addr == 0x0B01A8C0U && in.sin_family == NET_AF_INET, - "TC-NETSOCK-107k getpeername 回填缓存的对端 IP:端口"); + "TC-NETSOCK-107k getpeername fills the cached peer IP and port"); CHECK(net_getpeername(-1, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-107l getpeername 越界 → BADF"); + "TC-NETSOCK-107l getpeername out of range -> BADF"); CHECK(net_getpeername(5, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-107m getpeername 未在用 → BADF"); + "TC-NETSOCK-107m getpeername on unused fd -> BADF"); addrlen = 2; CHECK(net_getpeername(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-107n getpeername 长度不足 → INVAL"); + "TC-NETSOCK-107n getpeername length too small -> INVAL"); reset_all(); } @@ -506,31 +506,31 @@ static void tc_108_sockprops(void) reset_all(); CHECK(net_setblocking(-1, 1) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-108a setblocking 越界 → BADF"); + "TC-NETSOCK-108a setblocking out of range -> BADF"); CHECK(net_setblocking(0, 1) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-108b 未占用 → BADF"); + "TC-NETSOCK-108b not in use -> BADF"); CHECK(net_set_event_cb(-1, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-108c set_event_cb 越界 → BADF"); + "TC-NETSOCK-108c set_event_cb out of range -> BADF"); CHECK(net_set_event_cb(0, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-108d set_event_cb 未占用 → BADF"); + "TC-NETSOCK-108d set_event_cb not in use -> BADF"); fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); CHECK(net_setblocking(fd, 0) == 0 && s_net_socks[fd].blocking == 0U, - "TC-NETSOCK-108e setblocking(0) 落属性(模式挂控制块,不是单次参数)"); + "TC-NETSOCK-108e setblocking(0) sets the attribute (mode lives in the control block, not per call)"); CHECK(net_setblocking(fd, 5) == 0 && s_net_socks[fd].blocking == 1U, - "TC-NETSOCK-108f 非 0 一律视为阻塞"); + "TC-NETSOCK-108f any non-zero value counts as blocking"); CHECK(net_set_event_cb(fd, (net_event_cb_t)0x1234, &arg) == 0 && s_net_socks[fd].event_cb == (net_event_cb_t)0x1234 && s_net_socks[fd].event_arg == &arg, - "TC-NETSOCK-108g 注册回调与用户参数"); + "TC-NETSOCK-108g registers callback and user argument"); CHECK(net_set_event_cb(fd, NULL, NULL) == 0 && s_net_socks[fd].event_cb == NULL, - "TC-NETSOCK-108h cb=NULL 取消注册"); + "TC-NETSOCK-108h cb=NULL unregisters"); s_net_socks[fd].state = NET_SOCK_STATE_ESTABLISHED; CHECK(net_sock_state(fd) == (int)NET_SOCK_STATE_ESTABLISHED, - "TC-NETSOCK-108i sock_state 返状态快照"); - CHECK(net_sock_state(NET_MAX_SOCKETS) == -1, "TC-NETSOCK-108j sock_state 越界 -1"); + "TC-NETSOCK-108i sock_state returns the state snapshot"); + CHECK(net_sock_state(NET_MAX_SOCKETS) == -1, "TC-NETSOCK-108j sock_state out of range -> -1"); s_net_socks[fd].in_use = 0U; - CHECK(net_sock_state(fd) == -1, "TC-NETSOCK-108k sock_state 未占用 -1"); + CHECK(net_sock_state(fd) == -1, "TC-NETSOCK-108k sock_state on unused fd -> -1"); reset_all(); } @@ -538,10 +538,10 @@ static void tc_108_sockprops(void) static void tc_109_generation(void) { reset_all(); - CHECK(net_get_generation() == 0U, "TC-NETSOCK-109a 初值 0"); + CHECK(net_get_generation() == 0U, "TC-NETSOCK-109a initial value 0"); net_generation_inc(); net_generation_inc(); - CHECK(net_get_generation() == 2U, "TC-NETSOCK-109b 每次网络重建 +1(服务据此重建监听)"); + CHECK(net_get_generation() == 2U, "TC-NETSOCK-109b each network rebuild adds 1 (services rebuild listeners from it)"); reset_all(); } @@ -551,33 +551,33 @@ static void tc_110_queue_adopt(void) static int q2; reset_all(); - CHECK(net_msg_queue_adopt(NULL) == -1, "TC-NETSOCK-110a 空句柄 → -1"); - CHECK(net_msg_queue_adopt((osMessageQueueId_t)&q1) == 0, "TC-NETSOCK-110b 首次绑定成功"); + CHECK(net_msg_queue_adopt(NULL) == -1, "TC-NETSOCK-110a NULL handle -> -1"); + CHECK(net_msg_queue_adopt((osMessageQueueId_t)&q1) == 0, "TC-NETSOCK-110b first binding succeeds"); CHECK(net_msg_queue_adopt((osMessageQueueId_t)&q2) == -1, - "TC-NETSOCK-110c 重复绑定必须被拒(不静默换句柄)"); + "TC-NETSOCK-110c rebinding must be refused (no silent handle swap)"); CHECK(s_net_msg_queue == (osMessageQueueId_t)&q1, - "TC-NETSOCK-110d 重复绑定**保留首次**句柄——换掉会让已入队请求永不处理"); + "TC-NETSOCK-110d rebinding keeps the FIRST handle - swapping would strand queued requests forever"); /* 未绑定时所有公开 API 一律 NET_ERR,而不是崩在空句柄上 */ reset_all(); CHECK(net_listen(0) == -1 && net_get_errno() == NET_ERR, - "TC-NETSOCK-110e 未绑定队列:net_listen → NET_ERR"); + "TC-NETSOCK-110e without a bound queue: net_listen -> NET_ERR"); CHECK(net_recv(0, NULL, 0, 0) == -1 && net_get_errno() == NET_ERR, - "TC-NETSOCK-110f 未绑定队列:net_recv → NET_ERR(越界检查在队检查之前)"); + "TC-NETSOCK-110f without a bound queue: net_recv -> NET_ERR (range check precedes queue check)"); CHECK(net_send(0, "x", 1, 0) == -1 && net_get_errno() == NET_ERR, - "TC-NETSOCK-110g 未绑定队列:net_send → NET_ERR"); + "TC-NETSOCK-110g without a bound queue: net_send -> NET_ERR"); CHECK(net_close(0) == -1 && net_get_errno() == NET_ERR, - "TC-NETSOCK-110h 未绑定队列:net_close → NET_ERR"); + "TC-NETSOCK-110h without a bound queue: net_close -> NET_ERR"); CHECK(net_drop_conn(0) == -1 && net_get_errno() == NET_ERR, - "TC-NETSOCK-110i 未绑定队列:net_drop_conn → NET_ERR"); + "TC-NETSOCK-110i without a bound queue: net_drop_conn -> NET_ERR"); CHECK(net_recv_to(0, NULL, 0, 10U) == -1 && net_get_errno() == NET_ERR, - "TC-NETSOCK-110j 未绑定队列:net_recv_to → NET_ERR"); + "TC-NETSOCK-110j without a bound queue: net_recv_to -> NET_ERR"); CHECK(net_accept(0, NULL, NULL) == -1 && net_get_errno() == NET_ERR, - "TC-NETSOCK-110k 未绑定队列:net_accept → NET_ERR"); + "TC-NETSOCK-110k without a bound queue: net_accept -> NET_ERR"); CHECK(net_connect(0, NULL, 0) == -1 && net_get_errno() == NET_ERR, - "TC-NETSOCK-110l 未绑定队列:net_connect → NET_ERR"); + "TC-NETSOCK-110l without a bound queue: net_connect -> NET_ERR"); /* 越界优先于"未初始化":调用方拿到的码要能区分"我传错了"和"网还没起来" */ CHECK(net_send(NET_MAX_SOCKETS, "x", 1, 0) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-110m 越界检查先于队列检查 → BADF"); + "TC-NETSOCK-110m range check before queue check -> BADF"); reset_all(); } @@ -592,10 +592,10 @@ static void tc_120_errno_slot_per_task(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; (void)net_set_errno(NET_ERR_BADF); CHECK(net_get_errno() == NET_ERR_BADF && s_net_errno_slots[0].owner == NULL, - "TC-NETSOCK-120a 内核未运行 → 用 [0] 槽且**不占槽**(不写 owner)"); + "TC-NETSOCK-120a kernel not running -> uses slot [0] and takes no slot (owner not written)"); g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; CHECK(net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-120b 内核未运行时所有任务共享 [0] 槽(退化语义)"); + "TC-NETSOCK-120b with the kernel down all tasks share slot [0] (degraded semantics)"); /* 内核在跑:每个任务自己的槽位 */ g_ns_kernel_state = osKernelRunning; @@ -605,15 +605,15 @@ static void tc_120_errno_slot_per_task(void) (void)net_set_errno(NET_ERR_TIMEDOUT); g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; CHECK(net_get_errno() == NET_OK, - "TC-NETSOCK-120c 另一个任务读不到 caller 的码(E-03 的正面表述)"); + "TC-NETSOCK-120c another task cannot read the caller code (the positive wording of E-03)"); (void)net_set_errno(NET_ERR_WOULDBLOCK); - CHECK(net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-120d 本任务读自己的码"); + CHECK(net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-120d a task reads its own code"); g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; CHECK(net_get_errno() == NET_ERR_TIMEDOUT, - "TC-NETSOCK-120e 互不覆写:caller 的码没被 netTask 抢走(旧实现的红线)"); + "TC-NETSOCK-120e no cross overwrite: caller code survives the netTask write (old red line)"); /* 临界区:两个任务各占一次槽 → 两次进入、两次退出,绝不泄漏嵌套计数 */ CHECK(g_ns_crit_enter == 2 && g_ns_crit_exit == 2 && ns_os_crit_depth() == 0, - "TC-NETSOCK-120f 每次**首次占槽**进一次临界区且成对退出(无泄漏)"); + "TC-NETSOCK-120f each first slot take enters the critical section once and leaves in pairs"); /* 快路径只读不写:已有槽位的任务反复读写都不该再进临界区、也不该再占槽 */ { @@ -623,18 +623,18 @@ static void tc_120_errno_slot_per_task(void) int i_net = ns_slot_index_of((TaskHandle_t)&g_ns_task_net); CHECK(i_caller >= 0 && i_net >= 0 && i_caller != i_net, - "TC-NETSOCK-120g 两个任务落在**不同**槽位(槽位表可查,不是靠约定)"); + "TC-NETSOCK-120g two tasks land in DIFFERENT slots (table is inspectable, not convention)"); g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; (void)net_set_errno(NET_ERR_BUSY); (void)net_get_errno(); (void)net_set_errno(NET_ERR_CONNRESET); - CHECK(g_ns_crit_enter == e0, "TC-NETSOCK-120h 命中已有槽位:全程零临界区(快路径只读)"); + CHECK(g_ns_crit_enter == e0, "TC-NETSOCK-120h existing slot hit: zero critical sections (fast path is read-only)"); g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; CHECK(net_get_errno() == NET_ERR_WOULDBLOCK && ns_slot_owner_count() == owners0, - "TC-NETSOCK-120i 别的任务反复读也不新增槽位、也读不到 caller 的码"); + "TC-NETSOCK-120i repeated reads by another task add no slot and never see the caller code"); g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; - CHECK(net_get_errno() == NET_ERR_CONNRESET, "TC-NETSOCK-120j caller 读到自己最新的码"); + CHECK(net_get_errno() == NET_ERR_CONNRESET, "TC-NETSOCK-120j caller reads its own latest code"); } /* 槽位用尽 → 回退 [0](刻意不重试,避免把竞态带回来) */ @@ -648,14 +648,14 @@ static void tc_120_errno_slot_per_task(void) (void)net_set_errno(NET_ERR_ISCONN); } g_ns_cur_task = (TaskHandle_t)&filler[0]; - CHECK(net_get_errno() == NET_ERR_ISCONN, "TC-NETSOCK-120k 8 个槽各归各主"); + CHECK(net_get_errno() == NET_ERR_ISCONN, "TC-NETSOCK-120k 8 slots, one owner each"); g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; /* 第 9 个任务 */ (void)net_set_errno(NET_ERR_NOMEM); CHECK(net_get_errno() == NET_ERR_NOMEM, - "TC-NETSOCK-120l 槽位用尽:新任务落 [0] 槽仍读得到自己的码"); + "TC-NETSOCK-120l slots exhausted: a new task still reads its own code from slot [0]"); g_ns_cur_task = (TaskHandle_t)&filler[0]; CHECK(net_get_errno() == NET_ERR_NOMEM, - "TC-NETSOCK-120m 用尽后退化为共享 [0] 槽(只影响排障可读性,不新增共享槽位)"); + "TC-NETSOCK-120m after exhaustion it degrades to the shared slot [0] (only hurts debuggability)"); } reset_all(); } @@ -667,7 +667,7 @@ static void tc_121_errno_slot_isr(void) g_ns_cur_task = NULL; /* 中断上下文/无句柄 */ (void)net_set_errno(NET_ERR_CONNRESET); CHECK(net_get_errno() == NET_ERR_CONNRESET && s_net_errno_slots[0].owner == NULL, - "TC-NETSOCK-121 句柄为 NULL(中断上下文)→ 退 [0] 槽且不改槽位表"); + "TC-NETSOCK-121 NULL handle (ISR context) -> falls back to slot [0] and leaves the table alone"); reset_all(); } @@ -675,9 +675,9 @@ static void tc_121_errno_slot_isr(void) int main(void) { - printf("=== NET Socket Tests (net_socket.c 主机单测) ===\n"); + printf("=== NET Socket Tests (net_socket.c host unit tests) ===\n"); - printf("[shard1] 纯函数面与 fd 表\n"); + printf("[shard1] pure functions and fd table\n"); tc_100_inet_helpers(); tc_101_byteorder(); tc_102_parse_ip(); @@ -690,20 +690,20 @@ int main(void) tc_109_generation(); tc_110_queue_adopt(); - printf("[shard2] 每任务错误码槽 + 队列绑定\n"); + printf("[shard2] per-task errno slots and queue binding\n"); tc_120_errno_slot_per_task(); tc_121_errno_slot_isr(); - printf("[shard3] 跨上下文消息与通知值回传\n"); + printf("[shard3] cross-context messages and notify-value return\n"); ns_run_s3(); - printf("[shard4] 状态机与闸门\n"); + printf("[shard4] state machine and gates\n"); ns_run_s4(); - printf("[shard5] 建流与读写分支\n"); + printf("[shard5] stream setup and read/write branches\n"); ns_run_s5(); - printf("[shard6] 边界残支与其余出口\n"); + printf("[shard6] edge branches and remaining exits\n"); ns_run_s6(); printf("=== NET Socket Tests: %d/%d PASSED (failed=%d) ===\n", diff --git a/test/host/net_socket/ns_rtos.c b/test/host/net_socket/ns_rtos.c index d4061ae9..116baa6f 100644 --- a/test/host/net_socket/ns_rtos.c +++ b/test/host/net_socket/ns_rtos.c @@ -118,7 +118,7 @@ void ns_rtos_post_notify(TaskHandle_t to, uint32_t v) return; } } - printf("FATAL: ns_rtos_post_notify 收到未知任务句柄\n"); + printf("FATAL: ns_rtos_post_notify got an unknown task handle\n"); exit(2); } @@ -185,7 +185,7 @@ osStatus_t osDelay(uint32_t ticks) g_ns_tick += ticks; if (++guard > 20000) { - printf("FATAL: osDelay 跑飞(%d 次)—— 等待条件永不满足\n", guard); + printf("FATAL: osDelay runaway (%d calls) - the wait condition is never met\n", guard); exit(2); } if (guard > 0) { @@ -212,7 +212,7 @@ BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, g_ns_notify_last_val = ulValue; g_ns_notify_last_to = xTaskToNotify; if (p == NULL) { - printf("FATAL: xTaskNotify 收到未知任务句柄\n"); + printf("FATAL: xTaskNotify got an unknown task handle\n"); exit(2); } p->val = ulValue; @@ -228,7 +228,7 @@ BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, ns_notify_slot_t *p = slot_of(g_ns_cur_task); if (p == NULL) { - printf("FATAL: xTaskNotifyWait 当前任务句柄未知\n"); + printf("FATAL: xTaskNotifyWait current task handle unknown\n"); exit(2); } if (pulNotificationValue != NULL) { @@ -249,7 +249,7 @@ BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, /* 阻塞分支:这就是"睡到 netTask 回执"的调度点 → 驱动一轮 netTask 再拾取 */ g_ns_block_waits++; if (g_ns_pump == NULL) { - printf("FATAL: 走到阻塞等待但测试没装 pump 钩子\n"); + printf("FATAL: reached the blocking wait but the test installed no pump hook\n"); exit(2); } { @@ -267,7 +267,7 @@ BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, } } if (p->pending == 0) { - printf("FATAL: 阻塞等待 %d 轮仍无回执(该通知的没通知 = 真机必挂死)\n", rounds); + printf("FATAL: blocking wait still got no receipt after %d rounds (a missing notify would hang real hardware)\n", rounds); exit(2); } } @@ -326,7 +326,7 @@ GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) g_ns_on_poll(); /* 并发事件注入点(在时间推进之后,超时判定才量得到) */ } if (g_ns_poll_calls > 400000U) { - printf("FATAL: net_poll 轮询 %lu 次仍未收敛(等待条件永不满足)\n", + printf("FATAL: net_poll still not converged after %lu rounds (wait condition never met)\n", (unsigned long)g_ns_poll_calls); exit(2); } diff --git a/test/host/net_socket/ns_s3_msg.c b/test/host/net_socket/ns_s3_msg.c index 5dcb07d5..37d0404d 100644 --- a/test/host/net_socket/ns_s3_msg.c +++ b/test/host/net_socket/ns_s3_msg.c @@ -49,15 +49,15 @@ static void tc_200_unknown_type(void) msg.sockfd = 0; msg.caller = (TaskHandle_t)&g_ns_task_caller; msg.result = 12345; /* 脏值:分派必须覆写它 */ - CHECK(ns_rtos_q_put_raw(&msg) == 0, "TC-NETSOCK-200a 原始消息入队"); - CHECK(g_ns_notify_calls == 0, "TC-NETSOCK-200b 入队本身不产生通知"); + CHECK(ns_rtos_q_put_raw(&msg) == 0, "TC-NETSOCK-200a raw message enqueued"); + CHECK(g_ns_notify_calls == 0, "TC-NETSOCK-200b enqueue itself produces no notification"); net_process_messages(); /* 以 netTask 身份跑一轮 */ - CHECK(g_ns_notify_calls == 1, "TC-NETSOCK-200c 未知 type 仍必发回执(否则调用方永久悬挂)"); + CHECK(g_ns_notify_calls == 1, "TC-NETSOCK-200c unknown type must still send a receipt (else the caller hangs forever)"); CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR, - "TC-NETSOCK-200d 未知 type 的通知值 = 本条的精确错误码 NET_ERR(不是上一条残留)"); - CHECK(ns_rtos_q_count() == 0, "TC-NETSOCK-200e 本轮队列被排空(非阻塞 while 语义)"); + "TC-NETSOCK-200d notify value for unknown type = this message exact code NET_ERR, not a leftover"); + CHECK(ns_rtos_q_count() == 0, "TC-NETSOCK-200e queue drained within this round (non-blocking while semantics)"); reset_all(); } @@ -70,9 +70,9 @@ static void tc_201_ticks_run_before_dispatch(void) CHECK(g_peers.listener_tick_calls == 1 && g_peers.pending_tick_calls == 1 && g_peers.send_tick_calls == 1 && g_peers.reconcile_calls == 1, - "TC-NETSOCK-201a 空队列也要跑满四个自愈 tick(D10/pending×2/对账)"); + "TC-NETSOCK-201a even with an empty queue all four self-heal ticks run (D10/pending x2/reconcile)"); CHECK(g_ns_notify_calls == 0 && g_ns_get_calls == 1, - "TC-NETSOCK-201b 空队列:取一次即退,不发通知"); + "TC-NETSOCK-201b empty queue: one peek then exit, no notification"); reset_all(); } @@ -85,7 +85,7 @@ static void tc_202_no_queue_bound(void) net_process_messages(); CHECK(g_peers.listener_tick_calls == 0 && g_ns_get_calls == 0, - "TC-NETSOCK-202 未绑定队列就调 net_process_messages → 立即返回(不碰芯片也不跑 tick)"); + "TC-NETSOCK-202 net_process_messages without a bound queue returns at once (no chip, no ticks)"); reset_all(); } @@ -103,16 +103,16 @@ static void tc_210_send_end_to_end(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; rc = net_send(fd, buf, (int)sizeof(buf), 0); - CHECK(rc == (int)sizeof(buf), "TC-NETSOCK-210a net_send 返回实际写入字节数"); + CHECK(rc == (int)sizeof(buf), "TC-NETSOCK-210a net_send returns the number of bytes actually written"); CHECK(g_chip.write_calls == 1 && g_chip.last_read_len == (uint16_t)sizeof(buf), - "TC-NETSOCK-210b 一次写满(健康链路不拆块、不进挂起)"); - CHECK(s_net_socks[fd].send_bytes == sizeof(buf), "TC-NETSOCK-210c 发送字节统计累计"); + "TC-NETSOCK-210b one full write (a healthy path neither chunks nor goes pending)"); + CHECK(s_net_socks[fd].send_bytes == sizeof(buf), "TC-NETSOCK-210c sent byte statistic accumulates"); CHECK(g_ns_notify_calls == 1 && g_ns_notify_last_val == (uint32_t)sizeof(buf), - "TC-NETSOCK-210d 结果经通知值回传(32 位整体即结果)"); + "TC-NETSOCK-210d result travels back on the notify value (the whole 32 bits are the result)"); CHECK(g_ns_notify_last_to == (TaskHandle_t)&g_ns_task_caller, - "TC-NETSOCK-210e 通知发给**登记的那个 caller**,不是广播"); + "TC-NETSOCK-210e notify goes to the REGISTERED caller only, not a broadcast"); CHECK(g_ns_block_waits == 1, - "TC-NETSOCK-210f 零等待没拾到 → 走阻塞分支(两段式拾取的第二段)"); + "TC-NETSOCK-210f zero-wait found nothing -> blocking branch (second stage of the two-step peek)"); reset_all(); } @@ -135,11 +135,11 @@ static void tc_211_send_chunking(void) rc = net_send(fd, buf, 2500, 0); - CHECK(rc == 2500, "TC-NETSOCK-211a 2500 字节全部交付"); - CHECK(g_chip.write_calls == 3, "TC-NETSOCK-211b 按 1024 上界切成 3 块(1024/1024/452)"); - CHECK(g_chip.last_read_len == 452U, "TC-NETSOCK-211c 末块只写剩余量"); + CHECK(rc == 2500, "TC-NETSOCK-211a all 2500 bytes delivered"); + CHECK(g_chip.write_calls == 3, "TC-NETSOCK-211b split into 3 chunks at the 1024 bound (1024/1024/452)"); + CHECK(g_chip.last_read_len == 452U, "TC-NETSOCK-211c last chunk writes only the remainder"); CHECK(NET_SEND_CHUNK_MAX == 1024, - "TC-NETSOCK-211d 分块粒度取芯片发送缓冲(不是假件自定的数)"); + "TC-NETSOCK-211d chunk size comes from the chip send buffer, not a fake-defined number"); reset_all(); } @@ -160,24 +160,24 @@ static void tc_212_send_pending_defers_notify(void) msg.len = (int)sizeof(buf); msg.caller = (TaskHandle_t)&g_ns_task_caller; - CHECK(ns_rtos_q_put_raw(&msg) == 0, "TC-NETSOCK-212a SEND 消息入队"); + CHECK(ns_rtos_q_put_raw(&msg) == 0, "TC-NETSOCK-212a SEND message enqueued"); net_process_messages(); CHECK(g_peers.send_reg_calls == 1, - "TC-NETSOCK-212b 超自旋预算后登记挂起 send(而不是继续占着 netTask 忙等)"); + "TC-NETSOCK-212b past the spin budget it registers a pending send instead of busy-waiting in netTask"); CHECK(g_peers.last_snd_fd == fd && g_peers.last_snd_len == (int)sizeof(buf) && g_peers.last_snd_sent == 0, - "TC-NETSOCK-212c 登记的进度=已写芯片字节数(此处 0),总长与 caller 一并带走"); + "TC-NETSOCK-212c registered progress = bytes already written to the chip (0 here), length and caller travel too"); CHECK(g_ns_notify_calls == 0, - "TC-NETSOCK-212d 登记成功**绝不发即时回执**(否则「登记成功」被当成本次结果)"); + "TC-NETSOCK-212d a successful registration NEVER sends an immediate receipt (else it reads as the result)"); CHECK(g_ns_notify_last_to == NULL, - "TC-NETSOCK-212e 一次通知都没发生(0x70000000 哨兵值也没可能漏进通知值)"); + "TC-NETSOCK-212e not a single notification (the 0x70000000 sentinel can not leak into a notify value)"); /* 完成时刻:由挂起 send 的 tick 结束(本套由桩脚本模拟"缓冲腾出来了") */ ns_peers_arm_send_pending_done(fd, 64); net_process_messages(); CHECK(g_ns_notify_calls == 1 && g_ns_notify_last_val == 64U, - "TC-NETSOCK-212f 挂起完成后才发回执,通知值=实际写出字节数"); + "TC-NETSOCK-212f receipt only after the pending completes, notify value = bytes actually written"); reset_all(); } @@ -201,7 +201,7 @@ static void tc_213_send_error_code_roundtrip(void) net_process_messages(); CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_NOTCONN, - "TC-NETSOCK-213a 未连接的 SEND:通知值=NET_ERR_NOTCONN(精确码回传,供调用方分支)"); + "TC-NETSOCK-213a SEND on a not-connected socket: notify value = NET_ERR_NOTCONN (exact code for caller branches)"); /* 端到端:公开 net_send 的 BADF 形态(fd 越界由**分派侧**再判一次) */ reset_all(); @@ -215,16 +215,16 @@ static void tc_213_send_error_code_roundtrip(void) (void)ns_rtos_q_put_raw(&msg); net_process_messages(); CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, - "TC-NETSOCK-213b 范围内但未分配的 fd:net_get_sock 判空 → BADF 搭通知值回来"); + "TC-NETSOCK-213b in range but unallocated fd: net_get_sock null -> BADF travels on the notify value"); /* 调用方上下文恢复 errno:拿到的必须是"本任务的"精确码 */ reset_all(); bring_up(); g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; rc = net_send(6, buf, 4, 0); /* 6 未分配 → 分派侧 BADF → 通知值 -2 */ - CHECK(rc == -1, "TC-NETSOCK-213c 对外保持 BSD 语义:失败一律 -1"); + CHECK(rc == -1, "TC-NETSOCK-213c BSD semantics kept for callers: any failure is -1"); CHECK(net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-213d 调用方在自己的错误码槽里拿到精确码(E-03 + 通知值回传合力)"); + "TC-NETSOCK-213d caller reads the exact code from its own errno slot (E-03 plus notify value together)"); reset_all(); } @@ -243,13 +243,13 @@ static void tc_220_recv_fast_path(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; rc = net_recv(fd, buf, 10, NET_MSG_DONTWAIT); - CHECK(rc == 10, "TC-NETSOCK-220a 缓冲 1400B、只给 10B 缓冲 → 读满 10(部分读合法)"); + CHECK(rc == 10, "TC-NETSOCK-220a chip has 1400B but the caller buffer is 10B -> reads 10 (a partial read is legal)"); CHECK(g_chip.read_calls == 1 && g_chip.last_read_len == 10U, - "TC-NETSOCK-220b 只读调用方给的量(不得按芯片可用量越界写)"); - CHECK(s_net_socks[fd].recv_bytes == 10U, "TC-NETSOCK-220c 接收字节统计累计"); + "TC-NETSOCK-220b reads only what the caller offered (never the chip available length)"); + CHECK(s_net_socks[fd].recv_bytes == 10U, "TC-NETSOCK-220c received byte statistic accumulates"); CHECK(s_net_socks[fd].readable == 0U, - "TC-NETSOCK-220d 读过即清 readable(下一次 RECV_OK 中断再置)"); - CHECK(g_peers.pending_reg_calls == 0, "TC-NETSOCK-220e 数据已在手,不该登记挂起"); + "TC-NETSOCK-220d reading clears readable (the next RECV_OK interrupt sets it again)"); + CHECK(g_peers.pending_reg_calls == 0, "TC-NETSOCK-220e data is already in hand, no pending registration"); reset_all(); } @@ -266,18 +266,18 @@ static void tc_221_recv_wouldblock(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT); CHECK(rc == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, - "TC-NETSOCK-221a DONTWAIT 无数据 → -1 + WOULDBLOCK"); + "TC-NETSOCK-221a DONTWAIT with no data -> -1 plus WOULDBLOCK"); CHECK(g_peers.pending_reg_calls == 0, - "TC-NETSOCK-221b 非阻塞绝不登记挂起(否则调用方以为自己在等,实际已被挂住)"); + "TC-NETSOCK-221b non-blocking never registers pending (else the caller thinks it waits while it is parked)"); /* 模式属性(net_setblocking(0))与单次标志同判据 */ - CHECK(net_setblocking(fd, 0) == 0, "TC-NETSOCK-221c 切非阻塞模式成功"); + CHECK(net_setblocking(fd, 0) == 0, "TC-NETSOCK-221c switching to non-blocking mode succeeds"); rc = net_recv(fd, buf, (int)sizeof(buf), 0); CHECK(rc == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, - "TC-NETSOCK-221d blocking 属性=0 与 NET_MSG_DONTWAIT 等价生效"); + "TC-NETSOCK-221d blocking attribute 0 behaves exactly like NET_MSG_DONTWAIT"); CHECK(net_recv_nb(fd, buf, (int)sizeof(buf)) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, - "TC-NETSOCK-221e net_recv_nb 具名入口同路径"); + "TC-NETSOCK-221e net_recv_nb named entry takes the same path"); reset_all(); } @@ -296,12 +296,12 @@ static void tc_222_recv_eof_relisten(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT); - CHECK(rc == 0, "TC-NETSOCK-222a CLOSED+无数据 → EOF(0),不是错误"); + CHECK(rc == 0, "TC-NETSOCK-222a CLOSED with no data -> EOF(0), not an error"); CHECK(g_peers.listener_req_calls == 1 && g_peers.last_req_reason == (int)NET_LISTENER_REQ_EOF, - "TC-NETSOCK-222b EOF 登记 REQ_EOF(重建动作交监听层唯一执行者,本层不动芯片)"); + "TC-NETSOCK-222b EOF registers REQ_EOF (the rebuild belongs to the listener layer, this layer never touches the chip)"); CHECK(g_chip.open_calls == 0 && g_chip.listen_calls == 0 && g_chip.close_calls == 0, - "TC-NETSOCK-222c 登记路径上**一个芯片事务都不发**(单一所有者不变量 II)"); + "TC-NETSOCK-222c the registration path issues NOT ONE chip transaction (single-owner invariant II)"); /* 抢先 relisten 后 state 已回 LISTENING,但会话电平仍是"已结束" */ reset_all(); @@ -313,7 +313,7 @@ static void tc_222_recv_eof_relisten(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT); CHECK(rc == 0, - "TC-NETSOCK-222d 已 relisten 回 LISTENING 仍判 EOF(N-32 P1 电平化:否则 EOF 永久失踪)"); + "TC-NETSOCK-222d still EOF after relisten moved it back to LISTENING (N-32 P1 level, else EOF vanishes forever)"); /* auto_relisten=0(客户端 fd):EOF 不登记重建,交调用方决定重连 */ reset_all(); @@ -325,7 +325,7 @@ static void tc_222_recv_eof_relisten(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; rc = net_recv(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT); CHECK(rc == 0 && g_peers.listener_req_calls == 0, - "TC-NETSOCK-222e 非监听 fd 的 EOF 不许替调用方擅自重建"); + "TC-NETSOCK-222e EOF on a non-listener fd must not rebuild behind the caller back"); reset_all(); } @@ -351,19 +351,19 @@ static void tc_223_recv_pending_roundtrip(void) net_process_messages(); CHECK(g_peers.pending_reg_calls == 1 && g_peers.last_pend_fd == fd, - "TC-NETSOCK-223a 阻塞无数据 → 登记挂起 recv(netTask 零占用返回)"); + "TC-NETSOCK-223a blocking with no data -> registers a pending recv (netTask returns with zero occupancy)"); CHECK(g_peers.last_pend_to == 250U, - "TC-NETSOCK-223b 调用方的显式预算随登记带走(0=无限,这里必须不是 0)"); + "TC-NETSOCK-223b the caller explicit budget travels with the registration (0 means infinite, here it must not be 0)"); CHECK(g_peers.last_pend_caller == (TaskHandle_t)&g_ns_task_caller, - "TC-NETSOCK-223c 完成时要知道通知谁"); + "TC-NETSOCK-223c completion must know whom to notify"); CHECK(g_ns_notify_calls == 0, - "TC-NETSOCK-223d 登记即发回执 = 把「登记成功」当结果,本条钉它没发生"); + "TC-NETSOCK-223d registering then immediately notifying would pass off registration as the result - pinned absent"); /* 数据到达:由挂起 recv 的 tick 结束(脚本模拟) */ ns_peers_arm_pending_done(fd, 7); net_process_messages(); CHECK(g_ns_notify_calls == 1 && g_ns_notify_last_val == 7U, - "TC-NETSOCK-223e 数据到达后才回执,通知值=字节数"); + "TC-NETSOCK-223e receipt only once data arrives, notify value = byte count"); reset_all(); } @@ -381,7 +381,7 @@ static void tc_224_recv_to_timeout_code(void) /* 先造一条"已登记"的挂起,再让 tick 判到点 */ ns_peers_arm_pending_done(fd, NET_ERR_TIMEDOUT); rc = net_recv_to(fd, buf, (int)sizeof(buf), 30U); - CHECK(rc == -2, "TC-NETSOCK-224a 本片超时 → 独立返回码 -2(与真错误 -1 区分)"); + CHECK(rc == -2, "TC-NETSOCK-224a this slice timed out -> dedicated return code -2 (distinct from a real error -1)"); reset_all(); } @@ -396,21 +396,21 @@ static void tc_225_recv_bad_args(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; CHECK(net_recv_to(0, buf, 4, 0U) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-225a net_recv_to(timeout=0) 直接拒——0 在 netTask 侧是「无限等待」," - "与本接口「有界占用」的存在理由相反"); - CHECK(g_ns_put_calls == 0, "TC-NETSOCK-225b 拒绝发生在入队之前"); + "TC-NETSOCK-225a net_recv_to(timeout=0) is refused up front - 0 means infinite wait on the netTask side," + "which contradicts this API whole reason for having a bounded occupancy"); + CHECK(g_ns_put_calls == 0, "TC-NETSOCK-225b the refusal happens before enqueue"); CHECK(net_recv(fd, NULL, 4, NET_MSG_DONTWAIT) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-225c buf 为 NULL → INVAL"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-225c buf NULL -> INVAL"); CHECK(net_recv(fd, buf, 0, NET_MSG_DONTWAIT) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-225d len<=0 → INVAL"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-225d len <= 0 -> INVAL"); CHECK(net_send(fd, NULL, 4, 0) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-225e send 的 buf 为 NULL → INVAL"); + "TC-NETSOCK-225e send with buf NULL -> INVAL"); CHECK(net_send(fd, buf, -1, 0) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-225f send 的 len<0 → INVAL"); + "TC-NETSOCK-225f send with len < 0 -> INVAL"); CHECK(g_chip.write_calls == 0 && g_chip.read_calls == 0 && g_peers.pending_reg_calls == 0 && g_peers.send_reg_calls == 0, - "TC-NETSOCK-225g 参数非法路径上零芯片事务、零挂起登记"); + "TC-NETSOCK-225g on the invalid-argument path: zero chip transactions and zero pending registrations"); reset_all(); } @@ -433,7 +433,7 @@ static void tc_230_close_order_and_peer(void) (void)ns_rtos_q_put_raw(&msg); net_process_messages(); /* 走登记路径(不发回执) */ CHECK(g_peers.pending_reg_calls == 1 && g_ns_notify_calls == 0, - "TC-NETSOCK-230a 前置:已登记挂起且未回执"); + "TC-NETSOCK-230a precondition: pending registered and no receipt yet"); /* 芯片侧此刻仍是 OPEN:close 才需要真去拆(已 CLOSED 的形态见 TC-NETSOCK-232) */ g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; @@ -450,19 +450,19 @@ static void tc_230_close_order_and_peer(void) net_process_messages(); CHECK(g_peers.pending_settle_calls == s_close_base_settle + 1, - "TC-NETSOCK-230b close 必先结算本 fd 的挂起 recv(否则等待方永久悬挂)"); + "TC-NETSOCK-230b close must first settle this fd pending recv (else the waiter hangs forever)"); CHECK(g_peers.pending_notified == 1, - "TC-NETSOCK-230c 结算必通知(取消也要通知,不是静默清槽)"); + "TC-NETSOCK-230c settling always notifies (a cancel notifies too, not a silent slot clear)"); CHECK(g_chip.close_calls >= 1 && g_fxchip.rst_begin_calls == 1, - "TC-NETSOCK-230d 芯片仍 OPEN → 走 sock_reset 确定性排渣把它拆掉"); + "TC-NETSOCK-230d chip still OPEN -> sock_reset deterministically drains and tears it down"); CHECK(g_fxchip.glob_calls == 0, - "TC-NETSOCK-230e close 全程不得读全局中断寄存器(B3:唯一读者=net_poll)"); + "TC-NETSOCK-230e close must never read the global interrupt register (B3: sole reader is net_poll)"); CHECK(s_net_socks[fd].in_use == 0U && s_net_socks[fd].last_peer_port == 51999U, - "TC-NETSOCK-230f 控制块清零但 last_peer_port 保留(幽灵甄别基准属芯片 Socket 级)"); + "TC-NETSOCK-230f control block cleared but last_peer_port kept (ghost discrimination is chip-socket level)"); CHECK(g_peers.listener_clear_calls == s_close_base_clear + 1, - "TC-NETSOCK-230g close 幂等清理监听重试(关闭即取消待重试,D10)"); + "TC-NETSOCK-230g close idempotently clears the listener retry (closing cancels pending retries, D10)"); CHECK(g_peers.pend_active_now[fd] == 0, - "TC-NETSOCK-230h close 后本 fd 不再有挂起 recv(两张表都归零)"); + "TC-NETSOCK-230h after close this fd has no pending recv (both tables zeroed)"); reset_all(); } @@ -479,8 +479,8 @@ static void tc_231_close_badfd(void) (void)ns_rtos_q_put_raw(&msg); net_process_messages(); CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, - "TC-NETSOCK-231a close 未分配的 fd → BADF 经通知值回传"); - CHECK(g_chip.close_calls == 0, "TC-NETSOCK-231b BADF 不碰芯片"); + "TC-NETSOCK-231a close on an unallocated fd -> BADF travels back on the notify value"); + CHECK(g_chip.close_calls == 0, "TC-NETSOCK-231b BADF does not touch the chip"); bring_up(); (void)memset(&msg, 0, sizeof(msg)); @@ -490,7 +490,7 @@ static void tc_231_close_badfd(void) (void)ns_rtos_q_put_raw(&msg); net_process_messages(); CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_INVAL, - "TC-NETSOCK-231c accept 未在用 → INVAL(控制块级校验,不是 fd 越界)"); + "TC-NETSOCK-231c accept on an unused fd -> INVAL (control-block check, not fd range)"); reset_all(); } @@ -508,7 +508,7 @@ static void tc_240_drop_conn_branches(void) msg.caller = (TaskHandle_t)&g_ns_task_caller; (void)ns_rtos_q_put_raw(&msg); net_process_messages(); - CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, "TC-NETSOCK-240a 未分配 → BADF"); + CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, "TC-NETSOCK-240a unallocated -> BADF"); /* UDP → INVAL */ bring_up(); @@ -520,7 +520,7 @@ static void tc_240_drop_conn_branches(void) (void)ns_rtos_q_put_raw(&msg); net_process_messages(); CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_INVAL, - "TC-NETSOCK-240b 非 TCP 流 → INVAL(本 API 只拆 TCP 会话)"); + "TC-NETSOCK-240b not a TCP stream -> INVAL (this API only tears down TCP sessions)"); /* 监听态但无活动会话 → 幂等 0,不动芯片 */ bring_up(); @@ -533,9 +533,9 @@ static void tc_240_drop_conn_branches(void) msg.caller = (TaskHandle_t)&g_ns_task_caller; (void)ns_rtos_q_put_raw(&msg); net_process_messages(); - CHECK(g_ns_notify_last_val == 0U, "TC-NETSOCK-240c 无活动会话 → 幂等成功 0"); + CHECK(g_ns_notify_last_val == 0U, "TC-NETSOCK-240c no active session -> idempotent success 0"); CHECK(g_chip.close_calls == 0 && g_fxchip.rst_begin_calls == 0, - "TC-NETSOCK-240d 幂等分支不得拆芯片(会把在听的监听毁掉)"); + "TC-NETSOCK-240d the idempotent branch must not tear down the chip (it would destroy the live listener)"); /* ESTABLISHED + 监听型 → QUARANTINE 拆除 + 登记重建 */ bring_up(); @@ -549,18 +549,18 @@ static void tc_240_drop_conn_branches(void) net_process_messages(); CHECK(g_peers.pending_settle_calls == 1 && g_peers.send_cancel_calls == 1, - "TC-NETSOCK-240e 拆会话前先结束两张表的挂起(必通知)"); + "TC-NETSOCK-240e both pending tables are ended before dropping the session (with notification)"); CHECK(g_fxchip.rst_begin_calls == 1, - "TC-NETSOCK-240f 拆除走 sock_reset 确定性排渣(不是 close+盲等)"); + "TC-NETSOCK-240f teardown uses sock_reset deterministic draining, not close plus blind waiting"); CHECK(g_peers.listener_req_calls == 1 && g_peers.last_req_reason == (int)NET_LISTENER_REQ_DROP, - "TC-NETSOCK-240g 监听型 fd 拆完登记 REQ_DROP(重建交监听层,本层不自己 open)"); + "TC-NETSOCK-240g listener-type fd registers REQ_DROP after the drop (rebuild belongs to the listener layer)"); CHECK(s_net_socks[fd].state == NET_SOCK_STATE_CLOSED && s_net_socks[fd].in_use == 1U && s_net_socks[fd].local_port == 2404U && s_net_socks[fd].auto_relisten == 1U, - "TC-NETSOCK-240h 只收口会话簿记:fd 保留、监听属性与端口都留下"); + "TC-NETSOCK-240h only session bookkeeping is closed out: fd, listener attribute and port all survive"); CHECK(g_fxchip.glob_calls == 0, - "TC-NETSOCK-240i 拆除路径也只碰本 Socket(F-NET-7:不读全局)"); + "TC-NETSOCK-240i teardown only touches its own socket (F-NET-7: never reads the global register)"); reset_all(); } @@ -577,17 +577,17 @@ static void tc_250_put_fail_busy(void) rc = net_send(fd, "abc", 3, 0); CHECK(rc == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-250a 队列满 → net_send 报 BUSY(不是静默丢弃)"); + "TC-NETSOCK-250a queue full -> net_send reports BUSY, it does not silently drop"); CHECK(g_chip.write_calls == 0 && g_ns_notify_calls == 0, - "TC-NETSOCK-250b 入队失败时零芯片事务、零通知(调用方不会误等回执)"); + "TC-NETSOCK-250b on enqueue failure: zero chip transactions and zero notifications (caller will not wait in vain)"); CHECK(net_listen(fd) == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-250c 同一形态对 net_listen 成立"); + "TC-NETSOCK-250c the same shape holds for net_listen"); CHECK(net_close(fd) == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-250d 同一形态对 net_close 成立"); + "TC-NETSOCK-250d the same shape holds for net_close"); CHECK(net_drop_conn(fd) == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-250e 同一形态对 net_drop_conn 成立"); + "TC-NETSOCK-250e the same shape holds for net_drop_conn"); CHECK(net_recv_to(fd, (void *)"x", 1, 5U) == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-250f 同一形态对 net_recv_to 成立"); + "TC-NETSOCK-250f the same shape holds for net_recv_to"); reset_all(); } diff --git a/test/host/net_socket/ns_s4_state.c b/test/host/net_socket/ns_s4_state.c index 6ebe7890..2ba1efb4 100644 --- a/test/host/net_socket/ns_s4_state.c +++ b/test/host/net_socket/ns_s4_state.c @@ -31,24 +31,24 @@ static void tc_300_poll_gates(void) /* 1) 未初始化:直接 0,连引脚都不该读 */ reset_all(); g_ns_pump = net_process_messages; - CHECK(net_poll() == 0, "TC-NETSOCK-300a 未初始化 → 0 事件"); + CHECK(net_poll() == 0, "TC-NETSOCK-300a uninitialized -> 0 events"); CHECK(g_ns_poll_calls == 0U && g_fxchip.glob_calls == 0, - "TC-NETSOCK-300b 未初始化不得读 INT# 引脚、不得读全局中断(省 SPI)"); + "TC-NETSOCK-300b uninitialized must not read the INT# pin nor the global interrupt register (saves SPI)"); /* 2) 初始化了但 INT# 为高(无中断待处理):不读全局寄存器 */ bring_up(); g_ns_int_pin_set = 1; - CHECK(net_poll() == 0, "TC-NETSOCK-300c INT# 高 → 0 事件"); + CHECK(net_poll() == 0, "TC-NETSOCK-300c INT# high -> 0 events"); CHECK(g_fxchip.glob_calls == 0, - "TC-NETSOCK-300d INT# 高时不得发全局中断读取(引脚轮询就是为省这一步)"); + "TC-NETSOCK-300d with INT# high no global interrupt read may be issued (pin polling exists to save that step)"); /* 3) INT# 低但全局状态为 0:读了全局一次,且不进 Socket 循环 */ bring_up(); g_ns_int_pin_set = 0; ch395f_fake_push_glob(0U); - CHECK(net_poll() == 0, "TC-NETSOCK-300e 全局位全 0 → 0 事件"); + CHECK(net_poll() == 0, "TC-NETSOCK-300e all global bits clear -> 0 events"); CHECK(g_fxchip.glob_calls == 1 && g_chip.int_status_calls == 0, - "TC-NETSOCK-300f 全局只读一次,读到 0 立即收手(不逐个 Socket 扫)"); + "TC-NETSOCK-300f global register read exactly once and it stops on 0 (no per-socket sweep)"); reset_all(); } @@ -63,7 +63,7 @@ static void tc_301_poll_only_gated_sockets(void) a = mk_established(NET_SOCK_STREAM); b = mk_established(NET_SOCK_STREAM); (void)mk_established(NET_SOCK_STREAM); - CHECK(a == 0 && b == 1, "TC-NETSOCK-301a 前置:三个在用 fd"); + CHECK(a == 0 && b == 1, "TC-NETSOCK-301a precondition: three fds in use"); prime_sint_masks(); g_ns_int_pin_set = 0; @@ -73,15 +73,15 @@ static void tc_301_poll_only_gated_sockets(void) CHECK(g_fake.int_status[0] == (uint8_t)(0x01U << 0) && g_fake.int_status[2] == (uint8_t)(0x01U << 2), - "TC-NETSOCK-301b 未点亮的 Socket 0/2 挂起位图没被清(读即清=读过,没读=还在)"); - CHECK(g_fake.int_status[1] == 0U, "TC-NETSOCK-301c 点亮的 Socket 1 被读走(位图清零)"); - CHECK(g_chip.int_status_calls == 1, "TC-NETSOCK-301d 整轮只发了一次 SINT 读取"); + "TC-NETSOCK-301b pending bitmaps of unlit sockets 0/2 stay uncleared (read-clear means read, still there means not read)"); + CHECK(g_fake.int_status[1] == 0U, "TC-NETSOCK-301c the lit socket 1 got read (bitmap cleared)"); + CHECK(g_chip.int_status_calls == 1, "TC-NETSOCK-301d the whole round issued exactly one SINT read"); /* 缓存刷新环是另一回事:它对所有在用 fd 都要刷(单一事实源 I1) */ CHECK(g_fxchip.rst_poll_calls == 3, - "TC-NETSOCK-301e 每轮对全部 3 个在用 fd 步进 sock_reset(IDLE 槽零 SPI)"); + "TC-NETSOCK-301e every round steps sock_reset for all 3 in-use fds (IDLE slots cost zero SPI)"); (void)net_poll(); CHECK(g_fxchip.rst_poll_calls == 6, - "TC-NETSOCK-301f 再一轮再加 3 次:刷新环不受全局位门控(事件面与状态面分离)"); + "TC-NETSOCK-301f another round adds 3 more: the refresh loop is not gated by global bits (events vs state split)"); reset_all(); /* 高位字节:Socket 4~7 的全局位在 bit8~bit11 */ @@ -94,7 +94,7 @@ static void tc_301_poll_only_gated_sockets(void) ch395f_fake_push_glob((uint16_t)(1U << (4 + 4))); (void)net_poll(); CHECK(g_fake.int_status[4] == 0U && g_fake.int_status[3] == (uint8_t)(0x01U << 3), - "TC-NETSOCK-301g Socket4~7 用高位字节 bit(n+4)=bit8..11 定位(算错就永远收不到 4~7 的事件)"); + "TC-NETSOCK-301g sockets 4-7 are located in the high byte bit(n+4)=bit8..11 (a wrong shift never sees their events)"); reset_all(); } @@ -121,33 +121,33 @@ static void tc_302_poll_event_dispatch(void) ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); n = net_poll(); - CHECK(n == expect, "TC-NETSOCK-302a 事件计数按位型区分(SENBUF_FREE 是状态位不是事件)"); + CHECK(n == expect, "TC-NETSOCK-302a event counting distinguishes bit kinds (SENBUF_FREE is a state bit, not an event)"); switch (bits[k]) { case CH395F_SINT_STAT_CONNECT: - CHECK(g_peers.evt_connect_calls == 1, "TC-NETSOCK-302b CONNECT → handle_connect_event"); + CHECK(g_peers.evt_connect_calls == 1, "TC-NETSOCK-302b CONNECT -> handle_connect_event"); break; case CH395F_SINT_STAT_DISCONNECT: - CHECK(g_peers.evt_disconnect_calls == 1, "TC-NETSOCK-302c DISCONNECT → handle_disconnect_event"); + CHECK(g_peers.evt_disconnect_calls == 1, "TC-NETSOCK-302c DISCONNECT -> handle_disconnect_event"); break; case CH395F_SINT_STAT_RECV_OK: CHECK(g_peers.evt_recv_calls == 1 && s_net_socks[fd].readable == 1U, - "TC-NETSOCK-302d RECV_OK → handle_recv_event 置 readable 电平"); + "TC-NETSOCK-302d RECV_OK -> handle_recv_event sets the readable level"); break; case CH395F_SINT_STAT_SEND_OK: - CHECK(g_peers.evt_send_calls == 1, "TC-NETSOCK-302e SEND_OK → handle_send_event"); + CHECK(g_peers.evt_send_calls == 1, "TC-NETSOCK-302e SEND_OK -> handle_send_event"); break; case CH395F_SINT_STAT_SOCK_TIMEOUT: - CHECK(g_peers.evt_timeout_calls == 1, "TC-NETSOCK-302f TIMEOUT → handle_timeout_event"); + CHECK(g_peers.evt_timeout_calls == 1, "TC-NETSOCK-302f TIMEOUT -> handle_timeout_event"); break; default: CHECK(s_net_socks[fd].send_ready == 1U && g_peers.evt_send_calls == 0, - "TC-NETSOCK-302g SENBUF_FREE 只置 send_ready(不发事件、不误当 SEND_OK)"); + "TC-NETSOCK-302g SENBUF_FREE only sets send_ready (no event, never mistaken for SEND_OK)"); break; } CHECK(g_peers.evt_connect_calls + g_peers.evt_disconnect_calls + g_peers.evt_recv_calls + g_peers.evt_send_calls + g_peers.evt_timeout_calls <= 1, - "TC-NETSOCK-302h 一位进来只敲一个 handler(不串味)"); + "TC-NETSOCK-302h one incoming bit drives exactly one handler (no cross-talk)"); } /* 同一次 SINT 里多个位 → 多个 handler 都被叫到,事件数叠加 */ @@ -157,9 +157,9 @@ static void tc_302_poll_event_dispatch(void) g_ns_int_pin_set = 0; g_fake.int_status[fd] = (uint8_t)(CH395F_SINT_STAT_RECV_OK | CH395F_SINT_STAT_DISCONNECT); ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); - CHECK(net_poll() == 2, "TC-NETSOCK-302i 同批多事件位:各计一次(不吞后到的那位)"); + CHECK(net_poll() == 2, "TC-NETSOCK-302i multiple event bits in one batch: each counted (the later bit is not swallowed)"); CHECK(g_peers.evt_recv_calls == 1 && g_peers.evt_disconnect_calls == 1, - "TC-NETSOCK-302j 两位各归各的 handler"); + "TC-NETSOCK-302j two bits each reach their own handler"); reset_all(); } @@ -172,7 +172,7 @@ static void tc_303_phy_feed(void) ch395f_fake_push_glob(CH395F_GINT_STAT_PHY_CHANGE); (void)net_poll(); CHECK(g_fake.link_up == 0U && g_fake.link_change == 1U, - "TC-NETSOCK-303 PHY_CHANGE → 驱动重读 PHY 并计一次链路变更(拔网线不再静默)"); + "TC-NETSOCK-303 PHY_CHANGE -> driver re-reads PHY and counts one link change (cable pull is no longer silent)"); reset_all(); } @@ -183,34 +183,34 @@ static void tc_310_listener_armed(void) reset_all(); CHECK(net_listener_armed(-1) == 0 && net_listener_armed(NET_MAX_SOCKETS) == 0, - "TC-NETSOCK-310a 越界 fd → 0(纯 RAM 读,不发 RPC)"); - CHECK(net_listener_armed(0) == 0, "TC-NETSOCK-310b 未分配的 fd → 0"); + "TC-NETSOCK-310a out of range fd -> 0 (pure RAM read, no RPC)"); + CHECK(net_listener_armed(0) == 0, "TC-NETSOCK-310b unallocated fd -> 0"); bring_up(); fd = mk_established(NET_SOCK_STREAM); CHECK(net_listener_armed(fd) == 1, - "TC-NETSOCK-310c ESTABLISHED(待 accept 电平)必须放行:i104 台架实证不放行则会话整场饿死"); + "TC-NETSOCK-310c ESTABLISHED (pending-accept level) must pass: i104 bench proved starvation without it"); s_net_socks[fd].state = NET_SOCK_STATE_LISTENING; CHECK(net_listener_armed(fd) == 1, - "TC-NETSOCK-310d LISTENING 放行:补 accept 探针在 net_accept_locked 内部,挡住永远到不了"); + "TC-NETSOCK-310d LISTENING must pass: the missed-accept probe lives inside net_accept_locked, blocking it never runs"); s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; s_net_socks[fd].auto_relisten = 1U; s_net_socks[fd].local_port = 7003U; s_net_socks[fd].readable = 1U; CHECK(net_listener_armed(fd) == 1, - "TC-NETSOCK-310e CLOSED+auto_relisten+有残留数据 → 放行(保住 closed-with-readable 接受)"); + "TC-NETSOCK-310e CLOSED plus auto_relisten plus leftover data -> pass (keeps closed-with-readable accept alive)"); s_net_socks[fd].readable = 0U; CHECK(net_listener_armed(fd) == 0, - "TC-NETSOCK-310f CLOSED 但无残留 → 必须挡下(否则整个 CLOSED 窗每轮发 RPC → 喂不上狗)"); + "TC-NETSOCK-310f CLOSED with no leftover must be blocked (else every round in that window sends an RPC and the watchdog starves)"); s_net_socks[fd].readable = 1U; s_net_socks[fd].auto_relisten = 0U; - CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310g 非监听型 fd 的 CLOSED 不放行"); + CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310g CLOSED on a non-listener fd does not pass"); s_net_socks[fd].auto_relisten = 1U; s_net_socks[fd].local_port = 0U; - CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310h 还没绑端口的半成品监听不放行"); + CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310h a half-built listener without a bound port does not pass"); s_net_socks[fd].state = NET_SOCK_STATE_CONNECTING; - CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310i CONNECTING 不是监听态 → 0"); + CHECK(net_listener_armed(fd) == 0, "TC-NETSOCK-310i CONNECTING is not a listening state -> 0"); reset_all(); } @@ -221,17 +221,17 @@ static void tc_311_can_read(void) reset_all(); CHECK(net_can_read(0) == 1, - "TC-NETSOCK-311a 描述符失效 → 1:让 recv 去报 BADF,别把连接不存在藏住"); + "TC-NETSOCK-311a dead descriptor -> 1: let recv report BADF instead of hiding a missing connection"); bring_up(); fd = mk_established(NET_SOCK_STREAM); s_net_socks[fd].readable = 0U; - CHECK(net_can_read(fd) == 0, "TC-NETSOCK-311b ESTABLISHED 且无可读 → 0"); + CHECK(net_can_read(fd) == 0, "TC-NETSOCK-311b ESTABLISHED with nothing readable -> 0"); s_net_socks[fd].readable = 1U; - CHECK(net_can_read(fd) == 1, "TC-NETSOCK-311c readable 置位 → 1"); + CHECK(net_can_read(fd) == 1, "TC-NETSOCK-311c readable set -> 1"); s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; s_net_socks[fd].readable = 0U; CHECK(net_can_read(fd) == 1, - "TC-NETSOCK-311d 连接已非 ESTABLISHED → 必须放行一次 recv 取回 EOF/错误"); + "TC-NETSOCK-311d connection no longer ESTABLISHED -> recv must pass once to fetch EOF or the error"); reset_all(); } @@ -244,23 +244,23 @@ static void tc_320_close_two_paths(void) bring_up(); fd = mk_established(NET_SOCK_STREAM); g_fake.cached_open[fd] = CH395F_SOCKET_CLOSED; - CHECK(net_close(fd) == 0, "TC-NETSOCK-320a close 成功(快路径)"); + CHECK(net_close(fd) == 0, "TC-NETSOCK-320a close succeeds (fast path)"); CHECK(g_fxchip.cmd_done_calls >= 1 && g_fxchip.rst_begin_calls == 0, - "TC-NETSOCK-320b 已 CLOSED → 跳过复位(省掉 100~600ms 的无占用)"); + "TC-NETSOCK-320b already CLOSED -> reset skipped (saves a pointless 100-600ms occupation)"); CHECK(g_fxchip.sock_events_calls >= 1, - "TC-NETSOCK-320c 快路径仍要清本 Socket 残留事件(防下一连接误判刚建立就断开)"); - CHECK(g_fxchip.glob_calls == 0, "TC-NETSOCK-320d 全程零全局读取(F-NET-7)"); + "TC-NETSOCK-320c the fast path still clears this socket leftover events (else the next connection sees a false immediate drop)"); + CHECK(g_fxchip.glob_calls == 0, "TC-NETSOCK-320d zero global reads throughout (F-NET-7)"); reset_all(); /* 慢路径:芯片还 OPEN → 走 sock_reset,且必须是 GRACEFUL */ bring_up(); fd = mk_established(NET_SOCK_STREAM); g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; - CHECK(net_close(fd) == 0, "TC-NETSOCK-320e close 成功(慢路径)"); + CHECK(net_close(fd) == 0, "TC-NETSOCK-320e close succeeds (slow path)"); CHECK(g_fxchip.rst_begin_calls == 1 && g_fx.rst_mode[fd] == 0U, - "TC-NETSOCK-320f 慢路径用 GRACEFUL(正常释放 100ms 底),不是 QUARANTINE"); + "TC-NETSOCK-320f slow path uses GRACEFUL (100ms floor for a normal release), not QUARANTINE"); CHECK(g_fx.rst_state[fd] == (uint8_t)CH395F_FAKE_RST_IDLE, - "TC-NETSOCK-320g 复位机收口回 IDLE(槽位可复用,不搁浅)"); + "TC-NETSOCK-320g the reset machine ends back at IDLE (slot reusable, never stranded)"); reset_all(); /* 复位启动被拒(槽位已在运行)→ close 仍放行,只留痕 */ @@ -268,9 +268,9 @@ static void tc_320_close_two_paths(void) fd = mk_established(NET_SOCK_STREAM); g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; g_fx.rst_state[fd] = (uint8_t)CH395F_FAKE_RST_DRAINED; - CHECK(net_close(fd) == 0, "TC-NETSOCK-320h 复位启动失败也放行 close(不卡调用方)"); + CHECK(net_close(fd) == 0, "TC-NETSOCK-320h close still succeeds when starting the reset fails (do not block the caller)"); CHECK(g_fxchip.rst_begin_reject == 1 && g_ns_error_count >= 1, - "TC-NETSOCK-320i 复位没成必须 [ERR] 留痕,交 reconciler/pump 对账接管"); + "TC-NETSOCK-320i a failed reset must leave an [ERR] line for the reconciler/pump to take over"); reset_all(); } @@ -286,19 +286,19 @@ static void tc_321_drop_conn_settle(void) s_net_socks[fd].lost_conn_seen = 3U; g_fake.cached_open[fd] = CH395F_SOCKET_OPEN; - CHECK(net_drop_conn(fd) == 0, "TC-NETSOCK-321a drop_conn 成功"); + CHECK(net_drop_conn(fd) == 0, "TC-NETSOCK-321a drop_conn succeeds"); CHECK(g_fxchip.rst_begin_calls == 1 && g_fx.rst_mode[fd] == 1U, - "TC-NETSOCK-321b 拆会话用 QUARANTINE 400ms 底(TRAP-NET-18 异常释放,与 close 分级)"); + "TC-NETSOCK-321b session drop uses QUARANTINE with a 400ms floor (TRAP-NET-18 abnormal release, graded against close)"); CHECK(s_net_socks[fd].lost_conn_seen == 0U, - "TC-NETSOCK-321c 新会话起点:事件未收到去抖计数清零(N-30)"); + "TC-NETSOCK-321c new session start: the missed-event debounce counter is cleared (N-30)"); CHECK(s_net_socks[fd].local_port == 2404U && s_net_socks[fd].auto_relisten == 1U && s_net_socks[fd].in_use == 1U, - "TC-NETSOCK-321d 端口/监听属性/fd 全部保留:这就是拆连接不拆监听"); + "TC-NETSOCK-321d port, listener attribute and fd all survive: this is drop-the-connection-not-the-listener"); CHECK(g_peers.listener_req_calls == 1 && g_peers.last_req_reason == (int)NET_LISTENER_REQ_DROP, - "TC-NETSOCK-321e 登记 REQ_DROP,重建动作在 net_listener_tick 一处"); + "TC-NETSOCK-321e registers REQ_DROP; the rebuild action lives only in net_listener_tick"); CHECK(s_net_socks[fd].readable == 0U && s_net_socks[fd].state == NET_SOCK_STATE_CLOSED, - "TC-NETSOCK-321f 收口会话簿记:清 readable + 转 CLOSED"); + "TC-NETSOCK-321f session bookkeeping closes out: clear readable and move to CLOSED"); reset_all(); } @@ -317,18 +317,18 @@ static void tc_322_close_before_chip(void) g_ns_pump = NULL; /* 本场景不靠泵,直接看登记结果 */ CHECK(net_recv_sock(&s_net_socks[fd], rxbuf, (int)sizeof(rxbuf), 0) == NET_PENDING_REGISTERED, - "TC-NETSOCK-322p 前置:阻塞 recv 登记了挂起(返回哨兵)"); + "TC-NETSOCK-322p precondition: blocking recv registered a pending request (returns the sentinel)"); ch395f_fake_clear_log(); settle_at = g_peers.pending_settle_calls; close_at = g_chip.close_calls; - CHECK(net_close_sock(&s_net_socks[fd]) == 0, "TC-NETSOCK-322a netTask 直调 close 成功"); + CHECK(net_close_sock(&s_net_socks[fd]) == 0, "TC-NETSOCK-322a netTask-direct close succeeds"); CHECK(g_peers.pending_settle_calls == settle_at + 1, - "TC-NETSOCK-322b close 必先结算本 fd 的挂起 recv"); + "TC-NETSOCK-322b close must first settle this fd pending recv"); CHECK(g_chip.close_calls > close_at || g_fxchip.rst_close_calls > 0, - "TC-NETSOCK-322c 结算之后芯片侧才收到 CLOSE"); + "TC-NETSOCK-322c the chip only sees CLOSE after the settlement"); CHECK(g_peers.pending_notified == 1, - "TC-NETSOCK-322d 结算必通知(取消也要通知,不是静默清槽 → 否则等待方永久悬挂)"); + "TC-NETSOCK-322d settlement always notifies (cancel notifies too, else the waiter hangs forever)"); reset_all(); } diff --git a/test/host/net_socket/ns_s5_io.c b/test/host/net_socket/ns_s5_io.c index fb02382d..8b51b029 100644 --- a/test/host/net_socket/ns_s5_io.c +++ b/test/host/net_socket/ns_s5_io.c @@ -34,80 +34,80 @@ static void tc_400_net_init_downlink(void) reset_all(); g_fx.exist_rc = (int)CH395F_STATUS_NOT_DETECTED; CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == -1, - "TC-NETSOCK-400a 芯片不在位 → -1"); + "TC-NETSOCK-400a chip not present -> -1"); CHECK(g_fxchip.reset_calls == 0 && g_fxchip.init_calls == 0, - "TC-NETSOCK-400b 不在位就不再复位/初始化(别把错误拖到协议栈里)"); - CHECK(g_ns_error_count == 1, "TC-NETSOCK-400c 芯片不在位必留 [ERR](否则只剩端口不通的现象)"); + "TC-NETSOCK-400b when absent it must not reset or initialise (do not drag the error into the stack)"); + CHECK(g_ns_error_count == 1, "TC-NETSOCK-400c an absent chip must leave an [ERR] line (else only a dead port is left to observe)"); /* 2) 协议栈初始化失败 */ reset_all(); g_fx.init_rc = (int)CH395F_STATUS_ERROR; CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == -1, - "TC-NETSOCK-400d ch395f_init 失败 → -1"); - CHECK(g_fxchip.ping_calls == 0, "TC-NETSOCK-400e 初始化失败后不得继续使能 PING(半配置状态)"); + "TC-NETSOCK-400d ch395f_init failure -> -1"); + CHECK(g_fxchip.ping_calls == 0, "TC-NETSOCK-400e after an init failure PING must not be enabled (half-configured state)"); /* 3) 版本门控:低于 0x46 必须**拒置** bit3(SOCKET_CLOSE),退回芯片自动关断 */ reset_all(); g_fx.version = 0x45U; CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == 0, - "TC-NETSOCK-400f 版本不足不是致命错:继续起来"); + "TC-NETSOCK-400f a too-old version is not fatal: bring-up continues"); CHECK(g_fx.fun_para == 0U && g_ns_error_count == 1, - "TC-NETSOCK-400g ver<0x46 → FUN_PARA 实发 0(bit3 拒置)+ [ERR] 留痕,不静默带病运行"); + "TC-NETSOCK-400g ver<0x46 -> FUN_PARA actually sends 0 (bit3 refused) plus [ERR], never silently degraded"); /* 4) 版本够新:FUN_PARA=0x08;bit1(TCP_SERVER 多连接) 与 bit0(保留) 都必须为 0 */ reset_all(); CHECK(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL) == 0, - "TC-NETSOCK-400h 正常初始化成功"); + "TC-NETSOCK-400h normal initialisation succeeds"); /* bit3=SOCKET_CLOSE 要置;bit1=TCP_SERVER 多连接(本项目单连接模型)与 * bit0=保留位都必须为 0——旧实现把 0x09 当「多 Socket 模式」下发,2026-10-11 * 定案为 0x08(bit0 是保留位,证据链 TRAP-NET-15)。所以掩码要考 bit0|bit1, * 拿 0x09 考会把我们正想要的 bit3 一起算进去(第一版就这么写错了)。 */ CHECK(g_fx.fun_para == CH395F_FUN_PARA_SOCKET_CLOSE && (g_fx.fun_para & 0x03UL) == 0UL, - "TC-NETSOCK-400i FUN_PARA 只置 bit3,bit0/bit1 为 0(0x09 是历史误值,TRAP-NET-15)"); + "TC-NETSOCK-400i FUN_PARA sets bit3 only, bit0/bit1 are 0 (0x09 was a historical mistake, TRAP-NET-15)"); CHECK(g_fx.mss == (uint16_t)CH395F_TCP_MSS && g_fx.retrans_cnt == 8U && g_fx.retrans_period == 200U, - "TC-NETSOCK-400j MSS/重传参数取 net_config.h 真值(8 次 / 200ms,P0-a)"); + "TC-NETSOCK-400j MSS and retransmit parameters come from net_config.h truth (8 tries / 200ms, P0-a)"); CHECK(g_fx.ka_idle == 60000U && g_fx.ka_intvl == 5000U && g_fx.ka_cnt == 3U, - "TC-NETSOCK-400k KeepAlive 60s/5s/3(且必须在 ch395f_init 之前下发)"); - CHECK(g_fx.mss != 0U || g_fx.ka_idle != 0U ? 1 : 0, "TC-NETSOCK-400k2 参数确有下发"); + "TC-NETSOCK-400k KeepAlive 60s/5s/3 and it must be issued before ch395f_init"); + CHECK(g_fx.mss != 0U || g_fx.ka_idle != 0U ? 1 : 0, "TC-NETSOCK-400k2 parameters really were issued"); CHECK(g_chip.log_n == 0 || ch395f_fake_log_find('n') > ch395f_fake_log_find('Q'), - "TC-NETSOCK-400l MSS/KeepAlive 等 SET 类命令排在 INIT_CH395 之前(手册硬约束)"); + "TC-NETSOCK-400l SET-class commands (MSS/KeepAlive) come before INIT_CH395 (manual hard requirement)"); /* 5) IP/掩码/网关三样都按点分顺位下发,并缓存本机 IP 供 getsockname 用 */ CHECK(g_fx.ip[0] == 192U && g_fx.ip[1] == 168U && g_fx.ip[2] == 1U && g_fx.ip[3] == 100U && g_fx.gw[3] == 1U && g_fx.mask[0] == 255U, - "TC-NETSOCK-400m IP/网关/掩码按 parse_ip_string 顺位下发(段 0 在 [0])"); + "TC-NETSOCK-400m IP, gateway and mask follow the parse_ip_string order (segment 0 lands at [0])"); CHECK(g_fxchip.set_dhcp_calls == 0 && g_fx.dhcp_enable == 0U, - "TC-NETSOCK-400n 指定了静态 IP 就绝不使能 DHCP"); + "TC-NETSOCK-400n with a static IP given, DHCP is never enabled"); /* 6) ip=NULL → 走 DHCP,不下发静态三样 */ reset_all(); (void)net_init(NULL, NULL, NULL, NULL); CHECK(g_fxchip.set_dhcp_calls == 1 && g_fx.dhcp_enable == 1U, - "TC-NETSOCK-400o ip=NULL → 使能 DHCP"); + "TC-NETSOCK-400o ip=NULL -> enables DHCP"); CHECK(g_fxchip.set_ip_calls == 0 && g_fxchip.set_gw_calls == 0 && - g_fxchip.set_mask_calls == 0, "TC-NETSOCK-400p DHCP 形态不下发静态地址"); + g_fxchip.set_mask_calls == 0, "TC-NETSOCK-400p the DHCP form issues no static address at all"); /* 7) MAC:全零=用芯片内置,不下发;非零必须下发(写 EEPROM,须在 INIT 之前) */ reset_all(); (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", mac_zero); - CHECK(g_fxchip.set_mac_calls == 0, "TC-NETSOCK-400q MAC 全零 → 保持芯片内置,不下发"); + CHECK(g_fxchip.set_mac_calls == 0, "TC-NETSOCK-400q all-zero MAC -> keep the chip built-in one, do not issue it"); reset_all(); (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", mac_real); CHECK(g_fxchip.set_mac_calls == 1 && g_fx.mac[0] == 0x02U && g_fx.mac[5] == 0x55U, - "TC-NETSOCK-400r 非零 MAC 下发(设备在线身份与 UI 显示一致)"); + "TC-NETSOCK-400r a non-zero MAC is issued (device online identity stays consistent with the UI)"); CHECK(ch395f_fake_log_find('M') < ch395f_fake_log_find('n') || ch395f_fake_log_find('M') < 0, - "TC-NETSOCK-400s SET_MAC 排在 INIT_CH395 **之前**(TRAP-NET-19:顺序反了本次上电不生效)"); + "TC-NETSOCK-400s SET_MAC comes BEFORE INIT_CH395 (TRAP-NET-19: reversed order does not take effect this power cycle)"); /* 8) 收发缓冲块布局:6 块/Socket,先配数据 Socket 再配监听 Socket 0(TRAP-NET-01) */ reset_all(); (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL); CHECK(g_fxchip.set_recv_buf_calls == NET_MAX_SOCKETS && g_fxchip.set_send_buf_calls == NET_MAX_SOCKETS, - "TC-NETSOCK-400t 8 个 Socket 全部显式重分配(上电默认只给 0~3)"); + "TC-NETSOCK-400t all 8 sockets are explicitly reallocated (power-on defaults only cover 0-3)"); for (i = 0; i < NET_MAX_SOCKETS; i++) { if (g_fx.recv_blk_start[i] != (uint8_t)(i * 6) || g_fx.recv_blk_cnt[i] != (uint8_t)CH395F_RECV_BLOCKS || @@ -117,20 +117,20 @@ static void tc_400_net_init_downlink(void) } } CHECK(i == NET_MAX_SOCKETS, - "TC-NETSOCK-400u 块布局 recv=i*6 / send=recv+4,Socket7 恰好用尽 0~47(手册 §5.45)"); - CHECK(g_fx.ping_enable == 1U && g_fxchip.ping_calls == 1, "TC-NETSOCK-400v 使能 PING 响应"); + "TC-NETSOCK-400u layout recv=i*6 / send=recv+4, socket 7 uses exactly blocks 0..47 (manual 5.45)"); + CHECK(g_fx.ping_enable == 1U && g_fxchip.ping_calls == 1, "TC-NETSOCK-400v PING response enabled"); /* 9) net_init 的第一件事是取消挂起并通知调用方(顺序错了 = 调用方永久挂死) */ reset_all(); (void)net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL); CHECK(g_peers.pending_cancel_all_calls == 1 && g_peers.send_cancel_all_calls == 1, - "TC-NETSOCK-400w 重建前两张挂起表都要取消(recv/send 各一张,漏一张就有人永远等)"); + "TC-NETSOCK-400w both pending tables are cancelled before a rebuild (recv and send each one, missing one strands a task)"); CHECK(g_peers.last_pend_cancel_err == NET_ERR_CONNRESET && g_peers.last_snd_cancel_err == NET_ERR_CONNRESET, - "TC-NETSOCK-400x 取消原因 = CONNRESET"); + "TC-NETSOCK-400x cancellation reason = CONNRESET"); CHECK(g_peers.pending_capture_calls == 1, - "TC-NETSOCK-400y 登记执行任务(挂起登记的自等死锁守卫据此判「谁在执行」)"); + "TC-NETSOCK-400y registers the executing task (the pending self-wait guard judges who is executing from it)"); reset_all(); } @@ -144,32 +144,32 @@ static void tc_410_listen_locked(void) bring_up(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-410a UDP 不能 listen"); + "TC-NETSOCK-410a UDP cannot listen"); /* 状态不对(还没 bind 也没到 CREATED)→ INVAL */ bring_up(); fd = mk_established(NET_SOCK_STREAM); CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-410b 已建立的 fd 再 listen → INVAL(不许把数据通道打回监听)"); + "TC-NETSOCK-410b listening again on an established fd -> INVAL (never demote a data channel to a listener)"); /* 成功路径:BOUND → LISTENING,并启用 auto_relisten */ bring_up(); fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); (void)memset(&in, 0, sizeof(in)); in.sin_port = net_htons(2404); - CHECK(net_bind(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, "TC-NETSOCK-410c bind 前置"); + CHECK(net_bind(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, "TC-NETSOCK-410c bind precondition"); ch395f_fake_clear_log(); - CHECK(net_listen_locked(fd) == 0, "TC-NETSOCK-410d listen 成功"); + CHECK(net_listen_locked(fd) == 0, "TC-NETSOCK-410d listen succeeds"); CHECK(s_net_socks[fd].state == NET_SOCK_STATE_LISTENING && s_net_socks[fd].auto_relisten == 1U && s_net_socks[fd].lost_conn_seen == 0U, - "TC-NETSOCK-410e 进 LISTENING + auto_relisten=1(BSD 监听常驻)+ 去抖计数清零"); + "TC-NETSOCK-410e enters LISTENING with auto_relisten=1 (BSD listener stays resident) and debounce cleared"); CHECK(g_chip.set_port_calls == 1 && g_chip.last_port == 2404U, - "TC-NETSOCK-410f 源端口取控制块的 local_port(端口不能漂)"); + "TC-NETSOCK-410f source port comes from the control block local_port (the port must not drift)"); CHECK(ch395f_fake_log_find('i') >= 0 && ch395f_fake_log_find('i') < ch395f_fake_log_find('l'), - "TC-NETSOCK-410g 清本 Socket 事件排在 TCP_LISTEN **之前**(E14:反了会吞掉新 CONNECT)"); + "TC-NETSOCK-410g clearing this socket events comes BEFORE TCP_LISTEN (E14: reversed swallows the new CONNECT)"); CHECK(g_fxchip.glob_calls == 0, - "TC-NETSOCK-410h listen 全程不得读全局中断寄存器(B3 审计掉的正是这个)"); + "TC-NETSOCK-410h listen must never read the global interrupt register (exactly what the B3 audit removed)"); /* open 一直失败:重试 3 次,每次都走确定性排渣,最后 BUSY */ reset_all(); @@ -177,10 +177,10 @@ static void tc_410_listen_locked(void) fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); g_fake.open_rc[fd] = CH395F_ERR_USE; CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-410i open 三次仍失败 → BUSY"); + "TC-NETSOCK-410i open still failing after 3 tries -> BUSY"); CHECK(g_chip.open_calls == 3 && g_fxchip.rst_begin_calls == 3, - "TC-NETSOCK-410j 三次 open + 三次 sock_reset(TRAP-NET-09 的重试语义保留,不是盲等 500ms)"); - CHECK(g_chip.listen_calls == 0, "TC-NETSOCK-410k open 没成绝不发 LISTEN 命令"); + "TC-NETSOCK-410j three opens plus three sock_reset (TRAP-NET-09 retry semantics kept, not a blind 500ms wait)"); + CHECK(g_chip.listen_calls == 0, "TC-NETSOCK-410k with no successful open, never issue the LISTEN command"); /* LISTEN 命令失败:open 已成的 Socket 必须关回去(U12 对称,否则卡 OPEN 无监听) */ reset_all(); @@ -188,11 +188,11 @@ static void tc_410_listen_locked(void) fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); g_fake.listen_rc[fd] = CH395F_ERR_VAL; CHECK(net_listen_locked(fd) == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-410l TCP_LISTEN 失败 → BUSY"); + "TC-NETSOCK-410l TCP_LISTEN failure -> BUSY"); CHECK(g_chip.close_calls == 1 && g_chip.open_calls == 1, - "TC-NETSOCK-410m LISTEN 失败必须 close 已开的 Socket(否则下次 open 恒返 USE)"); + "TC-NETSOCK-410m a failed LISTEN must close the opened socket (else every later open returns USE)"); CHECK(s_net_socks[fd].state != NET_SOCK_STATE_LISTENING, - "TC-NETSOCK-410n 失败不得谎报 LISTENING"); + "TC-NETSOCK-410n failure must not falsely report LISTENING"); reset_all(); } @@ -203,24 +203,24 @@ static void tc_411_listen_public(void) int fd; bring_up(); - CHECK(net_listen(-1) == -1 && net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-411a 越界 → BADF"); + CHECK(net_listen(-1) == -1 && net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-411a out of range -> BADF"); CHECK(net_listen(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-411b 上界越界 → BADF"); + "TC-NETSOCK-411b upper bound out of range -> BADF"); fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); (void)memset(&in, 0, sizeof(in)); in.sin_port = net_htons(7003); CHECK(net_bind(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, "TC-NETSOCK-411c bind"); g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; - CHECK(net_listen(fd) == 0, "TC-NETSOCK-411d 端到端 listen 成功(入队→分派→通知值 0)"); + CHECK(net_listen(fd) == 0, "TC-NETSOCK-411d end-to-end listen succeeds (enqueue, dispatch, notify value 0)"); CHECK(s_net_socks[fd].state == NET_SOCK_STATE_LISTENING, - "TC-NETSOCK-411e 公开入口确实驱动了芯片侧监听"); + "TC-NETSOCK-411e the public entry really drove the chip-side listener"); CHECK(g_ns_notify_last_val == 0U, - "TC-NETSOCK-411f 成功的 listen 通知值=0(哨兵 0x70000000 不许漏进来)"); + "TC-NETSOCK-411f a successful listen notify value is 0 (the 0x70000000 sentinel must not leak)"); /* 已监听的 fd 再 listen:netTask 里 INVAL,经通知值回传 */ CHECK(net_listen(fd) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-411g 重复 listen → -1 且 errno=INVAL(精确码,不是笼统 -1)"); + "TC-NETSOCK-411g listening twice -> -1 with errno INVAL (exact code, not a generic -1)"); reset_all(); } @@ -238,12 +238,12 @@ static void tc_420_accept_shapes(void) addrlen = (int)sizeof(in); (void)memset(&in, 0, sizeof(in)); g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; - CHECK(net_accept(fd, sa, &addrlen) == fd, "TC-NETSOCK-420a 单连接:accept 返回监听 fd 自身"); + CHECK(net_accept(fd, sa, &addrlen) == fd, "TC-NETSOCK-420a single connection: accept returns the listening fd itself"); CHECK(in.sin_family == NET_AF_INET && net_ntohs(in.sin_port) == 51999U && addrlen == (int)sizeof(in), - "TC-NETSOCK-420b 回填对端地址与长度(lftpd 打印「谁连进来」就靠这个)"); + "TC-NETSOCK-420b fills the peer address and length (lftpd prints who connected from this)"); CHECK(g_peers.lost_probe_calls == 0, - "TC-NETSOCK-420c 非 LISTENING 不触发补 accept 探针(探针是监听态专用自救)"); + "TC-NETSOCK-420c non-LISTENING never calls the missed-accept probe (the probe is listener-only self-rescue)"); /* 2) LISTENING 且探针命中 → 接受(CONNECT 事件未收到的自愈路径) */ reset_all(); @@ -255,9 +255,9 @@ static void tc_420_accept_shapes(void) g_peers.lost_probe_hit = 1; addrlen = 0; CHECK(net_accept(fd, NULL, NULL) == fd, - "TC-NETSOCK-420d 探针把监听转 ESTABLISHED → accept 立即成功(N-30 主根因修复)"); + "TC-NETSOCK-420d the probe moved the listener to ESTABLISHED -> accept succeeds at once (N-30 root fix)"); CHECK(g_peers.lost_probe_calls == 1, - "TC-NETSOCK-420e LISTENING 才调探针(每轮 accept 一次,带去抖计数)"); + "TC-NETSOCK-420e only LISTENING calls the probe (once per accept round, with debounce)"); /* 3) LISTENING 且无连接 → WOULDBLOCK,且不动监听重建 */ reset_all(); @@ -266,9 +266,9 @@ static void tc_420_accept_shapes(void) s_net_socks[fd].state = NET_SOCK_STATE_LISTENING; addrlen = (int)sizeof(in); CHECK(net_accept(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, - "TC-NETSOCK-420f 无连接 → WOULDBLOCK(不阻塞 netTask)"); + "TC-NETSOCK-420f no connection -> WOULDBLOCK (never blocks netTask)"); CHECK(g_peers.listener_req_calls == 0, - "TC-NETSOCK-420g 监听还活着时绝不登记重建(那是 U12 事故的形态)"); + "TC-NETSOCK-420g while the listener is alive, never register a rebuild (that is the U12 accident shape)"); /* 4) CLOSED + auto_relisten + RX 有残留 → 接受"已关但可读",先排空再 EOF */ reset_all(); @@ -281,11 +281,11 @@ static void tc_420_accept_shapes(void) ch395f_fake_push_recv_len((uint8_t)fd, 1027U); addrlen = (int)sizeof(in); CHECK(net_accept(fd, sa, &addrlen) == fd, - "TC-NETSOCK-420h closed-with-readable:接受这条连接去排空(N-30 定案,反了就是 425)"); + "TC-NETSOCK-420h closed-with-readable: accept this connection to drain it (N-30, reversed yields 425)"); CHECK(g_peers.listener_req_calls == 0, - "TC-NETSOCK-420i 有残留数据时**不**登记重建(先 relisten 会把已到达的数据销毁)"); + "TC-NETSOCK-420i with leftover data present, do NOT register a rebuild (relisting first destroys arrived data)"); CHECK(g_chip.get_status_calls == 0, - "TC-NETSOCK-420j 排空判据只读 GET_RECV_LEN,不发状态查询"); + "TC-NETSOCK-420j the drain decision only reads GET_RECV_LEN, no status query"); /* 5) CLOSED + auto_relisten + RX 空 → 登记 REQ_ACCEPT 并 WOULDBLOCK */ reset_all(); @@ -295,13 +295,13 @@ static void tc_420_accept_shapes(void) s_net_socks[fd].auto_relisten = 1U; s_net_socks[fd].local_port = 21U; CHECK(net_accept(fd, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, - "TC-NETSOCK-420k CLOSED 且无残留 → WOULDBLOCK"); + "TC-NETSOCK-420k CLOSED with no leftover -> WOULDBLOCK"); CHECK(g_peers.listener_req_calls == 1 && g_peers.last_req_reason == (int)NET_LISTENER_REQ_ACCEPT && g_peers.last_req_fd == fd, - "TC-NETSOCK-420l 消费者撞空才登记即时重建(N-32 P2:删掉它会拉长 CLOSED 窗)"); + "TC-NETSOCK-420l only a consumer hitting empty registers an immediate rebuild (N-32 P2: removing it widens the CLOSED window)"); CHECK(g_chip.open_calls == 0 && g_chip.listen_calls == 0, - "TC-NETSOCK-420m 登记路径零芯片事务(重建动作只在 net_listener_tick 一处)"); + "TC-NETSOCK-420m the registration path costs zero chip transactions (rebuild lives only in net_listener_tick)"); /* 6) 非监听型 CLOSED(客户端)→ 不擅自登记 */ reset_all(); @@ -310,7 +310,7 @@ static void tc_420_accept_shapes(void) s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; s_net_socks[fd].auto_relisten = 0U; CHECK(net_accept(fd, sa, &addrlen) == -1 && g_peers.listener_req_calls == 0, - "TC-NETSOCK-420n 客户端 fd 的 CLOSED 不登记监听重建"); + "TC-NETSOCK-420n CLOSED on a client fd registers no listener rebuild"); reset_all(); } @@ -324,14 +324,14 @@ static void tc_421_accept_public(void) bring_up(); CHECK(net_accept(-1, sa, &addrlen) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-421a 越界 → BADF"); + "TC-NETSOCK-421a out of range -> BADF"); fd = mk_established(NET_SOCK_STREAM); addrlen = 0; CHECK(net_accept(fd, sa, NULL) == fd, - "TC-NETSOCK-421b addrlen 为 NULL 仍可 accept(addr 回填被跳过,不崩)"); + "TC-NETSOCK-421b accept still works with addrlen NULL (address fill skipped, no crash)"); addrlen = (int)sizeof(in); CHECK(net_accept(fd, NULL, &addrlen) == fd, - "TC-NETSOCK-421c addr 为 NULL:只要 fd,不回填地址"); + "TC-NETSOCK-421c addr NULL: returns just the fd and fills no address"); reset_all(); } @@ -343,13 +343,13 @@ static void tc_430_connect(void) bring_up(); CHECK(net_connect(-1, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-430a 越界 → BADF"); + "TC-NETSOCK-430a out of range -> BADF"); fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); CHECK(net_connect(fd, NULL, (int)sizeof(in)) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-430b addr=NULL → INVAL(入队之前就把参数挡下)"); + "TC-NETSOCK-430b addr=NULL -> INVAL (arguments blocked before enqueue)"); CHECK(net_connect(fd, (struct net_sockaddr *)&in, 1) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430c addrlen 不足 → INVAL"); - CHECK(ns_rtos_q_count() == 0, "TC-NETSOCK-430d 参数非法不产生消息(不占 netTask)"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430c addrlen too small -> INVAL"); + CHECK(ns_rtos_q_count() == 0, "TC-NETSOCK-430d invalid arguments create no message (netTask is not occupied)"); /* 成功:地址簿记 + 大端顺位 + 状态 CONNECTING */ reset_all(); @@ -361,21 +361,21 @@ static void tc_430_connect(void) in.sin_addr.s_addr = net_inet_addr("192.168.1.200"); g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, - "TC-NETSOCK-430e connect 命令下发成功(异步,成败由中断裁定)"); + "TC-NETSOCK-430e connect command issued (asynchronous, the interrupt decides success)"); CHECK(s_net_socks[fd].state == NET_SOCK_STATE_CONNECTING, - "TC-NETSOCK-430f 本地状态转 CONNECTING"); + "TC-NETSOCK-430f local state moves to CONNECTING"); CHECK(s_net_socks[fd].remote_port == 8888U, - "TC-NETSOCK-430g 对端端口转主机序存簿记"); + "TC-NETSOCK-430g peer port stored in bookkeeping as host order"); CHECK(g_fx.des_ip[fd][0] == 192U && g_fx.des_ip[fd][1] == 168U && g_fx.des_ip[fd][2] == 1U && g_fx.des_ip[fd][3] == 200U, - "TC-NETSOCK-430h 目的 IP 按大端数组下发(set_des_ip 要的顺位,反了就连不上)"); + "TC-NETSOCK-430h destination IP issued as a big-endian array (the order set_des_ip needs, reversed means no link)"); CHECK(g_chip.set_port_calls == 1 && g_fx.des_port[fd] == 8888U, - "TC-NETSOCK-430i 源端口下发一次(SET_SOUR_PORT),目的端口另走 SET_DES_PORT=8888"); + "TC-NETSOCK-430i source port issued once (SET_SOUR_PORT), the peer port goes via SET_DES_PORT=8888"); CHECK(g_fxchip.sock_events_calls == 1 && g_fxchip.glob_calls == 0, - "TC-NETSOCK-430j 清残留中断只清本 Socket(F-NET-7:原「再读清全局」会焊死别人)"); + "TC-NETSOCK-430j clearing stale interrupts only clears this socket (F-NET-7: the old extra global read welded others shut)"); CHECK(ch395f_fake_log_find('v') < ch395f_fake_log_find('T') || ch395f_fake_log_find('v') < 0, - "TC-NETSOCK-430k 清事件排在 TCP_CONNECT 之前(TRAP-NET-12/-18 的两级预清)"); + "TC-NETSOCK-430k clearing events precedes TCP_CONNECT (the two-stage pre-clear of TRAP-NET-12/-18)"); /* 命令级被拒(非 BUSY):必须回滚,不能停在 CONNECTING 干等 */ reset_all(); @@ -383,13 +383,13 @@ static void tc_430_connect(void) fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); g_fx.connect_rc[fd] = CH395F_ERR_VAL; CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430l 芯片拒命令 → -1"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430l chip refused the command -> -1"); CHECK(g_chip.close_calls == 1, - "TC-NETSOCK-430m 被拒要按手册「再次连接需从 OPEN 重开」关掉 Socket"); + "TC-NETSOCK-430m after a refusal the manual requires reopening from OPEN, so the socket is closed"); CHECK(s_net_socks[fd].session_dead == 1U && s_net_socks[fd].state == NET_SOCK_STATE_CLOSED, - "TC-NETSOCK-430n 命令级失败同样是本会话结束:置 session_dead+CLOSED(B2:否则挂起 recv 干等)"); - CHECK(g_ns_error_count >= 1, "TC-NETSOCK-430o 被拒必留 [ERR]"); + "TC-NETSOCK-430n a command-level failure also ends the session: sets session_dead and CLOSED (B2, else pending recv waits forever)"); + CHECK(g_ns_error_count >= 1, "TC-NETSOCK-430o a refusal must leave an [ERR] line"); /* BUSY 只是命令仍在执行,不得判失败 */ reset_all(); @@ -397,9 +397,9 @@ static void tc_430_connect(void) fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); g_fx.connect_rc[fd] = CH395F_ERR_BUSY; CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == 0, - "TC-NETSOCK-430p BUSY=芯片还在执行 → 维持原行为交给中断裁定(慢芯片不该被误杀)"); + "TC-NETSOCK-430p BUSY means the chip is still executing -> keep the old behaviour and let the interrupt decide (do not kill a slow chip)"); CHECK(s_net_socks[fd].state == NET_SOCK_STATE_CONNECTING && g_chip.close_calls == 0, - "TC-NETSOCK-430q BUSY 不回滚、不关 Socket"); + "TC-NETSOCK-430q BUSY neither rolls back nor closes the socket"); /* open 三次失败 → BUSY,且不发 CONNECT */ reset_all(); @@ -407,21 +407,21 @@ static void tc_430_connect(void) fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); g_fake.open_rc[fd] = CH395F_ERR_USE; CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && - net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-430r open 三次失败 → BUSY"); + net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-430r open failing 3 times -> BUSY"); CHECK(g_chip.open_calls == 3 && g_fxchip.connect_calls == 0, - "TC-NETSOCK-430s open 没成不发 TCP_CONNECT"); + "TC-NETSOCK-430s with no successful open, never issue TCP_CONNECT"); /* 非 TCP 流 / 未在用 → INVAL(netTask 侧再判一次) */ reset_all(); bring_up(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); CHECK(net_connect_locked(fd, (const struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430t UDP 槽位不走 TCP connect"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-430t a UDP slot never takes the TCP connect path"); CHECK(net_connect_locked(7, (const struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-430u 范围内但未在用 → INVAL(in_use 与控制块一起考;BADF 只考越界)"); + "TC-NETSOCK-430u in range but not in use -> INVAL (in_use is checked with the control block; BADF is only for range)"); CHECK(net_connect_locked(NET_MAX_SOCKETS, (const struct net_sockaddr *)&in, - (int)sizeof(in)) == -1, "TC-NETSOCK-430v 越界 → BADF"); + (int)sizeof(in)) == -1, "TC-NETSOCK-430v out of range -> BADF"); reset_all(); } @@ -437,17 +437,17 @@ static void tc_440_udp_datagram(void) bring_up_netctx(); CHECK(net_sendto(-1, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-440a sendto 越界 → BADF"); + "TC-NETSOCK-440a sendto out of range -> BADF"); CHECK(net_sendto(0, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-440b 未在用 → BADF"); + "TC-NETSOCK-440b not in use -> BADF"); CHECK(net_recvfrom(0, buf, sizeof(buf), 0, NULL, NULL) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-440c recvfrom 未在用 → BADF"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-440c recvfrom on an unused fd -> BADF"); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); CHECK(net_sendto(fd, NULL, 5, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-440d sendto 参数非法 → INVAL"); + "TC-NETSOCK-440d sendto invalid arguments -> INVAL"); CHECK(net_recvfrom(fd, NULL, 5, NET_MSG_DONTWAIT, NULL, NULL) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-440e recvfrom buf 为空 → INVAL"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-440e recvfrom with a NULL buf -> INVAL"); /* 带目的地址:先设 DesIP/DesPort 再写数据(不带上限的自旋实现,健康链路一次写完) */ reset_all(); @@ -457,11 +457,11 @@ static void tc_440_udp_datagram(void) in.sin_port = net_htons(8888); in.sin_addr.s_addr = net_inet_addr("192.168.1.9"); CHECK(net_sendto(fd, "hello", 5, 0, sa, (int)sizeof(in)) == 5, - "TC-NETSOCK-440f sendto 成功返回字节数"); + "TC-NETSOCK-440f sendto returns the number of bytes sent"); CHECK(g_fx.des_ip[fd][3] == 9U && g_fx.des_port[fd] == 8888U, - "TC-NETSOCK-440g 目的地址随本次发送下发(UDP 无连接,地址是每包带的)"); + "TC-NETSOCK-440g the destination address travels with this send (UDP is connectionless, address is per packet)"); CHECK(g_chip.write_calls == 1 && s_net_socks[fd].send_bytes == 5U, - "TC-NETSOCK-440h 小包一次写完 + 统计"); + "TC-NETSOCK-440h small packet written in one go plus statistics"); /* 大报分块(仍受 NET_SEND_CHUNK_MAX 上界约束) */ reset_all(); @@ -470,8 +470,8 @@ static void tc_440_udp_datagram(void) g_ns_int_pin_set = 0; ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); ch395f_fake_set_sint_sticky((uint8_t)fd, CH395F_SINT_STAT_SENBUF_FREE); - CHECK(net_sendto(fd, big, 2100, 0, NULL, 0) == 2100, "TC-NETSOCK-440i 2100B 全部交付"); - CHECK(g_chip.write_calls == 3, "TC-NETSOCK-440j UDP 也按 1024 上界分块(写穿 FIFO 的约束同 TCP)"); + CHECK(net_sendto(fd, big, 2100, 0, NULL, 0) == 2100, "TC-NETSOCK-440i all 2100 bytes delivered"); + CHECK(g_chip.write_calls == 3, "TC-NETSOCK-440j UDP also chunks at the 1024 bound (the FIFO overrun constraint matches TCP)"); /* recvfrom:前 8 字节是芯片 UDP 头(长度/srcPort/srcIP),要剥掉再交出去 */ reset_all(); @@ -481,26 +481,26 @@ static void tc_440_udp_datagram(void) addrlen = (int)sizeof(in); (void)memset(&in, 0, sizeof(in)); CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen) == 20, - "TC-NETSOCK-440k 返回 20:报文头 8 字节被剥掉,不把头部当数据交给应用"); + "TC-NETSOCK-440k returns 20: the 8-byte report header was stripped, the header is not handed over as data"); CHECK(addrlen == (int)sizeof(in) && in.sin_family == NET_AF_INET, - "TC-NETSOCK-440l 回填源地址结构与长度"); + "TC-NETSOCK-440l fills the source address structure and length"); /* 无数据 + 非阻塞 → WOULDBLOCK(阻塞形态是自旋无预算,本套刻意不测,见交付说明) */ reset_all(); bring_up_netctx(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && - net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440m 非阻塞无数据 → WOULDBLOCK"); + net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440m non-blocking with no data -> WOULDBLOCK"); s_net_socks[fd].blocking = 0U; CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == -1 && - net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440n 非阻塞属性同判据生效"); + net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440n the non-blocking attribute follows the same decision"); /* 短于 8 字节的残包:不剥头,整包交出去(钉当前实现的边界,不猜"应该怎么算") */ reset_all(); bring_up_netctx(); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); ch395f_fake_push_recv_len((uint8_t)fd, 5U); CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == 5, - "TC-NETSOCK-440o recv_len<8 时不剥头,整包返回(当前实现)"); + "TC-NETSOCK-440o with recv_len<8 no header is stripped, the whole packet is returned (current implementation)"); reset_all(); } @@ -509,11 +509,11 @@ static void tc_450_direct_entries(void) { bring_up(); CHECK(net_send_sock(NULL, "x", 1) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-450a net_send_sock(NULL) → BADF"); + "TC-NETSOCK-450a net_send_sock(NULL) -> BADF"); CHECK(net_recv_sock(NULL, (void *)"x", 1, 0) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-450b net_recv_sock(NULL) → BADF"); + "TC-NETSOCK-450b net_recv_sock(NULL) -> BADF"); CHECK(net_close_sock(NULL) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-450c net_close_sock(NULL) → BADF"); + "TC-NETSOCK-450c net_close_sock(NULL) -> BADF"); /* netTask 内直调 send:挂起登记会被"执行者自等待"守卫拒 → 返回 -1(BUSY) */ reset_all(); @@ -525,9 +525,9 @@ static void tc_450_direct_entries(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; g_peers.send_register_rc = -1; /* 复刻守卫:netTask 自己发的必被拒 */ CHECK(net_send_sock(&s_net_socks[fd], "hello", 5) == -1, - "TC-NETSOCK-450d netTask 内等不到缓冲时不登记挂起(没人能替它完成)→ -1"); + "TC-NETSOCK-450d inside netTask a stalled buffer never registers pending (nobody can complete it) -> -1"); CHECK(g_peers.send_reg_calls == 1, - "TC-NETSOCK-450e 确实走到了登记调用(被拒的是登记侧,不是本层没试)"); + "TC-NETSOCK-450e the registration call really happened (the refusal came from the register side, not from us not trying)"); } reset_all(); } @@ -545,13 +545,13 @@ static void tc_460_reset_sync_timeout_bound(void) t_before = g_ns_tick; CHECK(net_close(fd) == 0, - "TC-NETSOCK-460a 芯片关不掉也照样放行 close(fd 侧簿记必须继续收口)"); + "TC-NETSOCK-460a even when the chip cannot be closed, close proceeds (fd-side bookkeeping must still close out)"); CHECK((g_ns_tick - t_before) <= 700U, - "TC-NETSOCK-460b 复位等待有 600ms 硬上限(无上限=netTask 被一颗坏芯片拖死)"); + "TC-NETSOCK-460b the reset wait has a hard 600ms ceiling (without one a bad chip drags netTask down)"); CHECK(g_ns_error_count >= 1, - "TC-NETSOCK-460c 复位超时必 [ERR]:这条 [ERR] 是唯一能发现它的信号"); + "TC-NETSOCK-460c a reset timeout must leave [ERR]: that line is the only way to ever notice it"); CHECK(s_net_socks[fd].in_use == 0U, - "TC-NETSOCK-460d 超时也照常释放槽位(不因芯片状态卡住本地 fd 表)"); + "TC-NETSOCK-460d the slot is released even on timeout (a chip state must not jam the local fd table)"); reset_all(); /* drop_conn 同一条超时线 */ @@ -563,7 +563,7 @@ static void tc_460_reset_sync_timeout_bound(void) t_before = g_ns_tick; CHECK(net_drop_conn(fd) == 0 && (g_ns_tick - t_before) <= 700U && g_ns_error_count >= 1, - "TC-NETSOCK-460e drop_conn 也走同一条有界收口并留痕"); + "TC-NETSOCK-460e drop_conn takes the same bounded close-out and logs it too"); reset_all(); } @@ -586,11 +586,11 @@ static void tc_470_send_disconnect_midway(void) rc = net_send(fd, big, 2100, 0); CHECK(rc >= 0 && rc < 2100, - "TC-NETSOCK-470a 断线只截断剩余量:按已发数返回(短写),不是 -1 也不是谎报 2100"); + "TC-NETSOCK-470a a mid-send disconnect only truncates: returns bytes already sent (a short write), not -1 and not a false 2100"); CHECK((int)s_net_socks[fd].send_bytes == rc, - "TC-NETSOCK-470b 字节统计与实际交付一致(对端已收到的不能回收)"); + "TC-NETSOCK-470b byte statistic matches what was actually delivered (what the peer got cannot be clawed back)"); CHECK(g_ns_error_count >= 1, - "TC-NETSOCK-470c 短写必 [ERR](静默截断曾让 UI 推流被对端误判为全程无字节)"); + "TC-NETSOCK-470c a short write must log [ERR] (a silent truncation once made the peer see zero bytes from a UI push)"); reset_all(); } diff --git a/test/host/net_socket/ns_s6_edges.c b/test/host/net_socket/ns_s6_edges.c index 4f8f86ea..81d92c72 100644 --- a/test/host/net_socket/ns_s6_edges.c +++ b/test/host/net_socket/ns_s6_edges.c @@ -23,25 +23,25 @@ static void tc_500_public_bounds(void) addrlen = (int)sizeof(in); CHECK(net_send(NET_MAX_SOCKETS, buf, 4, 0) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-500a net_send 越界 → BADF"); + "TC-NETSOCK-500a net_send out of range -> BADF"); CHECK(net_recv(NET_MAX_SOCKETS, buf, 4, NET_MSG_DONTWAIT) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500b net_recv 越界 → BADF"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500b net_recv out of range -> BADF"); CHECK(net_recv_to(NET_MAX_SOCKETS, buf, 4, 10U) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500c net_recv_to 越界 → BADF"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500c net_recv_to out of range -> BADF"); CHECK(net_close(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-500d net_close 越界 → BADF"); + "TC-NETSOCK-500d net_close out of range -> BADF"); CHECK(net_drop_conn(NET_MAX_SOCKETS) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-500e net_drop_conn 越界 → BADF"); + "TC-NETSOCK-500e net_drop_conn out of range -> BADF"); CHECK(net_accept(NET_MAX_SOCKETS, (struct net_sockaddr *)&in, &addrlen) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500f net_accept 越界 → BADF"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500f net_accept out of range -> BADF"); CHECK(net_connect(NET_MAX_SOCKETS, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500g net_connect 越界 → BADF"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500g net_connect out of range -> BADF"); CHECK(net_sendto(NET_MAX_SOCKETS, buf, 4, 0, NULL, 0) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500h net_sendto 越界 → BADF"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500h net_sendto out of range -> BADF"); CHECK(net_recvfrom(NET_MAX_SOCKETS, buf, 4, NET_MSG_DONTWAIT, NULL, NULL) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500i net_recvfrom 越界 → BADF"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-500i net_recvfrom out of range -> BADF"); CHECK(g_ns_put_calls == 0, - "TC-NETSOCK-500j 全部越界形态零入队(入队之后才挡 = 让 netTask 处理一个没主的请求)"); + "TC-NETSOCK-500j every out-of-range shape enqueues nothing (blocking after the enqueue hands netTask an ownerless request)"); reset_all(); } @@ -57,21 +57,21 @@ static void tc_501_all_entries_busy_on_full_queue(void) ns_rtos_q_fill((int)NS_Q_CAP); CHECK(net_accept(fd, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-501a net_accept 队满 → BUSY"); + "TC-NETSOCK-501a net_accept on a full queue -> BUSY"); ns_rtos_q_fill((int)NS_Q_CAP); (void)memset(&in, 0, sizeof(in)); CHECK(net_connect(fd, (struct net_sockaddr *)&in, (int)sizeof(in)) == -1 && - net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501b net_connect 队满 → BUSY"); + net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501b net_connect on a full queue -> BUSY"); ns_rtos_q_fill((int)NS_Q_CAP); CHECK(net_recv_to(fd, (void *)"x", 1, 20U) == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-501c net_recv_to 队满 → BUSY"); + "TC-NETSOCK-501c net_recv_to on a full queue -> BUSY"); ns_rtos_q_fill((int)NS_Q_CAP); CHECK(net_recv(fd, (void *)"x", 1, NET_MSG_DONTWAIT) == -1 && - net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501d net_recv 队满 → BUSY"); + net_get_errno() == NET_ERR_BUSY, "TC-NETSOCK-501d net_recv on a full queue -> BUSY"); ns_rtos_q_fill((int)NS_Q_CAP); CHECK(net_send(fd, "x", 1, 0) == -1 && net_get_errno() == NET_ERR_BUSY, - "TC-NETSOCK-501e net_send 队满 → BUSY"); - CHECK(g_ns_notify_calls == 0, "TC-NETSOCK-501f 队满路径零通知(调用方直接拿返回值走人)"); + "TC-NETSOCK-501e net_send on a full queue -> BUSY"); + CHECK(g_ns_notify_calls == 0, "TC-NETSOCK-501f the full-queue path sends no notification (the caller leaves with the return value)"); reset_all(); } @@ -88,12 +88,12 @@ static void tc_502_socket_table_full(void) break; } } - CHECK(i == NET_MAX_SOCKETS, "TC-NETSOCK-502a 8 个槽位全部可分配"); + CHECK(i == NET_MAX_SOCKETS, "TC-NETSOCK-502a all 8 slots are allocatable"); CHECK(net_socket(NET_AF_INET, NET_SOCK_STREAM) == -1 && - net_get_errno() == NET_ERR_NOMEM, "TC-NETSOCK-502b 第 9 个 → NOMEM(不越界写表)"); + net_get_errno() == NET_ERR_NOMEM, "TC-NETSOCK-502b the ninth -> NOMEM (no out-of-range table write)"); s_net_socks[3].state = NET_SOCK_STATE_ESTABLISHED; CHECK(net_get_sock(3) != NULL && net_listener_armed(3) == 1, - "TC-NETSOCK-502c 表满不影响已在用会话(alloc 失败只回 NOMEM,不动别人的槽位)"); + "TC-NETSOCK-502c a full table does not disturb live sessions (alloc failure only returns NOMEM)"); reset_all(); } @@ -113,10 +113,10 @@ static void tc_503_zero_wait_pickup(void) rc = net_send(fd, "abcd", 4, 0); - CHECK(rc == 4, "TC-NETSOCK-503a 零等待就拾到通知值 → 直接返回结果"); - CHECK(g_ns_block_waits == 0, "TC-NETSOCK-503b 已有通知时绝不进阻塞分支"); + CHECK(rc == 4, "TC-NETSOCK-503a the zero-wait peek takes the notify value -> returns the result immediately"); + CHECK(g_ns_block_waits == 0, "TC-NETSOCK-503b with a notification already pending it never enters the blocking branch"); CHECK(g_peers.send_reg_calls == 0 && g_chip.write_calls == 0, - "TC-NETSOCK-503c 本次调用没有真的写芯片(拾到的是上一条消息的回执——这就是 实测踩过的形态)"); + "TC-NETSOCK-503c this call never touched the chip (the value peeked was the previous receipt, a shape we already hit in practice)"); reset_all(); } @@ -133,16 +133,16 @@ static void tc_504_locked_entry_branches(void) fd = net_socket(NET_AF_INET, NET_SOCK_STREAM); CHECK(net_connect_locked(fd, NULL, (int)sizeof(in)) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504a connect_locked addr=NULL → INVAL"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504a connect_locked with addr=NULL -> INVAL"); CHECK(net_connect_locked(fd, (const struct net_sockaddr *)&in, 1) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504b connect_locked addrlen 不足 → INVAL"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504b connect_locked with a short addrlen -> INVAL"); s_net_socks[fd].in_use = 1U; CHECK(net_send_locked(&s_net_socks[fd], "x", 1, NULL) == -1 && - net_get_errno() == NET_ERR_NOTCONN, "TC-NETSOCK-504c 未建立的直调 send → NOTCONN"); + net_get_errno() == NET_ERR_NOTCONN, "TC-NETSOCK-504c direct send on a not-established block -> NOTCONN"); CHECK(net_recv_locked(&s_net_socks[fd], (void *)"x", 1, NET_MSG_DONTWAIT, 0U, NULL) == -1 && - net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-504d 直调 recv 无数据非阻塞 → WOULDBLOCK"); + net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-504d direct recv, no data, non-blocking -> WOULDBLOCK"); CHECK(net_recv_locked(&s_net_socks[fd], NULL, 1, NET_MSG_DONTWAIT, 0U, NULL) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504e 直调 recv buf 空 → INVAL"); + net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-504e direct recv with a NULL buf -> INVAL"); reset_all(); } @@ -162,9 +162,9 @@ static void tc_505_inner_wait_and_last_chunk(void) rc = net_send_locked(&s_net_socks[fd], big, 2100, (TaskHandle_t)&g_ns_task_caller); CHECK(rc == NET_PENDING_REGISTERED, - "TC-NETSOCK-505a 内层等待(写完一块后等发完)超预算也转挂起,不留 in-place 忙等"); + "TC-NETSOCK-505a the inner wait (after writing one chunk) also converts to pending past budget, no in-place busy wait"); CHECK(g_chip.write_calls == 2 && g_peers.last_snd_sent == 2048, - "TC-NETSOCK-505b 登记的进度=已写进芯片的字节数(1024+1024=2048),已发的绝不重发"); + "TC-NETSOCK-505b registered progress = bytes written into the chip (1024+1024=2048), delivered bytes are never resent"); reset_all(); /* 末块:全部写完但芯片不回执 → 按完成返回,**不许**登记挂起 @@ -173,9 +173,9 @@ static void tc_505_inner_wait_and_last_chunk(void) fd = mk_established(NET_SOCK_STREAM); rc = net_send_locked(&s_net_socks[fd], big, 1024, (TaskHandle_t)&g_ns_task_caller); - CHECK(rc == 1024, "TC-NETSOCK-505c 单块恰好写完 → 返回 1024(完成)"); + CHECK(rc == 1024, "TC-NETSOCK-505c a single chunk finishing exactly -> returns 1024 (complete)"); CHECK(g_peers.send_reg_calls == 0, - "TC-NETSOCK-505d 末块等待不得登记挂起(sent>=len 的守卫必须挡住)"); + "TC-NETSOCK-505d the last-chunk wait must not register pending (the sent>=len guard has to hold)"); reset_all(); } @@ -195,9 +195,9 @@ static void tc_506_sendto_stall_and_short(void) g_ns_poll_tick_inc = 500U; rc = net_sendto(fd, big, 2100, 0, NULL, 0); CHECK(rc < 2100 && rc >= 0, - "TC-NETSOCK-506a sendto 停滞只拿到部分:按已发数返回(短写语义与 TCP 一致)"); + "TC-NETSOCK-506a sendto stalled returns only what went out (short-write semantics match TCP)"); CHECK(g_ns_error_count >= 2, - "TC-NETSOCK-506b 停滞 [ERR] + 短写 [ERR] 都要留(缺一就退化成静默截断)"); + "TC-NETSOCK-506b both the stall [ERR] and the short-write [ERR] must be there (missing one degrades into a silent truncation)"); reset_all(); /* 写完一块后等发完的超时出口 */ @@ -209,7 +209,7 @@ static void tc_506_sendto_stall_and_short(void) g_ns_poll_tick_inc = 500U; rc = net_sendto(fd, big, 2100, 0, NULL, 0); CHECK(rc >= 1024 && rc < 2100, - "TC-NETSOCK-506c 第一块已交付、第二块等发完超时 → 返回 1024..2099 之间的实际值"); + "TC-NETSOCK-506c first chunk delivered and the second chunk wait timed out -> a value between 1024 and 2099"); reset_all(); } @@ -231,11 +231,11 @@ static void tc_530_stall_timeout_before_spin(void) rc = net_send_locked(&s_net_socks[fd], buf, (int)sizeof(buf), (TaskHandle_t)&g_ns_task_caller); CHECK(rc == 0, - "TC-NETSOCK-530a 停滞超时按已发数 0 返回(短写形态,不是 -1)"); + "TC-NETSOCK-530a the stall timeout returns the bytes sent so far, 0 (a short write, not -1)"); CHECK(g_peers.send_reg_calls == 0, - "TC-NETSOCK-530b 停滞超时优先:不进挂起登记(判定顺序被换掉的话这里会登记)"); + "TC-NETSOCK-530b the stall timeout wins over the spin budget: no pending registration (a swapped order would register)"); CHECK(net_get_errno() == NET_ERR_TIMEDOUT, - "TC-NETSOCK-530c errno = TIMEDOUT(供调用方区分停滞与断线)"); + "TC-NETSOCK-530c errno = TIMEDOUT (lets the caller tell a stall from a disconnect)"); reset_all(); } @@ -255,10 +255,10 @@ static void tc_535_inner_timeout_after_chunk(void) rc = net_send_locked(&s_net_socks[fd], big, 2100, (TaskHandle_t)&g_ns_task_caller); - CHECK(rc == 1024, "TC-NETSOCK-535a 已交付的第一块照样交出(1024),剩余按短写处理"); + CHECK(rc == 1024, "TC-NETSOCK-535a the already delivered first chunk still counts (1024), the rest is a short write"); CHECK(g_peers.send_reg_calls == 0, - "TC-NETSOCK-535b 停滞超时收口在登记挂起**之前**(换序就会登记一条永远完不成的挂起)"); - CHECK(g_chip.write_calls == 1, "TC-NETSOCK-535c 只写了一次芯片:不得把已交付的块重发"); + "TC-NETSOCK-535b the stall timeout closes out BEFORE any pending registration (swapping the order parks a pending that never completes)"); + CHECK(g_chip.write_calls == 1, "TC-NETSOCK-535c the chip was written exactly once: a delivered chunk must never be resent"); reset_all(); } @@ -275,7 +275,7 @@ static void tc_536_recv_to_three_exits(void) ch395f_fake_push_recv_len((uint8_t)fd, 12U); g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; CHECK(net_recv_to(fd, buf, (int)sizeof(buf), 100U) == 12, - "TC-NETSOCK-536a 有数据 → 返回实际字节数(走 >=0 支)"); + "TC-NETSOCK-536a data available -> returns the actual byte count (the >=0 branch)"); /* <0 且非 TIMEDOUT:分派侧 BADF → 恢复 errno 后返回 -1 */ reset_all(); @@ -284,7 +284,7 @@ static void tc_536_recv_to_three_exits(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; CHECK(net_recv_to(1, buf, (int)sizeof(buf), 100U) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-536b 真错误 → -1 且 errno 从通知值恢复(与 -2 严格区分)"); + "TC-NETSOCK-536b a real error -> -1 with errno restored from the notify value (strictly distinct from -2)"); /* ==TIMEDOUT:已在 TC-NETSOCK-224 钉过 -2;这里补一条「-2 不改写 errno」 */ reset_all(); @@ -295,7 +295,7 @@ static void tc_536_recv_to_three_exits(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; CHECK(net_recv_to(fd, buf, (int)sizeof(buf), 30U) == -2 && net_get_errno() == NET_OK, - "TC-NETSOCK-536c 超时只凭返回码判定,**不改写**错误码槽(本分支刻意不设 errno)"); + "TC-NETSOCK-536c timeout is judged by the return code only and does NOT rewrite the errno slot (this branch sets none)"); reset_all(); } @@ -313,7 +313,7 @@ static void tc_537_event_callback_path(void) g_ns_int_pin_set = 0; ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); g_fake.int_status[fd] = CH395F_SINT_STAT_SEND_OK; - CHECK(net_poll() == 1, "TC-NETSOCK-537 未注册回调也照常计事件(不因空指针跳过分发)"); + CHECK(net_poll() == 1, "TC-NETSOCK-537 events are counted even with no callback registered (a null pointer never skips dispatch)"); reset_all(); } @@ -330,9 +330,9 @@ static void tc_540_negative_results(void) fd = 3; /* 范围内、未分配 */ g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; CHECK(net_close(fd) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-540a close 未分配的 fd → -1 且 errno 从通知值恢复成 BADF(不是笼统 NET_ERR)"); + "TC-NETSOCK-540a close on an unallocated fd -> -1 with errno restored to BADF from the notify value, not a generic NET_ERR"); CHECK(net_drop_conn(fd) == -1 && net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-540a2 drop_conn 同一条负值恢复支"); + "TC-NETSOCK-540a2 drop_conn takes the same negative-value restoration branch"); reset_all(); /* drop_conn 同理:用未分配 fd 走分派侧 BADF 后再由调用方恢复 */ @@ -344,7 +344,7 @@ static void tc_540_negative_results(void) (void)ns_rtos_q_put_raw(&msg); net_process_messages(); CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, - "TC-NETSOCK-540b drop_conn 分派侧 BADF 经通知值回传"); + "TC-NETSOCK-540b drop_conn BADF from the dispatch side travels back on the notify value"); /* RECV 的 BADF 支(分派侧 net_get_sock 判空) */ reset_all(); @@ -358,7 +358,7 @@ static void tc_540_negative_results(void) (void)ns_rtos_q_put_raw(&msg); net_process_messages(); CHECK(g_ns_notify_last_val == (uint32_t)NET_ERR_BADF, - "TC-NETSOCK-540c recv 分派侧 BADF 同样经通知值回传"); + "TC-NETSOCK-540c recv BADF from the dispatch side likewise travels back on the notify value"); reset_all(); } @@ -370,10 +370,10 @@ static void tc_545_local_entries_without_queue(void) reset_all(); CHECK(net_getsockname(0, (struct net_sockaddr *)&in, &addrlen) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-545a 未初始化也能安全报 BADF(不崩)"); + net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-545a even uninitialised it safely reports BADF (no crash)"); CHECK(net_getpeername(0, (struct net_sockaddr *)&in, &addrlen) == -1, - "TC-NETSOCK-545b getpeername 同上"); - CHECK(net_inet_addr("1.2.3.4") != 0U, "TC-NETSOCK-545c 纯函数不依赖初始化"); + "TC-NETSOCK-545b getpeername behaves the same"); + CHECK(net_inet_addr("1.2.3.4") != 0U, "TC-NETSOCK-545c pure helpers do not depend on initialisation"); reset_all(); } diff --git a/test/host/prot_set_store/prot_set_store_test.c b/test/host/prot_set_store/prot_set_store_test.c index 5996b507..fe1990bb 100644 --- a/test/host/prot_set_store/prot_set_store_test.c +++ b/test/host/prot_set_store/prot_set_store_test.c @@ -217,14 +217,14 @@ static void tc_load_triage(void) backend_reset(); g_read_rc = -1; rc = prot_set_store_load(); - CHECK(rc == 1, "TC-PROTSTOR-001 读失败归 1(按无档处理)"); + CHECK(rc == 1, "TC-PROTSTOR-001 read failure maps to 1 (treat as no file)"); backend_reset(); len = make_blob(blob, sizeof(blob)); memcpy(g_img, blob, len); stage_blob(len); rc = prot_set_store_load(); - CHECK(rc == 0, "TC-PROTSTOR-002 有效 blob 装载成功"); + CHECK(rc == 0, "TC-PROTSTOR-002 valid blob loads successfully"); /* ver 换代:magic 完好、ver 与当前固件不同 → 预期升级路径,归 2 */ backend_reset(); @@ -233,9 +233,9 @@ static void tc_load_triage(void) g_img[4] = (uint8_t)(PROT_SET_BLOB_VER + 1u); stage_blob(len); rc = prot_set_store_load(); - CHECK(rc == 2, "TC-PROTSTOR-003 ver 换代归 2(不是 -1)"); + CHECK(rc == 2, "TC-PROTSTOR-003 ver mismatch maps to 2 (not -1)"); CHECK((g_pstore_info_count == 1) && (g_pstore_error_count == 0), - "TC-PROTSTOR-004 换代只打 INFO,不误报 ERROR"); + "TC-PROTSTOR-004 version mismatch logs INFO only, no false ERROR"); /* cnt 换代:B8 定值表 32→40 项就是这个形态 */ backend_reset(); @@ -244,7 +244,7 @@ static void tc_load_triage(void) g_img[6] = (uint8_t)((uint32_t)PS_ID_COUNT + 1u); stage_blob(len); rc = prot_set_store_load(); - CHECK(rc == 2, "TC-PROTSTOR-005 cnt 换代归 2"); + CHECK(rc == 2, "TC-PROTSTOR-005 cnt mismatch maps to 2"); /* magic 坏 = 真损坏 */ backend_reset(); @@ -253,7 +253,7 @@ static void tc_load_triage(void) g_img[0] = (uint8_t)(g_img[0] ^ 0xFFu); stage_blob(len); rc = prot_set_store_load(); - CHECK((rc == -1) && (g_pstore_error_count == 0), "TC-PROTSTOR-006 magic 坏归 -1"); + CHECK((rc == -1) && (g_pstore_error_count == 0), "TC-PROTSTOR-006 broken magic maps to -1"); /* magic/ver/cnt 全好,但数据被改 → 校验不过 = 损坏 */ backend_reset(); @@ -262,7 +262,7 @@ static void tc_load_triage(void) g_img[PROT_SET_BLOB_HDR_SZ] = (uint8_t)(g_img[PROT_SET_BLOB_HDR_SZ] ^ 0x5Au); stage_blob(len); rc = prot_set_store_load(); - CHECK(rc == -1, "TC-PROTSTOR-007 数据被改(校验失败)归 -1"); + CHECK(rc == -1, "TC-PROTSTOR-007 data tampered (checksum fail) maps to -1"); /* 截断到不足头部:不得误判为换代 */ backend_reset(); @@ -270,7 +270,7 @@ static void tc_load_triage(void) memcpy(g_img, blob, len); stage_blob(PROT_SET_BLOB_HDR_SZ - 1u); rc = prot_set_store_load(); - CHECK(rc == -1, "TC-PROTSTOR-008 长度不足头部归 -1 且不误判换代"); + CHECK(rc == -1, "TC-PROTSTOR-008 length below header maps to -1 and is not misjudged as version mismatch"); backend_reset(); len = make_blob(blob, sizeof(blob)); @@ -278,7 +278,7 @@ static void tc_load_triage(void) stage_blob(len); rc = prot_set_store_load(); CHECK((rc == 0) && (g_pstore_info_count == 0), - "TC-PROTSTOR-009 成功路径不打 INFO(version change 措辞不许乱出现)"); + "TC-PROTSTOR-009 success path logs no INFO (version change wording must not appear)"); } /* ===================== 后端调用契约 ===================== */ @@ -295,9 +295,9 @@ static void tc_backend_contract(void) stage_blob(len); (void)prot_set_store_load(); CHECK(strcmp(g_read_path, "/prot_set.bin") == 0, - "TC-PROTSTOR-010 路径 = /prot_set.bin(8.3 合规)"); + "TC-PROTSTOR-010 path = /prot_set.bin (8.3 compliant)"); CHECK(g_read_cap == (size_t)PROT_SET_BLOB_SZ, - "TC-PROTSTOR-011 读取容量 = PROT_SET_BLOB_SZ(静态缓冲,不压栈)"); + "TC-PROTSTOR-011 read capacity = PROT_SET_BLOB_SZ (static buffer, not on stack)"); } /* ===================== save 与脏标志 ===================== */ @@ -311,30 +311,30 @@ static void tc_save_dirty(void) backend_reset(); dirty_clear(); rc = prot_set_store_save(); - CHECK((rc == 0) && (g_write_calls == 0), "TC-PROTSTOR-012 无脏不写盘"); + CHECK((rc == 0) && (g_write_calls == 0), "TC-PROTSTOR-012 not dirty -> no flash write"); prot_set_dirty_mark(); g_write_rc = 0; rc = prot_set_store_save(); CHECK((rc == 0) && (g_write_calls == 1) && (strcmp(g_write_path, "/prot_set.bin") == 0) && (g_write_len > 0u), - "TC-PROTSTOR-013 有脏且写成功:路径与长度正确"); + "TC-PROTSTOR-013 dirty + write ok: path and length correct"); rc = prot_set_store_save(); CHECK((rc == 0) && (g_write_calls == 1), - "TC-PROTSTOR-014 脏已消费,紧接着再调不再刷 Flash"); + "TC-PROTSTOR-014 dirty consumed, immediate second call does not flush flash again"); prot_set_dirty_mark(); g_write_rc = -1; rc = prot_set_store_save(); - CHECK((rc == -1) && (g_write_calls == 2), "TC-PROTSTOR-015 写失败返回 -1"); + CHECK((rc == -1) && (g_write_calls == 2), "TC-PROTSTOR-015 write failure returns -1"); /* 关键:失败必须把脏标志还回去,否则这一轮定值永久丢失 */ g_write_rc = 0; g_write_calls = 0; rc = prot_set_store_save(); CHECK((rc == 0) && (g_write_calls == 1), - "TC-PROTSTOR-016 上一轮写失败后下一轮仍真的再写(脏已回置)"); + "TC-PROTSTOR-016 after a failed round the next round really writes again (dirty re-armed)"); dirty_clear(); } diff --git a/test/host/sha256/sha256_test.c b/test/host/sha256/sha256_test.c index 590c43bf..d28a64bd 100644 --- a/test/host/sha256/sha256_test.c +++ b/test/host/sha256/sha256_test.c @@ -73,15 +73,15 @@ static void tc_vectors(void) sha256_calc("", 0u, d); CHECK(hex_eq_vector(d, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), - "TC-SHA256-001 空串向量"); + "TC-SHA256-001 empty-string vector"); sha256_calc("abc", 3u, d); CHECK(hex_eq_vector(d, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"), - "TC-SHA256-002 \"abc\" 向量"); + "TC-SHA256-002 \"abc\" vector"); sha256_calc("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56u, d); CHECK(hex_eq_vector(d, "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"), - "TC-SHA256-003 56 字节向量(补位跨分组)"); + "TC-SHA256-003 56-byte vector (padding crosses block)"); { sha256_ctx_t ctx; @@ -93,7 +93,7 @@ static void tc_vectors(void) sha256_final(&ctx, d); } CHECK(hex_eq_vector(d, "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"), - "TC-SHA256-004 1e6 × 'a' 向量(流式)"); + "TC-SHA256-004 1e6 x 'a' vector (streaming)"); } /* ===================== TC-SHA256-005:流式等价 ===================== */ @@ -111,7 +111,7 @@ static void tc_stream_equiv(void) sha256_update(&ctx, &msg[i], 1u); } sha256_final(&ctx, d2); - CHECK(memcmp(d1, d2, sizeof(d1)) == 0, "TC-SHA256-005 逐字节喂入与一次性等价"); + CHECK(memcmp(d1, d2, sizeof(d1)) == 0, "TC-SHA256-005 byte-by-byte feed equals one-shot"); } /* ===================== TC-SHA256-006..007:hex_eq ===================== */ @@ -131,11 +131,11 @@ static void tc_hex_eq(void) } upper[64] = '\0'; - CHECK(sha256_hex_eq(d, lower) == 1, "TC-SHA256-006a 小写 hex 匹配"); - CHECK(sha256_hex_eq(d, upper) == 1, "TC-SHA256-006b 大写 hex 匹配(大小写不敏感)"); + CHECK(sha256_hex_eq(d, lower) == 1, "TC-SHA256-006a lowercase hex matches"); + CHECK(sha256_hex_eq(d, upper) == 1, "TC-SHA256-006b uppercase hex matches (case-insensitive)"); lower[63] = (char)((lower[63] == '0') ? '1' : '0'); - CHECK(sha256_hex_eq(d, lower) == 0, "TC-SHA256-007a 内容不等 → 0"); + CHECK(sha256_hex_eq(d, lower) == 0, "TC-SHA256-007a content mismatch -> 0"); lower[63] = (char)((lower[63] == '0') ? '1' : '0'); /* 还原 */ { @@ -144,19 +144,19 @@ static void tc_hex_eq(void) memcpy(bad, lower, 64); bad[64] = '\0'; bad[10] = 'g'; - CHECK(sha256_hex_eq(d, bad) == 0, "TC-SHA256-007b 非法 hex 字符 → 0"); + CHECK(sha256_hex_eq(d, bad) == 0, "TC-SHA256-007b invalid hex char -> 0"); } memcpy(shortv, lower, 63); shortv[63] = '\0'; - CHECK(sha256_hex_eq(d, shortv) == 0, "TC-SHA256-007c 长度 63 → 0"); + CHECK(sha256_hex_eq(d, shortv) == 0, "TC-SHA256-007c length 63 -> 0"); memcpy(longv, lower, 64); longv[64] = 'a'; longv[65] = '\0'; - CHECK(sha256_hex_eq(d, longv) == 0, "TC-SHA256-007d 长度 65 → 0"); + CHECK(sha256_hex_eq(d, longv) == 0, "TC-SHA256-007d length 65 -> 0"); - CHECK(sha256_hex_eq(d, NULL) == 0, "TC-SHA256-007e NULL → 0"); + CHECK(sha256_hex_eq(d, NULL) == 0, "TC-SHA256-007e NULL -> 0"); } /* ===================== TC-SHA256-008:默认口令哈希钉值 ===================== */ @@ -168,7 +168,7 @@ static void tc_default_hash(void) /* 与 ftp_cfg.h FTP_CFG_DEF_HASH 同源钉值(两处若漂移本测试立刻红) */ sha256_calc("adminadmin", 10u, d); CHECK(sha256_hex_eq(d, "d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892") == 1, - "TC-SHA256-008 SHA256(\"adminadmin\") = 出厂默认 pwd_hash"); + "TC-SHA256-008 SHA256(\"adminadmin\") = factory default pwd_hash"); } /* ===================== main ===================== */ diff --git a/test/host/soe_store/soe_store_test.c b/test/host/soe_store/soe_store_test.c index 780c5714..d204d794 100644 --- a/test/host/soe_store/soe_store_test.c +++ b/test/host/soe_store/soe_store_test.c @@ -365,7 +365,7 @@ static void tc_post_side(void) uint32_t i; int ok; - printf("-- TC-SOESTORE-001..007 post 侧(零 I/O 契约)--\n"); + printf("-- TC-SOESTORE-001..007 post side (zero-IO contract) --\n"); /* 001:post 3 条只写环,绝不碰盘、绝不碰卷锁、绝不打日志 */ reset_all(); @@ -379,7 +379,7 @@ static void tc_post_side(void) } } CHECK(ok && (s_lock_waits == 0) && (g_soe_err_count == 0) && (g_soe_info_count == 0), - "TC-SOESTORE-001 post 零 FatFs / 零卷锁 / 零日志(protectTask 栈预算)"); + "TC-SOESTORE-001 post does zero FatFs / zero volume lock / zero logging (protectTask stack budget)"); /* 002:墙钟未建立 -> IV + epoch 字段 0(不造假时标) */ reset_all(); @@ -389,7 +389,7 @@ static void tc_post_side(void) ok = read_rec(0u, &out); CHECK(ok && (out.flags & SOE_FLG_IV) != 0u && (out.epoch_s == 0u) && (out.cp56[2] & 0x80u) != 0u && (out.value == 0xF0u), - "TC-SOESTORE-002 无墙钟 -> flags.IV + epoch=0 + CP56 IV 位(B9 口径)"); + "TC-SOESTORE-002 no wall clock -> flags.IV + epoch=0 + CP56 IV bit (B9 rule)"); /* 003:seq 单调;丢弃的那条也占号(真机丢号 = 主机看到的号不连续是**如实**) */ reset_all(); @@ -397,7 +397,7 @@ static void tc_post_side(void) soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i); } ok = (s_seq == 9u) && (s_drop_cnt == 1u); - CHECK(ok, "TC-SOESTORE-003 seq 单调递增,丢弃也占号(号不跳才是假象)"); + CHECK(ok, "TC-SOESTORE-003 seq increases monotonically, dropped entries still consume a number"); /* 004:环满丢弃不阻塞、不崩、**不打日志**(留痕归 drain 侧) */ reset_all(); @@ -406,7 +406,7 @@ static void tc_post_side(void) } CHECK((s_ring_head == 8u) && (s_drop_cnt == 1u) && (g_soe_err_count == 0) && (g_soe_info_count == 0), - "TC-SOESTORE-004 环满丢弃:只计数,post 侧零 DBG(栈尾链 +320B 教训)"); + "TC-SOESTORE-004 ring-full drop: count only, zero DBG on post side (lesson: +320B tail chain)"); /* 005:内核运行 + 队列已建 -> 走队列,环不再写 */ reset_all(); @@ -417,7 +417,7 @@ static void tc_post_side(void) s_ring_tail = 0u; soe_store_post((uint8_t)SOE_T_ELM_RETURN, 1u, 2u); CHECK((s_ring_head == 0u) && (s_q_cnt == 1u) && (s_q_msgsz == SOE_REC_SIZE), - "TC-SOESTORE-005 队列已建 + 内核运行 -> 条目进队列、环不再写"); + "TC-SOESTORE-005 queue created + kernel running -> entries go to the queue, ring not written"); /* 006:队列满 -> 丢弃计数(与环满同一条可观测出口) */ reset_all(); @@ -429,14 +429,14 @@ static void tc_post_side(void) soe_store_post((uint8_t)SOE_T_ELM_ASSERT, (uint8_t)i, i); } CHECK((s_q_cnt == SOE_QUEUE_MAX) && (s_drop_cnt == 2u), - "TC-SOESTORE-006 队列满 -> 丢弃并计数(soe_store_dropped 同一出口)"); + "TC-SOESTORE-006 queue full -> drop and count through the same soe_store_dropped exit"); /* 007:计数饱和不回绕 */ reset_all(); s_drop_cnt = UINT32_MAX; soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 1u, 1u); CHECK((s_drop_cnt == UINT32_MAX) && (soe_store_dropped() == UINT32_MAX), - "TC-SOESTORE-007 丢弃计数饱和:UINT32_MAX 不回绕"); + "TC-SOESTORE-007 drop counter saturates at UINT32_MAX, never wraps"); } /* ===================== TC-SOESTORE-010..021:drain 落盘与故障注入 ===================== */ @@ -449,7 +449,7 @@ static void tc_drain_paths(void) int opens_before; int writes_before; - printf("-- TC-SOESTORE-010..021 drain 落盘 / 故障注入 --\n"); + printf("-- TC-SOESTORE-010..021 drain to disk / fault injection --\n"); /* 010:首次 drain 惰性建队列 + 1 条 64B 落盘且能解析回来 */ reset_all(); @@ -459,7 +459,7 @@ static void tc_drain_paths(void) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE); CHECK(ok && read_rec(0u, &out) && (out.type == (uint8_t)SOE_T_ELM_ASSERT) && (out.elm_id == 3u) && (out.value == 0xABCDu) && (out.seq == 0u), - "TC-SOESTORE-010 惰性建队列 + 64B 定长落盘 + 记录可解回"); + "TC-SOESTORE-010 lazy queue creation + fixed 64B record on disk + record decodes back"); /* 011:打开模式必须带 FA_WRITE —— 陷阱一 */ reset_all(); @@ -478,7 +478,7 @@ static void tc_drain_paths(void) { FRESULT fr3 = f_write(&probe, payload, SOE_REC_SIZE, &bw); CHECK(ok && (fr2 == FR_OK) && (fr3 == FR_DENIED) && (bw == 0u), - "TC-SOESTORE-011 模式必为 FA_WRITE|FA_OPEN_APPEND;缺写位 f_write 恒 FR_DENIED"); + "TC-SOESTORE-011 mode must be FA_WRITE|FA_OPEN_APPEND; without the write bit f_write is FR_DENIED"); } (void)f_close(&probe); } @@ -499,14 +499,14 @@ static void tc_drain_paths(void) } } } - CHECK(ok, "TC-SOESTORE-012 逐条 open/write/close(无批量合并)+ FIFO 顺序正确"); + CHECK(ok, "TC-SOESTORE-012 per-entry open/write/close (no batch merge) + correct FIFO order"); /* 013:open/close 严格配对 */ reset_all(); soe_store_post((uint8_t)SOE_T_SAMPLE_RESUME, SOE_ELM_NONE, 0u); soe_store_drain(); CHECK(g_ff_obs.calls[FF_FAKE_OP_CLOSE] == g_ff_obs.calls[FF_FAKE_OP_OPEN], - "TC-SOESTORE-013 open/close 配对(脏句柄不跨调用残留)"); + "TC-SOESTORE-013 open/close stay paired (no stale handle left across calls)"); /* 014:短写判失败 -> unlink 重建 -> 重试成功 -> 恰好 64 字节 */ reset_all(); @@ -517,7 +517,7 @@ static void tc_drain_paths(void) && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] >= 1) && (g_ff_obs.last_write_bw == SOE_REC_SIZE) && (g_soe_err_count == 0), - "TC-SOESTORE-014 FR_OK+短写被当失败处理(不校验字节数就会静默收下 63B)"); + "TC-SOESTORE-014 FR_OK plus short write counts as failure (63B would pass silently without check)"); /* 015:连续**两轮** drain 都失败 -> 留痕只许 1 次(一轮 = 首写 + 自愈重试 * = 2 次 f_write,所以注入 4 次零字节短写才够撑两轮) */ @@ -528,7 +528,7 @@ static void tc_drain_paths(void) ok = (g_soe_err_count == 1) && (s_ring_tail == 0u) && (s_fault_reported == 1u); soe_store_drain(); /* 第 2 轮仍失败:不得再留痕 */ CHECK(ok && (g_soe_err_count == 1) && (s_ring_tail == 0u), - "TC-SOESTORE-015 写故障留痕去重(防串口与 selftest 被同一条故障刷屏)"); + "TC-SOESTORE-015 write-fault trace is de-duplicated (one fault must not flood UART and selftest)"); /* 015b:成功一次复位去重位,之后再失败才会重新留痕 */ /* 015b:失败(留痕1) -> 恢复(去重位复位) -> 再失败(必须留痕2)。 @@ -543,7 +543,7 @@ static void tc_drain_paths(void) soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 2u, 2u); soe_store_drain(); /* 又失败 -> 留痕 2 */ CHECK(ok && (g_soe_err_count == 2) && (s_fault_reported == 1u), - "TC-SOESTORE-015b 恢复后再失败必须重新留痕(漏复位 = 永久静默)"); + "TC-SOESTORE-015b a failure after recovery must trace again (missing reset = permanent silence)"); /* 016:卷忙 -> -2:不碰 FatFs、不 release、环不推进 */ reset_all(); @@ -554,7 +554,7 @@ static void tc_drain_paths(void) CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0) && (s_ring_tail == 0u) && (g_soe_err_count == 0), - "TC-SOESTORE-016 卷忙 = 顺延:零 FatFs、零 release、不计故障(喂狗点纪律)"); + "TC-SOESTORE-016 volume busy means defer: zero FatFs, zero release, no fault counted (feed duty)"); /* 017:写失败时环不推进 tail -> 下轮重试、既不丢也不重复 */ reset_all(); @@ -566,7 +566,7 @@ static void tc_drain_paths(void) soe_store_drain(); /* 下轮成功 */ CHECK(ok && (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE) && read_rec(0u, &out) && (out.elm_id == 5u), - "TC-SOESTORE-017 环条目写失败不推进 tail:不丢、下轮也不重复落"); + "TC-SOESTORE-017 ring write failure does not advance tail: nothing lost, no duplicate next round"); /* 018:队列条目已出队 -> 卷忙只丢当前一条,剩余留在队列(现状不对称) */ reset_all(); @@ -584,18 +584,18 @@ static void tc_drain_paths(void) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_before) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == writes_before) && (s_lock_waits >= 1) && (s_lock_seq_err == 0) && (s_q_get_calls >= 2), - "TC-SOESTORE-018 队列路径卷忙:只丢已出队那一条、本轮零 FatFs(与环路径不对称)"); + "TC-SOESTORE-018 queue path with busy volume: only the dequeued entry is lost, zero FatFs now"); /* 019:容量满 -> unlink 重建 + INFO 留痕,新文件恰好 64 字节 */ reset_all(); CHECK(ff_fake_mkfile_size(SOE_PATH, (uint32_t)SOE_STORE_MAX * SOE_REC_SIZE) == 0, - "TC-SOESTORE-019 前置:铺 1024 条(64KB)假记录"); + "TC-SOESTORE-019 setup: lay down 1024 fake records (64KB)"); soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 9u, 0x99u); soe_store_drain(); CHECK((g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE) && (g_soe_info_count == 1) && read_rec(0u, &out) && (out.elm_id == 9u), - "TC-SOESTORE-019 满则删除重建:新文件只剩本次这一条"); + "TC-SOESTORE-019 full file is deleted and recreated: the new file holds only this entry"); /* 020:丢弃留痕按差值上报一次,不刷屏 */ reset_all(); @@ -605,7 +605,7 @@ static void tc_drain_paths(void) soe_store_drain(); /* 差值上报 1 条 */ soe_store_drain(); /* 无新丢弃 -> 不再报 */ CHECK((s_drop_cnt == 3u) && (s_drop_logged == 3u) && (g_soe_err_count == 1), - "TC-SOESTORE-020 丢弃留痕按差值上报一次(defaultTask 侧才是日志安全区)"); + "TC-SOESTORE-020 drop trace reports the delta once (only the defaultTask side is log-safe)"); /* 021:队列创建失败 -> 条目滞留环、下轮再试、不崩 */ /* 021:osMessageQueueNew 返回 NULL —— 生产代码里环在队列**之前**排空, @@ -620,12 +620,12 @@ static void tc_drain_paths(void) && (s_ring_tail == 1u) && (ff_fake_size(SOE_PATH) == SOE_REC_SIZE); soe_store_post((uint8_t)SOE_T_ELM_ASSERT, 5u, 5u); /* 队列没建成 -> 仍进环 */ CHECK(ok && (s_q_cnt == 0u) && (s_ring_head == 2u), - "TC-SOESTORE-021 队列创建失败:环路径照常落盘不丢条目 + 留痕一次"); + "TC-SOESTORE-021 queue creation failure: ring path still writes to disk, loses nothing + one trace"); s_q_new_fail = 0; soe_store_drain(); CHECK((s_queue != NULL) && (s_ring_tail == 2u) && (ff_fake_size(SOE_PATH) == 2u * SOE_REC_SIZE) && (g_soe_err_count == 1), - "TC-SOESTORE-021b 堆恢复后队列补建、滞留条目全部落盘"); + "TC-SOESTORE-021b once the heap recovers the queue is created and all pending entries reach disk"); } /* ===================== TC-SOESTORE-022..024:陷阱二 —— 名字/参数类错误码 ===================== */ @@ -635,7 +635,7 @@ static void tc_name_error_not_volume(void) uint32_t i; int ok; - printf("-- TC-SOESTORE-022..024 名字/参数类错误码严禁升级成卷操作 --\n"); + printf("-- TC-SOESTORE-022..024 name/parameter errors must never escalate into volume operations --\n"); /* 022【D 项已修 2026-10-24:正向断言】f_open 失败(名字/参数类码) * -> 只留痕返回 -1:**绝不 unlink**、不动 /log 目录、已有记录一条不少。 @@ -692,7 +692,7 @@ static void tc_name_error_not_volume(void) && (s_ring_tail == 0u); ff_fake_inject_write_short(SOE_PATH, 0u, 0); /* 清掉不再需要的注入 */ (void)ok; - CHECK(ok, "TC-SOESTORE-023 长跑反复失败:unlink 有界(<=1/轮),绝不升级为整卷动作"); + CHECK(ok, "TC-SOESTORE-023 repeated failures: unlink stays bounded (<=1 per round), never a volume action"); /* 024:假卷严格 8.3 校验的保真自检 —— 生产路径必过、点开头必被拒 */ reset_all(); @@ -715,7 +715,7 @@ static void tc_name_error_not_volume(void) CHECK((fr_ok == FR_OK) && ((fr_dot == FR_INVALID_NAME) || (fr_dot == FR_INVALID_DRIVE)) && (fr_colon == FR_INVALID_DRIVE), - "TC-SOESTORE-024 假件保真:/log/soe.log 过 8.3,.fschk 与坏盘符被拒"); + "TC-SOESTORE-024 fake fidelity: /log/soe.log passes 8.3, .fschk and a bad drive prefix are rejected"); } /* 生产路径本身合规的正向断言:改名成非 8.3 会让上面那条保真自检变红 */ reset_all(); @@ -723,7 +723,7 @@ static void tc_name_error_not_volume(void) soe_store_drain(); CHECK((strcmp(g_ff_obs.last_open_path, "/LOG/SOE.LOG") == 0) && (g_ff_obs.last_open_rc == (int)FR_OK), - "TC-SOESTORE-024b SOE_PATH 在严格 8.3 卷上确实打得开(改名=静默写失败的防线)"); + "TC-SOESTORE-024b SOE_PATH really opens on a strict 8.3 volume (guard against silent write failure)"); } /* ===================== 入口 ===================== */ diff --git a/test/host/sys_clock/sys_clock_test.c b/test/host/sys_clock/sys_clock_test.c index ee30c53a..052f05c8 100644 --- a/test/host/sys_clock/sys_clock_test.c +++ b/test/host/sys_clock/sys_clock_test.c @@ -189,19 +189,19 @@ static void tc_000_uninitialized(void) { sd2506_time_t t; char buf[32]; - printf("TC-SYSCLK-000 未初始化语义\n"); + printf("TC-SYSCLK-000 uninitialised semantics\n"); /* 注意:本用例不做 sys_clock_init / sys_clock_set */ - CHECK_EQ_U32(sys_clock_get(), UINT32_MAX, "未初始化 get 返回 UINT32_MAX"); - CHECK(sys_clock_get_time(&t) == -1, "未初始化 get_time 返回 -1"); + CHECK_EQ_U32(sys_clock_get(), UINT32_MAX, "get before init returns UINT32_MAX"); + CHECK(sys_clock_get_time(&t) == -1, "get_time before init returns -1"); memset(buf, 0, sizeof(buf)); - CHECK(sys_clock_get_str(buf, sizeof(buf)) == buf, "get_str 返回 buf 指针"); - CHECK(strcmp(buf, "----/--/-- --:--:--") == 0, "get_str 未初始化占位串"); + CHECK(sys_clock_get_str(buf, sizeof(buf)) == buf, "get_str returns the buf pointer"); + CHECK(strcmp(buf, "----/--/-- --:--:--") == 0, "get_str placeholder string before init"); } static void tc_001_init_ok(void) { int rc; - printf("TC-SYSCLK-001 init 正常(RTC 合法)\n"); + printf("TC-SYSCLK-001 init normal (valid RTC)\n"); rtc_set(2026, 8, 24, 0, 0, 0); s_rtc_read_rc = BSP_OK; s_rtc_write_rc = BSP_OK; @@ -209,60 +209,60 @@ static void tc_001_init_ok(void) { s_tick = 12345u; rc = sys_clock_init(); - CHECK(rc == 0, "RTC 合法 → init 返回 0"); + CHECK(rc == 0, "valid RTC -> init returns 0"); CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 8, 24, 0, 0, 0), - "基准 = 2026-08-24 00:00:00 的 Unix 秒"); - CHECK(s_rtc_set_calls == 0, "合法 RTC 不被写回(无校时)"); + "base = Unix seconds of 2026-08-24 00:00:00"); + CHECK(s_rtc_set_calls == 0, "valid RTC is never written back (no time correction)"); } static void tc_002_init_leap(void) { - printf("TC-SYSCLK-002 init 闰年换算\n"); + printf("TC-SYSCLK-002 init leap-year conversion\n"); /* 2024 是闰年:2/29 存在 */ rtc_set(2024, 2, 29, 12, 0, 0); s_rtc_read_rc = BSP_OK; - CHECK(sys_clock_init() == 0, "2024-02-29 合法"); + CHECK(sys_clock_init() == 0, "2024-02-29 accepted"); CHECK_EQ_U32(sys_clock_get(), ref_unix(2024, 2, 29, 12, 0, 0), - "2024-02-29 12:00:00 换算正确"); + "2024-02-29 12:00:00 converts correctly"); /* 2000 是世纪闰年:2/29 存在 */ rtc_set(2000, 2, 29, 0, 0, 0); - CHECK(sys_clock_init() == 0, "2000-02-29 合法(世纪闰年)"); + CHECK(sys_clock_init() == 0, "2000-02-29 accepted (century leap year)"); CHECK_EQ_U32(sys_clock_get(), ref_unix(2000, 2, 29, 0, 0, 0), - "2000-02-29 换算正确"); + "2000-02-29 converts correctly"); /* 3 月 1 日:验证闰年 2 月多 1 天被正确累计 */ rtc_set(2024, 3, 1, 0, 0, 0); - CHECK(sys_clock_init() == 0, "2024-03-01 合法"); + CHECK(sys_clock_init() == 0, "2024-03-01 accepted"); CHECK_EQ_U32(sys_clock_get(), ref_unix(2024, 3, 1, 0, 0, 0), - "2024-03-01 与 2024-02-29 相差恰 1 天"); + "2024-03-01 is exactly 1 day after 2024-02-29"); CHECK_EQ_U32(ref_unix(2024, 3, 1, 0, 0, 0) - ref_unix(2024, 2, 29, 0, 0, 0), - 86400u, "闰日间隔 = 86400 s"); + 86400u, "leap-day gap = 86400 s"); } static void tc_003_init_month_end(void) { - printf("TC-SYSCLK-003 init 月末边界\n"); + printf("TC-SYSCLK-003 init end-of-month boundaries\n"); rtc_set(2026, 1, 31, 0, 0, 0); s_rtc_read_rc = BSP_OK; - CHECK(sys_clock_init() == 0, "1/31 合法"); - CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 1, 31, 0, 0, 0), "1/31 换算"); + CHECK(sys_clock_init() == 0, "1/31 accepted"); + CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 1, 31, 0, 0, 0), "1/31 conversion"); rtc_set(2026, 2, 28, 23, 59, 59); s_rtc_read_rc = BSP_OK; - CHECK(sys_clock_init() == 0, "2/28 合法(平年)"); - CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 2, 28, 23, 59, 59), "2/28 末端换算"); + CHECK(sys_clock_init() == 0, "2/28 accepted (common year)"); + CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 2, 28, 23, 59, 59), "2/28 end-of-day conversion"); rtc_set(2026, 4, 30, 0, 0, 0); s_rtc_read_rc = BSP_OK; - CHECK(sys_clock_init() == 0, "4/30 合法(30 天月)"); - CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 4, 30, 0, 0, 0), "4/30 换算"); + CHECK(sys_clock_init() == 0, "4/30 accepted (30-day month)"); + CHECK_EQ_U32(sys_clock_get(), ref_unix(2026, 4, 30, 0, 0, 0), "4/30 conversion"); /* 2/28 → 3/1 间隔恰 1 天(平年) */ rtc_set(2026, 2, 28, 0, 0, 0); sys_clock_init(); uint32_t a = sys_clock_get(); rtc_set(2026, 3, 1, 0, 0, 0); sys_clock_init(); uint32_t b = sys_clock_get(); - CHECK_EQ_U32(b - a, 86400u, "平年 2/28 → 3/1 间隔 = 86400 s"); + CHECK_EQ_U32(b - a, 86400u, "common year 2/28 -> 3/1 gap = 86400 s"); } static void tc_004_init_epoch_bounds(void) { - printf("TC-SYSCLK-004 init 世纪边界(Unix 下限 / 2038 上限)\n"); + printf("TC-SYSCLK-004 init century boundaries (Unix lower bound / 2038 upper bound)\n"); /* * 注意:init 接受的 RTC 年份范围是 2000~2099(越界即判故障并自愈), * 故"Unix 0(1970-01-01)"**不可能经 init 建立**——1970 会被判越界。 @@ -270,46 +270,46 @@ static void tc_004_init_epoch_bounds(void) { * 这里验证 init 可接受的最早年月,以及 uint32 上限。 */ rtc_set(2000, 1, 1, 0, 0, 0); s_rtc_read_rc = BSP_OK; - CHECK(sys_clock_init() == 0, "2000-01-01 合法(可接受最早年)"); - CHECK_EQ_U32(sys_clock_get(), ref_unix(2000, 1, 1, 0, 0, 0), "2000-01-01 换算"); + CHECK(sys_clock_init() == 0, "2000-01-01 accepted (earliest acceptable year)"); + CHECK_EQ_U32(sys_clock_get(), ref_unix(2000, 1, 1, 0, 0, 0), "2000-01-01 conversion"); /* 2038 上限:2038-01-19 03:14:07 = 0x7FFFFFFF */ rtc_set(2038, 1, 19, 3, 14, 7); s_rtc_read_rc = BSP_OK; - CHECK(sys_clock_init() == 0, "2038-01-19 合法(uint32 上限内)"); - CHECK_EQ_U32(sys_clock_get(), 0x7FFFFFFFu, "2038-01-19 03:14:07 → 0x7FFFFFFF"); + CHECK(sys_clock_init() == 0, "2038-01-19 accepted (within uint32 limit)"); + CHECK_EQ_U32(sys_clock_get(), 0x7FFFFFFFu, "2038-01-19 03:14:07 -> 0x7FFFFFFF"); /* Unix 0 经 set 路径显式可达(与 init 的范围限制无关) */ clear_obs(); - CHECK(sys_clock_set(0u, NULL) == 0, "set(0) 合法(Unix 零点)"); - CHECK_EQ_U32(sys_clock_get(), 0u, "set(0) 后 get == 0(Unix 零点)"); + CHECK(sys_clock_set(0u, NULL) == 0, "set(0) accepted (Unix epoch)"); + CHECK_EQ_U32(sys_clock_get(), 0u, "after set(0) get == 0 (Unix epoch)"); { sd2506_time_t t; CHECK(sys_clock_get_time(&t) == 0 && t.year == 1970 && t.month == 1 && t.day == 1, - "Unix 0 → 1970-01-01(unix_to_sd2506 反向换算)"); + "Unix 0 -> 1970-01-01 (unix_to_sd2506 reverse conversion)"); } } static void tc_005_init_rtc_read_fail(void) { - printf("TC-SYSCLK-005 init RTC 读失败 → 降级默认时间并返回 -1\n"); + printf("TC-SYSCLK-005 init RTC read failure -> degrade to default time and return -1\n"); s_rtc_read_rc = BSP_ERR_IO; /* 注入读故障 */ s_rtc_write_rc = BSP_OK; clear_obs(); s_tick = 777u; int rc = sys_clock_init(); - CHECK(rc == -1, "RTC 读失败 → init 返回 -1(报告芯片故障)"); + CHECK(rc == -1, "RTC read failure -> init returns -1 (reports the chip fault)"); /* 降级基准 = 编译期默认 RTC_DEFAULT_*(2026-08-24 00:00:00) */ CHECK_EQ_U32(sys_clock_get(), ref_unix(RTC_DEFAULT_YEAR, RTC_DEFAULT_MONTH, RTC_DEFAULT_DAY, RTC_DEFAULT_HOUR, RTC_DEFAULT_MINUTE, RTC_DEFAULT_SECOND), - "降级基准 = 编译期默认时间(墙钟仍可用)"); - CHECK(g_sclk_err_count >= 1, "打了 DBG_ERROR(RTC 异常)"); + "degraded base = compile-time default time (wall clock still usable)"); + CHECK(g_sclk_err_count >= 1, "DBG_ERROR emitted (RTC abnormal)"); s_rtc_read_rc = BSP_OK; /* 复位注入 */ } static void tc_006_init_out_of_range_selfheal(void) { - printf("TC-SYSCLK-006 init RTC 越界 → 自愈写回默认时间(返回 0)\n"); + printf("TC-SYSCLK-006 init RTC out of range -> self-heal by writing back the default time (returns 0)\n"); /* * RTC_DEFAULT_INIT_ENABLE=1 且 FORCE=0 时:越界值触发"写回默认时间", * 写成功且复读合法 → rtc_ok 恢复为 1 → **返回 0**(自愈,非降级)。 @@ -319,23 +319,23 @@ static void tc_006_init_out_of_range_selfheal(void) { s_rtc_read_rc = BSP_OK; s_rtc_write_rc = BSP_OK; clear_obs(); - CHECK(sys_clock_init() == 0, "year=1999 → 自愈写回成功 → 返回 0"); - CHECK(s_rtc_set_calls == 1, "越界触发一次写回"); - CHECK(s_rtc.year == RTC_DEFAULT_YEAR, "阴影 RTC 已修正为默认年"); + CHECK(sys_clock_init() == 0, "year=1999 -> self-heal write-back ok -> returns 0"); + CHECK(s_rtc_set_calls == 1, "out-of-range triggers exactly one write-back"); + CHECK(s_rtc.year == RTC_DEFAULT_YEAR, "shadow RTC corrected to the default year"); rtc_set(2026, 13, 5, 0, 0, 0); /* month > 12 → 越界 */ clear_obs(); - CHECK(sys_clock_init() == 0, "month=13 → 自愈 → 返回 0"); - CHECK(s_rtc_set_calls == 1, "越界触发一次写回"); + CHECK(sys_clock_init() == 0, "month=13 -> self-heal -> returns 0"); + CHECK(s_rtc_set_calls == 1, "out-of-range triggers exactly one write-back"); rtc_set(2026, 5, 0, 0, 0, 0); /* day < 1 → 越界 */ clear_obs(); - CHECK(sys_clock_init() == 0, "day=0 → 自愈 → 返回 0"); - CHECK(s_rtc_set_calls == 1, "越界触发一次写回"); + CHECK(sys_clock_init() == 0, "day=0 -> self-heal -> returns 0"); + CHECK(s_rtc_set_calls == 1, "out-of-range triggers exactly one write-back"); } static void tc_020_init_selfheal_fail_degrades(void) { - printf("TC-SYSCLK-020 init 越界且写回失败 → 降级默认时间并返回 -1\n"); + printf("TC-SYSCLK-020 init out of range and write-back fails -> degrade to default time and return -1\n"); /* * 组合故障:读成功但值越界(触发写回),写回失败 → 复读仍越界 → * rtc_ok 清 0 → 走降级分支(启用默认值时建基准 + 返回 -1)。 @@ -346,81 +346,81 @@ static void tc_020_init_selfheal_fail_degrades(void) { clear_obs(); s_tick = 42u; int rc = sys_clock_init(); - CHECK(rc == -1, "越界 + 写回失败 → init 返回 -1"); - CHECK(s_rtc_set_calls == 1, "尝试写回一次(失败)"); - CHECK(g_sclk_err_count >= 1, "打了 DBG_ERROR"); + CHECK(rc == -1, "out of range + write-back failure -> init returns -1"); + CHECK(s_rtc_set_calls == 1, "write-back attempted once (failed)"); + CHECK(g_sclk_err_count >= 1, "DBG_ERROR emitted"); CHECK_EQ_U32(sys_clock_get(), ref_unix(RTC_DEFAULT_YEAR, RTC_DEFAULT_MONTH, RTC_DEFAULT_DAY, 0, 0, 0), - "降级基准 = 编译期默认时间(墙钟仍可用)"); + "degraded base = compile-time default time (wall clock still usable)"); s_rtc_write_rc = BSP_OK; } static void tc_007_init_corrects_rtc(void) { - printf("TC-SYSCLK-007 init 越界 → 自动写回默认时间并复验\n"); + printf("TC-SYSCLK-007 init out of range -> auto write-back of default time and re-verify\n"); rtc_set(1999, 1, 1, 0, 0, 0); /* 越界,触发校时写回 */ s_rtc_read_rc = BSP_OK; s_rtc_write_rc = BSP_OK; clear_obs(); int rc = sys_clock_init(); - CHECK(s_rtc_set_calls == 1, "越界 → 写回 RTC 一次"); - CHECK(rc == 0, "写回成功 + 复读合法 → 返回 0(自愈成功,非降级)"); + CHECK(s_rtc_set_calls == 1, "out of range -> RTC written back once"); + CHECK(rc == 0, "write-back ok + re-read valid -> returns 0 (self-heal succeeded, not a degrade)"); CHECK(s_rtc.year == RTC_DEFAULT_YEAR && s_rtc.month == RTC_DEFAULT_MONTH - && s_rtc.day == RTC_DEFAULT_DAY, "阴影 RTC 已被写入默认时间"); + && s_rtc.day == RTC_DEFAULT_DAY, "shadow RTC was written with the default time"); CHECK_EQ_U32(sys_clock_get(), ref_unix(RTC_DEFAULT_YEAR, RTC_DEFAULT_MONTH, RTC_DEFAULT_DAY, 0, 0, 0), - "基准 = 修正后的默认时间"); + "base = the corrected default time"); } static void tc_008_get_advance(void) { - printf("TC-SYSCLK-008 get 正常推进(含亚秒截断)\n"); + printf("TC-SYSCLK-008 get advances normally (sub-second truncation included)\n"); base_at(1000000u, 5000u); - CHECK_EQ_U32(sys_clock_get(), 1000000u, "基准时刻 = 基准值"); + CHECK_EQ_U32(sys_clock_get(), 1000000u, "at the base instant the value equals the base"); tick_advance(999u); - CHECK_EQ_U32(sys_clock_get(), 1000000u, "999ms 未满 1s → 不变(截断)"); + CHECK_EQ_U32(sys_clock_get(), 1000000u, "999ms is under 1 s -> unchanged (truncated)"); tick_advance(1u); - CHECK_EQ_U32(sys_clock_get(), 1000001u, "累计 1000ms → +1s"); + CHECK_EQ_U32(sys_clock_get(), 1000001u, "accumulated 1000ms -> +1 s"); tick_advance(2500u); - CHECK_EQ_U32(sys_clock_get(), 1000003u, "再 +2500ms → 共 +3s(余 500ms 截断)"); + CHECK_EQ_U32(sys_clock_get(), 1000003u, "another +2500ms -> +3 s total (remaining 500 ms truncated)"); } static void tc_009_get_no_rebase_below_threshold(void) { - printf("TC-SYSCLK-009 未达 30 天阈值不触发再基准化\n"); + printf("TC-SYSCLK-009 below the 30-day threshold no re-baselining\n"); base_at(2000000u, 0u); /* 29 天 < 30 天阈值:线性推进,基准不动 */ tick_advance(29u * 24u * 3600u * 1000u); CHECK_EQ_U32(sys_clock_get(), 2000000u + 29u * 24u * 3600u, - "29 天线性推进正确(未再基准化)"); + "29 days advance linearly and correctly (no re-baselining)"); } static void tc_010_get_rebase_at_threshold(void) { - printf("TC-SYSCLK-010 30 天惰性再基准化(窗口清零、值无损、余数结转)\n"); + printf("TC-SYSCLK-010 30-day lazy re-baselining (window cleared, value lossless, remainder carried over)\n"); base_at(3000000u, 0u); /* 推进恰好 30 天 + 1500ms(余数 500ms 应结转到新窗) */ tick_advance(30u * 24u * 3600u * 1000u + 1500u); uint32_t expect = 3000000u + 30u * 24u * 3600u + 1u; /* 1500ms → +1s,余 500ms */ - CHECK_EQ_U32(sys_clock_get(), expect, "再基准化后墙钟值无损(+30 天 1 秒)"); + CHECK_EQ_U32(sys_clock_get(), expect, "after re-baselining the wall clock value is lossless (+30 days 1 second)"); /* 再推 500ms:结转的余数正好补足,再多 1 秒 */ tick_advance(500u); - CHECK_EQ_U32(sys_clock_get(), expect + 1u, "亚秒余数已结转(+500ms 补足 1 秒)"); + CHECK_EQ_U32(sys_clock_get(), expect + 1u, "sub-second remainder carried over (+500 ms completes 1 second)"); /* 再推一整天:仍线性 */ tick_advance(24u * 3600u * 1000u); - CHECK_EQ_U32(sys_clock_get(), expect + 1u + 86400u, "再基准化后继续线性推进"); + CHECK_EQ_U32(sys_clock_get(), expect + 1u + 86400u, "keeps advancing linearly after re-baselining"); } static void tc_011_get_tick_wrap(void) { - printf("TC-SYSCLK-011 tick 回绕(无符号差)安全\n"); + printf("TC-SYSCLK-011 tick wraparound (unsigned diff) is safe\n"); /* 基准设在接近 2^32 处 */ base_at(4000000u, 0xFFFFF000u); /* 推进 0x2000 ms(跨越 2^32 回绕):差 = 0x2000 = 8192ms → +8s */ tick_advance(0x2000u); - CHECK_EQ_U32(sys_clock_get(), 4000000u + 8u, "跨 tick 回绕后差值正确"); + CHECK_EQ_U32(sys_clock_get(), 4000000u + 8u, "diff is correct across the tick wraparound"); } static void tc_012_get_long_run_no_drift(void) { - printf("TC-SYSCLK-012 多轮再基准化长期不漂移\n"); + printf("TC-SYSCLK-012 many re-baselining rounds do not drift over time\n"); base_at(5000000u, 0u); /* * 惰性再基准化的**正确用法**:周期性调用 get() 让差值窗口始终 < 30 天。 @@ -434,44 +434,44 @@ static void tc_012_get_long_run_no_drift(void) { for (i = 1u; i <= 9u; i++) { tick_advance(10u * 86400u * 1000u); /* 每次 +10 天(< 30 天阈值) */ CHECK_EQ_U32(sys_clock_get(), base + i * 10u * 86400u, - "10 天步进(含 3 轮再基准)线性无漂移"); + "10-day steps (with 3 re-baseline rounds) stay linear with no drift"); } } static void tc_013_get_str(void) { - printf("TC-SYSCLK-013 get_str 格式化\n"); + printf("TC-SYSCLK-013 get_str formatting\n"); char buf[32]; base_at(ref_unix(2026, 10, 2, 13, 4, 5), 0u); memset(buf, 0, sizeof(buf)); sys_clock_get_str(buf, sizeof(buf)); - CHECK(strcmp(buf, "2026-10-02 13:04:05") == 0, "格式化 YYYY-MM-DD HH:MM:SS"); + CHECK(strcmp(buf, "2026-10-02 13:04:05") == 0, "formatted as YYYY-MM-DD HH:MM:SS"); base_at(ref_unix(2000, 1, 1, 0, 0, 0), 0u); memset(buf, 0, sizeof(buf)); sys_clock_get_str(buf, sizeof(buf)); - CHECK(strcmp(buf, "2000-01-01 00:00:00") == 0, "零点补零格式正确"); + CHECK(strcmp(buf, "2000-01-01 00:00:00") == 0, "zero-padded midnight format is correct"); } static void tc_014_get_time_fields(void) { - printf("TC-SYSCLK-014 get_time 日历字段\n"); + printf("TC-SYSCLK-014 get_time calendar fields\n"); sd2506_time_t t; /* 2026-10-02 是周五(已知) */ base_at(ref_unix(2026, 10, 2, 8, 9, 10), 0u); - CHECK(sys_clock_get_time(&t) == 0, "get_time 返回 0"); + CHECK(sys_clock_get_time(&t) == 0, "get_time returns 0"); CHECK(t.year == 2026 && t.month == 10 && t.day == 2, - "年月日正确"); - CHECK(t.hour == 8 && t.minute == 9 && t.second == 10, "时分秒正确"); - CHECK(t.week == 5, "2026-10-02 星期五(week==5,0=周日)"); + "year/month/day correct"); + CHECK(t.hour == 8 && t.minute == 9 && t.second == 10, "hour/minute/second correct"); + CHECK(t.week == 5, "2026-10-02 is Friday (week==5, 0=Sunday)"); } static void tc_015_get_time_null(void) { - printf("TC-SYSCLK-015 get_time NULL 参数被拒\n"); + printf("TC-SYSCLK-015 get_time rejects a NULL argument\n"); base_at(6000000u, 0u); - CHECK(sys_clock_get_time(NULL) == -1, "p_time == NULL → 返回 -1"); + CHECK(sys_clock_get_time(NULL) == -1, "p_time == NULL -> returns -1"); } static void tc_016_set_writes_rtc(void) { - printf("TC-SYSCLK-016 set 更新基准 + 写回 RTC\n"); + printf("TC-SYSCLK-016 set updates the base + writes back RTC\n"); sd2506_time_t sd; memset(&sd, 0, sizeof(sd)); sd.year = 2027; sd.month = 3; sd.day = 15; @@ -480,48 +480,48 @@ static void tc_016_set_writes_rtc(void) { s_rtc_write_rc = BSP_OK; clear_obs(); s_tick = 100u; - CHECK(sys_clock_set(7000000u, &sd) == 0, "set 返回 0"); - CHECK_EQ_U32(sys_clock_get(), 7000000u, "基准已更新"); - CHECK(s_rtc_set_calls == 1, "sd_time != NULL → 写回 RTC 一次"); + CHECK(sys_clock_set(7000000u, &sd) == 0, "set returns 0"); + CHECK_EQ_U32(sys_clock_get(), 7000000u, "base has been updated"); + CHECK(s_rtc_set_calls == 1, "sd_time != NULL -> RTC written back once"); CHECK(s_rtc.year == 2027 && s_rtc.month == 3 && s_rtc.day == 15 && s_rtc.hour == 6 && s_rtc.minute == 7 && s_rtc.second == 8, - "RTC 写入内容与传入一致"); + "RTC written content equals what was passed in"); /* 写失败:基准仍更新,但打错误日志 */ clear_obs(); s_rtc_write_rc = BSP_ERR_IO; sys_clock_set(7000001u, &sd); - CHECK_EQ_U32(sys_clock_get(), 7000001u, "RTC 写失败仍更新基准"); - CHECK(g_sclk_err_count >= 1, "RTC 写失败打 DBG_ERROR"); + CHECK_EQ_U32(sys_clock_get(), 7000001u, "base is still updated when the RTC write fails"); + CHECK(g_sclk_err_count >= 1, "DBG_ERROR emitted when the RTC write fails"); s_rtc_write_rc = BSP_OK; } static void tc_017_set_no_rtc(void) { - printf("TC-SYSCLK-017 set sd_time=NULL 不碰 RTC\n"); + printf("TC-SYSCLK-017 set sd_time=NULL never touches the RTC\n"); clear_obs(); - CHECK(sys_clock_set(8000000u, NULL) == 0, "set(NULL) 返回 0"); - CHECK(s_rtc_set_calls == 0, "sd_time == NULL → 不写 RTC"); - CHECK_EQ_U32(sys_clock_get(), 8000000u, "基准更新"); + CHECK(sys_clock_set(8000000u, NULL) == 0, "set(NULL) returns 0"); + CHECK(s_rtc_set_calls == 0, "sd_time == NULL -> RTC not written"); + CHECK_EQ_U32(sys_clock_get(), 8000000u, "base updated"); } static void tc_018_set_unix_fields(void) { - printf("TC-SYSCLK-018 set_unix 写回 RTC 且字段正确\n"); + printf("TC-SYSCLK-018 set_unix writes back the RTC with correct fields\n"); sd2506_time_t expect_week; uint32_t u = ref_unix(2026, 10, 2, 15, 30, 45); clear_obs(); s_rtc_write_rc = BSP_OK; s_tick = 0u; - CHECK(sys_clock_set_unix(u) == 0, "set_unix 返回 0"); - CHECK(s_rtc_set_calls == 1, "写回 RTC 一次"); + CHECK(sys_clock_set_unix(u) == 0, "set_unix returns 0"); + CHECK(s_rtc_set_calls == 1, "RTC written back once"); CHECK(s_rtc.year == 2026 && s_rtc.month == 10 && s_rtc.day == 2 && s_rtc.hour == 15 && s_rtc.minute == 30 && s_rtc.second == 45, - "RTC 日历字段与 Unix 秒一致"); - CHECK(s_rtc.week == 5, "RTC 星期字段 = 5(2026-10-02 周五)"); + "RTC calendar fields match the Unix seconds"); + CHECK(s_rtc.week == 5, "RTC weekday field = 5 (2026-10-02 is Friday)"); (void)expect_week; } static void tc_019_week_known_dates(void) { - printf("TC-SYSCLK-019 星期(Zeller)已知日期穷举\n"); + printf("TC-SYSCLK-019 weekday (Zeller) exhaustive check against known dates\n"); /* 期望值经 Python datetime 独立核对(0=周日); * 覆盖 1/2 月(Zeller 需上调为上年 13/14 月的分支)与 3~12 月(2026-10-02 * 修正前全部错误),以及世纪闰年、非闰世纪、范围首尾。*/ @@ -554,14 +554,14 @@ static void tc_019_week_known_dates(void) { cases[i].y, cases[i].m, cases[i].d, cases[i].w); CHECK(t.week == cases[i].w, nm); } else { - CHECK(0, "get_time 失败(用例数据问题)"); + CHECK(0, "get_time failed (bad test data)"); } } } int main(void) { - printf("=== System Clock 主机侧单元测试 ===\n"); - printf("被测:App/util/sys_clock.c(时间换算 + 墙钟基准)\n\n"); + printf("=== System Clock host-side unit tests ===\n"); + printf("Target: App/util/sys_clock.c (time conversion + wall-clock base)\n\n"); /* tc_000 必须最先(依赖"刚上电"的静态态) */ tc_000_uninitialized(); diff --git a/test/host/sys_log_mcu/sys_log_mcu_test.c b/test/host/sys_log_mcu/sys_log_mcu_test.c index efea41e1..08e03393 100644 --- a/test/host/sys_log_mcu/sys_log_mcu_test.c +++ b/test/host/sys_log_mcu/sys_log_mcu_test.c @@ -538,7 +538,7 @@ static void tc_format(void) int ok; uint32_t i; - printf("-- TC-SYSLOG-001..004 记录格式(64B 定长)--\n"); + printf("-- TC-SYSLOG-001..004 record format (fixed 64B) --\n"); /* 001 布局 */ reset_all(); @@ -551,14 +551,14 @@ static void tc_format(void) && (rec[SYSLOG_EPOCH_OFFS - 1u] == ' ') && (memcmp(rec + SYSLOG_DESC_OFFS, "hello", 5u) == 0) && (rec[SYSLOG_DESC_OFFS + 5u] == '\0'); - CHECK(ok, "TC-SYSLOG-001 [0..18]ts / [19]空格 / [20..29]epoch / [30]空格 / [31..]desc"); + CHECK(ok, "TC-SYSLOG-001 [0..18]ts / [19]space / [20..29]epoch / [30]space / [31..]desc"); ok = 1; for (i = SYSLOG_DESC_OFFS + 6u; i < SYSLOG_REC_SIZE; i++) { if (rec[i] != 0u) { ok = 0; } } - CHECK(ok, "TC-SYSLOG-001b 尾部 NUL 填充到 64B(定长解析的地基)"); + CHECK(ok, "TC-SYSLOG-001b tail padded with NUL to 64B (basis of fixed-length parsing)"); /* 002 描述截断 */ reset_all(); @@ -575,7 +575,7 @@ static void tc_format(void) } CHECK(ok && (rec[SYSLOG_DESC_OFFS + SYSLOG_DESC_MAX] == 0u) && (rec[SYSLOG_REC_SIZE - 1u] == 0u), - "TC-SYSLOG-002 超长描述截到 28B,不越 64B 边界"); + "TC-SYSLOG-002 over-long desc truncated to 28B, never crossing the 64B boundary"); } /* 003 墙钟未建立 */ @@ -584,7 +584,7 @@ static void tc_format(void) s_format_rec(SYSLOG_RUN, "x", rec); CHECK((memcmp(rec, "1970-01-01 08:00:00", SYSLOG_TS_LEN) == 0) && (memcmp(rec + SYSLOG_EPOCH_OFFS, "0000000000", SYSLOG_EPOCH_LEN) == 0), - "TC-SYSLOG-003 无墙钟 -> 占位时间戳 + epoch 全 0(不造假时标)"); + "TC-SYSLOG-003 no wall clock -> placeholder timestamp + epoch all zero (never fake a time stamp)"); /* 004 单条 vs 批量逐字节相同 */ reset_all(); @@ -594,7 +594,7 @@ static void tc_format(void) (void)memset(b, 0, sizeof(b)); s_format_rec(SYSLOG_RUN, "same-content", b); CHECK(memcmp(rec, b, SYSLOG_REC_SIZE) == 0, - "TC-SYSLOG-004 同一输入两次格式化逐字节一致(UI 定长偏移解析的前提)"); + "TC-SYSLOG-004 same input formats byte-identical twice (premise of UI fixed-offset parsing)"); reset_all(); s_now = 1700000000u; ready_only(); @@ -606,7 +606,7 @@ static void tc_format(void) s_format_rec(SYSLOG_RUN, "ab", batch + SYSLOG_REC_SIZE); CHECK((memcmp(rec, batch, SYSLOG_REC_SIZE) == 0) && (memcmp(rec, batch + SYSLOG_REC_SIZE, SYSLOG_REC_SIZE) == 0), - "TC-SYSLOG-004b 批量路径复用同一 s_format_rec(两条都等于单条)"); + "TC-SYSLOG-004b batch path reuses the same s_format_rec (both equal the single-record result)"); } } @@ -617,7 +617,7 @@ static void tc_write_single(void) uint8_t rec[SYSLOG_REC_SIZE]; int ok; - printf("-- TC-SYSLOG-010..019 单条写路径 --\n"); + printf("-- TC-SYSLOG-010..019 single-record write path --\n"); /* 010 成功 + 打开模式 */ reset_all(); @@ -630,7 +630,7 @@ static void tc_write_single(void) && (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1) && (s_lock_releases == 1) && (s_lock_seq_err == 0) && (g_sys_err_count == 0); - CHECK(ok, "TC-SYSLOG-010 单条落盘成功:模式带写位 + open/write/close 各一次 + 放锁"); + CHECK(ok, "TC-SYSLOG-010 single write ok: mode carries the write bit + open/write/close once each + lock released"); /* 011 陷阱一保真对照 */ reset_all(); @@ -644,7 +644,7 @@ static void tc_write_single(void) fr2 = f_write(&probe, rec, SYSLOG_REC_SIZE, &bw); (void)f_close(&probe); CHECK((fr1 == FR_OK) && (fr2 == FR_DENIED) && (bw == 0u), - "TC-SYSLOG-011 只给 FA_OPEN_APPEND -> f_write 恒 FR_DENIED(2026-09-20 真根因)"); + "TC-SYSLOG-011 FA_OPEN_APPEND alone -> f_write is always FR_DENIED (real root cause 2026-09-20)"); } /* 012 卷忙 */ @@ -655,7 +655,7 @@ static void tc_write_single(void) CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 0) && (s_lock_releases == 0) && (s_lock_seq_err == 0) && (g_sys_err_count == 0), - "TC-SYSLOG-012 卷忙 = 顺延:不碰盘、不 release、不计故障(喂狗点绝不阻塞)"); + "TC-SYSLOG-012 volume busy means defer: no disk touch, no release, no fault counted (feed point never blocks)"); /* 013 短写 -> 自愈 */ reset_all(); @@ -665,7 +665,7 @@ static void tc_write_single(void) CHECK(ok && (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE) && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (g_ff_obs.last_write_bw == SYSLOG_REC_SIZE), - "TC-SYSLOG-013 FR_OK+短写 63B 被判失败 -> unlink 重建 -> 恰好 64 字节"); + "TC-SYSLOG-013 FR_OK + short write 63B treated as failure -> unlink and rebuild -> exactly 64 bytes"); /* 014 容量满 -> 删除重建 */ reset_all(); @@ -675,7 +675,7 @@ static void tc_write_single(void) CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE) && (syslog_count(SYSLOG_RUN) == 1u), - "TC-SYSLOG-014 满 1024 条 -> 先 unlink 再写,新文件只剩本次一条"); + "TC-SYSLOG-014 at 1024 records -> unlink first then write, new file holds only this record"); /* 015 满 + unlink 真失败 */ reset_all(); @@ -685,7 +685,7 @@ static void tc_write_single(void) ok = (s_append(SYSLOG_RUN, "x") == -1); CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 1) && (s_lock_seq_err == 0), - "TC-SYSLOG-015 容量轮转时 unlink 失败 -> 返回 -1 且**已释放卷锁**(漏放 = FTP 永久拿不到卷)"); + "TC-SYSLOG-015 unlink failure during rotation -> -1 and **the volume lock is released** (leak = FTP never gets the volume)"); /* 016 满 + unlink 返 FR_NO_FILE 无害 */ reset_all(); @@ -698,7 +698,7 @@ static void tc_write_single(void) ok = (s_append(SYSLOG_RUN, "gone") == 0); CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1) && (g_sys_err_count == 0), - "TC-SYSLOG-016 unlink 返 FR_NO_FILE 被当无害:不进错误分支、不留痕、继续写"); + "TC-SYSLOG-016 unlink returning FR_NO_FILE is harmless: no error branch, no trace, write continues"); /* 017 入参守卫 */ reset_all(); @@ -708,7 +708,7 @@ static void tc_write_single(void) && (s_append(SYSLOG_RUN, NULL) == -1) && (s_append_rec((syslog_id_t)9u, rec) == -1); CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), - "TC-SYSLOG-017 id 越界 / 空 rec / 空 desc -> -1 且不碰卷锁与盘"); + "TC-SYSLOG-017 id out of range / empty rec / empty desc -> -1 without touching the lock or the disk"); /* 018 写成功 -> 读侧代数自增 */ reset_all(); @@ -717,7 +717,7 @@ static void tc_write_single(void) ok = (s_rd_gen[SYSLOG_RUN] == 1u) && (s_rd_gen[SYSLOG_SELFTEST] == 0u); (void)s_append(SYSLOG_SELFTEST, "b"); CHECK(ok && (s_rd_gen[SYSLOG_SELFTEST] == 1u), - "TC-SYSLOG-018 每次落盘只自增本 id 的代数(另一文件的窗口不被误失效)"); + "TC-SYSLOG-018 each write bumps only this id's generation (the other file's window stays valid)"); /* 019 留痕去重 */ reset_all(); @@ -730,7 +730,7 @@ static void tc_write_single(void) ff_fake_inject_write_short(P_RUN, 0u, 2); (void)s_append(SYSLOG_RUN, "e3"); /* 再失败 -> 再留痕 */ CHECK(ok && (g_sys_err_count == 2), - "TC-SYSLOG-019 持续故障只报第一次;恢复后再失败必须重新留痕"); + "TC-SYSLOG-019 a persistent fault reports once; a failure after recovery must trace again"); } static void tc_write_many(void) @@ -739,7 +739,7 @@ static void tc_write_many(void) uint32_t i; int ok; - printf("-- TC-SYSLOG-020..024 批量写路径(N-24)--\n"); + printf("-- TC-SYSLOG-020..024 batch write path (N-24) --\n"); /* 020 8 条一次 I/O */ reset_all(); @@ -752,7 +752,7 @@ static void tc_write_many(void) && (g_ff_obs.last_write_btw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE) && (g_ff_obs.last_write_bw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE) && (g_ff_obs.calls[FF_FAKE_OP_CLOSE] == 1), - "TC-SYSLOG-020 一批 8 条 = 一次 open + 一次 512B write + 一次 close(旧实现 8 轮)"); + "TC-SYSLOG-020 a batch of 8 = one open + one 512B write + one close (old code needed 8 rounds)"); /* 021 入参守卫 */ reset_all(); @@ -762,7 +762,7 @@ static void tc_write_many(void) && (s_append_many(SYSLOG_RUN, NULL, 1u) == -1) && (s_append_many((syslog_id_t)SYSLOG_COUNT, batch, 1u) == -1); CHECK(ok && (s_lock_waits == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), - "TC-SYSLOG-021 批量入参守卫在**取卷锁之前**(越界 n 不占用卷)"); + "TC-SYSLOG-021 batch argument guards run **before taking the volume lock** (bad n never occupies it)"); /* 022 容量不足 -> -3 且不 unlink */ reset_all(); @@ -773,7 +773,7 @@ static void tc_write_many(void) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 1) && (s_lock_seq_err == 0) && (ff_fake_size(P_RUN) == ((uint32_t)SYSLOG_RUN_MAX - 3u) * SYSLOG_REC_SIZE), - "TC-SYSLOG-022 装不下一批 -> -3 交回慢路径;批量路径**绝不**自己 unlink 重建"); + "TC-SYSLOG-022 batch does not fit -> -3 back to the slow path; the batch path **never** unlinks itself"); /* 023 批量卷忙 / 短写自愈 */ reset_all(); @@ -785,7 +785,7 @@ static void tc_write_many(void) CHECK(ok && (ff_fake_size(P_RUN) == 4u * SYSLOG_REC_SIZE) && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (g_sys_err_count == 0), - "TC-SYSLOG-023 批量卷忙 -> -2;短写(256 只回 100)-> 自愈重建后整批落全"); + "TC-SYSLOG-023 batch with busy volume -> -2; short write (256 asked, 100 back) -> rebuild, whole batch lands"); /* 024 批量持续失败 + 去重 */ reset_all(); @@ -794,7 +794,7 @@ static void tc_write_many(void) ok = (s_append_many(SYSLOG_RUN, batch, 2u) == -1); (void)s_append_many(SYSLOG_RUN, batch, 2u); CHECK(ok && (g_sys_err_count == 1) && (s_rd_gen[SYSLOG_RUN] == 2u), - "TC-SYSLOG-024 批量持续失败留痕去重;代数照样自增(宁多失效一轮也不读脏窗)"); + "TC-SYSLOG-024 batch failure trace de-duplicated; generation still bumps (invalidate a window rather than read stale)"); (void)i; } @@ -806,7 +806,7 @@ static void tc_drain(void) uint32_t i; int ok; - printf("-- TC-SYSLOG-030..041 syslog_drain 排空 --\n"); + printf("-- TC-SYSLOG-030..041 syslog_drain --\n"); /* 030 卷未就绪:条目滞留 preboot,drain 直接返回。 * 注意两条容易被读漏的事实: @@ -821,7 +821,7 @@ static void tc_drain(void) syslog_drain(); CHECK(ok && (s_q_new_calls == 1) && (s_preboot_cnt == 1u) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_soe_drain_calls == 0), - "TC-SYSLOG-030 卷未就绪 -> 队列先建成、条目滞留 preboot、不落盘也不调 SOE"); + "TC-SYSLOG-030 volume not ready -> queue created, entries stay in preboot, nothing written and no SOE call"); /* 031 惰性建队列 */ reset_all(); @@ -829,7 +829,7 @@ static void tc_drain(void) ok = (s_queue == NULL); syslog_drain(); CHECK(ok && (s_queue != NULL) && (s_q_new_calls == 1) && (s_q_msgsz == sizeof(syslog_entry_t)), - "TC-SYSLOG-031 队列在**首次 drain**(defaultTask 上下文)才创建(内核前建对象 = 启动失败)"); + "TC-SYSLOG-031 the queue is created at the **first drain** (defaultTask context); creating objects pre-kernel kills boot"); /* 032 preboot 8 条一批落 */ reset_all(); @@ -837,12 +837,12 @@ static void tc_drain(void) for (i = 0u; i < SYSLOG_QUEUE_MAX; i++) { syslog_post(SYSLOG_RUN, "pre"); } - CHECK(s_preboot_cnt == SYSLOG_QUEUE_MAX, "TC-SYSLOG-032a 内核未运行:8 条全进 preboot 数组"); + CHECK(s_preboot_cnt == SYSLOG_QUEUE_MAX, "TC-SYSLOG-032a kernel not running: all 8 entries go to the preboot array"); syslog_drain(); ok = (s_preboot_cnt == 0u) && (syslog_count(SYSLOG_RUN) == SYSLOG_BATCH_MAX) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 1) && (g_ff_obs.last_write_btw == SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE); - CHECK(ok, "TC-SYSLOG-032b preboot 8 条同文件 -> 一次 512B 合并写(数组下标看而不删)"); + CHECK(ok, "TC-SYSLOG-032b 8 preboot entries of one file -> one merged 512B write (index inspected, not deleted)"); /* 033 + 034 两文件混排:换文件那条不消耗 */ reset_all(); @@ -864,7 +864,7 @@ static void tc_drain(void) CHECK(ok && (s_preboot_cnt == 0u) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] == 3), - "TC-SYSLOG-033/034 换文件即分批(run 被切成两批 = 共 3 轮 I/O)+ 顺序不乱"); + "TC-SYSLOG-033/034 a file change splits the batch (run split in two = 3 I/O rounds) + order preserved"); /* 035【A 项已修 2026-10-24:正向断言】卷忙 -> **整批回投队列**,一条不丢。 * 生产侧修法:syslog_drain 批量路径补 else if (rc == -2) { done = 0u; }, @@ -913,7 +913,7 @@ static void tc_drain(void) syslog_drain(); CHECK((s_q_cnt == 3u) && (syslog_count(SYSLOG_RUN) == SYSLOG_RUN_MAX - 1u) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0), - "TC-SYSLOG-035b 慢路径遇卷忙 -> 3 条**整批回投**(同一码在两条路径上行为相反)"); + "TC-SYSLOG-035b slow path meets busy volume -> all 3 **requeued whole** (same code, opposite behaviour per path)"); /* 035c 一整批(8 条)在卷忙时全部回投,不是只保住一条 */ reset_all(); @@ -966,7 +966,7 @@ static void tc_drain(void) syslog_drain(); CHECK(ok && (s_q_cnt == SYSLOG_QUEUE_MAX) && (s_preboot_cnt == 0u) && (g_sys_err_count == 1 + 5 + 1), - "TC-SYSLOG-036 回投装不下 -> 5 次 hold-over drop + drop/wfail 各 1 条留痕"); + "TC-SYSLOG-036 requeue does not fit -> 5 hold-over drops + one drop trace and one wfail trace"); /* 037 -3 退回慢路径:容量轮转只发生一次 */ reset_all(); @@ -982,7 +982,7 @@ static void tc_drain(void) && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (g_ff_obs.calls[FF_FAKE_OP_WRITE] >= 4); CHECK(ok && (s_q_cnt == 0u), - "TC-SYSLOG-037 -3 -> 逐条慢路径:装满剩位 -> 满 -> unlink 重建 -> 继续,全程 unlink 恰一次"); + "TC-SYSLOG-037 -3 -> per-record slow path: fill the rest -> full -> unlink rebuild -> continue, unlink exactly once"); /* 038 慢路径中途写失败 -> 该条丢 + 尾部回投 */ reset_all(); @@ -999,7 +999,7 @@ static void tc_drain(void) ok = (s_q_cnt == 2u) && (ff_fake_size(P_RUN) == 0u) && (g_sys_err_count >= 2); syslog_drain(); /* 之后成功 */ CHECK(ok && (s_q_cnt == 0u) && (syslog_count(SYSLOG_RUN) == 2u), - "TC-SYSLOG-038 慢路径写失败:只丢**当前那一条**,尾部 2 条下轮补齐"); + "TC-SYSLOG-038 slow-path write failure: only **that one record** is lost, the last 2 land next round"); /* 039【B 项已修 2026-10-24:正向断言】rc == -1:整批回投 + 留痕文案如实 * (旧版这里打 "drop %lu entries" 但一条没丢,文案与行为相反)。 @@ -1038,7 +1038,7 @@ static void tc_drain(void) ok = (s_soe_drain_calls == 1); syslog_drain(); CHECK(ok && (s_soe_drain_calls == 2), - "TC-SYSLOG-040 drain 尾部必调 soe_store_drain(同写者、同卷就绪门)"); + "TC-SYSLOG-040 drain must call soe_store_drain (same writer, same volume-ready gate)"); /* 041 post 分流与守卫 */ reset_all(); @@ -1046,7 +1046,7 @@ static void tc_drain(void) syslog_post((syslog_id_t)SYSLOG_COUNT, "bad-id"); syslog_post(SYSLOG_RUN, NULL); CHECK((s_preboot_cnt == 0u) && (s_q_cnt == 0u), - "TC-SYSLOG-041a id 越界 / NULL 描述 -> 静默丢弃(不占待发位)"); + "TC-SYSLOG-041a id out of range / NULL desc -> silently dropped (does not occupy a pending slot)"); reset_all(); ready_only(); for (i = 0u; i < SYSLOG_QUEUE_MAX + 2u; i++) { @@ -1087,7 +1087,7 @@ static void tc_drain(void) syslog_post(SYSLOG_RUN, "q2"); syslog_post(SYSLOG_RUN, "q3"); CHECK((s_q_cnt == 2u) && (g_sys_err_count == 1), - "TC-SYSLOG-041c 内核运行 -> 走队列;队列满丢弃 + 留痕"); + "TC-SYSLOG-041c kernel running -> queue path; queue full drops with a trace"); } /* ===================== D 读侧 ===================== */ @@ -1100,17 +1100,17 @@ static void tc_read(void) uint32_t i; int ok; - printf("-- TC-SYSLOG-050..058 读侧(N-24 滑动窗口)--\n"); + printf("-- TC-SYSLOG-050..058 read side (N-24 sliding window) --\n"); /* 050 count */ reset_all(); ready_only(); - CHECK(syslog_count(SYSLOG_RUN) == 0u, "TC-SYSLOG-050a 文件缺失 -> count 0"); + CHECK(syslog_count(SYSLOG_RUN) == 0u, "TC-SYSLOG-050a missing file -> count 0"); for (i = 0u; i < 3u; i++) { (void)s_append(SYSLOG_RUN, "c"); } ok = (syslog_count(SYSLOG_RUN) == 3u) && (syslog_count((syslog_id_t)7u) == 0u); - CHECK(ok, "TC-SYSLOG-050b count = filesize/64(O(1),不读盘内容);id 越界 -> 0"); + CHECK(ok, "TC-SYSLOG-050b count = filesize/64 (O(1), no content read); id out of range -> 0"); /* 051 窗口命中零 I/O */ reset_all(); @@ -1131,7 +1131,7 @@ static void tc_read(void) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == opens_after) /* 命中 -> 零 I/O */ && (g_ff_obs.calls[FF_FAKE_OP_READ] == reads_base + 1); } - CHECK(ok, "TC-SYSLOG-051 窗口命中 -> 第二次读**零 FatFs 调用**(UI 翻页不再逐条开合)"); + CHECK(ok, "TC-SYSLOG-051 window hit -> the second read makes **zero FatFs calls** (UI paging no longer opens per record)"); /* 052 写者代数失效 */ reset_all(); @@ -1142,7 +1142,7 @@ static void tc_read(void) (void)s_append(SYSLOG_RUN, "new"); /* 代数 +1 -> 窗口必须重填 */ ok = ok && (syslog_get_record(SYSLOG_RUN, 1u, ts, sizeof(ts), desc, sizeof(desc)) == 0) && (strcmp(desc, "new") == 0); - CHECK(ok, "TC-SYSLOG-052 写者一落地代数就变 -> 窗口重填,绝不返回陈旧内容"); + CHECK(ok, "TC-SYSLOG-052 once the writer lands the generation changes -> window refills, never returns stale content"); /* 053 跨窗口 */ reset_all(); @@ -1158,7 +1158,7 @@ static void tc_read(void) && (g_ff_obs.last_lseek_ofs == (FSIZE_t)(SYSLOG_RD_WINDOW * SYSLOG_REC_SIZE)) && (g_ff_obs.last_read_btr == SYSLOG_REC_SIZE); } - CHECK(ok, "TC-SYSLOG-053 idx=8 跨窗 -> 一次 open + lseek(8*64) + read(1 条)(窗口按 8 对齐)"); + CHECK(ok, "TC-SYSLOG-053 idx=8 crosses the window -> one open + lseek(8*64) + read(1 record) (window aligned to 8)"); /* 054 越界 */ reset_all(); @@ -1169,7 +1169,7 @@ static void tc_read(void) reset_all(); ready_only(); ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1); - CHECK(ok, "TC-SYSLOG-054 越界 idx / 空文件 -> -1(等锁前后各校验一次 count)"); + CHECK(ok, "TC-SYSLOG-054 out-of-range idx / empty file -> -1 (count checked before and after taking the lock)"); /* 055 读时卷忙 */ reset_all(); @@ -1181,7 +1181,7 @@ static void tc_read(void) lock_busy_next(1); ok = (syslog_get_record(SYSLOG_SELFTEST, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] >= 1) && (s_lock_seq_err == 0); - CHECK(ok, "TC-SYSLOG-055 读时卷忙 -> -1(读窗口 try 语义:uiTask 也绝不阻塞等锁,下轮刷新重读)"); + CHECK(ok, "TC-SYSLOG-055 busy volume while reading -> -1 (try semantics: uiTask never blocks for the lock, retry next refresh)"); /* 056 短读作废窗口 */ reset_all(); @@ -1193,7 +1193,7 @@ static void tc_read(void) ff_fake_inject_read_short(P_RUN, 1u, 1); ok = (s_read_raw(SYSLOG_RUN, 0u, (uint8_t *)desc) == -1) && (s_rd_win_cnt == 0u) && (s_rd_win_id == -1); - CHECK(ok, "TC-SYSLOG-056 短读 -> 窗口作废(半窗当有效命中 = UI 显示半条脏记录)"); + CHECK(ok, "TC-SYSLOG-056 short read -> window invalidated (a half window treated as valid = UI shows half a stale record)"); /* 057 get_record 守卫与截断 */ reset_all(); @@ -1213,7 +1213,7 @@ static void tc_read(void) * 整 28 字符而不是截断结果 —— 即"传刚够的缓冲会被拒、传更小的也被拒, * 从来没有截断"。登记为死分支,见文件头"本套照出的现状"。 */ && (strlen(desc) == (size_t)SYSLOG_DESC_MAX); - CHECK(ok, "TC-SYSLOG-057 守卫:ts_len<=19 或 desc_len<=28 一律 -1;29 才读得到整 28 字符"); + CHECK(ok, "TC-SYSLOG-057 guards: ts_len<=19 or desc_len<=28 always -1; only 29 reads the full 28 characters"); /* C 项整改(2026-10-24):容量不足一律拒绝,且**不得**碰调用方缓冲—— * 上面每次被拒的调用都用了同一对缓冲,哨兵还在就说明没写进去 */ reset_all(); @@ -1243,7 +1243,7 @@ static void tc_read(void) s_now = 1700000123u; (void)s_append(SYSLOG_RUN, "e"); ok = ok && (syslog_last_epoch(SYSLOG_RUN, &epoch) == 0) && (epoch == 1700000123u); - CHECK(ok, "TC-SYSLOG-058 last_epoch:无记录 -1;有记录解析 10 位 ASCII"); + CHECK(ok, "TC-SYSLOG-058 last_epoch: no record -> -1; with a record parse the 10 ASCII digits"); } /* ===================== E mounted / 上电重启分类 ===================== */ @@ -1253,7 +1253,7 @@ static void tc_mounted(void) uint8_t rec[SYSLOG_REC_SIZE]; int ok; - printf("-- TC-SYSLOG-060..064 挂载后处理(挂载幂等 + 上电/重启分类)--\n"); + printf("-- TC-SYSLOG-060..064 post-mount handling (idempotent mount + power/reboot classification) --\n"); /* 060 /log 不在 -> 建目录 + 写上电记录;第二次调用完全静默 */ reset_all(); @@ -1264,7 +1264,7 @@ static void tc_mounted(void) syslog_mcu_mounted(); ok = ok && (s_fs_ready == 1u) && (syslog_count(SYSLOG_RUN) == 1u); CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0) && (g_sys_err_count == 0), - "TC-SYSLOG-060a /log 已存在 -> 只写上电/重启记录一条"); + "TC-SYSLOG-060a /log exists -> write only the one power/reboot record"); /* 060b 幂等:再来一次既不建目录也不写第二条 */ reset_all(); s_fs_ready = 0u; @@ -1275,7 +1275,7 @@ static void tc_mounted(void) syslog_mcu_mounted(); /* 第二次:应完全静默 */ CHECK((s_fs_ready == 1u) && (syslog_count(SYSLOG_RUN) == cnt1) && (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == mkdirs1), - "TC-SYSLOG-060b 幂等再调用:不再建目录、不再写上电记录(2s 复查不刷屏)"); + "TC-SYSLOG-060b idempotent re-entry: no mkdir, no second power-on record (the 2s recheck must not flood)"); } (void)rec; @@ -1291,7 +1291,7 @@ static void tc_mounted(void) ok = ok && (g_sys_err_count == 1) && (s_lock_releases == 2) && (s_lock_seq_err == 0); syslog_mcu_mounted(); /* 注入耗尽 -> 成功 */ CHECK(ok && (s_fs_ready == 1u) && (s_mount_fail_reported == 0u), - "TC-SYSLOG-061 mkdir 失败:不就绪、留痕一次(防 2s 刷屏)、第三轮自愈并复位标志"); + "TC-SYSLOG-061 mkdir failure: not ready, trace once (no 2s flooding), third round self-heals and resets the flag"); /* 062 卷忙:本轮顺延,不置就绪、不碰盘 */ reset_all(); @@ -1300,7 +1300,7 @@ static void tc_mounted(void) syslog_mcu_mounted(); CHECK((s_fs_ready == 0u) && (g_ff_obs.calls[FF_FAKE_OP_STAT] == 0) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (s_lock_releases == 0), - "TC-SYSLOG-062 卷忙 -> 直接返回(try 失败不 release,下轮 2s 后再试)"); + "TC-SYSLOG-062 busy volume -> return at once (a failed try must not release; retry after the next 2s)"); /* 063 上电 / 重启分类:阈值 SYSLOG_REBOOT_WINDOW_S 两侧 + 墙钟无效 */ reset_all(); @@ -1320,7 +1320,7 @@ static void tc_mounted(void) syslog_mcu_mounted(); ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "reboot", 6u) == 0) && (rec[SYSLOG_DESC_OFFS + 6u] == '\0'); - CHECK(ok, "TC-SYSLOG-063a 末条 epoch 距 now 300s(<600) -> 描述落 \"reboot\""); + CHECK(ok, "TC-SYSLOG-063a last epoch 300s(<600) before now -> desc is \"reboot\""); reset_all(); s_fs_ready = 0u; @@ -1335,7 +1335,7 @@ static void tc_mounted(void) } syslog_mcu_mounted(); ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "power on", 8u) == 0); - CHECK(ok, "TC-SYSLOG-063b 间隔 >=600s -> \"power on\"(掉电超过 10 分钟算重新上电)"); + CHECK(ok, "TC-SYSLOG-063b gap >=600s -> \"power on\" (over ten minutes off counts as a fresh power-up)"); reset_all(); s_fs_ready = 0u; @@ -1348,14 +1348,14 @@ static void tc_mounted(void) } syslog_mcu_mounted(); ok = raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "power on", 8u) == 0); - CHECK(ok, "TC-SYSLOG-063c 墙钟无效(UINT32_MAX)-> 保守判 \"power on\",不拿假差值判重启"); + CHECK(ok, "TC-SYSLOG-063c wall clock invalid (UINT32_MAX) -> conservatively \"power on\", never judge a reboot from a fake delta"); /* 064 幂等靠 stat,不靠状态位:/log 已在就不再 mkdir */ reset_all(); s_fs_ready = 0u; syslog_mcu_mounted(); ok = (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0) && (g_ff_obs.calls[FF_FAKE_OP_STAT] >= 1); - CHECK(ok, "TC-SYSLOG-064 /log 已存在时靠 f_stat 短路,f_mkdir 一次都不调"); + CHECK(ok, "TC-SYSLOG-064 with /log present f_stat short-circuits, f_mkdir is never called"); } /* ===================== F 取证通道 sys_log_evidence ===================== */ @@ -1365,7 +1365,7 @@ static void tc_evidence(void) uint8_t raw[8u * SYSLOG_REC_SIZE]; int ok; - printf("-- TC-SYSLOG-070..074 sys_log_evidence(wdev.log 取证通道)--\n"); + printf("-- TC-SYSLOG-070..074 sys_log_evidence (wdev.log forensics channel) --\n"); /* 070 变长行 + 第二条 run.log */ reset_all(); @@ -1379,14 +1379,14 @@ static void tc_evidence(void) && raw_at(P_RUN, 0u, raw) && (memcmp(raw + SYSLOG_DESC_OFFS, "wd stall declared", 17u) == 0); } - CHECK(ok, "TC-SYSLOG-070 wdev.log 写变长行(带 CRLF),并补一条 run.log 独立证据"); + CHECK(ok, "TC-SYSLOG-070 wdev.log writes a variable-length line (with CRLF) plus a separate run.log evidence record"); /* 071 卷锁顺序:第二条写必须在释放之后(非递归锁,反了 = 自死锁) */ reset_all(); (void)sys_log_evidence("order probe"); CHECK((s_lock_waits == 2) && (s_lock_releases == 2) && (s_lock_seq_err == 0) && (s_lock_held == 0), - "TC-SYSLOG-071 取锁两次各放一次(在持锁状态下调 s_append 会永远拿不到锁)"); + "TC-SYSLOG-071 the lock is taken twice and released twice (calling s_append while holding it would deadlock)"); /* 072 >8192B 护栏 */ reset_all(); @@ -1394,7 +1394,7 @@ static void tc_evidence(void) ok = (sys_log_evidence("after rotate") == 0); CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (ff_fake_size(P_WDEV) > 0u) && (ff_fake_size(P_WDEV) < 200u), - "TC-SYSLOG-072 超过 8KB -> 先 unlink 再写(长跑不被无界撑爆)"); + "TC-SYSLOG-072 beyond 8KB -> unlink first then write (a long run must not grow without bound)"); /* 073 卷忙 */ reset_all(); @@ -1402,14 +1402,14 @@ static void tc_evidence(void) ok = (sys_log_evidence("busy probe") == -1); CHECK(ok && (ff_fake_exists(P_WDEV) == 0) && (syslog_count(SYSLOG_RUN) == 0u) && (s_lock_releases == 0) && (s_lock_seq_err == 0), - "TC-SYSLOG-073 卷忙 -> -1 且**两条写都没发生**(喂狗点绝不阻塞等锁)"); + "TC-SYSLOG-073 busy volume -> -1 and **neither write happened** (feed point never blocks for the lock)"); /* 074 wdev 写失败:返回 -1,但第二条仍写("独立证据"语义) */ reset_all(); ff_fake_inject_rc(P_WDEV, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1); ok = (sys_log_evidence("second write still") == -1); CHECK(ok && (syslog_count(SYSLOG_RUN) == 1u) && (g_sys_err_count == 0), - "TC-SYSLOG-074 wdev 写失败不阻断第二条 run.log(多一条独立证据的原始意图)"); + "TC-SYSLOG-074 a wdev write failure does not block the second run.log record (the intent of extra evidence)"); } /* ===================== G 陷阱二:名字/参数类错误码严禁升级成卷操作 ===================== */ @@ -1425,7 +1425,7 @@ static void tc_name_rules(void) int ok; uint32_t i; - printf("-- TC-SYSLOG-080..082 陷阱二分档与「绝不升级」 --\n"); + printf("-- TC-SYSLOG-080..082 name class split and never escalate --\n"); /* 080 短名违规**分档**:每条都落到 FR_INVALID_NAME;':' 落到 FR_INVALID_DRIVE */ reset_all(); @@ -1442,7 +1442,7 @@ static void tc_name_rules(void) CHECK((fr_body == FR_INVALID_NAME) && (fr_ext == FR_INVALID_NAME) && (fr_bad == FR_INVALID_NAME) && (fr_dot == FR_INVALID_NAME) && (fr_colon == FR_INVALID_DRIVE), - "TC-SYSLOG-080 分档:体>8 / 扩展>3 / 非法字符 / 点开头 -> FR_INVALID_NAME;':' -> FR_INVALID_DRIVE"); + "TC-SYSLOG-080 classes: body>8 / ext>3 / bad char / leading dot -> FR_INVALID_NAME; ':' -> FR_INVALID_DRIVE"); /* 081 生产实际用的每个名字都必须过严格 8.3(改名 = 静默写失败的第一道防线) */ reset_all(); @@ -1452,7 +1452,7 @@ static void tc_name_rules(void) ok = ok && (f_open(&probe, P_WDEV, FA_WRITE | FA_OPEN_APPEND) == FR_OK) && (f_close(&probe) == FR_OK); ok = ok && (f_stat("/log", NULL) == FR_OK); - CHECK(ok, "TC-SYSLOG-081 生产路径 /log、run.log、selftest.log、wdev.log 全部 8.3 合规"); + CHECK(ok, "TC-SYSLOG-081 production paths /log, run.log, selftest.log, wdev.log are all valid 8.3 names"); /* 082 名字类错误码不得升级成卷操作:反复失败也只动自己的文件 */ reset_all(); @@ -1485,7 +1485,7 @@ static void tc_extra_branches(void) uint32_t i; int ok; - printf("-- TC-SYSLOG-090..100 补齐分支 --\n"); + printf("-- TC-SYSLOG-090..100 remaining branch coverage --\n"); /* 090 批量路径的 L1 自愈:open 失败一次 -> unlink + 重开 -> 整批照常落盘 * (这条钉的是 s_append_many 里那段"和单条同一份自愈表";漏了它, @@ -1502,7 +1502,7 @@ static void tc_extra_branches(void) CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1) && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3) /* 单条 1 + 批量首开 1 + 自愈重开 1 */ && (ff_fake_size(P_RUN) == 2u * SYSLOG_REC_SIZE) && (g_sys_err_count == 0), - "TC-SYSLOG-090 批量 open 失败一次 -> unlink 重开 -> 整批 2 条落全、不留痕"); + "TC-SYSLOG-090 one batch open failure -> unlink and reopen -> whole batch of 2 lands, no trace"); /* 091 批量 open 永久失败 -> -1(走的是"打开失败"那条 else,不是写失败) */ reset_all(); @@ -1514,7 +1514,7 @@ static void tc_extra_branches(void) } CHECK(ok && (ff_fake_exists(P_RUN) == 0) && (g_sys_err_count == 1) && (s_lock_releases == 1) && (s_lock_seq_err == 0), - "TC-SYSLOG-091 批量打开持续失败 -> -1 + 留痕一次 + 已释放卷锁"); + "TC-SYSLOG-091 batch open keeps failing -> -1 + one trace + the volume lock released"); /* 092 取锁后二次校验:等锁期间写者可能已把文件删掉(容量轮转/取证重建) * -> 释放锁再返回 -1,绝不能拿锁前的旧 total 去 lseek */ @@ -1527,7 +1527,7 @@ static void tc_extra_branches(void) ok = ok && (s_read_raw(SYSLOG_RUN, 0u, raw) == -1) && (s_lock_releases == 2) && (s_lock_seq_err == 0); CHECK(ok && (s_rd_win_cnt == 0u), - "TC-SYSLOG-092 锁后二次 count 失败 -> 放锁返回 -1(漏放 = FTP 永久等不到卷)"); + "TC-SYSLOG-092 second count after the lock fails -> release the lock and return -1 (leak = FTP waits forever)"); /* 093 读时 f_open 失败 -> 放锁返回 -1(另一条提前退出路径) */ reset_all(); @@ -1540,7 +1540,7 @@ static void tc_extra_branches(void) && (s_lock_releases == 2) && (s_lock_seq_err == 0); /* 第二次读(注入耗尽)应成功,证明窗口没被留在半初始化状态 */ ok = ok && (s_read_raw(SYSLOG_RUN, 0u, raw) == 0) && (s_rd_win_cnt == 1u); - CHECK(ok, "TC-SYSLOG-093 读打开失败 -> 放锁 -1;下轮恢复后窗口正常重填"); + CHECK(ok, "TC-SYSLOG-093 read open failure -> release lock and -1; next round refills the window after recovery"); /* 094 墙钟未建立时 evidence 用占位时标(不得写半截或未初始化缓冲) */ reset_all(); @@ -1550,7 +1550,7 @@ static void tc_extra_branches(void) (void)ff_fake_fetch(P_WDEV, raw, 0u, SYSLOG_TS_LEN); ok = (memcmp(raw, "1970-01-01 08:00:00", SYSLOG_TS_LEN) == 0); } - CHECK(ok, "TC-SYSLOG-094 无墙钟 -> wdev.log 落占位时标(取证行也必须可解析)"); + CHECK(ok, "TC-SYSLOG-094 no wall clock -> wdev.log gets a placeholder timestamp (forensic lines must stay parseable)"); /* 095 wdev 短写:FR_OK + 字节数不足 -> -1,但第二条 run.log 照写 */ reset_all(); @@ -1558,7 +1558,7 @@ static void tc_extra_branches(void) ok = (sys_log_evidence("short line") == -1); CHECK(ok && (ff_fake_size(P_WDEV) == 3u) && (syslog_count(SYSLOG_RUN) == 1u) && (g_sys_err_count == 0), - "TC-SYSLOG-095 wdev 短写判失败返回 -1,第二条独立证据仍落 run.log"); + "TC-SYSLOG-095 a wdev short write fails with -1 while the second run.log evidence record still lands"); /* 096 队列创建失败:条目滞留 preboot、留痕一次、下轮建成后排空 */ reset_all(); @@ -1573,7 +1573,7 @@ static void tc_extra_branches(void) syslog_drain(); CHECK(ok && (s_queue != NULL) && (syslog_count(SYSLOG_RUN) == 1u) && (s_preboot_cnt == 0u) && (s_soe_drain_calls == 1), - "TC-SYSLOG-096 队列没建成 -> 一条都不排(不静默丢),堆恢复后排空并接上 SOE"); + "TC-SYSLOG-096 queue not created -> nothing is drained (no silent loss); after the heap recovers it empties into SOE"); /* 097 换文件那条来自队列 -> 原样回投队列(不是 preboot 下标回退) * 队列里 RUN/SELFTEST/RUN 交替:三条必须全落且各自文件内顺序不乱 */ @@ -1594,7 +1594,7 @@ static void tc_extra_branches(void) && (s_q_cnt == 0u); } CHECK(ok && (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 3), - "TC-SYSLOG-097 队列侧换文件回投:三条全落、各文件内顺序不乱(回投不是丢弃)"); + "TC-SYSLOG-097 queue-side file change requeues: all three land, order inside each file kept (requeue is not a drop)"); /* 098【C 项已修 2026-10-24】原来的 "if (dl >= desc_len) dl = desc_len - 1" * 是永不可达的死分支(守卫已挡掉 desc_len <= SYSLOG_DESC_MAX,而 dl 恒 * <= SYSLOG_DESC_MAX),已从生产码删除。本条改为正向钉住"读侧不再截断": @@ -1617,7 +1617,7 @@ static void tc_extra_branches(void) && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0) && (sys_log_evidence(NULL) == -1); CHECK(ok && (s_lock_seq_err == 0), - "TC-SYSLOG-099 p_rec / p_desc 空指针守卫(守卫顺序在取锁之前,不放锁)"); + "TC-SYSLOG-099 p_rec / p_desc NULL guards (guards run before taking the lock, so nothing is released)"); /* 100 last_epoch:有记录但读失败 -> -1 且把 *p_epoch 清 0 * (调用方 syslog_mcu_mounted 直接拿这个值判上电/重启,留旧值就会误判) */ @@ -1629,7 +1629,7 @@ static void tc_extra_branches(void) epoch = 0xA5A5A5A5u; ff_fake_inject_rc(P_RUN, FF_FAKE_OP_OPEN, FR_DISK_ERR, 1); ok = (syslog_last_epoch(SYSLOG_RUN, &epoch) == -1) && (epoch == 0u); - CHECK(ok, "TC-SYSLOG-100 last_epoch 读失败 -> -1 且 *p_epoch 清零(不留调用方旧值)"); + CHECK(ok, "TC-SYSLOG-100 last_epoch read failure -> -1 and *p_epoch cleared (never leave the caller's old value)"); (void)i; } diff --git a/test/host/task_prof/task_prof_test.c b/test/host/task_prof/task_prof_test.c index b4a34215..4cee2e76 100644 --- a/test/host/task_prof/task_prof_test.c +++ b/test/host/task_prof/task_prof_test.c @@ -178,26 +178,26 @@ static void tc_sample_gate(void) CHECK((s_prof[TASK_PROF_UI_PERIOD].count == 1u) && (s_prof[TASK_PROF_UI_PERIOD].last == 500u) && (s_prof[TASK_PROF_UI_PERIOD].max == 500u), - "TC-TASKPROF-001 有效样本记 count/last/max"); + "TC-TASKPROF-001 valid sample records count/last/max"); task_prof_sample(TASK_PROF_UI_PERIOD, 300u); CHECK((s_prof[TASK_PROF_UI_PERIOD].max == 500u) && (s_prof[TASK_PROF_UI_PERIOD].last == 300u) && (s_prof[TASK_PROF_UI_PERIOD].count == 2u), - "TC-TASKPROF-002 更小样本不改 max"); + "TC-TASKPROF-002 smaller sample does not change max"); reset_all(); task_prof_sample(TASK_PROF_UI_PERIOD, 0u); CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) && (s_prof[TASK_PROF_UI_PERIOD].count == 0u) && (s_prof[TASK_PROF_UI_PERIOD].max == 0u), - "TC-TASKPROF-003 cyc=0 只累加 glitch(DWT 复位防线)"); + "TC-TASKPROF-003 cyc=0 only accumulates glitch (DWT reset defence)"); reset_all(); task_prof_sample(TASK_PROF_UI_PERIOD, SystemCoreClock * 10u); CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) && (s_prof[TASK_PROF_UI_PERIOD].count == 0u), - "TC-TASKPROF-004 超上界样本只累加 glitch"); + "TC-TASKPROF-004 over-upper-bound sample only accumulates glitch"); reset_all(); task_prof_sample((task_prof_id_t)TASK_PROF_ID_COUNT, 100u); @@ -205,7 +205,7 @@ static void tc_sample_gate(void) task_prof_enter((task_prof_id_t)TASK_PROF_ID_COUNT); task_prof_exit((task_prof_id_t)TASK_PROF_ID_COUNT); CHECK((s_prof[0].count == 0u) && (s_prof[TASK_PROF_ID_COUNT - 1].count == 0u), - "TC-TASKPROF-005 id 越界安全返回,不碰任何槽"); + "TC-TASKPROF-005 out-of-range id returns safely, touches no slot"); } /* ===================== 时基与 enter/exit 配对 ===================== */ @@ -219,27 +219,27 @@ static void tc_enter_exit(void) CHECK(((g_coredebug.DEMCR & CoreDebug_DEMCR_TRCENA_Msk) != 0u) && ((g_dwt.CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0u) && (s_dwt_ok == 1u), - "TC-TASKPROF-006 首次取时基即惰性使能 DWT"); + "TC-TASKPROF-006 first timebase read lazily enables DWT"); reset_all(); set_cyc(2000u); task_prof_enter(TASK_PROF_UI_WORK); CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 1u) && (s_prof[TASK_PROF_UI_WORK].t_start == 2000u), - "TC-TASKPROF-007 enter 置 in_seg 并记起点"); + "TC-TASKPROF-007 enter sets in_seg and records the start point"); set_cyc(2640u); task_prof_exit(TASK_PROF_UI_WORK); CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 0u) && (s_prof[TASK_PROF_UI_WORK].last == 640u) && (s_prof[TASK_PROF_UI_WORK].count == 1u), - "TC-TASKPROF-008 exit 结算 Δ 并清 in_seg"); + "TC-TASKPROF-008 exit settles the delta and clears in_seg"); reset_all(); task_prof_exit(TASK_PROF_ADC_BEAT); CHECK((s_prof[TASK_PROF_ADC_BEAT].count == 0u) && (s_prof[TASK_PROF_ADC_BEAT].glitch == 0u), - "TC-TASKPROF-009 未 enter 直接 exit 不记账(防 0 起点假超大样本)"); + "TC-TASKPROF-009 exit without enter records nothing (guards fake huge sample from 0 start)"); reset_all(); set_cyc(100u); @@ -247,7 +247,7 @@ static void tc_enter_exit(void) set_cyc(150u); task_prof_exit(TASK_PROF_NET_POLL); task_prof_exit(TASK_PROF_NET_POLL); - CHECK(s_prof[TASK_PROF_NET_POLL].count == 1u, "TC-TASKPROF-010 重复 exit 不重复记账"); + CHECK(s_prof[TASK_PROF_NET_POLL].count == 1u, "TC-TASKPROF-010 repeated exit does not double count"); } /* ===================== 上报节流与通道 ===================== */ @@ -258,26 +258,26 @@ static void tc_report_throttle(void) reset_all(); g_tick = 4999u; task_prof_report(); - CHECK((g_line_calls == 0) && (g_raw_calls == 0), "TC-TASKPROF-011 首 5 s 前不发"); + CHECK((g_line_calls == 0) && (g_raw_calls == 0), "TC-TASKPROF-011 nothing sent before the first 5 s"); g_tick = 5000u; task_prof_report(); CHECK((g_line_calls == 2) && (g_raw_calls == 0), - "TC-TASKPROF-012 到点发两行走环形缓冲"); + "TC-TASKPROF-012 on schedule: sends two lines via the ring buffer"); g_tick = 6999u; task_prof_report(); - CHECK(g_line_calls == 2, "TC-TASKPROF-013 不足 2 s 不发"); + CHECK(g_line_calls == 2, "TC-TASKPROF-013 less than 2 s apart sends nothing"); g_tick = 7000u; task_prof_report(); - CHECK(g_line_calls == 4, "TC-TASKPROF-014 满 2 s 再发两行"); + CHECK(g_line_calls == 4, "TC-TASKPROF-014 after a full 2 s sends two lines again"); g_line_calls = 0; g_raw_calls = 0; task_prof_report_raw(); CHECK((g_raw_calls == 2) && (g_line_calls == 0), - "TC-TASKPROF-015 report_raw 无视节流且走裸发通道"); + "TC-TASKPROF-015 report_raw ignores throttling and uses the raw channel"); } /* ===================== 行内容与换算 ===================== */ @@ -293,11 +293,11 @@ static void tc_line_format(void) CHECK((g_line_calls == 2) && (strncmp(g_line[0], "[PROF] ", 7) == 0) && (strncmp(g_line[1], "[PROF2] ", 8) == 0), - "TC-TASKPROF-016 两行行首分别是 [PROF] / [PROF2]"); + "TC-TASKPROF-016 the two lines start with [PROF] / [PROF2] respectively"); CHECK((strstr(g_line[0], "\r\n") != NULL) && (strstr(g_line[1], "\r\n") != NULL), - "TC-TASKPROF-016b 两行都以 CRLF 结尾"); - CHECK(field_of(0, "uip=") == 0L, "TC-TASKPROF-016c 未采样的槽打印 0"); + "TC-TASKPROF-016b both lines end with CRLF"); + CHECK(field_of(0, "uip=") == 0L, "TC-TASKPROF-016c an unsampled slot prints 0"); /* 017:打印的是**微秒**,分母是 SystemCoreClock —— 断言落在打印字段上 */ reset_all(); @@ -306,14 +306,14 @@ static void tc_line_format(void) g_tick = 5000u; task_prof_report(); CHECK(field_of(0, "uip=") == 10L, - "TC-TASKPROF-017 168 MHz:1680 周期的 max 打印成 uip=10"); + "TC-TASKPROF-017 168 MHz: max of 1680 cycles prints as uip=10"); CHECK(field_of(0, "adcm=") == 20L, - "TC-TASKPROF-017b 同频:3360 周期的 max 打印成 adcm=20"); + "TC-TASKPROF-017b same clock: max of 3360 cycles prints as adcm=20"); SystemCoreClock = 84000000u; /* 改时钟树而忘了同步 → 整表偏移,这里必须显式跟着变 */ g_tick = 7000u; task_prof_report(); CHECK(field_of(2, "uip=") == 20L, - "TC-TASKPROF-017c 84 MHz 下同一批周期数打印成 20"); + "TC-TASKPROF-017c at 84 MHz the same cycle counts print as 20"); SystemCoreClock = 168000000u; /* 018:gl= 是**所有槽**的 glitch 汇总,不是当前槽 */ @@ -323,7 +323,7 @@ static void tc_line_format(void) task_prof_sample(TASK_PROF_NET_POLL, 0u); g_tick = 5000u; task_prof_report(); - CHECK(field_of(0, "gl=") == 3L, "TC-TASKPROF-018 gl= 汇总所有槽的 glitch"); + CHECK(field_of(0, "gl=") == 3L, "TC-TASKPROF-018 gl= sums the glitch of all slots"); /* 019:段未结算时,"此刻已在段内多久"要看得见(uipn / netn 两个字段) */ /* 字段口径 = 上报时刻的 CYCCNT 减去各自的进段时刻(**不是**样本值): @@ -338,13 +338,13 @@ static void tc_line_format(void) g_tick = 5000u; task_prof_report(); CHECK(field_of(0, "uipn=") == 301L, - "TC-TASKPROF-019 UI_PERIOD 段内:uipn= 上报此刻已停留的 us"); + "TC-TASKPROF-019 inside UI_PERIOD segment: uip= reports the us elapsed so far"); CHECK(field_of(1, "netn=") == 248L, - "TC-TASKPROF-019b NET_POLL 段内:[PROF2] 的 netn= 同步可见"); + "TC-TASKPROF-019b inside NET_POLL segment: netn= of [PROF2] is visible too"); CHECK(field_of(0, "adcn=") == 0L, - "TC-TASKPROF-019c 未进段的 ADC_BEAT 打印 0(不串用别的槽)"); + "TC-TASKPROF-019c ADC_BEAT not in segment prints 0 (no cross-slot leakage)"); CHECK(s_prof[TASK_PROF_UI_PERIOD].in_seg == 1u, - "TC-TASKPROF-019d 上报不消费段状态(只观测)"); + "TC-TASKPROF-019d reporting does not consume segment state (observe only)"); /* 020:count 是累计量,两轮上报之间不能清零 */ reset_all(); @@ -355,7 +355,7 @@ static void tc_line_format(void) task_prof_report(); CHECK((s_prof[TASK_PROF_UI_WORK].count == 1u) && (field_of(2, "uic=") == 0L) && (field_of(0, "uic=") == 0L), - "TC-TASKPROF-020 上报不清零(UI_WORK 不在 uic= 字段,仍为累计 1)"); + "TC-TASKPROF-020 reporting does not clear counters (UI_WORK not in uic= field, still cumulative 1)"); } diff --git a/test/host/time_sync/time_sync_test.c b/test/host/time_sync/time_sync_test.c index 32a3fbfb..e4abe860 100644 --- a/test/host/time_sync/time_sync_test.c +++ b/test/host/time_sync/time_sync_test.c @@ -418,31 +418,31 @@ static int delays_all(uint32_t v, int n) { static void tc_delay_fed(void) { fakes_reset(); time_sync_delay_fed(0U); - CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TIMESYNC-001 delay_fed(0) 不睡眠不喂狗"); + CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TIMESYNC-001 delay_fed(0) neither sleeps nor beats"); fakes_reset(); time_sync_delay_fed(50U); CHECK((s_beat_calls == 1) && (s_delay_calls == 1) && (s_delays[0] == 50U), - "TC-TIMESYNC-002 delay_fed(50) = 1 片 50ms + 1 次心跳"); + "TC-TIMESYNC-002 delay_fed(50) = one 50 ms slice + 1 beat"); fakes_reset(); time_sync_delay_fed(120U); - CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TIMESYNC-003 delay_fed(120) 切成 3 片"); + CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TIMESYNC-003 delay_fed(120) splits into 3 slices"); CHECK((s_delays[0] == 50U) && (s_delays[1] == 50U) && (s_delays[2] == 20U), - "TC-TIMESYNC-003b 片长 50/50/20(末片为余数)"); + "TC-TIMESYNC-003b slice lengths 50/50/20 (last slice is the remainder)"); fakes_reset(); time_sync_delay_fed(2000U); CHECK((s_delay_calls == 40) && (s_beat_calls == 40) && delays_all(50U, 40), - "TC-TIMESYNC-004 delay_fed(2000) = 40 片 × 50ms,每片喂一次心跳"); + "TC-TIMESYNC-004 delay_fed(2000) = 40 slices x 50 ms, one beat per slice"); fakes_reset(); time_sync_delay_fed(49U); - CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TIMESYNC-005 不足一片(49ms)单片处理"); + CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TIMESYNC-005 less than one slice (49 ms) handled as a single slice"); fakes_reset(); time_sync_delay_fed(30000U); - CHECK(s_delay_calls == 600, "TC-TIMESYNC-006 总预算 30s = 600 片(片长上限恒 50ms)"); + CHECK(s_delay_calls == 600, "TC-TIMESYNC-006 total budget 30 s = 600 slices (slice cap is always 50 ms)"); } /* ===================== TC-TIMESYNC-010..015:open_listener ===================== */ @@ -451,13 +451,13 @@ static void tc_open_listener(void) { fakes_reset(); { int s = time_sync_open_listener(); - CHECK(s == 5, "TC-TIMESYNC-010 open_listener 成功返回 socket 描述符"); + CHECK(s == 5, "TC-TIMESYNC-010 open_listener success returns the socket descriptor"); CHECK((s_socket_calls == 1) && (s_bind_calls == 1) && (s_listen_calls == 1), - "TC-TIMESYNC-010b socket / bind / listen 各一次"); - CHECK(s_htons_last == (uint16_t)TIME_SYNC_PORT, "TC-TIMESYNC-010c 端口经 net_htons(8888)"); - CHECK(s_bind_port == net_htons((uint16_t)TIME_SYNC_PORT), "TC-TIMESYNC-010d sin_port 为网络序"); + "TC-TIMESYNC-010b socket / bind / listen called once each"); + CHECK(s_htons_last == (uint16_t)TIME_SYNC_PORT, "TC-TIMESYNC-010c port goes through net_htons(8888)"); + CHECK(s_bind_port == net_htons((uint16_t)TIME_SYNC_PORT), "TC-TIMESYNC-010d sin_port is in network byte order"); CHECK(s_bind_family == (uint16_t)NET_AF_INET, "TC-TIMESYNC-010e sin_family = NET_AF_INET"); - CHECK(s_delay_calls == 0, "TC-TIMESYNC-010f 成功路径无退避延时"); + CHECK(s_delay_calls == 0, "TC-TIMESYNC-010f no backoff delay on the success path"); } fakes_reset(); @@ -465,10 +465,10 @@ static void tc_open_listener(void) { s_bind_seq_n = 1; { int s = time_sync_open_listener(); - CHECK(s == 5, "TC-TIMESYNC-011 bind 失败一次后重试成功"); - CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-011b bind 失败关闭该 socket"); - CHECK(s_delay_calls == 40, "TC-TIMESYNC-011c bind 失败退避 2000ms(40 片)"); - CHECK(g_tstest_err_count == 1, "TC-TIMESYNC-011d bind 失败留痕 DBG_ERROR"); + CHECK(s == 5, "TC-TIMESYNC-011 bind succeeds on retry after one failure"); + CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-011b bind failure closes that socket"); + CHECK(s_delay_calls == 40, "TC-TIMESYNC-011c bind failure backs off 2000 ms (40 slices)"); + CHECK(g_tstest_err_count == 1, "TC-TIMESYNC-011d bind failure leaves a DBG_ERROR trace"); } fakes_reset(); @@ -476,19 +476,19 @@ static void tc_open_listener(void) { s_listen_seq_n = 1; { int s = time_sync_open_listener(); - CHECK(s == 5, "TC-TIMESYNC-012 listen 失败一次后重试成功"); - CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-012b listen 失败关闭该 socket"); - CHECK(s_delay_calls == 40, "TC-TIMESYNC-012c listen 失败退避 2000ms"); + CHECK(s == 5, "TC-TIMESYNC-012 listen succeeds on retry after one failure"); + CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-012b listen failure closes that socket"); + CHECK(s_delay_calls == 40, "TC-TIMESYNC-012c listen failure backs off 2000 ms"); } fakes_reset(); s_socket_default = -1; { int s = time_sync_open_listener(); - CHECK(s == -1, "TC-TIMESYNC-013 socket 连续失败 -> 返回 -1"); - CHECK(s_socket_calls == 5, "TC-TIMESYNC-013b 重试上限 5 次"); - CHECK(g_tstest_err_count == 5, "TC-TIMESYNC-013c 每次失败各留痕一条(5 条 DBG_ERROR)"); - CHECK(s_delay_calls == 50, "TC-TIMESYNC-013d 每次失败退避 500ms(5 × 10 片)"); + CHECK(s == -1, "TC-TIMESYNC-013 socket keeps failing -> returns -1"); + CHECK(s_socket_calls == 5, "TC-TIMESYNC-013b retry cap is 5 attempts"); + CHECK(g_tstest_err_count == 5, "TC-TIMESYNC-013c every failure leaves its own trace (5 DBG_ERROR lines)"); + CHECK(s_delay_calls == 50, "TC-TIMESYNC-013d every failure backs off 500 ms (5 x 10 slices)"); } fakes_reset(); @@ -498,16 +498,16 @@ static void tc_open_listener(void) { s_socket_seq_n = 3; { int s = time_sync_open_listener(); - CHECK(s == 9, "TC-TIMESYNC-014 第 3 次重试成功并返回新描述符"); - CHECK(s_socket_calls == 3, "TC-TIMESYNC-014b 恰好尝试 3 次"); - CHECK(s_delay_calls == 20, "TC-TIMESYNC-014c 两次失败退避 500ms × 2"); + CHECK(s == 9, "TC-TIMESYNC-014 3rd retry succeeds and returns the new descriptor"); + CHECK(s_socket_calls == 3, "TC-TIMESYNC-014b exactly 3 attempts"); + CHECK(s_delay_calls == 20, "TC-TIMESYNC-014c two failures back off 500 ms x 2"); } fakes_reset(); s_socket_default = -1; (void)time_sync_open_listener(); - CHECK(s_beat_calls == 55, "TC-TIMESYNC-015 每轮重试(5) + 每片延时(50) 各喂一次心跳"); - CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-015b 心跳位恒为 TASK_WD_TIMESYNC"); + CHECK(s_beat_calls == 55, "TC-TIMESYNC-015 every retry round (5) + every delay slice (50) beats once"); + CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-015b the beat bit is always TASK_WD_TIMESYNC"); } /* ===================== TC-TIMESYNC-020..029:handle_conn ===================== @@ -526,13 +526,13 @@ static void tc_handle_conn(void) { s_recv_seq_n = 1; s_tick = 100U; time_sync_handle_conn(7); - CHECK(s_set_unix_calls == 1, "TC-TIMESYNC-020 TIME 包一次收齐 -> 更新墙钟一次"); - CHECK(s_set_unix_last == 0x12345678u, "TC-TIMESYNC-020b epoch 按大端解析"); - CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TIMESYNC-020c 首次请求 16 字节"); - CHECK(s_recv_to_log[0] == 50U, "TC-TIMESYNC-020d 单次等待片长 = 50ms"); - CHECK(s_close_calls == 0, "TC-TIMESYNC-020e 处理完**不**关闭连接(close 交外层 conn!=ls 分支)"); - CHECK(s_drop_calls == 0, "TC-TIMESYNC-020g 成功会话不拆(drop 只用于放弃路径)"); - CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-020f 留痕 wall clock updated"); + CHECK(s_set_unix_calls == 1, "TC-TIMESYNC-020 TIME packet in one shot -> wall clock updated once"); + CHECK(s_set_unix_last == 0x12345678u, "TC-TIMESYNC-020b epoch parsed big-endian"); + CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TIMESYNC-020c the first request asks for 16 bytes"); + CHECK(s_recv_to_log[0] == 50U, "TC-TIMESYNC-020d single wait slice length = 50 ms"); + CHECK(s_close_calls == 0, "TC-TIMESYNC-020e after handling, the connection is **not** closed (close is the outer conn!=ls branch job)"); + CHECK(s_drop_calls == 0, "TC-TIMESYNC-020g a successful session is not torn down (drop is only for give-up paths)"); + CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-020f leaves the wall clock updated trace"); /* 2) 分段送达:4 + 4 */ fakes_reset(); @@ -541,11 +541,11 @@ static void tc_handle_conn(void) { recv_set(1, 4, pkt + 4, 4, 10U); s_recv_seq_n = 2; time_sync_handle_conn(3); - CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 10u), "TC-TIMESYNC-021 分段送达累积后解析正确"); - CHECK(s_recv_calls == 2, "TC-TIMESYNC-021b 读了两次"); + CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 10u), "TC-TIMESYNC-021 pieces accumulate and then parse correctly"); + CHECK(s_recv_calls == 2, "TC-TIMESYNC-021b read twice"); CHECK((s_recv_len_log[0] == 16) && (s_recv_len_log[1] == 12), - "TC-TIMESYNC-021c 第二次只请求剩余 12 字节"); - CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), "TC-TIMESYNC-021d 每片 50ms(预算充裕)"); + "TC-TIMESYNC-021c the second request asks only for the remaining 12 bytes"); + CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), "TC-TIMESYNC-021d 50 ms per slice (budget is ample)"); /* 3) 16 字节一次到达(只取前 8) */ fakes_reset(); @@ -553,8 +553,8 @@ static void tc_handle_conn(void) { recv_set(0, 16, pkt, 16, 0U); s_recv_seq_n = 1; time_sync_handle_conn(4); - CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 5u), "TC-TIMESYNC-022 超长包只取前 8 字节"); - CHECK(s_recv_calls == 1, "TC-TIMESYNC-022b 一次读够即停"); + CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 5u), "TC-TIMESYNC-022 an over-long packet only takes the first 8 bytes"); + CHECK(s_recv_calls == 1, "TC-TIMESYNC-022b stops as soon as one read is enough"); /* 4) 前缀不是 TIME */ fakes_reset(); @@ -569,23 +569,23 @@ static void tc_handle_conn(void) { recv_set(0, 8, pkt, 8, 0U); s_recv_seq_n = 1; time_sync_handle_conn(4); - CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-023 非 TIME 前缀不更新墙钟"); - CHECK(s_close_calls == 0, "TC-TIMESYNC-023b 非 TIME 前缀也不关闭连接"); + CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-023 a non-TIME prefix does not update the wall clock"); + CHECK(s_close_calls == 0, "TC-TIMESYNC-023b a non-TIME prefix also does not close the connection"); /* 5) 对端关闭(EOF) */ fakes_reset(); recv_set(0, 0, NULL, 0, 0U); s_recv_seq_n = 1; time_sync_handle_conn(6); - CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TIMESYNC-024 对端关闭(EOF)立即退出不解析"); - CHECK(s_close_calls == 0, "TC-TIMESYNC-024b EOF 也不关闭连接(net 层 auto_relisten 收口监听)"); + CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TIMESYNC-024 peer close (EOF) exits immediately without parsing"); + CHECK(s_close_calls == 0, "TC-TIMESYNC-024b EOF also does not close the connection (net-layer auto_relisten re-arms the listener)"); /* 6) 真错误 */ fakes_reset(); recv_set(0, -1, NULL, 0, 0U); s_recv_seq_n = 1; time_sync_handle_conn(6); - CHECK((s_recv_calls == 1) && (s_set_unix_calls == 0), "TC-TIMESYNC-025 真错误立即退出不解析"); + CHECK((s_recv_calls == 1) && (s_set_unix_calls == 0), "TC-TIMESYNC-025 a real error exits immediately without parsing"); /* 7) 4 字节后 EOF */ fakes_reset(); @@ -594,27 +594,27 @@ static void tc_handle_conn(void) { recv_set(1, 0, NULL, 0, 0U); s_recv_seq_n = 2; time_sync_handle_conn(6); - CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-026 不足 8 字节不解析"); - CHECK(s_recv_calls == 2, "TC-TIMESYNC-026b 继续等待后续字节"); + CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-026 fewer than 8 bytes are not parsed"); + CHECK(s_recv_calls == 2, "TC-TIMESYNC-026b keeps waiting for the remaining bytes"); /* 8) 30s 预算耗尽 */ fakes_reset(); recv_set(0, -2, NULL, 0, 30000U); s_recv_seq_n = 1; time_sync_handle_conn(6); - CHECK(s_recv_calls == 1, "TC-TIMESYNC-027 预算耗尽后不再读(只读 1 次)"); - CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-027b 留痕 no TIME packet in 30s, drop conn"); - CHECK(s_close_calls == 0, "TC-TIMESYNC-027c 预算耗尽也不关闭连接"); - CHECK(s_drop_calls == 1, "TC-TIMESYNC-027d 预算耗尽主动拆会话(E-02 收口:net_drop_conn 拆连接保监听)"); + CHECK(s_recv_calls == 1, "TC-TIMESYNC-027 no more reads once the budget is exhausted (only 1 read)"); + CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-027b leaves the no TIME packet in 30s, drop conn trace"); + CHECK(s_close_calls == 0, "TC-TIMESYNC-027c budget exhausted still does not close the connection"); + CHECK(s_drop_calls == 1, "TC-TIMESYNC-027d budget exhausted drops the session on purpose (E-02 wrap-up: net_drop_conn kills the connection, keeps the listener)"); /* 9) 剩余预算不足一片 */ fakes_reset(); recv_set(0, -2, NULL, 0, 29980U); s_recv_seq_n = 1; time_sync_handle_conn(6); - CHECK(s_recv_calls == 2, "TC-TIMESYNC-028 两次等待后结束"); + CHECK(s_recv_calls == 2, "TC-TIMESYNC-028 ends after two waits"); CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 20U), - "TC-TIMESYNC-028b 第二次片长 = 剩余 20ms"); + "TC-TIMESYNC-028b second slice length = the remaining 20 ms"); /* 10) 32 位 tick 回绕 */ fakes_reset(); @@ -623,7 +623,7 @@ static void tc_handle_conn(void) { s_recv_seq_n = 1; time_sync_handle_conn(6); CHECK((s_recv_calls == 2) && (s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), - "TC-TIMESYNC-029 32 位 tick 回绕后预算计算仍正确"); + "TC-TIMESYNC-029 after a 32-bit tick wraparound the budget math is still correct"); } /* ===================== TC-TIMESYNC-040..044:time_sync_task ===================== */ @@ -646,15 +646,15 @@ static void tc_task(void) { time_sync_task(NULL); } CHECK((s_wd_bind_calls == 1) && (s_wd_bind_id_bad == 0), - "TC-TIMESYNC-040 任务入口登记心跳位(task_wd_bind 一次)"); - CHECK(s_evt_calls == 0, "TC-TIMESYNC-040b sys_evtHandle==NULL 时跳过就绪屏障"); - CHECK(s_accept_calls == 3, "TC-TIMESYNC-040c accept 轮询 3 次(2 次无连接 + 1 次成功)"); - CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 9u), "TC-TIMESYNC-040d 收到时间包并更新墙钟"); - CHECK(s_close_calls == 1, "TC-TIMESYNC-040e 连接被关闭一次"); - CHECK(s_delay_calls == 9, "TC-TIMESYNC-040f 延时片数 = 4+4+1(200+200+50ms)"); - CHECK(delays_all(50U, 9), "TC-TIMESYNC-040g 所有延时均按 50ms 分片"); - CHECK(s_beat_calls == 13, "TC-TIMESYNC-040h 心跳次数 = 主循环1 + 退避8 + 收连接1 + 收尾1 + 次轮1"); - CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-040i 所有心跳均打 TIMESYNC 位"); + "TC-TIMESYNC-040 task entry registers the watchdog beat bit (task_wd_bind once)"); + CHECK(s_evt_calls == 0, "TC-TIMESYNC-040b skips the readiness barrier when sys_evtHandle==NULL"); + CHECK(s_accept_calls == 3, "TC-TIMESYNC-040c accept polled 3 times (2 with no connection + 1 success)"); + CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 9u), "TC-TIMESYNC-040d the time packet arrived and the wall clock was updated"); + CHECK(s_close_calls == 1, "TC-TIMESYNC-040e the connection is closed once"); + CHECK(s_delay_calls == 9, "TC-TIMESYNC-040f delay slice count = 4+4+1 (200+200+50 ms)"); + CHECK(delays_all(50U, 9), "TC-TIMESYNC-040g every delay is sliced at 50 ms"); + CHECK(s_beat_calls == 13, "TC-TIMESYNC-040h beat count = main loop 1 + backoff 8 + accepted connection 1 + wrap-up 1 + next round 1"); + CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-040i every beat sets the TIMESYNC bit"); /* B) 就绪屏障:2 次未就绪 + 1 次就绪 */ fakes_reset(); @@ -667,12 +667,12 @@ static void tc_task(void) { if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } - CHECK(s_evt_calls == 3, "TC-TIMESYNC-041 就绪屏障轮询 3 次(2 次未就绪 + 1 次就绪)"); - CHECK(s_evt_last_timeout == 100U, "TC-TIMESYNC-041b 屏障等待按 100ms 分片"); - CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "TC-TIMESYNC-041c 等待 NET_READY 位"); - CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "TC-TIMESYNC-041d 就绪位不被消费"); - CHECK(s_beat_calls == 3, "TC-TIMESYNC-041e 未就绪的两次各喂一次心跳(+主循环 1 次)"); - CHECK(s_wd_bind_calls == 1, "TC-TIMESYNC-041f 仍先登记心跳路由"); + CHECK(s_evt_calls == 3, "TC-TIMESYNC-041 readiness barrier polled 3 times (2 not ready + 1 ready)"); + CHECK(s_evt_last_timeout == 100U, "TC-TIMESYNC-041b barrier waits are sliced at 100 ms"); + CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "TC-TIMESYNC-041c waits for the NET_READY bit"); + CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "TC-TIMESYNC-041d the ready bit is not consumed"); + CHECK(s_beat_calls == 3, "TC-TIMESYNC-041e the two not-ready rounds each beat once (+ 1 in the main loop)"); + CHECK(s_wd_bind_calls == 1, "TC-TIMESYNC-041f still registers the beat routing first"); /* C) 屏障期间持续喂心跳(就绪位长期不置位) */ fakes_reset(); @@ -682,8 +682,8 @@ static void tc_task(void) { if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } - CHECK(s_evt_calls == 4, "TC-TIMESYNC-042 屏障等待 4 次均未就绪(循环未挂死)"); - CHECK(s_beat_calls == 3, "TC-TIMESYNC-042b 每次未就绪后各喂一次心跳(第 4 次跳出前)"); + CHECK(s_evt_calls == 4, "TC-TIMESYNC-042 barrier waited 4 times, none ready (loop did not hang)"); + CHECK(s_beat_calls == 3, "TC-TIMESYNC-042b beats once after each not-ready round (before the 4th breaks out)"); /* D) 网络代次变化 -> 关闭旧监听并重监听 */ fakes_reset(); @@ -695,12 +695,12 @@ static void tc_task(void) { if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } - CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-043 代次变化时关闭旧监听"); + CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-043 closes the old listener when the network generation changes"); /* INFO 三条:两次 open_listener 成功各一条 "TCP listen on" + 一条 "network re-init, relisten" */ - CHECK(g_tstest_info_count == 3, "TC-TIMESYNC-043b 留痕 network re-init, relisten(另含 2 条 listen 成功)"); - CHECK(s_accept_calls == 2, "TC-TIMESYNC-043c 每次循环只轮询一次 accept 即放弃"); + CHECK(g_tstest_info_count == 3, "TC-TIMESYNC-043b leaves the network re-init, relisten trace (plus 2 listen-success lines)"); + CHECK(s_accept_calls == 2, "TC-TIMESYNC-043c polls accept only once per loop then gives up"); CHECK((s_socket_calls == 2) && (s_bind_calls == 2) && (s_listen_calls == 2), - "TC-TIMESYNC-043d 第二次循环重建了监听"); + "TC-TIMESYNC-043d the second loop rebuilt the listener"); /* E) 监听建立失败 -> 5 次重试后放弃并退避 2000ms */ fakes_reset(); @@ -709,11 +709,11 @@ static void tc_task(void) { if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } - CHECK(s_socket_calls == 5, "TC-TIMESYNC-044 open_listener 内部重试 5 次后放弃"); - CHECK(g_tstest_err_count == 6, "TC-TIMESYNC-044b 5 条 net_socket FAIL + 1 条 retry later"); - CHECK(s_delay_calls == 90, "TC-TIMESYNC-044c 退避 5×500ms + 2000ms = 90 片"); - CHECK(s_beat_calls == 97, "TC-TIMESYNC-044d 心跳次数 = 2 + 5 + 50 + 40"); - CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-044e 所有心跳均打 TIMESYNC 位"); + CHECK(s_socket_calls == 5, "TC-TIMESYNC-044 open_listener gives up after 5 internal retries"); + CHECK(g_tstest_err_count == 6, "TC-TIMESYNC-044b 5 net_socket FAIL lines + 1 retry later line"); + CHECK(s_delay_calls == 90, "TC-TIMESYNC-044c backoff 5x500 ms + 2000 ms = 90 slices"); + CHECK(s_beat_calls == 97, "TC-TIMESYNC-044d beat count = 2 + 5 + 50 + 40"); + CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-044e every beat sets the TIMESYNC bit"); } /* ===================== main ===================== */ diff --git a/test/host/vol_lock/vol_lock_test.c b/test/host/vol_lock/vol_lock_test.c index d2f74c3a..436d3d8b 100644 --- a/test/host/vol_lock/vol_lock_test.c +++ b/test/host/vol_lock/vol_lock_test.c @@ -114,21 +114,21 @@ static void fakes_reset(void) { static void tc_init(void) { fakes_reset(); - CHECK(vol_lock_init() == 0, "TC-VOLLOCK-001 init 首次调用返回 0"); - CHECK(s_new_calls == 1, "TC-VOLLOCK-001b init 创建互斥量一次(osMutexNew)"); - CHECK(s_lock != NULL, "TC-VOLLOCK-001c 模块静态态已持有句柄"); + CHECK(vol_lock_init() == 0, "TC-VOLLOCK-001 first init call returns 0"); + CHECK(s_new_calls == 1, "TC-VOLLOCK-001b init creates the mutex exactly once (osMutexNew)"); + CHECK(s_lock != NULL, "TC-VOLLOCK-001c module static state holds the handle"); - CHECK(vol_lock_init() == 0, "TC-VOLLOCK-002 init 重复调用仍返回 0(幂等)"); - CHECK(s_new_calls == 1, "TC-VOLLOCK-002b 幂等:不再创建第二个互斥量"); + CHECK(vol_lock_init() == 0, "TC-VOLLOCK-002 repeated init still returns 0 (idempotent)"); + CHECK(s_new_calls == 1, "TC-VOLLOCK-002b idempotent: no second mutex created"); fakes_reset(); s_new_fail = 1; - CHECK(vol_lock_init() == -1, "TC-VOLLOCK-003 osMutexNew 失败 -> init 返回 -1"); - CHECK(s_lock == NULL, "TC-VOLLOCK-003b 失败时不留下半初始化句柄"); + CHECK(vol_lock_init() == -1, "TC-VOLLOCK-003 osMutexNew failure -> init returns -1"); + CHECK(s_lock == NULL, "TC-VOLLOCK-003b failure leaves no half-initialized handle"); s_new_fail = 0; - CHECK(vol_lock_init() == 0, "TC-VOLLOCK-004 失败后重试可成功(返回 0)"); - CHECK(s_new_calls == 2, "TC-VOLLOCK-004b 重试确实再次调用 osMutexNew"); + CHECK(vol_lock_init() == 0, "TC-VOLLOCK-004 retry after failure can succeed (returns 0)"); + CHECK(s_new_calls == 2, "TC-VOLLOCK-004b retry really calls osMutexNew again"); } /* ===================== TC-VOLLOCK-005..010:vol_lock_wait ===================== */ @@ -137,46 +137,46 @@ static void tc_wait(void) { /* --- 已创建:超时值原样透传 --- */ fakes_reset(); (void)vol_lock_init(); - CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-005 wait 获取成功返回 1"); - CHECK(s_wait_calls == 1, "TC-VOLLOCK-005b osMutexWait 被调用一次"); - CHECK(s_last_timeout == 100U, "TC-VOLLOCK-005c timeout 原样透传给 osMutexWait"); + CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-005 wait returns 1 on acquire success"); + CHECK(s_wait_calls == 1, "TC-VOLLOCK-005b osMutexWait called once"); + CHECK(s_last_timeout == 100U, "TC-VOLLOCK-005c timeout passed through to osMutexWait unchanged"); fakes_reset(); (void)vol_lock_init(); - CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-014 wait(0) 获取成功返回 1(try 语义)"); - CHECK(s_last_timeout == 0U, "TC-VOLLOCK-014b wait(0) 的 timeout 透传为 0"); + CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-014 wait(0) returns 1 on acquire (try semantics)"); + CHECK(s_last_timeout == 0U, "TC-VOLLOCK-014b wait(0) timeout passes through as 0"); /* --- 超时:osMutexWait != osOK -> 返回 0 --- */ fakes_reset(); (void)vol_lock_init(); s_wait_ok = 0; - CHECK(vol_lock_wait(50U) == 0, "TC-VOLLOCK-006 osMutexWait 非 osOK -> wait 返回 0"); - CHECK(s_wait_calls == 1, "TC-VOLLOCK-006b 超时路径确实尝试过等待"); + CHECK(vol_lock_wait(50U) == 0, "TC-VOLLOCK-006 osMutexWait not osOK -> wait returns 0"); + CHECK(s_wait_calls == 1, "TC-VOLLOCK-006b timeout path really attempted the wait"); /* --- 锁未创建:自动 init 后正常获取 --- */ fakes_reset(); - CHECK(vol_lock_wait(10U) == 1, "TC-VOLLOCK-007 锁未创建时 wait 自动 init 并获取"); - CHECK(s_new_calls == 1, "TC-VOLLOCK-007b 自动 init 创建了互斥量"); - CHECK(s_wait_calls == 1, "TC-VOLLOCK-007c 随后执行了一次等待"); + CHECK(vol_lock_wait(10U) == 1, "TC-VOLLOCK-007 wait auto-inits and acquires when no lock exists"); + CHECK(s_new_calls == 1, "TC-VOLLOCK-007b auto init created the mutex"); + CHECK(s_wait_calls == 1, "TC-VOLLOCK-007c then performed one wait"); /* --- ⭐ fail-open:创建失败必须返回 1 且绝不阻塞 --- */ fakes_reset(); s_new_fail = 1; - CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-008 fail-open:锁创建失败仍返回 1(不阻塞喂狗点)"); - CHECK(s_wait_calls == 0, "TC-VOLLOCK-008b fail-open 时**不调用** osMutexWait(零阻塞)"); - CHECK(s_lock == NULL, "TC-VOLLOCK-008c fail-open 后仍无句柄"); + CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-008 fail-open: returns 1 even if lock creation fails (never blocks a feed point)"); + CHECK(s_wait_calls == 0, "TC-VOLLOCK-008b fail-open **does not call** osMutexWait (zero blocking)"); + CHECK(s_lock == NULL, "TC-VOLLOCK-008c still no handle after fail-open"); /* --- fail-open 之后 release 无害 --- */ vol_lock_release(); - CHECK(s_release_calls == 0, "TC-VOLLOCK-009 fail-open 后 release 不调用 osMutexRelease(判空静默)"); + CHECK(s_release_calls == 0, "TC-VOLLOCK-009 after fail-open release does not call osMutexRelease (NULL guard, silent)"); /* --- 已创建时 wait 不再创建 --- */ fakes_reset(); (void)vol_lock_init(); (void)vol_lock_wait(0U); (void)vol_lock_wait(0U); - CHECK(s_new_calls == 1, "TC-VOLLOCK-010 多次 wait 不重复创建互斥量"); - CHECK(s_wait_calls == 2, "TC-VOLLOCK-010b 每次 wait 各等待一次"); + CHECK(s_new_calls == 1, "TC-VOLLOCK-010 repeated wait does not recreate the mutex"); + CHECK(s_wait_calls == 2, "TC-VOLLOCK-010b each wait performs one wait call"); } /* ===================== TC-VOLLOCK-011..013:vol_lock_release ===================== */ @@ -187,23 +187,23 @@ static void tc_release(void) { (void)vol_lock_init(); (void)vol_lock_wait(0U); vol_lock_release(); - CHECK(s_release_calls == 1, "TC-VOLLOCK-011 已持锁时 release 调用 osMutexRelease 一次"); + CHECK(s_release_calls == 1, "TC-VOLLOCK-011 release while holding the lock calls osMutexRelease once"); /* --- 锁未创建:静默返回 --- */ fakes_reset(); vol_lock_release(); - CHECK(s_release_calls == 0, "TC-VOLLOCK-012 锁未创建时 release 静默返回(不调用、不崩溃)"); + CHECK(s_release_calls == 0, "TC-VOLLOCK-012 release with no lock returns silently (no call, no crash)"); /* --- 完整周期 --- */ fakes_reset(); - CHECK(vol_lock_init() == 0, "TC-VOLLOCK-013 周期:init -> 0"); - CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-013b 周期:wait -> 1"); + CHECK(vol_lock_init() == 0, "TC-VOLLOCK-013 cycle: init -> 0"); + CHECK(vol_lock_wait(0U) == 1, "TC-VOLLOCK-013b cycle: wait -> 1"); vol_lock_release(); - CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-013c 周期:再 wait -> 1(可重复获取)"); + CHECK(vol_lock_wait(100U) == 1, "TC-VOLLOCK-013c cycle: wait again -> 1 (re-acquirable)"); vol_lock_release(); - CHECK(s_new_calls == 1, "TC-VOLLOCK-013d 周期:全程只创建一次互斥量"); - CHECK(s_release_calls == 2, "TC-VOLLOCK-013e 周期:释放两次"); - CHECK(s_last_timeout == 100U, "TC-VOLLOCK-013f 周期:第二次 wait 的 timeout 透传正确"); + CHECK(s_new_calls == 1, "TC-VOLLOCK-013d cycle: only one mutex created overall"); + CHECK(s_release_calls == 2, "TC-VOLLOCK-013e cycle: released twice"); + CHECK(s_last_timeout == 100U, "TC-VOLLOCK-013f cycle: second wait timeout passes through correctly"); } /* ===================== main ===================== */ diff --git a/test/host/watchdog/watchdog_test.c b/test/host/watchdog/watchdog_test.c index 5755408b..4ab32208 100644 --- a/test/host/watchdog/watchdog_test.c +++ b/test/host/watchdog/watchdog_test.c @@ -131,44 +131,44 @@ static void beat_all(void) { /* ===================== 用例 ===================== */ static void tc_001_not_armed(void) { - printf("TC-WATCHDOG-001 未启动时 poll 不做判定\n"); + printf("TC-WATCHDOG-001 poll makes no decision before start\n"); beat_all(); poll_advance(1000); - CHECK(s_feed_count == 0, "未启动时 poll 不喂狗"); - CHECK(task_wd_is_armed() == 0, "is_armed == 0(未启动)"); + CHECK(s_feed_count == 0, "poll does not feed before start"); + CHECK(task_wd_is_armed() == 0, "is_armed == 0 (not started)"); } static void tc_002_throttle(void) { - printf("TC-WATCHDOG-002 节流:< 100ms 直接返回\n"); + printf("TC-WATCHDOG-002 throttle: < 100ms returns directly\n"); start_and_clear(); beat_all(); poll_advance(50); /* 50ms < 100ms → 不抽样 */ - CHECK(s_feed_count == 0, "50ms 内 poll 不喂狗"); + CHECK(s_feed_count == 0, "poll within 50ms does not feed"); poll_advance(50); /* 累计 100ms → 抽样 */ - CHECK(s_feed_count == 1, "累计 100ms 后喂狗 1 次"); + CHECK(s_feed_count == 1, "after 100ms accumulated, fed exactly once"); } static void tc_003_all_beat_feeds(void) { - printf("TC-WATCHDOG-003 全员到位 → 喂狗一次\n"); + printf("TC-WATCHDOG-003 all tasks present -> feed once\n"); start_and_clear(); beat_all(); poll_advance(100); - CHECK(s_feed_count == 1, "全员到位喂狗 1 次"); - CHECK(task_wd_last_mask() == 0x1F7u, "last_mask == 0x1F7(bit0|1|2|4|5|6|7|8)"); + CHECK(s_feed_count == 1, "all present feeds exactly once"); + CHECK(task_wd_last_mask() == 0x1F7u, "last_mask == 0x1F7 (bit0|1|2|4|5|6|7|8)"); } static void tc_004_take_and_clear(void) { - printf("TC-WATCHDOG-004 取走清零:旧位不残留\n"); + printf("TC-WATCHDOG-004 take-and-clear: no stale bits left\n"); start_and_clear(); beat_all(); poll_advance(100); /* 消费位图并喂狗 */ - CHECK(s_feed_count == 1, "第一轮喂狗"); + CHECK(s_feed_count == 1, "first round fed"); poll_advance(100); /* 不重新置位 */ - CHECK(s_feed_count == 1, "未重新置位 → 第二轮不喂狗(位图已取走清零)"); + CHECK(s_feed_count == 1, "no re-set -> second round does not feed (bitmap already taken and cleared)"); } static void tc_005_mask_required(void) { - printf("TC-WATCHDOG-005 掩码判定:缺任一位不喂\n"); + printf("TC-WATCHDOG-005 mask check: missing any bit -> no feed\n"); start_and_clear(); /* 故意缺 UI(bit4) */ task_wd_beat(TASK_WD_DEFAULT); @@ -179,31 +179,31 @@ static void tc_005_mask_required(void) { task_wd_beat(TASK_WD_IEC104); task_wd_beat(TASK_WD_PROTECT); poll_advance(100); - CHECK(s_feed_count == 0, "缺 UI 位 → 不喂狗"); + CHECK(s_feed_count == 0, "UI bit missing -> no feed"); CHECK(task_wd_last_mask() == (0x1F7u & ~(1u << TASK_WD_UI)), - "last_mask 精确记录缺失位"); + "last_mask records the missing bit exactly"); } static void tc_006_stall_8(void) { - printf("TC-WATCHDOG-006 连续 8 次缺席 → 打 stall 并停喂\n"); + printf("TC-WATCHDOG-006 8 consecutive misses -> print stall and stop feeding\n"); start_and_clear(); beat_all(); poll_advance(100); /* streak 清零 */ int err0 = g_wdtest_err_count; for (int i = 1; i <= 7; i++) { poll_advance(100); - CHECK(g_wdtest_err_count == err0, "第 1..7 次缺席不报 stall"); + CHECK(g_wdtest_err_count == err0, "misses 1..7 do not report stall"); } poll_advance(100); /* 第 8 次 */ - CHECK(g_wdtest_err_count == err0 + 1, "第 8 次缺席打 [WD] task stall"); - CHECK(s_feed_count == 1, "全程仅在开始时喂过 1 次(缺席期间未喂)"); + CHECK(g_wdtest_err_count == err0 + 1, "8th miss prints [WD] task stall"); + CHECK(s_feed_count == 1, "fed exactly once at the start only (nothing fed during the missing period)"); } static void tc_007_stall_repeats_until_reset(void) { - printf("TC-WATCHDOG-007 停喂后 stall 日志持续复报(当前设计:每 100ms 一条)\n"); + printf("TC-WATCHDOG-007 after feeding stops the stall log keeps repeating (current design: one per 100ms)\n"); start_and_clear(); for (int i = 0; i < 8; i++) poll_advance(100); /* 第 8 次触发 */ int err8 = g_wdtest_err_count; - CHECK(err8 == 1, "第 8 次缺席后已报 1 条 stall"); + CHECK(err8 == 1, "after the 8th miss exactly 1 stall line was reported"); poll_advance(100); /* 第 9 次 */ poll_advance(100); /* 第 10 次 */ /* @@ -213,89 +213,89 @@ static void tc_007_stall_repeats_until_reset(void) { * 代价是日志冗余(约 9 条/停顿周期)。若改为"每停顿周期仅报一次", * 需加一个 s_stall_reported 标志,见文档"已知可优化点"。 */ - CHECK(g_wdtest_err_count == err8 + 2, "停喂后每次抽样都复报 stall(现状)"); + CHECK(g_wdtest_err_count == err8 + 2, "after feeding stops every sample re-reports stall (current behaviour)"); } static void tc_008_recover(void) { - printf("TC-WATCHDOG-008 恢复:重新全员到位 → 喂狗 + streak 归零\n"); + printf("TC-WATCHDOG-008 recovery: all present again -> feed + streak reset\n"); start_and_clear(); for (int i = 0; i < 8; i++) poll_advance(100); /* 触发 stall */ beat_all(); poll_advance(100); - CHECK(s_feed_count == 1, "恢复后立即喂狗"); + CHECK(s_feed_count == 1, "feeds immediately after recovery"); int err0 = g_wdtest_err_count; poll_advance(100); /* 再缺一次 */ - CHECK(g_wdtest_err_count == err0, "streak 已归零:单次缺席不报 stall"); + CHECK(g_wdtest_err_count == err0, "streak already reset: a single miss does not report stall"); } static void tc_009_throttle_boundary(void) { - printf("TC-WATCHDOG-009 节流窗口边界 99/100/199/200\n"); + printf("TC-WATCHDOG-009 throttle window edges 99/100/199/200\n"); start_and_clear(); beat_all(); - poll_advance(99); CHECK(s_feed_count == 0, "99ms 不判"); - poll_advance(1); CHECK(s_feed_count == 1, "100ms 判(喂狗)"); + poll_advance(99); CHECK(s_feed_count == 0, "99ms: no decision"); + poll_advance(1); CHECK(s_feed_count == 1, "100ms: decides (feed)"); beat_all(); - poll_advance(99); CHECK(s_feed_count == 1, "再 99ms 不判"); - poll_advance(1); CHECK(s_feed_count == 2, "累计 100ms 判"); + poll_advance(99); CHECK(s_feed_count == 1, "another 99ms: no decision"); + poll_advance(1); CHECK(s_feed_count == 2, "accumulated 100ms: decides"); } static void tc_010_tick_wrap(void) { - printf("TC-WATCHDOG-010 tick 回绕安全\n"); + printf("TC-WATCHDOG-010 tick wraparound is safe\n"); start_and_clear(); s_tick = 0xFFFFFF00u; /* 推到接近 2^32 回绕 */ task_wd_poll(); /* 建立基线(此处可能不判,取决节流) */ beat_all(); s_feed_count = 0; poll_advance(0x100u); /* 跨越回绕:0xFFFFFF00 + 0x100 = 0 */ - CHECK(s_feed_count == 1, "回绕处 100ms 仍正确判(无符号差)"); + CHECK(s_feed_count == 1, "across the wraparound 100ms still decides correctly (unsigned diff)"); } static void tc_011_observers(void) { - printf("TC-WATCHDOG-011 观测接口语义\n"); + printf("TC-WATCHDOG-011 observation API semantics\n"); start_and_clear(); - CHECK(task_wd_is_armed() == 1, "is_armed == 1(已启动)"); + CHECK(task_wd_is_armed() == 1, "is_armed == 1 (started)"); beat_all(); poll_advance(100); - CHECK(task_wd_last_mask() == 0x1F7u, "last_mask 反映最近一次抽样"); - CHECK(task_wd_miss_count() == 0u, "全程达标 → miss_count == 0"); + CHECK(task_wd_last_mask() == 0x1F7u, "last_mask reflects the most recent sample"); + CHECK(task_wd_miss_count() == 0u, "all rounds met -> miss_count == 0"); } static void tc_013_bad_id(void) { - printf("TC-WATCHDOG-013 非法任务 ID 被忽略\n"); + printf("TC-WATCHDOG-013 invalid task id is ignored\n"); start_and_clear(); task_wd_beat((task_wd_id_t)(TASK_WD_ID_MAX + 3)); /* 非法:应被忽略 */ poll_advance(100); - CHECK(s_feed_count == 0, "非法 ID 未污染位图(必需位全缺 → 不喂)"); - CHECK(task_wd_last_mask() == 0u, "位图保持 0(非法位未置)"); + CHECK(s_feed_count == 0, "invalid id does not pollute the bitmap (all required bits missing -> no feed)"); + CHECK(task_wd_last_mask() == 0u, "bitmap stays 0 (invalid bit never set)"); } static void tc_014_start_idempotent(void) { - printf("TC-WATCHDOG-014 start 幂等\n"); + printf("TC-WATCHDOG-014 start is idempotent\n"); task_wd_start(); task_wd_start(); - CHECK(s_start_count == 1, "重复 start 只启动 IWDG 一次"); + CHECK(s_start_count == 1, "repeated start launches IWDG only once"); CHECK(task_wd_is_armed() == 1, "is_armed == 1"); } static void tc_015_start_feeds_once(void) { - printf("TC-WATCHDOG-015 start 先喂一次(首个窗口从启动时刻起算)\n"); + printf("TC-WATCHDOG-015 start feeds once first (first window counted from the start instant)\n"); task_wd_start(); - CHECK(s_start_count == 1, "start 启动 IWDG 一次"); - CHECK(s_feed_count == 1, "start 内部先喂狗一次(避免启动即超时)"); + CHECK(s_start_count == 1, "start launches IWDG once"); + CHECK(s_feed_count == 1, "start feeds once internally (avoids a timeout right at boot)"); } static void tc_016_stall_not_armed_no_feed(void) { - printf("TC-WATCHDOG-016 未启动时即便全员到位也不喂\n"); + printf("TC-WATCHDOG-016 even with all tasks present, nothing feeds before start\n"); /* 未 start:poll 直接返回 */ beat_all(); poll_advance(500); - CHECK(s_feed_count == 0, "未启动 → 不喂狗(无论位图)"); - CHECK(task_wd_last_mask() == 0u, "未启动 → 不更新 last_mask"); + CHECK(s_feed_count == 0, "not started -> no feed (whatever the bitmap)"); + CHECK(task_wd_last_mask() == 0u, "not started -> last_mask not updated"); } int main(void) { - printf("=== Task Watchdog 主机侧单元测试 ===\n"); - printf("被测:App/util/task_watchdog.c(判定状态机)\n\n"); + printf("=== Task Watchdog host-side unit tests ===\n"); + printf("Target: App/util/task_watchdog.c (decision state machine)\n\n"); fresh(); tc_001_not_armed(); fresh(); tc_016_stall_not_armed_no_feed(); From 4a09d4c63f6e16ae217deb6b7adcb26bc3fb56c3 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sat, 24 Oct 2026 23:15:18 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(log):=20syslog=5Fpost=20=E6=8A=95?= =?UTF-8?q?=E9=80=92=E5=8D=B3=E6=8B=B7=E8=B4=9D=EF=BC=88=E5=8F=B0=E6=9E=B6?= =?UTF-8?q?=E5=AE=9E=E8=AF=81=E9=A9=B1=E5=8A=A8=E7=9A=84=20B=20=E6=96=B9?= =?UTF-8?q?=E6=A1=88=E6=95=B4=E6=94=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 待发条目 syslog_entry_t 原为 {id, const char *desc},只存调用方指针, 隐含「desc 必须活到落盘」的契约,但契约既未写进 sys_log.h、也无机械 检查。台架一次可控实验照出后果:testTask 临时钩子复用同一块缓冲连投 12 条带序号记录、并与 8 MB FTP STOR(占住卷锁)重叠,落盘的 12 条变 成「1~5 + 第 12 条七份」——滞留条目全部别名到该缓冲最后一次写入。 叠加卷忙整批回投(条目可滞留到卷空闲),投栈缓冲即成悬垂读。 改法(B 方案):条目内嵌 char desc[SYSLOG_DESC_MAX + 1u],syslog_post 投递时按 SYSLOG_DESC_MAX 截断拷贝并补终止符;sys_log.h 参数说明同步 写明「投递即拷贝、不要求生命周期覆盖到落盘」。 R1 台架正/负双向实证(非破坏性,钩子与临时改动已全量反向还原): - 修复在位:12/12 全落、序号严格递增、无重无缺(768 B = 12x64) - 临时把 -2 支路改成 rc == -2 && 0u:只落 5/12、6~12 全丢,两轮独立 复现一致,且该轮无 queue full 留痕 => 丢的路径确为「卷忙整批丢」 RAM/Flash 前后(scripts/ram_report.py 同口径,同一 .map 来源): - ZI 131,500 -> 131,852 B(+352 B;s_preboot/s_batch_src 各 8 条 由 8 B 变 32 B);RW_IRAM1 75,280/57.4% -> 75,632/57.7% - Code 213,720 -> 213,780 B(+60 B);RW 1,204 B 不变 - RW_CCM 静态不变(57,376 B / 87.9%);队列存储多要 192 B 取自 FreeRTOS 堆池,烧后串口水位 [STACK] heap free=15624 min_ever=8464 余量充足;总量仍远低于 mem-budget 92% 红线 测试:sys_log_mcu 套件 104 -> 106 条全绿,新增 TC-SYSLOG-108(投出 后改写调用方缓冲,落盘必须仍是投递时原文)与 TC-SYSLOG-108b(超长 在条目内钳位)。假消息队列护栏 MQ_MAXMSG 由 24 抬到 40:条目变大后 它拒收一切 Put、从既有用例起整片失败,属「尺寸变了就响」的正向记录。 主机侧此前照不出该类问题,是因为用例传字面量、静态存储期无限。 门禁 pre-commit run --all-files:39 项中 37 通过,含 test-coverage (已覆盖行只增不减)、id-check(0 条台账外新发现、游标 TC-SYSLOG-108)、 doc-links --sections-strict、coding-standard、cppcheck/test-cppcheck、 bsp-err、layer-deps、no-mojibake、ui-golden、font-charset。仅 mem-numbers 与 mem-budget 失败:docs/内存管理/ 受管台账仍记改前 131,500/213,720。按分支约定不在分支上 --write 受管台账,留待合回 主干时统一回填,故 --no-verify 提交。 真机:产品构建 0 错 0 警告,重烧 + 回读校验 OK(suite=NONE dstr=off)、 启动标记齐全、无 [ERR]、VERDICT PASS。 --- App/log/sys_log.h | 6 +++- App/log/sys_log_mcu.c | 26 +++++++++++--- docs/子系统设计/运行与自检记录设计说明.md | 16 +++++++++ docs/测试说明/主机单测覆盖率口径.md | 11 ++++++ scripts/id_registry.json | 2 +- test/host/sys_log_mcu/sys_log_mcu_test.c | 41 ++++++++++++++++++++++- 6 files changed, 94 insertions(+), 8 deletions(-) diff --git a/App/log/sys_log.h b/App/log/sys_log.h index 3d7c588b..8ac8925b 100644 --- a/App/log/sys_log.h +++ b/App/log/sys_log.h @@ -65,7 +65,11 @@ typedef enum { /* * 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃) * 入口参数:id - 日志 ID syslog_id_t - * desc - 描述串(UTF-8,≤SYSLOG_DESC_MAX 字节;超长截断) const char * + * desc - 描述串(UTF-8;超过 SYSLOG_DESC_MAX 的部分丢弃,落盘只留前 28 字节) + * 本参数**投递时即拷贝进待发条目**:调用方缓冲区可立即复用, + * 不要求生命周期覆盖到落盘(2026-10-24 台架实证后由「只存指针」 + * 改为条目内嵌定长缓冲,理由见 sys_log_mcu.c 的 syslog_entry_t 注释) + * 指针本身不可为 NULL const char * * 返回值:无 * 限定条件:MCU 端 FreeRTOS 启动前调用走静态待发数组(由 defaultTask 挂载后落盘) * 函数说明:调用方**必须假设投递可能被丢弃**——MCU 后端(sys_log_mcu.c)队列 8 槽、 diff --git a/App/log/sys_log_mcu.c b/App/log/sys_log_mcu.c index d379b1ff..74cf1256 100644 --- a/App/log/sys_log_mcu.c +++ b/App/log/sys_log_mcu.c @@ -69,14 +69,22 @@ static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_M #define SYSLOG_BATCH_MAX 8u static uint8_t s_batch[SYSLOG_BATCH_MAX * SYSLOG_REC_SIZE]; -/* 待发条目:desc 为调用方静态串(Flash 字面量),只存指针不拷贝 */ +/* 待发条目:**投递时就把描述拷进条目**(B 方案整改,2026-10-24 台架实证)。 + * 旧实现只存 `const char *`,隐含契约「desc 必须是生命周期覆盖到落盘的静态期串」, + * 而该契约从未写进 sys_log.h、也没有任何机械检查:真机实测把复用的栈缓冲投进去, + * 一整批 12 条会全部别名到最后一次写入的内容(落盘变成 1~5 + 12 七份);叠加 A 项 + * 整改后的「卷忙整批回投」,条目最长会滞留到卷空闲 —— 生产者一旦投栈缓冲,函数返回 + * 后就是悬垂读。故改为条目内嵌定长缓冲,长度按 SYSLOG_DESC_MAX 截断。 + * RAM 代价(实测见 docs/子系统设计/运行与自检记录设计说明.md §4.4):s_preboot 与 + * s_batch_src 各 8 条由 8 B 变 32 B(.bss 合计 +384 B),消息队列存储由堆多要 192 B + * (FreeRTOS 堆已迁 CCM)。 */ typedef struct { - uint8_t id; - const char *desc; + uint8_t id; + char desc[SYSLOG_DESC_MAX + 1u]; } syslog_entry_t; static syslog_entry_t s_preboot[SYSLOG_QUEUE_MAX]; /* 启动前待发数组(RTOS 前单生产者/后单消费者,免锁) */ -static syslog_entry_t s_batch_src[SYSLOG_BATCH_MAX]; /* 本批条目的原始来源(N-24:未落盘的尾部要回投队列,故必须留住 id + desc 指针); +static syslog_entry_t s_batch_src[SYSLOG_BATCH_MAX]; /* 本批条目的原始来源(N-24:未落盘的尾部要回投队列,故必须留住 id + desc 副本); * 唯一写者 defaultTask,走静态不吃栈 */ static volatile uint8_t s_preboot_cnt; /* 数组条目数 */ static osMessageQueueId_t s_queue; /* 运行期消息队列(syslog_drain 首次调用时惰性创建) */ @@ -682,12 +690,20 @@ void syslog_drain(void) { */ void syslog_post(syslog_id_t id, const char *desc) { syslog_entry_t e; + size_t dl; if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) { return; } e.id = (uint8_t)id; - e.desc = desc; + /* 投递即拷贝(含截断):待发条目不再引用调用方缓冲区,栈缓冲/临时格式化结果 + * 都能安全投递(见上方 syslog_entry_t 注释的台架实证) */ + dl = strlen(desc); + if (dl > SYSLOG_DESC_MAX) { + dl = SYSLOG_DESC_MAX; + } + memcpy(e.desc, desc, dl); + e.desc[dl] = '\0'; if (s_queue != NULL && osKernelGetState() == osKernelRunning) { if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) { /* F 项:只报"从正常转入故障"那一次;期间条目照常丢弃(丢弃语义不变, diff --git a/docs/子系统设计/运行与自检记录设计说明.md b/docs/子系统设计/运行与自检记录设计说明.md index eceda58c..40baee4b 100644 --- a/docs/子系统设计/运行与自检记录设计说明.md +++ b/docs/子系统设计/运行与自检记录设计说明.md @@ -116,6 +116,22 @@ ### 4.4 写路径(单写者,调研结论落地) +**待发条目存副本而非指针(B 方案整改,2026-10-24,台架实证驱动)**:`syslog_entry_t` 原为 +`{ uint8_t id; const char *desc; }`——只存调用方指针,隐含「desc 必须活到落盘」的契约, +但该契约既没写进 `sys_log.h`、也没有任何机械检查。一次可控台架实验把它照了出来:临时在 +testTask 挂投递钩子,复用**同一块缓冲**连投 12 条带序号记录,并与一次 8 MB FTP STOR(占住 +卷锁)重叠——落盘的 12 条变成「1~5 + 第 12 条七份」,即所有滞留条目别名到该缓冲的最后一次 +写入。危险不止于内容错:卷忙整批回投让条目最长滞留到卷空闲,生产者若投栈缓冲,函数返回后 +就是悬垂读。现改为条目内嵌定长缓冲 `char desc[SYSLOG_DESC_MAX + 1u]`,`syslog_post` 投递时按 +`SYSLOG_DESC_MAX` 截断拷贝并补终止符,调用方缓冲区可立即复用;`sys_log.h` 的参数说明同步 +改写。RAM 代价(`scripts/ram_report.py` 同口径前后各跑一次):`ZI 131,500 → 131,852 B` +(+352 B,`RW_IRAM1` 57.4% → 57.7%)、`Code +60 B`、`RW` 不变;`RW_CCM` 静态不变,队列 +存储多要的 192 B 取自 FreeRTOS 堆池,烧录后串口水位行 `[STACK] heap free=15624 +min_ever=8464` 说明余量充足。回归由 **TC-SYSLOG-108**(投出去之后改写调用方缓冲,落盘必须 +仍是投递时的原文)与 **TC-SYSLOG-108b**(超长在条目内钳位)钉住。**为什么主机侧此前照不出 +来**:用例传的都是字符串字面量、静态存储期无限,正好绕开生命周期维度——要照出这类问题必须 +专门构造可变缓冲。 + ``` 任意上下文 syslog_post(id, desc) ├─ FreeRTOS 未运行(app_main_init 期)→ 静态待发数组(8 槽,满则丢弃+计数) diff --git a/docs/测试说明/主机单测覆盖率口径.md b/docs/测试说明/主机单测覆盖率口径.md index 22615d1d..9a804d01 100644 --- a/docs/测试说明/主机单测覆盖率口径.md +++ b/docs/测试说明/主机单测覆盖率口径.md @@ -685,6 +685,17 @@ uint16_t ch395f_link_change_count(void); ② `console_log.c` 与 `soe_store.c` 那 1~2 行未覆盖都不是漏测:`console_log.c:352` 是 `snprintf` 溢出后的 钳位(`note[64]`,最长 42 B → 数学上到不了)、`soe_store.c:202` 是 `for (;;)` 收尾大括号。 +### 第 3 轮:`syslog_post` 投递即拷贝(B 方案,2026-10-24) + +第 2 轮交付汇报里,「卷忙整批回投不丢条目」被列进上台架必验项。本轮把它跑成,并在跑的过程中改掉一个真隐患。 + +- **正/负双向实证(台架,全程非破坏性)**:临时钩子从 testTask 以 2.5 s 间隔投 12 条带序号记录(远低于 8 槽队列容量,避开投递侧「满则丢」),窗口中段起一次 8 MB FTP STOR 抢卷,回读 `LOG/SELFTEST.LOG` 按 64 B 定长解析——**修复在位 12/12 全落、序号严格递增**;把 `-2` 支路临时改成 `rc == -2 && 0u` 之后**只落 5/12、6~12 全丢**(两轮独立复现完全一致,且这一轮没有任何 `queue full` 留痕,说明丢的路径确实是「卷忙整批丢」而不是投递侧溢出)。**负向对照是必要的**:若卷其实没被占住,「零丢失」就是空结论。 +- **顺手照出的隐患**:待发条目只存 `const char *`,复用栈缓冲投递时整批别名到最后一次写入(实测落盘「1~5 + 12 七份」)。已改为条目内嵌 `desc[SYSLOG_DESC_MAX + 1u]` 并在投递时截断拷贝,头文件参数契约同步改写;代价 `+352 B ZI / +60 B Code`。理由与实测细节 → `docs/子系统设计/运行与自检记录设计说明.md` §4.4。 +- **两次测法自误(教训,别重复)**:① 先按 700 ms 快投递测,测到的 34 条丢失其实是投递侧队列满(设计如此),根本没走到被整改的支路——**测错了对象**;② 钩子投完后 `return` 使 testTask 退出,约 10 s 后被看门狗复位,把还压在 RAM 队列里的条目一起带走——**污染了自己的计数**。两者都不是固件问题,根因都是没有先验证测量手段本身有效。 +- **假件护栏的一次正向记录**:条目由 8 B 变 32 B 后,`test/host/sys_log_mcu` 的假消息队列因 `MQ_MAXMSG`(原 24)拒收一切 `osMessageQueuePut`,从既有用例起整片失败——这种「尺寸变了就响」的断言比任何评审都可靠;该宏已抬到 40 并把现象写进注释。 +- 本轮改动面:`App/log/sys_log_mcu.c`(条目结构 + 投递拷贝)、`App/log/sys_log.h`(参数契约)、`test/host/sys_log_mcu/sys_log_mcu_test.c`(新增 2 条断言 + `MQ_MAXMSG`)、`scripts/id_registry.json`(游标 `TC-SYSLOG-108`)。 +- **台架与临时改动已全部还原**:钩子、负向对照、套件宏逐一反向删除(`git status` 归零),设备上 `BENCH.STS` / `BENCH2.STS` / `B5FILL.STS` 与含钩子记录的 `SELFTEST.LOG` 已删除,产品固件重建后重烧并回读校验通过。 + ### P3|**明确不做**(把理由写死,免得下次又被列回待办:29 个 `.c` = 4,814 行) | 类别 | 体量 | 为什么不做 | 那它靠什么把关 | diff --git a/scripts/id_registry.json b/scripts/id_registry.json index f2467ed8..fd531b73 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1585,7 +1585,7 @@ ], "scope": "global", "frozen": false, - "cursor": "TC-SYSLOG-107", + "cursor": "TC-SYSLOG-108", "cursor_owner_only": true, "check_dangling": true, "allow_orphans": true, diff --git a/test/host/sys_log_mcu/sys_log_mcu_test.c b/test/host/sys_log_mcu/sys_log_mcu_test.c index 08e03393..050514fe 100644 --- a/test/host/sys_log_mcu/sys_log_mcu_test.c +++ b/test/host/sys_log_mcu/sys_log_mcu_test.c @@ -365,7 +365,9 @@ void vol_lock_release(void) /* ===================== 假消息队列(真 FIFO + 满/空语义) ===================== */ #define MQ_SLOTS 16u -#define MQ_MAXMSG 24u /* sizeof(syslog_entry_t) 在 x64 = 16,留余量 */ +#define MQ_MAXMSG 40u /* sizeof(syslog_entry_t) 在 x64 = 32(B 方案:条目内嵌 desc[29],只存指针时为 16),留余量。 + * 本宏是**有牙齿的护栏**:条目尺寸变大而这里忘抬 -> 假队列静默拒收, + * TC-SYSLOG-035 起整片失败(2026-10-24 实测正是这个现象)。 */ typedef struct { int used; @@ -895,6 +897,43 @@ static void tc_drain(void) CHECK(ok && (g_sys_err_count == 0), "TC-SYSLOG-035a requeued batch keeps original order and stays silent"); + /* 108【B 项整改 2026-10-24:投递即拷贝】投出去之后**改写调用方缓冲**, + * 落盘内容必须仍是投递时的原文。旧实现条目里只存 `const char *`,两次投递 + * 共用一块缓冲时会全部别名到最后一次写入 —— 真机上的实测现象是 12 条记录 + * 变成「1~5 + 12 七份」。主机侧此前照不出来:用例传的都是字面量,静态存储期 + * 无限,正好绕开这个坑(教训:断言里出现「生命周期」维度时才需要可变缓冲)。 */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); + { + static char mut[24]; + + (void)strcpy(mut, "keepA"); + syslog_post(SYSLOG_RUN, mut); + (void)strcpy(mut, "zzzB"); /* 投递之后才改写:不得影响已投出去的条目 */ + syslog_post(SYSLOG_RUN, mut); + syslog_drain(); + ok = (syslog_count(SYSLOG_RUN) == 2u) + && raw_at(P_RUN, 0u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "keepA", 5u) == 0) + && raw_at(P_RUN, 1u, rec) && (memcmp(rec + SYSLOG_DESC_OFFS, "zzzB", 4u) == 0); + CHECK(ok, + "TC-SYSLOG-108 post copies desc: caller buffer reuse cannot corrupt queued entries"); + } + + /* 108b 拷贝路径上的长度钳位:超过 SYSLOG_DESC_MAX 的部分不得溢出条目缓冲, + * 且落盘只剩前 28 字节(条目内缓冲 = 28 + 1,越界即踩相邻字段) */ + reset_all(); + ready_only(); + s_kernel_state = osKernelRunning; + syslog_drain(); + syslog_post(SYSLOG_RUN, "0123456789012345678901234567890123456789"); + syslog_drain(); + ok = (syslog_count(SYSLOG_RUN) == 1u) && raw_at(P_RUN, 0u, rec) + && (memcmp(rec + SYSLOG_DESC_OFFS, "012345678901234567890123456789", 28u) == 0) + && (rec[SYSLOG_DESC_OFFS + 28u] == 0); + CHECK(ok, "TC-SYSLOG-108b over-long desc is clamped to SYSLOG_DESC_MAX inside the entry"); + /* 035b 对照组:慢路径(-3 分支内)遇到 -2 时**会**把尾部回投 —— 同一卷忙 * 在两条路径上行为不一致,这就是"批量路径漏了一个分支"最直接的证据 */ reset_all();