test(NETCFG,CFG): P1①四个主机单测模块,并修 net_cfg 的 WAIT 契约与定长错记

按 docs/测试说明/主机单测覆盖率口径.md §9 的清单吃 P1 第①批「配置与工具」四块,
新增 4 个主机单测模块共 104 条断言,全部 MSVC 编译通过并 PASS:
- test/host/net_cfg/(45 条):出厂默认可用、注册幂等与内容、load 报 -1 仍返回 0
  (RAM 恒有效契约)、跨任务保存交接状态机 IDLE/WAIT/OK/FAIL、MAC 三态优先级、
  IPv4 定长格式化。
- test/host/ftp_cfg/(16 条):[ftp] 段注册(**p_cb 必须为 NULL**,口令下次登录才
  生效)、默认口令判定大小写不敏感、NULL 一律判不等(fail-closed)。
- test/host/prot_set_store/(16 条):blob 返回码分流 1 无档 / 0 成功 / **2 换代
  (只打 INFO)** / -1 损坏;写盘失败必须回置脏标志(否则定值静默丢失)。
- test/host/task_prof/(27 条):无效样本闸门(防 DWT 复位把 max 抬到 2^32)、
  未 enter 就 exit 不记账、首次 5s + 每 2s 节流、环形缓冲与裸发两通道、
  周期→微秒换算随 SystemCoreClock 变。

覆盖率收益(已 --update-baseline 登记):套件 16 → 20 全绿;files_tested 19 → 23;
未触及 125 → 121 个文件(.c 47 → 43、9,657 → 9,228 行);分母 2,893 → 3,163 插桩
行;line=2811/3163(88.9%)、func=225/240(93.8%)。§9 的恒等式同步更新且仍咬合:
43 = 9(P1 剩) + 5(P2) + 29(P3)、9,228 = 2,188 + 2,226 + 4,814。

新单测照出并修掉两处真问题(都在 net_cfg,不是测试写错):
1. 实现违反自己的文档契约:net_cfg.h 明写"属主任务写盘中显示 WAIT",但
   net_cfg_save_run 为了不丢新请求会先清 pending 再写盘 → 写盘那几百毫秒里 UI
   保存行退回上一轮的 OK/IDLE(第一次保存按 ENTER 后屏幕上还是 ENTER),用户只
   会再按一次。补 s_save_busy 标志,TC-NETCFG-028/026/030 钉死(含失败路径必须
   清 busy,否则 UI 永久卡 WAIT)。
2. "定长 11 字符"是错的:%03u.%03u.%03u.%03u = 4 段×3 位 + 3 个点 = **15**。这个数
   错了 4 处(net_cfg.h、net_cfg.c、网络配置说明.md 两处)——是我照抄它才撞上
   FAIL,错在文档不是代码。已全部更正,并确认 len ≥ 16 的要求与 UI_BUF_STRLEN=32
   本来就没问题(无截断风险)。

桩的口径(沿用既有模块,不另起一套):真实头优先——cfg_file.h / cfg_file_store.h /
console_log.h / prot_set.h 全用真头(接口一漂移就编译期报错),只有 main.h 必须桩
(真实 main.h 拉整棵 HAL 头树,PC 上编不动;桩里字段名与位掩码宏名与 CMSIS 一致)。
prot_set_store 连真实 prot_set.c 一起编:blob 的编解码与脏标志在它里面,只有拿真
serialize 产出的 blob 才能造出"换代"与"损坏"两种真实形态,手搓等于测自己的桩。
配置引擎本身(解析/写盘/沉降)归 test/host/cfg_file 的 103 条,本批不重复测。

编号门禁:按 2026-10-21 批 3 的 B 层口径补建 4 族 TC-HOST-*(NETCFG 游标 056 /
FTPCFG 014 / PROTSTOR 016 / TASKPROF 020,代码即属主登记处)。这一步不是走形式:
建族之前文档里引用 TC-NETCFG-028 被判「台账之外新账」直接卡提交,建族后才通过;
编号规范 §8.4 变更记录加一行说明族数 9 → 13。id_registry.json 只做增量插入(+88 行,
无重排)。

自查(提交时 pre-commit run --all-files,**仅 3 条失败且都与本分支改动无关**):
- mem-numbers / mem-budget:本 worktree 因需验证代码改动而 @build 过,新生成的 .map
  与 docs/内存管理 受管快照漂移。实测增量 ER_IROM1 +12 B、RW_IRAM1 +8 B、Flash
  +16 B、**CCM 0**,水位百分比全不变(改动 = 1 个静态字节 + 几条指令)。按并行分支
  约定**不在分支里 --write**,台账由 Lead 合并后统一回填。
- ui-golden:simulator/tests/ref/*.bin 写在 .gitignore 里(黄金参考是本地生成物),
  新建的 worktree 天生没有,报的是"缺黄金参考"而非像素差异;本分支零 UI 文件改动。
- 其余全部 Passed,含 @build 0 错误 0 警告、test-cppcheck(它抓到过我一条 p!=NULL
  后又解引用的写法,已改为断言+显式守卫)、coding-standard、doc-links(35 处=存量)、
  id-check --strict、mojibake 与 BOM/CR/U+FFFD/行尾逐项卫生。
- 自审修掉自己写的两条恒真断言(形如 x==0?1:1 的假绿)与一个重复编号标签。
- 4 个新模块**尚未挂 pre-commit 钩子**:.pre-commit-config.yaml 是单写文件,任务工作
  树内不改;覆盖率脚本按 test/host/**/run_unit_test.py 自动发现,已实跑 20/20。
