Files
STM32F4-Base/test/host/cfg_file/cfg_file_test.c
T
wandering1 7efa58d9a1
CI / 静态门禁 (pre-commit) (push) Failing after 25s
CI / 主机侧单测 (MSVC) (push) Successful in 48s
network: 台架 IP 迁到 192.168.31.100,并把重复写死的值收进单一真源
分两段(用户裁定 2026-11-02):先做地址迁移,再把同类字面量收敛成
单一真源,以后再改地址只动一处。

一、IP 迁移 192.168.1.100 → 192.168.31.100(gw 同步 → .31.1)
- 工厂默认:Drivers/BSP/NETCFG/net_cfg.c 的 NET_CFG_DEF_IP/_GW
- 注释/文档示例、主机与板级测试、台架 py 脚本、两份 sim_config.ini
- 字节序教学点同步换算 hex:0x6401A8C0 → 0x641FA8C0(内存
  C0 A8 1F 64),旧版示例引文与历史 Trap 记录按冻结纪律不动
- UI 黄金基准:网口设置 4 态 + shot_info 共 5 张 .bin 由
  check_content_pages.py --update 重刷(渲染值 IP 变了,属预期变更)
- 保留判定:客户点表引用(IEC104/继电保护规划)、芯片手册摘录、
  日期冻结的变更记录行、已退役台架档案 MDK-ARM/retracted_bench_*

二、单一真源收口(A+B+C+D,用户批准全做)
- 新建 test/host/bench_defaults.py:台架 py 默认值唯一定义点
  (DEVICE_IP/DEVICE_GW/PC_IP/NET_BCAST),22 个脚本改为引用;
  MDK-ARM 两探针与 scripts/board_ui_nav.py 经 sys.path shim 引入
- test/board/net_test_task.h 新增 TEST_PC_PEER_IP,板测三处
  PHASE*_REMOTE_IP 改引该宏(保留 -D 覆盖);Phase1 断言改
  memcmp 数组比较,数组各只出现一次
- simulator/tests/check_content_pages.py 从 net_cfg.c 正则解析
  NET_CFG_DEF_* 推导期望值(解析失败即报错、不静默兜底),
  脚本内台架 IP 字面量清零
- 装饰性注释去具体值(net_cfg.h/net_socket.h/net_socket.c/
  net_socket_sim.c);字节序教学 hex 对刻意保留为唯一写值点
- 刻意不动:主机单测夹具 IP(逐文件钉住的任意值是正确测试
  实践)、文档正文 IP(那是文档内容本身)

顺带:scripts/gen_task_table.py 规范串清掉「,B5」——编号清退提交
72b4b094 改了受管区没重跑生成器,task-table 门禁因此一直红

验证:产品 @build 0/0;CH395F 套件开关构建 0/0 后还原产品态;
6 套主机单测 rc=0;content_pages(含宏推导路径)/菜单 8 态/
推流位比对全 PASS;bom-drift 99 无漂移;doc-links/coding-standard/
cppcheck 绿;py_compile 23 文件 + --help 冒烟(含 shim 绝对路径)
2026-11-02 14:46:42 +08:00

