728d60675f
三条道在同一棵树上交付,故合并成**一次权威构建**(@build 0 错 0 警,Code=235236 RO=26308 RW=1572
ZI=136924)后同笔入库;台账已按该构建回填(`ram_baseline.py --update` + `--write`、`chec
k_mem_numbers.py
--write`),凭证与实际 .map 同源。归属如下:
· **通讯设置页**(道G task-9):新增 `Drivers/BSP/UI/ui_page_comm.{c,h}`(通道六注入,与死
区页同骨架同纪
律),`#page=comm` 绑定使 bound **21 → 22**、内容页黄金 **47 → 53**(旧 47 页字节零变化,
新增六态含
WAIT/OK 两张);`iec104_comm` 脏位 + store_save(**无事件位**)挂 defaultTask 同点位
轮询;`port_104` 由
宏改走 getter、`IEC104_SERV_PORT` 删除;字体 +听/监(185 字全覆)。**实机链已闭环**:FTP 外改
[comm]
的 ca/port → 复位 → 串口出现 `section 'comm' (2 keys)` 与 `i104: listening o
n 9004` → 还原 2404,全程无
FAIL ⇒「重启即生效 + 端口真值」不是推断是实测。上限核对 6/8 段、2/16 键,超限现象由 TC-CONFIG-056/057
钉死(注册 -1 + `comm init: FAIL` 锚 + getter 回 ROM),**不抬宏**。
· **开机账本**(道F task-12):新增 `App/util/boot_stats.{c,h}`——上电次数落 SD2506 用户
SRAM(magic +
小端计数 + 版本 + 异或校验),一次上电 = 1 读 + ≤1 写,**零 NAND、零卷锁、零配置段**;四态互斥
UNKNOWN / FRESH / CONTINUED / RESTARTED(BLF=1 才算电池域掉过电),**读失败绝不回写**;
文案口径「自 RTC
电池周期起」,**禁写终身次数**。复位原因优先级链从 `app_main.c` 的 if-else 收进一份真源(串口与屏幕同
源);104 四个只增不清计数 + `iec104_serv_get_stats()` 留在 serv、未塞进 link(会被 open
/close 洗掉)。
运行时长按用户裁定 = **只报本次开机以来**,零新增代码。新套件 `test/host/boot_stats` 21/21,五处注入
**全部有牙**(含「漂一字节被校验和挡下而不是报 256 倍错账」)。
· **adc_cal 可诊断化**(道E task-43):三个 volatile 计数 + `adc_cal_get_stats()`
/ `adc_cal_note_persisted()`
(水位**盖写非自增**,因为一次 save 覆盖整档,数 save 会留假未落盘);「差额 × 脏位」三态语义唯一落在
`adc_cal.h`,其中「差额 > 0 且脏位恒 0」= 绕过语义 setter 的静默丢失;TC-ADCCAL-019~023。
三条**必须留档的自纠**(都是执行道自己报的,我不替他们润色):
① stdint 那条断链是**宿主套件抓到、通讯道先修**,采集道承认自己是重复添加者并删掉自己那份;
② 「exit 2 不是证据,断言变红才是」——采集道第一版反证注入了未声明标识符导致编译错,差点当成功;
③ 套件汇总行原写 `s_total/s_total`,失败时会打印「21/21 PASSED (failed=1)」这种自相矛盾的行,已改。
另修:`check_registration.py` 的 `SIM_EXEMPT` 加一行豁免 `App/util/boot_stats.c
`(依赖 I2C 与 RCC 位掩码,
MCU 专属;核过 `sys_clock.c`/`sha256.c` 同形态先例),注册反向校验 exit 1 → 0。发号 TC-IEC1
04-034、
TC-UIDATA-029 已实抬游标(`TC-IEC104-033→034`、`TC-UIDATA-026→029`)。
门禁:id-check --strict 与 plain 均 rc=0、doc-links --all rc=0、注册校验 0、cppche
ck / coding-standard /
bom-drift 0;iec104 179/179、ui_data 295/295、boot_stats 21/21、adc_cal 11
7/117、run_par 67/67;菜单 8 状态
池 5368 不变、内容页 53/53。**已知遗留**:内容页数 47 → 53 的全仓文本同步尚未做(另卡跟踪),本笔只保
证代码与黄金一致;面板注键真机那一腿缺 CLI,交台架道补。
1648 lines
78 KiB
C
1648 lines
78 KiB
C
/*
|
||
* 模块名称: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-029 通讯参数通道六(道G task-9):未注入防御 + 注入后转发
|
||
* (行现状逐字段 / 带外拒不置脏 / 脏位零缓存跟随 / 撤销脱离)
|
||
* 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");
|
||
}
|
||
|
||
/* ── 道G task-9 通道六:通讯参数注入(027/028 的第五张表复刻到第六张表)──
|
||
* 编号 029 = 向 Lead 申请发号(cursor_owner_only),cursor 现 026。
|
||
* 假件语义 = simulator/main.c 同形(带内写副本、带外拒、写成功置脏)。 */
|
||
static uint32_t s_cf_val[2] = { 1u, 2404u };
|
||
static uint32_t s_cf_lo[2] = { 1u, 1024u };
|
||
static uint32_t s_cf_hi[2] = { 65535u, 65535u };
|
||
static uint8_t s_cf_dirty;
|
||
static uint8_t s_cf_last_idx = 0xFFu;
|
||
static uint32_t s_cf_last_val;
|
||
static int s_cf_calls;
|
||
|
||
static int cf_stub_row(uint8_t a_idx, ui_data_comm_row_t *a_out)
|
||
{
|
||
s_cf_calls++;
|
||
if (a_idx >= UI_DATA_COMM_ROW_N) {
|
||
return -1;
|
||
}
|
||
a_out->val = s_cf_val[a_idx];
|
||
a_out->lo = s_cf_lo[a_idx];
|
||
a_out->hi = s_cf_hi[a_idx];
|
||
return 0;
|
||
}
|
||
|
||
static int cf_stub_set(uint8_t a_idx, uint32_t a_val)
|
||
{
|
||
s_cf_calls++;
|
||
s_cf_last_idx = a_idx;
|
||
s_cf_last_val = a_val;
|
||
if ((a_idx >= UI_DATA_COMM_ROW_N) || (a_val < s_cf_lo[a_idx]) ||
|
||
(a_val > s_cf_hi[a_idx])) {
|
||
return -1;
|
||
}
|
||
s_cf_val[a_idx] = a_val;
|
||
s_cf_dirty = 1u;
|
||
return 0;
|
||
}
|
||
|
||
static uint8_t cf_stub_pending(void)
|
||
{
|
||
s_cf_calls++;
|
||
return s_cf_dirty;
|
||
}
|
||
|
||
static const ui_data_comm_ops_t s_cf_stub = {
|
||
cf_stub_row, cf_stub_set, cf_stub_pending
|
||
};
|
||
|
||
/* 残表:只缺 pending——三口里最容易被"顺手少注一件"漏掉的那个(027l 同法) */
|
||
static const ui_data_comm_ops_t s_cf_broken = {
|
||
cf_stub_row, cf_stub_set, NULL
|
||
};
|
||
|
||
/* TC-UIDATA-029a-e 未注入防御:三个口一律安全返回、不碰出参、不假报脏 */
|
||
static void tc_comm_ch6_not_bound(void)
|
||
{
|
||
ui_data_comm_row_t r;
|
||
const uint32_t sentinel = 0xA5A5A5A5uL;
|
||
|
||
(void)ui_data_comm_bind(NULL);
|
||
r.val = sentinel;
|
||
CHECK(ui_data_comm_row(0u, &r) == -1, "TC-UIDATA-029a row not bound is -1");
|
||
CHECK(r.val == sentinel, "TC-UIDATA-029b row not bound wrote nothing");
|
||
CHECK(ui_data_comm_set(0u, 2u) == -1, "TC-UIDATA-029c set not bound is -1");
|
||
CHECK(ui_data_comm_pending() == 0u, "TC-UIDATA-029d pending not bound is 0 (never fabricate WAIT)");
|
||
CHECK(ui_data_comm_bind(&s_cf_broken) == -1, "TC-UIDATA-029e incomplete table refused");
|
||
}
|
||
|
||
/* TC-UIDATA-029f-o 注入后转发言行:现状逐字段原样上抛、写口 idx/值精确送达、
|
||
* 脏位零缓存跟随(页面 WAIT/OK 的正确性全系于这两条) */
|
||
static void tc_comm_ch6_forwarding(void)
|
||
{
|
||
ui_data_comm_row_t r;
|
||
|
||
CHECK(ui_data_comm_bind(&s_cf_stub) == 0, "TC-UIDATA-029f complete table accepted");
|
||
CHECK(ui_data_comm_row(UI_DATA_COMM_ROW_PORT, &r) == 0, "TC-UIDATA-029g row forwarded");
|
||
CHECK((r.val == 2404u) && (r.lo == 1024u) && (r.hi == 65535u),
|
||
"TC-UIDATA-029h row fields copied verbatim (band = executor's, not a copy)");
|
||
CHECK(ui_data_comm_row(UI_DATA_COMM_ROW_N, &r) == -1,
|
||
"TC-UIDATA-029i out-of-range idx rejected at the port too");
|
||
CHECK(ui_data_comm_row(0u, NULL) == -1, "TC-UIDATA-029j NULL out refused even bound");
|
||
CHECK(ui_data_comm_set(UI_DATA_COMM_ROW_CA, 0u) == -1,
|
||
"TC-UIDATA-029k out-of-band write rejected (band owned by executor)");
|
||
CHECK(s_cf_dirty == 0u, "TC-UIDATA-029l rejection left the section clean");
|
||
CHECK(ui_data_comm_set(UI_DATA_COMM_ROW_CA, 7u) == 0, "TC-UIDATA-029m in-band write accepted");
|
||
CHECK((s_cf_last_idx == UI_DATA_COMM_ROW_CA) && (s_cf_last_val == 7u),
|
||
"TC-UIDATA-029n executor got exact idx and value");
|
||
CHECK(ui_data_comm_pending() == 1u, "TC-UIDATA-029o dirty forwarded (WAIT leg)");
|
||
s_cf_dirty = 0u;
|
||
CHECK(ui_data_comm_pending() == 0u, "TC-UIDATA-029p clean follows immediately (OK leg, zero cache)");
|
||
(void)ui_data_comm_bind(NULL);
|
||
CHECK(ui_data_comm_pending() == 0u, "TC-UIDATA-029q unbind restores safe defaults");
|
||
s_cf_dirty = 1u; /* 桩此刻仍脏:撤销后连问都不该问到桩 */
|
||
CHECK(ui_data_comm_pending() == 0u, "TC-UIDATA-029r unbind stops reaching executor entirely");
|
||
s_cf_dirty = 0u;
|
||
s_cf_val[0] = 1u; /* 复位假件,别给后面的用例留串台状态 */
|
||
}
|
||
|
||
/* ===================== 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();
|
||
|
||
tc_comm_ch6_not_bound(); /* 道G task-9 通道六(TC-UIDATA-029) */
|
||
tc_comm_ch6_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;
|
||
}
|