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:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
* 对前导零兼容(十进制逐段解析)
|
||||
|
||||
@@ -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
@@ -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 段 |
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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 */
|
||||
@@ -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))
|
||||
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user