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:
@@ -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 */
|
||||
Reference in New Issue
Block a user