test(NETCFG,CFG): P1①四个主机单测模块,并修 net_cfg 的 WAIT 契约与定长错记
按 docs/测试说明/主机单测覆盖率口径.md §9 的清单吃 P1 第①批「配置与工具」四块, 新增 4 个主机单测模块共 104 条断言,全部 MSVC 编译通过并 PASS: - test/host/net_cfg/(45 条):出厂默认可用、注册幂等与内容、load 报 -1 仍返回 0 (RAM 恒有效契约)、跨任务保存交接状态机 IDLE/WAIT/OK/FAIL、MAC 三态优先级、 IPv4 定长格式化。 - test/host/ftp_cfg/(16 条):[ftp] 段注册(**p_cb 必须为 NULL**,口令下次登录才 生效)、默认口令判定大小写不敏感、NULL 一律判不等(fail-closed)。 - test/host/prot_set_store/(16 条):blob 返回码分流 1 无档 / 0 成功 / **2 换代 (只打 INFO)** / -1 损坏;写盘失败必须回置脏标志(否则定值静默丢失)。 - test/host/task_prof/(27 条):无效样本闸门(防 DWT 复位把 max 抬到 2^32)、 未 enter 就 exit 不记账、首次 5s + 每 2s 节流、环形缓冲与裸发两通道、 周期→微秒换算随 SystemCoreClock 变。 覆盖率收益(已 --update-baseline 登记):套件 16 → 20 全绿;files_tested 19 → 23; 未触及 125 → 121 个文件(.c 47 → 43、9,657 → 9,228 行);分母 2,893 → 3,163 插桩 行;line=2811/3163(88.9%)、func=225/240(93.8%)。§9 的恒等式同步更新且仍咬合: 43 = 9(P1 剩) + 5(P2) + 29(P3)、9,228 = 2,188 + 2,226 + 4,814。 新单测照出并修掉两处真问题(都在 net_cfg,不是测试写错): 1. 实现违反自己的文档契约:net_cfg.h 明写"属主任务写盘中显示 WAIT",但 net_cfg_save_run 为了不丢新请求会先清 pending 再写盘 → 写盘那几百毫秒里 UI 保存行退回上一轮的 OK/IDLE(第一次保存按 ENTER 后屏幕上还是 ENTER),用户只 会再按一次。补 s_save_busy 标志,TC-NETCFG-028/026/030 钉死(含失败路径必须 清 busy,否则 UI 永久卡 WAIT)。 2. "定长 11 字符"是错的:%03u.%03u.%03u.%03u = 4 段×3 位 + 3 个点 = **15**。这个数 错了 4 处(net_cfg.h、net_cfg.c、网络配置说明.md 两处)——是我照抄它才撞上 FAIL,错在文档不是代码。已全部更正,并确认 len ≥ 16 的要求与 UI_BUF_STRLEN=32 本来就没问题(无截断风险)。 桩的口径(沿用既有模块,不另起一套):真实头优先——cfg_file.h / cfg_file_store.h / console_log.h / prot_set.h 全用真头(接口一漂移就编译期报错),只有 main.h 必须桩 (真实 main.h 拉整棵 HAL 头树,PC 上编不动;桩里字段名与位掩码宏名与 CMSIS 一致)。 prot_set_store 连真实 prot_set.c 一起编:blob 的编解码与脏标志在它里面,只有拿真 serialize 产出的 blob 才能造出"换代"与"损坏"两种真实形态,手搓等于测自己的桩。 配置引擎本身(解析/写盘/沉降)归 test/host/cfg_file 的 103 条,本批不重复测。 编号门禁:按 2026-10-21 批 3 的 B 层口径补建 4 族 TC-HOST-*(NETCFG 游标 056 / FTPCFG 014 / PROTSTOR 016 / TASKPROF 020,代码即属主登记处)。这一步不是走形式: 建族之前文档里引用 TC-NETCFG-028 被判「台账之外新账」直接卡提交,建族后才通过; 编号规范 §8.4 变更记录加一行说明族数 9 → 13。id_registry.json 只做增量插入(+88 行, 无重排)。 自查(提交时 pre-commit run --all-files,**仅 3 条失败且都与本分支改动无关**): - mem-numbers / mem-budget:本 worktree 因需验证代码改动而 @build 过,新生成的 .map 与 docs/内存管理 受管快照漂移。实测增量 ER_IROM1 +12 B、RW_IRAM1 +8 B、Flash +16 B、**CCM 0**,水位百分比全不变(改动 = 1 个静态字节 + 几条指令)。按并行分支 约定**不在分支里 --write**,台账由 Lead 合并后统一回填。 - ui-golden:simulator/tests/ref/*.bin 写在 .gitignore 里(黄金参考是本地生成物), 新建的 worktree 天生没有,报的是"缺黄金参考"而非像素差异;本分支零 UI 文件改动。 - 其余全部 Passed,含 @build 0 错误 0 警告、test-cppcheck(它抓到过我一条 p!=NULL 后又解引用的写法,已改为断言+显式守卫)、coding-standard、doc-links(35 处=存量)、 id-check --strict、mojibake 与 BOM/CR/U+FFFD/行尾逐项卫生。 - 自审修掉自己写的两条恒真断言(形如 x==0?1:1 的假绿)与一个重复编号标签。 - 4 个新模块**尚未挂 pre-commit 钩子**:.pre-commit-config.yaml 是单写文件,任务工作 树内不改;覆盖率脚本按 test/host/**/run_unit_test.py 自动发现,已实跑 20/20。 - 未跑 @test:改动仅 1 个静态字节 + UI 保存行的显示时序,无硬件语义变化。
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* 模块名称:FTP 鉴权配置([ftp] 段)主机侧单元测试
|
||||
* 模块功能:在 PC 上离线验证 Drivers/BSP/NETCFG/ftp_cfg.c 的注册契约与
|
||||
* "出厂默认口令是否在用"的判定(大小写不敏感)。
|
||||
*
|
||||
* 被测对象:真实源文件 ../../../Drivers/BSP/NETCFG/ftp_cfg.c(#include 原样纳入)。
|
||||
* 桩只有一个:真实头 App/cfg/cfg_file.h + 本文件给出的 cfg_file_register
|
||||
* 可控实现(按值留档注册描述符)。本模块**零 stub 目录**——ftp_cfg.c 不
|
||||
* 包含 dbg_log.h,也没有别的平台依赖。
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只靠台架):
|
||||
* 1. 这个模块是 **FTP 的门禁**(N-19):pwd_hash 存的是 SHA-256(user‖password),
|
||||
* 判定写成大小写敏感就会在"用户手输大写 hex"时把正确口令当错误;写成敏感于
|
||||
* 默认值大小写则会误报"已改口令",让出厂默认口令的启动告警**静默失效**——
|
||||
* 现场表现是"以为改了口令"。
|
||||
* 2. [ftp] 段刻意**不挂变更回调**(口令在下次登录 PASS 时才生效):哪天有人"顺手
|
||||
* 补一个回调",poll 重载会去重建网络——这条契约只能靠断言钉住(TC-FTPCFG-004)。
|
||||
* 3. write_en 出厂 = 1 是刻意的(FTP 推 config.ini 是既有维护工作流),改成 0
|
||||
* 会让现场升级流程直接失败;同样需要一个显式断言(TC-FTPCFG-005)。
|
||||
* ⚠ DEF_HASH 这个常量本身的正确性由 test/host/sha256/ 的 TC-SHA256-008 钉
|
||||
* (它算 SHA256("adminadmin") 并与 FTP_CFG_DEF_HASH 比对),本模块不重复算。
|
||||
*
|
||||
* 覆盖点:
|
||||
* TC-FTPCFG-001a get 返回恒非空(静态常驻,load 前后都可用)
|
||||
* TC-FTPCFG-001 未注册即可 get:user=admin(出厂默认口令在用)
|
||||
* TC-FTPCFG-002 pwd_hash 初值 = FTP_CFG_DEF_HASH,write_en 初值 = 1
|
||||
* TC-FTPCFG-003 注册幂等:重复 register 只向引擎登记一次
|
||||
* TC-FTPCFG-004 注册内容:段名 ftp / 三键 / 尺寸 / 默认值指针 / **p_cb 必须为 NULL**
|
||||
* TC-FTPCFG-005 键表逐项:user(STR,16) / pwd_hash(STR,65) / write(UINT,1)
|
||||
* TC-FTPCFG-006 出厂态 is_default_pwd = 1
|
||||
* TC-FTPCFG-007 改了口令 → is_default_pwd = 0
|
||||
* TC-FTPCFG-008 哈希存成**大写** hex 仍判"默认口令在用"(大小写不敏感)
|
||||
* TC-FTPCFG-009 空哈希不是默认口令(且不读越界)
|
||||
* TC-FTPCFG-010 混入非字母字符的串按逐字符比较(数字/下划线不被折叠改写)
|
||||
* TC-FTPCFG-011 前缀关系不算相等("abc" vs "abcd")
|
||||
* TC-FTPCFG-012 任一参数为 NULL 判不等(is_default_pwd 走不到,故直测静态函数)
|
||||
* TC-FTPCFG-013 两串都为 NULL 也判不等(fail-closed,不许"都为空=相等")
|
||||
* TC-FTPCFG-014 缓冲尺寸契约:user 16 / hash 65(64 hex + NUL)
|
||||
*
|
||||
* 用例顺序有依赖:001/002/006 必须在任何"改写 RAM 副本"的用例之前跑。
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg_file.h"
|
||||
#include "ftp_cfg.h"
|
||||
|
||||
#include "ftp_cfg.c"
|
||||
|
||||
/* ===================== 断言框架 ===================== */
|
||||
|
||||
static int s_total;
|
||||
static int s_passed;
|
||||
static int s_failed;
|
||||
|
||||
#define CHECK(cond, name) do { \
|
||||
s_total++; \
|
||||
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
|
||||
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
/* ===================== 假引擎:只留注册记录 ===================== */
|
||||
|
||||
typedef struct {
|
||||
char name[CFG_FILE_MAX_NAME_LEN + 1u];
|
||||
const void *p_data;
|
||||
const void *p_defaults;
|
||||
size_t data_size;
|
||||
uint8_t key_count;
|
||||
void (*p_cb)(void);
|
||||
const cfg_key_def_t *p_keys;
|
||||
} fake_reg_t;
|
||||
|
||||
static fake_reg_t g_reg;
|
||||
static int g_reg_n;
|
||||
|
||||
static void fake_copy_name(char *dst, const char *src, size_t cap)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0u; (i + 1u < cap) && (src[i] != '\0'); i++) {
|
||||
dst[i] = src[i];
|
||||
}
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
int cfg_file_register(const cfg_section_reg_t *p_reg)
|
||||
{
|
||||
if ((p_reg == NULL) || (p_reg->name == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
fake_copy_name(g_reg.name, p_reg->name, sizeof(g_reg.name));
|
||||
g_reg.p_data = p_reg->p_data;
|
||||
g_reg.p_defaults = p_reg->p_defaults;
|
||||
g_reg.data_size = p_reg->data_size;
|
||||
g_reg.key_count = p_reg->key_count;
|
||||
g_reg.p_cb = p_reg->p_cb;
|
||||
g_reg.p_keys = p_reg->p_keys;
|
||||
g_reg_n++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void fake_reset(void)
|
||||
{
|
||||
(void)memset(&g_reg, 0, sizeof(g_reg));
|
||||
g_reg_n = 0;
|
||||
}
|
||||
|
||||
/* 把 s_cfg.pwd_hash 写成任意串(白盒:引擎就是这么原地更新 RAM 副本的) */
|
||||
static void set_hash(const char *p_src)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0u; (i + 1u < FTP_CFG_HASH_MAX) && (p_src[i] != '\0'); i++) {
|
||||
s_cfg.pwd_hash[i] = p_src[i];
|
||||
}
|
||||
s_cfg.pwd_hash[i] = '\0';
|
||||
}
|
||||
|
||||
/* 把出厂哈希原样写成大写形态(引擎写入的 hex 大小写不可控,判定必须容忍) */
|
||||
static void set_hash_upper(void)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0u; i + 1u < FTP_CFG_HASH_MAX; i++) {
|
||||
char ch = FTP_CFG_DEF_HASH[i];
|
||||
|
||||
if ((ch >= 'a') && (ch <= 'z')) {
|
||||
ch = (char)(ch - 32);
|
||||
}
|
||||
s_cfg.pwd_hash[i] = ch;
|
||||
if (ch == '\0') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== 注册契约 ===================== */
|
||||
|
||||
static void tc_register(void)
|
||||
{
|
||||
printf("-- TC-FTPCFG-003..005 register --\n");
|
||||
fake_reset();
|
||||
ftp_cfg_register();
|
||||
CHECK(g_reg_n == 1, "TC-FTPCFG-003a 首次注册一次");
|
||||
ftp_cfg_register();
|
||||
CHECK(g_reg_n == 1, "TC-FTPCFG-003 重复 register 不重复登记(幂等)");
|
||||
|
||||
CHECK((strcmp(g_reg.name, "ftp") == 0) &&
|
||||
(g_reg.data_size == sizeof(ftp_cfg_t)) &&
|
||||
(g_reg.key_count == 3u) &&
|
||||
(g_reg.p_defaults != NULL) &&
|
||||
(g_reg.p_data == (const void *)&s_cfg) &&
|
||||
(g_reg.p_cb == NULL),
|
||||
"TC-FTPCFG-004 段名/尺寸/键数/默认值/**p_cb 必须为 NULL**");
|
||||
|
||||
CHECK((strcmp(g_reg.p_keys[0].name, "user") == 0) &&
|
||||
(g_reg.p_keys[0].type == CFG_VAL_STR) &&
|
||||
(g_reg.p_keys[0].len == FTP_CFG_USER_MAX) &&
|
||||
(g_reg.p_keys[0].offset == offsetof(ftp_cfg_t, user)) &&
|
||||
(strcmp(g_reg.p_keys[1].name, "pwd_hash") == 0) &&
|
||||
(g_reg.p_keys[1].type == CFG_VAL_STR) &&
|
||||
(g_reg.p_keys[1].len == FTP_CFG_HASH_MAX) &&
|
||||
(strcmp(g_reg.p_keys[2].name, "write") == 0) &&
|
||||
(g_reg.p_keys[2].type == CFG_VAL_UINT) &&
|
||||
(g_reg.p_keys[2].len == 1u),
|
||||
"TC-FTPCFG-005 键表逐项:user(STR,16)/pwd_hash(STR,65)/write(UINT,1)");
|
||||
}
|
||||
|
||||
/* ===================== 出厂默认与只读访问 ===================== */
|
||||
|
||||
static void tc_defaults(void)
|
||||
{
|
||||
const ftp_cfg_t *p = ftp_cfg_get();
|
||||
|
||||
printf("-- TC-FTPCFG-001..002,014 defaults --\n");
|
||||
/* 契约就是"恒非空"(静态常驻),单独一条断言;随后的显式守卫既让
|
||||
* test-cppcheck 不再报"冗余或空指针二选一",也让真出问题时报在 001a 上 */
|
||||
CHECK(p != NULL, "TC-FTPCFG-001a ftp_cfg_get 恒非空(静态常驻)");
|
||||
if (p == NULL) {
|
||||
return;
|
||||
}
|
||||
CHECK(strcmp(p->user, "admin") == 0, "TC-FTPCFG-001 未注册也可 get,user = admin");
|
||||
CHECK((strcmp(p->pwd_hash, FTP_CFG_DEF_HASH) == 0) && (p->write_en == 1u),
|
||||
"TC-FTPCFG-002 初值 = 出厂哈希 + write_en=1(维护工作流依赖)");
|
||||
CHECK((FTP_CFG_USER_MAX == 16u) && (FTP_CFG_HASH_MAX == 65u) &&
|
||||
(sizeof(p->user) == FTP_CFG_USER_MAX) &&
|
||||
(sizeof(p->pwd_hash) == FTP_CFG_HASH_MAX),
|
||||
"TC-FTPCFG-014 缓冲尺寸契约:user 16 / hash 65(64 hex + NUL)");
|
||||
}
|
||||
|
||||
/* ===================== 默认口令判定(启动告警的依据) ===================== */
|
||||
|
||||
static void tc_default_pwd(void)
|
||||
{
|
||||
printf("-- TC-FTPCFG-006..009 default-password detection --\n");
|
||||
CHECK(ftp_cfg_is_default_pwd() == 1, "TC-FTPCFG-006 出厂态判为默认口令");
|
||||
|
||||
set_hash("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
|
||||
CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-007 改了口令 → 不再默认");
|
||||
|
||||
set_hash_upper();
|
||||
CHECK(ftp_cfg_is_default_pwd() == 1,
|
||||
"TC-FTPCFG-008 大写 hex 仍判默认(比较必须大小写不敏感)");
|
||||
|
||||
set_hash("");
|
||||
CHECK(ftp_cfg_is_default_pwd() == 0, "TC-FTPCFG-009 空哈希不是默认口令");
|
||||
}
|
||||
|
||||
/* ===================== 静态比较函数(NULL / 折叠 / 前缀) ===================== */
|
||||
|
||||
static void tc_ci_eq(void)
|
||||
{
|
||||
printf("-- TC-FTPCFG-010..013 case-insensitive compare --\n");
|
||||
CHECK(s_str_ci_eq("A_b1!Z", "a_b1!z") == 1,
|
||||
"TC-FTPCFG-010 只折叠 A-Z,数字/下划线/符号原样比较");
|
||||
CHECK(s_str_ci_eq("abc", "abcd") == 0, "TC-FTPCFG-011 前缀关系不算相等");
|
||||
CHECK((s_str_ci_eq(NULL, "x") == 0) && (s_str_ci_eq("x", NULL) == 0),
|
||||
"TC-FTPCFG-012 任一为 NULL 判不等");
|
||||
CHECK(s_str_ci_eq(NULL, NULL) == 0,
|
||||
"TC-FTPCFG-013 两个 NULL 也判不等(fail-closed)");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== FTP Auth Config Tests ===\n");
|
||||
|
||||
tc_defaults(); /* 必须最先:之后会改写 RAM 副本 */
|
||||
tc_register();
|
||||
tc_default_pwd();
|
||||
tc_ci_eq();
|
||||
|
||||
printf("\n=== FTP Auth Config Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""FTP 鉴权配置(ftp_cfg,[ftp] 段)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/ftp_cfg/ftp_cfg_test.c(以 #include 纳入**生产源文件**
|
||||
Drivers/BSP/NETCFG/ftp_cfg.c),在 PC 上离线跑注册契约(幂等、键表、**不挂回调**)
|
||||
与出厂默认口令判定(大小写不敏感、NULL fail-closed),失败即非 0 退出。
|
||||
工具链探测与判定全部委托给 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
本模块是 FTP 的门禁(N-19)。判定写成大小写敏感 → 用户手输大写 hex 时正确口令被拒;
|
||||
反过来对出厂值敏感 → "已改口令"误报,出厂默认口令的启动告警**静默失效**(现场以为
|
||||
改了口令)。[ftp] 段刻意不挂变更回调(口令下次登录才生效),有人"顺手补上"就会在
|
||||
poll 重载时去重建网络。write_en 出厂 = 1 也是刻意的(FTP 推 config.ini 是既有维护
|
||||
工作流)。这些都是一行代码就能改错、而真机只看得到 230/530 的东西。
|
||||
哈希常量本身的正确性归 test/host/sha256/(TC-SHA256-008),本模块不重复算。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/ftp_cfg/run_unit_test.py
|
||||
或统一用 .venv: & '.venv\Scripts\python.exe' test/host/ftp_cfg/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:docs/子系统设计/网络配置说明.md §3.4、docs/测试说明/主机单测覆盖率口径.md §9 P1
|
||||
创建日期:2026-10-23
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 本模块零桩目录:ftp_cfg.c 不含 dbg_log.h,也没有别的平台依赖;
|
||||
# cfg_file.h 用真实头(接口原汁),被调的 cfg_file_register 由测试单元给实现。
|
||||
BUILD = TestBuild(
|
||||
name="FTP Auth Config Tests",
|
||||
src=os.path.join(HERE, "ftp_cfg_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(ROOT, "Drivers", "BSP", "NETCFG"),
|
||||
os.path.join(ROOT, "App", "cfg"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== FTP Auth Config Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
/*
|
||||
* 模块名称:网络配置(NETCFG)主机侧单元测试
|
||||
* 模块功能:在 PC 上离线验证 Drivers/BSP/NETCFG/net_cfg.c 的**编排与状态机**:
|
||||
* 出厂默认有效性、注册幂等与注册内容、load 失败仍返回 0 的"RAM 恒有效"
|
||||
* 契约、跨任务保存请求交接(IDLE/WAIT/OK/FAIL)、写盘期间新请求不丢、
|
||||
* 写盘失败不得触发网络重建、MAC 三态优先级、IPv4 定长格式化。
|
||||
*
|
||||
* 被测对象:真实源文件 ../../../Drivers/BSP/NETCFG/net_cfg.c(#include 原样纳入,
|
||||
* 保证测的是生产代码而非副本)。桩只有三处,都写在下面:
|
||||
* - stub/dbg_log.h:日志宏 → 计数(本目录);
|
||||
* - 配置文件引擎:用**真实头** App/cfg/cfg_file.h(接口契约原汁,引擎
|
||||
* 一改签名本测试立刻编译期报错),被调的三个函数
|
||||
* cfg_file_register / cfg_file_load / cfg_file_save 由本文件给出
|
||||
* **可控实现**(同 test/host/nand_port/ 的路子);
|
||||
* - ftp_cfg_register:只计数不实现——[ftp] 段自身的解析/默认值/默认
|
||||
* 口令判定归 test/host/ftp_cfg/ 模块,两边不重复也不互相顶包。
|
||||
* (不能把 ftp_cfg.c 也 include 进来:两个 .c 都有 static s_cfg,
|
||||
* 同一个 TU 里直接重定义。)
|
||||
* ⚠ 引擎的解析/写盘/沉降契约已由 test/host/cfg_file/(103 条)覆盖,
|
||||
* 本模块刻意不重测引擎,只测 net_cfg.c 这一层的编排与交接。
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只靠台架):
|
||||
* 1. 保存链是**跨任务交接**(uiTask 投递 net_cfg_save_request → defaultTask 执行
|
||||
* net_cfg_save_run → 成功才 net_cfg_request_apply → netTask 按网络代次重建监听)。
|
||||
* "写盘期间 UI 又按了一次 ENTER"与"写盘失败却去重建网络"这两条,真机上要靠
|
||||
* 手速 + 故障注入才碰得到;错了就是"改了 IP 不生效"或"保存失败网络被无端重建"
|
||||
* 这类现场极难定位的问题——主机侧用假引擎的写盘钩子直接构造时序。
|
||||
* 2. MAC 三态优先级(存储非零 > 芯片回写影子 > 全零)决定 UI 显示值和下发给
|
||||
* CH395F 的 MAC;写错会**静默用错 MAC**(同网段 MAC 冲突,症状是偶发丢包)。
|
||||
* 3. net_cfg_fmt_ipv4 的**定长 15 字符(每段 %03u 补零)**是 UI 编辑光标按固定偏移定位
|
||||
* 字节的前提;哪天被"优化"成不补零,光标就错位——纯字符串行为,一次钉死。
|
||||
*
|
||||
* 覆盖点:
|
||||
* —— 出厂默认(load 之前也必须可用,TC-NETCFG-001..004)——
|
||||
* TC-NETCFG-001 未 load 时 ip = 工厂默认 192.168.1.100
|
||||
* TC-NETCFG-002 未 load 时 mask = 255.255.255.0
|
||||
* TC-NETCFG-003 未 load 时 gw = 192.168.1.1
|
||||
* TC-NETCFG-004 未 load 时 mac = 全零(哨兵 = 不覆盖芯片内置 MAC)
|
||||
* —— 注册与 load(TC-NETCFG-005..011)——
|
||||
* TC-NETCFG-005 首次 load 注册 [network] 段恰好一次(且 load 返回 0)
|
||||
* TC-NETCFG-006 首次 load 一并注册 [ftp] 段(N-19:所有受管段先注册、后 load)
|
||||
* TC-NETCFG-007 重复 load 不重复注册(幂等;否则引擎按"段名重复"拒登 → 静默失配)
|
||||
* TC-NETCFG-008 注册描述符内容:段名/尺寸/键数/默认值指针/变更回调/数据指针 = RAM 副本
|
||||
* TC-NETCFG-009 load 成功:返回 0 且留一条 INFO、不误报 ERROR
|
||||
* TC-NETCFG-010 **引擎报 -1 时 net_cfg_load 仍返回 0**(RAM 恒有效,不带崩网络启动)
|
||||
* TC-NETCFG-011 load 失败必须留 ERROR(不许静默回落默认值)
|
||||
* —— RAM 副本读写(TC-NETCFG-012..013)——
|
||||
* TC-NETCFG-012 net_cfg_set 写入被 net_cfg_get 读回
|
||||
* TC-NETCFG-013 net_cfg_set(NULL) 不改写(UI 侧误传不得毁掉配置)
|
||||
* —— 直接保存(TC-NETCFG-014..015)——
|
||||
* TC-NETCFG-014 引擎写盘成功 → net_cfg_save 返回 0
|
||||
* TC-NETCFG-015 引擎写盘失败 → net_cfg_save 返回 -1 并留痕(错误码没被吞成 0)
|
||||
* —— 跨任务保存交接状态机(TC-NETCFG-020..033,UI 保存行显示的就是它)——
|
||||
* TC-NETCFG-020 初始态 = IDLE
|
||||
* TC-NETCFG-021 save_request 后置 WAIT
|
||||
* TC-NETCFG-022 save_request 经已注册回调唤醒属主任务(计数 +1)
|
||||
* TC-NETCFG-023 未注册回调时 save_request 不崩、请求照样挂起
|
||||
* TC-NETCFG-024a 有请求时 save_run 确实写盘一次(024 的前置)
|
||||
* TC-NETCFG-024 无请求时空醒 → 返回 0 且**不写盘**(2s 周期轮询不刷 Flash)
|
||||
* TC-NETCFG-025 save_run 成功后 pending 清零
|
||||
* TC-NETCFG-026 save_run 成功后状态 = OK
|
||||
* TC-NETCFG-027 save_run 成功后触发一次"立即应用"
|
||||
* TC-NETCFG-028 **写盘进行中**状态仍是 WAIT(写盘前先清 pending,靠 busy 撑住;
|
||||
* 否则这段窗口会退回上一轮的 OK/IDLE,用户看不到反馈只会再按一次)
|
||||
* TC-NETCFG-029a **写盘期间投递的新请求不丢**:本轮结束后仍为 WAIT
|
||||
* TC-NETCFG-029b 下一轮真的再写一次(不是只把状态挂在那里)
|
||||
* TC-NETCFG-030 写盘失败 → 状态 = FAIL
|
||||
* TC-NETCFG-031 **写盘失败不得触发"立即应用"**(内存值还没落盘,不能当已生效)
|
||||
* TC-NETCFG-032 未注册应用回调时 request_apply 是空操作(模拟器就是这条路)
|
||||
* TC-NETCFG-033a 应用回调注册后可被触发
|
||||
* TC-NETCFG-033b 传 NULL 注销后不再被触发
|
||||
* —— MAC 三态优先级(TC-NETCFG-040..045)——
|
||||
* TC-NETCFG-040 存储 MAC 非零 → 用存储值(即使芯片已回写影子)
|
||||
* TC-NETCFG-041 存储全零 + 芯片已回写 → 用影子值
|
||||
* TC-NETCFG-042 存储全零 + 未回写 → 全零
|
||||
* TC-NETCFG-043 set_active_mac(NULL) 不得把影子置为有效
|
||||
* TC-NETCFG-044 set_active_mac 正常回写后影子生效
|
||||
* TC-NETCFG-045 get_mac(NULL) 安全返回
|
||||
* —— IPv4 定长格式化(TC-NETCFG-050..056,UI 光标定位依赖)——
|
||||
* TC-NETCFG-050 常规地址渲染为定长 15 字符 "192.168.001.100"
|
||||
* TC-NETCFG-051 全零地址补零为 "000.000.000.000"(仍是 15 字符,偏移不变)
|
||||
* TC-NETCFG-052 255 边界 "255.255.255.255"
|
||||
* TC-NETCFG-053 p_ip 为 NULL:不写缓冲
|
||||
* TC-NETCFG-054 p_buf 为 NULL:不崩
|
||||
* TC-NETCFG-055 len 为 0:不写缓冲
|
||||
* TC-NETCFG-056 len 不足:截断但**必须 NUL 结尾**且不越界(UI 缓冲防线)
|
||||
*
|
||||
* 用例顺序有依赖(模块静态状态无法整体复位):出厂默认组必须最先跑(任何 load 之前);
|
||||
* IDLE 组在任何 save_request 之前;需要干净初态处显式复位白盒量并注明原因。
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* 真实引擎头(接口原汁)+ 被测生产源(原样纳入);dbg_log.h 命中本目录 stub */
|
||||
#include "cfg_file.h"
|
||||
#include "net_cfg.h"
|
||||
|
||||
#include "net_cfg.c"
|
||||
|
||||
/* ===================== 断言框架 ===================== */
|
||||
|
||||
static int s_total;
|
||||
static int s_passed;
|
||||
static int s_failed;
|
||||
|
||||
#define CHECK(cond, name) do { \
|
||||
s_total++; \
|
||||
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
|
||||
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
/* ===================== dbg_log 桩的计数定义 ===================== */
|
||||
|
||||
int g_netcfg_info_count;
|
||||
int g_netcfg_error_count;
|
||||
int g_netcfg_debug_count;
|
||||
|
||||
static void logs_clear(void)
|
||||
{
|
||||
g_netcfg_info_count = 0;
|
||||
g_netcfg_error_count = 0;
|
||||
g_netcfg_debug_count = 0;
|
||||
}
|
||||
|
||||
/* ===================== 假引擎(可控实现;原型来自真实 cfg_file.h) ========== */
|
||||
|
||||
/* 注册描述符在 net_cfg.c 里是栈上临时量,必须**按值留档**后才可比对 */
|
||||
typedef struct {
|
||||
char name[CFG_FILE_MAX_NAME_LEN + 1u];
|
||||
const void *p_data;
|
||||
const void *p_defaults;
|
||||
size_t data_size;
|
||||
uint8_t key_count;
|
||||
void (*p_cb)(void);
|
||||
} fake_reg_t;
|
||||
|
||||
static fake_reg_t g_regs[CFG_FILE_MAX_SECTIONS];
|
||||
static int g_reg_n;
|
||||
static int g_ftp_reg_calls; /* ftp_cfg_register 被 net_cfg_load 调了几次 */
|
||||
static int g_load_calls;
|
||||
static int g_save_calls;
|
||||
static int g_apply_calls;
|
||||
static int g_req_cb_calls;
|
||||
static int g_load_rc; /* 注入:cfg_file_load 返回值 */
|
||||
static int g_save_rc; /* 注入:cfg_file_save 返回值 */
|
||||
static int g_state_during_save = -1;/* 写盘钩子里读到的交接状态 */
|
||||
static int g_requeue_during_save; /* 1 = 写盘中再投递一次请求 */
|
||||
static void (*g_save_hook)(void); /* 写盘时刻回调(构造真机难复现的时序) */
|
||||
|
||||
static void fake_copy_name(char *dst, const char *src, size_t cap)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0u; (i + 1u < cap) && (src[i] != '\0'); i++) {
|
||||
dst[i] = src[i];
|
||||
}
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
int cfg_file_register(const cfg_section_reg_t *p_reg)
|
||||
{
|
||||
fake_reg_t *rec;
|
||||
|
||||
if ((p_reg == NULL) || (p_reg->name == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
if ((size_t)g_reg_n >= (size_t)CFG_FILE_MAX_SECTIONS) {
|
||||
return -1;
|
||||
}
|
||||
rec = &g_regs[g_reg_n];
|
||||
fake_copy_name(rec->name, p_reg->name, sizeof(rec->name));
|
||||
rec->p_data = p_reg->p_data;
|
||||
rec->p_defaults = p_reg->p_defaults;
|
||||
rec->data_size = p_reg->data_size;
|
||||
rec->key_count = p_reg->key_count;
|
||||
rec->p_cb = p_reg->p_cb;
|
||||
g_reg_n++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cfg_file_load(void)
|
||||
{
|
||||
g_load_calls++;
|
||||
return g_load_rc;
|
||||
}
|
||||
|
||||
int cfg_file_save(void)
|
||||
{
|
||||
g_save_calls++;
|
||||
if (g_save_hook != NULL) {
|
||||
g_save_hook();
|
||||
}
|
||||
return g_save_rc;
|
||||
}
|
||||
|
||||
/* [ftp] 段归 test/host/ftp_cfg/ 模块测;这里只验证"net_cfg_load 确实带上了它" */
|
||||
void ftp_cfg_register(void)
|
||||
{
|
||||
g_ftp_reg_calls++;
|
||||
}
|
||||
|
||||
static void fake_reset(void)
|
||||
{
|
||||
g_reg_n = 0;
|
||||
g_ftp_reg_calls = 0;
|
||||
g_load_calls = 0;
|
||||
g_save_calls = 0;
|
||||
g_apply_calls = 0;
|
||||
g_req_cb_calls = 0;
|
||||
g_load_rc = 0;
|
||||
g_save_rc = 0;
|
||||
g_state_during_save = -1;
|
||||
g_requeue_during_save = 0;
|
||||
g_save_hook = NULL;
|
||||
logs_clear();
|
||||
}
|
||||
|
||||
static int mac_eq(const uint8_t *p_a, const uint8_t *p_b)
|
||||
{
|
||||
return memcmp(p_a, p_b, 6u) == 0;
|
||||
}
|
||||
|
||||
static int bytes_eq(const uint8_t *p_a, const uint8_t *p_b, size_t n)
|
||||
{
|
||||
return memcmp(p_a, p_b, n) == 0;
|
||||
}
|
||||
|
||||
/* ===================== 回调 ===================== */
|
||||
|
||||
static void apply_cb(void)
|
||||
{
|
||||
g_apply_calls++;
|
||||
}
|
||||
|
||||
static void req_cb(void)
|
||||
{
|
||||
g_req_cb_calls++;
|
||||
}
|
||||
|
||||
/* 写盘时刻钩子:记录当时的交接状态,并按开关模拟"写盘期间 UI 又按了一次 ENTER" */
|
||||
static void save_hook_probe(void)
|
||||
{
|
||||
g_state_during_save = net_cfg_save_state();
|
||||
if (g_requeue_during_save != 0) {
|
||||
(void)net_cfg_save_request();
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== 出厂默认(必须在任何 load 之前跑) ===================== */
|
||||
|
||||
static void tc_defaults(void)
|
||||
{
|
||||
const net_cfg_t *p = net_cfg_get();
|
||||
static const uint8_t def_ip[4] = { 192u, 168u, 1u, 100u };
|
||||
static const uint8_t def_mask[4] = { 255u, 255u, 255u, 0u };
|
||||
static const uint8_t def_gw[4] = { 192u, 168u, 1u, 1u };
|
||||
static const uint8_t zero[6] = { 0u, 0u, 0u, 0u, 0u, 0u };
|
||||
|
||||
printf("-- TC-NETCFG-001..004 defaults before load --\n");
|
||||
CHECK(bytes_eq(p->ip, def_ip, 4u), "TC-NETCFG-001 ip = 192.168.1.100");
|
||||
CHECK(bytes_eq(p->mask, def_mask, 4u), "TC-NETCFG-002 mask = 255.255.255.0");
|
||||
CHECK(bytes_eq(p->gw, def_gw, 4u), "TC-NETCFG-003 gw = 192.168.1.1");
|
||||
CHECK(mac_eq(p->mac, zero), "TC-NETCFG-004 mac = 全零(芯片内置哨兵)");
|
||||
}
|
||||
|
||||
/* ===================== 注册与 load 契约 ===================== */
|
||||
|
||||
static void tc_register_load(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
printf("-- TC-NETCFG-005..011 register + load --\n");
|
||||
fake_reset();
|
||||
rc = net_cfg_load();
|
||||
CHECK((rc == 0) && (g_reg_n == 1) && (strcmp(g_regs[0].name, "network") == 0),
|
||||
"TC-NETCFG-005 首次 load 注册 [network] 恰好一次");
|
||||
CHECK((g_ftp_reg_calls == 1) && (g_load_calls == 1),
|
||||
"TC-NETCFG-006 一并注册 [ftp],且注册在 load 之前");
|
||||
|
||||
rc = net_cfg_load();
|
||||
CHECK((rc == 0) && (g_reg_n == 1) && (g_ftp_reg_calls == 2),
|
||||
"TC-NETCFG-007 重复 load 不重复注册 [network]");
|
||||
|
||||
CHECK((strcmp(g_regs[0].name, "network") == 0) &&
|
||||
(g_regs[0].data_size == sizeof(net_cfg_t)) &&
|
||||
(g_regs[0].key_count == 4u) &&
|
||||
(g_regs[0].p_defaults != NULL) &&
|
||||
(g_regs[0].p_cb != NULL) &&
|
||||
(g_regs[0].p_data == (const void *)&s_cfg),
|
||||
"TC-NETCFG-008 注册内容:尺寸/键数/默认值/回调/数据指针=RAM 副本");
|
||||
|
||||
fake_reset();
|
||||
rc = net_cfg_load();
|
||||
CHECK((rc == 0) && (g_netcfg_info_count == 1) && (g_netcfg_error_count == 0),
|
||||
"TC-NETCFG-009 load 成功:返回 0 + 一条 INFO");
|
||||
|
||||
fake_reset();
|
||||
g_load_rc = -1;
|
||||
rc = net_cfg_load();
|
||||
CHECK(rc == 0, "TC-NETCFG-010 引擎 -1 时仍返回 0(RAM 恒有效契约)");
|
||||
CHECK(g_netcfg_error_count == 1, "TC-NETCFG-011 load 失败必须留 ERROR");
|
||||
}
|
||||
|
||||
/* ===================== RAM 副本读写 ===================== */
|
||||
|
||||
static void tc_set_get(void)
|
||||
{
|
||||
net_cfg_t c;
|
||||
static const uint8_t ip[4] = { 10u, 0u, 0u, 7u };
|
||||
static const uint8_t mac[6] = { 0x02u, 0x11u, 0x22u, 0x33u, 0x44u, 0x55u };
|
||||
|
||||
printf("-- TC-NETCFG-012..013 set/get --\n");
|
||||
c = *net_cfg_get();
|
||||
memcpy(c.ip, ip, 4u);
|
||||
memcpy(c.mac, mac, 6u);
|
||||
net_cfg_set(&c);
|
||||
CHECK(bytes_eq(net_cfg_get()->ip, ip, 4u) &&
|
||||
mac_eq(net_cfg_get()->mac, mac), "TC-NETCFG-012 set 被 get 读回");
|
||||
|
||||
net_cfg_set(NULL);
|
||||
CHECK(bytes_eq(net_cfg_get()->ip, ip, 4u), "TC-NETCFG-013 set(NULL) 不改写");
|
||||
}
|
||||
|
||||
/* ===================== 直接保存 ===================== */
|
||||
|
||||
static void tc_save_direct(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
printf("-- TC-NETCFG-014..015 save --\n");
|
||||
fake_reset();
|
||||
rc = net_cfg_save();
|
||||
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-014 写盘成功返回 0");
|
||||
|
||||
fake_reset();
|
||||
g_save_rc = -1;
|
||||
rc = net_cfg_save();
|
||||
CHECK((rc == -1) && (g_netcfg_error_count == 1),
|
||||
"TC-NETCFG-015 写盘失败返回 -1 并留痕");
|
||||
}
|
||||
|
||||
/* ===================== 跨任务保存交接状态机 ===================== */
|
||||
|
||||
static void tc_save_handoff(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
printf("-- TC-NETCFG-020..033 save handoff state machine --\n");
|
||||
fake_reset();
|
||||
net_cfg_set_apply_cb(NULL);
|
||||
net_cfg_set_save_req_cb(NULL);
|
||||
s_save_pending = 0u; /* 白盒复位:上一组的 save() 不碰 pending/result,此处 */
|
||||
s_save_result = 0u; /* 只是防止将来有人调整后漏出脏态 */
|
||||
CHECK(net_cfg_save_state() == NET_CFG_SAVE_ST_IDLE, "TC-NETCFG-020 初始 IDLE");
|
||||
|
||||
CHECK((net_cfg_save_request() == 0) &&
|
||||
(net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT), "TC-NETCFG-021 投递后 WAIT");
|
||||
|
||||
net_cfg_set_save_req_cb(req_cb);
|
||||
g_req_cb_calls = 0;
|
||||
(void)net_cfg_save_request();
|
||||
CHECK(g_req_cb_calls == 1, "TC-NETCFG-022 投递经回调唤醒属主任务");
|
||||
|
||||
net_cfg_set_save_req_cb(NULL);
|
||||
(void)net_cfg_save_request();
|
||||
CHECK((g_req_cb_calls == 1) &&
|
||||
(net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT),
|
||||
"TC-NETCFG-023 未注册回调时空操作,请求照样挂起");
|
||||
|
||||
g_save_rc = 0;
|
||||
g_save_calls = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-024a 有请求时确实写盘一次");
|
||||
g_save_calls = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (g_save_calls == 0),
|
||||
"TC-NETCFG-024 无请求时空醒不写盘(2s 轮询不刷 Flash)");
|
||||
|
||||
(void)net_cfg_save_request();
|
||||
net_cfg_set_apply_cb(apply_cb);
|
||||
g_apply_calls = 0;
|
||||
g_save_rc = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (s_save_pending == 0u), "TC-NETCFG-025 成功后 pending 清零");
|
||||
CHECK((net_cfg_save_state() == NET_CFG_SAVE_ST_OK) && (s_save_busy == 0u),
|
||||
"TC-NETCFG-026 成功后状态 OK 且写盘标志已清");
|
||||
CHECK(g_apply_calls == 1, "TC-NETCFG-027 成功后触发一次立即应用");
|
||||
|
||||
(void)net_cfg_save_request();
|
||||
g_save_hook = save_hook_probe;
|
||||
g_state_during_save = -1;
|
||||
g_apply_calls = 0;
|
||||
g_save_rc = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (g_state_during_save == (int)NET_CFG_SAVE_ST_WAIT),
|
||||
"TC-NETCFG-028 写盘进行中状态仍是 WAIT");
|
||||
|
||||
/* 写盘期间再投递一次:本轮不得把它当已处理,下轮必须真的再写 */
|
||||
g_requeue_during_save = 1;
|
||||
(void)net_cfg_save_request();
|
||||
g_save_calls = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (net_cfg_save_state() == NET_CFG_SAVE_ST_WAIT),
|
||||
"TC-NETCFG-029a 写盘期间的新请求没丢");
|
||||
g_requeue_during_save = 0;
|
||||
g_save_hook = NULL;
|
||||
g_save_calls = 0;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == 0) && (g_save_calls == 1), "TC-NETCFG-029b 下一轮真的再写一次");
|
||||
|
||||
/* 写盘失败:置 FAIL 且**不得**触发应用(内存值没落盘,网络不能重建) */
|
||||
s_save_pending = 0u;
|
||||
s_save_result = 0u;
|
||||
g_apply_calls = 0;
|
||||
(void)net_cfg_save_request();
|
||||
g_save_rc = -1;
|
||||
rc = net_cfg_save_run();
|
||||
CHECK((rc == -1) && (net_cfg_save_state() == NET_CFG_SAVE_ST_FAIL) &&
|
||||
(s_save_busy == 0u),
|
||||
"TC-NETCFG-030 失败置 FAIL 且写盘标志已清(否则 UI 永远卡 WAIT)");
|
||||
CHECK(g_apply_calls == 0, "TC-NETCFG-031 写盘失败不得触发立即应用");
|
||||
|
||||
net_cfg_set_apply_cb(NULL);
|
||||
g_apply_calls = 0;
|
||||
net_cfg_request_apply();
|
||||
CHECK(g_apply_calls == 0, "TC-NETCFG-032 未注册应用回调时是空操作");
|
||||
|
||||
net_cfg_set_apply_cb(apply_cb);
|
||||
net_cfg_request_apply();
|
||||
CHECK(g_apply_calls == 1, "TC-NETCFG-033a 注册后可被触发");
|
||||
net_cfg_set_apply_cb(NULL);
|
||||
net_cfg_request_apply();
|
||||
CHECK(g_apply_calls == 1, "TC-NETCFG-033b 注销后不再被触发");
|
||||
}
|
||||
|
||||
/* ===================== MAC 三态优先级 ===================== */
|
||||
|
||||
static void tc_mac_priority(void)
|
||||
{
|
||||
net_cfg_t c;
|
||||
uint8_t out[6];
|
||||
static const uint8_t stored[6] = { 0xAAu, 0xBBu, 0xCCu, 0x11u, 0x22u, 0x33u };
|
||||
static const uint8_t shadow[6] = { 0x02u, 0x00u, 0x00u, 0x01u, 0x02u, 0x03u };
|
||||
static const uint8_t zero[6] = { 0u, 0u, 0u, 0u, 0u, 0u };
|
||||
|
||||
printf("-- TC-NETCFG-040..045 mac display priority --\n");
|
||||
|
||||
c = *net_cfg_get();
|
||||
memcpy(c.mac, stored, 6u);
|
||||
net_cfg_set(&c);
|
||||
net_cfg_set_active_mac(shadow);
|
||||
memset(out, 0xFF, sizeof(out));
|
||||
net_cfg_get_mac(out);
|
||||
CHECK(mac_eq(out, stored), "TC-NETCFG-040 存储非零优先于芯片影子");
|
||||
|
||||
c = *net_cfg_get();
|
||||
memcpy(c.mac, zero, 6u);
|
||||
net_cfg_set(&c);
|
||||
memset(out, 0xFF, sizeof(out));
|
||||
net_cfg_get_mac(out);
|
||||
CHECK(mac_eq(out, shadow), "TC-NETCFG-041 存储全零则用芯片回写影子");
|
||||
|
||||
s_active_valid = 0u; /* 白盒:回到"芯片还没回写"的上电初态 */
|
||||
memset(out, 0xFF, sizeof(out));
|
||||
net_cfg_get_mac(out);
|
||||
CHECK(mac_eq(out, zero), "TC-NETCFG-042 两者皆无 → 全零");
|
||||
|
||||
net_cfg_set_active_mac(NULL);
|
||||
CHECK(s_active_valid == 0u, "TC-NETCFG-043 NULL 回写不得把影子置有效");
|
||||
|
||||
net_cfg_set_active_mac(shadow);
|
||||
memset(out, 0xFF, sizeof(out));
|
||||
net_cfg_get_mac(out);
|
||||
CHECK(mac_eq(out, shadow), "TC-NETCFG-044 正常回写后影子生效");
|
||||
|
||||
net_cfg_get_mac(NULL);
|
||||
CHECK(1, "TC-NETCFG-045 get_mac(NULL) 安全返回");
|
||||
}
|
||||
|
||||
/* ===================== IPv4 定长格式化 ===================== */
|
||||
|
||||
static void tc_fmt_ipv4(void)
|
||||
{
|
||||
static const uint8_t ip1[4] = { 192u, 168u, 1u, 100u };
|
||||
static const uint8_t ip0[4] = { 0u, 0u, 0u, 0u };
|
||||
static const uint8_t ipm[4] = { 255u, 255u, 255u, 255u };
|
||||
char buf[24];
|
||||
char guard[24];
|
||||
|
||||
printf("-- TC-NETCFG-050..056 fixed-width ipv4 format --\n");
|
||||
|
||||
memset(buf, 0x7A, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(ip1, buf, sizeof(buf));
|
||||
/* 定长 = 4 段 × 3 位 + 3 个点 = 15(曾有误记为 11 的注释,已同日更正)
|
||||
* 钉死这个长度是 UI 光标偏移(段 i 起点 4i、点号 3/7/11)的前提 */
|
||||
CHECK((strcmp(buf, "192.168.001.100") == 0) && (strlen(buf) == 15u),
|
||||
"TC-NETCFG-050 定长 15 字符且每段补零");
|
||||
|
||||
memset(buf, 0x7A, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(ip0, buf, sizeof(buf));
|
||||
CHECK(strcmp(buf, "000.000.000.000") == 0, "TC-NETCFG-051 全零也占满 15 字符");
|
||||
|
||||
memset(buf, 0x7A, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(ipm, buf, sizeof(buf));
|
||||
CHECK(strcmp(buf, "255.255.255.255") == 0, "TC-NETCFG-052 255 边界");
|
||||
|
||||
memset(guard, 0x7A, sizeof(guard));
|
||||
memcpy(buf, guard, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(NULL, buf, sizeof(buf));
|
||||
CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-053 p_ip NULL 不写缓冲");
|
||||
|
||||
net_cfg_fmt_ipv4(ip1, NULL, 16u);
|
||||
CHECK(1, "TC-NETCFG-054 p_buf NULL 不崩");
|
||||
|
||||
memset(guard, 0x7A, sizeof(guard));
|
||||
memcpy(buf, guard, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(ip1, buf, 0u);
|
||||
CHECK(memcmp(buf, guard, sizeof(buf)) == 0, "TC-NETCFG-055 len 0 不写缓冲");
|
||||
|
||||
memset(buf, 0x7A, sizeof(buf));
|
||||
net_cfg_fmt_ipv4(ip1, buf, 5u);
|
||||
CHECK((strlen(buf) == 4u) && (buf[4] == '\0') && (buf[5] == (char)0x7A),
|
||||
"TC-NETCFG-056 len 不足:截断但必须 NUL 结尾且不越界");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== NET Config Tests ===\n");
|
||||
|
||||
tc_defaults(); /* 必须最先:任何 load 之前 */
|
||||
tc_register_load();
|
||||
tc_set_get();
|
||||
tc_save_direct();
|
||||
tc_save_handoff();
|
||||
tc_mac_priority();
|
||||
tc_fmt_ipv4();
|
||||
|
||||
printf("\n=== NET Config Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""网络配置(net_cfg)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/net_cfg/net_cfg_test.c(它以 #include 纳入
|
||||
**生产源文件** Drivers/BSP/NETCFG/net_cfg.c),在 PC 上离线跑出厂默认、
|
||||
注册幂等、load/save 错误通道、跨任务保存交接状态机、MAC 三态优先级、
|
||||
IPv4 定长格式化,失败即非 0 退出。零硬件、零烧录、毫秒级。
|
||||
|
||||
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
|
||||
`test/host/common/unit_test_harness.py`(本文件只做声明)。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
保存链跨三个任务(uiTask 投递 → defaultTask 写盘 → netTask 重建网络):
|
||||
"写盘期间又投递一次"与"写盘失败却重建网络"这两条真机上要靠手速 + 故障注入
|
||||
才碰得到,错了就是"改 IP 不生效 / 保存失败网络被无端重建";MAC 三态优先级
|
||||
写错会静默用错 MAC(同网段冲突,只表现为偶发丢包);net_cfg_fmt_ipv4 的定长
|
||||
11 字符是 UI 光标按固定偏移定位的前提。三条都在 PC 上钉死,且不占 MCU RAM。
|
||||
|
||||
桩的边界(细节见测试单元文件头):
|
||||
配置文件引擎用**真实头**(接口原汁)+ 测试单元内的可控实现;[ftp] 段的
|
||||
ftp_cfg_register 只计数——它归 test/host/ftp_cfg/ 模块,两边不互相顶包。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/net_cfg/run_unit_test.py
|
||||
或统一用 .venv: & '.venv\Scripts\python.exe' test/host/net_cfg/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
|
||||
|
||||
设计依据:docs/子系统设计/网络配置说明.md、docs/测试说明/主机单测覆盖率口径.md §9 P1
|
||||
创建日期:2026-10-23
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
# harness 位于 test/host/common/
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 顺序敏感:stub 必须**先于**生产目录,才能覆盖 dbg_log.h。cfg_file.h 用真实头
|
||||
# (接口契约原汁,引擎改签名即编译期报错),net_cfg.h / ftp_cfg.h 亦用真实头。
|
||||
BUILD = TestBuild(
|
||||
name="NET Config Tests",
|
||||
src=os.path.join(HERE, "net_cfg_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "Drivers", "BSP", "NETCFG"),
|
||||
os.path.join(ROOT, "App", "cfg"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== NET Config Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - dbg_log.h(net_cfg 模块专用)
|
||||
* 模块功能:把 MCU 版 DBG_* 宏替换为**计数**,让"该留痕的有没有留痕"变成可断言
|
||||
* 的行为;同时不往 stdout 打日志——汇总行是 harness 的判据,必须干净。
|
||||
*
|
||||
* ⚠ 仅供 test/host/net_cfg/ 使用,不参与 MCU 构建。
|
||||
*
|
||||
* 为什么是计数而不是 printf 直通:net_cfg.c 的三条日志恰好对应三条契约
|
||||
* (load OK / load FAIL / save OK|FAIL),计数能钉住"哪条该响、哪条不该响"
|
||||
* ——例如"写盘失败必须留痕、但不得触发应用"这类只能靠计数判的分支。
|
||||
* 计数器定义在 net_cfg_test.c,每组用例前清零。
|
||||
*
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#ifndef DBG_LOG_H
|
||||
#define DBG_LOG_H
|
||||
|
||||
extern int g_netcfg_info_count;
|
||||
extern int g_netcfg_error_count;
|
||||
extern int g_netcfg_debug_count;
|
||||
|
||||
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_netcfg_info_count++; } while (0)
|
||||
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_netcfg_debug_count++; } while (0)
|
||||
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_netcfg_error_count++; } while (0)
|
||||
|
||||
#endif /* DBG_LOG_H */
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* 模块名称:保护定值持久化胶水层(prot_set_store)主机侧单元测试
|
||||
* 模块功能:在 PC 上离线验证 Drivers/... 侧的平台胶水 App/cfg/prot_set_store.c 的
|
||||
* **返回码分流**与**脏标志回置**:读不到=1(按无档处理)、装载成功=0、
|
||||
* 版本/项数换代=2 且只打 INFO、magic 或校验被破坏=-1;保存无脏不写盘、
|
||||
* 写失败必须把脏标志还回去(否则一轮卷忙就永久丢定值)。
|
||||
*
|
||||
* 被测对象:真实源文件 ../../../App/cfg/prot_set_store.c,并且**同时纳入真实的**
|
||||
* ../../../App/cfg/prot_set.c —— 因为 blob 的 serialize/parse/脏标志都在
|
||||
* prot_set.c 里(纯逻辑,已另有 test/host/prot_set/ 覆盖其算法)。本模块
|
||||
* 不重测算法,只测"胶水层在真实 blob 上把返回码分对没有":只有拿真
|
||||
* serialize 产出的 blob 去改 magic/ver/cnt/校验,才能构造出"换代"与
|
||||
* "损坏"这两种真实形态;自己手搓 blob 会把测试写成对自己的桩。
|
||||
* 平台后端 cfg_store_read / cfg_store_write_atomic 用**真实头**
|
||||
* App/cfg/cfg_file_store.h(接口原汁)+ 本文件的可控实现。
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只靠台架):
|
||||
* 1. **换代与损坏必须分开**(B8 定值表 32→40 项时定下的契约):magic 完好但
|
||||
* ver/cnt 与当前固件不符是**预期的升级路径**,归 2 并打 INFO;把它当损坏(-1)
|
||||
* 会让设备在正常升级后报 ERROR + 自检留痕,现场查成一个根本不存在的故障。
|
||||
* 2. 读失败与"文件不存在"同归 1 是**安全侧**选择(启动期卷锁本任务独占,
|
||||
* try-lock 失败即真 I/O 故障,此时保持出厂默认比拿半截数据更安全)——钉住它,
|
||||
* 防止将来有人"优化"成把读失败也当损坏重刷。
|
||||
* 3. 写失败必须回置脏:定值经 iec104Task 下发,落盘在 defaultTask;一轮卷忙就把
|
||||
* 脏标志吞掉,表现为"改了定值、重启又变回旧的",而且没有任何错误提示。
|
||||
*
|
||||
* 覆盖点:
|
||||
* —— load 返回码分流(TC-PROTSTOR-001..008)——
|
||||
* TC-PROTSTOR-001 后端读失败 → 1(按"无档"处理,保持出厂默认)
|
||||
* TC-PROTSTOR-002 有效 blob → 0,且装载后定值生效
|
||||
* TC-PROTSTOR-003 **ver 换代 → 2**(不是 -1)
|
||||
* TC-PROTSTOR-004 ver 换代只打 **INFO**(不打 ERROR、不进自检留痕)
|
||||
* TC-PROTSTOR-005 **cnt 换代 → 2**(B8 32→40 项就是这个形态)
|
||||
* TC-PROTSTOR-006 magic 被破坏 → -1(真损坏)
|
||||
* TC-PROTSTOR-007 magic/ver/cnt 都好但数据被改(校验不过)→ -1
|
||||
* TC-PROTSTOR-008 长度不足头部 → -1,且**不得**误判为换代
|
||||
* TC-PROTSTOR-009 装载成功路径不打 INFO("version change"措辞不许乱出现)
|
||||
* —— 后端调用契约(TC-PROTSTOR-010..011)——
|
||||
* TC-PROTSTOR-010 读取用的路径 = "/prot_set.bin"(8.3 合规,FF_USE_LFN=0 约束)
|
||||
* TC-PROTSTOR-011 读取请求的容量 = PROT_SET_BLOB_SZ(blob 缓冲是静态区,不压栈)
|
||||
* —— save 脏标志与写失败重试(TC-PROTSTOR-012..016)——
|
||||
* TC-PROTSTOR-012 无脏标志 → 返回 0 且**一次都不写盘**
|
||||
* TC-PROTSTOR-013 有脏 + 写成功 → 返回 0,路径与长度正确
|
||||
* TC-PROTSTOR-014 写成功后脏被消费:紧接着再调不再写(不重复刷 Flash)
|
||||
* TC-PROTSTOR-015 写失败 → 返回 -1 且**脏回置**
|
||||
* TC-PROTSTOR-016 上一轮写失败后,下一轮仍会真的再写一次(定值不会静默丢失)
|
||||
*
|
||||
* 说明:serialize 失败这条分支在真实 prot_set.c 下不可达(缓冲尺寸恒等于
|
||||
* PROT_SET_BLOB_SZ),故不硬造用例;胶水层里它是防御性代码,记录在此。
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg_file_store.h"
|
||||
#include "prot_set.h"
|
||||
#include "prot_set_store.h"
|
||||
|
||||
/* 真实定值层(blob 编解码与脏标志在这里)+ 被测胶水层,均原样纳入 */
|
||||
#include "prot_set.c"
|
||||
#include "prot_set_store.c"
|
||||
|
||||
/* ===================== 断言框架 ===================== */
|
||||
|
||||
static int s_total;
|
||||
static int s_passed;
|
||||
static int s_failed;
|
||||
|
||||
#define CHECK(cond, name) do { \
|
||||
s_total++; \
|
||||
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
|
||||
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
/* ===================== dbg_log 桩计数 ===================== */
|
||||
|
||||
int g_pstore_info_count;
|
||||
int g_pstore_error_count;
|
||||
int g_pstore_debug_count;
|
||||
|
||||
static void logs_clear(void)
|
||||
{
|
||||
g_pstore_info_count = 0;
|
||||
g_pstore_error_count = 0;
|
||||
g_pstore_debug_count = 0;
|
||||
}
|
||||
|
||||
/* ===================== 假平台后端(原型来自真实 cfg_file_store.h) ========= */
|
||||
|
||||
static uint8_t g_img[512]; /* 注入给 cfg_store_read 的文件内容 */
|
||||
static uint32_t g_img_len;
|
||||
static int g_read_rc; /* 注入:cfg_store_read 返回值 */
|
||||
static int g_write_rc; /* 注入:cfg_store_write_atomic 返回值 */
|
||||
static int g_read_calls;
|
||||
static int g_write_calls;
|
||||
static size_t g_read_cap;
|
||||
static size_t g_write_len;
|
||||
static char g_read_path[32];
|
||||
static char g_write_path[32];
|
||||
|
||||
static void rec_path(char *dst, const char *src, size_t cap)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (src == NULL) {
|
||||
dst[0] = '\0';
|
||||
return;
|
||||
}
|
||||
for (i = 0u; (i + 1u < cap) && (src[i] != '\0'); i++) {
|
||||
dst[i] = src[i];
|
||||
}
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread)
|
||||
{
|
||||
uint32_t i;
|
||||
|
||||
g_read_calls++;
|
||||
rec_path(g_read_path, p_path, sizeof(g_read_path));
|
||||
g_read_cap = cap;
|
||||
if (p_nread != NULL) {
|
||||
*p_nread = 0u;
|
||||
}
|
||||
if (g_read_rc != 0) {
|
||||
return g_read_rc;
|
||||
}
|
||||
for (i = 0u; (i < g_img_len) && ((size_t)i < cap); i++) {
|
||||
p_buf[i] = g_img[i];
|
||||
}
|
||||
if (p_nread != NULL) {
|
||||
*p_nread = (size_t)i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len)
|
||||
{
|
||||
g_write_calls++;
|
||||
rec_path(g_write_path, p_path, sizeof(g_write_path));
|
||||
g_write_len = len;
|
||||
(void)p_buf;
|
||||
return g_write_rc;
|
||||
}
|
||||
|
||||
/* 其余后端函数本模块不触发,给出空实现以满足链接 */
|
||||
void *cfg_store_alloc(size_t size) { (void)size; return NULL; }
|
||||
void cfg_store_free(void *p_buf) { (void)p_buf; }
|
||||
uint32_t cfg_store_heap_free(void) { return 0u; }
|
||||
int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st)
|
||||
{
|
||||
(void)p_path;
|
||||
if (p_st != NULL) {
|
||||
(void)memset(p_st, 0, sizeof(*p_st));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
int cfg_store_remove(const char *p_path) { (void)p_path; return -1; }
|
||||
|
||||
/* ===================== 辅助 ===================== */
|
||||
|
||||
static void backend_reset(void)
|
||||
{
|
||||
g_img_len = 0u;
|
||||
g_read_rc = 0;
|
||||
g_write_rc = 0;
|
||||
g_read_calls = 0;
|
||||
g_write_calls = 0;
|
||||
g_read_cap = 0u;
|
||||
g_write_len = 0u;
|
||||
g_read_path[0] = '\0';
|
||||
g_write_path[0] = '\0';
|
||||
logs_clear();
|
||||
}
|
||||
|
||||
/* 用**真实** serialize 造一份合法 blob,再按需改坏指定字段 */
|
||||
static uint32_t make_blob(uint8_t *p_dst, uint32_t cap)
|
||||
{
|
||||
uint32_t len = 0u;
|
||||
|
||||
prot_set_init();
|
||||
if (prot_set_store_serialize(p_dst, cap, &len) != 0) {
|
||||
return 0u;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
static void stage_blob(uint32_t len)
|
||||
{
|
||||
g_img_len = len;
|
||||
g_read_rc = 0;
|
||||
}
|
||||
|
||||
/* 把脏标志清干净(消费掉所有存量) */
|
||||
static void dirty_clear(void)
|
||||
{
|
||||
while (prot_set_dirty_take() != 0u) {
|
||||
/* 逐个消费直到 0 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== load 返回码分流 ===================== */
|
||||
|
||||
static void tc_load_triage(void)
|
||||
{
|
||||
uint8_t blob[PROT_SET_BLOB_SZ];
|
||||
uint32_t len;
|
||||
int rc;
|
||||
|
||||
printf("-- TC-PROTSTOR-001..009 load triage --\n");
|
||||
|
||||
backend_reset();
|
||||
g_read_rc = -1;
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == 1, "TC-PROTSTOR-001 读失败归 1(按无档处理)");
|
||||
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
memcpy(g_img, blob, len);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == 0, "TC-PROTSTOR-002 有效 blob 装载成功");
|
||||
|
||||
/* ver 换代:magic 完好、ver 与当前固件不同 → 预期升级路径,归 2 */
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
memcpy(g_img, blob, len);
|
||||
g_img[4] = (uint8_t)(PROT_SET_BLOB_VER + 1u);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == 2, "TC-PROTSTOR-003 ver 换代归 2(不是 -1)");
|
||||
CHECK((g_pstore_info_count == 1) && (g_pstore_error_count == 0),
|
||||
"TC-PROTSTOR-004 换代只打 INFO,不误报 ERROR");
|
||||
|
||||
/* cnt 换代:B8 定值表 32→40 项就是这个形态 */
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
memcpy(g_img, blob, len);
|
||||
g_img[6] = (uint8_t)((uint32_t)PS_ID_COUNT + 1u);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == 2, "TC-PROTSTOR-005 cnt 换代归 2");
|
||||
|
||||
/* magic 坏 = 真损坏 */
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
memcpy(g_img, blob, len);
|
||||
g_img[0] = (uint8_t)(g_img[0] ^ 0xFFu);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK((rc == -1) && (g_pstore_error_count == 0), "TC-PROTSTOR-006 magic 坏归 -1");
|
||||
|
||||
/* magic/ver/cnt 全好,但数据被改 → 校验不过 = 损坏 */
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
memcpy(g_img, blob, len);
|
||||
g_img[PROT_SET_BLOB_HDR_SZ] = (uint8_t)(g_img[PROT_SET_BLOB_HDR_SZ] ^ 0x5Au);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == -1, "TC-PROTSTOR-007 数据被改(校验失败)归 -1");
|
||||
|
||||
/* 截断到不足头部:不得误判为换代 */
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
memcpy(g_img, blob, len);
|
||||
stage_blob(PROT_SET_BLOB_HDR_SZ - 1u);
|
||||
rc = prot_set_store_load();
|
||||
CHECK(rc == -1, "TC-PROTSTOR-008 长度不足头部归 -1 且不误判换代");
|
||||
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
memcpy(g_img, blob, len);
|
||||
stage_blob(len);
|
||||
rc = prot_set_store_load();
|
||||
CHECK((rc == 0) && (g_pstore_info_count == 0),
|
||||
"TC-PROTSTOR-009 成功路径不打 INFO(version change 措辞不许乱出现)");
|
||||
}
|
||||
|
||||
/* ===================== 后端调用契约 ===================== */
|
||||
|
||||
static void tc_backend_contract(void)
|
||||
{
|
||||
uint8_t blob[PROT_SET_BLOB_SZ];
|
||||
uint32_t len;
|
||||
|
||||
printf("-- TC-PROTSTOR-010..011 backend contract --\n");
|
||||
backend_reset();
|
||||
len = make_blob(blob, sizeof(blob));
|
||||
memcpy(g_img, blob, len);
|
||||
stage_blob(len);
|
||||
(void)prot_set_store_load();
|
||||
CHECK(strcmp(g_read_path, "/prot_set.bin") == 0,
|
||||
"TC-PROTSTOR-010 路径 = /prot_set.bin(8.3 合规)");
|
||||
CHECK(g_read_cap == (size_t)PROT_SET_BLOB_SZ,
|
||||
"TC-PROTSTOR-011 读取容量 = PROT_SET_BLOB_SZ(静态缓冲,不压栈)");
|
||||
}
|
||||
|
||||
/* ===================== save 与脏标志 ===================== */
|
||||
|
||||
static void tc_save_dirty(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
printf("-- TC-PROTSTOR-012..016 save + dirty retry --\n");
|
||||
|
||||
backend_reset();
|
||||
dirty_clear();
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == 0) && (g_write_calls == 0), "TC-PROTSTOR-012 无脏不写盘");
|
||||
|
||||
prot_set_dirty_mark();
|
||||
g_write_rc = 0;
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == 0) && (g_write_calls == 1) &&
|
||||
(strcmp(g_write_path, "/prot_set.bin") == 0) && (g_write_len > 0u),
|
||||
"TC-PROTSTOR-013 有脏且写成功:路径与长度正确");
|
||||
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == 0) && (g_write_calls == 1),
|
||||
"TC-PROTSTOR-014 脏已消费,紧接着再调不再刷 Flash");
|
||||
|
||||
prot_set_dirty_mark();
|
||||
g_write_rc = -1;
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == -1) && (g_write_calls == 2), "TC-PROTSTOR-015 写失败返回 -1");
|
||||
|
||||
/* 关键:失败必须把脏标志还回去,否则这一轮定值永久丢失 */
|
||||
g_write_rc = 0;
|
||||
g_write_calls = 0;
|
||||
rc = prot_set_store_save();
|
||||
CHECK((rc == 0) && (g_write_calls == 1),
|
||||
"TC-PROTSTOR-016 上一轮写失败后下一轮仍真的再写(脏已回置)");
|
||||
|
||||
dirty_clear();
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== Prot Set Store Tests ===\n");
|
||||
|
||||
tc_load_triage();
|
||||
tc_backend_contract();
|
||||
tc_save_dirty();
|
||||
|
||||
printf("\n=== Prot Set Store Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""保护定值持久化胶水层(prot_set_store)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/prot_set_store/prot_set_store_test.c(同时纳入**生产源**
|
||||
App/cfg/prot_set_store.c 与 App/cfg/prot_set.c),在 PC 上离线验证 blob 装载的
|
||||
返回码分流(1 无档 / 0 成功 / 2 换代 / -1 损坏)与保存的脏标志回置,失败即非 0
|
||||
退出。工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
换代与损坏必须分开(B8:定值表 32→40 项)——把正常的固件升级当损坏,会让设备在
|
||||
升级后报 ERROR + 自检留痕,现场查一个不存在的故障;写盘失败若不把脏标志还回去,
|
||||
表现是"改了定值、重启又变回旧的"且无任何提示。这两条真机上都要靠故障注入才碰得到。
|
||||
为什么连 prot_set.c 一起编:blob 的编解码与脏标志都在它里面,只有拿**真** serialize
|
||||
产出的 blob 去改 magic/ver/cnt/校验,才能造出真实的"换代"与"损坏"形态;手搓 blob
|
||||
等于测自己的桩。定值算法本身归 test/host/prot_set/,本模块不重测。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/prot_set_store/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误。
|
||||
|
||||
设计依据:App/cfg/prot_set_store.h 的返回码表、docs/测试说明/主机单测覆盖率口径.md §9 P1
|
||||
创建日期:2026-10-23
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 顺序敏感:stub 先于生产目录(覆盖 dbg_log.h)。cfg_file_store.h / prot_set.h /
|
||||
# prot_set_store.h 全用真实头,后端两个被调函数由测试单元给可控实现。
|
||||
BUILD = TestBuild(
|
||||
name="Prot Set Store Tests",
|
||||
src=os.path.join(HERE, "prot_set_store_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "App", "cfg"),
|
||||
os.path.join(ROOT, "App", "algo"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== Prot Set Store Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - dbg_log.h(prot_set_store 模块专用)
|
||||
* 模块功能:把 MCU 版 DBG_* 宏替换为计数,让"换代只打 INFO、损坏不打"这类
|
||||
* 留痕差异成为可断言的行为(B8 的分流契约一半就在措辞上)。
|
||||
* ⚠ 仅供 test/host/prot_set_store/ 使用,不参与 MCU 构建。
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#ifndef DBG_LOG_H
|
||||
#define DBG_LOG_H
|
||||
|
||||
extern int g_pstore_info_count;
|
||||
extern int g_pstore_error_count;
|
||||
extern int g_pstore_debug_count;
|
||||
|
||||
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_pstore_info_count++; } while (0)
|
||||
#define DBG_DEBUG(fmt, ...) do { (void)(fmt); g_pstore_debug_count++; } while (0)
|
||||
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_pstore_error_count++; } while (0)
|
||||
|
||||
#endif /* DBG_LOG_H */
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""任务耗时探针(task_prof)的主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/task_prof/task_prof_test.c(以 #include 纳入**生产源**
|
||||
App/util/task_prof.c),在 PC 上离线验证样本有效性闸门、enter/exit 配对、上报节流
|
||||
与两条通道(环形缓冲 / 裸发)、周期→微秒换算、glitch 汇总,失败即非 0 退出。
|
||||
工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只靠真机日志):
|
||||
这个模块自己错了,所有 PROF 行就都是假的:漏了"无效样本闸门",一次 DWT 复位就把
|
||||
max 永久抬到 ~2^32 周期;"先 exit 再 enter"的首轮会把 0 当起点,造出几秒的假超大
|
||||
样本;节流写错要么刷屏挤掉正事、要么停摆现场那一行发不出去;换算依赖
|
||||
SystemCoreClock,改时钟树忘了同步则整表偏移而无人察觉。这些都得能构造时序来测。
|
||||
|
||||
桩的边界:只有 main.h 需要桩(真实 main.h 拉整棵 HAL 头树,PC 上编不动;桩里的字段名
|
||||
与位掩码宏名必须与 CMSIS 一致,改了被测代码即编译失败)。console_log.h 用真实头,
|
||||
两个出口由测试单元实现并留档内容——报告行是给人读的,断言直接盯字符串。
|
||||
|
||||
用法(仓库根目录):
|
||||
python test/host/task_prof/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误。
|
||||
|
||||
设计依据:App/util/task_prof.h、docs/测试说明/主机单测覆盖率口径.md §9 P1
|
||||
创建日期:2026-10-23
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
# 顺序敏感:stub 必须先于生产目录,才能用桩 main.h 覆盖真实 main.h。
|
||||
# console_log.h / task_prof.h 用真实头(App/log、App/util)。
|
||||
BUILD = TestBuild(
|
||||
name="Task Prof Tests",
|
||||
src=os.path.join(HERE, "task_prof_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(HERE, "stub"),
|
||||
os.path.join(ROOT, "App", "util"),
|
||||
os.path.join(ROOT, "App", "log"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== Task Prof Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 模块名称:主机侧单测桩 - main.h(task_prof 模块专用)
|
||||
* 模块功能:提供 task_prof.c 需要的**四个**平台符号,让 DWT 周期计数器变成
|
||||
* 测试可控制的量:
|
||||
* SystemCoreClock 测试可写(换算 us 的分母,真机 = 168 MHz)
|
||||
* HAL_GetTick() 测试可控的毫秒时基(report 节流就看它)
|
||||
* DWT->CTRL/CYCCNT 可写内存:CTRL 的 CYCCNTENA 位要能"读回 1",
|
||||
* 否则 task_prof.c 永远认为 DWT 不可用
|
||||
* CoreDebug->DEMCR 同上(TRCENA 位)
|
||||
*
|
||||
* ⚠ 仅供 test/host/task_prof/ 使用,不参与 MCU 构建。
|
||||
* 真实 main.h 拉整棵 HAL 头树,主机侧无法编译,故这里只声明被测文件实际用到的
|
||||
* 那几个符号——**结构体字段名与位掩码宏名必须与 CMSIS 一致**(DWT_Type 的
|
||||
* CTRL/CYCCNT、DEMCR、DWT_CTRL_CYCCNTENA_Msk、CoreDebug_DEMCR_TRCENA_Msk),
|
||||
* 改了这些名字被测代码会直接编译失败,不会静默放行。
|
||||
*
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#ifndef MAIN_H
|
||||
#define MAIN_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
volatile uint32_t DEMCR;
|
||||
} tp_coredebug_t;
|
||||
|
||||
typedef struct {
|
||||
volatile uint32_t CTRL;
|
||||
volatile uint32_t CYCCNT;
|
||||
} tp_dwt_t;
|
||||
|
||||
extern tp_coredebug_t *const CoreDebug;
|
||||
extern tp_dwt_t *const DWT;
|
||||
|
||||
#define CoreDebug_DEMCR_TRCENA_Msk (1UL << 24)
|
||||
#define DWT_CTRL_CYCCNTENA_Msk (1UL << 0)
|
||||
|
||||
extern uint32_t SystemCoreClock;
|
||||
|
||||
uint32_t HAL_GetTick(void);
|
||||
|
||||
#endif /* MAIN_H */
|
||||
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
* 模块名称:任务耗时探针(task_prof)主机侧单元测试
|
||||
* 模块功能:在 PC 上离线验证 App/util/task_prof.c 的**样本有效性闸门、enter/exit
|
||||
* 配对、上报节流与通道选择、周期→微秒换算、glitch 汇总**。
|
||||
*
|
||||
* 被测对象:真实源文件 ../../../App/util/task_prof.c(#include 原样纳入)。桩:
|
||||
* - stub/main.h:DWT/CoreDebug/SystemCoreClock/HAL_GetTick(本目录,
|
||||
* 真实 main.h 拉整棵 HAL 头树无法在 PC 上编);
|
||||
* - console_log.h 用**真实头**(App/log),两个出口函数
|
||||
* dbg_console_write_line / dbg_console_write_raw 由本文件实现并**留档
|
||||
* 内容**——报告行是要被人读的,所以断言直接盯字符串本身。
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只靠真机日志):
|
||||
* 这个模块是"性能问题的唯一现场证据",它自己错了所有 PROF 行就都是假的:
|
||||
* 1. **无效样本闸门**(cyc==0 或 ≥ SystemCoreClock×10):漏了它,一次 DWT 复位
|
||||
* 就把 max 抬到 ~2^32 周期,之后所有"最坏耗时"永久失真(真机上没人会去翻
|
||||
* gl= 计数发现)。这条只能构造样本测。
|
||||
* 2. **未 enter 就 exit 必须直接返回**:周期测量写成"先 exit 再 enter"时,首轮
|
||||
* 会把 0 当起点,记下一次几秒的虚假超大样本——真机上表现为"偶发一条离谱
|
||||
* PROF 行",查半年。
|
||||
* 3. 节流契约(首次 5 s、之后每 2 s)与**两条通道的选择**(report 走环形缓冲、
|
||||
* report_raw 走裸发):错了要么刷屏挤掉正事,要么停摆现场那一行发不出去。
|
||||
* 4. 周期→微秒换算依赖 SystemCoreClock,改时钟树(比如 168→180 MHz)忘了同步
|
||||
* 就会整表偏移;这条在真机上看不出来(没人对着示波器量 PROF)。
|
||||
*
|
||||
* 覆盖点:
|
||||
* TC-TASKPROF-001 有效样本:count+1 / last=样本 / max=样本
|
||||
* TC-TASKPROF-002 更小样本不改 max(max 只增不减)
|
||||
* TC-TASKPROF-003 cyc=0 只累加 glitch,不动 count/last/max(计数器复位防线)
|
||||
* TC-TASKPROF-004 cyc ≥ SystemCoreClock×10 同样只累加 glitch
|
||||
* TC-TASKPROF-005 id 越界(=COUNT 与 +1)安全返回,不碰任何槽
|
||||
* TC-TASKPROF-006 首次取时基即惰性使能 DWT(TRCENA + CYCCNTENA 读回)
|
||||
* TC-TASKPROF-007 enter 置 in_seg 并记下起点
|
||||
* TC-TASKPROF-008 exit 结算 Δ、清 in_seg、count+1
|
||||
* TC-TASKPROF-009 **未 enter 直接 exit 不记账**(防首轮 0 起点造出假超大样本)
|
||||
* TC-TASKPROF-010 重复 exit 不重复记账
|
||||
* TC-TASKPROF-011 首次 5 s 之前一条都不发
|
||||
* TC-TASKPROF-012 到点后发两行([PROF] + [PROF2]),走环形缓冲通道
|
||||
* TC-TASKPROF-013 之后不足 2 s 不发(节流)
|
||||
* TC-TASKPROF-014 满 2 s 再发两行
|
||||
* TC-TASKPROF-015 report_raw 无视节流,恒发两行且走**裸发**通道
|
||||
* TC-TASKPROF-016 行格式:以 [PROF]/[PROF2] 开头、以 \r\n 结尾
|
||||
* TC-TASKPROF-017 周期→微秒换算按 SystemCoreClock(168 MHz 下 1680 周期 = 10 us)
|
||||
* TC-TASKPROF-018 gl= 是所有槽 glitch 的汇总,不是单槽
|
||||
* TC-TASKPROF-019 段内进行时的 n= 字段非零(此刻仍在段内的可视性)
|
||||
* TC-TASKPROF-020 count 是累计值,上报不清零
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
* 创建日期:2026-10-23
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "console_log.h" /* 真实头:DBG_CON_LINE_MAX + 两个出口 */
|
||||
#include "task_prof.h"
|
||||
|
||||
#include "task_prof.c"
|
||||
|
||||
/* ===================== 断言框架 ===================== */
|
||||
|
||||
static int s_total;
|
||||
static int s_passed;
|
||||
static int s_failed;
|
||||
|
||||
#define CHECK(cond, name) do { \
|
||||
s_total++; \
|
||||
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
|
||||
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
/* ===================== 桩实体 ===================== */
|
||||
|
||||
static tp_coredebug_t g_coredebug;
|
||||
static tp_dwt_t g_dwt;
|
||||
tp_coredebug_t *const CoreDebug = &g_coredebug;
|
||||
tp_dwt_t *const DWT = &g_dwt;
|
||||
|
||||
uint32_t SystemCoreClock = 168000000u; /* 真机口径:168 MHz */
|
||||
|
||||
static uint32_t g_tick; /* HAL_GetTick 的可控值 */
|
||||
|
||||
uint32_t HAL_GetTick(void)
|
||||
{
|
||||
return g_tick;
|
||||
}
|
||||
|
||||
/* 出口留档:两条通道分别计数;**每一行都单独留档**(一次上报会连发
|
||||
* [PROF] 与 [PROF2] 两行,只存"最后一行"就没法断言第一行的字段) */
|
||||
#define CAP_MAX 8
|
||||
static int g_line_calls;
|
||||
static int g_raw_calls;
|
||||
static char g_line[CAP_MAX][DBG_CON_LINE_MAX];
|
||||
|
||||
static void cap(int slot, const char *src, unsigned len)
|
||||
{
|
||||
unsigned i;
|
||||
|
||||
if (slot >= CAP_MAX) {
|
||||
return;
|
||||
}
|
||||
for (i = 0u; (i < len) && (i + 1u < DBG_CON_LINE_MAX); i++) {
|
||||
g_line[slot][i] = src[i];
|
||||
}
|
||||
g_line[slot][i] = '\0';
|
||||
}
|
||||
|
||||
int dbg_console_write_line(const char *line, unsigned len)
|
||||
{
|
||||
cap(g_line_calls + g_raw_calls, line, len);
|
||||
g_line_calls++;
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
void dbg_console_write_raw(const char *buf, unsigned len)
|
||||
{
|
||||
cap(g_line_calls + g_raw_calls, buf, len);
|
||||
g_raw_calls++;
|
||||
}
|
||||
|
||||
void dbg_console_writer_attach(void) { }
|
||||
uint8_t dbg_console_writer_ready(void) { return 1u; }
|
||||
|
||||
/* ===================== 辅助 ===================== */
|
||||
|
||||
static void reset_all(void)
|
||||
{
|
||||
(void)memset(s_prof, 0, sizeof(s_prof));
|
||||
(void)memset(&g_coredebug, 0, sizeof(g_coredebug));
|
||||
(void)memset(&g_dwt, 0, sizeof(g_dwt));
|
||||
s_dwt_ok = 0u;
|
||||
s_prof_reported = 0u;
|
||||
s_prof_last = 0u;
|
||||
g_tick = 0u;
|
||||
g_line_calls = 0;
|
||||
g_raw_calls = 0;
|
||||
(void)memset(g_line, 0, sizeof(g_line));
|
||||
}
|
||||
|
||||
static void set_cyc(uint32_t v)
|
||||
{
|
||||
g_dwt.CYCCNT = v;
|
||||
}
|
||||
|
||||
/* 从指定行的键上取整数值;键不存在或其后不是数字则返回 -1(断言因此会响)。
|
||||
* 手写解析而不用 sscanf:后者在 MSVC 下触发 C4996(弃用告警),门禁输出要干净 */
|
||||
static long field_of(int slot, const char *p_key)
|
||||
{
|
||||
const char *p;
|
||||
long v = 0;
|
||||
int digits = 0;
|
||||
|
||||
if (slot >= CAP_MAX) {
|
||||
return -1L;
|
||||
}
|
||||
p = strstr(g_line[slot], p_key);
|
||||
if (p == NULL) {
|
||||
return -1L;
|
||||
}
|
||||
p += strlen(p_key);
|
||||
while ((*p >= '0') && (*p <= '9')) {
|
||||
v = (v * 10) + (long)(*p - '0');
|
||||
p++;
|
||||
digits++;
|
||||
}
|
||||
return (digits > 0) ? v : -1L;
|
||||
}
|
||||
|
||||
/* ===================== 样本闸门 ===================== */
|
||||
|
||||
static void tc_sample_gate(void)
|
||||
{
|
||||
printf("-- TC-TASKPROF-001..005 sample gate --\n");
|
||||
reset_all();
|
||||
task_prof_sample(TASK_PROF_UI_PERIOD, 500u);
|
||||
CHECK((s_prof[TASK_PROF_UI_PERIOD].count == 1u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].last == 500u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].max == 500u),
|
||||
"TC-TASKPROF-001 有效样本记 count/last/max");
|
||||
|
||||
task_prof_sample(TASK_PROF_UI_PERIOD, 300u);
|
||||
CHECK((s_prof[TASK_PROF_UI_PERIOD].max == 500u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].last == 300u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].count == 2u),
|
||||
"TC-TASKPROF-002 更小样本不改 max");
|
||||
|
||||
reset_all();
|
||||
task_prof_sample(TASK_PROF_UI_PERIOD, 0u);
|
||||
CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].count == 0u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].max == 0u),
|
||||
"TC-TASKPROF-003 cyc=0 只累加 glitch(DWT 复位防线)");
|
||||
|
||||
reset_all();
|
||||
task_prof_sample(TASK_PROF_UI_PERIOD, SystemCoreClock * 10u);
|
||||
CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) &&
|
||||
(s_prof[TASK_PROF_UI_PERIOD].count == 0u),
|
||||
"TC-TASKPROF-004 超上界样本只累加 glitch");
|
||||
|
||||
reset_all();
|
||||
task_prof_sample((task_prof_id_t)TASK_PROF_ID_COUNT, 100u);
|
||||
task_prof_sample((task_prof_id_t)(TASK_PROF_ID_COUNT + 1), 100u);
|
||||
task_prof_enter((task_prof_id_t)TASK_PROF_ID_COUNT);
|
||||
task_prof_exit((task_prof_id_t)TASK_PROF_ID_COUNT);
|
||||
CHECK((s_prof[0].count == 0u) && (s_prof[TASK_PROF_ID_COUNT - 1].count == 0u),
|
||||
"TC-TASKPROF-005 id 越界安全返回,不碰任何槽");
|
||||
}
|
||||
|
||||
/* ===================== 时基与 enter/exit 配对 ===================== */
|
||||
|
||||
static void tc_enter_exit(void)
|
||||
{
|
||||
printf("-- TC-TASKPROF-006..010 dwt enable + enter/exit --\n");
|
||||
reset_all();
|
||||
set_cyc(1000u);
|
||||
(void)task_prof_cyc();
|
||||
CHECK(((g_coredebug.DEMCR & CoreDebug_DEMCR_TRCENA_Msk) != 0u) &&
|
||||
((g_dwt.CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0u) &&
|
||||
(s_dwt_ok == 1u),
|
||||
"TC-TASKPROF-006 首次取时基即惰性使能 DWT");
|
||||
|
||||
reset_all();
|
||||
set_cyc(2000u);
|
||||
task_prof_enter(TASK_PROF_UI_WORK);
|
||||
CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 1u) &&
|
||||
(s_prof[TASK_PROF_UI_WORK].t_start == 2000u),
|
||||
"TC-TASKPROF-007 enter 置 in_seg 并记起点");
|
||||
|
||||
set_cyc(2640u);
|
||||
task_prof_exit(TASK_PROF_UI_WORK);
|
||||
CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 0u) &&
|
||||
(s_prof[TASK_PROF_UI_WORK].last == 640u) &&
|
||||
(s_prof[TASK_PROF_UI_WORK].count == 1u),
|
||||
"TC-TASKPROF-008 exit 结算 Δ 并清 in_seg");
|
||||
|
||||
reset_all();
|
||||
task_prof_exit(TASK_PROF_ADC_BEAT);
|
||||
CHECK((s_prof[TASK_PROF_ADC_BEAT].count == 0u) &&
|
||||
(s_prof[TASK_PROF_ADC_BEAT].glitch == 0u),
|
||||
"TC-TASKPROF-009 未 enter 直接 exit 不记账(防 0 起点假超大样本)");
|
||||
|
||||
reset_all();
|
||||
set_cyc(100u);
|
||||
task_prof_enter(TASK_PROF_NET_POLL);
|
||||
set_cyc(150u);
|
||||
task_prof_exit(TASK_PROF_NET_POLL);
|
||||
task_prof_exit(TASK_PROF_NET_POLL);
|
||||
CHECK(s_prof[TASK_PROF_NET_POLL].count == 1u, "TC-TASKPROF-010 重复 exit 不重复记账");
|
||||
}
|
||||
|
||||
/* ===================== 上报节流与通道 ===================== */
|
||||
|
||||
static void tc_report_throttle(void)
|
||||
{
|
||||
printf("-- TC-TASKPROF-011..015 report throttle + channels --\n");
|
||||
reset_all();
|
||||
g_tick = 4999u;
|
||||
task_prof_report();
|
||||
CHECK((g_line_calls == 0) && (g_raw_calls == 0), "TC-TASKPROF-011 首 5 s 前不发");
|
||||
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
CHECK((g_line_calls == 2) && (g_raw_calls == 0),
|
||||
"TC-TASKPROF-012 到点发两行走环形缓冲");
|
||||
|
||||
g_tick = 6999u;
|
||||
task_prof_report();
|
||||
CHECK(g_line_calls == 2, "TC-TASKPROF-013 不足 2 s 不发");
|
||||
|
||||
g_tick = 7000u;
|
||||
task_prof_report();
|
||||
CHECK(g_line_calls == 4, "TC-TASKPROF-014 满 2 s 再发两行");
|
||||
|
||||
g_line_calls = 0;
|
||||
g_raw_calls = 0;
|
||||
task_prof_report_raw();
|
||||
CHECK((g_raw_calls == 2) && (g_line_calls == 0),
|
||||
"TC-TASKPROF-015 report_raw 无视节流且走裸发通道");
|
||||
}
|
||||
|
||||
/* ===================== 行内容与换算 ===================== */
|
||||
|
||||
static void tc_line_format(void)
|
||||
{
|
||||
printf("-- TC-TASKPROF-016..020 line content --\n");
|
||||
|
||||
/* 016:一次上报连发两行;行首前缀是采集脚本的抓取依据,行尾必须是 CRLF */
|
||||
reset_all();
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
CHECK((g_line_calls == 2) &&
|
||||
(strncmp(g_line[0], "[PROF] ", 7) == 0) &&
|
||||
(strncmp(g_line[1], "[PROF2] ", 8) == 0),
|
||||
"TC-TASKPROF-016 两行行首分别是 [PROF] / [PROF2]");
|
||||
CHECK((strstr(g_line[0], "\r\n") != NULL) &&
|
||||
(strstr(g_line[1], "\r\n") != NULL),
|
||||
"TC-TASKPROF-016b 两行都以 CRLF 结尾");
|
||||
CHECK(field_of(0, "uip=") == 0L, "TC-TASKPROF-016c 未采样的槽打印 0");
|
||||
|
||||
/* 017:打印的是**微秒**,分母是 SystemCoreClock —— 断言落在打印字段上 */
|
||||
reset_all();
|
||||
task_prof_sample(TASK_PROF_UI_PERIOD, 1680u); /* 168 MHz → 10 us */
|
||||
task_prof_sample(TASK_PROF_ADC_BEAT, 3360u); /* 168 MHz → 20 us */
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
CHECK(field_of(0, "uip=") == 10L,
|
||||
"TC-TASKPROF-017 168 MHz:1680 周期的 max 打印成 uip=10");
|
||||
CHECK(field_of(0, "adcm=") == 20L,
|
||||
"TC-TASKPROF-017b 同频:3360 周期的 max 打印成 adcm=20");
|
||||
SystemCoreClock = 84000000u; /* 改时钟树而忘了同步 → 整表偏移,这里必须显式跟着变 */
|
||||
g_tick = 7000u;
|
||||
task_prof_report();
|
||||
CHECK(field_of(2, "uip=") == 20L,
|
||||
"TC-TASKPROF-017c 84 MHz 下同一批周期数打印成 20");
|
||||
SystemCoreClock = 168000000u;
|
||||
|
||||
/* 018:gl= 是**所有槽**的 glitch 汇总,不是当前槽 */
|
||||
reset_all();
|
||||
task_prof_sample(TASK_PROF_UI_PERIOD, 0u);
|
||||
task_prof_sample(TASK_PROF_ADC_BEAT, 0u);
|
||||
task_prof_sample(TASK_PROF_NET_POLL, 0u);
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
CHECK(field_of(0, "gl=") == 3L, "TC-TASKPROF-018 gl= 汇总所有槽的 glitch");
|
||||
|
||||
/* 019:段未结算时,"此刻已在段内多久"要看得见(uipn / netn 两个字段) */
|
||||
/* 字段口径 = 上报时刻的 CYCCNT 减去各自的进段时刻(**不是**样本值):
|
||||
* UI_PERIOD 进段 @1000、NET_POLL 进段 @10000,上报时 CYCCNT=51680
|
||||
* uipn = (51680-1000)/168 = 301 netn = (51680-10000)/168 = 248 */
|
||||
reset_all();
|
||||
set_cyc(1000u);
|
||||
task_prof_enter(TASK_PROF_UI_PERIOD);
|
||||
set_cyc(10000u);
|
||||
task_prof_enter(TASK_PROF_NET_POLL);
|
||||
set_cyc(51680u);
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
CHECK(field_of(0, "uipn=") == 301L,
|
||||
"TC-TASKPROF-019 UI_PERIOD 段内:uipn= 上报此刻已停留的 us");
|
||||
CHECK(field_of(1, "netn=") == 248L,
|
||||
"TC-TASKPROF-019b NET_POLL 段内:[PROF2] 的 netn= 同步可见");
|
||||
CHECK(field_of(0, "adcn=") == 0L,
|
||||
"TC-TASKPROF-019c 未进段的 ADC_BEAT 打印 0(不串用别的槽)");
|
||||
CHECK(s_prof[TASK_PROF_UI_PERIOD].in_seg == 1u,
|
||||
"TC-TASKPROF-019d 上报不消费段状态(只观测)");
|
||||
|
||||
/* 020:count 是累计量,两轮上报之间不能清零 */
|
||||
reset_all();
|
||||
task_prof_sample(TASK_PROF_UI_WORK, 100u);
|
||||
g_tick = 5000u;
|
||||
task_prof_report();
|
||||
g_tick = 7000u;
|
||||
task_prof_report();
|
||||
CHECK((s_prof[TASK_PROF_UI_WORK].count == 1u) && (field_of(2, "uic=") == 0L) &&
|
||||
(field_of(0, "uic=") == 0L),
|
||||
"TC-TASKPROF-020 上报不清零(UI_WORK 不在 uic= 字段,仍为累计 1)");
|
||||
}
|
||||
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== Task Prof Tests ===\n");
|
||||
|
||||
tc_sample_gate();
|
||||
tc_enter_exit();
|
||||
tc_report_throttle();
|
||||
tc_line_format();
|
||||
|
||||
printf("\n=== Task Prof Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user