Files
STM32F4-Base/test/host/ui_data/ui_data_test.c
T
wandering1 77779d49d6 feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐
UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机
OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批
预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block
(口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng
反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定
内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按
is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。

记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在
defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE);
事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身
(一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。
为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核,
丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。

为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费
两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过;
RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同
码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。

实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B;
RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS,
protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→
164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34,
既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、
不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页
连跑两次逐字节一致;字库 diff 空。

顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽
16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前
靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值
全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py
模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。

未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行;
LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
2026-10-26 15:51:12 +08:00

938 lines
44 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI 数据快照层主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/UI/ui_data.c(快照存储 / 发布盖章 /
* 只读访问)与跨层契约下标空间的一致性。
*
* 被测对象:真实源文件 ../../../../Drivers/BSP/UI/ui_data.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本;本模块零平台依赖,无需任何桩)
*
* 为什么要主机侧单测(而不是只靠模拟器像素回归与台架):
* 1) 快照层是"页面看到的整个世界",它错一处全部页面一起错,而像素回归只看得到
* "当前接了线的页面"——未接线的通道(遥信 27 点、定值 40 项)错了不会让任何
* 一张参考图变红,只有把契约下标逐项钉死才照得出来;
* 2) 跨层"魔法下标"(yc 序 = adc_semantic_t、q10 序 = prot_set_id_t)是解耦的
* 代价,契约把它从运行期风险降级为编译/测试期错误的第 3 条断言就落在这里
* (同一 TU 里同时 include 两侧头,见
* docs/UI设计/UI_数据供给设计说明.md §9.4);
* 3) "首帧之前显示 -- 而不是 0.000"是诚实性判据,板子上电只有一次、台架碰不到,
* 主机侧可以随时复现。
*
* 字符串字面量一律纯 ASCII(编码铁律:断言名是打印用的,不参与渲染);
* 用例名里的通道/点号语义见各 TC 上方注释。
*
* 覆盖点(TC-UIDATA-xxx;属主登记处 = 本目录,族 TC-HOST-UIDATA):
* TC-UIDATA-001 跨层数量一致:UI_DATA_YC_N == ADC_SEM_COUNT、SET_N == PS_ID_COUNT
* TC-UIDATA-002 跨层语义序逐项一致(UA/UB/UC/UN/IA/IB/IC/IN/TEMP = 0..8,
* TEMP 恒为末槽——页面据此判"哪一路是码值域")
* TC-UIDATA-003 首帧之前:三通道读 -1、seq = 0(页面渲染 "--" 的依据)
* TC-UIDATA-004 发布后逐字段一致 + 序号盖章(入帧 seq 被忽略、由本层 +1)
* TC-UIDATA-005 连续发布 seq 单调递增;三通道各自独立计数、内容互不串台
* TC-UIDATA-006 NULL 防御:publish 返 -1 且存量不动;read 返 -2 且输出不动
* TC-UIDATA-007 init 幂等复位(发布后再 init -> 回到无首帧态)
* TC-UIDATA-008 量纲/品质常量:eng 三档互不重合且 PIN=0;六个品质位两两不相交;
* 三帧宽度在内存预算内(快照不是 LVGL 对象,只占 .bss)
* TC-UIDATA-009 遥信位集合 = YX15~18/23/31(六点);bits 与 valid 相互独立
* TC-UIDATA-010 相量槽 ph_n 原样透传(写侧已钳位,本层不再加工)
* TC-UIDATA-011 时钟字段容量足装 "YYYY-MM-DD HH:MM:SS"(19 + NUL)且不越界
* TC-UIDATA-012 SOE 事件类型不透明编号与 App/log/soe_rec.h 枚举逐项相等(含
* 未标定闭锁=7 / 标定复归=8);八类事件全部有显示名、名长不超
* 值列宽、采样与标定两组四名互不相同、越界编号回占位
* TC-UIDATA-013 SOE 元件编号与 App/algo/prot_elm.h 枚举逐项相等(0..8 +
* PE_COUNT == 表长);元件名不超名列宽、非元件/越界回占位
* TC-UIDATA-014 注入 reader 后的两行渲染:时标行/内容行分工、单条读盘缓存
* (同记录两行只读一次)、IV 时标抑制、读失败退化与不绑空态
* ── 以下 6 条 = 定值写路径(道F task-1,docs/任务规划/UI补齐.md §2 第 1 行)──
* TC-UIDATA-015 队列深度 4:四格都进得去、第五格满返 -1 且状态转 ERR_BUSY;
* FIFO 顺序(最后投的一条最后落定)、空队列 drain 返 0、
* drain(NULL) 返 -1 且不动队列、投递成功即转 WAIT
* TC-UIDATA-016 请求表示法:NULL/带非法位 mask 返 -2 且状态与队列都不动;
* mask=0 空请求返 0 且直接落 OK 不占槽;请求尺寸与队列 .bss
* 预算钉死(168 B/格 × 4 格)——加深或加宽必须先过这条
* TC-UIDATA-017 正常往返:请求 → WAIT → drain → OK → 新值可读;**只提交 mask
* 内的 id**(未提交的槽一位都不动,这是"从显示值回提全表会
* 顺手改掉返回系数"那条坑的机器防线)
* TC-UIDATA-018 越界不改值:整批预检任一项越界 → ERR_RANGE 且**一字不写**
* (含同批里合法的那几项)+ err_mask 指认越界项 + 下一次成功
* 落定后 err_mask 自动清零
* TC-UIDATA-019 闭锁拒写:locked 置位 → ERR_LOCKED、丢弃本条(不排队等解锁)、
* 值区一字不动;解闭锁后重新提交可成功
* TC-UIDATA-020 apply 中途失败(预检与执行不一致的防御分支):如实 ERR_RANGE,
* 已生效的前几项不回滚(prot_set 无回滚接口,谎称整批未写更坏)
* TC-UIDATA-021 v2 帧扩展:UI_DATA_VER == 2、既有字段偏移逐字节见证(只允许尾
* 追加)、cal_block 两种态(未标定/已标定)整帧往返
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-25
*/
#include <stdio.h>
#include <stddef.h> /* offsetof:v2 帧布局见证(只许尾追加) */
#include <string.h>
/* 生产源文件原样纳入(纯内存叶子,零平台依赖,直接编) */
#include "ui_data.c"
/* 契约另一侧的头:跨层下标空间一致性断言(本目录同时 include BSP 与 App 两侧头,
* 这就是契约 §9.4 第 3 条断言的落点) */
#include "adc_input.h" /* adc_semantic_t:遥测下标真源 */
#include "prot_set.h" /* prot_set_id_t:定值下标真源 */
/* 第二个被测件:SOE 显示名映射组件(道E task-14 落的表现层映射表)。
* 本组件已解除 LVGL 依赖(容量宏本地化,见其 .c 内注释),可直接编进主机单测 */
#include "ui_w_soe.c"
#include "soe_rec.h" /* soe_type_t:事件类型编号真源 */
#include "prot_elm.h" /* prot_elm_id_t:元件编号真源 */
/* ===================== 断言框架 ===================== */
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 void fill_rt(ui_data_rt_t *p, uint8_t seed) {
uint8_t i;
(void)memset(p, 0, sizeof(*p));
p->seq = 777u; /* 故意填非 0:本层应忽略并自己盖章 */
p->tick_ms = 12345u + seed;
p->valid = 1u;
p->eng = UI_DATA_ENG_PIN;
p->ph_n = (uint8_t)(seed % (UI_DATA_PH_MAX + 1u));
for (i = 0u; i < UI_DATA_YC_N; i++) {
p->yc[i].val = (float)i + 0.5f;
p->yc[i].qds = (uint8_t)(i + 1u);
}
for (i = 0u; i < UI_DATA_PH_MAX; i++) {
p->ph[i].val = (float)i * 2.0f;
p->ph[i].qds = (uint8_t)(0xA0u + i);
}
p->yx_bits = 0x000000010000ULL;
p->yx_valid = 0x4043C000ULL;
p->freq_valid = 1u;
p->freq_hz = 50.02f;
p->clock_ok = 1u;
(void)snprintf(p->clock, sizeof(p->clock), "%s", "2026-10-25 08:00:00");
}
/* 逐字段比对(seq 除外:本层盖章后必然与入帧不同,比对前由调用方对齐) */
static int rt_equals(const ui_data_rt_t *a, const ui_data_rt_t *b) {
if (a->tick_ms != b->tick_ms) return 0;
if (a->valid != b->valid) return 0;
if (a->eng != b->eng) return 0;
if (a->ph_n != b->ph_n) return 0;
if (a->yx_bits != b->yx_bits) return 0;
if (a->yx_valid != b->yx_valid) return 0;
if (a->freq_valid != b->freq_valid) return 0;
if (a->freq_hz != b->freq_hz) return 0;
if (a->clock_ok != b->clock_ok) return 0;
if (strcmp(a->clock, b->clock) != 0) return 0;
if (memcmp(a->yc, b->yc, sizeof(a->yc)) != 0) return 0;
if (memcmp(a->ph, b->ph, sizeof(a->ph)) != 0) return 0;
return 1;
}
/* ===================== TC-UIDATA-001 / 002:跨层下标空间 ===================== */
static void tc_contract_indices(void)
{
CHECK(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT, "TC-UIDATA-001a yc slots == ADC_SEM_COUNT");
CHECK(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT, "TC-UIDATA-001b q10 slots == PS_ID_COUNT");
CHECK((uint32_t)ADC_SEM_UA == 0u, "TC-UIDATA-002a UA == 0");
CHECK((uint32_t)ADC_SEM_UB == 1u, "TC-UIDATA-002b UB == 1");
CHECK((uint32_t)ADC_SEM_UC == 2u, "TC-UIDATA-002c UC == 2");
CHECK((uint32_t)ADC_SEM_UN == 3u, "TC-UIDATA-002d UN == 3");
CHECK((uint32_t)ADC_SEM_IA == 4u, "TC-UIDATA-002e IA == 4");
CHECK((uint32_t)ADC_SEM_IB == 5u, "TC-UIDATA-002f IB == 5");
CHECK((uint32_t)ADC_SEM_IC == 6u, "TC-UIDATA-002g IC == 6");
CHECK((uint32_t)ADC_SEM_IN == 7u, "TC-UIDATA-002h IN == 7");
/* TEMP 必须是末槽:写侧"sem < ADC_SEM_TEMP 才是电气量"的 eng 算法与页面的
* RAW 位判定都靠这个位置,插中间等于两条判据一起错位 */
CHECK((uint32_t)ADC_SEM_TEMP == (UI_DATA_YC_N - 1u), "TC-UIDATA-002i TEMP is last slot");
}
/* ===================== TC-UIDATA-003:首帧之前 ===================== */
static void tc_before_first_frame(void)
{
ui_data_rt_t rt;
ui_data_par_t par;
ui_data_info_t info;
ui_data_init();
(void)memset(&rt, 0x5A, sizeof(rt));
(void)memset(&par, 0x5A, sizeof(par));
(void)memset(&info, 0x5A, sizeof(info));
CHECK(ui_data_rt(&rt) == -1, "TC-UIDATA-003a rt read before first frame == -1");
CHECK(ui_data_par(&par) == -1, "TC-UIDATA-003b par read before first frame == -1");
CHECK(ui_data_info(&info) == -1, "TC-UIDATA-003c info read before first frame == -1");
CHECK((ui_data_rt_seq() == 0u) && (ui_data_par_seq() == 0u) && (ui_data_info_seq() == 0u),
"TC-UIDATA-003d all three seq == 0");
/* 输出缓冲必须一字不动——页面缓存被写脏会把"无数据"渲染成"有数据" */
{
const uint8_t *p = (const uint8_t *)&rt;
int dirty = 0;
size_t i;
for (i = 0u; i < sizeof(rt); i++) {
if (p[i] != 0x5Au) {
dirty = 1;
break;
}
}
CHECK(dirty == 0, "TC-UIDATA-003e output buffer untouched on failure");
}
}
/* ===================== TC-UIDATA-004 / 005:发布盖章与通道独立 ===================== */
static void tc_publish_and_seq(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_par_t par;
ui_data_par_t pg;
uint32_t seq1;
ui_data_init();
fill_rt(&src, 3u);
CHECK(ui_data_publish_rt(&src) == 0, "TC-UIDATA-004a publish returns 0");
CHECK(ui_data_rt_seq() == 1u, "TC-UIDATA-004b seq stamped 1 (incoming 777 ignored)");
(void)memset(&got, 0, sizeof(got));
CHECK(ui_data_rt(&got) == 0, "TC-UIDATA-004c readable after first publish");
got.seq = src.seq; /* 序号由本层盖章,其余字段须逐字节一致 */
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-004d all fields preserved verbatim");
/* 连续发布:seq 单调 +1,内容取最新一帧 */
fill_rt(&src, 9u);
(void)ui_data_publish_rt(&src);
seq1 = ui_data_rt_seq();
CHECK(seq1 == 2u, "TC-UIDATA-005a second publish bumps seq to 2");
(void)ui_data_rt(&got);
got.seq = src.seq;
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-005b latest frame is the one served");
/* 三通道独立:发 par 不动 rt 的 seq,也不动 rt 内容 */
(void)memset(&par, 0, sizeof(par));
par.valid = 1u;
par.checksum = 0xABCDu;
par.q10[0] = PROT_SET_Q10(10.0);
par.q10[UI_DATA_SET_N - 1u] = PROT_SET_Q10(55.5);
(void)ui_data_publish_par(&par);
CHECK((ui_data_par_seq() == 1u) && (ui_data_rt_seq() == seq1),
"TC-UIDATA-005c par publish does not touch rt seq");
(void)memset(&pg, 0, sizeof(pg));
CHECK((ui_data_par(&pg) == 0) && (pg.checksum == 0xABCDu)
&& (pg.q10[0] == PROT_SET_Q10(10.0))
&& (pg.q10[UI_DATA_SET_N - 1u] == PROT_SET_Q10(55.5)),
"TC-UIDATA-005d par stored verbatim (Q10 not rescaled here)");
(void)ui_data_rt(&got);
got.seq = src.seq;
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-005e par publish does not pollute rt");
}
/* ===================== TC-UIDATA-006:NULL 防御 ===================== */
static void tc_null_guard(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_init();
fill_rt(&src, 1u);
(void)ui_data_publish_rt(&src);
CHECK(ui_data_publish_rt(NULL) == -1, "TC-UIDATA-006a publish(NULL) == -1");
CHECK(ui_data_rt_seq() == 1u, "TC-UIDATA-006b failed publish keeps seq");
CHECK(ui_data_rt(NULL) == -2, "TC-UIDATA-006c read(NULL) == -2");
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
got.seq = src.seq;
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-006d stored frame still intact");
}
/* ===================== TC-UIDATA-007:init 幂等复位 ===================== */
static void tc_init_idempotent(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_init();
fill_rt(&src, 2u);
(void)ui_data_publish_rt(&src);
(void)ui_data_publish_rt(NULL);
ui_data_init();
ui_data_init(); /* 重复调用仍幂等 */
CHECK(ui_data_rt_seq() == 0u, "TC-UIDATA-007a seq reset to 0 by init");
CHECK(ui_data_rt(&got) == -1, "TC-UIDATA-007b back to no-frame state after init");
}
/* ===================== TC-UIDATA-008:量纲与品质常量 ===================== */
static void tc_constants(void)
{
CHECK((UI_DATA_ENG_PIN == 0u) && (UI_DATA_ENG_SEC == 1u) && (UI_DATA_ENG_PRI == 2u),
"TC-UIDATA-008a eng levels are 0/1/2 (pin-V / secondary / primary)");
CHECK((UI_DATA_Q_IV | UI_DATA_Q_NT | UI_DATA_Q_OV | UI_DATA_Q_SB | UI_DATA_Q_BL
| UI_DATA_Q_RAW) == 0x3Fu, "TC-UIDATA-008b six quality bits fill low 6 bits");
CHECK((UI_DATA_Q_IV & UI_DATA_Q_NT) == 0u, "TC-UIDATA-008c IV/NT disjoint");
CHECK((UI_DATA_Q_OV & UI_DATA_Q_SB) == 0u, "TC-UIDATA-008d OV/SB disjoint");
CHECK((UI_DATA_Q_BL & UI_DATA_Q_RAW) == 0u, "TC-UIDATA-008e BL/RAW disjoint");
/* 内存预算护栏(docs/UI设计/UI_数据供给设计说明.md §10):快照是 .bss 里的
* 定长结构,不是 LVGL 对象;一旦有人往里塞大数组或指针,这里先响 */
CHECK(sizeof(ui_data_rt_t) <= 256u, "TC-UIDATA-008f rt frame within budget (<=256B)");
CHECK(sizeof(ui_data_par_t) <= 200u, "TC-UIDATA-008g par frame within budget (<=200B)");
CHECK(sizeof(ui_data_info_t) <= 128u, "TC-UIDATA-008h info frame within budget (<=128B)");
}
/* ===================== TC-UIDATA-009:遥信位集合与独立性 ===================== */
static void tc_yx_points(void)
{
/* V0.1 有硬件来源的遥信点集合(YX15~18/23/31,1 基序号):与 104 点表同源,
* 写侧 App/app/ui_data_feed.c 与 simulator/main.c 固定帧都用同一组位号。
* 任何一处改动都会让本条红,逼着改动方同时更新页面名表与位图镜像注释 */
const uint64_t valid_expect = ((uint64_t)1u << 14u) | ((uint64_t)1u << 15u) |
((uint64_t)1u << 16u) | ((uint64_t)1u << 17u) |
((uint64_t)1u << 22u) | ((uint64_t)1u << 30u);
ui_data_rt_t src;
ui_data_rt_t got;
const uint64_t dp_expect = 0x0FULL; /* V0.1 双点段 = bit0~3(YX1~4 开关位置类) */
ui_data_init();
fill_rt(&src, 5u);
src.yx_bits = 0u; /* 全部点位"分/未动作" */
src.yx_valid = valid_expect;
src.yx_dp = dp_expect;
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK(got.yx_valid == valid_expect, "TC-UIDATA-009a valid mask == YX15/16/17/18/23/31");
CHECK(got.yx_bits == 0u, "TC-UIDATA-009b bits and valid are independent fields");
/* 双点位图必须原样透传:页面按它选词(合位/分位 vs 动作/复归),快照层若做"归一化"
* 或按 valid 掩一次,页面就会把双点当单点形容——同一条事实两处解释必然漂 */
CHECK(got.yx_dp == dp_expect, "TC-UIDATA-009c dp mask passed through unmasked");
/* 点表与名表共用点数(1 基 IOA 1~33)。这里是**有意**再抄一遍字面量:测试的职责是
* 独立见证,协议层那个 #define 在 .c 内不可见,改点数时必须有人被绊一下——绊到后
* 要同时改:App/protocol/iec104/iec104_pts.c 的 YX_N、ui_data.h 的 UI_DATA_YX_N、
* 页面名表 ui_page_yx.c(有编译期数组守卫兜着)、本条 */
CHECK((uint32_t)UI_DATA_YX_N == 33u, "TC-UIDATA-009d point count is 33 (YX_N witness)");
}
/* ===================== TC-UIDATA-010:相量槽透传 ===================== */
static void tc_phasor_slots(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
uint8_t i;
ui_data_init();
fill_rt(&src, UI_DATA_PH_MAX);
src.ph_n = UI_DATA_PH_MAX;
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK(got.ph_n == UI_DATA_PH_MAX, "TC-UIDATA-010a ph_n passed through unchanged");
i = 0u;
while ((i < UI_DATA_PH_MAX) && (got.ph[i].val == src.ph[i].val)) {
i++;
}
CHECK(i == UI_DATA_PH_MAX, "TC-UIDATA-010b every phasor slot kept its value");
}
/* ===================== TC-UIDATA-011:时钟字段容量 ===================== */
static void tc_clock_field(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
char probe[UI_DATA_CLOCK_LEN];
(void)snprintf(probe, sizeof(probe), "%s", "2026-10-25 08:00:00");
CHECK(strlen(probe) == 19u, "TC-UIDATA-011a clock string is 19 chars");
CHECK(UI_DATA_CLOCK_LEN == 20u, "TC-UIDATA-011b capacity is 19 + NUL");
ui_data_init();
fill_rt(&src, 7u);
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK((got.clock_ok == 1u) && (strcmp(got.clock, "2026-10-25 08:00:00") == 0)
&& (got.clock[UI_DATA_CLOCK_LEN - 1u] == '\0'),
"TC-UIDATA-011c full string stored, last byte still NUL");
}
/* ====== TC-UIDATA-012 / 013:SOE 不透明编号与显示名映射(现场核对枚举头) ====== */
/* 名字表版式护栏:名列 7 汉字 = 21 字节、值列 4 汉字 = 12 字节(UTF-8 3 字节/字);
* 超宽只会被屏幕裁掉,不会被任何像素门禁报出来,所以在这里钉死
* (宽度口径 docs/UI设计/页面扩充详细方案.md §2.3 第 4 条) */
static int all_names_fit(const char *const *tbl, unsigned n, size_t max_bytes) {
unsigned i;
for (i = 0u; i < n; i++) {
if (tbl[i] == NULL) return 0;
if (strlen(tbl[i]) == 0u) return 0;
if (strlen(tbl[i]) > max_bytes) return 0;
}
return 1;
}
static void tc_soe_codes(void)
{
/* 编号与 App/log/soe_rec.h 枚举逐项相等(不是照抄文档转述,是 include 真头对表) */
CHECK((uint8_t)SOE_T_ELM_ASSERT == UI_W_SOE_T_ELM_ASSERT, "TC-UIDATA-012a type ELM_ASSERT");
CHECK((uint8_t)SOE_T_ELM_RETURN == UI_W_SOE_T_ELM_RETURN, "TC-UIDATA-012b type ELM_RETURN");
CHECK((uint8_t)SOE_T_PLATE_OUT == UI_W_SOE_T_PLATE_OUT, "TC-UIDATA-012c type PLATE_OUT");
CHECK((uint8_t)SOE_T_PLATE_IN == UI_W_SOE_T_PLATE_IN, "TC-UIDATA-012d type PLATE_IN");
CHECK((uint8_t)SOE_T_SAMPLE_BLOCK == UI_W_SOE_T_SAMPLE_BLOCK, "TC-UIDATA-012e type SAMPLE_BLOCK");
CHECK((uint8_t)SOE_T_SAMPLE_RESUME == UI_W_SOE_T_SAMPLE_RESUME, "TC-UIDATA-012f type SAMPLE_RESUME");
CHECK((uint8_t)SOE_T_CAL_BLOCK == UI_W_SOE_T_CAL_BLOCK, "TC-UIDATA-012g type CAL_BLOCK == 7");
CHECK((uint8_t)SOE_T_CAL_RESUME == UI_W_SOE_T_CAL_RESUME, "TC-UIDATA-012h type CAL_RESUME == 8");
CHECK((uint8_t)SOE_T_COUNT == UI_W_SOE_T_COUNT, "TC-UIDATA-012i sentinel COUNT == 9");
CHECK(SOE_ELM_NONE == UI_W_SOE_ELM_NONE, "TC-UIDATA-012j ELM_NONE == 0xFF");
CHECK(SOE_FLG_IV == UI_W_SOE_FLG_IV, "TC-UIDATA-012k FLG_IV == 0x80");
/* 八类事件全部有名(新加类型忘配名 = 屏上"--",像素门禁照不出来) */
{
uint8_t t;
int ok = 1;
size_t longest = 0u;
for (t = 1u; t < UI_W_SOE_T_COUNT; t++) {
const char *nm = ui_w_soe_type_name(t);
if (nm == NULL) {
ok = 0; /* 契约上不可达(映射表恒返回串),留作防御分支 */
} else {
if ((strcmp(nm, "--") == 0) || (strlen(nm) == 0u)) {
ok = 0;
}
if (strlen(nm) > longest) {
longest = strlen(nm);
}
}
}
CHECK(ok == 1, "TC-UIDATA-012l every type has a display name");
/* 宽度按**实际画到哪一列**判:1/2 两类(动作/返回)画在值列(4 汉字 = 12 字节),
* 3..8 这些非元件事件的类型名画在名列(7 汉字 = 21 字节)。一刀切按 12 判会把
* "未标定闭锁"这类正常名字判成违规,一刀切按 21 判又放过了值列超宽——分列才咬手 */
CHECK((strlen(ui_w_soe_type_name(UI_W_SOE_T_ELM_ASSERT)) <= 12u)
&& (strlen(ui_w_soe_type_name(UI_W_SOE_T_ELM_RETURN)) <= 12u),
"TC-UIDATA-012m element event names fit value column (<=4 CJK)");
CHECK(longest <= 21u, "TC-UIDATA-012m2 type names fit name column (<=7 CJK)");
CHECK(strcmp(ui_w_soe_type_name(0u), "--") == 0, "TC-UIDATA-012n code 0 -> placeholder");
CHECK(strcmp(ui_w_soe_type_name(UI_W_SOE_T_COUNT), "--") == 0,
"TC-UIDATA-012o out-of-range -> placeholder");
/* 类型 5/6(采样)与 7/8(标定)四者必须互不相同:同名 = 抄表时串行,
* 现场排障会把"未标定闭锁"读成"采样闭锁" */
CHECK((strcmp(ui_w_soe_type_name(UI_W_SOE_T_SAMPLE_BLOCK),
ui_w_soe_type_name(UI_W_SOE_T_CAL_BLOCK)) != 0)
&& (strcmp(ui_w_soe_type_name(UI_W_SOE_T_SAMPLE_RESUME),
ui_w_soe_type_name(UI_W_SOE_T_CAL_RESUME)) != 0)
&& (strcmp(ui_w_soe_type_name(UI_W_SOE_T_CAL_BLOCK),
ui_w_soe_type_name(UI_W_SOE_T_CAL_RESUME)) != 0),
"TC-UIDATA-012p sample/cal lock-resume names are distinct");
}
}
static void tc_soe_elms(void)
{
CHECK(PE_OC1 == 0, "TC-UIDATA-013a PE_OC1 == 0");
CHECK(PE_OC2 == 1, "TC-UIDATA-013b PE_OC2 == 1");
CHECK(PE_OC3 == 2, "TC-UIDATA-013c PE_OC3 == 2");
CHECK(PE_OL == 3, "TC-UIDATA-013d PE_OL == 3");
CHECK(PE_OV == 4, "TC-UIDATA-013e PE_OV == 4");
CHECK(PE_UV == 5, "TC-UIDATA-013f PE_UV == 5");
CHECK(PE_3U0 == 6, "TC-UIDATA-013g PE_3U0 == 6");
CHECK(PE_OF == 7, "TC-UIDATA-013h PE_OF == 7");
CHECK(PE_UF == 8, "TC-UIDATA-013i PE_UF == 8");
CHECK((uint32_t)PE_COUNT == UI_W_SOE_ELM_N, "TC-UIDATA-013j PE_COUNT == UI_W_SOE_ELM_N");
CHECK(all_names_fit(s_elm_name, UI_W_SOE_ELM_N, 21u) == 1,
"TC-UIDATA-013k element names fit name column (<=7 CJK)");
CHECK(strcmp(ui_w_soe_elm_name(UI_W_SOE_ELM_NONE), "--") == 0,
"TC-UIDATA-013l non-element id -> placeholder");
CHECK(strcmp(ui_w_soe_elm_name(UI_W_SOE_ELM_N), "--") == 0,
"TC-UIDATA-013m out-of-range element -> placeholder");
}
/* ====== TC-UIDATA-014:注入 reader 后的两行渲染与单条缓存 ====== */
static uint32_t s_get_calls;
static ui_w_soe_rec_t s_fake_rec;
static uint16_t fake_count(void)
{
return 3u;
}
static int fake_get(uint32_t a_idx, ui_w_soe_rec_t *a_out)
{
s_get_calls++;
if (a_idx >= 3u) {
return -1;
}
(void)memcpy(a_out, &s_fake_rec, sizeof(*a_out));
a_out->seq = a_idx;
return 0;
}
static const ui_soe_reader_t s_fake_reader = { fake_count, fake_get };
static void tc_soe_render(void)
{
ui_w_soe_t inst;
char name[UI_W_SOE_TS_LEN + 16u];
char value[UI_W_SOE_TS_LEN + 16u];
(void)memset(&inst, 0, sizeof(inst));
(void)memset(&s_fake_rec, 0, sizeof(s_fake_rec));
s_reader = NULL;
CHECK(ui_w_soe_count(&inst) == 1u, "TC-UIDATA-014a unbound reader -> empty pseudo row");
ui_w_soe_bind(&s_fake_reader);
CHECK(ui_w_soe_count(&inst) == 6u, "TC-UIDATA-014b 3 records -> 6 rows");
/* 元件事件:名列 = 元件,值列 = 动作/返回 */
(void)strcpy(s_fake_rec.ts, "2026-10-25 08:00:00");
s_fake_rec.type = UI_W_SOE_T_ELM_ASSERT;
s_fake_rec.elm = (uint8_t)PE_OC1;
s_fake_rec.flags = 0u;
s_get_calls = 0u;
(void)memset(name, 0, sizeof(name));
(void)memset(value, 0, sizeof(value));
ui_w_soe_refresh(&inst, 0u, name, value);
CHECK((name[0] != 0) && (strlen(name) == 19u) && (value[0] == 0),
"TC-UIDATA-014c ts row shows full timestamp");
ui_w_soe_refresh(&inst, 1u, name, value);
CHECK((strlen(name) == 12u) && (strlen(value) == 6u) && (s_get_calls == 1u),
"TC-UIDATA-014d content row uses cached record (single disk read)");
/* IV 时标:不显示猜测时间,值列给"时钟无效" */
s_fake_rec.flags = UI_W_SOE_FLG_IV;
(void)memset(&inst, 0, sizeof(inst));
ui_w_soe_refresh(&inst, 0u, name, value);
CHECK((strcmp(name, "--") == 0) && (strlen(value) == 12u),
"TC-UIDATA-014e IV timestamp suppressed, marked in value column");
/* 非元件事件:类型名进名列,值列留空。15 字节 = "未标定闭锁" 5 汉字 × 3 B;
* 主机测试的字面量必须纯 ASCII(无 BOM 文件里中文串会吞引号),所以这里只按
* 字节数 + "不是占位串"判,串内容由 simulator 侧的 TEXTS 断言在像素上核 */
s_fake_rec.flags = 0u;
s_fake_rec.type = UI_W_SOE_T_CAL_BLOCK;
s_fake_rec.elm = UI_W_SOE_ELM_NONE;
(void)memset(&inst, 0, sizeof(inst));
ui_w_soe_refresh(&inst, 3u, name, value);
CHECK((strlen(name) == 15u) && (name[0] != '-') && (value[0] == 0),
"TC-UIDATA-014f non-element row: 5-CJK type name in name column");
/* 越界记录号:读失败退化为占位,不留脏缓存 */
ui_w_soe_refresh(&inst, 998u, name, value);
CHECK(strcmp(name, "--") == 0, "TC-UIDATA-014g failed read -> placeholder, no stale cache");
ui_w_soe_bind(NULL);
}
/* ====== TC-UIDATA-015~020:定值写路径(队列 + 状态机 + 注入执行侧) ====== */
/*
* 假执行侧(替掉 App/algo/prot_set):一张 40 项值区 + 一张上下限表。
* ⚠ 本单测不链 App 侧任何 .c(prot_set 依赖 Flash/校验和编排,且写路径的**契约**
* 本来就只是"三个函数的返回值"),所以测的是 ui_data.c 里那份真实的队列 +
* 状态机代码:越界不改值、闭锁拒写、只提交改动 id 三条判据都落在生产代码上,
* 不是测试里的副本。真实 App 实现的等价性由 ui_data_feed.c 的三个包函数
* (feed_param_locked/check/apply)在代码审查与台架侧保证。
*/
#define FAKE_LO_ALL (-100000) /* 默认宽松下限:单条 TC 里再逐行收紧 */
#define FAKE_HI_ALL (100000)
static int32_t s_fake_val[UI_DATA_SET_N];
static int32_t s_fake_lo[UI_DATA_SET_N];
static int32_t s_fake_hi[UI_DATA_SET_N];
static uint8_t s_fake_locked;
static uint8_t s_fake_poison; /* 0xFF = 不投毒;否则该 id 的 apply 必失败 */
static uint32_t s_fake_apply_calls;
static uint8_t fake_locked(void)
{
return s_fake_locked;
}
static int fake_check(uint8_t a_id, int32_t a_q10)
{
if (a_id >= (uint8_t)UI_DATA_SET_N) {
return -1;
}
if ((a_q10 < s_fake_lo[a_id]) || (a_q10 > s_fake_hi[a_id])) {
return -1;
}
return 0;
}
static int fake_apply(uint8_t a_id, int32_t a_q10)
{
if (fake_check(a_id, a_q10) != 0) {
return -1;
}
if ((s_fake_poison != 0xFFu) && (a_id == s_fake_poison)) {
return -1; /* 投毒:模拟"预检过了但执行侧仍拒"(理论不可达的防御分支) */
}
s_fake_val[a_id] = a_q10;
s_fake_apply_calls++;
return 0;
}
static const ui_data_param_ops_t s_fake_ops = { fake_locked, fake_check, fake_apply };
/* 每个 TC 开头调一次:值区填可辨识的旧值 + 快照与队列复位 */
static void fake_reset(void)
{
uint8_t i;
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
s_fake_val[i] = (int32_t)i * 1000;
s_fake_lo[i] = FAKE_LO_ALL;
s_fake_hi[i] = FAKE_HI_ALL;
}
s_fake_locked = 0u;
s_fake_poison = 0xFFu;
s_fake_apply_calls = 0u;
ui_data_init();
}
/* 投一条只改若干项的请求;未列出的 id 不进 mask(= 本次不提交) */
static int fake_submit(const uint8_t *a_ids, const int32_t *a_vals, uint8_t a_n)
{
ui_data_param_req_t req;
uint8_t i;
(void)memset(&req, 0, sizeof(req));
for (i = 0u; i < a_n; i++) {
req.mask |= ((uint64_t)1u << a_ids[i]);
req.q10[a_ids[i]] = a_vals[i];
}
return ui_data_param_request(&req);
}
static int32_t fake_val(uint8_t a_id)
{
return s_fake_val[a_id];
}
static void tc_param_queue(void)
{
ui_data_param_req_t req;
uint8_t k;
int accepted = 1;
int was_wait = 1;
fake_reset();
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE,
"TC-UIDATA-015a state IDLE after init (never submitted)");
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015b drain on empty queue == 0");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE,
"TC-UIDATA-015c empty drain leaves state alone (not WAIT)");
CHECK(ui_data_param_err_mask() == 0u, "TC-UIDATA-015d err_mask clean at idle");
/* 四格全进得去;每投一条状态就该是 WAIT */
(void)memset(&req, 0, sizeof(req));
req.mask = 1u; /* 只改 id0 */
for (k = 0u; k < UI_DATA_PARAM_QD; k++) {
req.q10[0] = (int32_t)(1000 + (int32_t)k);
if (ui_data_param_request(&req) != 0) {
accepted = 0;
}
if (ui_data_param_state() != UI_DATA_PARAM_ST_WAIT) {
was_wait = 0;
}
}
CHECK(UI_DATA_PARAM_QD == 4u, "TC-UIDATA-015e queue depth is 4 (contract witness)");
CHECK(accepted == 1, "TC-UIDATA-015f four requests all accepted");
CHECK(was_wait == 1, "TC-UIDATA-015g state WAIT while queued");
/* 第五格:满 → 返 -1 + 状态 ERR_BUSY(在队四条一条不掉) */
req.q10[0] = 9999;
CHECK(ui_data_param_request(&req) == -1, "TC-UIDATA-015h fifth request rejected (-1)");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_BUSY,
"TC-UIDATA-015i rejected request surfaces ERR_BUSY");
/* FIFO:前三条落定后仍是 WAIT,第四条才落终态;值 = 最后投的那条(1003) */
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-015j drain processes one");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT,
"TC-UIDATA-015k still WAIT with 3 requests left in queue");
(void)ui_data_param_drain(&s_fake_ops);
(void)ui_data_param_drain(&s_fake_ops);
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT,
"TC-UIDATA-015l WAIT until the last queued request settles");
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-015m fourth request drained");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-015n terminal OK when drained");
CHECK((fake_val(0) == 1003) && (s_fake_apply_calls == 4u),
"TC-UIDATA-015o FIFO: last queued value lands, all four applied");
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015p queue empty again");
/* ops 无效:不动队列也不动状态 */
fake_reset();
CHECK(fake_submit(NULL, NULL, 0u) == 0, "TC-UIDATA-015q empty request accepted (see 016)");
CHECK(ui_data_param_drain(NULL) == -1, "TC-UIDATA-015r drain(NULL) == -1");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK,
"TC-UIDATA-015s failed drain does not rewrite state");
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015t failed drain does not consume");
}
static void tc_param_request_shape(void)
{
ui_data_param_req_t req;
fake_reset();
CHECK(ui_data_param_request(NULL) == -2, "TC-UIDATA-016a request(NULL) == -2");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE,
"TC-UIDATA-016b invalid request leaves state IDLE (page must not show ERR for a bug)");
(void)memset(&req, 0, sizeof(req));
req.mask = ((uint64_t)1u << 63u); /* 第 63 项不存在 */
CHECK(ui_data_param_request(&req) == -2, "TC-UIDATA-016c mask with bit63 == -2");
req.mask = ((uint64_t)1u << (UI_DATA_SET_N - 1u)); /* 第 39 项 = 最后一项,合法 */
CHECK(ui_data_param_request(&req) == 0, "TC-UIDATA-016d last legal bit accepted");
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-016e drained last-bit request");
CHECK(s_fake_apply_calls == 1u, "TC-UIDATA-016f item 39 really reached the executor");
/* 空请求:不占槽、直接 OK("没改东西却按了保存"是正常操作) */
fake_reset();
req.mask = 0u;
CHECK(ui_data_param_request(&req) == 0, "TC-UIDATA-016g empty request returns 0");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-016h empty request settles OK");
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-016i empty request consumed no slot");
/* 内存预算护栏:请求是 .bss 里的定长槽,168 B/格 × 4 格 = 672 B
* (台账 docs/内存管理/RAM使用分析与优化方案.md;扩深度/加宽必须先过这条) */
CHECK(sizeof(ui_data_param_req_t) == 168u, "TC-UIDATA-016j request is 168 bytes");
CHECK(sizeof(s_pq) == (672u), "TC-UIDATA-016k queue .bss is 4 x 168 = 672 bytes");
CHECK(sizeof(s_pq) <= 768u, "TC-UIDATA-016l queue .bss within budget (<=768B)");
CHECK((uint32_t)UI_DATA_SET_N <= 64u, "TC-UIDATA-016m item count fits the 64-bit mask");
}
static void tc_param_roundtrip(void)
{
const uint8_t ids[3] = { 2u, 3u, 7u };
const int32_t vals[3] = { 12345, 60000, 7 };
fake_reset();
CHECK(fake_submit(ids, vals, 3u) == 0, "TC-UIDATA-017a batch of three submitted");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT, "TC-UIDATA-017b WAIT before drain");
CHECK(fake_val(2) == 2000, "TC-UIDATA-017c old value still in effect before drain");
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-017d drained one request");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-017e OK after drain");
CHECK((fake_val(2) == 12345) && (fake_val(3) == 60000) && (fake_val(7) == 7),
"TC-UIDATA-017f new values readable after roundtrip");
/* 只提交改动项:mask 外的槽一位都不动(Q10 往返精度坑的机器防线) */
CHECK((fake_val(0) == 0) && (fake_val(1) == 1000) && (fake_val(4) == 4000)
&& (fake_val(6) == 6000) && (fake_val(8) == 8000)
&& (fake_val(UI_DATA_SET_N - 1u) == (int32_t)(UI_DATA_SET_N - 1u) * 1000),
"TC-UIDATA-017g untouched items are byte-identical (mask-only commit)");
CHECK(s_fake_apply_calls == 3u, "TC-UIDATA-017h exactly the masked items were applied");
/* 回环的后半段:App 落值后重发参数帧 → 页面按新 seq 读到新值 */
{
ui_data_par_t frame;
ui_data_par_t got;
uint8_t i;
(void)memset(&frame, 0, sizeof(frame));
frame.valid = 1u;
frame.checksum = 0x1234u;
frame.cal_block = 0u;
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
frame.q10[i] = s_fake_val[i];
}
CHECK(ui_data_publish_par(&frame) == 0, "TC-UIDATA-017i par frame republished");
(void)memset(&got, 0, sizeof(got));
CHECK((ui_data_par(&got) == 0) && (got.q10[2] == 12345) && (got.q10[7] == 7),
"TC-UIDATA-017j page reads back the committed values");
}
}
static void tc_param_range_atomic(void)
{
/* 非 const:本 TC 复用同一组数组跑三种越界组合(改了再投) */
uint8_t ids[2] = { 2u, 5u };
int32_t vals[2] = { 12345, 99999 };
fake_reset();
s_fake_hi[5] = 100; /* 第 5 项上限 100 → 99999 越界;第 2 项本身合法 */
CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-018a out-of-range batch accepted for queue");
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-018b drained");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_RANGE, "TC-UIDATA-018c state ERR_RANGE");
CHECK(ui_data_param_err_mask() == ((uint64_t)1u << 5u),
"TC-UIDATA-018d err_mask points at the offending item");
CHECK((fake_val(2) == 2000) && (fake_val(5) == 5000),
"TC-UIDATA-018e all-or-nothing: the legal item in the same batch NOT written");
CHECK(s_fake_apply_calls == 0u, "TC-UIDATA-018f precheck blocks every apply call");
/* 多项同时越界:err_mask 全给(页面逐行反显,不是只报第一行) */
fake_reset();
s_fake_lo[6] = 500;
s_fake_hi[5] = 100;
ids[0] = 5u;
ids[1] = 6u;
vals[0] = 99999; /* 超上限 */
vals[1] = 60; /* 低于下限 */
CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-018g two-item bad batch queued");
(void)ui_data_param_drain(&s_fake_ops);
CHECK(ui_data_param_err_mask() == (((uint64_t)1u << 5u) | ((uint64_t)1u << 6u)),
"TC-UIDATA-018h err_mask lists every offending item (both bounds)");
/* 下一次成功落定 → err_mask 自动清零(否则页面把上次越界显示成这次) */
ids[0] = 2u;
vals[0] = 12345;
CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-018i good request queued");
(void)ui_data_param_drain(&s_fake_ops);
CHECK((ui_data_param_state() == UI_DATA_PARAM_ST_OK) && (ui_data_param_err_mask() == 0u),
"TC-UIDATA-018j err_mask cleared on next successful commit");
}
static void tc_param_locked(void)
{
const uint8_t ids[1] = { 2u };
const int32_t vals[1] = { 12345 };
fake_reset();
s_fake_locked = 1u;
CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-019a request queued while locked");
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-019b drained under lock");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_LOCKED, "TC-UIDATA-019c state ERR_LOCKED");
CHECK((fake_val(2) == 2000) && (s_fake_apply_calls == 0u),
"TC-UIDATA-019d locked commit writes nothing (no apply call)");
CHECK(ui_data_param_err_mask() == 0u, "TC-UIDATA-019e lock failure is not a range failure");
CHECK(ui_data_param_drain(&s_fake_ops) == 0,
"TC-UIDATA-019f locked request is dropped, not queued waiting for unlock");
/* 解闭锁后重新提交 → OK(闭锁丢弃的是那一条,不是永久拉黑写路径) */
s_fake_locked = 0u;
CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-019g resubmit after unlock accepted");
(void)ui_data_param_drain(&s_fake_ops);
CHECK((ui_data_param_state() == UI_DATA_PARAM_ST_OK) && (fake_val(2) == 12345),
"TC-UIDATA-019h write path healthy again after unlock");
}
static void tc_param_apply_fail(void)
{
const uint8_t ids[2] = { 3u, 7u };
const int32_t vals[2] = { 33333, 77777 };
fake_reset();
s_fake_poison = 7u; /* 预检全过、执行侧仍拒第 7 项(两真源不一致的防御分支) */
CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-020a request with poisoned item queued");
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-020b drained");
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_RANGE,
"TC-UIDATA-020c apply failure reported as ERR_RANGE (not silent OK)");
CHECK(ui_data_param_err_mask() == ((uint64_t)1u << 7u), "TC-UIDATA-020d failing item named");
CHECK(fake_val(3) == 33333, "TC-UIDATA-020e earlier item already applied is NOT rolled back");
CHECK(fake_val(7) == 7000, "TC-UIDATA-020f failing item itself unchanged");
}
static void tc_param_frame_v2(void)
{
ui_data_par_t src;
ui_data_par_t got;
/* 版本号是页面与写侧唯一对齐凭据;v2 = 尾追加 cal_block */
CHECK(UI_DATA_VER == 2u, "TC-UIDATA-021a contract version is 2");
/* v1 布局逐字节见证:字段只允许尾追加,任何插入/重排都会让下面四条红
* (页面按名字取字段,错位是静默的——只有把偏移钉死才照得出来) */
CHECK(offsetof(ui_data_par_t, seq) == 0u, "TC-UIDATA-021b seq offset unchanged");
CHECK(offsetof(ui_data_par_t, bad_mask) == 16u, "TC-UIDATA-021c bad_mask offset unchanged");
CHECK(offsetof(ui_data_par_t, q10) == 24u, "TC-UIDATA-021d q10 offset unchanged");
CHECK(offsetof(ui_data_par_t, cal_block) == 184u,
"TC-UIDATA-021e cal_block is the appended tail field (offset 184)");
CHECK(sizeof(ui_data_par_t) == 192u, "TC-UIDATA-021f par frame is 192 B (+8 for v2)");
/* 写侧组装缓冲是 rt/par/info 三帧共用的 union(App/app/ui_data_feed.c 的 s_scr):
* rt 恒大于 par,所以给 par 追加字段**不会**连带抬升那块 .bss——加字段之前
* 先确认这条,否则每次尾追加都悄悄多啃一份 RAM(台账只记总量,看不出是谁长的) */
CHECK(sizeof(ui_data_rt_t) == 208u, "TC-UIDATA-021g rt frame is 208 B (union stays put)");
CHECK(sizeof(ui_data_par_t) < sizeof(ui_data_rt_t),
"TC-UIDATA-021h par still smaller than rt (shared scratch unchanged)");
/* 闭锁态:1 = 未标定(写侧按 UA/UN/IA 三路 VALID 现算,本层只存真值) */
ui_data_init();
(void)memset(&src, 0, sizeof(src));
src.valid = 1u;
src.locked = 0u;
src.cal_block = 1u;
CHECK(ui_data_publish_par(&src) == 0, "TC-UIDATA-021g publish blocked frame");
(void)memset(&got, 0, sizeof(got));
CHECK((ui_data_par(&got) == 0) && (got.cal_block == 1u) && (got.locked == 0u),
"TC-UIDATA-021h cal_block=1 roundtrip, distinct from locked");
/* 解卡态:写侧填 0 后覆盖发布(同一份存储不会粘住旧值) */
src.cal_block = 0u;
(void)ui_data_publish_par(&src);
(void)memset(&got, 0xAA, sizeof(got));
CHECK((ui_data_par(&got) == 0) && (got.cal_block == 0u),
"TC-UIDATA-021i cal_block=0 roundtrip (no stale block flag)");
/* 写侧默认值纪律:调用方清零后不填本字段 = 0(未闭锁),首帧不会假报徽标 */
(void)memset(&src, 0, sizeof(src));
src.valid = 1u;
(void)ui_data_publish_par(&src);
(void)ui_data_par(&got);
CHECK(got.cal_block == 0u, "TC-UIDATA-021j zero-initialized frame means not blocked");
}
/* ===================== main ===================== */
int main(void)
{
printf("=== UI Data Snapshot Tests ===\n");
printf("-- contract index space --\n");
tc_contract_indices();
printf("-- before first frame --\n");
tc_before_first_frame();
printf("-- publish and seq --\n");
tc_publish_and_seq();
printf("-- null guard --\n");
tc_null_guard();
printf("-- init idempotent --\n");
tc_init_idempotent();
printf("-- constants --\n");
tc_constants();
printf("-- yx points --\n");
tc_yx_points();
printf("-- phasor slots --\n");
tc_phasor_slots();
printf("-- clock field --\n");
tc_clock_field();
printf("-- soe codes and names --\n");
tc_soe_codes();
printf("-- soe elements --\n");
tc_soe_elms();
printf("-- soe render --\n");
tc_soe_render();
printf("-- param write path: queue --\n");
tc_param_queue();
printf("-- param write path: request shape --\n");
tc_param_request_shape();
printf("-- param write path: roundtrip --\n");
tc_param_roundtrip();
printf("-- param write path: range atomic --\n");
tc_param_range_atomic();
printf("-- param write path: locked --\n");
tc_param_locked();
printf("-- param write path: apply failure --\n");
tc_param_apply_fail();
printf("-- param frame v2 (cal_block) --\n");
tc_param_frame_v2();
printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}