测试:新增 cfg_file 主机侧单测并修复外科手术保存死码缺陷(S1-b+S2)
This commit is contained in:
@@ -99,6 +99,7 @@ docs/
|
||||
|---|---|
|
||||
| [`看门狗单元测试说明.md`](测试说明/看门狗单元测试说明.md) | Task Watchdog 判定状态机(位图 / 掩码 / 节流 / tick 回绕),39 用例 |
|
||||
| [`系统时钟单元测试说明.md`](测试说明/系统时钟单元测试说明.md) | sys_clock 时间换算 / 星期 Zeller / 30 天惰性再基准化 / RTC 故障降级,100 断言点 |
|
||||
| [`配置文件单元测试说明.md`](测试说明/配置文件单元测试说明.md) | cfg_file INI 解析 / 值编解码 / load-save-poll 契约 / 超长守卫,139 断言点(含抓出的外科手术保存死码 P0) |
|
||||
|
||||
**模块测试指南**(动手写测试前先读)
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 261,200 B (255.08 KB) | 1,048,576 B (1024 KB) | 24.9% |
|
||||
| ER_IROM1 | 261,196 B (255.07 KB) | 1,048,576 B (1024 KB) | 24.9% |
|
||||
| RW_IRAM1 | 84,864 B (82.88 KB) | 131,072 B (128 KB) | 64.7% |
|
||||
| RW_CCM | 49,184 B (48.03 KB) | 65,536 B (64 KB) | 75.0% |
|
||||
| **Flash (ROM 合计)** | **261,900 B (255.8 KB)** | 1,048,576 B (1024 KB) | **25.0%** |
|
||||
| **Flash (ROM 合计)** | **261,896 B (255.8 KB)** | 1,048,576 B (1024 KB) | **25.0%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
@@ -240,6 +240,8 @@ static void phaseN_run(void) {
|
||||
| `test/host/verify_log.py` | `test/host/` | 日志标记校验(Required / Forbidden → PASS / FAIL)+ 可选套件汇总行校验(T-2) |
|
||||
| **`test/host/common/unit_test_harness.py`** | `test/host/common/` | **L0.5 通用单测 harness**:工具链探测 / `INCLUDE`-`LIB` 拼接 / 编译运行 / 退出码契约。各模块运行器只做声明式配置(T-3) |
|
||||
| **`test/host/watchdog/run_unit_test.py`** | `test/host/watchdog/` | **L0.5 主机侧单测**:声明 `TestBuild` 后调 harness,穷举看门狗判定状态机(零硬件、秒级)。**需本机装有 VS(`cl.exe`)**,见 §4.2 环境要求 |
|
||||
| **`test/host/sys_clock/run_unit_test.py`** | `test/host/sys_clock/` | **L0.5 主机侧单测**:穷举 `sys_clock` 时间换算 / 星期 / 惰性再基准化 / RTC 故障降级(100 断言,S1-a) |
|
||||
| **`test/host/cfg_file/run_unit_test.py`** | `test/host/cfg_file/` | **L0.5 主机侧单测**:INI 解析 / 值编解码 / 段注册 / load-save-poll 契约,内存虚拟 FS 驱动(139 断言,S1-b) |
|
||||
| **`scripts/ci_local.py`** | `scripts/` | **本地跑与 CI 等价的门禁**(L0 + L0.5):自动选用装了 pre-commit 的解释器;`.map` 自适应跳过 `mem-numbers`;退出码 0/1/2 与 CI 对齐。见 §9.3 |
|
||||
| `.opencode/serial_config.json` | 仓库 | **串口端口单一配置源**(当前 COM6 / 115200);换端口只改这个文件 |
|
||||
| `test/host/ftp_*.py`(4 个)/ `ch395f_socket_test.py` / `time_sync_server.py` | `test/host/` | PC 端交互测试工具(FTP 客户端 / socket 回显 / 对时推送) |
|
||||
@@ -280,6 +282,34 @@ static void phaseN_run(void) {
|
||||
- 测试专用钩子(如静态态复位)用 `#ifdef <模块>_UNIT_TEST` 守卫,MCU 构建不编入(零符号);
|
||||
- 单测**不能替代**板端验证:真机寄存器 / 复位原因 / 时序仍须 L1/L2 台架闭环。
|
||||
|
||||
#### 4.2.1 L0.5 适格模块台账(2026-10-02 加,S2)
|
||||
|
||||
**问题**:L0.5 起步阶段只有"看门狗"一个先例,**下一个该测谁**靠临时判断,易遗漏高价值模块。
|
||||
|
||||
**判据**(是否适格 L0.5):模块的核心逻辑是否**无副作用**且**与硬件解耦**——
|
||||
满足"纯逻辑 + 已抽象平台层(或依赖全是外部符号/宏)"即可主机侧穷举。
|
||||
|
||||
| 模块 | 规模 | 状态 | 平台层形态 | 备注 |
|
||||
|---|---|---|---|---|
|
||||
| `App/util/task_watchdog.c` | — | ✅ 已覆盖(39 断言) | 内联 `__weak` 钩子 | 范式来源;`#ifdef TASK_WATCHDOG_UNIT_TEST` 复位钩子 |
|
||||
| `App/util/sys_clock.c` | 479 行 | ✅ 已覆盖(100 断言,2026-10-02 S1-a) | 外部符号(`HAL_GetTick`/`sd2506_*`)+ 宏 | **零侵入**(无需 `#ifdef`);首测即抓出 Zeller 星期缺陷 |
|
||||
| `App/cfg/cfg_file.c` | 1250 行 | ✅ 已覆盖(139 断言,2026-10-02 S1-b) | 后端抽象(`cfg_file_store.h`) | 内存虚拟 FS 桩;**首测即抓出外科手术保存死码 P0**(见 §5) |
|
||||
| `App/log/sys_log_mcu.c` | — | ⬜ 候选 | 记录编解码(定长 64B) | 编解码纯逻辑;写入端依赖 FatFS + 队列,测编解码即可 |
|
||||
| `App/util/time_sync.c` | — | ⬜ 候选 | 协议帧编解码 | 报文组装/解析纯逻辑;网络收发不测 |
|
||||
| `Drivers/BSP/NETCFG/net_cfg_legacy.c` | — | ⬜ 候选 | 旧档 NCFG 迁移 + 自复制 CRC | CRC 校验与旧格式解析;可与 cfg_file 同桩 |
|
||||
| `App/net/*`(网络栈上层) | — | ⬜ 待评估 | 依赖 socket/CH395F | 纯协议状态机部分或可测,需先确认抽象质量 |
|
||||
| `App/util/rs485.c` | — | ⏸ 待功能实现 | — | 上游 B2;当前为空桩 |
|
||||
| `Drivers/BSP/TPAFE5160/*.c` | — | ❌ 不适格 | — | 寄存器序列为主,需真机时序 |
|
||||
| 其余驱动(CH395F/GD5F2GQ5/SD2506) | — | ❌ 不适格 | — | 需硬件时序 / SPI / I2C 总线 |
|
||||
| `App/util/crc.c` · `ringbuf.c` | 120/154 行 | ⛔ 已删除(2026-10-02 S8) | — | 孤儿文件,零引用,已 `git rm` |
|
||||
|
||||
**使用方式**:新增/改动上述模块前,按表判断是否需补 L0.5;`⬜ 候选` 表示"适格但未做",
|
||||
在性价比允许时应排入(判据:边界密集、零硬件、单次真机回归 30s~2min vs 单测 2~5s)。
|
||||
|
||||
> ⭐ **两条实证**:`sys_clock`(S1-a)与 `cfg_file`(S1-b)**首次落地即各抓出一处
|
||||
> 生产缺陷**(Zeller 星期公式错误 / 外科手术保存死码)。这印证了本台账的性价比判断——
|
||||
> 高价值纯逻辑模块的 L0.5 覆盖,是**投入最小、产出最大**的测试改进方向。
|
||||
|
||||
### 4.3 新工具约定
|
||||
|
||||
- **test/ 自动化脚本为纯 Python 标准库**(2026-09-20 由 PowerShell 迁移:绕开控制台代码页/执行策略陷阱,全部 I/O 显式 UTF-8/字节级处理;`requirements.txt` 仍须纯 ASCII——pip 按 GBK 代码页解码);
|
||||
@@ -544,11 +574,15 @@ CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runn
|
||||
| `docs/测试说明/串口助手设计说明.md` | 串口调试工具(Python 串口助手) |
|
||||
| `docs/测试说明/自动化测试流程.md` | @test 闭环细节 |
|
||||
| `docs/测试说明/测试文档模板规范.md` | 测试文档模板与命名 |
|
||||
| `docs/测试说明/看门狗单元测试说明.md` | 看门狗判定状态机(L0.5 主机侧) |
|
||||
| `docs/测试说明/系统时钟单元测试说明.md` | 系统时钟换算(L0.5 主机侧) |
|
||||
| `docs/测试说明/配置文件单元测试说明.md` | 配置引擎解析/契约(L0.5 主机侧) |
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-10-02 | **S1-b + S2**:新增 **§4.2.1「L0.5 适格模块台账」**(判据 + 模块状态表 + "两条实证":sys_clock / cfg_file 首测即各抓一处生产缺陷);§4.1 登记 `test/host/cfg_file/` 新单测。同步新增 [`配置文件单元测试说明.md`](配置文件单元测试说明.md)(含抓出的 `s_encode` 返回值误判 P0 缺陷记录) |
|
||||
| 2026-10-02 | **S3 + S14**:§9.1 第 2 条改正(原文"强制跑 `mem-numbers`"自相矛盾 → 改为"按 `.map` 有无自适应跳过")并加 🔴 门禁盲区声明;§9.2 补 S3 结果可见性(`Tee-Object` 落盘 + `$GITHUB_STEP_SUMMARY` + `::notice::` 汇总行,不用 `actions/upload-artifact`)。另:§4.1 登记 `test/host/sys_clock/` 新单测(S1-a) |
|
||||
| 2026-10-01 | 新增 **§9 CI 持续集成(Gitea Actions,T-6)**:`.gitea/workflows/ci.yml`(`l0-gates` + `l05-host-unit-tests`,Windows runner);**L0 + L0.5 上 CI、L1/L2/L3 留本地**;刻意不跑 `@build`、`.map` 缺失时自适应跳过 `mem-numbers`;新增 `scripts/ci_local.py` 本地等价命令(§4.1 登记);§1 补 CI 说明;`docs/待处理内容.md` T-6 销项 |
|
||||
| 2026-10-01 | §3.6 新增 **`test/run_all_suites.py`** 多套件回归编排(`try/finally` 无条件还原套件开关;T-5);`test/README.md` 同步 |
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
# 配置文件引擎单元测试说明(主机侧)
|
||||
|
||||
> **模块**:Config File Engine(通用配置文件引擎,`App/cfg/cfg_file.c`)
|
||||
> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层)——在 PC 上离线验证 INI 解析、
|
||||
> 值类型编解码、段注册校验与 load/save/poll 契约,零硬件、零烧录、毫秒级、不占 MCU 的 RAM。
|
||||
> **状态**:✅ 已落地(20 用例 / 139 断言点,`@build` 0/0,已挂 pre-commit 钩子)
|
||||
> **日期**:2026-10-02
|
||||
> **关联文档**:
|
||||
> [`../../AGENTS.md`](../../AGENTS.md)(平台约束 · FatFs/内存铁律)、
|
||||
> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、
|
||||
> [`看门狗单元测试说明.md`](看门狗单元测试说明.md)、
|
||||
> [`系统时钟单元测试说明.md`](系统时钟单元测试说明.md)(同层范本)、
|
||||
> [`../子系统设计/配置文件设计说明.md`](../子系统设计/配置文件设计说明.md)(模块设计)、
|
||||
> [`../审查记录/测试与架构改进建议.md`](../审查记录/测试与架构改进建议.md)(S1-b)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 为什么单独做主机侧单测
|
||||
|
||||
`cfg_file.c`(1250 行)是**双端共享核心**(MCU + PC 模拟器),承担:
|
||||
|
||||
| 半边 | 内容 | 可测性 |
|
||||
|---|---|---|
|
||||
| **纯逻辑**(被测) | INI 段/键解析、四种值类型(UINT/IP4/MAC/STR)编解码、段注册校验、外科手术式保存(保留注释/未知段/未知键)、超长守卫、沉降去抖轮询与分段回调 | 纯"文本 ↔ 结构"变换,与硬件无关 → **可在 PC 上位穷举** |
|
||||
| **平台后端** | 文件 I/O 与临时缓冲分配(`cfg_file_store.h` 的 8 个接口) | 已抽象为后端 → 主机侧给内存虚拟 FS 即可 |
|
||||
|
||||
板端套件(`test/board/`)**无法穷举边界**,更无法构造以下场景:
|
||||
|
||||
- 畸形 INI(段头前出现键值行、缺段、重复键、CRLF/LF 混排、段名/键名大小写混用);
|
||||
- 四种值类型的**边界**(UINT 极值/带后缀、IP4 段数/超 255、MAC 分隔符/大小写、STR 恰好放下);
|
||||
- **超长文件**(> `CFG_FILE_MAX_LEN` = 4096 B)→ 工厂默认 + 重建模板 + 自检留痕;
|
||||
- **故障注入**:读取失败、写盘失败、堆分配失败(真机需掉电/坏块才能复现);
|
||||
- **外科手术保存**:对**已存在**文件保留注释/未知段、受管键原位改写、缺失段/键补写、输出超长拒写。
|
||||
|
||||
这些用主机侧单测一次性钉死,且**不占 MCU 的 RAM**(本项目 RAM 已 95%,见 `docs/内存管理/`)。
|
||||
|
||||
> ⭐ **本单测首次落地即抓出一处 P0 生产缺陷**——`s_encode()` 的返回值被误判,
|
||||
> 使**外科手术保存的受管值改写路径沦为死码**:对已存在的 `/config.ini` 保存时,
|
||||
> 受管键的值**从不被改写**(静默不落盘,而 `net_cfg_save()` 仍返回成功并打
|
||||
> "save: OK")。详见 §5。
|
||||
|
||||
### 1.2 被测对象与桩
|
||||
|
||||
**关键设计**:`cfg_file.c` 的全部文件/堆操作都经 `cfg_file_store.h` 的 8 个接口
|
||||
(`cfg_store_alloc/free/heap_free`、`cfg_store_read`、`cfg_store_write_atomic`、
|
||||
`cfg_store_stat`、`cfg_store_remove`、以及 3 个路径查询)。测试单元提供这 8 个接口的
|
||||
**强符号桩**(内存虚拟文件系统),**生产代码零改动**(缺陷修复除外,见 §5)。
|
||||
|
||||
```
|
||||
test/host/cfg_file/cfg_file_test.c
|
||||
├── #include "dbg_log.h" → stub/dbg_log.h (DBG_* → 计数)
|
||||
├── #include "cfg_file_store.h" → 真实头(后端接口契约原汁)
|
||||
├── #include "sys_log.h" → 真实头(syslog_id_t 枚举 + syslog_post 原型)
|
||||
├── #include "cfg_file.h" → 真实头(被测 API 原型)
|
||||
└── #include "cfg_file.c" ← 生产源文件(App/cfg/),原样纳入
|
||||
```
|
||||
|
||||
| 后端接口 | MCU 端(`cfg_file_store_mcu.c`) | 主机侧桩 |
|
||||
|---|---|---|
|
||||
| `cfg_store_alloc/free` | FreeRTOS 堆 `pvPortMalloc/vPortFree` | 宿主 CRT `malloc/free` |
|
||||
| `cfg_store_heap_free` | `xPortGetFreeHeapSize` | 恒返回 0(模拟器语义) |
|
||||
| `cfg_store_read` | FatFS 读 `/config.ini` | 从内存虚拟盘复制;可注入失败 `s_fail_read` |
|
||||
| `cfg_store_write_atomic` | 临时文件 + 原子改名 | 写入内存虚拟盘并 `mtime++`;可注入失败 `s_fail_write` |
|
||||
| `cfg_store_stat` | FatFS `f_stat` → `(size, fdate<<16\|fmtime)` | 返回 `(s_vfs_len, s_vfs_mtime)`;可注入失败 `s_fail_stat` |
|
||||
| `cfg_store_remove` | FatFS 删除 | 清空虚拟盘存在标志 |
|
||||
| `syslog_post` | 写 `/log/selftest.log` | 计次 `s_syslog_count`(断言"超长/写失败"留痕) |
|
||||
| `DBG_ERROR/INFO/DEBUG` | USART + RTC 时间戳 | 计数 `g_cfgtest_err/info/debug_count` |
|
||||
|
||||
> **生产端零增量**:`cfg_file.c` 未因本单测新增任何 `#ifdef` 钩子(缺陷修复属独立
|
||||
> 修正,见 §5)。
|
||||
|
||||
### 1.3 测试环境
|
||||
|
||||
| 依赖 | 版本 / 说明 |
|
||||
|---|---|
|
||||
| 编译器 | 本机 MSVC(VS2019/2022/2026 任一),由 `run_unit_test.py` 自动探测 |
|
||||
| Python | 仓库 `.venv` 或任意 python3(**纯标准库**) |
|
||||
| 硬件 | 无(不需要开发板 / 串口 / ST-LINK / NAND) |
|
||||
|
||||
工具链探测 / `INCLUDE`-`LIB` 拼接 / 编译运行 / 退出码判定全部委托通用 harness
|
||||
[`../../test/host/common/unit_test_harness.py`](../../test/host/common/unit_test_harness.py),
|
||||
本模块 `run_unit_test.py` 只**声明** `TestBuild`(源文件 / 桩目录 / 汇总行标记)。
|
||||
|
||||
### 1.4 运行方式
|
||||
|
||||
```powershell
|
||||
# 仓库根目录(统一用 .venv)
|
||||
& '.venv\Scripts\python.exe' test/host/cfg_file/run_unit_test.py
|
||||
```
|
||||
|
||||
退出码:`0`=PASS / `1`=测试失败 / `2`=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
### 1.5 回归接入
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| **触发范围** | `App/cfg/cfg_file.c` / `cfg_file.h`、`App/cfg/cfg_file_store.h`、`App/log/sys_log.h`,或 `test/host/cfg_file/**`、`test/host/common/unit_test_harness.py` 变动 |
|
||||
| **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑 |
|
||||
| **pre-commit 钩子** | `cfg-file-unit-test`(本地钩子,`.pre-commit-config.yaml` §7c) |
|
||||
| **汇总行** | `=== Config File Tests: N/N PASSED (failed=0) ===` |
|
||||
| **与板端测试的关系** | 互补:本单测保证**解析/编解码/契约逻辑**正确;板端保证 **NAND 上 FatFS 真实读写链路**正确 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 测试用例
|
||||
|
||||
> 命名 `TC-CFG-NNN`(CFG = Config File)。全部在 `cfg_file_test.c` 内,主机侧运行。
|
||||
> 部分用例直接调用 `static` 内部函数(白盒,因源文件被 `#include` 纳入同一编译单元)。
|
||||
|
||||
### 2.1 值类型编解码
|
||||
|
||||
| 用例 | 断言 | 验证的设计点 |
|
||||
|---|---|---|
|
||||
| **TC-CFG-001** | UINT 1/2/4 字节往返(0 / 最大值);小端序(`0x1234` → `34 12`) | 小端编解码 |
|
||||
| **TC-CFG-002** | UINT 非法:空 / 非数字 / 带后缀 / 超范围 / 负数 / len=3 | 严格校验 |
|
||||
| **TC-CFG-003** | IP4:`0.0.0.0` / `255.255.255.255` / 常规;段数不足/超、>255、非数字、尾点、空 | 点分四段 |
|
||||
| **TC-CFG-004** | MAC:`:` / `-` / `.` 三种分隔符、大小写;过短/过长/非十六进制/坏分隔符 | 分隔符 = **字节边界**替代 |
|
||||
| **TC-CFG-005** | STR:正常 + NUL;恰好 `len-1` 放下;`len` 字符拒绝;空串 | 长度边界 |
|
||||
|
||||
### 2.2 INI 解析
|
||||
|
||||
| 用例 | 断言 | 验证的设计点 |
|
||||
|---|---|---|
|
||||
| **TC-CFG-010** | `;` 与 `#` 注释、空行忽略 | 注释语义 |
|
||||
| **TC-CFG-011** | LF-only 行结束符解析正常 | CRLF/LF 兼容 |
|
||||
| **TC-CFG-012** | 段名/键名大小写无关;`=` 两侧与值两侧空白修剪 | 宽松匹配 |
|
||||
| **TC-CFG-013** | 键值行 `=` 前后空白、值尾空白被修剪 | 值修剪 |
|
||||
| **TC-CFG-014** | 未知键 / 未知段 / 缺失键 → 保留工厂默认,不崩溃 | 前向兼容 |
|
||||
| **TC-CFG-014b** | 非法值 → 保留默认 + `DBG_ERROR` 留痕 | 无效键留痕 |
|
||||
| **TC-CFG-015** | 段头之前的键值行被忽略 | 段作用域 |
|
||||
|
||||
### 2.3 段注册校验
|
||||
|
||||
| 用例 | 断言 | 验证的设计点 |
|
||||
|---|---|---|
|
||||
| **TC-CFG-020** | 正常注册;重复段名拒绝;NULL 描述符 / NULL p_data 拒绝 | 参数防御 |
|
||||
| **TC-CFG-021** | UINT len=3、MAC len≠6、STR len>79、key len=0 拒绝 | 键定义校验 |
|
||||
| **TC-CFG-022** | 段名 ≥ `CFG_NAME_MAX`(16) 拒绝 | 段名长度 |
|
||||
| **TC-CFG-023** | 挂回调段 `data_size > CFG_SNAP_MAX`(64) 拒绝;同段不挂回调则接受 | 回调段快照约束 |
|
||||
|
||||
### 2.4 load / save / poll 契约
|
||||
|
||||
| 用例 | 断言 | 验证的设计点 |
|
||||
|---|---|---|
|
||||
| **TC-CFG-030** | 文件缺失 → 生成出厂模板写盘 + 返回 0;模板含段头与键 | 首启建文件 |
|
||||
| **TC-CFG-031** | 文件存在 → 有效键进 RAM 副本 | 正常加载 |
|
||||
| **TC-CFG-032** | 超长文件 → 工厂默认 + 重建模板 + 自检留痕 + 返回 0 | 损坏自愈 |
|
||||
| **TC-CFG-033** | 读取失败 → 返回 -1,RAM 保持工厂默认 | 读故障 |
|
||||
| **TC-CFG-034** | 未注册任何段 → 返回 -1 | 前置校验 |
|
||||
| **TC-CFG-040** | 文件缺失 → 重建完整文档 | 整文档生成 |
|
||||
| **TC-CFG-041** | 外科手术:保留注释/未知段/未知键,**受管键原位改写** | ⭐ 核心特性 |
|
||||
| **TC-CFG-042** | 缺失受管段/键 → 文末补写 | 补写路径 |
|
||||
| **TC-CFG-043** | 输出超长 → 拒绝保存(-1) + 原文件不动 + 自检留痕 | 溢出保护 |
|
||||
| **TC-CFG-050** | 首轮 poll 建立基线,不重载 | 基线建立 |
|
||||
| **TC-CFG-051/052** | 沉降去抖(连续 2 次稳定才重载);与基线一致不重载 | 去抖语义 |
|
||||
| **TC-CFG-053** | 变更后重载 + 挂回调段差异触发回调 | 变更响应 |
|
||||
| **TC-CFG-055** | `(size, mtime)` 令牌不变 → 不重载(令牌 = 变更信号) | 令牌契约 |
|
||||
| **TC-CFG-054** | poll 检出超长 → 工厂默认 + 重建模板 + 留痕;**不触发回调** | 在线修复通道保留 |
|
||||
|
||||
**当前结果**:139/139 PASSED(`failed=0`)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 如何扩展用例
|
||||
|
||||
1. 在 `cfg_file_test.c` 加一个 `static void tc_0NN_xxx(void)`;
|
||||
2. 用例开头调 `vfs_reset()`(复位模块静态态 + 虚拟盘 + 观测计数)+ `register_std()`;
|
||||
3. 用 `vfs_put(text)` 建立/更新文件(**会** `mtime++`,模拟一次外部写盘);
|
||||
需"内容变但令牌不变"的边界用 `vfs_put_notoken(text)`;
|
||||
4. 用 `vfs_get()` 取当前文件文本做检索断言(注释/未知段是否保留等);
|
||||
5. 用故障注入开关 `s_fail_read / s_fail_write / s_fail_alloc / s_fail_stat` 构造故障路径;
|
||||
6. `CHECK(cond, "描述")` 断言;在 `main()` 里注册。
|
||||
|
||||
> ⚠ **`vfs_put` 与 `vfs_put_notoken` 的区别很关键**:poll 依 `(size, mtime)` 判变更,
|
||||
> 若只改内容而不动令牌(且长度不变),poll **按设计不会重载**。构造"应重载"的场景
|
||||
> 必须用 `vfs_put`;构造"令牌不变"边界才用 `vfs_put_notoken`。
|
||||
>
|
||||
> ⚠ **`vfs_reset()` 直接 `memset` 生产模块的静态态**(`s_sections` / `s_base` /
|
||||
> `s_sec_hdr` / `s_key_line` 等)——这是白盒单测的可接受代价(源文件已被 `#include`,
|
||||
> 静态态在同一编译单元内可见)。新增静态变量时若不复位,跨用例会互相污染。
|
||||
|
||||
---
|
||||
|
||||
## 4. 平台与隔离机制
|
||||
|
||||
### 4.1 为什么不需要 `#ifdef` 测试钩子(与看门狗的差异)
|
||||
|
||||
看门狗的平台层是**内联 `__weak` 函数**,测试时必须用 `#ifndef TASK_WATCHDOG_UNIT_TEST`
|
||||
排除默认实现。`cfg_file.c` 则**经 `cfg_file_store.h` 调用外部符号**——这些符号在主机侧
|
||||
本就不存在(只有声明),测试单元给出定义即可,**无需任何生产代码钩子**(与 `sys_clock`
|
||||
同属"零侵入"范式)。
|
||||
|
||||
### 4.2 隔离:如何在不改生产代码的前提下复位模块静态态
|
||||
|
||||
引擎静态态(`s_sections` / `s_base` / `s_last` / `s_stable` / `s_base_valid` /
|
||||
`s_sec_hdr` / `s_key_line`)均为文件静态、无公开复位接口。隔离手段:
|
||||
`vfs_reset()` 一次性 `memset` 全部静态态 + 清空虚拟盘 + 清零观测计数,等价于"刚上电"。
|
||||
段 RAM 副本(测试自有的 `s_net_data` / `s_sys_data`)同时复位工厂默认。
|
||||
|
||||
### 4.3 与板端套件的分工
|
||||
|
||||
| 关注点 | 主机侧 L0.5(本单测) | 板端 L1 |
|
||||
|---|---|---|
|
||||
| INI 解析 / 值编解码 / 契约逻辑 | ✅ 穷举 | 不覆盖 |
|
||||
| 段注册校验 / 故障注入 | ✅ 穷举 | 不覆盖 |
|
||||
| NAND 上 FatFS 真实读写 | ❌ | ✅ |
|
||||
| 掉电安全 / 原子改名在真实 FS 上的行为 | ❌ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 5. ⭐ 本单测抓出的 P0 生产缺陷(已修复)
|
||||
|
||||
### 5.1 现象
|
||||
|
||||
`TC-CFG-041`(外科手术保存)首次运行即失败:对**已存在**的 `/config.ini` 调用
|
||||
`cfg_file_save()`,即使 RAM 副本的值已改(如 `u8: 1 → 200`),落盘文件仍是
|
||||
**原值** `u8 = 1`。
|
||||
|
||||
### 5.2 根因
|
||||
|
||||
`s_encode()` 的返回约定是"**成功 = 文本长度(≥1);失败 = -1**"(见函数头注释)。
|
||||
但外科手术保存的 **3 个调用点**写成了 `if (s_encode(...) == 0)`:
|
||||
|
||||
```c
|
||||
/* 旧代码(错误)——对任何非空值,s_encode 返回长度 ≥1,`== 0` 恒假 */
|
||||
if (s_encode(p_key, (const uint8_t *)s_sections[i_sec].p_data + p_key->offset,
|
||||
v, sizeof(v)) == 0) {
|
||||
s_emit_kv(...); /* 永远不执行 */
|
||||
} else {
|
||||
s_emit_line(p_out, ..., p_line); /* 永远走这里:原样输出旧行 */
|
||||
}
|
||||
```
|
||||
|
||||
结果:`s_emit_kv`(写入新值)成为**死码**,受管键行永远走 `else` 原样输出。
|
||||
唯一判对的地方是 `s_gen_document()`(整文档生成,用 `< 0`),故**只有"文件缺失/损坏 →
|
||||
重建文档"路径正常**——这解释了为何此前从未暴露:
|
||||
|
||||
- 板端套件保存时目标文件总是**缺失/新建**(走 `s_gen_document`);
|
||||
- 真机首次上电后 `/config.ini` 由模板创建,此后**每次改 IP 保存**都走外科手术路径
|
||||
→ **受管值静默不落盘**,而 `net_cfg_save()` 仍返回 0 并打 `save: OK`。
|
||||
|
||||
### 5.3 影响面
|
||||
|
||||
| 维度 | 评估 |
|
||||
|---|---|
|
||||
| **触发条件** | 已存在 `/config.ini`(正常运行的设备 100% 命中) |
|
||||
| **后果** | 网络配置(IP/掩码/网关/MAC)经 UI 或 FTP 修改后**保存不生效**(重启后回旧值) |
|
||||
| **可观测性** | ❌ 无任何报错——返回成功、日志 "save: OK",属**静默数据丢失** |
|
||||
| **注释/未知段保留** | 该特性本身正常(走 `s_emit_line`)——讽刺的是"保留原文"正是掩盖 bug 的原因 |
|
||||
| **引入时间** | 自 `cfg_file` 首个提交 `9766141` 起存在,从未修复 |
|
||||
|
||||
### 5.4 修复
|
||||
|
||||
将 3 个外科手术调用点的判定由 `== 0` 改为 `>= 0`(与 `s_gen_document` 及函数约定一致),
|
||||
并加注释说明返回值语义:
|
||||
|
||||
```c
|
||||
/* s_encode 成功返回文本长度(≥1),失败返回 -1:判成功用 `>= 0`。
|
||||
* 旧代码写 `== 0`(恒假)→ 受管键行永远走 else 原样输出 →
|
||||
* 对已存在文件保存时受管值从不改写(静默不落盘,2026-10-02 修复) */
|
||||
if (s_encode(p_key, ..., v, sizeof(v)) >= 0) {
|
||||
s_emit_kv(...);
|
||||
}
|
||||
```
|
||||
|
||||
修复后 `TC-CFG-041` / `TC-CFG-042` 转 PASS,`@build` 0 errors 0 warnings。
|
||||
|
||||
### 5.5 建议后续动作
|
||||
|
||||
- **板端补一条回归**:保存到**已存在**文件 → 重新 load → 断言值已按新值落盘
|
||||
(现有板端套件只测了"文件缺失 → 重建"路径,是本缺陷长期漏网的直接原因)。
|
||||
- 检查是否存在同类"返回长度却按 `== 0` 判成功"的调用点(本次已全库核对 `s_encode`
|
||||
的 4 处调用点,除本缺陷外其余正确)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-10-02 | 创建;20 用例 / 139 断言点全绿;抓出并修复 `s_encode` 返回值误判 P0 缺陷(外科手术保存死码);挂 pre-commit 钩子 `cfg-file-unit-test` |
|
||||
@@ -19,6 +19,14 @@
|
||||
|
||||
**当前未闭环 25 项:P1×5、P2×20。无 P0。**
|
||||
|
||||
> **2026-10-02 追记(L0.5 单测新发现,已修复)**:**E-63(P0)** ——
|
||||
> `App/cfg/cfg_file.c` 外科手术保存的 3 处 `s_encode(...) == 0` 判定恒假
|
||||
> (该函数成功返回**文本长度**而非 0)→ **受管值改写路径沦为死码**:
|
||||
> 对已存在的 `/config.ini` 保存时受管键**从不被改写**(静默不落盘),
|
||||
> 而 `net_cfg_save()` 仍返回 0 并打 `save: OK`。由本日新增的 `cfg_file`
|
||||
> 主机侧单测(S1-b)首跑抓出,已修复并通过 `@build` 0/0。**已闭环**,
|
||||
> 明细见 §2.1。
|
||||
|
||||
首版 59 项中 **34 项已闭环**(见 §2),其中 3 项 P0 全部修复并在位。剩余问题分三类:
|
||||
|
||||
| 类别 | 数量 | 性质 | 处置 |
|
||||
@@ -101,6 +109,7 @@
|
||||
| **E-01** | `App/cfg/cfg_file.c` | `cfg_file_poll` 的"超长=损坏"检查是**死代码**(读长被钉死在阈值)→ >4096B 配置被截断当有效解析 → 界外内容静默回工厂默认 → 基线粘住错误状态 → 下次开机 load 守卫判"损坏" → **用户配置文件整体销毁** | `d030cb0` + 关联 `70f1cc8`(`f_rename` 不覆盖既有目标) |
|
||||
| **E-31** | `nand_ftl.c` / `journal.c` | 扫描读失败被**覆盖为 `TOO_BAD`** → 非空白卷在 I/O 故障下被误判"全新"→ 直接 `clear` → 映射表整体丢失 | `da1a6e8` |
|
||||
| **E-02** | `net_socket.c` / `lftpd` / `time_sync` | 一条空闲 FTP 控制连接把 netTask 独占 30s → **全体消费者队列停摆**(实测冻结远端显示 17.7s) | `f8d6e90`+`4a1605b`(分片)→ `a45c0cb`(L3 pending 化,结构性消灭) |
|
||||
| **E-63** | `App/cfg/cfg_file.c` | 外科手术保存 3 处 `s_encode(...) == 0` 判定恒假(函数成功返回**文本长度**)→ 受管值改写为**死码**:对已存在 `/config.ini` 保存时受管键从不改写(**静默不落盘**,`net_cfg_save` 仍报 OK) | **2026-10-02**(L0.5 `cfg_file` 单测首跑抓出)——判据由 `== 0` 改 `>= 0`;见 [`../测试说明/配置文件单元测试说明.md`](../测试说明/配置文件单元测试说明.md) §5 |
|
||||
|
||||
### 2.2 P1 十五项
|
||||
|
||||
|
||||
Reference in New Issue
Block a user