Files
STM32F4-Base/test/host/ui_data/ui_data_test.c
T
wandering1 48a8ef0564 feat(UI): 遥测死区与系数 2 页——注入表第四张、三态用脏位、WAIT 与 OK 各出一张黄金
这两页是「UI补齐」里唯一带**持久化语义**的一档,所以整笔的设计重心不在画面上,而在**页面如何诚实表达
「已受理」与「已落盘」**——这一点由 setpage-scout 在开工前的只读查实钉下来:cfg_file_save() 全仓唯一调用
者是 run_par_store_save(),而 default_task.c:246 **每轮无条件**调它、函数首行 s_dirty==0 就直接返回
⇒
落盘动力本来就在 defaultTask,不在 104 会话、也不在事件位。因此本笔走 A2(UI 不当授权写者),不需要
新事件位、不需要新同步口。

页面三态判据 = run_par_dirty_peek():ENTER / WAIT(脏位仍在 = 已受理未落盘)/ OK(脏位归零 = 整档已外科
保存成功)/ 写被拒 = ERR。两条限制写在页面注释里没藏:脏位是**整档全局**的(一趟落 [run_par] 9 键 +
[adc_cal] 两段,归零只证明「这一档落盘了」,不证明「我这一项单独落盘」);他人随后再改会重新置脏 ⇒ 沿用
clear/clock 页的结果锁存,把「我这次动作的结果」与「档位此刻的状态」分开。

**为什么黄金必须出两张**(这条是本轮最有价值的防假证据规则):模拟器假件**保留脏位**、全仓唯一的
s_yt_dirty=0 写点是 sim_yt_pump()(setter 只置不清),于是 WAIT 与 OK 各出一帧——两组同键序、唯一差别是
EXTRA 里的 --yt-nopump。若假件同步清脏位,真机的 WAIT 态永远不会被像素回归覆盖,黄金就成了「只钉住最
好看那一帧」。TC-UIDATA-028o 另钉住 ui_data 层零缓存。

形状:新增 ui_data 第四张注入表(八字段整表校验、拒残表),实现落在装配层 App/app/ui_data_feed.c,
页面零 App include(分层方向 + 模拟器不链 run_par 两条老理由都成立);共用视图模型 ui_w_ytview 承载两页。
合法域一律由执行侧出口给(run_par_db_range / RUN_PAR_K_MAX_UV),页面不抄第二真源——系数页 v1 只用粗守
并在注释写明两层守卫互不掩盖,µV/码 ×[0.5,2.0] 那条真带的出口器归 task-43,**没有**在页面里抄常量。

取舍与遗留(如实):系数行 d=0 整 µV(1e9 装不下两位小数,注释写了「要小数改哪、不收紧假上限」);b 覆盖
无口未做;徽标只画系数页;行名因「率」缺字改为 Hz死区(font-charset 机器先抓到的,不是我目测判的)。

实测:@build 0 错误 0 警告(Code=233,900 / RW-data=1,536);ui_data 单测 277/277、run_par
67/67 零回归;
内容页黄金 47 页全过、**存量 40 张逐字节零漂移**(git 里只有 7 张新增);menu_rows / 池水位 / font-charset
/
bom / mojibake / nested-comment / cppcheck / coding-standard / layer-deps /
bsp-err 全 0 新增;编号五项式
refs 8,778 / dec 7,622 / und **1,156**(上限 1,166)/ 比值 86.83%(基线 86.0% 之上)/ 错误差值
0,注释零裸形
字母+数字。generated-fresh 与 registration-check 本地 PASS;新增 6 只源文件按规矩**双端注册同笔**(叶子
+
registry 19→21 + uvprojx + CMakeLists),uvprojx 这次属「真的增删源文件」才入库。

移交三条不混进本笔:① 全量 --selftest 现 exit 1 是 task-41 硬闸在模拟器上也拒 reset/clear(既有红,快检档
不受影响),归台架道定夺;② 内容页 40→47 的全仓文档计数改写由 Lead 派;③ run_par_deadband_set 的限定
条件语措辞(「iec104Task 上下文」已被装配层裁定覆盖)归算法层下一笔。
2026-10-27 05:07:18 +08:00

