/* * 模块名称: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-CFG-001 UINT 1/2/4 字节小端往返(含 0 / 最大值) * TC-CFG-002 UINT 非法:空 / 非数字 / 带后缀 / 超范围 * TC-CFG-003 IP4 正常与非法(段数不足、>255、前导非数字、多余后缀) * TC-CFG-004 MAC 三种分隔符 + 大小写 + 非法 * TC-CFG-005 STR 正常 / 超长拒绝 / 边界(恰好放下) * —— INI 解析(s_split_line / s_match_section / s_key_value / s_parse)—— * TC-CFG-010 注释(; 与 #)与空行忽略 * TC-CFG-011 CRLF / LF 混排 * TC-CFG-012 段名大小写无关匹配、前后空白修剪 * TC-CFG-013 键名大小写无关、值前后空白修剪 * TC-CFG-014 未知段 / 未知键留默认(不崩溃) * TC-CFG-015 键值行出现在段头之前 = 忽略 * —— 注册校验(cfg_file_register)—— * TC-CFG-020 正常注册 / 重复段名拒绝 / NULL 参数拒绝 * TC-CFG-021 非法键定义拒绝(UINT len 非 1/2/4、MAC len≠6、STR len>79) * TC-CFG-022 段名超长拒绝(≥ CFG_NAME_MAX) * TC-CFG-023 挂回调段 data_size > CFG_SNAP_MAX 拒绝 * —— load 契约(cfg_file_load)—— * TC-CFG-030 文件缺失 -> 生成出厂模板写盘 + 返回 0 * TC-CFG-031 文件存在 -> 解析有效键进 RAM 副本 * TC-CFG-032 超长文件(> CFG_FILE_MAX_LEN)-> 工厂默认 + 重建模板 + 自检留痕 * TC-CFG-033 读取失败 -> 返回 -1(RAM 保持工厂默认) * TC-CFG-034 未注册任何段 -> 返回 -1 * —— save 契约(cfg_file_save)—— * TC-CFG-040 文件缺失 -> 重建完整文档 * TC-CFG-041 外科手术:保留注释/未知段/未知键,受管键原位改写 * TC-CFG-042 缺失受管段/键补写 * TC-CFG-043 输出超长 -> 拒绝保存(-1) 且原文件不动 * —— poll 契约(cfg_file_poll)—— * TC-CFG-050 首轮建立基线(不重载) * TC-CFG-051 沉降去抖:连续 2 次稳定才重载 * TC-CFG-052 与基线一致不重载 * TC-CFG-053 变更后重载 + 挂回调段差异触发回调 * TC-CFG-054 溢出到文件超长 -> 工厂默认 + 重建模板 + 自检留痕(不触发回调) * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * * 创建日期:2026-10-02 */ #include #include #include #include /* 先纳入桩头(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"; static const char *s_legacy_path = "/netcfg.dat"; /* ---- 堆:直接映射宿主 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; } const char *cfg_store_legacy_path(void) { return s_legacy_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],无回调,用于验证多段与超长字段拒绝 */ typedef struct { uint32_t baud; /* offset 0 len 4 UINT */ } sec_sys_t; static sec_sys_t s_sys_data; static const sec_sys_t s_sys_def = { 115200u }; static const cfg_key_def_t s_sys_keys[] = { { "baud", CFG_VAL_UINT, offsetof(sec_sys_t, baud), 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"); } /* ===================== 用例 ===================== */ /* TC-CFG-001/002:UINT 往返与非法值 */ static void tc_uint(void) { printf("[TC-CFG-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-CFG-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-CFG-003:IP4 */ static void tc_ip4(void) { uint8_t d[4]; printf("[TC-CFG-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.1.100") == 0 && d[0] == 192u && d[1] == 168u && d[2] == 1u && 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-CFG-004:MAC */ static void tc_mac(void) { uint8_t d[6]; printf("[TC-CFG-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-CFG-005:STR */ static void tc_str(void) { uint8_t d[8]; printf("[TC-CFG-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-CFG-010~015:INI 解析 */ static void tc_parse(void) { printf("[TC-CFG-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-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-014: absent key keeps default (u16)"); CHECK(s_net_data.name[0] == 'd', "TC-014: unknown key leaves default (name)"); CHECK(s_sys_data.baud == 9600u, "multi-section: sys parsed"); } printf("[TC-CFG-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-CFG-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-CFG-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-CFG-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-CFG-020~023:register 校验 */ static void tc_register(void) { printf("[TC-CFG-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-CFG-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_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_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-CFG-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-CFG-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"); } } /* TC-CFG-030~034:load 契约 */ static void tc_load(void) { printf("[TC-CFG-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-CFG-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-CFG-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-CFG-034] load with no registered section -> -1\n"); { vfs_reset(); CHECK(cfg_file_load() == -1, "no section registered returns -1"); } } /* TC-CFG-040~043:save 契约 */ static void tc_save(void) { printf("[TC-CFG-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-CFG-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-CFG-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-CFG-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"); } } } /* TC-CFG-050~054:poll 契约 */ static void tc_poll(void) { printf("[TC-CFG-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-CFG-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-CFG-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-CFG-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-CFG-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_ip4(); tc_mac(); tc_str(); tc_parse(); tc_register(); 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; }