1082 lines
43 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:cfg_file 主机侧单元测试
* 模块功能:在 PC 上对 App/cfg/cfg_file.c(通用配置引擎核心)做穷举验证。
*
* 被测对象:真实源文件 ../../../App/cfg/cfg_file.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。平台文件操作由本文件提供的
* **内存虚拟文件系统**桩实现(cfg_store_* 系列强符号)。
*
* 为什么要主机侧单测(而不是只用板端套件):
* 1. 配置引擎的核心是"文本 <-> 结构"的纯变换(INI 段/键解析、五种值类型
* 编解码、外科手术式保存、超长守卫),与硬件无关;板端套件无法穷举
* 畸形输入,更无法在毫秒内构造"超长文件 / 写失败 / 堆分配失败"。
* 2. 主机侧可**精确注入**故障:文件缺失、读取失败、写失败、分配失败——
* 这些在真机上极难复现(需掉电/坏块)。
* 3. 不占 MCU 的 RAM。
*
* 覆盖点(对应 docs/测试说明/单元测试/配置文件单元测试说明.md 的 TC-CFG-xxx):
* —— 值类型编解码(s_encode / s_decode 经公开 load/save 间接验证 + 直接白盒)——
* TC-CONFIG-001 UINT 1/2/4 字节小端往返(含 0 / 最大值)
* TC-CONFIG-002 UINT 非法:空 / 非数字 / 带后缀 / 超范围
* TC-CONFIG-003 IP4 正常与非法(段数不足、>255、前导非数字、多余后缀)
* TC-CONFIG-004 MAC 三种分隔符 + 大小写 + 非法
* TC-CONFIG-005 STR 正常 / 超长拒绝 / 边界(恰好放下)
* TC-CONFIG-006 INT 合法解码:0 / -1 / INT32_MIN / INT32_MAX / 各宽度边界 /
* 前导 + / 前导 0(「数据可信」有符号零点类型:负零点偏移可落盘)
* TC-CONFIG-007 INT 非法拒绝:空 / 非数字 / 带后缀 / 越界(含 strtol 饱和带,
* 即超 INT32 但钳位后恰落界内的文本)/ 坏 len
* TC-CONFIG-008 INT 编码带负号输出 + 注册校验(INT len=3 拒绝)+
* [sys] 段 load/save/load 全链往返一致(出厂模板带负号默认)
* —— INI 解析(s_split_line / s_match_section / s_key_value / s_parse)——
* TC-CONFIG-010 注释(; 与 #)与空行忽略
* TC-CONFIG-011 CRLF / LF 混排
* TC-CONFIG-012 段名大小写无关匹配、前后空白修剪
* TC-CONFIG-013 键名大小写无关、值前后空白修剪
* TC-CONFIG-014 未知段 / 未知键留默认(不崩溃)
* TC-CONFIG-015 键值行出现在段头之前 = 忽略
* —— 注册校验(cfg_file_register)——
* TC-CONFIG-020 正常注册 / 重复段名拒绝 / NULL 参数拒绝
* TC-CONFIG-021 非法键定义拒绝(UINT len 非 1/2/4、MAC len≠6、STR len>79)
* TC-CONFIG-022 段名超长拒绝(≥ CFG_NAME_MAX)
* TC-CONFIG-023 挂回调段 data_size > CFG_SNAP_MAX 拒绝
* 键名超长拒绝(≥ CFG_NAME_MAX);恰好 15 字符(= CFG_FILE_MAX_NAME_LEN)接受
* —— load 契约(cfg_file_load)——
* TC-CONFIG-030 文件缺失 -> 生成出厂模板写盘 + 返回 0
* TC-CONFIG-031 文件存在 -> 解析有效键进 RAM 副本
* TC-CONFIG-032 超长文件(> CFG_FILE_MAX_LEN)-> 工厂默认 + 重建模板 + 自检留痕
* TC-CONFIG-033 读取失败 -> 返回 -1(RAM 保持工厂默认)
* TC-CONFIG-034 未注册任何段 -> 返回 -1
* —— save 契约(cfg_file_save)——
* TC-CONFIG-040 文件缺失 -> 重建完整文档
* TC-CONFIG-041 外科手术:保留注释/未知段/未知键,受管键原位改写
* TC-CONFIG-042 缺失受管段/键补写
* TC-CONFIG-043 输出超长 -> 拒绝保存(-1) 且原文件不动
* 受管键行超 CFG_LINE_TEST_MAX -> 拒绝保存(-1) 原文件不动;
* 非受管键的超长行照旧放过(原样保留)
* —— poll 契约(cfg_file_poll)——
* TC-CONFIG-050 首轮建立基线(不重载)
* TC-CONFIG-051 沉降去抖:连续 2 次稳定才重载
* 与基线一致不重载
* TC-CONFIG-053 变更后重载 + 挂回调段差异触发回调
* TC-CONFIG-054 溢出到文件超长 -> 工厂默认 + 重建模板 + 自检留痕(不触发回调)
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-02
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* 先纳入桩头(DBG_* 映射为计数),再纳入生产源码 */
#include "dbg_log.h"
/* ===================== 内存虚拟文件系统(cfg_store_* 强符号) =====================
*
* 生产代码经 cfg_file_store.h 声明的 8 个接口做全部文件/堆操作。本桩提供:
* - 一块固定容量的内存"磁盘"(模拟 config.ini 的字节内容)
* - 统计 read/write/alloc 失败注入开关(复现真机极难构造的故障路径)
* - stat 的 (size, mtime) 令牌:mtime 由单测手动改,驱动 poll 的沉降逻辑
*/
#include "cfg_file_store.h"
#define VFS_CAP 8192u /* 虚拟盘容量(> CFG_FILE_MAX_LEN,便于构造超长 */
/* 虚拟文件:内容 + 长度 + mtime 令牌 + 是否存在 */
static uint8_t s_vfs[VFS_CAP];
static size_t s_vfs_len = 0u;
static uint32_t s_vfs_mtime = 100u;
static int s_vfs_exists = 0;
/* 故障注入开关 */
static int s_fail_read = 0; /* cfg_store_read 返回 -1 */
static int s_fail_write = 0; /* cfg_store_write_atomic 返回 -1 */
static int s_fail_alloc = 0; /* cfg_store_alloc 返回 NULL */
static int s_fail_stat = 0; /* cfg_store_stat 返回 -1 */
/* 观测计数 */
static int s_write_count = 0;
static int s_read_count = 0;
/* 路径常量(与生产逻辑无关,仅作为桩内部标识) */
static const char *s_cfg_path = "/config.ini";
static const char *s_tmp_path = "/config.tmp";
/* ---- 堆:直接映射宿主 CRT(单测不模拟 MicroLIB 512B 堆约束) ---- */
void *cfg_store_alloc(size_t size) {
if (s_fail_alloc) {
return NULL;
}
return malloc(size);
}
void cfg_store_free(void *p_buf) {
free(p_buf);
}
uint32_t cfg_store_heap_free(void) {
return 0u; /* 模拟器语义:不跟踪宿主堆 */
}
/* ---- 文件操作 ---- */
int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread) {
size_t n;
(void)p_path;
s_read_count++;
if (s_fail_read || !s_vfs_exists) {
return -1;
}
n = s_vfs_len;
if (n > cap) {
n = cap;
}
(void)memcpy(p_buf, s_vfs, n);
*p_nread = n;
return 0;
}
int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len) {
(void)p_path;
s_write_count++;
if (s_fail_write) {
return -1;
}
if (len > VFS_CAP) {
return -1; /* 虚拟盘放不下 */
}
(void)memcpy(s_vfs, p_buf, len);
s_vfs_len = len;
s_vfs_exists = 1;
/* 真实后端每次写盘 mtime 必变(分钟粒度,同分钟内可能不变);
* 单测里显式 +1 让"写盘 = 令牌变化"确定可预期 */
s_vfs_mtime++;
return 0;
}
int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) {
(void)p_path;
if (s_fail_stat || !s_vfs_exists) {
return -1;
}
p_st->size = (uint32_t)s_vfs_len;
p_st->mtime = s_vfs_mtime;
return 0;
}
int cfg_store_remove(const char *p_path) {
(void)p_path;
s_vfs_exists = 0;
s_vfs_len = 0u;
return 0;
}
const char *cfg_store_config_path(void) { return s_cfg_path; }
const char *cfg_store_tmp_path(void) { return s_tmp_path; }
/* ---- sys_log 桩:只计数自检留痕(不落盘) ---- */
#include "sys_log.h"
static int s_syslog_count = 0;
void syslog_post(syslog_id_t id, const char *desc) {
(void)id;
(void)desc;
s_syslog_count++;
}
/* ---- dbg_log 桩观测计数定义(stub/dbg_log.h 声明为 extern) ---- */
int g_cfgtest_err_count = 0;
int g_cfgtest_info_count = 0;
int g_cfgtest_debug_count = 0;
/* ===================== 测试框架(极简) ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#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)
/* ===================== 被测代码 ===================== */
/* 原样纳入生产源文件(stub 的 dbg_log.h 生效) */
#include "cfg_file.c"
/* ===================== 测试用段定义 ===================== */
/* 段 A:[net],含四种值类型,data_size ≤ 64 可挂回调 */
typedef struct {
uint8_t u8; /* offset 0 len 1 UINT */
uint16_t u16; /* offset 2 len 2 UINT(对齐后) */
uint8_t ip[4]; /* offset 4 len 4 IP4 */
uint8_t mac[6]; /* offset 8 len 6 MAC */
char name[12]; /* offset 14 len 12 STR */
} sec_net_t;
static sec_net_t s_net_data;
static const sec_net_t s_net_def = {
7u, 1000u, {192u, 168u, 1u, 100u}, {0x00u, 0x11u, 0x22u, 0x33u, 0x44u, 0x55u}, "default"
};
static int s_net_cb_count = 0;
static void net_on_change(void) { s_net_cb_count++; }
/* 注意:键定义 offset 相对结构体起始,按字段真实偏移书写 */
static const cfg_key_def_t s_net_keys[] = {
{ "u8", CFG_VAL_UINT, offsetof(sec_net_t, u8), 1u },
{ "u16", CFG_VAL_UINT, offsetof(sec_net_t, u16), 2u },
{ "ip", CFG_VAL_IP4, offsetof(sec_net_t, ip), 4u },
{ "mac", CFG_VAL_MAC, offsetof(sec_net_t, mac), 6u },
{ "name", CFG_VAL_STR, offsetof(sec_net_t, name), 12u },
};
/* 段 B:[sys],无回调,用于验证多段、超长字段拒绝与 INT 有符号往返(有符号零点类型)。
* 字段按自然对齐排布:u32@0 / i8@4 / i16@6 / i32@8,offset 用 offsetof 取真值 */
typedef struct {
uint32_t baud; /* offset 0 len 4 UINT */
int8_t ofs8; /* offset 4 len 1 INT */
int16_t ofs16; /* offset 6 len 2 INT(负默认:模板须带负号写出) */
int32_t ofs32; /* offset 8 len 4 INT(默认 INT32_MIN:极值往返) */
} sec_sys_t;
static sec_sys_t s_sys_data;
static const sec_sys_t s_sys_def = { 115200u, -5, -300, (-2147483647 - 1) };
static const cfg_key_def_t s_sys_keys[] = {
{ "baud", CFG_VAL_UINT, offsetof(sec_sys_t, baud), 4u },
{ "ofs8", CFG_VAL_INT, offsetof(sec_sys_t, ofs8), 1u },
{ "ofs16", CFG_VAL_INT, offsetof(sec_sys_t, ofs16), 2u },
{ "ofs32", CFG_VAL_INT, offsetof(sec_sys_t, ofs32), 4u },
};
/* ===================== 辅助 ===================== */
/* 复位"模块静态态 + 虚拟盘 + 全部观测计数" = 刚上电 */
static void vfs_reset(void) {
memset(s_vfs, 0, sizeof(s_vfs));
s_vfs_len = 0u;
s_vfs_mtime = 100u;
s_vfs_exists = 0;
s_fail_read = s_fail_write = s_fail_alloc = s_fail_stat = 0;
s_write_count = s_read_count = 0;
s_syslog_count = 0;
g_cfgtest_err_count = 0;
g_cfgtest_info_count = 0;
g_cfgtest_debug_count = 0;
s_net_cb_count = 0;
/* 清空段注册表(生产静态态无公开复位接口,测试用直接 memset——
* 这是白盒单测的可接受代价,模块静态态布局在头文件可见) */
memset(s_sections, 0, sizeof(s_sections));
memset(&s_base, 0, sizeof(s_base));
memset(&s_last, 0, sizeof(s_last));
s_stable = 0u;
s_base_valid = 0u;
memset(s_snap, 0, sizeof(s_snap));
memset(s_sec_hdr, 0, sizeof(s_sec_hdr));
memset(s_key_line, 0, sizeof(s_key_line));
/* 段 RAM 副本复位工厂默认 */
s_net_data = s_net_def;
s_sys_data = s_sys_def;
}
/* 向虚拟盘写入一段文本(建立/更新"文件已存在"状态)。
* 语义 = 一次外部写盘:内容变 + mtime 令牌变(真实后端每次写盘 mtime 必变,
* 是 poll 变更检测的信号源)。需精确控制令牌时用 vfs_put_notoken()。 */
static void vfs_put(const char *text) {
size_t n = strlen(text);
if (n > VFS_CAP) {
n = VFS_CAP;
}
memcpy(s_vfs, text, n);
s_vfs_len = n;
s_vfs_exists = 1;
s_vfs_mtime++;
}
/* 写入内容但**不改** mtime 令牌(用于构造"内容变、令牌不变"的边界) */
static void vfs_put_notoken(const char *text) {
size_t n = strlen(text);
if (n > VFS_CAP) {
n = VFS_CAP;
}
memcpy(s_vfs, text, n);
s_vfs_len = n;
s_vfs_exists = 1;
}
/* 取虚拟盘当前内容为 NUL 结尾字符串(供检索断言);返回静态缓冲 */
static const char *vfs_get(void) {
static char buf[VFS_CAP + 1];
memcpy(buf, s_vfs, s_vfs_len);
buf[s_vfs_len] = '\0';
return buf;
}
/* 注册标准两段 */
static void register_std(void) {
cfg_section_reg_t a;
cfg_section_reg_t b;
memset(&a, 0, sizeof(a));
a.name = "net";
a.p_data = &s_net_data;
a.p_defaults = &s_net_def;
a.data_size = sizeof(sec_net_t);
a.p_keys = s_net_keys;
a.key_count = (uint8_t)(sizeof(s_net_keys) / sizeof(s_net_keys[0]));
a.p_cb = net_on_change;
memset(&b, 0, sizeof(b));
b.name = "sys";
b.p_data = &s_sys_data;
b.p_defaults = &s_sys_def;
b.data_size = sizeof(sec_sys_t);
b.p_keys = s_sys_keys;
b.key_count = (uint8_t)(sizeof(s_sys_keys) / sizeof(s_sys_keys[0]));
b.p_cb = NULL;
CHECK(cfg_file_register(&a) == 0, "register net section");
CHECK(cfg_file_register(&b) == 0, "register sys section");
}
/* 容量钉专用:把注册表填到指定段数(名字 s0..sN,共用 net 段 RAM/键表) */
static int register_fill(int n)
{
char nm[4];
int i;
int ok = 1;
for (i = 0; i < n; i++) {
cfg_section_reg_t r;
memset(&r, 0, sizeof(r));
nm[0] = 's'; nm[1] = (char)('0' + (i / 10) % 10); nm[2] = (char)('0' + (i % 10)); nm[3] = 0;
r.name = nm;
r.p_data = &s_net_data;
r.p_defaults = &s_net_def;
r.data_size = sizeof(sec_net_t);
r.p_keys = s_net_keys;
r.key_count = (uint8_t)(sizeof(s_net_keys) / sizeof(s_net_keys[0]));
r.p_cb = NULL;
if (cfg_file_register(&r) != 0) {
ok = 0;
}
}
return ok;
}
/* TC-CONFIG-056/057:段数与键数容量钉(道G task-8——[comm] 上线后生产占 6/8,
* 容量与"满则拒"行为必须先在这里显式化,别让第 7 段的接入者在真机上才发现顶格) */
static void tc_capacity(void)
{
printf("[TC-CONFIG-056] section capacity: MAX_SECTIONS pinned, 9th rejected\n");
{
char nm[8];
cfg_section_reg_t r;
/* 数字钉:段上限必须是 8(cfg_file.h:39);改数字 = 容量台账变化,必须
* 同步 docs/子系统设计/配置文件设计说明.md 与本用例生产占位说明 */
CHECK(CFG_FILE_MAX_SECTIONS == 8u, "CFG_FILE_MAX_SECTIONS pinned to 8");
vfs_reset();
CHECK(register_fill((int)CFG_FILE_MAX_SECTIONS) == 1, "filling exactly 8 sections all accepted");
memset(nm, 0, sizeof(nm));
(void)memcpy(nm, "sx", 3u);
memset(&r, 0, sizeof(r));
r.name = nm;
r.p_data = &s_net_data;
r.p_defaults = &s_net_def;
r.data_size = sizeof(sec_net_t);
r.p_keys = s_net_keys;
r.key_count = (uint8_t)(sizeof(s_net_keys) / sizeof(s_net_keys[0]));
CHECK(cfg_file_register(&r) == -1, "9th section rejected when table full");
CHECK(s_sections[CFG_FILE_MAX_SECTIONS - 1u].used == 1u,
"rejection left no partial slot (table intact after full)");
}
printf("[TC-CONFIG-057] key capacity: 16 accepted, 17 rejected (MAX_KEYS pinned)\n");
{
static const cfg_key_def_t keys17[17] = {
{ "k0", CFG_VAL_UINT, 0u, 1u }, { "k1", CFG_VAL_UINT, 1u, 1u },
{ "k2", CFG_VAL_UINT, 2u, 1u }, { "k3", CFG_VAL_UINT, 3u, 1u },
{ "k4", CFG_VAL_UINT, 4u, 1u }, { "k5", CFG_VAL_UINT, 5u, 1u },
{ "k6", CFG_VAL_UINT, 6u, 1u }, { "k7", CFG_VAL_UINT, 7u, 1u },
{ "k8", CFG_VAL_UINT, 8u, 1u }, { "k9", CFG_VAL_UINT, 9u, 1u },
{ "k10", CFG_VAL_UINT, 10u, 1u }, { "k11", CFG_VAL_UINT, 11u, 1u },
{ "k12", CFG_VAL_UINT, 12u, 1u }, { "k13", CFG_VAL_UINT, 13u, 1u },
{ "k14", CFG_VAL_UINT, 14u, 1u }, { "k15", CFG_VAL_UINT, 15u, 1u },
{ "k16", CFG_VAL_UINT, 16u, 1u },
};
static uint8_t kd[17];
cfg_section_reg_t r;
CHECK(CFG_FILE_MAX_KEYS == 16u, "CFG_FILE_MAX_KEYS pinned to 16");
vfs_reset();
memset(&r, 0, sizeof(r));
r.name = "capk";
r.p_data = kd;
r.p_defaults = kd;
r.data_size = sizeof(kd);
r.key_count = 16u;
r.p_keys = keys17;
CHECK(cfg_file_register(&r) == 0, "16-key section accepted at limit");
memset(&r, 0, sizeof(r));
r.name = "capk2";
r.p_data = kd;
r.p_defaults = kd;
r.data_size = sizeof(kd);
r.key_count = 17u;
r.p_keys = keys17;
CHECK(cfg_file_register(&r) == -1, "17-key section rejected over CFG_FILE_MAX_KEYS");
}
}
/* ===================== 用例 ===================== */
/* TC-CONFIG-001/002:UINT 往返与非法值 */
static void tc_uint(void) {
printf("[TC-CONFIG-001] UINT encode/decode round-trip\n");
{
uint8_t d1[1]; uint8_t d2[2]; uint8_t d4[4];
CHECK(s_decode_uint(d1, 1u, "255") == 0 && d1[0] == 0xFFu, "u8 max 255");
CHECK(s_decode_uint(d1, 1u, "0") == 0 && d1[0] == 0u, "u8 zero");
CHECK(s_decode_uint(d2, 2u, "65535") == 0 && d2[0] == 0xFFu && d2[1] == 0xFFu,
"u16 max little-endian");
CHECK(s_decode_uint(d4, 4u, "4294967295") == 0 &&
d4[0] == 0xFFu && d4[3] == 0xFFu, "u32 max little-endian");
/* 小端序验证:0x1234 -> d[0]=0x34 d[1]=0x12 */
CHECK(s_decode_uint(d2, 2u, "4660") == 0 && d2[0] == 0x34u && d2[1] == 0x12u,
"u16 0x1234 little-endian order");
}
printf("[TC-CONFIG-002] UINT illegal values rejected\n");
{
uint8_t d[4];
CHECK(s_decode_uint(d, 1u, "") == -1, "empty rejected");
CHECK(s_decode_uint(d, 1u, "abc") == -1, "non-numeric rejected");
CHECK(s_decode_uint(d, 1u, "12x") == -1, "trailing garbage rejected");
CHECK(s_decode_uint(d, 1u, "256") == -1, "u8 overflow rejected");
CHECK(s_decode_uint(d, 2u, "65536") == -1, "u16 overflow rejected");
CHECK(s_decode_uint(d, 3u, "1") == -1, "invalid len 3 rejected");
CHECK(s_decode_uint(d, 1u, "-1") == -1, "negative rejected");
}
}
/* TC-CONFIG-003:IP4 */
static void tc_ip4(void) {
uint8_t d[4];
printf("[TC-CONFIG-003] IPv4 decode\n");
CHECK(s_decode_ip4(d, "0.0.0.0") == 0 && d[0] == 0u && d[3] == 0u, "all-zero");
CHECK(s_decode_ip4(d, "255.255.255.255") == 0 &&
d[0] == 255u && d[3] == 255u, "all-255");
CHECK(s_decode_ip4(d, "192.168.31.100") == 0 &&
d[0] == 192u && d[1] == 168u && d[2] == 31u && d[3] == 100u, "typical");
CHECK(s_decode_ip4(d, "1.2.3") == -1, "too few octets rejected");
CHECK(s_decode_ip4(d, "1.2.3.4.5") == -1, "too many octets rejected");
CHECK(s_decode_ip4(d, "256.1.1.1") == -1, "octet >255 rejected");
CHECK(s_decode_ip4(d, "a.1.1.1") == -1, "non-digit rejected");
CHECK(s_decode_ip4(d, "1.1.1.") == -1, "trailing dot rejected");
CHECK(s_decode_ip4(d, "") == -1, "empty rejected");
}
/* TC-CONFIG-004:MAC */
static void tc_mac(void) {
uint8_t d[6];
printf("[TC-CONFIG-004] MAC decode (separators/case)\n");
CHECK(s_decode_mac(d, "00:11:22:33:44:55") == 0 &&
d[0] == 0x00u && d[5] == 0x55u, "colon separator");
CHECK(s_decode_mac(d, "00-11-22-33-44-55") == 0 &&
d[1] == 0x11u, "dash separator");
/* 点号仅在**字节边界**作分隔符替代(与 ':'/'-' 等价,见 cfg_file.h 注释);
* 不支持 Cisco 4-4-4 分组("0011.2233.4455")——那不是本引擎的契约 */
CHECK(s_decode_mac(d, "00.11.22.33.44.55") == 0 &&
d[0] == 0x00u && d[1] == 0x11u && d[5] == 0x55u, "dot separator (byte boundary)");
CHECK(s_decode_mac(d, "AA:BB:CC:DD:EE:FF") == 0 &&
d[0] == 0xAAu && d[5] == 0xFFu, "uppercase hex");
CHECK(s_decode_mac(d, "aa:bb:cc:dd:ee:ff") == 0 &&
d[0] == 0xAAu, "lowercase hex");
CHECK(s_decode_mac(d, "00:11:22:33:44") == -1, "too short rejected");
CHECK(s_decode_mac(d, "00:11:22:33:44:55:66") == -1, "too long rejected");
CHECK(s_decode_mac(d, "00:11:22:33:44:zz") == -1, "non-hex rejected");
CHECK(s_decode_mac(d, "00:11:22:33:44;55") == -1, "bad separator rejected");
CHECK(s_decode_mac(d, "") == -1, "empty rejected");
}
/* TC-CONFIG-005:STR */
static void tc_str(void) {
uint8_t d[8];
printf("[TC-CONFIG-005] STR decode (length boundary)\n");
CHECK(s_decode_str(d, 8u, "hello") == 0 && d[0] == 'h' && d[5] == '\0',
"normal string + NUL");
CHECK(s_decode_str(d, 8u, "1234567") == 0 && d[6] == '7' && d[7] == '\0',
"exactly len-1 chars fits");
CHECK(s_decode_str(d, 8u, "12345678") == -1, "len chars rejected (no room for NUL)");
CHECK(s_decode_str(d, 8u, "") == 0 && d[0] == '\0', "empty string ok");
}
/* TC-CONFIG-006/007:INT 解码(「数据可信」有符号零点类型——负零点偏移) */
static void tc_int_decode(void) {
uint8_t d[4];
printf("[TC-CONFIG-006] INT decode valid\n");
CHECK(s_decode_int(d, 4u, "0") == 0 && d[0] == 0u && d[3] == 0u, "int32 zero");
CHECK(s_decode_int(d, 4u, "-1") == 0 && d[0] == 0xFFu && d[1] == 0xFFu &&
d[2] == 0xFFu && d[3] == 0xFFu, "-1 = four FF bytes (two's complement)");
CHECK(s_decode_int(d, 4u, "-2147483648") == 0 && d[0] == 0x00u &&
d[1] == 0x00u && d[2] == 0x00u && d[3] == 0x80u, "INT32_MIN accepted");
CHECK(s_decode_int(d, 4u, "2147483647") == 0 && d[0] == 0xFFu &&
d[1] == 0xFFu && d[2] == 0xFFu && d[3] == 0x7Fu, "INT32_MAX accepted");
CHECK(s_decode_int(d, 1u, "-128") == 0 && d[0] == 0x80u, "int8 -128");
CHECK(s_decode_int(d, 1u, "127") == 0 && d[0] == 0x7Fu, "int8 +127");
CHECK(s_decode_int(d, 2u, "-32768") == 0 && d[0] == 0x00u && d[1] == 0x80u,
"int16 -32768");
CHECK(s_decode_int(d, 2u, "32767") == 0 && d[0] == 0xFFu && d[1] == 0x7Fu,
"int16 +32767");
CHECK(s_decode_int(d, 4u, "+42") == 0 && d[0] == 42u, "leading + accepted");
CHECK(s_decode_int(d, 4u, "-0") == 0 && d[0] == 0u && d[3] == 0u, "-0 decodes 0");
CHECK(s_decode_int(d, 4u, "00012") == 0 && d[0] == 12u && d[3] == 0u,
"leading zeros accepted");
printf("[TC-CONFIG-007] INT decode invalid rejected\n");
CHECK(s_decode_int(d, 4u, "") == -1, "empty rejected");
CHECK(s_decode_int(d, 4u, "abc") == -1, "non-numeric rejected");
CHECK(s_decode_int(d, 4u, "12x") == -1, "trailing garbage rejected");
CHECK(s_decode_int(d, 4u, "-") == -1, "bare sign rejected");
CHECK(s_decode_int(d, 4u, "1.5") == -1, "decimal point rejected");
CHECK(s_decode_int(d, 4u, "0x10") == -1, "hex text rejected");
CHECK(s_decode_int(d, 1u, "-129") == -1, "int8 -129 rejected");
CHECK(s_decode_int(d, 1u, "128") == -1, "int8 128 rejected");
CHECK(s_decode_int(d, 2u, "-32769") == -1, "int16 -32769 rejected");
CHECK(s_decode_int(d, 2u, "32768") == -1, "int16 32768 rejected");
CHECK(s_decode_int(d, 4u, "2147483648") == -1, "INT32_MAX + 1 rejected (ERANGE)");
CHECK(s_decode_int(d, 4u, "-2147483649") == -1, "INT32_MIN - 1 rejected (ERANGE)");
CHECK(s_decode_int(d, 4u, "99999999999") == -1, "huge magnitude rejected (ERANGE)");
CHECK(s_decode_int(d, 4u, "-99999999999") == -1, "huge negative rejected (ERANGE)");
CHECK(s_decode_int(d, 3u, "1") == -1, "invalid len 3 rejected");
}
/* TC-CONFIG-008:INT 编码带负号 + [sys] 段 load/save/load 全链往返 */
static void tc_int_encode(void) {
char buf[24];
uint8_t field[4];
static const cfg_key_def_t k32 = { "ofs32", CFG_VAL_INT, 0u, 4u };
static const cfg_key_def_t k8 = { "ofs8", CFG_VAL_INT, 0u, 1u };
printf("[TC-CONFIG-008] INT encode (sign in text)\n");
field[0] = 0xFFu; field[1] = 0xFFu; field[2] = 0xFFu; field[3] = 0xFFu;
CHECK(s_encode(&k32, field, buf, sizeof(buf)) > 0 && strcmp(buf, "-1") == 0,
"FFFFFFFF encodes -1");
field[0] = 0x00u; field[1] = 0x00u; field[2] = 0x00u; field[3] = 0x80u;
CHECK(s_encode(&k32, field, buf, sizeof(buf)) > 0 &&
strcmp(buf, "-2147483648") == 0, "INT32_MIN bytes encode with sign");
field[0] = 0xFFu; field[1] = 0xFFu; field[2] = 0xFFu; field[3] = 0x7Fu;
CHECK(s_encode(&k32, field, buf, sizeof(buf)) > 0 &&
strcmp(buf, "2147483647") == 0, "INT32_MAX bytes encode");
field[0] = 0x80u;
CHECK(s_encode(&k8, field, buf, sizeof(buf)) > 0 && strcmp(buf, "-128") == 0,
"int8 0x80 encodes -128");
field[0] = 0x7Fu;
CHECK(s_encode(&k8, field, buf, sizeof(buf)) > 0 && strcmp(buf, "127") == 0,
"int8 0x7F encodes 127");
printf("[TC-CONFIG-008] INT section roundtrip (load -> save -> load)\n");
vfs_reset();
register_std();
CHECK(cfg_file_load() == 0, "load creates template");
CHECK(strstr(vfs_get(), "ofs32=-2147483648") != NULL,
"template INT32_MIN default written with sign");
CHECK(strstr(vfs_get(), "ofs8=-5") != NULL, "template int8 default -5");
s_sys_data.ofs32 = -123456;
s_sys_data.ofs16 = -2;
s_sys_data.ofs8 = -128;
CHECK(cfg_file_save() == 0, "save with negatives");
CHECK(strstr(vfs_get(), "ofs32=-123456") != NULL, "negative saved with sign");
CHECK(strstr(vfs_get(), "ofs8=-128") != NULL, "int8 -128 saved");
s_sys_data.ofs32 = 1;
s_sys_data.ofs16 = 1;
s_sys_data.ofs8 = 1;
CHECK(cfg_file_load() == 0, "reload ok");
CHECK(s_sys_data.ofs32 == -123456 && s_sys_data.ofs16 == -2 &&
s_sys_data.ofs8 == -128, "INT values roundtrip identical");
/* 越界文本 = 该键拒绝、留工厂默认并留痕;同段其余键不受牵连 */
vfs_reset();
register_std();
vfs_put("[sys]\r\nbaud=9600\r\nofs32=2147483648\r\nofs8=-129\r\nofs16=-300\r\n");
CHECK(cfg_file_load() == 0, "load with out-of-range INT keys");
CHECK(s_sys_data.ofs32 == s_sys_def.ofs32, "out-of-range ofs32 keeps default");
CHECK(s_sys_data.ofs8 == s_sys_def.ofs8, "out-of-range ofs8 keeps default");
CHECK(s_sys_data.ofs16 == -300, "valid ofs16 in same section parsed");
CHECK(s_sys_data.baud == 9600u, "UINT key beside INT parsed");
}
/* TC-CONFIG-010~015:INI 解析 */
static void tc_parse(void) {
printf("[TC-CONFIG-010..015] INI parsing via load\n");
{
vfs_reset();
register_std();
vfs_put(
"; comment line\r\n"
"# hash comment\r\n"
"\r\n"
"[net]\r\n"
"u8 = 42\r\n"
"ip = 10.0.0.1\r\n"
"unknown_key = whatever\r\n"
"\r\n"
"[unknown_section]\r\n"
"x = 1\r\n"
"\r\n"
"[sys]\r\n"
"baud = 9600\r\n");
CHECK(cfg_file_load() == 0, "load returns 0");
CHECK(s_net_data.u8 == 42u, "TC-CONFIG-010/013: comment skipped, key parsed, ws trimmed");
CHECK(s_net_data.ip[0] == 10u && s_net_data.ip[3] == 1u, "ip parsed");
CHECK(s_net_data.u16 == s_net_def.u16, "TC-CONFIG-014: absent key keeps default (u16)");
CHECK(s_net_data.name[0] == 'd', "TC-CONFIG-014: unknown key leaves default (name)");
CHECK(s_sys_data.baud == 9600u, "multi-section: sys parsed");
}
printf("[TC-CONFIG-012] section/key name case-insensitive + value whitespace trim\n");
{
vfs_reset();
register_std();
/* 段名大小写无关;键名大小写无关;'=' 两侧与值两侧空白修剪。
* 注意:段名括号内**不**支持前导空白([ NET ] 中的前导空格会保留),
* 这是既有契约(s_match_section 只 trim 尾空白),不是本用例的断言点。 */
vfs_put(
"[NET]\r\n"
" U8 = 9 \r\n"
" IP = 1.2.3.4\r\n");
CHECK(cfg_file_load() == 0, "load ok");
CHECK(s_net_data.u8 == 9u, "uppercase section+key matched (case-insensitive)");
CHECK(s_net_data.ip[0] == 1u && s_net_data.ip[3] == 4u, "value whitespace trimmed");
}
printf("[TC-CONFIG-011] LF-only line endings\n");
{
vfs_reset();
register_std();
vfs_put("[net]\nu8=77\nip=8.8.8.8\n");
CHECK(cfg_file_load() == 0, "load ok with LF");
CHECK(s_net_data.u8 == 77u, "LF: value parsed");
CHECK(s_net_data.ip[3] == 8u, "LF: ip parsed");
}
printf("[TC-CONFIG-015] key-value before any section ignored\n");
{
vfs_reset();
register_std();
vfs_put("u8 = 123\r\n[net]\r\nip = 5.5.5.5\r\n");
CHECK(cfg_file_load() == 0, "load ok");
CHECK(s_net_data.u8 == s_net_def.u8, "orphan key before section ignored");
CHECK(s_net_data.ip[0] == 5u, "valid key after section parsed");
}
printf("[TC-CONFIG-014b] invalid value keeps factory default + logs\n");
{
vfs_reset();
register_std();
vfs_put("[net]\r\nu8 = 999\r\nip = not-an-ip\r\n");
g_cfgtest_err_count = 0;
CHECK(cfg_file_load() == 0, "load ok despite invalid values");
CHECK(s_net_data.u8 == s_net_def.u8, "invalid u8 keeps default");
CHECK(s_net_data.ip[0] == s_net_def.ip[0], "invalid ip keeps default");
CHECK(g_cfgtest_err_count >= 2, "invalid values logged (DBG_ERROR)");
}
}
/* TC-CONFIG-020~023:register 校验 */
static void tc_register(void) {
printf("[TC-CONFIG-020] register validation\n");
{
cfg_section_reg_t r;
vfs_reset();
memset(&r, 0, sizeof(r));
r.name = "net";
r.p_data = &s_net_data;
r.p_defaults = &s_net_def;
r.data_size = sizeof(sec_net_t);
r.p_keys = s_net_keys;
r.key_count = 5u;
CHECK(cfg_file_register(&r) == 0, "valid register");
CHECK(cfg_file_register(&r) == -1, "duplicate section name rejected");
CHECK(cfg_file_register(NULL) == -1, "NULL descriptor rejected");
r.name = "another";
r.p_data = NULL;
CHECK(cfg_file_register(&r) == -1, "NULL p_data rejected");
}
printf("[TC-CONFIG-021] invalid key definitions rejected\n");
{
cfg_section_reg_t r;
static const cfg_key_def_t bad_uint[] = { { "k", CFG_VAL_UINT, 0u, 3u } };
static const cfg_key_def_t bad_int[] = { { "k", CFG_VAL_INT, 0u, 3u } };
static const cfg_key_def_t bad_mac[] = { { "k", CFG_VAL_MAC, 0u, 4u } };
static const cfg_key_def_t bad_str[] = { { "k", CFG_VAL_STR, 0u, 80u } };
static const cfg_key_def_t zero_len[] = { { "k", CFG_VAL_UINT, 0u, 0u } };
vfs_reset();
memset(&r, 0, sizeof(r));
r.name = "x";
r.p_data = &s_net_data;
r.p_defaults = &s_net_def;
r.data_size = 8u;
r.key_count = 1u;
r.p_keys = bad_uint;
CHECK(cfg_file_register(&r) == -1, "UINT len=3 rejected");
r.p_keys = bad_int;
CHECK(cfg_file_register(&r) == -1, "INT len=3 rejected");
r.p_keys = bad_mac;
CHECK(cfg_file_register(&r) == -1, "MAC len=4 rejected");
r.p_keys = bad_str;
CHECK(cfg_file_register(&r) == -1, "STR len=80 rejected");
r.p_keys = zero_len;
CHECK(cfg_file_register(&r) == -1, "key len=0 rejected");
}
printf("[TC-CONFIG-022] section name length limit\n");
{
cfg_section_reg_t r;
vfs_reset();
memset(&r, 0, sizeof(r));
r.name = "0123456789abcdef"; /* 16 chars >= CFG_NAME_MAX(16) */
r.p_data = &s_net_data;
r.p_defaults = &s_net_def;
r.data_size = 8u;
r.p_keys = s_sys_keys;
r.key_count = 1u;
CHECK(cfg_file_register(&r) == -1, "section name >= 16 chars rejected");
}
printf("[TC-CONFIG-023] callback section data_size limit\n");
{
cfg_section_reg_t r;
vfs_reset();
memset(&r, 0, sizeof(r));
r.name = "cb";
r.p_data = &s_net_data;
r.p_defaults = &s_net_def;
r.data_size = CFG_SNAP_MAX + 1u; /* > 64,挂回调则拒绝 */
r.p_keys = s_sys_keys;
r.key_count = 1u;
r.p_cb = net_on_change;
CHECK(cfg_file_register(&r) == -1, "callback with data_size>64 rejected");
r.p_cb = NULL;
CHECK(cfg_file_register(&r) == 0, "same section without callback accepted");
}
printf("key name length limit\n");
{
cfg_section_reg_t r;
static const cfg_key_def_t long_key[] = {
{ "0123456789abcdef", CFG_VAL_UINT, 0u, 1u } /* 16 chars >= CFG_NAME_MAX(16) */
};
static const cfg_key_def_t max_key[] = {
{ "0123456789abcde", CFG_VAL_UINT, 0u, 1u } /* 15 chars = CFG_FILE_MAX_NAME_LEN */
};
vfs_reset();
memset(&r, 0, sizeof(r));
r.name = "klen";
r.p_data = &s_net_data;
r.p_defaults = &s_net_def;
r.data_size = 8u;
r.key_count = 1u;
r.p_keys = long_key;
CHECK(cfg_file_register(&r) == -1, "key name >= 16 chars rejected");
r.p_keys = max_key;
CHECK(cfg_file_register(&r) == 0, "key name == 15 chars accepted");
}
}
/* TC-CONFIG-030~034:load 契约 */
static void tc_load(void) {
printf("[TC-CONFIG-030] missing file -> factory template written\n");
{
vfs_reset();
register_std();
s_write_count = 0;
CHECK(cfg_file_load() == 0, "load returns 0 when file missing");
CHECK(s_vfs_exists == 1, "template file created");
CHECK(s_write_count == 1, "one write happened");
/* 模板须含段头与键 */
CHECK(strstr(vfs_get(), "[net]") != NULL, "template has [net]");
CHECK(strstr(vfs_get(), "[sys]") != NULL, "template has [sys]");
CHECK(strstr(vfs_get(), "u8=7") != NULL, "template has default u8=7");
}
printf("[TC-CONFIG-032] oversized file -> factory default + regenerate + syslog\n");
{
size_t i;
vfs_reset();
register_std();
/* 构造 > CFG_FILE_MAX_LEN 的文件 */
for (i = 0; i < CFG_FILE_MAX_LEN + 100u; i++) {
s_vfs[i] = (uint8_t)'A';
}
s_vfs_len = CFG_FILE_MAX_LEN + 100u;
s_vfs_exists = 1;
s_net_data.u8 = 0xEEu; /* 预置脏值,验证被复位 */
s_syslog_count = 0;
CHECK(cfg_file_load() == 0, "oversized load returns 0");
CHECK(s_net_data.u8 == s_net_def.u8, "RAM reset to factory default");
CHECK(s_syslog_count >= 1, "self-test log recorded");
CHECK(s_vfs_len <= CFG_FILE_MAX_LEN, "file regenerated within limit");
}
printf("[TC-CONFIG-033] read failure -> -1, RAM keeps factory default\n");
{
vfs_reset();
register_std();
vfs_put("[net]\r\nu8=99\r\n");
s_fail_read = 1;
CHECK(cfg_file_load() == -1, "read failure returns -1");
CHECK(s_net_data.u8 == s_net_def.u8, "RAM stays factory default");
}
printf("[TC-CONFIG-034] load with no registered section -> -1\n");
{
vfs_reset();
CHECK(cfg_file_load() == -1, "no section registered returns -1");
}
}
/* TC-CONFIG-040~043:save 契约 */
static void tc_save(void) {
printf("[TC-CONFIG-040] save with missing file -> full document\n");
{
vfs_reset();
register_std();
s_net_data.u8 = 12u;
s_sys_data.baud = 38400u;
CHECK(cfg_file_save() == 0, "save ok");
CHECK(s_vfs_exists == 1, "file created");
CHECK(strstr(vfs_get(), "u8=12") != NULL, "current value written");
CHECK(strstr(vfs_get(), "baud=38400") != NULL, "second section written");
}
printf("[TC-CONFIG-041] surgical save preserves comments/unknown\n");
{
vfs_reset();
register_std();
vfs_put(
"; my precious comment\r\n"
"[net]\r\n"
"u8 = 1\r\n"
"vendor_key = keepme\r\n"
"\r\n"
"[vendor]\r\n"
"x = y\r\n");
s_net_data.u8 = 200u;
CHECK(cfg_file_save() == 0, "surgical save ok");
CHECK(strstr(vfs_get(), "; my precious comment") != NULL, "comment preserved");
CHECK(strstr(vfs_get(), "vendor_key = keepme") != NULL ||
strstr(vfs_get(), "vendor_key=keepme") != NULL, "unknown key preserved");
CHECK(strstr(vfs_get(), "[vendor]") != NULL, "unknown section preserved");
CHECK(strstr(vfs_get(), "u8=200") != NULL, "managed key rewritten in place");
}
printf("[TC-CONFIG-042] surgical save appends missing managed section/key\n");
{
vfs_reset();
register_std();
vfs_put(
"; head\r\n"
"[net]\r\n"
"u8 = 3\r\n");
s_sys_data.baud = 57600u;
CHECK(cfg_file_save() == 0, "save ok");
CHECK(strstr(vfs_get(), "[sys]") != NULL, "missing section appended");
CHECK(strstr(vfs_get(), "baud=57600") != NULL, "missing key written");
}
printf("[TC-CONFIG-043] output overflow -> reject, original untouched\n");
{
size_t i;
vfs_reset();
register_std();
/* 构造一个接近上限、且含大量未知内容的文件;外科手术输出会超限 */
for (i = 0; i < CFG_FILE_MAX_LEN - 4u; i++) {
s_vfs[i] = (uint8_t)((i % 60u == 0u) ? '\n' : 'Z');
}
s_vfs_len = CFG_FILE_MAX_LEN - 4u;
s_vfs_exists = 1;
s_syslog_count = 0;
/* 记录原文件长度,验证不动 */
{
size_t before = s_vfs_len;
int r = cfg_file_save();
CHECK(r == -1, "overflow rejected with -1");
CHECK(s_vfs_len == before, "original file untouched on overflow");
CHECK(s_syslog_count >= 1, "overflow self-test logged");
}
}
printf("over-long managed key line -> reject\n");
{
char buf[512];
size_t n;
size_t i;
size_t before;
int r;
/* 构造受管键 u8 的超长行(行总长 > CFG_LINE_TEST_MAX):值为 200 位数字。
* 修复前:Pass1 静默跳过该行 → Pass2 在段头补写新 KV + 原文超长行原样保留
* → 同一键两行。修复后:显式拒绝保存,原文件不动。 */
vfs_reset();
register_std();
n = (size_t)snprintf(buf, sizeof(buf), "[net]\r\nu8 = ");
for (i = 0u; i < 200u && n < sizeof(buf) - 3u; i++) {
buf[n++] = '9';
}
buf[n++] = '\r';
buf[n++] = '\n';
buf[n] = '\0';
vfs_put(buf);
s_net_data.u8 = 7u;
s_syslog_count = 0;
before = s_vfs_len;
r = cfg_file_save();
CHECK(r == -1, "over-long managed key line rejected (-1)");
CHECK(s_vfs_len == before, "original file untouched on over-long line");
CHECK(s_syslog_count >= 1, "line-too-long self-test logged");
/* 反向:**非受管键**的超长行不属本守卫范围,应照旧原样保留、保存成功 */
vfs_reset();
register_std();
n = (size_t)snprintf(buf, sizeof(buf), "[net]\r\nu8=1\r\nvendor = ");
for (i = 0u; i < 200u && n < sizeof(buf) - 3u; i++) {
buf[n++] = 'k';
}
buf[n++] = '\r';
buf[n++] = '\n';
buf[n] = '\0';
vfs_put(buf);
s_net_data.u8 = 9u;
CHECK(cfg_file_save() == 0, "over-long unknown key line tolerated");
CHECK(strstr(vfs_get(), "u8=9") != NULL, "managed key still rewritten");
CHECK(strstr(vfs_get(), "vendor = kkk") != NULL, "unknown long line preserved");
}
}
/* TC-CONFIG-050~054:poll 契约 */
static void tc_poll(void) {
printf("[TC-CONFIG-050] poll first round establishes baseline\n");
{
vfs_reset();
register_std();
vfs_put("[net]\r\nu8=5\r\n");
cfg_file_poll(); /* 建基线 */
CHECK(s_net_data.u8 == s_net_def.u8, "no reload on first poll (baseline only)");
CHECK(s_base_valid == 1u, "baseline established");
}
printf("[TC-CONFIG-051/052] settle debounce + no-change skip\n");
{
vfs_reset();
register_std();
vfs_put("[net]\r\nu8=5\r\n");
cfg_file_poll(); /* baseline */
cfg_file_poll(); /* stable 1, same as base -> no reload */
CHECK(s_net_data.u8 == s_net_def.u8, "same-as-base poll: no reload");
/* 改变文件(mtime 变) -> 需连续 CFG_FILE_SETTLE_CNT 次稳定才重载 */
vfs_put("[net]\r\nu8=88\r\n");
cfg_file_poll(); /* stable=1, 未沉降 */
CHECK(s_net_data.u8 == s_net_def.u8, "not reloaded before settle");
cfg_file_poll(); /* stable=2 -> 重载 */
CHECK(s_net_data.u8 == 88u, "reloaded after settle");
}
printf("[TC-CONFIG-053] change triggers reload + callback\n");
{
vfs_reset();
register_std();
vfs_put("[net]\r\nu8=5\r\nip=1.1.1.1\r\n");
cfg_file_poll(); /* baseline */
s_net_cb_count = 0;
vfs_put("[net]\r\nu8=5\r\nip=2.2.2.2\r\n"); /* ip 变化 + mtime 令牌变 */
cfg_file_poll();
cfg_file_poll(); /* settle -> reload + cb */
CHECK(s_net_data.ip[0] == 2u, "changed value reloaded");
CHECK(s_net_cb_count >= 1, "callback fired on section change");
}
printf("[TC-CONFIG-055] content change without size/mtime token change -> not detected\n");
{
vfs_reset();
register_std();
vfs_put("[net]\r\nu8=55\r\n"); /* 基线:u8 两位 */
cfg_file_poll(); /* baseline */
/* 只改内容、且**长度与 mtime 都不变**:(size,mtime) 令牌相同 -> 不重载。
* 这是既有契约(令牌 = 变更信号源);真实写盘 mtime 必变,故不构成漏更新 */
vfs_put_notoken("[net]\r\nu8=77\r\n"); /* 同为两位字符,size 不变 */
cfg_file_poll();
cfg_file_poll();
CHECK(s_net_data.u8 == s_net_def.u8,
"same-token change ignored (size+mtime is the change signal)");
}
printf("[TC-CONFIG-054] poll detects oversized file -> factory + regenerate + syslog\n");
{
size_t i;
vfs_reset();
register_std();
vfs_put("[net]\r\nu8=5\r\n");
cfg_file_poll(); /* baseline */
/* 膨胀文件到超限 */
for (i = 0; i < CFG_FILE_MAX_LEN + 50u; i++) {
s_vfs[i] = (uint8_t)'B';
}
s_vfs_len = CFG_FILE_MAX_LEN + 50u;
s_vfs_mtime++;
s_net_data.u8 = 0xABu;
s_syslog_count = 0;
s_net_cb_count = 0;
cfg_file_poll();
cfg_file_poll(); /* settle -> 走超长分支 */
CHECK(s_net_data.u8 == s_net_def.u8, "oversized poll resets to factory default");
CHECK(s_syslog_count >= 1, "oversized poll logged to self-test");
CHECK(s_vfs_len <= CFG_FILE_MAX_LEN, "file regenerated within limit");
CHECK(s_net_cb_count == 0, "no callback on oversized (online-repair channel kept)");
}
}
int main(void) {
printf("=== Config File Tests ===\n");
tc_uint();
tc_int_decode();
tc_int_encode();
tc_ip4();
tc_mac();
tc_str();
tc_parse();
tc_register();
tc_capacity();
tc_load();
tc_save();
tc_poll();
printf("\n=== Config File Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}