506eccc68f
本次会话先后销项与关闭: - 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。
875 lines
33 KiB
C
875 lines
33 KiB
C
/*
|
||
* 模块名称: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;
|
||
}
|