- 未跑 @test:改动仅 1 个静态字节 + UI 保存行的显示时序,无硬件语义变化。
This commit is contained in:
2026-10-23 15:25:24 +08:00
parent e70b861215
commit b1291f1308
18 changed files with 2033 additions and 37 deletions
+14 -4
View File
@@ -60,6 +60,7 @@ static uint8_t s_active_mac[6]; /* 芯片实际 MAC 影子(netTask 回写
static uint8_t s_active_valid; /* 影子有效标志(0 = 尚未回写) */
static net_cfg_apply_cb_t s_apply_cb; /* "立即应用"平台回调(未注册 = 空操作) */
static volatile uint8_t s_save_pending; /* 1 = 有待执行保存请求(UI 投递/属主执行) */
static volatile uint8_t s_save_busy; /* 1 = 属主任务正在写盘(维持 UI 的 WAIT 显示) */
static volatile uint8_t s_save_result; /* 最近一次保存结果:0 = 无;1 = 成功;2 = 失败 */
static net_cfg_save_req_cb_t s_save_req_cb; /* "保存请求"平台通知(未注册 = 空操作) */
@@ -234,7 +235,10 @@ int net_cfg_save_request(void) {
* 函数说明:见 net_cfg.h 同名函数说明
*/
int net_cfg_save_state(void) {
if (s_save_pending != 0u) {
/* "请求已投递未执行"与"正在写盘"都显示 WAIT:pending 在写盘前就被清掉
* (为了不让写盘期间的新请求丢失),单看它会在这几百毫秒里退回上一轮的
* OK/IDLE —— 用户按了 ENTER 却看不到反馈,只会再按一次。busy 补上这一段。 */
if ((s_save_pending != 0u) || (s_save_busy != 0u)) {
return (int)NET_CFG_SAVE_ST_WAIT;
}
if (s_save_result == 1u) {
@@ -252,8 +256,10 @@ int net_cfg_save_state(void) {
* 返回值:0 - 无待处理请求或保存成功;-1 - 写盘失败 int
* 限定条件:仅配置引擎属主任务上下文(MCU = defaultTask;模拟器 = 主循环)
* 函数说明:先清"待保存"标志(写盘期间的新请求重位置位,下轮再执行,
* 不丢失);写盘成功 → 置 OK 并触发"立即应用"(netTask 重建
* 网络,无需重启);失败 → 置 FAIL,不触发应用(网络保持)
* 不丢失),同时置"写盘中"标志让 UI 在这段窗口里仍显示 WAIT
* (否则退回上一轮结果,见 net_cfg_save_state);写盘成功 → 置 OK
* 并触发"立即应用"(netTask 重建网络,无需重启);失败 → 置 FAIL,
* 不触发应用(网络保持)。两条路径都会清"写盘中"标志
*/
int net_cfg_save_run(void) {
int r;
@@ -262,8 +268,11 @@ int net_cfg_save_run(void) {
return 0; /* 无待处理请求(空醒/周期轮询) */
}
s_save_pending = 0u;
s_save_busy = 1u; /* 写盘期间维持 WAIT(见 net_cfg_save_state 注释) */
r = net_cfg_save();
s_save_busy = 0u;
if (r == 0) {
s_save_result = 1u;
net_cfg_request_apply();
@@ -352,7 +361,8 @@ void net_cfg_get_mac(uint8_t *p_out) {
* len - 缓冲长度 size_t
* 返回值:无
* 限定条件:p_ip/p_buf 非空;len ≥ 16
* 函数说明:"192.168.001.100"(定长 11 字符,UI 编辑光标按固定偏移定位字节)
* 函数说明:"192.168.001.100"(定长 **15** 字符 = 4 段 × 3 位 + 3 个点,UI 编辑
* 光标按固定偏移定位字节:段 i 起点 4i、点号在 3/7/11)
*/
void net_cfg_fmt_ipv4(const uint8_t *p_ip, char *p_buf, size_t len) {
if (p_ip == NULL || p_buf == NULL || len == 0u) {
+1 -1
View File
@@ -204,7 +204,7 @@ void net_cfg_get_mac(uint8_t *p_out);
* p_buf - 输出缓冲(不可为 NULL) char *
* len - 缓冲长度 size_t
* 返回值:无
* 限定条件:p_ip/p_buf 非空;len ≥ 16(输出 11 字符 + '\0')
* 限定条件:p_ip/p_buf 非空;len ≥ 16(输出 15 字符 + '\0')
* 函数说明:"192.168.001.100"——每段 3 位补零使字符串定长,UI 编辑光标
* 可按固定偏移(4i/3i 起始)定位字节;net_init 的 parse_ip_string
* 对前导零兼容(十进制逐段解析)
+2 -2
View File
@@ -228,7 +228,7 @@ sequenceDiagram
| 行 | 名称 | 值格式 |
|---|---|---|
| 0 | IP地址 | `192.168.001.100`(每段 `%03u` 补零,**定长 11 字符**) |
| 0 | IP地址 | `192.168.001.100`(每段 `%03u` 补零,**定长 15 字符** = 4 段 × 3 位 + 3 个点) |
| 1 | 子网掩码 | `255.255.255.000` |
| 2 | 默认网关 | `192.168.001.001` |
| 3 | MAC地址 | `A4:94:B6:12:34:56`(6 字节十六进制,冒号分隔) |
@@ -277,7 +277,7 @@ ENTER 进入选中地址行 → `↑/↓` 调活动字节(0..255 **环绕**)
| `net_cfg_set()` | 更新 RAM 副本(不写盘) |
| `net_cfg_set_active_mac()` | 回写芯片实际 MAC 影子(netTask 调) |
| `net_cfg_get_mac()` | 取显示用 MAC(存储非零 → 存储;否则影子;否则全零) |
| `net_cfg_fmt_ipv4()` | 4 字节 → `"192.168.001.100"` 定长字符串 |
| `net_cfg_fmt_ipv4()` | 4 字节 → `"192.168.001.100"` 定长 15 字符串(缓冲 len ≥ 16) |
---
@@ -241,6 +241,18 @@
| 内核前装配 | `app_main.c`(135) | 被测对象本身就是"`osKernelStart` 之前不许调内核 API"的时序,主机侧无法忠实复现(PC 上没有那个约束) | 同上 + `cubemx-survival` 审计 |
**四个数字对得上**:`47 = 13(P1) + 5(P2) + 29(P3)`、`9,657 = 2,617 + 2,226 + 4,814`。
**落地进度(同日,P1 的第 ① 批已吃掉)**:新增 4 个主机单测模块 —— `test/host/net_cfg/`(45 断言)、
`test/host/ftp_cfg/`(16)、`test/host/prot_set_store/`(16)、`test/host/task_prof/`(27),
`files_tested` 19 → **23**、`files_untested` 125 → **121**(未触及 `.c` 47 → **43**、行数 9,657 → **9,228**)、
分母 2,893 → **3,163** 插桩行(新纳入 `net_cfg.c` 108/112、`task_prof.c` 88/93、`ftp_cfg.c` 35/36、
`prot_set_store.c` 27/29),套件 16 → **20** 全绿。恒等式跟着更新且仍然咬合:
`43 = 9(P1 剩) + 5(P2) + 29(P3)`、`9,228 = 2,188 + 2,226 + 4,814`——**剩下 43 个里 29 个是 P3 刻意不做**。
顺带由新单测照出并修掉两处(都在 `net_cfg`,`@build` 0/0 实测 ER_IROM1 +12 B / RW_IRAM1 +8 B / CCM 0):
① `net_cfg_save_run` 先清 `pending` 再写盘,导致写盘窗口里 UI 保存行退回上一轮的 OK/IDLE,
违反 `net_cfg.h` 自己写的"写盘中显示 WAIT"→ 补 `s_save_busy` 标志(TC-NETCFG-028/026/030 钉死);
② "定长 11 字符"是错的(`%03u`×4 + 3 个点 = **15**),`net_cfg.h`/`net_cfg.c`/本文所引设计文档
共四处已更正(缓冲要求 `len >= 16` 本来是对的,UI 侧 `UI_BUF_STRLEN`=32 未受影响)。
也就是说:**"覆盖率不该 100%"这件事在这份清单里是算出来的,不是嘴上说的**——P3 那 4,814 行永远不进分母,
而 P1+P2 做完约 `4.8k` 行进分母(现在的分母只有 2,893),`files_tested` 从 19 → 37。
@@ -259,6 +271,9 @@
|---|---|
| 2026-10-23 | 新建。落地 `scripts/check_test_coverage.py` 与 `scripts/test_coverage_baseline.json`:首次实测 16 套全通过,并集 2552/2893 行(88.2%)、函数 193/207(93.2%),在测 19 文件 / 未触及 125 文件。同步销项 `docs/问题记录/项目审查与优化建议报告.md` §4.8 空白 ①(覆盖率口径)与 §5.5 第 5 条前半、§0.1 / §0.4 / §7 对应行;`malloc(` 正则那条**同日也落地**(`coding-standard` CODE-RULE-20,见 `docs/门禁说明/coding-standard.md`)。**同日补 §2.1**:用户问「为什么不用 GCC」,实测本机 WSL2 Ubuntu 有 gcc/gcov 15.2.0,16 套单测在 gcc 下**全部编过且全绿**、`gcov -b -c` 确实给出分支覆盖(`nand_ftl.c` 分支 41.40%)——但换编译器=换分母(`sha256.c` gcc 105 行 vs MSVC 114 行),故默认通道不变。 |
| 2026-10-23 | **用户裁定:本仓只留 MSVC 一条覆盖率通道**,不建 gcc/WSL 第二条("后面可能引入更多问题")。实测数据与三条代价保留在 §2.1 当决策依据,§7 第 1 条把分支覆盖改记为**已接受缺口**,§8 下一步删掉"建通道"并写明重开该裁定的前提(必须先答"两份分母怎么对齐")。脚本与基线**不受影响**,仍是 `mscoverage` 一条路 |
| 2026-10-23 | **P1 第①批落地**(net_cfg / ftp_cfg / prot_set_store / task_prof 四个模块,104 条断言):
套件 16→20、`files_tested` 19→23、分母 2,893→3,163;照出并修掉 `net_cfg` 的 WAIT 契约违背与
"定长 11 字符"错记(详见 §9 落地进度) |
| 2026-10-23 | 新增 **§9 建桩优先级清单**:对 47 个未触及 `.c`(9,657 行)逐个画像(有效代码行 / 函数数 /
逻辑度=1−硬件符号密度 / 要桩的外部头 / 可复用现有桩 / fix 提交数),分为 **P1 13 个 2,617 行、
P2 5 个 2,226 行、P3 29 个 4,814 行明确不做**(四条对得上:13+5+29=47、三段相加=9,657);
+2 -1
View File
@@ -417,7 +417,7 @@ git grep -n '<旧字面量>' # 零残留
| 改动 | 动机 | 对比例的影响 |
|---|---|---|
| 主机侧单测建 **9 族 `TC-HOST-<模块码>`**,属主登记处 = `test/host/<目录>/*`(代码即登记处) | 实测这些号在代码目录外**零消费者**(唯一"外部引用"是 `id_aliases.json` 自己),逐号补文档只会造出没人读的复述层 | 悬空错误 −508 中的 430(**口径修正,不是仓库变好**) |
| 主机侧单测建 **9 族 `TC-HOST-<模块码>`**,属主登记处 = `test/host/<目录>/*`(代码即登记处;**2026-10-23 起同一口径新增 4 族,现 13 族**,见 §8.4 变更记录) | 实测这些号在代码目录外**零消费者**(唯一"外部引用"是 `id_aliases.json` 自己),逐号补文档只会造出没人读的复述层 | 悬空错误 −508 中的 430(**口径修正,不是仓库变好**) |
| `subitem_ok`:小写子项后缀(`TC-TIMESYNC-003b`)母号在册即算已定义 | §4.1 明允许小写后缀,是合法形态;双向追溯仍按完整字面量报差异,可见性不丢 | 悬空 −74(同上,属口径) |
| **`snapshot_only`**:§4.3 冻结历史快照族(`N-RPT`/`D-RPT`/`S-RPT`/`R-RPT`/`F-RPT`/`P-AUDIT`/`A-AUDIT`/`B-AUDIT`)**不参与"定义位优先"** | 曾试过让定义位优先无差别生效:`N-21`(登记簿条目已销项)被审计报告里同号的另一码事"接管",门禁转而去报 ⑥"请写报告文档名"——**照做就制造 §1.2 的 `N-31` 撞号**;实测该做法使校验 6 从 266 暴涨到 1437、比例假崩到 68.7% | 校验 6 回到 269(与 B 层前持平) |
| **补属主**:`docs/任务规划/README.md` 加入 `N`/`S-LDG`/`T-LDG` 族的 `owner_docs` | 这是**我的配置漏项**:活登记处在 README(`N-21` 出现 2 次),我原先只登记了 `登记簿.md`(`N-21` = 0)→ 真条目被误判悬空 74 处 | 悬空 −74(**修 bug,不是口径**) |
@@ -556,6 +556,7 @@ git grep -n '<旧字面量>' # 零残留
| 日期 | 变更 | 说明 |
|---|---|---|
| 2026-10-23 | **主机侧单测族 +4(现共 13 族)** | 覆盖率口径 `docs/测试说明/主机单测覆盖率口径.md` §9 P1 第①批新建 `test/host/{net_cfg,ftp_cfg,prot_set_store,task_prof}` 四个模块,按 2026-10-21 批 3 的同一 B 层口径补建 `TC-HOST-{NETCFG,FTPCFG,PROTSTOR,TASKPROF}`(代码即属主登记处、`allow_orphans`、游标按实测最大号 056/014/016/020)。**验证了这条口径真的在拦东西**:建族前,文档里引用 `TC-NETCFG-028` 被判「台账之外的新账」而卡提交;建族后 `--strict` 通过 |
| 2026-10-14 | 新增 P-7 与引用可读性规则 | §2 加 **P-7「编号不语义,引用必语义」**(编号本体保持无语义并冻结,但正文引用必须带语义括注、禁裸 ID 独担信息)→ 六条原则变**七条**;§5.1 加前置步"先问能不能不加号";新增 **§5.6「编号密集文档的对照节(强制)」**(≥3 个编号族必须加「导读:编号与术语」);§4.1 补登记 **`E\d+`**(N34 自愈实验编号族,已封闭)并把 `N-\d{2}` 的在册范围订正为「曾用最大 `N-33`,现存至 `N-29`」;§1.2 悬空引用示例补 `N-32`;§7.1 同步订正。起因:`docs/网络/网络自愈架构设计.md` 合并与导读节实践暴露"裸编号引用不可读",用户裁定以后尽量用语义标识 |
| (规划稿) | 建立本文档 | 全库扫描 1,385 个不同编号 / 19,306 处出现后定稿的两层编号体系(全局编号 + 文档内编号)、六条原则、目标前缀总表、三批迁移映射与 `check_ids.py` 门禁设计;用户决策:`E-xx` 冻结保留、严重度档位词 `P0~P3` 不变、文档落 `docs/编号规范.md`、阶段 1 三批全做 |
| 2026-10-21 | **批 5:追溯器口径补齐(未改任何号)** | `check_test_traceability.py` 加入与 `check_ids.py`/`rename_ids.py`/`doc-links` 同源的「变更记录 / `[示例]` / `[计划]` 行不参与比对」豁免,并新增 `frozen_ids` 留痕(`--verbose` 可列);`DOC-ONLY` 38 → 29、总差异 45 → 36,**交集 260 → 260 不变**(证明没吞掉真对应)。批 5 原计划的 8 处文档侧改名**被证据否掉**(全在变更记录行、别名批 2 已登记),未落盘;经过与教训、以及剩 29 处的三类真问题都写进 §8.3 批 5 段 |
+88
View File
@@ -1592,6 +1592,94 @@
"subitem_ok": true,
"owner_note": "B 层口径同其余 TC-HOST 族;本族是 2026-10-21 补漏——上批模块码改名前它是纯数字 `TC-104-*`,未进当时的悬空普查,故漏建族",
"cursor_note": "游标按实测最大号收录(2026-10-21)"
},
{
"name": "TC-HOST-NETCFG",
"class": "TC",
"title": "主机侧单测用例号:网络配置编排与跨任务保存交接(test/host/net_cfg)",
"pattern": "\\bTC-NETCFG-[0-9]{1,4}[a-z]?\\b",
"digit_width": [
2,
4
],
"owner_docs_glob": [
"test/host/net_cfg/*"
],
"scope": "global",
"frozen": false,
"cursor": "TC-NETCFG-056",
"cursor_owner_only": true,
"check_dangling": true,
"allow_orphans": true,
"subitem_ok": true,
"owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);本族 2026-10-23 随覆盖率口径 §9 P1 第①批新建模块补登记",
"cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-FTPCFG",
"class": "TC",
"title": "主机侧单测用例号:FTP 鉴权配置 [ftp] 段(test/host/ftp_cfg)",
"pattern": "\\bTC-FTPCFG-[0-9]{1,4}[a-z]?\\b",
"digit_width": [
2,
4
],
"owner_docs_glob": [
"test/host/ftp_cfg/*"
],
"scope": "global",
"frozen": false,
"cursor": "TC-FTPCFG-014",
"cursor_owner_only": true,
"check_dangling": true,
"allow_orphans": true,
"subitem_ok": true,
"owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);本族 2026-10-23 随覆盖率口径 §9 P1 第①批新建模块补登记",
"cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-PROTSTOR",
"class": "TC",
"title": "主机侧单测用例号:保护定值持久化胶水层(test/host/prot_set_store)",
"pattern": "\\bTC-PROTSTOR-[0-9]{1,4}[a-z]?\\b",
"digit_width": [
2,
4
],
"owner_docs_glob": [
"test/host/prot_set_store/*"
],
"scope": "global",
"frozen": false,
"cursor": "TC-PROTSTOR-016",
"cursor_owner_only": true,
"check_dangling": true,
"allow_orphans": true,
"subitem_ok": true,
"owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);本族 2026-10-23 随覆盖率口径 §9 P1 第①批新建模块补登记",
"cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-TASKPROF",
"class": "TC",
"title": "主机侧单测用例号:任务耗时探针与上报节流(test/host/task_prof)",
"pattern": "\\bTC-TASKPROF-[0-9]{1,4}[a-z]?\\b",
"digit_width": [
2,
4
],
"owner_docs_glob": [
"test/host/task_prof/*"
],
"scope": "global",
"frozen": false,
"cursor": "TC-TASKPROF-020",
"cursor_owner_only": true,
"check_dangling": true,
"allow_orphans": true,
"subitem_ok": true,
"owner_note": "B 层口径(2026-10-21 批 3 定:这些号在代码目录外零消费者,属主登记处 = 单测代码本身,不逐号补文档);本族 2026-10-23 随覆盖率口径 §9 P1 第①批新建模块补登记",
"cursor_note": "游标按实测最大号收录(2026-10-23):越界即视为未登记的冒号牌"
}
],
"unmanaged_bare": {
+78 -29
View File
@@ -44,8 +44,8 @@
"func_rate": 0.9565,
"funcs_covered": 22,
"funcs_valid": 23,
"line_rate": 0.9609,
"lines_covered": 172,
"line_rate": 0.9665,
"lines_covered": 173,
"lines_valid": 179
},
"App/algo/sample_ring.c": {
@@ -64,6 +64,14 @@
"lines_covered": 572,
"lines_valid": 652
},
"App/cfg/prot_set_store.c": {
"func_rate": 1.0,
"funcs_covered": 4,
"funcs_valid": 4,
"line_rate": 0.931,
"lines_covered": 27,
"lines_valid": 29
},
"App/fs/vol_lock.c": {
"func_rate": 1.0,
"funcs_covered": 3,
@@ -128,6 +136,14 @@
"lines_covered": 156,
"lines_valid": 157
},
"App/util/task_prof.c": {
"func_rate": 1.0,
"funcs_covered": 9,
"funcs_valid": 9,
"line_rate": 0.9462,
"lines_covered": 88,
"lines_valid": 93
},
"App/util/task_watchdog.c": {
"func_rate": 1.0,
"funcs_covered": 8,
@@ -151,6 +167,22 @@
"line_rate": 0.9737,
"lines_covered": 37,
"lines_valid": 38
},
"Drivers/BSP/NETCFG/ftp_cfg.c": {
"func_rate": 1.0,
"funcs_covered": 4,
"funcs_valid": 4,
"line_rate": 0.9722,
"lines_covered": 35,
"lines_valid": 36
},
"Drivers/BSP/NETCFG/net_cfg.c": {
"func_rate": 0.9375,
"funcs_covered": 15,
"funcs_valid": 16,
"line_rate": 0.9643,
"lines_covered": 108,
"lines_valid": 112
}
},
"generated_by": "scripts/check_test_coverage.py --update-baseline",
@@ -193,6 +225,13 @@
"lines_total": 152,
"ok": true
},
"ftp_cfg": {
"files": {
"Drivers/BSP/NETCFG/ftp_cfg.c": 36
},
"lines_total": 36,
"ok": true
},
"iec104": {
"files": {
"App/algo/freq.c": 130,
@@ -214,6 +253,13 @@
"lines_total": 253,
"ok": true
},
"net_cfg": {
"files": {
"Drivers/BSP/NETCFG/net_cfg.c": 112
},
"lines_total": 112,
"ok": true
},
"phasor": {
"files": {
"App/algo/phasor.c": 55,
@@ -240,6 +286,14 @@
"lines_total": 179,
"ok": true
},
"prot_set_store": {
"files": {
"App/algo/prot_set.c": 179,
"App/cfg/prot_set_store.c": 29
},
"lines_total": 208,
"ok": true
},
"sha256": {
"files": {
"App/util/sha256.c": 114
@@ -262,6 +316,13 @@
"lines_total": 157,
"ok": true
},
"task_prof": {
"files": {
"App/util/task_prof.c": 93
},
"lines_total": 93,
"ok": true
},
"time_sync": {
"files": {
"App/sync/time_sync.c": 99
@@ -295,6 +356,7 @@
"App/algo/prot_set.c",
"App/algo/sample_ring.c",
"App/cfg/cfg_file.c",
"App/cfg/prot_set_store.c",
"App/fs/vol_lock.c",
"App/log/soe_rec.c",
"App/protocol/iec104/iec104_asdu.c",
@@ -303,22 +365,25 @@
"App/sync/time_sync.c",
"App/util/sha256.c",
"App/util/sys_clock.c",
"App/util/task_prof.c",
"App/util/task_watchdog.c",
"Drivers/BSP/GD5F2GQ5UE/nand_ftl.c",
"Drivers/BSP/NET/lftpd/lftpd_auth.c"
"Drivers/BSP/NET/lftpd/lftpd_auth.c",
"Drivers/BSP/NETCFG/ftp_cfg.c",
"Drivers/BSP/NETCFG/net_cfg.c"
],
"totals": {
"files_generated_excluded": 5,
"files_tested": 19,
"files_untested": 125,
"func_rate": 0.9324,
"funcs_covered": 193,
"funcs_valid": 207,
"line_rate": 0.8821,
"lines_covered": 2552,
"lines_valid": 2893,
"modules_ok": 16,
"modules_total": 16
"files_tested": 23,
"files_untested": 121,
"func_rate": 0.9375,
"funcs_covered": 225,
"funcs_valid": 240,
"line_rate": 0.8887,
"lines_covered": 2811,
"lines_valid": 3163,
"modules_ok": 20,
"modules_total": 20
},
"untested_sources": [
{
@@ -405,18 +470,10 @@
"code_lines": 171,
"path": "Drivers/BSP/NET/lftpd/lftpd_inet.c"
},
{
"code_lines": 167,
"path": "Drivers/BSP/NETCFG/net_cfg.c"
},
{
"code_lines": 162,
"path": "Drivers/BSP/NET/net_select.c"
},
{
"code_lines": 160,
"path": "App/util/task_prof.c"
},
{
"code_lines": 157,
"path": "Drivers/BSP/TPAFE5160/tpafe5160.c"
@@ -489,10 +546,6 @@
"code_lines": 62,
"path": "App/protocol/iec104/iec104_asdu.h"
},
{
"code_lines": 59,
"path": "Drivers/BSP/NETCFG/ftp_cfg.c"
},
{
"code_lines": 56,
"path": "App/protocol/iec104/iec104_link.h"
@@ -517,10 +570,6 @@
"code_lines": 44,
"path": "App/log/soe_rec.h"
},
{
"code_lines": 43,
"path": "App/cfg/prot_set_store.c"
},
{
"code_lines": 42,
"path": "Drivers/BSP/UI/ui_content_page.h"
+244
View File
@@ -0,0 +1,244 @@
/*
* 模块名称:FTP 鉴权配置([ftp] 段)主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/NETCFG/ftp_cfg.c 的注册契约与
* "出厂默认口令是否在用"的判定(大小写不敏感)。
*
* 被测对象:真实源文件 ../../../Drivers/BSP/NETCFG/ftp_cfg.c(#include 原样纳入)。
* 桩只有一个:真实头 App/cfg/cfg_file.h + 本文件给出的 cfg_file_register
* 可控实现(按值留档注册描述符)。本模块**零 stub 目录**——ftp_cfg.c 不
* 包含 dbg_log.h,也没有别的平台依赖。
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. 这个模块是 **FTP 的门禁**(N-19):pwd_hash 存的是 SHA-256(user‖password),
* 判定写成大小写敏感就会在"用户手输大写 hex"时把正确口令当错误;写成敏感于
* 默认值大小写则会误报"已改口令",让出厂默认口令的启动告警**静默失效**——
* 现场表现是"以为改了口令"。
* 2. [ftp] 段刻意**不挂变更回调**(口令在下次登录 PASS 时才生效):哪天有人"顺手
* 补一个回调",poll 重载会去重建网络——这条契约只能靠断言钉住(TC-FTPCFG-004)。
* 3. write_en 出厂 = 1 是刻意的(FTP 推 config.ini 是既有维护工作流),改成 0
* 会让现场升级流程直接失败;同样需要一个显式断言(TC-FTPCFG-005)。
* ⚠ DEF_HASH 这个常量本身的正确性由 test/host/sha256/ 的 TC-SHA256-008 钉
* (它算 SHA256("adminadmin") 并与 FTP_CFG_DEF_HASH 比对),本模块不重复算。
*
* 覆盖点:
* TC-FTPCFG-001a get 返回恒非空(静态常驻,load 前后都可用)
* TC-FTPCFG-001 未注册即可 get:user=admin(出厂默认口令在用)
* TC-FTPCFG-002 pwd_hash 初值 = FTP_CFG_DEF_HASH,write_en 初值 = 1
* TC-FTPCFG-003 注册幂等:重复 register 只向引擎登记一次
* TC-FTPCFG-004 注册内容:段名 ftp / 三键 / 尺寸 / 默认值指针 / **p_cb 必须为 NULL**
* TC-FTPCFG-005 键表逐项:user(STR,16) / pwd_hash(STR,65) / write(UINT,1)
* TC-FTPCFG-006 出厂态 is_default_pwd = 1
* TC-FTPCFG-007 改了口令 → is_default_pwd = 0
* TC-FTPCFG-008 哈希存成**大写** hex 仍判"默认口令在用"(大小写不敏感)
* TC-FTPCFG-009 空哈希不是默认口令(且不读越界)
* TC-FTPCFG-010 混入非字母字符的串按逐字符比较(数字/下划线不被折叠改写)
* TC-FTPCFG-011 前缀关系不算相等("abc" vs "abcd")
* TC-FTPCFG-012 任一参数为 NULL 判不等(is_default_pwd 走不到,故直测静态函数)
* TC-FTPCFG-013 两串都为 NULL 也判不等(fail-closed,不许"都为空=相等")
* TC-FTPCFG-014 缓冲尺寸契约:user 16 / hash 65(64 hex + NUL)
*
* 用例顺序有依赖:001/002/006 必须在任何"改写 RAM 副本"的用例之前跑。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <string.h>
#include "cfg_file.h"
#include "ftp_cfg.h"
#include "ftp_cfg.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)
/* ===================== 假引擎:只留注册记录 ===================== */
typedef struct {
char name[CFG_FILE_MAX_NAME_LEN + 1u];
const void *p_data;
const void *p_defaults;
size_t data_size;
uint8_t key_count;
void (*p_cb)(void);
const cfg_key_def_t *p_keys;
} fake_reg_t;
static fake_reg_t g_reg;
static int g_reg_n;
static void fake_copy_name(char *dst, const char *src, size_t cap)
{
size_t i;
for (i = 0u; (i + 1u < cap) && (src[i] != '\0'); i++) {
dst[i] = src[i];
}
dst[i] = '\0';
}
int cfg_file_register(const cfg_section_reg_t *p_reg)
{
if ((p_reg == NULL) || (p_reg->name == NULL)) {
return -1;
}
fake_copy_name(g_reg.name, p_reg->name, sizeof(g_reg.name));
g_reg.p_data = p_reg->p_data;
g_reg.p_defaults = p_reg->p_defaults;
g_reg.data_size = p_reg->data_size;
g_reg.key_count = p_reg->key_count;
g_reg.p_cb = p_reg->p_cb;
g_reg.p_keys = p_reg->p_keys;
g_reg_n++;
return 0;
}
static void fake_reset(void)
{
(void)memset(&g_reg, 0, sizeof(g_reg));
g_reg_n = 0;
}
/* 把 s_cfg.pwd_hash 写成任意串(白盒:引擎就是这么原地更新 RAM 副本的) */
static void set_hash(const char *p_src)
{
size_t i;
for (i = 0u; (i + 1u < FTP_CFG_HASH_MAX) && (p_src[i] != '\0'); i++) {
s_cfg.pwd_hash[i] = p_src[i];
}
s_cfg.pwd_hash[i] = '\0';
}
/* 把出厂哈希原样写成大写形态(引擎写入的 hex 大小写不可控,判定必须容忍) */
static void set_hash_upper(void)
{
size_t i;
for (i = 0u; i + 1u < FTP_CFG_HASH_MAX; i++) {
char ch = FTP_CFG_DEF_HASH[i];
if ((ch >= 'a') && (ch <= 'z')) {
ch = (char)(ch - 32);
}
s_cfg.pwd_hash[i] = ch;
if (ch == '\0') {
break;
}
}
}
/* ===================== 注册契约 ===================== */
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 首次注册一次");
ftp_cfg_register();
CHECK(g_reg_n == 1, "TC-FTPCFG-003 重复 register 不重复登记(幂等)");
CHECK((strcmp(g_reg.name, "ftp") == 0) &&
(g_reg.data_size == sizeof(ftp_cfg_t)) &&
(g_reg.key_count == 3u) &&
(g_reg.p_defaults != NULL) &&
(g_reg.p_data == (const void *)&s_cfg) &&
(g_reg.p_cb == NULL),
"TC-FTPCFG-004 段名/尺寸/键数/默认值/**p_cb 必须为 NULL**");
CHECK((strcmp(g_reg.p_keys[0].name, "user") == 0) &&
(g_reg.p_keys[0].type == CFG_VAL_STR) &&
(g_reg.p_keys[0].len == FTP_CFG_USER_MAX) &&
(g_reg.p_keys[0].offset == offsetof(ftp_cfg_t, user)) &&
(strcmp(g_reg.p_keys[1].name, "pwd_hash") == 0) &&
(g_reg.p_keys[1].type == CFG_VAL_STR) &&
(g_reg.p_keys[1].len == FTP_CFG_HASH_MAX) &&
(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)");
}
/* ===================== 出厂默认与只读访问 ===================== */
static void tc_defaults(void)
{
const ftp_cfg_t *p = ftp_cfg_get();
printf("-- TC-FTPCFG-001..002,014 defaults --\n");
/* 契约就是"恒非空"(静态常驻),单独一条断言;随后的显式守卫既让
* test-cppcheck 不再报"冗余或空指针二选一",也让真出问题时报在 001a 上 */
CHECK(p != NULL, "TC-FTPCFG-001a ftp_cfg_get 恒非空(静态常驻)");
if (p == NULL) {
return;
}
CHECK(strcmp(p->user, "admin") == 0, "TC-FTPCFG-001 未注册也可 get,user = admin");
CHECK((strcmp(p->pwd_hash, FTP_CFG_DEF_HASH) == 0) && (p->write_en == 1u),
"TC-FTPCFG-002 初值 = 出厂哈希 + write_en=1(维护工作流依赖)");
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)");
}
/* ===================== 默认口令判定(启动告警的依据) ===================== */
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 出厂态判为默认口令");
set_hash("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-007 改了口令 → 不再默认");
set_hash_upper();
CHECK(ftp_cfg_is_default_pwd() == 1,
"TC-FTPCFG-008 大写 hex 仍判默认(比较必须大小写不敏感)");
set_hash("");
CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-009 空哈希不是默认口令");
}
/* ===================== 静态比较函数(NULL / 折叠 / 前缀) ===================== */
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 前缀关系不算相等");
CHECK((s_str_ci_eq(NULL, "x") == 0) && (s_str_ci_eq("x", NULL) == 0),
"TC-FTPCFG-012 任一为 NULL 判不等");
CHECK(s_str_ci_eq(NULL, NULL) == 0,
"TC-FTPCFG-013 两个 NULL 也判不等(fail-closed)");
}
/* ===================== 入口 ===================== */
int main(void)
{
printf("=== FTP Auth Config Tests ===\n");
tc_defaults(); /* 必须最先:之后会改写 RAM 副本 */
tc_register();
tc_default_pwd();
tc_ci_eq();
printf("\n=== FTP Auth Config Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""FTP 鉴权配置(ftp_cfg,[ftp] 段)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/ftp_cfg/ftp_cfg_test.c(以 #include 纳入**生产源文件**
Drivers/BSP/NETCFG/ftp_cfg.c),在 PC 上离线跑注册契约(幂等、键表、**不挂回调**)
与出厂默认口令判定(大小写不敏感、NULL fail-closed),失败即非 0 退出。
工具链探测与判定全部委托给 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只用台架):
本模块是 FTP 的门禁(N-19)。判定写成大小写敏感 → 用户手输大写 hex 时正确口令被拒;
反过来对出厂值敏感 → "已改口令"误报,出厂默认口令的启动告警**静默失效**(现场以为
改了口令)。[ftp] 段刻意不挂变更回调(口令下次登录才生效),有人"顺手补上"就会在
poll 重载时去重建网络。write_en 出厂 = 1 也是刻意的(FTP 推 config.ini 是既有维护
工作流)。这些都是一行代码就能改错、而真机只看得到 230/530 的东西。
哈希常量本身的正确性归 test/host/sha256/(TC-SHA256-008),本模块不重复算。
用法(仓库根目录):
python test/host/ftp_cfg/run_unit_test.py
或统一用 .venv: & '.venv\Scripts\python.exe' test/host/ftp_cfg/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:docs/子系统设计/网络配置说明.md §3.4、docs/测试说明/主机单测覆盖率口径.md §9 P1
创建日期:2026-10-23
"""
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
# 本模块零桩目录:ftp_cfg.c 不含 dbg_log.h,也没有别的平台依赖;
# cfg_file.h 用真实头(接口原汁),被调的 cfg_file_register 由测试单元给实现。
BUILD = TestBuild(
name="FTP Auth Config Tests",
src=os.path.join(HERE, "ftp_cfg_test.c"),
include_dirs=[
os.path.join(ROOT, "Drivers", "BSP", "NETCFG"),
os.path.join(ROOT, "App", "cfg"),
HERE,
],
summary_marker="=== FTP Auth Config Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+549
View File
@@ -0,0 +1,549 @@
/*
* 模块名称:网络配置(NETCFG)主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/NETCFG/net_cfg.c 的**编排与状态机**:
* 出厂默认有效性、注册幂等与注册内容、load 失败仍返回 0 的"RAM 恒有效"
* 契约、跨任务保存请求交接(IDLE/WAIT/OK/FAIL)、写盘期间新请求不丢、
* 写盘失败不得触发网络重建、MAC 三态优先级、IPv4 定长格式化。
*
* 被测对象:真实源文件 ../../../Drivers/BSP/NETCFG/net_cfg.c(#include 原样纳入,
* 保证测的是生产代码而非副本)。桩只有三处,都写在下面:
* - stub/dbg_log.h:日志宏 → 计数(本目录);
* - 配置文件引擎:用**真实头** App/cfg/cfg_file.h(接口契约原汁,引擎
* 一改签名本测试立刻编译期报错),被调的三个函数
* cfg_file_register / cfg_file_load / cfg_file_save 由本文件给出
* **可控实现**(同 test/host/nand_port/ 的路子);
* - ftp_cfg_register:只计数不实现——[ftp] 段自身的解析/默认值/默认
* 口令判定归 test/host/ftp_cfg/ 模块,两边不重复也不互相顶包。
* (不能把 ftp_cfg.c 也 include 进来:两个 .c 都有 static s_cfg,
* 同一个 TU 里直接重定义。)
* ⚠ 引擎的解析/写盘/沉降契约已由 test/host/cfg_file/(103 条)覆盖,
* 本模块刻意不重测引擎,只测 net_cfg.c 这一层的编排与交接。
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. 保存链是**跨任务交接**(uiTask 投递 net_cfg_save_request → defaultTask 执行
* net_cfg_save_run → 成功才 net_cfg_request_apply → netTask 按网络代次重建监听)。
* "写盘期间 UI 又按了一次 ENTER"与"写盘失败却去重建网络"这两条,真机上要靠
* 手速 + 故障注入才碰得到;错了就是"改了 IP 不生效"或"保存失败网络被无端重建"
* 这类现场极难定位的问题——主机侧用假引擎的写盘钩子直接构造时序。
* 2. MAC 三态优先级(存储非零 > 芯片回写影子 > 全零)决定 UI 显示值和下发给
* CH395F 的 MAC;写错会**静默用错 MAC**(同网段 MAC 冲突,症状是偶发丢包)。
* 3. net_cfg_fmt_ipv4 的**定长 15 字符(每段 %03u 补零)**是 UI 编辑光标按固定偏移定位
* 字节的前提;哪天被"优化"成不补零,光标就错位——纯字符串行为,一次钉死。
*
* 覆盖点:
* —— 出厂默认(load 之前也必须可用,TC-NETCFG-001..004)——
* TC-NETCFG-001 未 load 时 ip = 工厂默认 192.168.1.100
* TC-NETCFG-002 未 load 时 mask = 255.255.255.0
* TC-NETCFG-003 未 load 时 gw = 192.168.1.1
* TC-NETCFG-004 未 load 时 mac = 全零(哨兵 = 不覆盖芯片内置 MAC)
* —— 注册与 load(TC-NETCFG-005..011)——
* TC-NETCFG-005 首次 load 注册 [network] 段恰好一次(且 load 返回 0)
* TC-NETCFG-006 首次 load 一并注册 [ftp] 段(N-19:所有受管段先注册、后 load)
* TC-NETCFG-007 重复 load 不重复注册(幂等;否则引擎按"段名重复"拒登 → 静默失配)
* TC-NETCFG-008 注册描述符内容:段名/尺寸/键数/默认值指针/变更回调/数据指针 = RAM 副本
* TC-NETCFG-009 load 成功:返回 0 且留一条 INFO、不误报 ERROR
* TC-NETCFG-010 **引擎报 -1 时 net_cfg_load 仍返回 0**(RAM 恒有效,不带崩网络启动)
* TC-NETCFG-011 load 失败必须留 ERROR(不许静默回落默认值)
* —— RAM 副本读写(TC-NETCFG-012..013)——
* TC-NETCFG-012 net_cfg_set 写入被 net_cfg_get 读回
* TC-NETCFG-013 net_cfg_set(NULL) 不改写(UI 侧误传不得毁掉配置)
* —— 直接保存(TC-NETCFG-014..015)——
* TC-NETCFG-014 引擎写盘成功 → net_cfg_save 返回 0
* TC-NETCFG-015 引擎写盘失败 → net_cfg_save 返回 -1 并留痕(错误码没被吞成 0)
* —— 跨任务保存交接状态机(TC-NETCFG-020..033,UI 保存行显示的就是它)——
* TC-NETCFG-020 初始态 = IDLE
* TC-NETCFG-021 save_request 后置 WAIT
* TC-NETCFG-022 save_request 经已注册回调唤醒属主任务(计数 +1)
* TC-NETCFG-023 未注册回调时 save_request 不崩、请求照样挂起
* TC-NETCFG-024a 有请求时 save_run 确实写盘一次(024 的前置)
* TC-NETCFG-024 无请求时空醒 → 返回 0 且**不写盘**(2s 周期轮询不刷 Flash)
* TC-NETCFG-025 save_run 成功后 pending 清零
* TC-NETCFG-026 save_run 成功后状态 = OK
* TC-NETCFG-027 save_run 成功后触发一次"立即应用"
* TC-NETCFG-028 **写盘进行中**状态仍是 WAIT(写盘前先清 pending,靠 busy 撑住;
* 否则这段窗口会退回上一轮的 OK/IDLE,用户看不到反馈只会再按一次)
* TC-NETCFG-029a **写盘期间投递的新请求不丢**:本轮结束后仍为 WAIT
* TC-NETCFG-029b 下一轮真的再写一次(不是只把状态挂在那里)
* TC-NETCFG-030 写盘失败 → 状态 = FAIL
* TC-NETCFG-031 **写盘失败不得触发"立即应用"**(内存值还没落盘,不能当已生效)
* TC-NETCFG-032 未注册应用回调时 request_apply 是空操作(模拟器就是这条路)
* TC-NETCFG-033a 应用回调注册后可被触发
* TC-NETCFG-033b 传 NULL 注销后不再被触发
* —— MAC 三态优先级(TC-NETCFG-040..045)——
* TC-NETCFG-040 存储 MAC 非零 → 用存储值(即使芯片已回写影子)
* TC-NETCFG-041 存储全零 + 芯片已回写 → 用影子值
* TC-NETCFG-042 存储全零 + 未回写 → 全零
* TC-NETCFG-043 set_active_mac(NULL) 不得把影子置为有效
* TC-NETCFG-044 set_active_mac 正常回写后影子生效
* TC-NETCFG-045 get_mac(NULL) 安全返回
* —— IPv4 定长格式化(TC-NETCFG-050..056,UI 光标定位依赖)——
* TC-NETCFG-050 常规地址渲染为定长 15 字符 "192.168.001.100"
* TC-NETCFG-051 全零地址补零为 "000.000.000.000"(仍是 15 字符,偏移不变)
* TC-NETCFG-052 255 边界 "255.255.255.255"
* TC-NETCFG-053 p_ip 为 NULL:不写缓冲
* TC-NETCFG-054 p_buf 为 NULL:不崩
* TC-NETCFG-055 len 为 0:不写缓冲
* TC-NETCFG-056 len 不足:截断但**必须 NUL 结尾**且不越界(UI 缓冲防线)
*
* 用例顺序有依赖(模块静态状态无法整体复位):出厂默认组必须最先跑(任何 load 之前);
* IDLE 组在任何 save_request 之前;需要干净初态处显式复位白盒量并注明原因。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <string.h>
/* 真实引擎头(接口原汁)+ 被测生产源(原样纳入);dbg_log.h 命中本目录 stub */
#include "cfg_file.h"
#include "net_cfg.h"
#include "net_cfg.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)
/* ===================== dbg_log 桩的计数定义 ===================== */
int g_netcfg_info_count;
int g_netcfg_error_count;
int g_netcfg_debug_count;
static void logs_clear(void)
{
g_netcfg_info_count = 0;
g_netcfg_error_count = 0;
g_netcfg_debug_count = 0;
}
/* ===================== 假引擎(可控实现;原型来自真实 cfg_file.h) ========== */
/* 注册描述符在 net_cfg.c 里是栈上临时量,必须**按值留档**后才可比对 */
typedef struct {
char name[CFG_FILE_MAX_NAME_LEN + 1u];
const void *p_data;
const void *p_defaults;
size_t data_size;
uint8_t key_count;
void (*p_cb)(void);
} fake_reg_t;
static fake_reg_t g_regs[CFG_FILE_MAX_SECTIONS];
static int g_reg_n;
static int g_ftp_reg_calls; /* ftp_cfg_register 被 net_cfg_load 调了几次 */
static int g_load_calls;
static int g_save_calls;
static int g_apply_calls;
static int g_req_cb_calls;
static int g_load_rc; /* 注入:cfg_file_load 返回值 */
static int g_save_rc; /* 注入:cfg_file_save 返回值 */
static int g_state_during_save = -1;/* 写盘钩子里读到的交接状态 */
static int g_requeue_during_save; /* 1 = 写盘中再投递一次请求 */
static void (*g_save_hook)(void); /* 写盘时刻回调(构造真机难复现的时序) */
static void fake_copy_name(char *dst, const char *src, size_t cap)
{
size_t i;
for (i = 0u; (i + 1u < cap) && (src[i] != '\0'); i++) {
dst[i] = src[i];
}
dst[i] = '\0';
}
int cfg_file_register(const cfg_section_reg_t *p_reg)
{
fake_reg_t *rec;
if ((p_reg == NULL) || (p_reg->name == NULL)) {
return -1;
}
if ((size_t)g_reg_n >= (size_t)CFG_FILE_MAX_SECTIONS) {
return -1;
}
rec = &g_regs[g_reg_n];
fake_copy_name(rec->name, p_reg->name, sizeof(rec->name));
rec->p_data = p_reg->p_data;
rec->p_defaults = p_reg->p_defaults;
rec->data_size = p_reg->data_size;
rec->key_count = p_reg->key_count;
rec->p_cb = p_reg->p_cb;
g_reg_n++;
return 0;
}
int cfg_file_load(void)
{
g_load_calls++;
return g_load_rc;
}
int cfg_file_save(void)
{
g_save_calls++;
if (g_save_hook != NULL) {
g_save_hook();
}
return g_save_rc;
}
/* [ftp] 段归 test/host/ftp_cfg/ 模块测;这里只验证"net_cfg_load 确实带上了它" */
void ftp_cfg_register(void)
{
g_ftp_reg_calls++;
}
static void fake_reset(void)
{
g_reg_n = 0;
g_ftp_reg_calls = 0;
g_load_calls = 0;
g_save_calls = 0;
g_apply_calls = 0;
g_req_cb_calls = 0;
g_load_rc = 0;
g_save_rc = 0;
g_state_during_save = -1;
g_requeue_during_save = 0;
g_save_hook = NULL;
logs_clear();
}
static int mac_eq(const uint8_t *p_a, const uint8_t *p_b)
{
return memcmp(p_a, p_b, 6u) == 0;
}
static int bytes_eq(const uint8_t *p_a, const uint8_t *p_b, size_t n)
{
return memcmp(p_a, p_b, n) == 0;
}
/* ===================== 回调 ===================== */
static void apply_cb(void)
{
g_apply_calls++;
}
static void req_cb(void)
{
g_req_cb_calls++;
}
/* 写盘时刻钩子:记录当时的交接状态,并按开关模拟"写盘期间 UI 又按了一次 ENTER" */
static void save_hook_probe(void)
{
g_state_during_save = net_cfg_save_state();
if (g_requeue_during_save != 0) {
(void)net_cfg_save_request();
}
}
/* ===================== 出厂默认(必须在任何 load 之前跑) ===================== */
static void tc_defaults(void)
{
const net_cfg_t *p = net_cfg_get();
static const uint8_t def_ip[4] = { 192u, 168u, 1u, 100u };
static const uint8_t def_mask[4] = { 255u, 255u, 255u, 0u };
static const uint8_t def_gw[4] = { 192u, 168u, 1u, 1u };
static const uint8_t zero[6] = { 0u, 0u, 0u, 0u, 0u, 0u };
printf("-- TC-NETCFG-001..004 defaults before load --\n");
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 = 全零(芯片内置哨兵)");
}
/* ===================== 注册与 load 契约 ===================== */
static void tc_register_load(void)
{
int rc;
printf("-- TC-NETCFG-005..011 register + load --\n");
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] 恰好一次");
CHECK((g_ftp_reg_calls == 1) && (g_load_calls == 1),
"TC-NETCFG-006 一并注册 [ftp],且注册在 load 之前");
rc = net_cfg_load();
CHECK((rc == 0) && (g_reg_n == 1) && (g_ftp_reg_calls == 2),
"TC-NETCFG-007 重复 load 不重复注册 [network]");
CHECK((strcmp(g_regs[0].name, "network") == 0) &&
(g_regs[0].data_size == sizeof(net_cfg_t)) &&
(g_regs[0].key_count == 4u) &&
(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 副本");
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");
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");
}
/* ===================== RAM 副本读写 ===================== */
static void tc_set_get(void)
{
net_cfg_t c;
static const uint8_t ip[4] = { 10u, 0u, 0u, 7u };
static const uint8_t mac[6] = { 0x02u, 0x11u, 0x22u, 0x33u, 0x44u, 0x55u };
printf("-- TC-NETCFG-012..013 set/get --\n");
c = *net_cfg_get();
memcpy(c.ip, ip, 4u);
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 读回");
net_cfg_set(NULL);
CHECK(bytes_eq(net_cfg_get()->ip, ip, 4u), "TC-NETCFG-013 set(NULL) 不改写");
}
/* ===================== 直接保存 ===================== */
static void tc_save_direct(void)
{
int rc;
printf("-- TC-NETCFG-014..015 save --\n");
fake_reset();
rc = net_cfg_save();
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-014 写盘成功返回 0");
fake_reset();
g_save_rc = -1;
rc = net_cfg_save();
CHECK((rc == -1) && (g_netcfg_error_count == 1),
"TC-NETCFG-015 写盘失败返回 -1 并留痕");
}
/* ===================== 跨任务保存交接状态机 ===================== */
static void tc_save_handoff(void)
{
int rc;
printf("-- TC-NETCFG-020..033 save handoff state machine --\n");
fake_reset();
net_cfg_set_apply_cb(NULL);
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_request() == 0) &&
(net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT), "TC-NETCFG-021 投递后 WAIT");
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 投递经回调唤醒属主任务");
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 未注册回调时空操作,请求照样挂起");
g_save_rc = 0;
g_save_calls = 0;
rc = net_cfg_save_run();
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-024a 有请求时确实写盘一次");
g_save_calls = 0;
rc = net_cfg_save_run();
CHECK((rc == 0) && (g_save_calls == 0),
"TC-NETCFG-024 无请求时空醒不写盘(2s 轮询不刷 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((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 成功后触发一次立即应用");
(void)net_cfg_save_request();
g_save_hook = save_hook_probe;
g_state_during_save = -1;
g_apply_calls = 0;
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");
/* 写盘期间再投递一次:本轮不得把它当已处理,下轮必须真的再写 */
g_requeue_during_save = 1;
(void)net_cfg_save_request();
g_save_calls = 0;
rc = net_cfg_save_run();
CHECK((rc == 0) && (net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT),
"TC-NETCFG-029a 写盘期间的新请求没丢");
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 下一轮真的再写一次");
/* 写盘失败:置 FAIL 且**不得**触发应用(内存值没落盘,网络不能重建) */
s_save_pending = 0u;
s_save_result = 0u;
g_apply_calls = 0;
(void)net_cfg_save_request();
g_save_rc = -1;
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 写盘失败不得触发立即应用");
net_cfg_set_apply_cb(NULL);
g_apply_calls = 0;
net_cfg_request_apply();
CHECK(g_apply_calls == 0, "TC-NETCFG-032 未注册应用回调时是空操作");
net_cfg_set_apply_cb(apply_cb);
net_cfg_request_apply();
CHECK(g_apply_calls == 1, "TC-NETCFG-033a 注册后可被触发");
net_cfg_set_apply_cb(NULL);
net_cfg_request_apply();
CHECK(g_apply_calls == 1, "TC-NETCFG-033b 注销后不再被触发");
}
/* ===================== MAC 三态优先级 ===================== */
static void tc_mac_priority(void)
{
net_cfg_t c;
uint8_t out[6];
static const uint8_t stored[6] = { 0xAAu, 0xBBu, 0xCCu, 0x11u, 0x22u, 0x33u };
static const uint8_t shadow[6] = { 0x02u, 0x00u, 0x00u, 0x01u, 0x02u, 0x03u };
static const uint8_t zero[6] = { 0u, 0u, 0u, 0u, 0u, 0u };
printf("-- TC-NETCFG-040..045 mac display priority --\n");
c = *net_cfg_get();
memcpy(c.mac, stored, 6u);
net_cfg_set(&c);
net_cfg_set_active_mac(shadow);
memset(out, 0xFF, sizeof(out));
net_cfg_get_mac(out);
CHECK(mac_eq(out, stored), "TC-NETCFG-040 存储非零优先于芯片影子");
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 存储全零则用芯片回写影子");
s_active_valid = 0u; /* 白盒:回到"芯片还没回写"的上电初态 */
memset(out, 0xFF, sizeof(out));
net_cfg_get_mac(out);
CHECK(mac_eq(out, zero), "TC-NETCFG-042 两者皆无 → 全零");
net_cfg_set_active_mac(NULL);
CHECK(s_active_valid == 0u, "TC-NETCFG-043 NULL 回写不得把影子置有效");
net_cfg_set_active_mac(shadow);
memset(out, 0xFF, sizeof(out));
net_cfg_get_mac(out);
CHECK(mac_eq(out, shadow), "TC-NETCFG-044 正常回写后影子生效");
net_cfg_get_mac(NULL);
CHECK(1, "TC-NETCFG-045 get_mac(NULL) 安全返回");
}
/* ===================== IPv4 定长格式化 ===================== */
static void tc_fmt_ipv4(void)
{
static const uint8_t ip1[4] = { 192u, 168u, 1u, 100u };
static const uint8_t ip0[4] = { 0u, 0u, 0u, 0u };
static const uint8_t ipm[4] = { 255u, 255u, 255u, 255u };
char buf[24];
char guard[24];
printf("-- TC-NETCFG-050..056 fixed-width ipv4 format --\n");
memset(buf, 0x7A, sizeof(buf));
net_cfg_fmt_ipv4(ip1, buf, sizeof(buf));
/* 定长 = 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 字符且每段补零");
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 字符");
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 边界");
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 不写缓冲");
net_cfg_fmt_ipv4(ip1, NULL, 16u);
CHECK(1, "TC-NETCFG-054 p_buf NULL 不崩");
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 不写缓冲");
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 结尾且不越界");
}
/* ===================== 入口 ===================== */
int main(void)
{
printf("=== NET Config Tests ===\n");
tc_defaults(); /* 必须最先:任何 load 之前 */
tc_register_load();
tc_set_get();
tc_save_direct();
tc_save_handoff();
tc_mac_priority();
tc_fmt_ipv4();
printf("\n=== NET Config Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""网络配置(net_cfg)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/net_cfg/net_cfg_test.c(它以 #include 纳入
**生产源文件** Drivers/BSP/NETCFG/net_cfg.c),在 PC 上离线跑出厂默认、
注册幂等、load/save 错误通道、跨任务保存交接状态机、MAC 三态优先级、
IPv4 定长格式化,失败即非 0 退出。零硬件、零烧录、毫秒级。
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
`test/host/common/unit_test_harness.py`(本文件只做声明)。
为什么要主机侧单测(而不是只用台架):
保存链跨三个任务(uiTask 投递 → defaultTask 写盘 → netTask 重建网络):
"写盘期间又投递一次"与"写盘失败却重建网络"这两条真机上要靠手速 + 故障注入
才碰得到,错了就是"改 IP 不生效 / 保存失败网络被无端重建";MAC 三态优先级
写错会静默用错 MAC(同网段冲突,只表现为偶发丢包);net_cfg_fmt_ipv4 的定长
11 字符是 UI 光标按固定偏移定位的前提。三条都在 PC 上钉死,且不占 MCU RAM。
桩的边界(细节见测试单元文件头):
配置文件引擎用**真实头**(接口原汁)+ 测试单元内的可控实现;[ftp] 段的
ftp_cfg_register 只计数——它归 test/host/ftp_cfg/ 模块,两边不互相顶包。
用法(仓库根目录):
python test/host/net_cfg/run_unit_test.py
或统一用 .venv: & '.venv\Scripts\python.exe' test/host/net_cfg/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:docs/子系统设计/网络配置说明.md、docs/测试说明/主机单测覆盖率口径.md §9 P1
创建日期:2026-10-23
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
# harness 位于 test/host/common/
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
# 顺序敏感:stub 必须**先于**生产目录,才能覆盖 dbg_log.h。cfg_file.h 用真实头
# (接口契约原汁,引擎改签名即编译期报错),net_cfg.h / ftp_cfg.h 亦用真实头。
BUILD = TestBuild(
name="NET Config Tests",
src=os.path.join(HERE, "net_cfg_test.c"),
include_dirs=[
os.path.join(HERE, "stub"),
os.path.join(ROOT, "Drivers", "BSP", "NETCFG"),
os.path.join(ROOT, "App", "cfg"),
HERE,
],
summary_marker="=== NET Config Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+27
View File
@@ -0,0 +1,27 @@
/*
* 模块名称:主机侧单测桩 - dbg_log.h(net_cfg 模块专用)
* 模块功能:把 MCU 版 DBG_* 宏替换为**计数**,让"该留痕的有没有留痕"变成可断言
* 的行为;同时不往 stdout 打日志——汇总行是 harness 的判据,必须干净。
*
* ⚠ 仅供 test/host/net_cfg/ 使用,不参与 MCU 构建。
*
* 为什么是计数而不是 printf 直通:net_cfg.c 的三条日志恰好对应三条契约
* (load OK / load FAIL / save OK|FAIL),计数能钉住"哪条该响、哪条不该响"
* ——例如"写盘失败必须留痕、但不得触发应用"这类只能靠计数判的分支。
* 计数器定义在 net_cfg_test.c,每组用例前清零。
*
* 创建日期:2026-10-23
*/
#ifndef DBG_LOG_H
#define DBG_LOG_H
extern int g_netcfg_info_count;
extern int g_netcfg_error_count;
extern int g_netcfg_debug_count;
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_netcfg_info_count++; } while (0)
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_netcfg_debug_count++; } while (0)
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_netcfg_error_count++; } while (0)
#endif /* DBG_LOG_H */
@@ -0,0 +1,355 @@
/*
* 模块名称:保护定值持久化胶水层(prot_set_store)主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/... 侧的平台胶水 App/cfg/prot_set_store.c 的
* **返回码分流**与**脏标志回置**:读不到=1(按无档处理)、装载成功=0、
* 版本/项数换代=2 且只打 INFO、magic 或校验被破坏=-1;保存无脏不写盘、
* 写失败必须把脏标志还回去(否则一轮卷忙就永久丢定值)。
*
* 被测对象:真实源文件 ../../../App/cfg/prot_set_store.c,并且**同时纳入真实的**
* ../../../App/cfg/prot_set.c —— 因为 blob 的 serialize/parse/脏标志都在
* prot_set.c 里(纯逻辑,已另有 test/host/prot_set/ 覆盖其算法)。本模块
* 不重测算法,只测"胶水层在真实 blob 上把返回码分对没有":只有拿真
* serialize 产出的 blob 去改 magic/ver/cnt/校验,才能构造出"换代"与
* "损坏"这两种真实形态;自己手搓 blob 会把测试写成对自己的桩。
* 平台后端 cfg_store_read / cfg_store_write_atomic 用**真实头**
* App/cfg/cfg_file_store.h(接口原汁)+ 本文件的可控实现。
*
* 为什么要主机侧单测(而不是只靠台架):
* 1. **换代与损坏必须分开**(B8 定值表 32→40 项时定下的契约):magic 完好但
* ver/cnt 与当前固件不符是**预期的升级路径**,归 2 并打 INFO;把它当损坏(-1)
* 会让设备在正常升级后报 ERROR + 自检留痕,现场查成一个根本不存在的故障。
* 2. 读失败与"文件不存在"同归 1 是**安全侧**选择(启动期卷锁本任务独占,
* try-lock 失败即真 I/O 故障,此时保持出厂默认比拿半截数据更安全)——钉住它,
* 防止将来有人"优化"成把读失败也当损坏重刷。
* 3. 写失败必须回置脏:定值经 iec104Task 下发,落盘在 defaultTask;一轮卷忙就把
* 脏标志吞掉,表现为"改了定值、重启又变回旧的",而且没有任何错误提示。
*
* 覆盖点:
* —— load 返回码分流(TC-PROTSTOR-001..008)——
* TC-PROTSTOR-001 后端读失败 → 1(按"无档"处理,保持出厂默认)
* TC-PROTSTOR-002 有效 blob → 0,且装载后定值生效
* TC-PROTSTOR-003 **ver 换代 → 2**(不是 -1)
* TC-PROTSTOR-004 ver 换代只打 **INFO**(不打 ERROR、不进自检留痕)
* TC-PROTSTOR-005 **cnt 换代 → 2**(B8 32→40 项就是这个形态)
* TC-PROTSTOR-006 magic 被破坏 → -1(真损坏)
* TC-PROTSTOR-007 magic/ver/cnt 都好但数据被改(校验不过)→ -1
* TC-PROTSTOR-008 长度不足头部 → -1,且**不得**误判为换代
* TC-PROTSTOR-009 装载成功路径不打 INFO("version change"措辞不许乱出现)
* —— 后端调用契约(TC-PROTSTOR-010..011)——
* TC-PROTSTOR-010 读取用的路径 = "/prot_set.bin"(8.3 合规,FF_USE_LFN=0 约束)
* TC-PROTSTOR-011 读取请求的容量 = PROT_SET_BLOB_SZ(blob 缓冲是静态区,不压栈)
* —— save 脏标志与写失败重试(TC-PROTSTOR-012..016)——
* TC-PROTSTOR-012 无脏标志 → 返回 0 且**一次都不写盘**
* TC-PROTSTOR-013 有脏 + 写成功 → 返回 0,路径与长度正确
* TC-PROTSTOR-014 写成功后脏被消费:紧接着再调不再写(不重复刷 Flash)
* TC-PROTSTOR-015 写失败 → 返回 -1 且**脏回置**
* TC-PROTSTOR-016 上一轮写失败后,下一轮仍会真的再写一次(定值不会静默丢失)
*
* 说明:serialize 失败这条分支在真实 prot_set.c 下不可达(缓冲尺寸恒等于
* PROT_SET_BLOB_SZ),故不硬造用例;胶水层里它是防御性代码,记录在此。
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <string.h>
#include "cfg_file_store.h"
#include "prot_set.h"
#include "prot_set_store.h"
/* 真实定值层(blob 编解码与脏标志在这里)+ 被测胶水层,均原样纳入 */
#include "prot_set.c"
#include "prot_set_store.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)
/* ===================== dbg_log 桩计数 ===================== */
int g_pstore_info_count;
int g_pstore_error_count;
int g_pstore_debug_count;
static void logs_clear(void)
{
g_pstore_info_count = 0;
g_pstore_error_count = 0;
g_pstore_debug_count = 0;
}
/* ===================== 假平台后端(原型来自真实 cfg_file_store.h) ========= */
static uint8_t g_img[512]; /* 注入给 cfg_store_read 的文件内容 */
static uint32_t g_img_len;
static int g_read_rc; /* 注入:cfg_store_read 返回值 */
static int g_write_rc; /* 注入:cfg_store_write_atomic 返回值 */
static int g_read_calls;
static int g_write_calls;
static size_t g_read_cap;
static size_t g_write_len;
static char g_read_path[32];
static char g_write_path[32];
static void rec_path(char *dst, const char *src, size_t cap)
{
size_t i;
if (src == NULL) {
dst[0] = '\0';
return;
}
for (i = 0u; (i + 1u < cap) && (src[i] != '\0'); i++) {
dst[i] = src[i];
}
dst[i] = '\0';
}
int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread)
{
uint32_t i;
g_read_calls++;
rec_path(g_read_path, p_path, sizeof(g_read_path));
g_read_cap = cap;
if (p_nread != NULL) {
*p_nread = 0u;
}
if (g_read_rc != 0) {
return g_read_rc;
}
for (i = 0u; (i < g_img_len) && ((size_t)i < cap); i++) {
p_buf[i] = g_img[i];
}
if (p_nread != NULL) {
*p_nread = (size_t)i;
}
return 0;
}
int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len)
{
g_write_calls++;
rec_path(g_write_path, p_path, sizeof(g_write_path));
g_write_len = len;
(void)p_buf;
return g_write_rc;
}
/* 其余后端函数本模块不触发,给出空实现以满足链接 */
void *cfg_store_alloc(size_t size) { (void)size; return NULL; }
void cfg_store_free(void *p_buf) { (void)p_buf; }
uint32_t cfg_store_heap_free(void) { return 0u; }
int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st)
{
(void)p_path;
if (p_st != NULL) {
(void)memset(p_st, 0, sizeof(*p_st));
}
return -1;
}
int cfg_store_remove(const char *p_path) { (void)p_path; return -1; }
/* ===================== 辅助 ===================== */
static void backend_reset(void)
{
g_img_len = 0u;
g_read_rc = 0;
g_write_rc = 0;
g_read_calls = 0;
g_write_calls = 0;
g_read_cap = 0u;
g_write_len = 0u;
g_read_path[0] = '\0';
g_write_path[0] = '\0';
logs_clear();
}
/* 用**真实** serialize 造一份合法 blob,再按需改坏指定字段 */
static uint32_t make_blob(uint8_t *p_dst, uint32_t cap)
{
uint32_t len = 0u;
prot_set_init();
if (prot_set_store_serialize(p_dst, cap, &len) != 0) {
return 0u;
}
return len;
}
static void stage_blob(uint32_t len)
{
g_img_len = len;
g_read_rc = 0;
}
/* 把脏标志清干净(消费掉所有存量) */
static void dirty_clear(void)
{
while (prot_set_dirty_take() != 0u) {
/* 逐个消费直到 0 */
}
}
/* ===================== load 返回码分流 ===================== */
static void tc_load_triage(void)
{
uint8_t blob[PROT_SET_BLOB_SZ];
uint32_t len;
int rc;
printf("-- TC-PROTSTOR-001..009 load triage --\n");
backend_reset();
g_read_rc = -1;
rc = prot_set_store_load();
CHECK(rc == 1, "TC-PROTSTOR-001 读失败归 1(按无档处理)");
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 装载成功");
/* ver 换代:magic 完好、ver 与当前固件不同 → 预期升级路径,归 2 */
backend_reset();
len = make_blob(blob, sizeof(blob));
memcpy(g_img, blob, len);
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((g_pstore_info_count == 1) && (g_pstore_error_count == 0),
"TC-PROTSTOR-004 换代只打 INFO,不误报 ERROR");
/* cnt 换代:B8 定值表 32→40 项就是这个形态 */
backend_reset();
len = make_blob(blob, sizeof(blob));
memcpy(g_img, blob, len);
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");
/* magic 坏 = 真损坏 */
backend_reset();
len = make_blob(blob, sizeof(blob));
memcpy(g_img, blob, len);
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");
/* magic/ver/cnt 全好,但数据被改 → 校验不过 = 损坏 */
backend_reset();
len = make_blob(blob, sizeof(blob));
memcpy(g_img, blob, len);
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");
/* 截断到不足头部:不得误判为换代 */
backend_reset();
len = make_blob(blob, sizeof(blob));
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 且不误判换代");
backend_reset();
len = make_blob(blob, sizeof(blob));
memcpy(g_img, blob, len);
stage_blob(len);
rc = prot_set_store_load();
CHECK((rc == 0) && (g_pstore_info_count == 0),
"TC-PROTSTOR-009 成功路径不打 INFO(version change 措辞不许乱出现)");
}
/* ===================== 后端调用契约 ===================== */
static void tc_backend_contract(void)
{
uint8_t blob[PROT_SET_BLOB_SZ];
uint32_t len;
printf("-- TC-PROTSTOR-010..011 backend contract --\n");
backend_reset();
len = make_blob(blob, sizeof(blob));
memcpy(g_img, blob, len);
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 合规)");
CHECK(g_read_cap == (size_t)PROT_SET_BLOB_SZ,
"TC-PROTSTOR-011 读取容量 = PROT_SET_BLOB_SZ(静态缓冲,不压栈)");
}
/* ===================== save 与脏标志 ===================== */
static void tc_save_dirty(void)
{
int rc;
printf("-- TC-PROTSTOR-012..016 save + dirty retry --\n");
backend_reset();
dirty_clear();
rc = prot_set_store_save();
CHECK((rc == 0) && (g_write_calls == 0), "TC-PROTSTOR-012 无脏不写盘");
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 有脏且写成功:路径与长度正确");
rc = prot_set_store_save();
CHECK((rc == 0) && (g_write_calls == 1),
"TC-PROTSTOR-014 脏已消费,紧接着再调不再刷 Flash");
prot_set_dirty_mark();
g_write_rc = -1;
rc = prot_set_store_save();
CHECK((rc == -1) && (g_write_calls == 2), "TC-PROTSTOR-015 写失败返回 -1");
/* 关键:失败必须把脏标志还回去,否则这一轮定值永久丢失 */
g_write_rc = 0;
g_write_calls = 0;
rc = prot_set_store_save();
CHECK((rc == 0) && (g_write_calls == 1),
"TC-PROTSTOR-016 上一轮写失败后下一轮仍真的再写(脏已回置)");
dirty_clear();
}
/* ===================== 入口 ===================== */
int main(void)
{
printf("=== Prot Set Store Tests ===\n");
tc_load_triage();
tc_backend_contract();
tc_save_dirty();
printf("\n=== Prot Set Store Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""保护定值持久化胶水层(prot_set_store)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/prot_set_store/prot_set_store_test.c(同时纳入**生产源**
App/cfg/prot_set_store.c 与 App/cfg/prot_set.c),在 PC 上离线验证 blob 装载的
返回码分流(1 无档 / 0 成功 / 2 换代 / -1 损坏)与保存的脏标志回置,失败即非 0
退出。工具链与判定委托 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只用台架):
换代与损坏必须分开(B8:定值表 32→40 项)——把正常的固件升级当损坏,会让设备在
升级后报 ERROR + 自检留痕,现场查一个不存在的故障;写盘失败若不把脏标志还回去,
表现是"改了定值、重启又变回旧的"且无任何提示。这两条真机上都要靠故障注入才碰得到。
为什么连 prot_set.c 一起编:blob 的编解码与脏标志都在它里面,只有拿**真** serialize
产出的 blob 去改 magic/ver/cnt/校验,才能造出真实的"换代"与"损坏"形态;手搓 blob
等于测自己的桩。定值算法本身归 test/host/prot_set/,本模块不重测。
用法(仓库根目录):
python test/host/prot_set_store/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:App/cfg/prot_set_store.h 的返回码表、docs/测试说明/主机单测覆盖率口径.md §9 P1
创建日期:2026-10-23
"""
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 先于生产目录(覆盖 dbg_log.h)。cfg_file_store.h / prot_set.h /
# prot_set_store.h 全用真实头,后端两个被调函数由测试单元给可控实现。
BUILD = TestBuild(
name="Prot Set Store Tests",
src=os.path.join(HERE, "prot_set_store_test.c"),
include_dirs=[
os.path.join(HERE, "stub"),
os.path.join(ROOT, "App", "cfg"),
os.path.join(ROOT, "App", "algo"),
HERE,
],
summary_marker="=== Prot Set Store Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+20
View File
@@ -0,0 +1,20 @@
/*
* 模块名称:主机侧单测桩 - dbg_log.h(prot_set_store 模块专用)
* 模块功能:把 MCU 版 DBG_* 宏替换为计数,让"换代只打 INFO、损坏不打"这类
* 留痕差异成为可断言的行为(B8 的分流契约一半就在措辞上)。
* ⚠ 仅供 test/host/prot_set_store/ 使用,不参与 MCU 构建。
* 创建日期:2026-10-23
*/
#ifndef DBG_LOG_H
#define DBG_LOG_H
extern int g_pstore_info_count;
extern int g_pstore_error_count;
extern int g_pstore_debug_count;
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_pstore_info_count++; } while (0)
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_pstore_debug_count++; } while (0)
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_pstore_error_count++; } while (0)
#endif /* DBG_LOG_H */
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""任务耗时探针(task_prof)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/task_prof/task_prof_test.c(以 #include 纳入**生产源**
App/util/task_prof.c),在 PC 上离线验证样本有效性闸门、enter/exit 配对、上报节流
与两条通道(环形缓冲 / 裸发)、周期→微秒换算、glitch 汇总,失败即非 0 退出。
工具链与判定委托 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只靠真机日志):
这个模块自己错了,所有 PROF 行就都是假的:漏了"无效样本闸门",一次 DWT 复位就把
max 永久抬到 ~2^32 周期;"先 exit 再 enter"的首轮会把 0 当起点,造出几秒的假超大
样本;节流写错要么刷屏挤掉正事、要么停摆现场那一行发不出去;换算依赖
SystemCoreClock,改时钟树忘了同步则整表偏移而无人察觉。这些都得能构造时序来测。
桩的边界:只有 main.h 需要桩(真实 main.h 拉整棵 HAL 头树,PC 上编不动;桩里的字段名
与位掩码宏名必须与 CMSIS 一致,改了被测代码即编译失败)。console_log.h 用真实头,
两个出口由测试单元实现并留档内容——报告行是给人读的,断言直接盯字符串。
用法(仓库根目录):
python test/host/task_prof/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:App/util/task_prof.h、docs/测试说明/主机单测覆盖率口径.md §9 P1
创建日期:2026-10-23
"""
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 必须先于生产目录,才能用桩 main.h 覆盖真实 main.h。
# console_log.h / task_prof.h 用真实头(App/log、App/util)。
BUILD = TestBuild(
name="Task Prof Tests",
src=os.path.join(HERE, "task_prof_test.c"),
include_dirs=[
os.path.join(HERE, "stub"),
os.path.join(ROOT, "App", "util"),
os.path.join(ROOT, "App", "log"),
HERE,
],
summary_marker="=== Task Prof Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+44
View File
@@ -0,0 +1,44 @@
/*
* 模块名称:主机侧单测桩 - main.h(task_prof 模块专用)
* 模块功能:提供 task_prof.c 需要的**四个**平台符号,让 DWT 周期计数器变成
* 测试可控制的量:
* SystemCoreClock 测试可写(换算 us 的分母,真机 = 168 MHz)
* HAL_GetTick() 测试可控的毫秒时基(report 节流就看它)
* DWT->CTRL/CYCCNT 可写内存:CTRL 的 CYCCNTENA 位要能"读回 1",
* 否则 task_prof.c 永远认为 DWT 不可用
* CoreDebug->DEMCR 同上(TRCENA 位)
*
* ⚠ 仅供 test/host/task_prof/ 使用,不参与 MCU 构建。
* 真实 main.h 拉整棵 HAL 头树,主机侧无法编译,故这里只声明被测文件实际用到的
* 那几个符号——**结构体字段名与位掩码宏名必须与 CMSIS 一致**(DWT_Type 的
* CTRL/CYCCNT、DEMCR、DWT_CTRL_CYCCNTENA_Msk、CoreDebug_DEMCR_TRCENA_Msk),
* 改了这些名字被测代码会直接编译失败,不会静默放行。
*
* 创建日期:2026-10-23
*/
#ifndef MAIN_H
#define MAIN_H
#include <stdint.h>
typedef struct {
volatile uint32_t DEMCR;
} tp_coredebug_t;
typedef struct {
volatile uint32_t CTRL;
volatile uint32_t CYCCNT;
} tp_dwt_t;
extern tp_coredebug_t *const CoreDebug;
extern tp_dwt_t *const DWT;
#define CoreDebug_DEMCR_TRCENA_Msk (1UL << 24)
#define DWT_CTRL_CYCCNTENA_Msk (1UL << 0)
extern uint32_t SystemCoreClock;
uint32_t HAL_GetTick(void);
#endif /* MAIN_H */
+376
View File
@@ -0,0 +1,376 @@
/*
* 模块名称:任务耗时探针(task_prof)主机侧单元测试
* 模块功能:在 PC 上离线验证 App/util/task_prof.c 的**样本有效性闸门、enter/exit
* 配对、上报节流与通道选择、周期→微秒换算、glitch 汇总**。
*
* 被测对象:真实源文件 ../../../App/util/task_prof.c(#include 原样纳入)。桩:
* - stub/main.h:DWT/CoreDebug/SystemCoreClock/HAL_GetTick(本目录,
* 真实 main.h 拉整棵 HAL 头树无法在 PC 上编);
* - console_log.h 用**真实头**(App/log),两个出口函数
* dbg_console_write_line / dbg_console_write_raw 由本文件实现并**留档
* 内容**——报告行是要被人读的,所以断言直接盯字符串本身。
*
* 为什么要主机侧单测(而不是只靠真机日志):
* 这个模块是"性能问题的唯一现场证据",它自己错了所有 PROF 行就都是假的:
* 1. **无效样本闸门**(cyc==0 或 ≥ SystemCoreClock×10):漏了它,一次 DWT 复位
* 就把 max 抬到 ~2^32 周期,之后所有"最坏耗时"永久失真(真机上没人会去翻
* gl= 计数发现)。这条只能构造样本测。
* 2. **未 enter 就 exit 必须直接返回**:周期测量写成"先 exit 再 enter"时,首轮
* 会把 0 当起点,记下一次几秒的虚假超大样本——真机上表现为"偶发一条离谱
* PROF 行",查半年。
* 3. 节流契约(首次 5 s、之后每 2 s)与**两条通道的选择**(report 走环形缓冲、
* report_raw 走裸发):错了要么刷屏挤掉正事,要么停摆现场那一行发不出去。
* 4. 周期→微秒换算依赖 SystemCoreClock,改时钟树(比如 168→180 MHz)忘了同步
* 就会整表偏移;这条在真机上看不出来(没人对着示波器量 PROF)。
*
* 覆盖点:
* TC-TASKPROF-001 有效样本:count+1 / last=样本 / max=样本
* TC-TASKPROF-002 更小样本不改 max(max 只增不减)
* TC-TASKPROF-003 cyc=0 只累加 glitch,不动 count/last/max(计数器复位防线)
* TC-TASKPROF-004 cyc ≥ SystemCoreClock×10 同样只累加 glitch
* TC-TASKPROF-005 id 越界(=COUNT 与 +1)安全返回,不碰任何槽
* TC-TASKPROF-006 首次取时基即惰性使能 DWT(TRCENA + CYCCNTENA 读回)
* TC-TASKPROF-007 enter 置 in_seg 并记下起点
* TC-TASKPROF-008 exit 结算 Δ、清 in_seg、count+1
* TC-TASKPROF-009 **未 enter 直接 exit 不记账**(防首轮 0 起点造出假超大样本)
* TC-TASKPROF-010 重复 exit 不重复记账
* TC-TASKPROF-011 首次 5 s 之前一条都不发
* TC-TASKPROF-012 到点后发两行([PROF] + [PROF2]),走环形缓冲通道
* TC-TASKPROF-013 之后不足 2 s 不发(节流)
* TC-TASKPROF-014 满 2 s 再发两行
* TC-TASKPROF-015 report_raw 无视节流,恒发两行且走**裸发**通道
* TC-TASKPROF-016 行格式:以 [PROF]/[PROF2] 开头、以 \r\n 结尾
* TC-TASKPROF-017 周期→微秒换算按 SystemCoreClock(168 MHz 下 1680 周期 = 10 us)
* TC-TASKPROF-018 gl= 是所有槽 glitch 的汇总,不是单槽
* TC-TASKPROF-019 段内进行时的 n= 字段非零(此刻仍在段内的可视性)
* TC-TASKPROF-020 count 是累计值,上报不清零
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <string.h>
#include "console_log.h" /* 真实头:DBG_CON_LINE_MAX + 两个出口 */
#include "task_prof.h"
#include "task_prof.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 tp_coredebug_t g_coredebug;
static tp_dwt_t g_dwt;
tp_coredebug_t *const CoreDebug = &g_coredebug;
tp_dwt_t *const DWT = &g_dwt;
uint32_t SystemCoreClock = 168000000u; /* 真机口径:168 MHz */
static uint32_t g_tick; /* HAL_GetTick 的可控值 */
uint32_t HAL_GetTick(void)
{
return g_tick;
}
/* 出口留档:两条通道分别计数;**每一行都单独留档**(一次上报会连发
* [PROF] 与 [PROF2] 两行,只存"最后一行"就没法断言第一行的字段) */
#define CAP_MAX 8
static int g_line_calls;
static int g_raw_calls;
static char g_line[CAP_MAX][DBG_CON_LINE_MAX];
static void cap(int slot, const char *src, unsigned len)
{
unsigned i;
if (slot >= CAP_MAX) {
return;
}
for (i = 0u; (i < len) && (i + 1u < DBG_CON_LINE_MAX); i++) {
g_line[slot][i] = src[i];
}
g_line[slot][i] = '\0';
}
int dbg_console_write_line(const char *line, unsigned len)
{
cap(g_line_calls + g_raw_calls, line, len);
g_line_calls++;
return (int)len;
}
void dbg_console_write_raw(const char *buf, unsigned len)
{
cap(g_line_calls + g_raw_calls, buf, len);
g_raw_calls++;
}
void dbg_console_writer_attach(void) { }
uint8_t dbg_console_writer_ready(void) { return 1u; }
/* ===================== 辅助 ===================== */
static void reset_all(void)
{
(void)memset(s_prof, 0, sizeof(s_prof));
(void)memset(&g_coredebug, 0, sizeof(g_coredebug));
(void)memset(&g_dwt, 0, sizeof(g_dwt));
s_dwt_ok = 0u;
s_prof_reported = 0u;
s_prof_last = 0u;
g_tick = 0u;
g_line_calls = 0;
g_raw_calls = 0;
(void)memset(g_line, 0, sizeof(g_line));
}
static void set_cyc(uint32_t v)
{
g_dwt.CYCCNT = v;
}
/* 从指定行的键上取整数值;键不存在或其后不是数字则返回 -1(断言因此会响)。
* 手写解析而不用 sscanf:后者在 MSVC 下触发 C4996(弃用告警),门禁输出要干净 */
static long field_of(int slot, const char *p_key)
{
const char *p;
long v = 0;
int digits = 0;
if (slot >= CAP_MAX) {
return -1L;
}
p = strstr(g_line[slot], p_key);
if (p == NULL) {
return -1L;
}
p += strlen(p_key);
while ((*p >= '0') && (*p <= '9')) {
v = (v * 10) + (long)(*p - '0');
p++;
digits++;
}
return (digits > 0) ? v : -1L;
}
/* ===================== 样本闸门 ===================== */
static void tc_sample_gate(void)
{
printf("-- TC-TASKPROF-001..005 sample gate --\n");
reset_all();
task_prof_sample(TASK_PROF_UI_PERIOD, 500u);
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");
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");
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 复位防线)");
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");
reset_all();
task_prof_sample((task_prof_id_t)TASK_PROF_ID_COUNT, 100u);
task_prof_sample((task_prof_id_t)(TASK_PROF_ID_COUNT + 1), 100u);
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 越界安全返回,不碰任何槽");
}
/* ===================== 时基与 enter/exit 配对 ===================== */
static void tc_enter_exit(void)
{
printf("-- TC-TASKPROF-006..010 dwt enable + enter/exit --\n");
reset_all();
set_cyc(1000u);
(void)task_prof_cyc();
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");
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 并记起点");
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");
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 起点假超大样本)");
reset_all();
set_cyc(100u);
task_prof_enter(TASK_PROF_NET_POLL);
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 不重复记账");
}
/* ===================== 上报节流与通道 ===================== */
static void tc_report_throttle(void)
{
printf("-- TC-TASKPROF-011..015 report throttle + channels --\n");
reset_all();
g_tick = 4999u;
task_prof_report();
CHECK((g_line_calls == 0) && (g_raw_calls == 0), "TC-TASKPROF-011 首 5 s 前不发");
g_tick = 5000u;
task_prof_report();
CHECK((g_line_calls == 2) && (g_raw_calls == 0),
"TC-TASKPROF-012 到点发两行走环形缓冲");
g_tick = 6999u;
task_prof_report();
CHECK(g_line_calls == 2, "TC-TASKPROF-013 不足 2 s 不发");
g_tick = 7000u;
task_prof_report();
CHECK(g_line_calls == 4, "TC-TASKPROF-014 满 2 s 再发两行");
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 无视节流且走裸发通道");
}
/* ===================== 行内容与换算 ===================== */
static void tc_line_format(void)
{
printf("-- TC-TASKPROF-016..020 line content --\n");
/* 016:一次上报连发两行;行首前缀是采集脚本的抓取依据,行尾必须是 CRLF */
reset_all();
g_tick = 5000u;
task_prof_report();
CHECK((g_line_calls == 2) &&
(strncmp(g_line[0], "[PROF] ", 7) == 0) &&
(strncmp(g_line[1], "[PROF2] ", 8) == 0),
"TC-TASKPROF-016 两行行首分别是 [PROF] / [PROF2]");
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");
/* 017:打印的是**微秒**,分母是 SystemCoreClock —— 断言落在打印字段上 */
reset_all();
task_prof_sample(TASK_PROF_UI_PERIOD, 1680u); /* 168 MHz → 10 us */
task_prof_sample(TASK_PROF_ADC_BEAT, 3360u); /* 168 MHz → 20 us */
g_tick = 5000u;
task_prof_report();
CHECK(field_of(0, "uip=") == 10L,
"TC-TASKPROF-017 168 MHz:1680 周期的 max 打印成 uip=10");
CHECK(field_of(0, "adcm=") == 20L,
"TC-TASKPROF-017b 同频:3360 周期的 max 打印成 adcm=20");
SystemCoreClock = 84000000u; /* 改时钟树而忘了同步 → 整表偏移,这里必须显式跟着变 */
g_tick = 7000u;
task_prof_report();
CHECK(field_of(2, "uip=") == 20L,
"TC-TASKPROF-017c 84 MHz 下同一批周期数打印成 20");
SystemCoreClock = 168000000u;
/* 018:gl= 是**所有槽**的 glitch 汇总,不是当前槽 */
reset_all();
task_prof_sample(TASK_PROF_UI_PERIOD, 0u);
task_prof_sample(TASK_PROF_ADC_BEAT, 0u);
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");
/* 019:段未结算时,"此刻已在段内多久"要看得见(uipn / netn 两个字段) */
/* 字段口径 = 上报时刻的 CYCCNT 减去各自的进段时刻(**不是**样本值):
* UI_PERIOD 进段 @1000、NET_POLL 进段 @10000,上报时 CYCCNT=51680
* uipn = (51680-1000)/168 = 301 netn = (51680-10000)/168 = 248 */
reset_all();
set_cyc(1000u);
task_prof_enter(TASK_PROF_UI_PERIOD);
set_cyc(10000u);
task_prof_enter(TASK_PROF_NET_POLL);
set_cyc(51680u);
g_tick = 5000u;
task_prof_report();
CHECK(field_of(0, "uipn=") == 301L,
"TC-TASKPROF-019 UI_PERIOD 段内:uipn= 上报此刻已停留的 us");
CHECK(field_of(1, "netn=") == 248L,
"TC-TASKPROF-019b NET_POLL 段内:[PROF2] 的 netn= 同步可见");
CHECK(field_of(0, "adcn=") == 0L,
"TC-TASKPROF-019c 未进段的 ADC_BEAT 打印 0(不串用别的槽)");
CHECK(s_prof[TASK_PROF_UI_PERIOD].in_seg == 1u,
"TC-TASKPROF-019d 上报不消费段状态(只观测)");
/* 020:count 是累计量,两轮上报之间不能清零 */
reset_all();
task_prof_sample(TASK_PROF_UI_WORK, 100u);
g_tick = 5000u;
task_prof_report();
g_tick = 7000u;
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)");
}
/* ===================== 入口 ===================== */
int main(void)
{
printf("=== Task Prof Tests ===\n");
tc_sample_gate();
tc_enter_exit();
tc_report_throttle();
tc_line_format();
printf("\n=== Task Prof Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}