Files
STM32F4-Base/test/host/cfg_file/cfg_file_test.c
T
wandering1 506eccc68f docs: 错误码传播审计收口并删除该文档(E-xx 转历史编号族)
本次会话先后销项与关闭:
- E-61 SD2506 清 INTAF:原实现只读取 CTR1,而按手册 ARST=0 时读操作
  不清标志,属空操作;改为读-改-写 CTR1,并按手册订正 CTR1 位宏
  (INTAF/INTDF/BLF 位置,ARST 归 CTR3)
- E-16 cfg_file:新增 CFG_FILE_MAX_NAME_LEN(段名/键名 ≤15),注册时
  拒绝超长键名;s_gen_document 两处 snprintf 改检查式;补 TC-CFG-024
  用例(主机单测 149→151 PASSED)
- E-23 TC704:PHASE7_MULTI_COUNT 8→5(并发上限 = 8 − 3 个常驻监听),
  accept 失败分支加网络代次检查(网络重建即重建监听)
- E-26(UDP)按“保留 API + 文档定案”关闭;E-30 评估为不必要后关闭;
  N-4(ACCEPT 决定不实施)关闭

审计文档剩余 5 项 P2 + 2 项监督,经用户裁定“都不重要”后整体删除:
- 删除 docs/问题记录/错误码传播审计.md(634 行)
- 九类缺陷模式表迁入 docs/编号规范.md §4.2.1(PAT-01~09)
- E-xx 转为纯历史编号族:编号规范 §4.1 属主改“无(原属主已删除)”,
  §4.3 保持冻结;全仓 26 处引用按“保留编号、去悬空文档名”处理
- 顺带修正 N-4 收口遗留:排期详述 C1 才是 E-25/E-27/E-28 的承载处
  (审计 §4 曾误写为登记簿);README 两处 C1 旧名改回“net 层跨上下文收口”

一并提交此前遗留的体系层评审报告 2026-10-14 复核 WIP(六维评分复核、
未闭环 Top 5→3、三区水位实测刷新)。

校验:Keil 构建 0 错误 0 警告(Code=212344);check_doc_links
--sections-strict 与 --all(142 文档)通过;内存双台账刷新后均 PASS。
2026-10-15 07:39:38 +08:00

875 lines
33 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-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 拒绝
* TC-CFG-024 键名超长拒绝(≥ CFG_NAME_MAX);恰好 15 字符(= CFG_FILE_MAX_NAME_LEN)接受
* —— 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) 且原文件不动
* TC-CFG-044 受管键行超 CFG_LINE_TEST_MAX -> 拒绝保存(-1) 原文件不动(P2-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 <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],无回调,用于验证多段与超长字段拒绝 */
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");
}
printf("[TC-CFG-024] 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-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");
}
}
printf("[TC-CFG-044] over-long managed key line -> reject (P2-1)\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-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;
}