1546 lines
74 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 两种态(未标定/已标定)整帧往返
* ── 以下 5 组 = 装置维护通道与时钟换算(道F task-3,
* docs/任务规划/UI补齐.md §2 第 3 行「装置维护 3 页」的地基)──
* TC-UIDATA-022 时钟换算与"无效值拒收":12 条锚点正向换算 + 反函数互逆 +
* **时区反证**(不得含 28800 偏移)+ 逐月天数(闰/平)+ 非法组合
* 逐条 -2 + epoch 0/越域反算 -2(1970 绝不被反算出来)
* TC-UIDATA-023 时标串 parse/format:正常串、空串(无效时钟)、记录侧占位串
* "1970-01-01 08:00:00" 必须解析失败、分隔位/长度/尾字符/型对值非法
* TC-UIDATA-024 跨层编号顺序契约:UI_W_REC_LOG_* 逐字等于 syslog_id_t 枚举序 +
* UI 侧缓冲容量与 SYSLOG 契约同宽 + UI_DATA_CLEAR_ST_* 与
* SYSLOG_CLEAR_* 逐值相等 + 面板两条留痕串各自不超描述上限
* ("与远端串不同"不在这里判:字面量互比是恒真空断言,被
* test-cppcheck 抓过,真判据落在 TC-UIDATA-026s)
* TC-UIDATA-025 维护通道未注入防御:六口安全返回 + 残表整表拒绝且不覆盖好表
* TC-UIDATA-026 注入后转发言行:epoch 0 哨兵不下发、reset_req 自行投操作记录=0 次
* TC-UIDATA-027 遥测整定通道未注入防御:八个口一律安全返回、不碰出参;
* 残表整表拒绝且不覆盖好表(与 025 同一条纪律的第五张表复刻)
* TC-UIDATA-028 遥测整定注入后转发言行:行现状逐字段原样上抛、写口的 idx 与
* 工程量原样下传、返回码直通、pending 无状态直读(本层零缓存——
* 页面 OK/WAIT 的唯一依据必须是执行侧此刻的脏位)
* (谁触发谁投,防"远端 + 本地"双投)、clear 状态码直通不改码
*
* 通过:全部 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:元件编号真源 */
#include "sys_log.h" /* syslog_id_t / SYSLOG_CLEAR_*:记录编号与清除状态真源 */
#include "ui_w_records.h" /* UI_W_REC_LOG_*:UI 侧记录源编号(同一 TU 里核对顺序契约) */
/* ===================== 断言框架 ===================== */
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");
}
/* ============ 装置维护写通道与时钟换算(道F task-3,UI 补齐 §2 第 3 行)============ */
/*
* 维护执行侧桩件:只回答"哪个口被调了几次、最后一次收到什么",不做任何业务判断。
* 计数而不是布尔:多处偷投同一条操作记录(本道最怕的"远端 + 本地各投一次")
* 只有靠次数才照得出来。
*/
static int s_stub_clock_rc;
static int s_stub_clock_calls;
static uint32_t s_stub_clock_last;
static int s_stub_reset_rc;
static int s_stub_reset_calls;
static uint32_t s_stub_active;
static int s_stub_note_calls;
static char s_stub_note_last[32];
static int s_stub_clear_calls;
static uint8_t s_stub_clear_last_id;
static int s_stub_clear_state_calls;
static uint8_t s_stub_clear_state_id;
static int s_stub_clear_state_rc;
static int stub_clock_set(uint32_t a_epoch)
{
s_stub_clock_calls++;
s_stub_clock_last = a_epoch;
return s_stub_clock_rc;
}
static int stub_reset_req(void)
{
s_stub_reset_calls++;
return s_stub_reset_rc;
}
static uint32_t stub_active_count(void)
{
return s_stub_active;
}
static void stub_oper_note(const char *a_desc)
{
s_stub_note_calls++;
if (a_desc != NULL) {
(void)strncpy(s_stub_note_last, a_desc, sizeof(s_stub_note_last) - 1u);
s_stub_note_last[sizeof(s_stub_note_last) - 1u] = 0;
}
}
static int stub_clear_req(uint8_t a_id)
{
s_stub_clear_calls++;
s_stub_clear_last_id = a_id;
return 0;
}
static int stub_clear_state(uint8_t a_id)
{
s_stub_clear_state_calls++;
s_stub_clear_state_id = a_id;
return s_stub_clear_state_rc;
}
static void stub_reset_all(void)
{
s_stub_clock_rc = 0;
s_stub_clock_calls = 0;
s_stub_clock_last = 0u;
s_stub_reset_rc = 0;
s_stub_reset_calls = 0;
s_stub_active = 0u;
s_stub_note_calls = 0;
s_stub_note_last[0] = '\0';
s_stub_clear_calls = 0;
s_stub_clear_last_id = 0u;
s_stub_clear_state_calls = 0;
s_stub_clear_state_id = 0u;
s_stub_clear_state_rc = UI_DATA_CLEAR_ST_NONE;
}
static const ui_data_maint_ops_t s_maint_stub = {
stub_clock_set, stub_reset_req, stub_active_count,
stub_oper_note, stub_clear_req, stub_clear_state
};
/* 故意缺一条(clear_state 为 NULL)的残表:注入必须整表拒绝,不能半途接住 */
static const ui_data_maint_ops_t s_maint_broken = {
stub_clock_set, stub_reset_req, stub_active_count,
stub_oper_note, stub_clear_req, NULL
};
/* 日历用例:字段 → 期望秒(期望值由独立实现(Python/JS 格里历)算出,不自我复制) */
typedef struct {
uint16_t y;
uint8_t mo;
uint8_t d;
uint8_t h;
uint8_t mi;
uint8_t s;
uint32_t epoch;
} clock_case_t;
static const clock_case_t k_clock_cases[] = {
{ 2000u, 1u, 1u, 0u, 0u, 0u, 946684800u }, /* 年域下限(RTC 年字段起点) */
{ 2000u, 2u, 29u, 0u, 0u, 0u, 951782400u }, /* 千禧闰年 2/29 */
{ 2000u, 3u, 1u, 0u, 0u, 0u, 951868800u }, /* 闰日次日:月序跨界 */
{ 2001u, 2u, 28u, 23u, 59u, 59u, 983404799u }, /* 平年 2 月末 + 当日最大秒 */
{ 2004u, 2u, 29u, 12u, 0u, 0u, 1078056000u }, /* 4 年闰规则 */
{ 2016u,12u, 31u, 0u, 0u, 0u, 1483142400u }, /* 年末 */
{ 2020u, 6u, 15u, 6u, 30u, 45u, 1592202645u }, /* 任意中段 */
{ 2024u, 2u, 29u, 12u, 34u, 56u, 1709210096u }, /* 近一个闰日 */
{ 2025u, 2u, 28u, 0u, 0u, 1u, 1740700801u }, /* 平年 2/28 */
{ 2026u,10u, 25u, 8u, 0u, 0u, 1792915200u }, /* 模拟器固定帧那一刻 */
{ 2037u,12u, 31u, 23u, 59u, 59u, 2145916799u }, /* 上界前一天 */
{ 2038u, 1u, 19u, 3u, 14u, 7u, 2147483647u } /* uint32 秒域上界 = 0x7FFFFFFF */
};
/*
* TC-UIDATA-022 时钟换算与"无效值拒收"
* a) 12 条锚点正向换算(期望值来自独立格里历实现,不是本函数的自我复述)
* b) 反函数逐条回原文(互逆性:屏上显示的时间 = 存进去的时间,不多不少一小时)
* c) **时区反证**:2026-10-25 08:00:00 必须是 1792915200,而不是它 +28800;
* 本工程墙钟 epoch 域就是本地时间,谁在这里补了 8 小时,模拟器照样自洽、
* 只有真机上"屏上输 08:00、存进去 16:00"(App/task/ui_task.c 的 SOE 时标注记
* 是同一条纪律的另一处防线)
* d) 逐月天数(含闰年 2 月 29 / 平年 2 月 28 / 出域月返 0)
* e) 非法组合逐条拒(2 月 30、非闰 2 月 29、月 0/13、日 0/32、时 24、分秒 60、
* 年 1999/2039),且 -2 与"越能力上界"的 -3 可分
* f) 边界外秒域反算(epoch 0 与 0x80000000 都拒;0 是记录侧占位起点,绝不能
* 被反算成 1970 显示到屏上)
*/
static void tc_clock_convert(void)
{
ui_data_clock_t t;
uint32_t e;
uint32_t i;
int rc;
int ok_fwd = 1;
int ok_rev = 1;
int n = (int)(sizeof(k_clock_cases) / sizeof(k_clock_cases[0]));
for (i = 0u; i < (uint32_t)n; i++) {
(void)memset(&t, 0, sizeof(t));
t.year = k_clock_cases[i].y;
t.month = k_clock_cases[i].mo;
t.day = k_clock_cases[i].d;
t.hour = k_clock_cases[i].h;
t.minute = k_clock_cases[i].mi;
t.second = k_clock_cases[i].s;
e = 0u;
rc = ui_data_clock_to_epoch(&t, &e);
if ((rc != 0) || (e != k_clock_cases[i].epoch)) {
ok_fwd = 0;
printf(" case %u: rc=%d got=%lu want=%lu\n", (unsigned)i, rc,
(unsigned long)e, (unsigned long)k_clock_cases[i].epoch);
}
(void)memset(&t, 0xFF, sizeof(t));
rc = ui_data_clock_from_epoch(k_clock_cases[i].epoch, &t);
if ((rc != 0) || (t.year != k_clock_cases[i].y) || (t.month != k_clock_cases[i].mo) ||
(t.day != k_clock_cases[i].d) || (t.hour != k_clock_cases[i].h) ||
(t.minute != k_clock_cases[i].mi) || (t.second != k_clock_cases[i].s)) {
ok_rev = 0;
printf(" rev case %u: rc=%d got=%u-%u-%u %u:%u:%u\n", (unsigned)i, rc,
(unsigned)t.year, (unsigned)t.month, (unsigned)t.day,
(unsigned)t.hour, (unsigned)t.minute, (unsigned)t.second);
}
}
CHECK(ok_fwd, "TC-UIDATA-022a fields to epoch over 12 anchors");
CHECK(ok_rev, "TC-UIDATA-022b epoch to fields is the exact inverse");
/* 时区反证:本函数不得含任何时区偏移 */
(void)memset(&t, 0, sizeof(t));
t.year = 2026u; t.month = 10u; t.day = 25u; t.hour = 8u; t.minute = 0u; t.second = 0u;
e = 0u;
(void)ui_data_clock_to_epoch(&t, &e);
CHECK(e == 1792915200u, "TC-UIDATA-022c no timezone shift baked in (local epoch domain)");
CHECK(e != (1792915200u + 28800u), "TC-UIDATA-022c2 must not be plus 8 hours");
/* 逐月天数(闰/平两年 + 出域月) */
{
static const uint8_t k_leap[12] = { 31u,29u,31u,30u,31u,30u,31u,31u,30u,31u,30u,31u };
static const uint8_t k_flat[12] = { 31u,28u,31u,30u,31u,30u,31u,31u,30u,31u,30u,31u };
int ok_dim = 1;
for (i = 1u; i <= 12u; i++) {
if (ui_data_clock_days_in_month(2000u, (uint8_t)i) != k_leap[i - 1u]) ok_dim = 0;
if (ui_data_clock_days_in_month(2025u, (uint8_t)i) != k_flat[i - 1u]) ok_dim = 0;
}
CHECK(ok_dim, "TC-UIDATA-022d days in month (leap 2000 / flat 2025)");
CHECK(ui_data_clock_days_in_month(2000u, 0u) == 0u, "TC-UIDATA-022d2 month 0 means invalid");
CHECK(ui_data_clock_days_in_month(2000u, 13u) == 0u, "TC-UIDATA-022d3 month 13 means invalid");
}
/* 非法组合逐条:一律 -2(输入错),不是 -3(能力边界) */
{
static const clock_case_t k_bad[] = {
{ 2025u, 2u, 29u, 0u, 0u, 0u, 0u }, /* 非闰年 2/29 */
{ 2026u, 2u, 30u, 0u, 0u, 0u, 0u }, /* 2 月 30 日 */
{ 2026u, 4u, 31u, 0u, 0u, 0u, 0u }, /* 4 月无 31 日 */
{ 2026u, 0u, 1u, 0u, 0u, 0u, 0u }, /* 月 0 */
{ 2026u,13u, 1u, 0u, 0u, 0u, 0u }, /* 月 13 */
{ 2026u,10u, 0u, 0u, 0u, 0u, 0u }, /* 日 0 */
{ 2026u,10u, 32u, 0u, 0u, 0u, 0u }, /* 日 32 */
{ 2026u,10u, 25u,24u, 0u, 0u, 0u }, /* 时 24 */
{ 2026u,10u, 25u, 0u,60u, 0u, 0u }, /* 分 60 */
{ 2026u,10u, 25u, 0u, 0u,60u, 0u }, /* 秒 60 */
{ 1999u, 1u, 1u, 0u, 0u, 0u, 0u }, /* 年 < RTC 年域 */
{ 2039u, 1u, 1u, 0u, 0u, 0u, 0u } /* 年 > uint32 秒域 */
};
int ok_bad = 1;
for (i = 0u; i < (uint32_t)(sizeof(k_bad) / sizeof(k_bad[0])); i++) {
/* 逐字段搬(两种结构体尺寸不同,靠强转按字节拷是给自己埋雷) */
t.year = k_bad[i].y;
t.month = k_bad[i].mo;
t.day = k_bad[i].d;
t.hour = k_bad[i].h;
t.minute = k_bad[i].mi;
t.second = k_bad[i].s;
rc = ui_data_clock_to_epoch(&t, &e);
if (rc != -2) {
ok_bad = 0;
printf(" bad case %u: rc=%d (want -2)\n", (unsigned)i, rc);
}
}
CHECK(ok_bad, "TC-UIDATA-022e invalid field combos rejected with -2");
CHECK(ui_data_clock_to_epoch(NULL, &e) == -1, "TC-UIDATA-022e2 NULL input is -1");
CHECK(ui_data_clock_to_epoch(&t, NULL) == -1, "TC-UIDATA-022e3 NULL output is -1");
}
/* 反算越界:0 是"墙钟无效"的占位起点,绝不能显示成 1970-01-01 */
CHECK(ui_data_clock_from_epoch(0u, &t) == -2, "TC-UIDATA-022f epoch 0 rejected (never show 1970)");
CHECK(ui_data_clock_from_epoch(946684799u, &t) == -2, "TC-UIDATA-022f2 epoch below 2000 rejected");
CHECK(ui_data_clock_from_epoch(0x80000000u, &t) == -2, "TC-UIDATA-022f3 epoch above uint32 domain");
CHECK(ui_data_clock_from_epoch(0u, NULL) == -1, "TC-UIDATA-022f4 NULL output is -1");
}
/*
* TC-UIDATA-023 时标串 parse / format
* 快照 rt.clock 的串型(19 字符)是页面预填的唯一来源;无效时钟时它是空串,
* 记录侧的占位串 "1970-01-01 08:00:00" 同样必须解析失败——否则页面会把
* 占位串当真实时间预填,用户照抄保存就等于把 1970 写进 RTC。
*/
static void tc_clock_string(void)
{
ui_data_clock_t t;
char buf[24];
uint32_t e;
(void)memset(&t, 0, sizeof(t)); /* 解析失败按契约不写输出,先给个确定的初值 */
e = 0u;
CHECK(ui_data_clock_parse("2026-10-25 08:00:00", &t) == 0, "TC-UIDATA-023a parse well-formed ts");
if (t.year == 2026u && t.month == 10u && t.day == 25u && t.hour == 8u) {
(void)ui_data_clock_to_epoch(&t, &e);
CHECK(e == 1792915200u, "TC-UIDATA-023b parsed ts maps to same epoch as fields");
} else {
CHECK(0, "TC-UIDATA-023b parsed fields wrong");
}
CHECK(ui_data_clock_parse("", &t) == -2, "TC-UIDATA-023c empty ts (clock invalid) is -2");
CHECK(ui_data_clock_parse("1970-01-01 08:00:00", &t) == -2,
"TC-UIDATA-023d record placeholder ts must not parse (no 1970 on screen)");
CHECK(ui_data_clock_parse("2026-10-25T08:00:00", &t) == -2, "TC-UIDATA-023e wrong separator");
CHECK(ui_data_clock_parse("2026-10-25 08:00:0", &t) == -2, "TC-UIDATA-023f short ts");
CHECK(ui_data_clock_parse("2026-10-25 08:00:001", &t) == -2, "TC-UIDATA-023g trailing junk");
CHECK(ui_data_clock_parse("2O26-10-25 08:00:00", &t) == -2, "TC-UIDATA-023h non digit year");
CHECK(ui_data_clock_parse("2026-02-30 00:00:00", &t) == -2, "TC-UIDATA-023i shaped but invalid day");
CHECK(ui_data_clock_parse("20260101 00:00:00", &t) == -2, "TC-UIDATA-023j missing dashes");
CHECK(ui_data_clock_parse(NULL, &t) == -1, "TC-UIDATA-023k NULL ts is -1");
CHECK(ui_data_clock_parse("2026-10-25 08:00:00", NULL) == -1, "TC-UIDATA-023l NULL out is -1");
(void)memset(buf, 0x7A, sizeof(buf));
CHECK(ui_data_clock_format(&t, buf, sizeof(buf)) == 0, "TC-UIDATA-023m format ok into 24B buffer");
CHECK(strcmp(buf, "2026-10-25 08:00:00") == 0, "TC-UIDATA-023n formatted string round-trips");
(void)memset(buf, 0x7A, sizeof(buf));
CHECK(ui_data_clock_format(&t, buf, 19u) == -1, "TC-UIDATA-023o capacity 19 refused (need 20)");
CHECK(buf[0] == (char)0x7A, "TC-UIDATA-023p refused format writes nothing");
CHECK(ui_data_clock_format(NULL, buf, sizeof(buf)) == -1, "TC-UIDATA-023q NULL in is -1");
}
/*
* TC-UIDATA-024 跨层编号顺序契约(Lead 裁定:UI 侧编号逐字等于 syslog_id_t 枚举序,
* 只准尾部追加)+ 清除状态码逐值相等 + 墙钟字段容量与串型一致。
* 适配层始终显式映射(从不强转),这条断言钉的是"两套编号今天确实相等"这件事
* 本身——枚举中间插值会安静地把事故读成告警,只有在这里才会响。
*/
static void tc_record_codes(void)
{
CHECK(UI_W_REC_LOG_RUN == (uint8_t)SYSLOG_RUN, "TC-UIDATA-024a RUN order");
CHECK(UI_W_REC_LOG_SELFTEST == (uint8_t)SYSLOG_SELFTEST, "TC-UIDATA-024b SELFTEST order");
CHECK(UI_W_REC_LOG_ACCIDENT == (uint8_t)SYSLOG_ACCIDENT, "TC-UIDATA-024c ACCIDENT order");
CHECK(UI_W_REC_LOG_ALARM == (uint8_t)SYSLOG_ALARM, "TC-UIDATA-024d ALARM order");
CHECK(UI_W_REC_LOG_START == (uint8_t)SYSLOG_START, "TC-UIDATA-024e START order");
CHECK(UI_W_REC_LOG_OPER == (uint8_t)SYSLOG_OPER, "TC-UIDATA-024f OPER order");
CHECK(UI_W_REC_LOG_YK == (uint8_t)SYSLOG_YK, "TC-UIDATA-024g YK order");
CHECK(UI_W_REC_LOG_N == (uint8_t)SYSLOG_COUNT, "TC-UIDATA-024h count matches sentinel (append only below)");
CHECK(UI_DATA_CLOCK_TS_LEN == SYSLOG_TS_LEN, "TC-UIDATA-024i ts field length same contract on both sides");
CHECK(UI_W_REC_TS_LEN == (SYSLOG_TS_LEN + 1u), "TC-UIDATA-024j UI ts buffer covers SYSLOG_TS_LEN + NUL");
CHECK(UI_W_REC_DESC_LEN == (SYSLOG_DESC_MAX + 1u), "TC-UIDATA-024k UI desc buffer covers max + NUL");
CHECK(UI_DATA_CLEAR_ST_NONE == SYSLOG_CLEAR_NONE, "TC-UIDATA-024l clear NONE value");
CHECK(UI_DATA_CLEAR_ST_OK == SYSLOG_CLEAR_OK, "TC-UIDATA-024m clear OK value");
CHECK(UI_DATA_CLEAR_ST_WAIT == SYSLOG_CLEAR_WAIT, "TC-UIDATA-024n clear WAIT value");
CHECK(UI_DATA_CLEAR_ST_FAIL == SYSLOG_CLEAR_FAIL, "TC-UIDATA-024o clear FAIL value");
/* 操作记录描述上限:本地/远端两条留痕串都必须装得下,超了会被投递侧静默截断 */
CHECK(strlen("force reset by panel") <= (size_t)SYSLOG_DESC_MAX,
"TC-UIDATA-024p panel reset note fits desc limit");
CHECK(strlen("clock set by panel") <= (size_t)SYSLOG_DESC_MAX,
"TC-UIDATA-024q panel clock note fits desc limit");
/* 这里曾写 CHECK(strcmp("force reset by panel", "force reset") != 0, ...):
* 两个字面量互比 = 恒真,test-cppcheck 一眼抓出(钉口径的断言永不可能失败,
* 比根本没有断言更坏——它让人以为这条已经被管住了)。同一判据挪到
* TC-UIDATA-026s:拿实际落进执行侧缓冲的串去比远端那条文案 */
}
/*
* TC-UIDATA-025 维护通道未注入时的防御:六个口一律安全返回、不崩;
* 残表(某成员 NULL)整表拒绝且不覆盖已生效的好表——半途接住会在"按下才崩"的位置崩。
* "通道此刻绑没绑"一律用**可观察行为**判(025i/025k/025m 走 reset_req 的返回值),
* 不再有 ui_data_maint_ready() 那种状态查询口:它写完之后全仓零消费者、只有本
* 用例在读,等于给下一个人留一个看起来能用的假接口(Lead 裁定 2026-10-26,
* 与 generated-fresh 今天抓的是同一类东西)。真需要装配自检时,让它带着消费者
* 一起回来。
*/
static void tc_maint_not_bound(void)
{
(void)ui_data_maint_bind(NULL);
stub_reset_all();
CHECK(ui_data_maint_clock_set(1792915200u) == -1, "TC-UIDATA-025b clock_set not bound is -1");
CHECK(ui_data_maint_reset_req() == -1, "TC-UIDATA-025c reset_req not bound is -1");
CHECK(ui_data_maint_active_count() == 0u, "TC-UIDATA-025d active_count not bound is 0");
CHECK(ui_data_maint_oper_note("x") == -1, "TC-UIDATA-025e oper_note not bound is -1");
CHECK(ui_data_maint_clear_req(UI_W_REC_LOG_RUN) == -1, "TC-UIDATA-025f clear_req not bound is -1");
CHECK(ui_data_maint_clear_state(UI_W_REC_LOG_RUN) == UI_DATA_CLEAR_ST_NONE,
"TC-UIDATA-025g clear_state not bound is NONE");
CHECK(ui_data_maint_bind(&s_maint_broken) == -1, "TC-UIDATA-025h incomplete table refused");
CHECK(ui_data_maint_reset_req() == -1, "TC-UIDATA-025i refused table left channel unbound");
CHECK(ui_data_maint_bind(&s_maint_stub) == 0, "TC-UIDATA-025j complete table accepted");
CHECK(ui_data_maint_reset_req() == 0, "TC-UIDATA-025k bound table forwards");
CHECK(ui_data_maint_bind(&s_maint_broken) == -1, "TC-UIDATA-025l broken table refused again");
CHECK(ui_data_maint_reset_req() == 0, "TC-UIDATA-025m refusal keeps the good table active");
}
/*
* TC-UIDATA-026 注入后的转发言行(页面侧全部判据的地基):
* - clock_set(0) 是本层保留的"未初始化"哨兵,必须挡在下发之前(桩计数为 0)
* - reset_req **不得**自己投操作记录:谁触发谁投,这条留痕属页面;
* 在转发层偷投 = 同一次操作两个真源,也正是"远端那条已投过"会被重复投的形态
* - clear_state 返回值直通不改码(WAIT/FAIL 都是正常显示态)
* - oper_note(NULL) 不投递、不崩
*/
static void tc_maint_forwarding(void)
{
(void)ui_data_maint_bind(&s_maint_stub);
stub_reset_all();
CHECK(ui_data_maint_clock_set(0u) == -2, "TC-UIDATA-026a epoch 0 sentinel refused locally");
CHECK(s_stub_clock_calls == 0, "TC-UIDATA-026b sentinel never reaches executor");
s_stub_clock_rc = 0;
CHECK(ui_data_maint_clock_set(1792915200u) == 0, "TC-UIDATA-026c clock_set forwarded");
CHECK((s_stub_clock_calls == 1) && (s_stub_clock_last == 1792915200u),
"TC-UIDATA-026d executor got the exact epoch");
s_stub_clock_rc = -1;
CHECK(ui_data_maint_clock_set(1792915200u) == -1, "TC-UIDATA-026e executor failure propagates");
CHECK(ui_data_maint_reset_req() == 0, "TC-UIDATA-026f reset accepted");
CHECK(s_stub_reset_calls == 1, "TC-UIDATA-026g executor reset called once");
CHECK(s_stub_note_calls == 0, "TC-UIDATA-026h forwarding layer must NOT post the oper record");
(void)ui_data_maint_oper_note("force reset by panel");
CHECK((s_stub_note_calls == 1) && (strcmp(s_stub_note_last, "force reset by panel") == 0),
"TC-UIDATA-026i page-side note reaches executor verbatim");
/* 面板这条与远端 YK11 那条(App/protocol/iec104/iec104_pts.c 投的
* "force reset")必须是不同串,否则审计面再也分不出"主站下发的"还是
* "现场有人按了面板"。比的是执行侧缓冲里的实际内容,不是两个常量互比 */
CHECK(strcmp(s_stub_note_last, "force reset") != 0,
"TC-UIDATA-026s posted note differs from the remote YK11 wording");
CHECK(ui_data_maint_oper_note(NULL) == -1, "TC-UIDATA-026j NULL note refused");
CHECK(s_stub_note_calls == 1, "TC-UIDATA-026k NULL note not posted");
s_stub_reset_rc = -1;
CHECK(ui_data_maint_reset_req() == -1, "TC-UIDATA-026l rejected reset propagates");
s_stub_active = 3u;
CHECK(ui_data_maint_active_count() == 3u, "TC-UIDATA-026m active count forwarded (proof of reset)");
CHECK((ui_data_maint_clear_req(UI_W_REC_LOG_YK) == 0) && (s_stub_clear_last_id == UI_W_REC_LOG_YK),
"TC-UIDATA-026n clear req id passed through unchanged");
s_stub_clear_state_rc = UI_DATA_CLEAR_ST_WAIT;
CHECK(ui_data_maint_clear_state(UI_W_REC_LOG_OPER) == UI_DATA_CLEAR_ST_WAIT,
"TC-UIDATA-026o WAIT passed through (normal state, not failure)");
s_stub_clear_state_rc = UI_DATA_CLEAR_ST_FAIL;
CHECK(ui_data_maint_clear_state(UI_W_REC_LOG_OPER) == UI_DATA_CLEAR_ST_FAIL,
"TC-UIDATA-026p FAIL passed through unchanged");
CHECK(s_stub_clear_state_id == UI_W_REC_LOG_OPER, "TC-UIDATA-026q state queried the asked id");
CHECK(s_stub_clear_state_calls == 2, "TC-UIDATA-026r state queried once per call");
}
/* ── 道F task-7:通道五(遥测整定注入表)桩 ─────────────────────────────── */
static uint8_t s_yt_db_calls;
static uint8_t s_yt_db_last_idx;
static float s_yt_db_last_val;
static int s_yt_db_set_rc;
static uint8_t s_yt_cal_calls;
static uint8_t s_yt_cal_last_idx;
static float s_yt_cal_last_uv;
static int s_yt_cal_set_rc;
static uint8_t s_yt_blocked;
static uint8_t s_yt_pending_ret;
static int s_yt_pending_calls;
static uint8_t yt_stub_db_n(void)
{
return UI_DATA_YT_DB_N;
}
static int yt_stub_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out)
{
if (a_idx >= UI_DATA_YT_DB_N) {
return -1;
}
a_out->val = 0.125f + (float)a_idx;
a_out->lo = 0.0f;
a_out->hi = 50.0f;
a_out->unit = UI_DATA_YT_UNIT_V;
a_out->decimals = 3u;
return 0;
}
static int yt_stub_db_set(uint8_t a_idx, float a_eng)
{
s_yt_db_calls++;
s_yt_db_last_idx = a_idx;
s_yt_db_last_val = a_eng;
return s_yt_db_set_rc;
}
static uint8_t yt_stub_cal_n(void)
{
return UI_DATA_YT_CH_N;
}
static int yt_stub_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out)
{
if (a_idx >= UI_DATA_YT_CH_N) {
return -1;
}
a_out->val = 152.5f;
a_out->lo = 0.0f;
a_out->hi = 1000000000.0f;
a_out->unit = UI_DATA_YT_UNIT_UV;
a_out->decimals = 0u;
return 0;
}
static int yt_stub_cal_k_set(uint8_t a_idx, float a_uv)
{
s_yt_cal_calls++;
s_yt_cal_last_idx = a_idx;
s_yt_cal_last_uv = a_uv;
return s_yt_cal_set_rc;
}
static uint8_t yt_stub_blocked(void)
{
return s_yt_blocked;
}
static uint8_t yt_stub_pending(void)
{
s_yt_pending_calls++;
return s_yt_pending_ret;
}
static const ui_data_yt_ops_t s_yt_stub = {
yt_stub_db_n, yt_stub_db_row, yt_stub_db_set, yt_stub_cal_n,
yt_stub_cal_row, yt_stub_cal_k_set, yt_stub_blocked, yt_stub_pending
};
/* 残表:只缺最后一口(pending)——八口校验里最容易被"顺手少注一件"漏掉的那个 */
static const ui_data_yt_ops_t s_yt_broken = {
yt_stub_db_n, yt_stub_db_row, yt_stub_db_set, yt_stub_cal_n,
yt_stub_cal_row, yt_stub_cal_k_set, yt_stub_blocked, NULL
};
/*
* TC-UIDATA-027 未注入防御(025 的第五张表复刻):八个口一律安全返回;
* db_row/cal_row 未注入时**不得碰出参**(页面拿栈上未初始化结构当现状读,
* 比返回 -1 更坏——这里用预填哨兵钉住"一个字节都没写")。
*/
static void tc_yt_not_bound(void)
{
ui_data_yt_row_t r;
float sentinel = -777.0f;
(void)ui_data_yt_bind(NULL);
r.val = sentinel;
CHECK(ui_data_yt_db_n() == 0u, "TC-UIDATA-027a db_n not bound is 0");
CHECK(ui_data_yt_db_row(0u, &r) == -1, "TC-UIDATA-027b db_row not bound is -1");
CHECK(r.val == sentinel, "TC-UIDATA-027c db_row not bound wrote nothing");
r.val = sentinel;
CHECK(ui_data_yt_cal_row(0u, &r) == -1, "TC-UIDATA-027d cal_row not bound is -1");
CHECK(r.val == sentinel, "TC-UIDATA-027e cal_row not bound wrote nothing");
CHECK(ui_data_yt_cal_n() == 0u, "TC-UIDATA-027f cal_n not bound is 0");
CHECK(ui_data_yt_db_set(0u, 1.0f) == -1, "TC-UIDATA-027g db_set not bound is -1");
CHECK(ui_data_yt_cal_k_set(0u, 1.0f) == -1, "TC-UIDATA-027h cal_k_set not bound is -1");
CHECK(ui_data_yt_cal_blocked() == 0u, "TC-UIDATA-027i blocked not bound is 0 (never fabricate)");
CHECK(ui_data_yt_pending() == 0u, "TC-UIDATA-027j pending not bound is 0");
CHECK(ui_data_yt_db_row(0u, NULL) == -1, "TC-UIDATA-027k NULL out refused even bound-or-not");
CHECK(ui_data_yt_bind(&s_yt_broken) == -1, "TC-UIDATA-027l incomplete table refused");
CHECK(ui_data_yt_db_n() == 0u, "TC-UIDATA-027m refused table left channel unbound");
CHECK(ui_data_yt_bind(&s_yt_stub) == 0, "TC-UIDATA-027n complete table accepted");
CHECK(ui_data_yt_db_n() == UI_DATA_YT_DB_N, "TC-UIDATA-027o bound table forwards count");
CHECK(ui_data_yt_bind(&s_yt_broken) == -1, "TC-UIDATA-027p broken refused again");
CHECK(ui_data_yt_db_n() == UI_DATA_YT_DB_N, "TC-UIDATA-027q refusal keeps the good table active");
}
/*
* TC-UIDATA-028 转发言行(两张页面全部判据的地基):
* - 行现状逐字段原样上抛(val/lo/hi/unit/decimals 五样一个不许"顺手加工"——
* 合法域是执行侧事实,转发层改一个数就是第二真源)
* - 写口 idx 与工程量原样下传,执行侧返回码直通(0/-1 两态,页面按 ERR 显示)
* - pending 无状态直读:本层每问一次转发一次(calls 增长)、桩改什么口径立刻
* 变什么口径——页面锁存的合法性建立在"本层不缓存"之上
* - 越界 idx 由执行侧判(桩返 -1 直通),本层不抄一份行数守卫
*/
static void tc_yt_forwarding(void)
{
ui_data_yt_row_t r;
(void)ui_data_yt_bind(&s_yt_stub);
s_yt_db_calls = 0u; s_yt_cal_calls = 0u; s_yt_pending_calls = 0;
s_yt_db_set_rc = 0; s_yt_cal_set_rc = 0;
CHECK(ui_data_yt_db_row(2u, &r) == 0, "TC-UIDATA-028a db_row forwarded");
CHECK((r.val == (0.125f + 2.0f)) && (r.lo == 0.0f) && (r.hi == 50.0f) &&
(r.unit == UI_DATA_YT_UNIT_V) && (r.decimals == 3u),
"TC-UIDATA-028b row fields pass through verbatim (no second source of truth)");
CHECK(ui_data_yt_db_row(UI_DATA_YT_DB_N, &r) == -1, "TC-UIDATA-028c executor-side oob idx propagates");
CHECK((ui_data_yt_db_set(3u, 0.25f) == 0), "TC-UIDATA-028d db_set accepted");
CHECK((s_yt_db_calls == 1u) && (s_yt_db_last_idx == 3u) && (s_yt_db_last_val == 0.25f),
"TC-UIDATA-028e executor got exact idx and engineering value");
s_yt_db_set_rc = -1;
CHECK(ui_data_yt_db_set(3u, 0.25f) == -1, "TC-UIDATA-028f executor rejection propagates");
CHECK((ui_data_yt_cal_n() == UI_DATA_YT_CH_N) && (ui_data_yt_cal_row(7u, &r) == 0) &&
(r.unit == UI_DATA_YT_UNIT_UV) && (r.hi == 1000000000.0f) && (r.decimals == 0u),
"TC-UIDATA-028g cal row forwards (coarse-guard hi verbatim)");
s_yt_cal_set_rc = 0;
CHECK(ui_data_yt_cal_k_set(7u, 153.0f) == 0, "TC-UIDATA-028h cal_k_set accepted");
CHECK((s_yt_cal_calls == 1u) && (s_yt_cal_last_idx == 7u) && (s_yt_cal_last_uv == 153.0f),
"TC-UIDATA-028i cal executor got exact idx and uv value");
s_yt_cal_set_rc = -1;
CHECK(ui_data_yt_cal_k_set(7u, 0.0f) == -1, "TC-UIDATA-028j cal rejection propagates (k<=0 leg)");
s_yt_blocked = 1u;
CHECK(ui_data_yt_cal_blocked() == 1u, "TC-UIDATA-028k blocked badge forwarded");
s_yt_blocked = 0u;
CHECK(ui_data_yt_cal_blocked() == 0u, "TC-UIDATA-028l badge clear follows executor immediately");
s_yt_pending_ret = 1u;
CHECK(ui_data_yt_pending() == 1u, "TC-UIDATA-028m pending=dirty forwarded (WAIT leg)");
s_yt_pending_ret = 0u;
CHECK(ui_data_yt_pending() == 0u, "TC-UIDATA-028n pending follows to clean (OK leg, zero cache)");
CHECK(s_yt_pending_calls == 2, "TC-UIDATA-028o every question reaches executor (stateless layer)");
(void)ui_data_yt_bind(NULL);
CHECK(ui_data_yt_pending() == 0u, "TC-UIDATA-028p unbind restores safe defaults");
/* 未注入不得把问题漏给桩:计数不涨 = 撤销后桩彻底脱离(028o 只数了绑定期的
* 两次,这条把"撤销后一次都不到"补成闭环) */
CHECK(s_yt_pending_calls == 2, "TC-UIDATA-028q unbind stops reaching executor entirely");
}
/* ===================== 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("-- clock convert --\n");
tc_clock_convert();
printf("-- clock ts string --\n");
tc_clock_string();
printf("-- record id order contract --\n");
tc_record_codes();
printf("-- maint channel: not bound --\n");
tc_maint_not_bound();
printf("-- maint channel: forwarding --\n");
tc_maint_forwarding();
printf("-- yt channel: not bound --\n");
tc_yt_not_bound();
printf("-- yt channel: forwarding --\n");
tc_yt_forwarding();
printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}