feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐
UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机 OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批 预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block (口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng 反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定 内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按 is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。 记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在 defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE); 事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身 (一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。 为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核, 丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。 为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费 两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过; RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同 码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。 实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B; RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS, protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→ 164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34, 既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、 不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页 连跑两次逐字节一致;字库 diff 空。 顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽 16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前 靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值 全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py 模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。 未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行; LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND), 已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
This commit is contained in:
@@ -24,6 +24,11 @@
|
||||
* TC-IEC104-011 t2 到期发 S
|
||||
* TC-IEC104-012 t3 空闲发 TESTFR act;未答 t1 超时 → IDLE;con 清挂起不误杀
|
||||
* TC-IEC104-013 遥控两段式:选择/执行成功;失配 PN;拒绝类选择段即 PN
|
||||
* TC-IEC104-030 遥控 / 操作记录投递(道F task-10 的 104 侧写入点):未知 IOA、
|
||||
* YK11 选择与执行、YK12 选择 / 执行 / 非法 DPC 各投哪一类哪一句,外加
|
||||
* "拒绝不投操作记录""记录与压板实际状态一致""文案不超 28 字节"三条会
|
||||
* 静默变坏但台架看不出的点(YK12 的 set_sealed 失败腿不可达,理由写在
|
||||
* 该用例注释里,没有为它伪造定值范围)
|
||||
* TC-IEC104-014 定值两段写(真实 prot_set):越界 PN;执行后 get_f + selfcheck
|
||||
* TC-IEC104-015 C_RD:YC 命中 / 定值命中 / 未知 IOA COT14+EVI
|
||||
* TC-IEC104-016 C_CS:clock_sync 收正确 epoch;IV → PN;clock_sync 失败 → PN
|
||||
@@ -148,6 +153,65 @@ void syslog_post(syslog_id_t id, const char *desc) {
|
||||
rp_s_syslog_count++;
|
||||
}
|
||||
|
||||
/* ---- syslog_post_quiet 桩:记下投了什么(task-10 遥控 / 操作记录用) ----
|
||||
* 真机上 quiet 与 post 的差别只在"投递侧不打日志",收/丢判据逐条相同;
|
||||
* 本套件是协议栈单测,不验投递本身(那归 test/host/sys_log_mcu),只验
|
||||
* **板端源在每条返回路径上投了哪一类、哪一句**——所以这里存成环形日志。 */
|
||||
#define RP_REC_MAX 16u
|
||||
static uint8_t rp_s_rec_id[RP_REC_MAX];
|
||||
static char rp_s_rec_desc[RP_REC_MAX][SYSLOG_DESC_MAX + 1u];
|
||||
static uint32_t rp_s_rec_n; /* 累计条数(不回绕,供"一条都没多投"断言) */
|
||||
static uint32_t rp_s_drop_quiet; /* 本桩不模拟丢弃,恒 0 */
|
||||
|
||||
void syslog_post_quiet(syslog_id_t id, const char *desc) {
|
||||
uint32_t slot;
|
||||
size_t dl = 0u;
|
||||
|
||||
if (desc == NULL) {
|
||||
return;
|
||||
}
|
||||
slot = rp_s_rec_n % RP_REC_MAX;
|
||||
rp_s_rec_id[slot] = (uint8_t)id;
|
||||
while ((dl < SYSLOG_DESC_MAX) && (desc[dl] != '\0')) {
|
||||
dl++;
|
||||
}
|
||||
(void)memcpy(rp_s_rec_desc[slot], desc, dl);
|
||||
rp_s_rec_desc[slot][dl] = '\0';
|
||||
rp_s_rec_n++;
|
||||
}
|
||||
|
||||
uint32_t syslog_post_dropped(void) { return rp_s_drop_quiet; }
|
||||
|
||||
/* 第 k 条(0 起)记录;越界返回 -1 */
|
||||
static int rp_rec_at(uint32_t k, syslog_id_t id, const char *desc)
|
||||
{
|
||||
if (k >= rp_s_rec_n) {
|
||||
return -1;
|
||||
}
|
||||
return (rp_s_rec_id[k % RP_REC_MAX] == (uint8_t)id)
|
||||
&& (strcmp(rp_s_rec_desc[k % RP_REC_MAX], desc) == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 某类记录一共投了几条 */
|
||||
static uint32_t rp_rec_cnt(syslog_id_t id)
|
||||
{
|
||||
uint32_t i, n = 0u;
|
||||
|
||||
for (i = 0u; (i < rp_s_rec_n) && (i < RP_REC_MAX); i++) {
|
||||
if (rp_s_rec_id[i] == (uint8_t)id) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void rp_rec_clear(void)
|
||||
{
|
||||
rp_s_rec_n = 0u;
|
||||
(void)memset(rp_s_rec_id, 0, sizeof(rp_s_rec_id));
|
||||
(void)memset(rp_s_rec_desc, 0, sizeof(rp_s_rec_desc));
|
||||
}
|
||||
|
||||
/* ---- adc_input_desc 桩:V0.1 权威映射(docs/硬件平台/ADC通道映射说明.md) ---- */
|
||||
#include "adc_input.h"
|
||||
|
||||
@@ -809,6 +873,93 @@ static void tc_dc_yk12(void)
|
||||
"TC-IEC104-024 an invalid DPC returns PN in the select phase");
|
||||
}
|
||||
|
||||
/* TC-IEC104-030(道F task-10):遥控记录——板端默认源 s_src_yk_exec() 的三条返回
|
||||
* 路径各投哪一类记录、投几句、是否顺手投了操作记录。
|
||||
* 为什么直接调 s_src_yk_exec 而不是发 ASDU:本套件绑定的是**假源** f_yk,
|
||||
* YK11 / 未知 IOA 在 f_yk 里就被短路(只有 YK12 转发到真板端源,见 f_yk),
|
||||
* 走 ASDU 反而测不到要验的那两条腿;"ASDU -> link -> pts -> 源"这条转发链
|
||||
* 已由 TC-IEC104-013/024 覆盖,不重复。**投递本身**(quiet 通道的收/丢)归
|
||||
* test/host/sys_log_mcu,本用例只钉"哪条路径投了哪一条"。
|
||||
* 覆盖不到的一条腿(登记,不假装):YK12 的 rc!=0 分支要 prot_set_set_sealed
|
||||
* 返 -1 才进得去,而 PS_GLOBAL_EN 的 min/max 恰好就是本函数会写的 0/1 两值
|
||||
* → 该产品路径上不可达,是防御性分支;不为它伪造定值范围(那是改判据)。 */
|
||||
static void tc_yk_records(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
|
||||
reset_all(now);
|
||||
rp_rec_clear();
|
||||
|
||||
/* 1) 未知 IOA:拒绝,只投一条遥控记录;拒绝没改动任何东西 -> 不该有操作记录 */
|
||||
CHECK(s_src_yk_exec(0x6000u, (uint8_t)IEC104_DPC_ON, 1u) == -1,
|
||||
"TC-IEC104-030 an IOA without output hardware is still rejected");
|
||||
CHECK((rp_s_rec_n == 1u) && (rp_rec_at(0u, SYSLOG_YK, "YK unknown ioa") == 1),
|
||||
"TC-IEC104-030 the reject path posts exactly one telecontrol record");
|
||||
CHECK(rp_rec_cnt(SYSLOG_OPER) == 0u,
|
||||
"TC-IEC104-030 a rejected command posts no operation record");
|
||||
|
||||
/* 2) YK11 选择段:受理但未动作 -> 只有遥控记录 */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Au, (uint8_t)IEC104_DPC_ON, 1u) == 0)
|
||||
&& (rp_s_rec_n == 1u) && (rp_rec_at(0u, SYSLOG_YK, "YK11 select") == 1),
|
||||
"TC-IEC104-030 the YK11 select phase posts a select record and acts on nothing");
|
||||
CHECK(rp_rec_cnt(SYSLOG_OPER) == 0u,
|
||||
"TC-IEC104-030 the select phase is not an operation yet");
|
||||
|
||||
/* 3) YK11 执行段:遥控记录 + 操作记录各一条,顺序 = 先命令后动作 */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Au, (uint8_t)IEC104_DPC_ON, 0u) == 0)
|
||||
&& (rp_s_rec_n == 2u)
|
||||
&& (rp_rec_at(0u, SYSLOG_YK, "YK11 exec") == 1)
|
||||
&& (rp_rec_at(1u, SYSLOG_OPER, "force reset") == 1),
|
||||
"TC-IEC104-030 the YK11 execute phase posts one telecontrol plus one operation record");
|
||||
|
||||
/* 4) YK12 非法双点值:拒绝 + 一条带原因的遥控记录,压板不动 */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Bu, 0u, 0u) == -1) && (rp_s_rec_n == 1u)
|
||||
&& (rp_rec_at(0u, SYSLOG_YK, "YK12 bad dpc") == 1),
|
||||
"TC-IEC104-030 an invalid DPC posts the reason and changes nothing");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u,
|
||||
"TC-IEC104-030 the invalid DPC leaves the soft plate engaged");
|
||||
|
||||
/* 5) YK12 执行 OFF:遥控 off + 操作 plate out,且压板真的退出了(记录与事实同步) */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Bu, (uint8_t)IEC104_DPC_OFF, 0u) == 0)
|
||||
&& (rp_s_rec_n == 2u)
|
||||
&& (rp_rec_at(0u, SYSLOG_YK, "YK12 exec off") == 1)
|
||||
&& (rp_rec_at(1u, SYSLOG_OPER, "plate out") == 1),
|
||||
"TC-IEC104-030 releasing the soft plate records both the command and the operation");
|
||||
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u,
|
||||
"TC-IEC104-030 the record agrees with the plate state the settings layer holds");
|
||||
|
||||
/* 6) YK12 执行 ON:对称的另一半(只测 OFF 的话,两条 desc 写反也发现不了) */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Bu, (uint8_t)IEC104_DPC_ON, 0u) == 0)
|
||||
&& (rp_rec_at(0u, SYSLOG_YK, "YK12 exec on") == 1)
|
||||
&& (rp_rec_at(1u, SYSLOG_OPER, "plate in") == 1),
|
||||
"TC-IEC104-030 engaging the soft plate records the matching pair");
|
||||
|
||||
/* 7) 选择段不落地:YK12 select 只投一条,压板状态不变 */
|
||||
rp_rec_clear();
|
||||
CHECK((s_src_yk_exec(0x600Bu, (uint8_t)IEC104_DPC_OFF, 1u) == 0)
|
||||
&& (rp_s_rec_n == 1u) && (rp_rec_at(0u, SYSLOG_YK, "YK12 select") == 1)
|
||||
&& (prot_set_get_ctl(PS_GLOBAL_EN) == 1u),
|
||||
"TC-IEC104-030 the YK12 select phase records the intent without changing the plate");
|
||||
|
||||
/* 8) 文案长度契约:desc 全是 ROM 常量,任何一条超过 SYSLOG_DESC_MAX 都会在
|
||||
* 投递侧被静默截断(屏上少几个字,没人会去数)-> 在单测里钉住。 */
|
||||
CHECK((strlen(YK_REC_UNKNOWN) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK11_EXEC) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK12_OFF) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK12_FAIL) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_DPC_BAD) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK12_SEL) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK11_SEL) <= SYSLOG_DESC_MAX)
|
||||
&& (strlen(YK_REC_YK12_ON) <= SYSLOG_DESC_MAX),
|
||||
"TC-IEC104-030 every telecontrol description fits the 28-byte record field");
|
||||
rp_rec_clear();
|
||||
}
|
||||
|
||||
static void tc_se(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
@@ -1283,6 +1434,7 @@ int main(void)
|
||||
tc_t3_t1();
|
||||
tc_dc();
|
||||
tc_dc_yk12();
|
||||
tc_yk_records();
|
||||
tc_se();
|
||||
tc_rd();
|
||||
tc_cs();
|
||||
|
||||
@@ -28,10 +28,17 @@
|
||||
* 否则对端会以为设备时间是 1970-01-01。
|
||||
* 4. 活性分频(每 32 轮才探一次)是 CPU 预算的取舍:写错成每轮都探,就把 100ms
|
||||
* 轮询变成了每轮两次系统调用;写错成分频相位,TESTFR 会迟迟不发。
|
||||
* 5. 操作记录是**审计面**(道F task-19):只要"投了"而不管"改成功了没有",屏上的
|
||||
* 操作记录页就会出现从没发生过的对时;越界拒绝与 RTC 写失败这两条腿必须
|
||||
* 一条都不投,而这只有能注入 sys_clock_set_unix 的返回值才测得到。
|
||||
* 同时钉住"用 quiet 口不用 post 口"——post 的两个满态留痕分支各带约 216 B
|
||||
* 内联行缓冲,iec104Task 的静态最坏链放不下它(台架与单测都看不出这个差别,
|
||||
* 只有 stack_report 会响,所以先在源码侧钉住)。
|
||||
*
|
||||
* 覆盖点:TC-I104SRV-001..004 建监听;010..013 发送与连续短写判死;020..023 时钟
|
||||
* 桥接;030..033 代次重建与 accept 门控;034..036 活性分频与对端 FIN;037..041
|
||||
* 错误丢弃与协议掉线重生。
|
||||
* 错误丢弃与协议掉线重生;050..053 对时的操作记录(道F task-19:成功才记、
|
||||
* 越界拒绝与写钟失败两条腿都不记、每次对时各记一条、且只走 quiet 投递口)。
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
@@ -194,6 +201,61 @@ static int g_wd_beats;
|
||||
static int g_wd_last_id;
|
||||
static uint32_t g_now;
|
||||
|
||||
/* ===================== sys_log 桩(task-19 操作记录) =====================
|
||||
* 两个投递口都抓下来:quiet 是 iec104Task 该用的那个(本任务栈深,不许在调用链上
|
||||
* 内联日志行缓冲),post 一旦在这里被调到就是"绕回旧口",053 会红。
|
||||
* 投递本身(收/丢/计数)归 test/host/sys_log_mcu,本套只钉"哪条腿投了哪一句"。 */
|
||||
#include "sys_log.h"
|
||||
|
||||
#define RP_I4_REC_MAX 8u
|
||||
static uint8_t rp_i4_id[RP_I4_REC_MAX];
|
||||
static char rp_i4_desc[RP_I4_REC_MAX][SYSLOG_DESC_MAX + 1u];
|
||||
static uint32_t rp_i4_n; /* quiet 通道累计(不回绕,供"恰好几条"断言) */
|
||||
static uint32_t rp_i4_post_n; /* 误用 syslog_post 的次数,期望恒 0 */
|
||||
|
||||
static void rp_i4_rec_clear(void)
|
||||
{
|
||||
rp_i4_n = 0u;
|
||||
rp_i4_post_n = 0u;
|
||||
(void)memset(rp_i4_id, 0, sizeof(rp_i4_id));
|
||||
(void)memset(rp_i4_desc, 0, sizeof(rp_i4_desc));
|
||||
}
|
||||
|
||||
void syslog_post_quiet(syslog_id_t id, const char *desc)
|
||||
{
|
||||
uint32_t slot = rp_i4_n % RP_I4_REC_MAX;
|
||||
size_t dl = 0u;
|
||||
|
||||
if (desc == NULL) {
|
||||
return;
|
||||
}
|
||||
while ((dl < SYSLOG_DESC_MAX) && (desc[dl] != '\0')) {
|
||||
dl++;
|
||||
}
|
||||
(void)memcpy(rp_i4_desc[slot], desc, dl);
|
||||
rp_i4_desc[slot][dl] = '\0';
|
||||
rp_i4_id[slot] = (uint8_t)id;
|
||||
rp_i4_n++;
|
||||
}
|
||||
|
||||
void syslog_post(syslog_id_t id, const char *desc)
|
||||
{
|
||||
(void)id;
|
||||
(void)desc;
|
||||
rp_i4_post_n++;
|
||||
}
|
||||
|
||||
uint32_t syslog_post_dropped(void) { return 0u; }
|
||||
|
||||
static int rp_i4_at(uint32_t k, syslog_id_t id, const char *desc)
|
||||
{
|
||||
if (k >= rp_i4_n) {
|
||||
return 0;
|
||||
}
|
||||
return (rp_i4_id[k % RP_I4_REC_MAX] == (uint8_t)id)
|
||||
&& (strcmp(rp_i4_desc[k % RP_I4_REC_MAX], desc) == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
uint32_t sys_clock_get(void) { return g_clock_val; }
|
||||
int sys_clock_set_unix(uint32_t unix_secs)
|
||||
{
|
||||
@@ -250,6 +312,7 @@ static void reset_all(void)
|
||||
g_lk_state = IEC104_ST_RUN;
|
||||
g_pts_init = 0;
|
||||
g_clock_val = 1700000000u;
|
||||
rp_i4_rec_clear();
|
||||
g_clock_set_rc = 0;
|
||||
g_clock_last_secs = 0u;
|
||||
g_clock_set_calls = 0;
|
||||
@@ -367,6 +430,38 @@ static void tc_clock(void)
|
||||
reset_all();
|
||||
g_clock_val = 1700000123u;
|
||||
CHECK(s_epoch_ms(NULL) == 1700000123000ULL, "TC-I104SRV-023 with a synced clock the answer is in milliseconds");
|
||||
|
||||
/* ---- task-19:对时 = 操作记录(审计面只记真发生过的动作) ---- */
|
||||
reset_all();
|
||||
(void)s_clock_sync(NULL, 1700000123000ULL);
|
||||
CHECK((rp_i4_n == 1u) && (rp_i4_at(0u, SYSLOG_OPER, "clock set by 104") == 1),
|
||||
"TC-I104SRV-050 a successful sync books exactly one operation record naming the source");
|
||||
|
||||
reset_all();
|
||||
(void)s_clock_sync(NULL, 0x100000000ULL * 1000ULL);
|
||||
CHECK((rp_i4_n == 0u) && (g_clock_set_calls == 0),
|
||||
"TC-I104SRV-050b the out-of-range rejection books nothing (an audit record for something that never happened is worse than none)");
|
||||
|
||||
reset_all();
|
||||
g_clock_set_rc = -1; /* 写钟失败(RTC 回写失败那一类) */
|
||||
CHECK(s_clock_sync(NULL, 1700000123000ULL) == -1,
|
||||
"TC-I104SRV-051 a failed clock write still answers the link layer with -1");
|
||||
CHECK(rp_i4_n == 0u,
|
||||
"TC-I104SRV-051b a failed clock write books no operation record");
|
||||
|
||||
reset_all();
|
||||
(void)s_clock_sync(NULL, 1700000123000ULL);
|
||||
(void)s_clock_sync(NULL, 1700000456000ULL);
|
||||
CHECK((rp_i4_n == 2u) && (rp_i4_at(1u, SYSLOG_OPER, "clock set by 104") == 1),
|
||||
"TC-I104SRV-052 every re-sync is its own operation record (no dedup, no latching)");
|
||||
|
||||
/* 053:用错投递口的形状——iec104Task 是深链任务,只许用 quiet。
|
||||
* 顺带量文案长度:超过 SYSLOG_DESC_MAX 会在投递侧被静默截断。 */
|
||||
reset_all();
|
||||
(void)s_clock_sync(NULL, 1700000123000ULL);
|
||||
CHECK((rp_i4_post_n == 0u) && (rp_i4_n == 1u)
|
||||
&& (strlen("clock set by 104") <= SYSLOG_DESC_MAX),
|
||||
"TC-I104SRV-053 the record goes out over the stack-safe channel only, and its text fits the record field");
|
||||
}
|
||||
|
||||
/* ===================== 代次重建与 accept 门控 ===================== */
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
* TC-PROTELEM-012 低频 UF(B8/L3-5):欠量方向 + RET_UF 滞回 + 方向隔离
|
||||
* TC-PROTELEM-013 run() 接线(B8):测频驱动 OF 动作;幅值门失效 OF 复位且
|
||||
* 相量类元件仍被评估(不整拍跳过)
|
||||
* TC-PROTELEM-014 启动位图与动作位图分离(道F task-10「保护启动」记录的前置):
|
||||
* 延时段已启动未到点时两枚位图必须不同 + 不变式 asserted ⊆ picked
|
||||
* TC-PROTELEM-015 动作边沿的记录分类(道F task-10「事故 / 保护告警」):
|
||||
* 逐项独立读 PS_*_TRIP、改定值即改类别、无该投退项的五项恒告警
|
||||
* (同时挡住 trip_ctl=0xFF 的越界读)、越界编号不落记录
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 创建日期:2026-10-11(B5);2026-10-12 扩 OF/UF(B8)
|
||||
@@ -442,6 +447,101 @@ static void tc_run_freq_wiring(void) {
|
||||
"TC-PROTELEM-013 run(): UV still evaluated with zero-amplitude (no whole-beat skip)");
|
||||
}
|
||||
|
||||
/* TC-PROTELEM-014(道F task-10):启动位图与动作位图**分离**。
|
||||
* 「保护启动」记录的边沿检测靠 pick_mask,若它等于 active_mask(或干脆没实现,
|
||||
* 让 protect_task 去轮询 state),"启动了但延时段还没到点"这一整段在记录里就是
|
||||
* 空的——而那恰恰是这一页唯一比"动作记录"多出来的信息。所以这里把两者的
|
||||
* **不等**关系钉死,并钉住不变式 asserted ⊆ picked。 */
|
||||
static void tc_pick_mask(void) {
|
||||
float m[PE_COUNT];
|
||||
|
||||
prot_set_init();
|
||||
prot_elm_init();
|
||||
(void)prot_set_set(PS_OC1_DLY, PROT_SET_Q10(100.0)); /* 延时段 */
|
||||
(void)prot_set_set(PS_OC3_DLY, PROT_SET_Q10(0.0)); /* 瞬时段 */
|
||||
mk_base(m);
|
||||
m[PE_OC1] = 10.1f; /* 越限,但 100 ms 未到 */
|
||||
m[PE_OC3] = 2.1f; /* 越限,dly=0 同拍动作 */
|
||||
|
||||
prot_elm_eval(6000u, m);
|
||||
CHECK(prot_elm_pick_mask() == ((1uL << PE_OC1) | (1uL << PE_OC3)),
|
||||
"TC-PROTELEM-014 pick mask carries the delayed element that has not tripped yet");
|
||||
CHECK(prot_elm_active_mask() == (1uL << PE_OC3),
|
||||
"TC-PROTELEM-014 active mask still excludes it (the two masks are genuinely different)");
|
||||
CHECK((prot_elm_active_mask() & ~prot_elm_pick_mask()) == 0u,
|
||||
"TC-PROTELEM-014 invariant: asserted is a subset of picked");
|
||||
|
||||
prot_elm_eval(6100u, m); /* OC1 定时限到点 */
|
||||
CHECK(prot_elm_active_mask() == ((1uL << PE_OC1) | (1uL << PE_OC3)),
|
||||
"TC-PROTELEM-014 after the delay the active mask catches up");
|
||||
CHECK(prot_elm_pick_mask() == ((1uL << PE_OC1) | (1uL << PE_OC3)),
|
||||
"TC-PROTELEM-014 pick mask unchanged while the level holds");
|
||||
|
||||
m[PE_OC1] = 9.4f; /* 跌破返回门槛 */
|
||||
m[PE_OC3] = 1.0f;
|
||||
prot_elm_eval(6200u, m);
|
||||
CHECK((prot_elm_pick_mask() == 0u) && (prot_elm_active_mask() == 0u),
|
||||
"TC-PROTELEM-014 both masks clear together below the return threshold");
|
||||
}
|
||||
|
||||
/* TC-PROTELEM-015(道F task-10):动作上升沿该归"事故"还是"保护告警"。
|
||||
* 判据的唯一来源是定值表里的跳闸投退项 PS_*_TRIP(枚举注释即"0 告警 / 1 跳闸"),
|
||||
* 元件->投退项的对应关系在 k_pe[] 的 trip_ctl 列。这里要钉的是三件容易做错的事:
|
||||
* a) **逐项独立**:OC1 投入、OC2 退出必须给出不同类别(做成一个全局开关、
|
||||
* 或在本函数里另写一份"哪些元件算跳闸"的名单,都会在这里翻红——而那份
|
||||
* 名单一旦与 g_prot_set_meta / k_pe[] 分叉,就是"整定视图与记录分类打架",
|
||||
* setview-check 也只认既有表);
|
||||
* b) **改定值即改类别**:同一元件同一状态,写 1 变事故、写 0 变告警,
|
||||
* 证明它是查表而不是编译期常量;
|
||||
* c) **没有该投退项的五项恒告警**,且**不会串到别人的字**:把全部 TRIP 项
|
||||
* 写成 1,OV/UV/3U0/OF/UF 仍然是告警——这条同时是 trip_ctl=0xFF 那格的
|
||||
* 越界读防线(若实现把 0xFF 直接喂给 prot_set_get_ctl(),读到的是定值区
|
||||
* 之外,类别会随内存内容漂,这条必红)。
|
||||
* 越界编号返回"不落任何记录"。 */
|
||||
static void tc_record_kind(void) {
|
||||
prot_set_init();
|
||||
prot_elm_init();
|
||||
|
||||
/* a) 逐项独立 */
|
||||
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OC2_TRIP, PROT_SET_Q10(0));
|
||||
CHECK((prot_elm_record_kind(PE_OC1) == PROT_REC_TRIP)
|
||||
&& (prot_elm_record_kind(PE_OC2) == PROT_REC_ALARM),
|
||||
"TC-PROTELEM-015 the kind is read per element, not from one global switch");
|
||||
|
||||
/* b) 改定值即改类别(同元件、同状态) */
|
||||
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(0));
|
||||
CHECK(prot_elm_record_kind(PE_OC1) == PROT_REC_ALARM,
|
||||
"TC-PROTELEM-015 releasing the trip word turns the same element into an alarm");
|
||||
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(1));
|
||||
CHECK(prot_elm_record_kind(PE_OC1) == PROT_REC_TRIP,
|
||||
"TC-PROTELEM-015 re-engaging it turns it back into an accident (no latching)");
|
||||
(void)prot_set_set(PS_OC3_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OL_TRIP, PROT_SET_Q10(1));
|
||||
CHECK((prot_elm_record_kind(PE_OC3) == PROT_REC_TRIP)
|
||||
&& (prot_elm_record_kind(PE_OL) == PROT_REC_TRIP),
|
||||
"TC-PROTELEM-015 OC3/OL follow their own trip words too");
|
||||
|
||||
/* c) 无跳闸投退项的五项:全表写 1 之后仍恒告警(也挡住 0xFF 越界读) */
|
||||
(void)prot_set_set(PS_OC1_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OC2_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OC3_TRIP, PROT_SET_Q10(1));
|
||||
(void)prot_set_set(PS_OL_TRIP, PROT_SET_Q10(1));
|
||||
CHECK((prot_elm_record_kind(PE_OV) == PROT_REC_ALARM)
|
||||
&& (prot_elm_record_kind(PE_UV) == PROT_REC_ALARM)
|
||||
&& (prot_elm_record_kind(PE_3U0) == PROT_REC_ALARM)
|
||||
&& (prot_elm_record_kind(PE_OF) == PROT_REC_ALARM)
|
||||
&& (prot_elm_record_kind(PE_UF) == PROT_REC_ALARM),
|
||||
"TC-PROTELEM-015 alarm-only elements stay alarms with every trip word engaged");
|
||||
|
||||
/* 守卫:越界编号 = 一条都不许落 */
|
||||
CHECK(prot_elm_record_kind(PE_COUNT) == PROT_REC_NONE,
|
||||
"TC-PROTELEM-015 out-of-range id -> no record at all");
|
||||
/* 只读性:判定不改任何状态(protectTask 每拍都会问,问完还要跑判据) */
|
||||
CHECK(prot_elm_pick_mask() == 0u,
|
||||
"TC-PROTELEM-015 the classifier leaves the element states untouched");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== Prot Elm Tests ===\n");
|
||||
tc_init_clear();
|
||||
@@ -457,6 +557,8 @@ int main(void) {
|
||||
tc_of_hysteresis();
|
||||
tc_uf_direction();
|
||||
tc_run_freq_wiring();
|
||||
tc_pick_mask();
|
||||
tc_record_kind();
|
||||
|
||||
printf("\n=== Prot Elm Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
@@ -7,7 +7,12 @@ r"""运行/自检记录落盘后端(sys_log_mcu)的主机侧单元测试运
|
||||
App/log/sys_log_mcu.c,并纳入共享假卷 test/host/common/fatfs_fake.c),在 PC 上离线
|
||||
跑 64B 定长记录格式、单条/批量两条写路径、syslog_drain 的 preboot+队列排空与
|
||||
"失败回投不丢条目"、日志读写放大收口的 8 条滑动读窗口 + 写者代数失效、挂载幂等与
|
||||
上电/重启分类、sys_log_evidence 的 wdev.log 取证通道,失败即非 0 退出。
|
||||
上电/重启分类、sys_log_evidence 的 wdev.log 取证通道、「清除记录」的请求/落地分离
|
||||
与四条判据(请求侧零盘触点、drain 内恰好一次 unlink、删后重建可继续写、
|
||||
FR_NO_FILE 幂等返 0、卷占用/真失败返 -1 且不误删原文件)、以及七类记录(道F
|
||||
task-10 新增 accid/alarm/start/oper/yk 五类)的存储面——逐 id 验"哪类落哪个
|
||||
文件、各用各的容量上限、名字过严格 8.3",以及 quiet 投递通道的丢弃计数与
|
||||
"投递侧零留痕",失败即非 0 退出。
|
||||
工具链与判定委托 test/host/common/unit_test_harness.py。
|
||||
|
||||
为什么要主机侧单测(而不是只用台架):
|
||||
@@ -20,7 +25,11 @@ r"""运行/自检记录落盘后端(sys_log_mcu)的主机侧单元测试运
|
||||
3. **挂载幂等**:syslog_mcu_mounted 曾在建 /log 失败后永久 no-op(运行期日志与自检
|
||||
全部滞留),改成幂等 + 重试后又必须防"每 2s 刷一条同样错误" → 要注入 mkdir
|
||||
失败才测得到,且必须能数出"到底留痕几次"。
|
||||
4. **读窗口 try 语义 / 日志读写放大收口的读侧**:窗口"命中即零 I/O"与"写者一落地就失效"是一枚硬币两面,
|
||||
4. **「清除记录」的请求 / 落地分离(2026-10-26 加)**:在 UI 按键回调里直接 f_unlink
|
||||
也能删掉文件、台架当场看不出毛病,代价是把 FAT 目录项回写挂进 uiTask 的
|
||||
100 ms 喂狗节拍、并与唯一写者自己的"容量满 unlink 重建"交错。这条边界只能靠
|
||||
数调用钉住:请求侧零 FatFs / 零卷锁、删除只在 syslog_drain 内且恰好一次 unlink。
|
||||
5. **读窗口 try 语义 / 日志读写放大收口的读侧**:窗口"命中即零 I/O"与"写者一落地就失效"是一枚硬币两面,
|
||||
只做前者会把陈旧读当正确、只做后者会退回逐条 open/seek/close 的旧实现。
|
||||
|
||||
桩的边界:ff.h / sys_log.h / soe_store.h / sys_clock.h / vol_lock.h 用**真实头**;
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
* 失败回投不丢条目、-3 退回慢路径)
|
||||
* D 读侧(日志读写放大收口的 8 条滑动窗口 + 写者代数失效 + 越界/忙/短读处置)
|
||||
* 外加 syslog_mcu_mounted 的 挂载幂等与上电/重启分类、sys_log_evidence
|
||||
* 的 wdev.log 取证通道(8KB 护栏 + 第二条写必须在释放卷锁之后)。
|
||||
* 的 wdev.log 取证通道(8KB 护栏 + 第二条写必须在释放卷锁之后),
|
||||
* 以及「清除记录」的请求 / 落地分离与四条判据(K 族,2026-10-26)。
|
||||
*
|
||||
* 被测对象:真实源文件 App/log/sys_log_mcu.c(#include 原样纳入,不改生产代码)。
|
||||
* 假件与桩:
|
||||
@@ -28,6 +29,20 @@
|
||||
* 4. **读窗口 try 语义 / 日志读写放大收口的读侧**:读窗口"命中即零 I/O"与"写者一落地就必须失效重填"是同一
|
||||
* 枚硬币的两面——只做前者会把陈旧读当正确,只做后者会退化成逐条 open/seek/close
|
||||
* 的旧实现(UI 翻 20 条 = 20 次 FAT 往返)。两者都靠假卷的调用计数才断言得了。
|
||||
* 5. **「清除记录」的请求 / 落地分离**(2026-10-26 加,K 族):最顺手的写法是在 UI
|
||||
* 按键回调里直接 f_unlink —— 文件确实删了,台架当场也看不出毛病,代价是把 FAT
|
||||
* 目录项回写挂进 uiTask 每 100 ms 一次的喂狗节拍,并与唯一写者自己的"容量满
|
||||
* unlink 重建"交错。这条"谁在什么上下文碰盘"的边界,只有离线数调用才钉得住:
|
||||
* 请求侧必须零 FatFs / 零卷锁(TC-SYSLOG-110),删除必须恰好一次 unlink 且
|
||||
* 发生在排空之后(TC-SYSLOG-111/117)。真机上这条写错的表现为偶发丢一段日志
|
||||
* 或一次误复位,串口里没有任何一行会指着这处代码。
|
||||
* 6. **一张表的下标序 = 枚举序**(2026-10-26 加,L 族):记录类型就是"哪个文件",
|
||||
* 于是 s_paths[]/s_caps[] 与 syslog_id_t 的**逐项同序**成了正确性本身。少写一项
|
||||
* 编译期就报,写错序却什么都不报错——事故记录会安静地写进告警文件,屏上一切
|
||||
* 正常,只有查记录的人被误导。这类"顺序即语义"的耦合只能靠逐 id 断言"我投的
|
||||
* 这句出现在哪个路径、别的文件里没有"来钉(TC-SYSLOG-130),容量同理(131)。
|
||||
* quiet 投递通道也在这一族:它存在的唯一理由是"投递侧不许带日志行缓冲",
|
||||
* 所以必须断言**丢弃时零留痕**(132/135),否则退化回 syslog_post 也没人发现。
|
||||
*
|
||||
* 覆盖点(TC 号族 SYSLOG,形如 TC-SYSLOG-001,族内按功能分档;缺口在小节末尾如实登记):
|
||||
* —— A 记录格式 ——
|
||||
@@ -129,11 +144,69 @@
|
||||
* TC-SYSLOG-081 生产路径全过严格 8.3:/log /run.log /selftest.log /wdev.log /soe.log
|
||||
* TC-SYSLOG-082 FR_INVALID_NAME 反复失败 20 轮:unlink 次数有界、/log 目录与
|
||||
* 邻居文件一根毛没动(自愈只针对自己的文件)
|
||||
* —— K 记录删除(「清除记录」前置,2026-10-26)——
|
||||
* TC-SYSLOG-110 请求侧只登记:零卷锁、零 FatFs、记录还在盘上(uiTask 唯一被
|
||||
* 允许做的事就是发这个请求)
|
||||
* TC-SYSLOG-111 落地在 syslog_drain(defaultTask)内:恰好一次 unlink、零 open、
|
||||
* count 归零、卷锁配对、邻居文件不动、零留痕
|
||||
* TC-SYSLOG-112 删后重建可继续写:清除后下一条记录把文件建回来、打开模式带写位、
|
||||
* 64/64 写全、读侧从 idx 0 读到新内容
|
||||
* TC-SYSLOG-113 FR_NO_FILE 幂等返 0:无留痕、卷锁配对、读侧代数照常自增
|
||||
* TC-SYSLOG-114 卷占用返 -1:零盘触点、不 release(没拿到锁)、不计故障、原文件一字节不少
|
||||
* TC-SYSLOG-115 请求路径遇卷忙 = 静默顺延(WAIT 不是 FAIL、请求挂回、零留痕),
|
||||
* 下一轮卷空闲自己落地(用户不必重按)
|
||||
* TC-SYSLOG-116 unlink 真失败 -> -1 + 留痕一次 + 文件完好;请求侧判 FAIL 且
|
||||
* **绝不自动重试**(反复删自己的日志 = 写侧刚修掉的坑,别再造一遍)
|
||||
* TC-SYSLOG-117 drain 内顺序本身也是判据:本轮排空出来的旧记录一起被清掉,
|
||||
* 删除之后新投的条目仍落盘(清除不等于把日志关掉)
|
||||
* TC-SYSLOG-118 同 id 重复请求合并为一次 unlink(请求是标志,不是计数器)
|
||||
* TC-SYSLOG-119 只删自己:清 RUN 不碰 SELFTEST,也不碰取证文件 wdev.log
|
||||
* TC-SYSLOG-120 三处入口的入参守卫都在取锁之前;坏 id 不污染合法 id 的状态
|
||||
* TC-SYSLOG-121 删除成功 -> 读侧代数自增(屏上"已清除"而窗口还装着旧记录 = 错数据);
|
||||
* 删除失败 -> 代数不变、缓存记录仍可读(盘上什么都没动)
|
||||
* TC-SYSLOG-122 /log 目录不在 -> FR_NO_PATH **不算**幂等成功:-1 + 一次留痕 +
|
||||
* 零 f_mkdir(名字/参数类码不得升级成卷级动作);目录回来后 FAIL 不粘
|
||||
*
|
||||
* 覆盖率台账(--module sys_log_mcu 自量):App/log/sys_log_mcu.c 310/313 行 =
|
||||
* 99.0%、函数 12/12。未覆盖 3 行:L628(下面第三条死分支)、L727~728
|
||||
* (sys_log_evidence 里 snprintf 返回 <=0 的分支——宿主 snprintf 不可能失败,
|
||||
* 假件也不伪造返回值;为刷数字去伪造等于把断言写在假件规则上,宁可留空)。
|
||||
* —— L 五类记录与 quiet 投递通道(道F task-10,2026-10-26)——
|
||||
* TC-SYSLOG-130 七类记录各进各的文件、各带各的描述:**id -> 文件的表序就是语义**
|
||||
* TC-SYSLOG-130b 卷里恰好七个记录文件,没有第八个拼错名字被顺手建出来的
|
||||
* TC-SYSLOG-130c 七条生产路径逐条过严格 8.3(主名<=8 / 扩展=3 / 不以点开头)
|
||||
* TC-SYSLOG-131 accid 到自己那格(512)轮转,邻居 alarm 一字节没动
|
||||
* TC-SYSLOG-131b yk 用的是自己那格(256)——三档上限互不相同,能照出 s_caps[] 串格
|
||||
* TC-SYSLOG-132 quiet 通道队列满:计数前进、投递侧**零留痕**、drain 侧按差值报一次
|
||||
* TC-SYSLOG-133 清除记录 = 一次运维动作 -> 落一条操作记录点名被清的那份;被清那份仍归零
|
||||
* TC-SYSLOG-133b 清 oper.log 自己时**不投**操作记录(否则刚删空又变 1 条,砸掉归零判据)
|
||||
* TC-SYSLOG-134 quiet 的入参守卫:坏 id / NULL 既不投也不计丢弃(参数错 != 容量故障)
|
||||
* TC-SYSLOG-135 启动前数组满同样计数、同样静默(另一条腿,与 F 项的两个通道分区一致)
|
||||
* TC-SYSLOG-136 同内容在七类之间**逐字节相同**:记录体不编码类型 = "文件即类型"的机器表达
|
||||
*
|
||||
* 覆盖率台账(`scripts/check_test_coverage.py --module sys_log_mcu` 本机实跑):
|
||||
* App/log/sys_log_mcu.c **403/406 行 = 99.3%、函数 22/22**。
|
||||
* 上一轮(K 族)是 377/380 行、函数 18/18 —— L 族新增 4 枚函数
|
||||
* (s_entry_fill / s_put / syslog_post_quiet / syslog_post_dropped)与 26 行
|
||||
* 可执行代码**全部**有断言跑到:覆盖行 +26、函数 +4,未覆盖仍是同一批 3 行老账
|
||||
* (一条死分支 + sys_log_evidence 里 snprintf<=0),**一条没涨**。
|
||||
* ⚠ 全量门禁(40 套并集 + 基线只增不减)由提交前那一次统一跑,本行只是自量记录。
|
||||
* L 族变异实测(2026-10-26,三处同时注入后逐字节还原):
|
||||
* ① s_paths[] 里 accid 与 alarm 两格**对调**(就是"表序错"那个静默错法);
|
||||
* ② 给 quiet 的丢弃分支补一条 `DBG_ERROR`(= 悄悄退化成 syslog_post);
|
||||
* ③ 去掉"清 oper.log 自己不投操作记录"那半枚守卫。
|
||||
* → 137 掉到 132:**TC-SYSLOG-130 / 131**(①)、**132 / 135**(②)、
|
||||
* **133b**(③)各自转红且互不牵连——每条新纪律都被单独钉住,不是靠一锅大断言。
|
||||
* App/log/sys_log_mcu.c **377/380 行 = 99.2%、函数 18/18**。
|
||||
* 本轮(K 族)之前是 328/330 行、函数 13/13 —— 新增 5 个函数
|
||||
* (s_clear_try / s_clear_service / syslog_clear_apply / syslog_request_clear /
|
||||
* syslog_clear_state)与 50 行新增可执行代码(已覆盖 +49):新增行**全部**有断言
|
||||
* 跑到,未覆盖仍是 3 行,一条没涨。
|
||||
* 仍未覆盖的 3 行是同一批老账:一条死分支 + sys_log_evidence 里 snprintf 返回
|
||||
* <=0 的分支——宿主 snprintf 不可能失败,假件也不伪造返回值;为刷数字去伪造
|
||||
* 等于把断言写在假件规则上,宁可留空。
|
||||
* 变异实测("断言有牙齿"的证据,2026-10-26,改完已逐字节还原):
|
||||
* ① 在排空循环**之前**多调一次 `s_clear_service()`(净效果 = 删除抢在条目落盘之前
|
||||
* 执行,尾部那次因请求已销账而空转);② 把 `FR_NO_PATH` 也算成幂等成功。
|
||||
* 两处一起改 → TC-SYSLOG-115 / 115b / 117 / 117b / 122 **五条转红**(126 → 121),
|
||||
* 还原后复绿 126/126。也就是说"删除必须排在条目排空之后落地"与"名字/参数类码
|
||||
* 不算幂等成功"这两条本来只写在注释里的纪律,现在真的会被测试拦住。
|
||||
*
|
||||
* 本套照出的四条现状 —— 已在 task-4(2026-10-24)全部处置,相应断言已翻成正向:
|
||||
*
|
||||
@@ -172,6 +245,18 @@
|
||||
* (TC-SOESTORE-022 / 022b),两侧的差异是刻意的,注释也互指。
|
||||
*
|
||||
* 故意没测(写在这里,不留"看起来全覆盖"的错觉):
|
||||
* - 真机上 uiTask 发请求与 defaultTask 落地的并发:主机单线程编不出真竞态,本套只
|
||||
* 钉"请求侧零盘触点 + 落地只在 drain 里"这两条形状;两个 volatile 单字节交接的
|
||||
* 最坏竞态(读到上一次的结果 / 失败被新请求清掉)写在 sys_log.h 的函数头里。
|
||||
* - quiet 通道与 post 通道在**同一轮**里交错时的抑制位归属(quiet 成功不复位 F 项
|
||||
* 那两个位):这是刻意的设计(那两个位描述 post 自己的历史),TC-SYSLOG-107c 已
|
||||
* 钉住两位互不干扰;"quiet 投成功把 post 的故障静默位清了"这种错向,本套没有
|
||||
* 单独一条——要钉它得先给两个通道各造一套满态时序,收益不抵这套的复杂度。
|
||||
* - 五类记录各自的**业务触发时序**(哪个元件边沿算事故、哪条 104 命令算操作)不在
|
||||
* 本套:那是判据与协议侧的事,分别由 test/host/prot_elm 的 TC-PROTELEM-015 与
|
||||
* test/host/iec104 的 TC-IEC104-030 钉;本套只管"投进来以后存储对不对"。
|
||||
* - 删除之后"下一次上电会不会多写一条 power on":那是 syslog_mcu_mounted() 的
|
||||
* 上电分类逻辑(E 族已测),与删除路径无关,两边各测各的。
|
||||
* - 真跨任务并发:读窗口用"对齐 32 位读写在 ARM 上原子"免锁,主机 x64 + 单线程
|
||||
* 编不出真竞态;本套只断言单写者语义与代数失效逻辑本身。
|
||||
* - f_close 内部 sync_fs -> disk_ioctl(CTRL_SYNC) -> ftl_flush_cache() 的掉电落盘
|
||||
@@ -218,6 +303,12 @@ static int s_failed;
|
||||
#define P_RUN "/log/run.log"
|
||||
#define P_SELFTEST "/log/selftest.log"
|
||||
#define P_WDEV "/log/wdev.log"
|
||||
/* 五类新记录(task-10,L 族):生产用的就是这七条路径,改名必须同步这里 */
|
||||
#define P_ACCID "/log/accid.log"
|
||||
#define P_ALARM "/log/alarm.log"
|
||||
#define P_START "/log/start.log"
|
||||
#define P_OPER "/log/oper.log"
|
||||
#define P_YK "/log/yk.log"
|
||||
|
||||
/* ===================== 假件与生产源码纳入 ===================== */
|
||||
|
||||
@@ -494,18 +585,28 @@ static void reset_all(void)
|
||||
s_queue = NULL;
|
||||
s_fs_ready = 0u;
|
||||
s_mount_fail_reported = 0u;
|
||||
/* B / F 项整改新增的留痕节流位:跨场景必须显式清(否则第二个故障场景看不到留痕) */
|
||||
s_requeue_reported[SYSLOG_RUN] = 0u;
|
||||
s_requeue_reported[SYSLOG_SELFTEST] = 0u;
|
||||
/* B / F 项整改新增的留痕节流位:跨场景必须显式清(否则第二个故障场景看不到留痕)。
|
||||
* ⚠ 按 SYSLOG_COUNT 循环清,不要逐 id 手写——task-10 把 id 从 2 扩到 7 之后,
|
||||
* 任何"逐枚点名"的清理都会静默漏掉新增的那几格,症状是"单独跑绿、连跑偶红"。 */
|
||||
(void)memset(s_requeue_reported, 0, sizeof(s_requeue_reported));
|
||||
s_post_qfull_reported = 0u;
|
||||
s_post_pboot_full_reported = 0u;
|
||||
/* quiet 投递通道(L 族):丢弃计数与 drain 侧已上报水位都要归零,
|
||||
* 否则上一个场景的丢弃会让本场景第一条留痕被当成"已经报过" */
|
||||
s_quiet_drop = 0u;
|
||||
s_quiet_reported = 0u;
|
||||
(void)memset(s_rd_win, 0, sizeof(s_rd_win));
|
||||
s_rd_win_id = -1;
|
||||
s_rd_win_base = 0u;
|
||||
s_rd_win_cnt = 0u;
|
||||
s_rd_win_gen = 0u;
|
||||
s_rd_gen[SYSLOG_RUN] = 0u;
|
||||
s_rd_gen[SYSLOG_SELFTEST] = 0u;
|
||||
(void)memset(s_rd_gen, 0, sizeof(s_rd_gen)); /* 同上:按数组清,扩 id 不会漏 */
|
||||
/* 「清除记录」的请求 / 结果交接(本套新增,K 族):两枚都必须显式清,
|
||||
* 否则上一个场景留下的 WAIT/FAIL 会让下一个场景第一眼就读到错状态 */
|
||||
/* 「清除记录」的请求 / 结果交接(K 族):两枚按数组清,理由同 s_rd_gen——
|
||||
* 五类新记录每一类都可被清除,漏清一格就是上一场景的 WAIT/FAIL 咬下一场景。 */
|
||||
(void)memset(s_clear_req, 0, sizeof(s_clear_req));
|
||||
(void)memset(s_clear_st, 0, sizeof(s_clear_st)); /* 0 == SYSLOG_CLEAR_NONE */
|
||||
/* 卷状态:/log 由 defaultTask 挂载时建好;本套多数场景直接假定已建,
|
||||
* "没建 -> 补建" 那条路径单独用 TC-SYSLOG-060/061 测 */
|
||||
(void)ff_fake_mkdir_direct("/log");
|
||||
@@ -1838,6 +1939,469 @@ static void tc_post_throttle(void)
|
||||
"TC-SYSLOG-107c the two channels keep independent throttle bits");
|
||||
}
|
||||
|
||||
/* ===================== K 记录删除(「清除记录」前置) ===================== */
|
||||
|
||||
/*
|
||||
* 这一族钉的是"谁在什么上下文把文件删掉",加上删除本身的四条判据:
|
||||
* 计数归零(111)、文件不在时幂等返 0(113)、卷占用/真失败返 -1 且不误删原文件
|
||||
* (114/116)、删后重建可继续写(112)。
|
||||
* 为什么值得离线测:最顺手的写法是在 UI 按键回调里直接 f_unlink —— 台架上当场看不出
|
||||
* 毛病,代价是把 FAT 目录项回写挂进 uiTask 每 100 ms 一次的喂狗节拍,并与唯一写者
|
||||
* 自己的"容量满 unlink 重建"交错。请求/落地分开之后,这条边界只能靠"数请求侧的盘
|
||||
* 触点"和"数删除发生在哪一轮"钉住(110/111/117 干的就是这件事)。
|
||||
* 一处限度说清楚:本目录的卷锁假件不记录超时入参,所以"绝不阻塞"只能断言成
|
||||
* "try 失败 = 零盘触点 + 不 release + 不计故障"(114),测不到传进去的超时值到底
|
||||
* 是不是 0;要测那一层得给共用的卷锁假件加一列观测,不属本套的范围。
|
||||
*/
|
||||
static void tc_clear(void)
|
||||
{
|
||||
char ts[SYSLOG_TS_LEN + 1u];
|
||||
char desc[SYSLOG_DESC_MAX + 1u];
|
||||
uint8_t rec[SYSLOG_REC_SIZE];
|
||||
uint32_t i;
|
||||
int ok;
|
||||
|
||||
printf("-- TC-SYSLOG-110..121 record clear (request / land split) --\n");
|
||||
|
||||
/* 110 请求侧:登记就是登记 —— 一把锁都不取、一个 FatFs 调用都没有、记录还在盘上。
|
||||
* 这是"UI 只能发请求"的全部技术含义:按键回调在 uiTask,那侧不许碰存储。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "old1");
|
||||
(void)s_append(SYSLOG_RUN, "old2");
|
||||
{
|
||||
int open_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||||
int unlink_base = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
|
||||
int lock_base = s_lock_waits;
|
||||
int rel_base = s_lock_releases;
|
||||
ok = (syslog_request_clear(SYSLOG_RUN) == 0)
|
||||
&& (syslog_count(SYSLOG_RUN) == 2u)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == open_base)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlink_base)
|
||||
&& (s_lock_waits == lock_base)
|
||||
&& (s_lock_releases == rel_base);
|
||||
CHECK(ok, "TC-SYSLOG-110 request only books the ask: zero volume lock, zero FatFs, records still on the volume");
|
||||
}
|
||||
|
||||
/* 111 落地发生在 drain(defaultTask)里:恰好一次 unlink(不 open)、计数归零、
|
||||
* 锁成对、零留痕,且邻居文件 selftest.log 一字节没动 */
|
||||
(void)s_append(SYSLOG_SELFTEST, "neighbour");
|
||||
{
|
||||
int open_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||||
syslog_drain();
|
||||
ok = (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
|
||||
&& (syslog_count(SYSLOG_RUN) == 0u)
|
||||
&& (ff_fake_exists(P_RUN) == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||||
&& (strcmp(g_ff_obs.last_unlink_path, "/LOG/RUN.LOG") == 0) /* 观测位存的是归一化后的 8.3 大写名 */
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == open_base)
|
||||
&& (s_lock_seq_err == 0)
|
||||
&& (g_sys_err_count == 0)
|
||||
&& (ff_fake_exists(P_SELFTEST) == 1)
|
||||
&& (ff_fake_size(P_SELFTEST) == SYSLOG_REC_SIZE);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-111 drain lands it: one unlink and zero open, count 0, lock paired, no trace, neighbour intact");
|
||||
|
||||
/* 112 判据"删后重建可继续写":文件被删之后,下一条记录必须把文件建回来并写全。
|
||||
* 陷阱一在这里才有牙齿 —— 只有"文件不存在"这一条路才走 f_open 的创建分支,
|
||||
* 少了 FA_WRITE 位,平时写得好好的,清除之后从此每条静默写不进去。 */
|
||||
ok = (s_append(SYSLOG_RUN, "after-clear") == 0)
|
||||
&& (ff_fake_exists(P_RUN) == 1)
|
||||
&& (syslog_count(SYSLOG_RUN) == 1u)
|
||||
&& (g_ff_obs.last_open_mode == (BYTE)(FA_WRITE | FA_OPEN_APPEND))
|
||||
&& (g_ff_obs.last_write_bw == SYSLOG_REC_SIZE);
|
||||
CHECK(ok, "TC-SYSLOG-112 after a clear the next record recreates the file with the write bit set and lands 64/64 bytes");
|
||||
ok = ok && (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
|
||||
&& (strcmp(desc, "after-clear") == 0);
|
||||
CHECK(ok, "TC-SYSLOG-112b the recreated file is readable from index 0 (cleared history never comes back)");
|
||||
|
||||
/* 113 幂等:文件本来就不存在 -> 返 0、只留一次 unlink、零留痕、卷锁照样配对 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
{
|
||||
int rel_base = s_lock_releases;
|
||||
uint32_t gen = s_rd_gen[SYSLOG_RUN];
|
||||
ok = (ff_fake_exists(P_RUN) == 0)
|
||||
&& (syslog_clear_apply(SYSLOG_RUN) == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||||
&& (g_sys_err_count == 0)
|
||||
&& (s_lock_releases == rel_base + 1)
|
||||
&& (s_lock_seq_err == 0)
|
||||
&& (s_rd_gen[SYSLOG_RUN] == gen + 1u);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-113 FR_NO_FILE is an idempotent success: 0 back, no trace, lock released, window bumped");
|
||||
|
||||
/* 114 卷占用:公开 API 返 -1,一个盘操作都没有、不 release(没拿到锁)、不计故障、
|
||||
* 原文件一字节不少。"try 失败不许 release"是漏锁 = FTP 永久等不到卷的老账。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "keepme");
|
||||
{
|
||||
int open_base = g_ff_obs.calls[FF_FAKE_OP_OPEN];
|
||||
int unlink_base = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
|
||||
int rel_base = s_lock_releases;
|
||||
lock_busy_next(1);
|
||||
ok = (syslog_clear_apply(SYSLOG_RUN) == -1)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == open_base)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlink_base)
|
||||
&& (s_lock_releases == rel_base)
|
||||
&& (s_lock_seq_err == 0)
|
||||
&& (g_sys_err_count == 0)
|
||||
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE)
|
||||
&& (syslog_count(SYSLOG_RUN) == 1u);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-114 busy volume -> -1 with zero disk touch, no release, no fault counted, original file intact");
|
||||
|
||||
/* 115 走请求路径时卷占用 = 静默顺延,不是失败:状态仍是 WAIT、请求挂回、零留痕;
|
||||
* 下一轮卷空闲后同一个请求自己落地(用户不必重按一次按钮)。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
syslog_drain(); /* 建队列,本轮空转 */
|
||||
(void)s_append(SYSLOG_RUN, "keepme");
|
||||
(void)syslog_request_clear(SYSLOG_RUN);
|
||||
lock_busy_next(1);
|
||||
g_sys_err_count = 0;
|
||||
syslog_drain(); /* 这一轮的清除拿锁失败 */
|
||||
ok = (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT)
|
||||
&& (ff_fake_exists(P_RUN) == 1)
|
||||
&& (g_sys_err_count == 0)
|
||||
&& (s_lock_seq_err == 0);
|
||||
CHECK(ok, "TC-SYSLOG-115 busy while landing defers: ask kept, state WAIT (not FAIL), file untouched, silent");
|
||||
syslog_drain(); /* 卷空闲 -> 落地 */
|
||||
ok = ok && (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
|
||||
&& (syslog_count(SYSLOG_RUN) == 0u);
|
||||
CHECK(ok, "TC-SYSLOG-115b the deferred ask lands on the next round without a second press");
|
||||
|
||||
/* 116 真失败(非 FR_NO_FILE):apply 返 -1 + 留痕一次;请求侧判 FAIL 且绝不自动
|
||||
* 重试 —— 反复 unlink 自己的日志文件是本项目在写侧刚修掉的坑(名字/参数类
|
||||
* 错误码同名同因,重试只会每轮删一次历史),删除路径不许再造一遍。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "precious");
|
||||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 4);
|
||||
ok = (syslog_clear_apply(SYSLOG_RUN) == -1)
|
||||
&& (g_sys_err_count == 1)
|
||||
&& (ff_fake_exists(P_RUN) == 1)
|
||||
&& (ff_fake_size(P_RUN) == SYSLOG_REC_SIZE);
|
||||
CHECK(ok, "TC-SYSLOG-116 a real unlink error -> -1, one trace, the log file itself is still there");
|
||||
reset_all();
|
||||
ready_only();
|
||||
syslog_drain();
|
||||
(void)s_append(SYSLOG_RUN, "precious");
|
||||
(void)syslog_request_clear(SYSLOG_RUN);
|
||||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 4);
|
||||
syslog_drain();
|
||||
{
|
||||
int unlink_base = g_ff_obs.calls[FF_FAKE_OP_UNLINK];
|
||||
ok = (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_FAIL)
|
||||
&& (ff_fake_exists(P_RUN) == 1);
|
||||
syslog_drain(); /* 不许自己再删一次 */
|
||||
ok = ok && (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == unlink_base)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_FAIL);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-116b a failed clear latches FAIL and is never retried on its own (re-press is the retry)");
|
||||
|
||||
/* 117 drain 内的顺序是判据的一部分:本轮排空出来的旧记录必须一起被清掉
|
||||
* (先删后写 = 白清,count 不归零);而删除**之后**投的条目要留下
|
||||
* (否则"清除记录"变成了"关掉日志")。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain(); /* 建队列 */
|
||||
for (i = 0u; i < 3u; i++) {
|
||||
syslog_post(SYSLOG_RUN, "old");
|
||||
}
|
||||
(void)syslog_request_clear(SYSLOG_RUN);
|
||||
syslog_drain(); /* 排空 3 条 -> 然后删除 */
|
||||
ok = (syslog_count(SYSLOG_RUN) == 0u) && (ff_fake_exists(P_RUN) == 0)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK);
|
||||
CHECK(ok, "TC-SYSLOG-117 the clear runs after the entry drain: the queued backlog does not survive it");
|
||||
syslog_post(SYSLOG_RUN, "new");
|
||||
syslog_drain();
|
||||
ok = ok && (syslog_count(SYSLOG_RUN) == 1u)
|
||||
&& (raw_at(P_RUN, 0u, rec) != 0)
|
||||
&& (memcmp(rec + SYSLOG_DESC_OFFS, "new", 3u) == 0);
|
||||
CHECK(ok, "TC-SYSLOG-117b a record posted after the clear still lands: clearing is not disabling logging");
|
||||
|
||||
/* 118 同一 id 连点两次只登记一次(合并成一次删除,不是计数器) */
|
||||
reset_all();
|
||||
ready_only();
|
||||
ok = (syslog_request_clear(SYSLOG_RUN) == 0) && (syslog_request_clear(SYSLOG_RUN) == 0)
|
||||
&& (s_clear_req[SYSLOG_RUN] == 1u);
|
||||
syslog_drain();
|
||||
ok = ok && (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 1)
|
||||
&& (s_clear_req[SYSLOG_RUN] == 0u);
|
||||
CHECK(ok, "TC-SYSLOG-118 duplicate asks merge into exactly one unlink (the ask is a flag, not a counter)");
|
||||
|
||||
/* 119 只删自己:清 RUN 不许碰 SELFTEST,也不许碰取证文件 wdev.log ——
|
||||
* 三个名字都是严格 8.3 短名,逐个对应,不存在"通配删除"。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "r");
|
||||
(void)s_append(SYSLOG_SELFTEST, "s");
|
||||
(void)ff_fake_mkfile_size(P_WDEV, 128u);
|
||||
ok = (syslog_clear_apply(SYSLOG_RUN) == 0)
|
||||
&& (ff_fake_exists(P_RUN) == 0)
|
||||
&& (ff_fake_exists(P_SELFTEST) == 1)
|
||||
&& (ff_fake_exists(P_WDEV) == 1)
|
||||
&& (strcmp(g_ff_obs.last_unlink_path, "/LOG/RUN.LOG") == 0);
|
||||
CHECK(ok, "TC-SYSLOG-119 clearing one id never touches the other log or the watchdog evidence file");
|
||||
|
||||
/* 120 入参守卫:三处公开入口都必须在取锁之前挡住,并且不污染合法 id 的状态 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
ok = (syslog_request_clear((syslog_id_t)SYSLOG_COUNT) == -1)
|
||||
&& (syslog_clear_apply((syslog_id_t)SYSLOG_COUNT) == -1)
|
||||
&& (syslog_clear_state((syslog_id_t)99u) == SYSLOG_CLEAR_NONE)
|
||||
&& (s_lock_waits == 0)
|
||||
&& (s_lock_releases == 0)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_UNLINK] == 0);
|
||||
CHECK(ok, "TC-SYSLOG-120 out-of-range id is refused before the lock and the disk (a guard behind the lock occupies the volume for nothing)");
|
||||
reset_all();
|
||||
ready_only();
|
||||
ok = (syslog_request_clear(SYSLOG_RUN) == 0)
|
||||
&& (syslog_request_clear((syslog_id_t)SYSLOG_COUNT) == -1)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT)
|
||||
&& (syslog_clear_state((syslog_id_t)SYSLOG_COUNT) == SYSLOG_CLEAR_NONE);
|
||||
CHECK(ok, "TC-SYSLOG-120b a rejected request leaves a valid id's pending ask and state untouched");
|
||||
|
||||
/* 121 读侧:删除成功必须让读者的 8 条窗口作废(否则屏上显示"已清除"而记录还在);
|
||||
* 失败路径不许自增 —— 盘上什么都没动,窗口里仍是真数据。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
for (i = 0u; i < 3u; i++) {
|
||||
(void)s_append(SYSLOG_RUN, "w");
|
||||
}
|
||||
{
|
||||
uint32_t gen = 0u;
|
||||
ok = (syslog_get_record(SYSLOG_RUN, 2u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
|
||||
gen = s_rd_gen[SYSLOG_RUN];
|
||||
ok = ok && (syslog_clear_apply(SYSLOG_RUN) == 0)
|
||||
&& (s_rd_gen[SYSLOG_RUN] == gen + 1u)
|
||||
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == -1);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-121 a successful clear invalidates the reader window (no records left to show)");
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)s_append(SYSLOG_RUN, "keep");
|
||||
ok = (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0);
|
||||
{
|
||||
uint32_t gen = s_rd_gen[SYSLOG_RUN];
|
||||
ff_fake_inject_rc(P_RUN, FF_FAKE_OP_UNLINK, FR_DISK_ERR, 1);
|
||||
ok = ok && (syslog_clear_apply(SYSLOG_RUN) == -1)
|
||||
&& (s_rd_gen[SYSLOG_RUN] == gen)
|
||||
&& (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) == 0)
|
||||
&& (strcmp(desc, "keep") == 0);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-121b a failed clear neither bumps the generation nor loses the cached valid record");
|
||||
|
||||
/* 122 /log 目录本身不在:假件与真实 ff.c 一样落 FR_NO_PATH。这条刻意**不算**幂等
|
||||
* 成功(记录读不到是真的,但成因是目录没了而不是"被清空",把卷异常洗成
|
||||
* "清除成功"是另一种撒谎),而且本函数一个卷级动作都不做:不补目录、
|
||||
* 不重开、不碰别的对象——补目录归 syslog_mcu_mounted() 每 2s 的重试。 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
ok = (f_unlink("/log") == FR_OK) && (ff_fake_exists("/log") == 0);
|
||||
{
|
||||
int mkdir_base = g_ff_obs.calls[FF_FAKE_OP_MKDIR];
|
||||
ok = ok && (syslog_clear_apply(SYSLOG_RUN) == -1)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == mkdir_base)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_MKDIR] == 0)
|
||||
&& (ff_fake_exists("/log") == 0)
|
||||
&& (g_sys_err_count == 1);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-122 FR_NO_PATH is not counted as an idempotent success: -1, one trace, and zero f_mkdir (no volume-level action on a name/param code)");
|
||||
(void)ff_fake_mkdir_direct("/log"); /* 目录回来了(模拟挂载门补建成功) */
|
||||
ok = (syslog_request_clear(SYSLOG_RUN) == 0)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_WAIT);
|
||||
syslog_drain();
|
||||
ok = ok && (syslog_clear_state(SYSLOG_RUN) == SYSLOG_CLEAR_OK)
|
||||
&& (syslog_count(SYSLOG_RUN) == 0u);
|
||||
CHECK(ok, "TC-SYSLOG-122b FAIL is not sticky: once the directory is back a new request clears normally");
|
||||
}
|
||||
|
||||
/* ===================== L 五类记录与 quiet 投递通道(task-10) ===================== */
|
||||
|
||||
/* 七类记录一枚描述、一枚路径:描述自带类别号,用来证明"id -> 文件"的对应关系
|
||||
* 与两端表序一致(表序错 = 事故安静地写进告警文件,屏上看不出任何毛病)。 */
|
||||
static const char *const s_kind_desc[SYSLOG_COUNT] = {
|
||||
"k0run", "k1selftest", "k2accid", "k3alarm", "k4start", "k5oper", "k6yk"
|
||||
};
|
||||
static const char *const s_kind_path[SYSLOG_COUNT] = {
|
||||
P_RUN, P_SELFTEST, P_ACCID, P_ALARM, P_START, P_OPER, P_YK
|
||||
};
|
||||
|
||||
static void tc_kinds(void)
|
||||
{
|
||||
uint8_t raw[SYSLOG_REC_SIZE];
|
||||
int ok;
|
||||
uint32_t i;
|
||||
int base_entries;
|
||||
|
||||
printf("-- TC-SYSLOG-130..136 five record kinds + quiet posting channel --\n");
|
||||
|
||||
/* 130 一类一个文件:每类只进自己那份,且没有第八个文件被顺手建出来 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
base_entries = ff_fake_count_entries();
|
||||
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
|
||||
(void)s_append((syslog_id_t)i, s_kind_desc[i]);
|
||||
}
|
||||
ok = 1;
|
||||
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
|
||||
if (ff_fake_size(s_kind_path[i]) != SYSLOG_REC_SIZE) { ok = 0; break; }
|
||||
if (!raw_at(s_kind_path[i], 0u, raw)) { ok = 0; break; }
|
||||
if (memcmp(raw + SYSLOG_DESC_OFFS, s_kind_desc[i], strlen(s_kind_desc[i])) != 0) { ok = 0; break; }
|
||||
if (raw[SYSLOG_DESC_OFFS + strlen(s_kind_desc[i])] != 0u) { ok = 0; break; }
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-130 each kind lands in its own file and carries its own description (id-to-path table order is semantics)");
|
||||
CHECK(ff_fake_count_entries() == (base_entries + (int)SYSLOG_COUNT),
|
||||
"TC-SYSLOG-130b exactly seven record files exist, none created under a mistyped 8.3 name");
|
||||
|
||||
/* 130c 五类新路径都过严格 8.3(081 只覆盖老三类 + wdev;扩 id 必须一起扩这条) */
|
||||
ok = 1;
|
||||
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
|
||||
/* 上面已经真打开写成功过;这里再核一次名字本身:主名 <=8、扩展 = log、
|
||||
* 不以点开头、不含非法字符 */
|
||||
const char *p = s_kind_path[i];
|
||||
const char *slash = strrchr(p, '/');
|
||||
size_t body = 0u, ext = 0u;
|
||||
const char *q;
|
||||
|
||||
if ((slash == NULL) || (slash[1] == '.')) { ok = 0; break; }
|
||||
for (q = slash + 1; (*q != '\0') && (*q != '.'); q++) { body++; }
|
||||
if (*q == '.') { for (q++; *q != '\0'; q++) { ext++; } }
|
||||
if ((body == 0u) || (body > 8u) || (ext != 3u)) { ok = 0; break; }
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-130c all seven production paths are strict 8.3 short names (FF_USE_LFN=0)");
|
||||
|
||||
/* 131 各用各的容量上限:accid 到自己那格(512)轮转,且不动邻居 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)ff_fake_mkfile_size(P_ACCID, (SYSLOG_ACCIDENT_MAX - 1u) * SYSLOG_REC_SIZE);
|
||||
(void)ff_fake_mkfile_size(P_ALARM, (SYSLOG_ALARM_MAX - 1u) * SYSLOG_REC_SIZE);
|
||||
(void)put_rec(SYSLOG_ACCIDENT, "cap-a1");
|
||||
ok = (syslog_count(SYSLOG_ACCIDENT) == SYSLOG_ACCIDENT_MAX)
|
||||
&& (ff_fake_size(P_ACCID) == (SYSLOG_ACCIDENT_MAX * SYSLOG_REC_SIZE));
|
||||
(void)put_rec(SYSLOG_ACCIDENT, "cap-a2"); /* 到顶 -> 删除重建,只剩本次一条 */
|
||||
ok = ok && (syslog_count(SYSLOG_ACCIDENT) == 1u)
|
||||
&& (ff_fake_size(P_ACCID) == SYSLOG_REC_SIZE);
|
||||
CHECK(ok && (ff_fake_size(P_ALARM) == ((SYSLOG_ALARM_MAX - 1u) * SYSLOG_REC_SIZE)),
|
||||
"TC-SYSLOG-131 the accident log rotates on its own cap and leaves the alarm log alone");
|
||||
|
||||
/* 131b yk 用的是自己那格(256),不是抄来的 512/1024 —— 三档上限互不相同,
|
||||
* 所以这一条能区分出"s_caps[] 串格"这种安静错误 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
(void)ff_fake_mkfile_size(P_YK, (SYSLOG_YK_MAX - 1u) * SYSLOG_REC_SIZE);
|
||||
(void)put_rec(SYSLOG_YK, "cap-y1");
|
||||
ok = (syslog_count(SYSLOG_YK) == SYSLOG_YK_MAX);
|
||||
(void)put_rec(SYSLOG_YK, "cap-y2");
|
||||
ok = ok && (syslog_count(SYSLOG_YK) == 1u) && (ff_fake_size(P_YK) == SYSLOG_REC_SIZE);
|
||||
CHECK(ok, "TC-SYSLOG-131b the telecontrol log rotates on its own 256-record cap (per-id caps are not shared)");
|
||||
|
||||
/* 132 quiet 通道(队列侧):丢弃要计数,但投递侧一条日志都不许打
|
||||
* ——这正是它存在的唯一理由,退化成 syslog_post 的别名必须被照出来 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain(); /* 建队列 */
|
||||
g_sys_err_count = 0;
|
||||
for (i = 0u; i < (SYSLOG_QUEUE_MAX + 4u); i++) {
|
||||
syslog_post_quiet(SYSLOG_ACCIDENT, "q");
|
||||
}
|
||||
ok = (s_q_cnt == SYSLOG_QUEUE_MAX) && (syslog_post_dropped() == 4u)
|
||||
&& (g_sys_err_count == 0);
|
||||
syslog_drain(); /* 落盘 + drain 侧按差值补一条 */
|
||||
ok = ok && (syslog_count(SYSLOG_ACCIDENT) == SYSLOG_QUEUE_MAX)
|
||||
&& (g_sys_err_count == 1) && (syslog_post_dropped() == 4u);
|
||||
syslog_drain(); /* 计数没前进 -> 不得重复报 */
|
||||
CHECK(ok && (g_sys_err_count == 1),
|
||||
"TC-SYSLOG-132 quiet drops are counted and reported once from the writer side, never at the posting site");
|
||||
|
||||
/* 133 清除记录 = 一次真实运维动作 -> 落操作记录;且不影响被清那份的归零判据 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain();
|
||||
syslog_post(SYSLOG_RUN, "before clear");
|
||||
syslog_drain();
|
||||
ok = (syslog_count(SYSLOG_RUN) == 1u);
|
||||
ok = ok && (syslog_request_clear(SYSLOG_RUN) == 0);
|
||||
syslog_drain(); /* 删除落地 + 投递那条操作记录 */
|
||||
ok = ok && (syslog_count(SYSLOG_RUN) == 0u)
|
||||
&& (syslog_clear_state(SYSLOG_RUN) == (int)SYSLOG_CLEAR_OK);
|
||||
syslog_drain(); /* 操作记录这才落盘 */
|
||||
ok = ok && (syslog_count(SYSLOG_OPER) == 1u) && raw_at(P_OPER, 0u, raw)
|
||||
&& (memcmp(raw + SYSLOG_DESC_OFFS, "clear run.log", 13u) == 0);
|
||||
CHECK(ok, "TC-SYSLOG-133 clearing a log writes one operation record naming the file, while the cleared log stays at zero");
|
||||
|
||||
/* 133b 反过来:清 oper.log 自己时不得投递操作记录,否则"清除成功"这一条
|
||||
* 会把刚删空的文件又写成 1 条,直接砸掉 task-4 的「count 归零」判据 */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain();
|
||||
syslog_post(SYSLOG_OPER, "an operation");
|
||||
syslog_drain();
|
||||
ok = (syslog_count(SYSLOG_OPER) == 1u);
|
||||
ok = ok && (syslog_request_clear(SYSLOG_OPER) == 0);
|
||||
syslog_drain();
|
||||
ok = ok && (syslog_clear_state(SYSLOG_OPER) == (int)SYSLOG_CLEAR_OK)
|
||||
&& (syslog_count(SYSLOG_OPER) == 0u);
|
||||
syslog_drain(); /* 再排一轮:真投了自我记录,这轮就写回去 */
|
||||
CHECK(ok && (syslog_count(SYSLOG_OPER) == 0u) && (syslog_post_dropped() == 0u),
|
||||
"TC-SYSLOG-133b clearing the operation log itself posts nothing, so the zeroed-count criterion survives");
|
||||
|
||||
/* 134 守卫:越界 id / NULL 描述在 quiet 通道上既不投递也不计丢弃
|
||||
* ("被守卫挡下"与"满了被丢掉"是两件事,混起来就等于把参数错报成容量故障) */
|
||||
reset_all();
|
||||
ready_only();
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain();
|
||||
g_sys_err_count = 0;
|
||||
syslog_post_quiet((syslog_id_t)SYSLOG_COUNT, "bad-id");
|
||||
syslog_post_quiet(SYSLOG_RUN, NULL);
|
||||
ok = (syslog_post_dropped() == 0u) && (s_q_cnt == 0u) && (s_preboot_cnt == 0u)
|
||||
&& (g_ff_obs.calls[FF_FAKE_OP_OPEN] == 0) && (g_sys_err_count == 0);
|
||||
CHECK(ok, "TC-SYSLOG-134 quiet channel guards bad id and NULL desc without counting them as drops and without touching the volume");
|
||||
|
||||
/* 135 启动前数组侧同样计数、同样静默(两个通道各自独立,与 F 项那两个抑制位
|
||||
* 的分区方式一致;漏掉这一侧就会"运行期有账、启动期没账") */
|
||||
reset_all();
|
||||
ready_only(); /* 内核未跑 -> 走 s_preboot */
|
||||
g_sys_err_count = 0;
|
||||
for (i = 0u; i < (SYSLOG_QUEUE_MAX + 3u); i++) {
|
||||
syslog_post_quiet(SYSLOG_START, "p");
|
||||
}
|
||||
ok = (s_preboot_cnt == SYSLOG_QUEUE_MAX) && (syslog_post_dropped() == 3u)
|
||||
&& (g_sys_err_count == 0);
|
||||
s_kernel_state = osKernelRunning;
|
||||
syslog_drain();
|
||||
CHECK(ok && (syslog_count(SYSLOG_START) == SYSLOG_QUEUE_MAX) && (g_sys_err_count == 1),
|
||||
"TC-SYSLOG-135 pre-boot array overflow is counted and traced from the writer side too (8 slots kept, 3 lost)");
|
||||
|
||||
/* 136 五类记录与老两类**逐字节同构**:同一枚 s_format_rec,记录体里没有类型字段
|
||||
* ——这就是"文件即类型"这条决策的机器表达(task-11 页面因此只需一套解析) */
|
||||
reset_all();
|
||||
{
|
||||
uint8_t a[SYSLOG_REC_SIZE];
|
||||
uint8_t b[SYSLOG_REC_SIZE];
|
||||
s_now = 1760234567u;
|
||||
s_clock_ok = 1;
|
||||
s_format_rec(SYSLOG_RUN, "same text", a);
|
||||
s_format_rec(SYSLOG_YK, "same text", b);
|
||||
ok = (memcmp(a, b, SYSLOG_REC_SIZE) == 0);
|
||||
s_format_rec(SYSLOG_ACCIDENT, "same text", b);
|
||||
ok = ok && (memcmp(a, b, SYSLOG_REC_SIZE) == 0);
|
||||
s_format_rec(SYSLOG_OPER, "same text", b);
|
||||
ok = ok && (memcmp(a, b, SYSLOG_REC_SIZE) == 0);
|
||||
}
|
||||
CHECK(ok, "TC-SYSLOG-136 a record body does not encode its kind: same content is byte-identical across all seven kinds");
|
||||
}
|
||||
|
||||
/* ===================== 入口 ===================== */
|
||||
|
||||
int main(void)
|
||||
@@ -1855,6 +2419,8 @@ int main(void)
|
||||
tc_extra_branches();
|
||||
tc_selfheal_codes();
|
||||
tc_post_throttle();
|
||||
tc_clear();
|
||||
tc_kinds();
|
||||
|
||||
printf("\n=== Sys Log Mcu Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
@@ -41,6 +41,25 @@
|
||||
* PE_COUNT == 表长);元件名不超名列宽、非元件/越界回占位
|
||||
* TC-UIDATA-014 注入 reader 后的两行渲染:时标行/内容行分工、单条读盘缓存
|
||||
* (同记录两行只读一次)、IV 时标抑制、读失败退化与不绑空态
|
||||
* ── 以下 6 条 = 定值写路径(道F task-1,docs/任务规划/UI补齐.md §2 第 1 行)──
|
||||
* TC-UIDATA-015 队列深度 4:四格都进得去、第五格满返 -1 且状态转 ERR_BUSY;
|
||||
* FIFO 顺序(最后投的一条最后落定)、空队列 drain 返 0、
|
||||
* drain(NULL) 返 -1 且不动队列、投递成功即转 WAIT
|
||||
* TC-UIDATA-016 请求表示法:NULL/带非法位 mask 返 -2 且状态与队列都不动;
|
||||
* mask=0 空请求返 0 且直接落 OK 不占槽;请求尺寸与队列 .bss
|
||||
* 预算钉死(168 B/格 × 4 格)——加深或加宽必须先过这条
|
||||
* TC-UIDATA-017 正常往返:请求 → WAIT → drain → OK → 新值可读;**只提交 mask
|
||||
* 内的 id**(未提交的槽一位都不动,这是"从显示值回提全表会
|
||||
* 顺手改掉返回系数"那条坑的机器防线)
|
||||
* TC-UIDATA-018 越界不改值:整批预检任一项越界 → ERR_RANGE 且**一字不写**
|
||||
* (含同批里合法的那几项)+ err_mask 指认越界项 + 下一次成功
|
||||
* 落定后 err_mask 自动清零
|
||||
* TC-UIDATA-019 闭锁拒写:locked 置位 → ERR_LOCKED、丢弃本条(不排队等解锁)、
|
||||
* 值区一字不动;解闭锁后重新提交可成功
|
||||
* TC-UIDATA-020 apply 中途失败(预检与执行不一致的防御分支):如实 ERR_RANGE,
|
||||
* 已生效的前几项不回滚(prot_set 无回滚接口,谎称整批未写更坏)
|
||||
* TC-UIDATA-021 v2 帧扩展:UI_DATA_VER == 2、既有字段偏移逐字节见证(只允许尾
|
||||
* 追加)、cal_block 两种态(未标定/已标定)整帧往返
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
@@ -49,6 +68,7 @@
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stddef.h> /* offsetof:v2 帧布局见证(只许尾追加) */
|
||||
#include <string.h>
|
||||
|
||||
/* 生产源文件原样纳入(纯内存叶子,零平台依赖,直接编) */
|
||||
@@ -521,6 +541,351 @@ static void tc_soe_render(void)
|
||||
ui_w_soe_bind(NULL);
|
||||
}
|
||||
|
||||
/* ====== TC-UIDATA-015~020:定值写路径(队列 + 状态机 + 注入执行侧) ====== */
|
||||
|
||||
/*
|
||||
* 假执行侧(替掉 App/algo/prot_set):一张 40 项值区 + 一张上下限表。
|
||||
* ⚠ 本单测不链 App 侧任何 .c(prot_set 依赖 Flash/校验和编排,且写路径的**契约**
|
||||
* 本来就只是"三个函数的返回值"),所以测的是 ui_data.c 里那份真实的队列 +
|
||||
* 状态机代码:越界不改值、闭锁拒写、只提交改动 id 三条判据都落在生产代码上,
|
||||
* 不是测试里的副本。真实 App 实现的等价性由 ui_data_feed.c 的三个包函数
|
||||
* (feed_param_locked/check/apply)在代码审查与台架侧保证。
|
||||
*/
|
||||
#define FAKE_LO_ALL (-100000) /* 默认宽松下限:单条 TC 里再逐行收紧 */
|
||||
#define FAKE_HI_ALL (100000)
|
||||
|
||||
static int32_t s_fake_val[UI_DATA_SET_N];
|
||||
static int32_t s_fake_lo[UI_DATA_SET_N];
|
||||
static int32_t s_fake_hi[UI_DATA_SET_N];
|
||||
static uint8_t s_fake_locked;
|
||||
static uint8_t s_fake_poison; /* 0xFF = 不投毒;否则该 id 的 apply 必失败 */
|
||||
static uint32_t s_fake_apply_calls;
|
||||
|
||||
static uint8_t fake_locked(void)
|
||||
{
|
||||
return s_fake_locked;
|
||||
}
|
||||
|
||||
static int fake_check(uint8_t a_id, int32_t a_q10)
|
||||
{
|
||||
if (a_id >= (uint8_t)UI_DATA_SET_N) {
|
||||
return -1;
|
||||
}
|
||||
if ((a_q10 < s_fake_lo[a_id]) || (a_q10 > s_fake_hi[a_id])) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fake_apply(uint8_t a_id, int32_t a_q10)
|
||||
{
|
||||
if (fake_check(a_id, a_q10) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if ((s_fake_poison != 0xFFu) && (a_id == s_fake_poison)) {
|
||||
return -1; /* 投毒:模拟"预检过了但执行侧仍拒"(理论不可达的防御分支) */
|
||||
}
|
||||
s_fake_val[a_id] = a_q10;
|
||||
s_fake_apply_calls++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const ui_data_param_ops_t s_fake_ops = { fake_locked, fake_check, fake_apply };
|
||||
|
||||
/* 每个 TC 开头调一次:值区填可辨识的旧值 + 快照与队列复位 */
|
||||
static void fake_reset(void)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
|
||||
s_fake_val[i] = (int32_t)i * 1000;
|
||||
s_fake_lo[i] = FAKE_LO_ALL;
|
||||
s_fake_hi[i] = FAKE_HI_ALL;
|
||||
}
|
||||
s_fake_locked = 0u;
|
||||
s_fake_poison = 0xFFu;
|
||||
s_fake_apply_calls = 0u;
|
||||
ui_data_init();
|
||||
}
|
||||
|
||||
/* 投一条只改若干项的请求;未列出的 id 不进 mask(= 本次不提交) */
|
||||
static int fake_submit(const uint8_t *a_ids, const int32_t *a_vals, uint8_t a_n)
|
||||
{
|
||||
ui_data_param_req_t req;
|
||||
uint8_t i;
|
||||
|
||||
(void)memset(&req, 0, sizeof(req));
|
||||
for (i = 0u; i < a_n; i++) {
|
||||
req.mask |= ((uint64_t)1u << a_ids[i]);
|
||||
req.q10[a_ids[i]] = a_vals[i];
|
||||
}
|
||||
return ui_data_param_request(&req);
|
||||
}
|
||||
|
||||
static int32_t fake_val(uint8_t a_id)
|
||||
{
|
||||
return s_fake_val[a_id];
|
||||
}
|
||||
|
||||
static void tc_param_queue(void)
|
||||
{
|
||||
ui_data_param_req_t req;
|
||||
uint8_t k;
|
||||
int accepted = 1;
|
||||
int was_wait = 1;
|
||||
|
||||
fake_reset();
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE,
|
||||
"TC-UIDATA-015a state IDLE after init (never submitted)");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015b drain on empty queue == 0");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE,
|
||||
"TC-UIDATA-015c empty drain leaves state alone (not WAIT)");
|
||||
CHECK(ui_data_param_err_mask() == 0u, "TC-UIDATA-015d err_mask clean at idle");
|
||||
|
||||
/* 四格全进得去;每投一条状态就该是 WAIT */
|
||||
(void)memset(&req, 0, sizeof(req));
|
||||
req.mask = 1u; /* 只改 id0 */
|
||||
for (k = 0u; k < UI_DATA_PARAM_QD; k++) {
|
||||
req.q10[0] = (int32_t)(1000 + (int32_t)k);
|
||||
if (ui_data_param_request(&req) != 0) {
|
||||
accepted = 0;
|
||||
}
|
||||
if (ui_data_param_state() != UI_DATA_PARAM_ST_WAIT) {
|
||||
was_wait = 0;
|
||||
}
|
||||
}
|
||||
CHECK(UI_DATA_PARAM_QD == 4u, "TC-UIDATA-015e queue depth is 4 (contract witness)");
|
||||
CHECK(accepted == 1, "TC-UIDATA-015f four requests all accepted");
|
||||
CHECK(was_wait == 1, "TC-UIDATA-015g state WAIT while queued");
|
||||
|
||||
/* 第五格:满 → 返 -1 + 状态 ERR_BUSY(在队四条一条不掉) */
|
||||
req.q10[0] = 9999;
|
||||
CHECK(ui_data_param_request(&req) == -1, "TC-UIDATA-015h fifth request rejected (-1)");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_BUSY,
|
||||
"TC-UIDATA-015i rejected request surfaces ERR_BUSY");
|
||||
|
||||
/* FIFO:前三条落定后仍是 WAIT,第四条才落终态;值 = 最后投的那条(1003) */
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-015j drain processes one");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT,
|
||||
"TC-UIDATA-015k still WAIT with 3 requests left in queue");
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT,
|
||||
"TC-UIDATA-015l WAIT until the last queued request settles");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-015m fourth request drained");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-015n terminal OK when drained");
|
||||
CHECK((fake_val(0) == 1003) && (s_fake_apply_calls == 4u),
|
||||
"TC-UIDATA-015o FIFO: last queued value lands, all four applied");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015p queue empty again");
|
||||
|
||||
/* ops 无效:不动队列也不动状态 */
|
||||
fake_reset();
|
||||
CHECK(fake_submit(NULL, NULL, 0u) == 0, "TC-UIDATA-015q empty request accepted (see 016)");
|
||||
CHECK(ui_data_param_drain(NULL) == -1, "TC-UIDATA-015r drain(NULL) == -1");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK,
|
||||
"TC-UIDATA-015s failed drain does not rewrite state");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-015t failed drain does not consume");
|
||||
}
|
||||
|
||||
static void tc_param_request_shape(void)
|
||||
{
|
||||
ui_data_param_req_t req;
|
||||
|
||||
fake_reset();
|
||||
CHECK(ui_data_param_request(NULL) == -2, "TC-UIDATA-016a request(NULL) == -2");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_IDLE,
|
||||
"TC-UIDATA-016b invalid request leaves state IDLE (page must not show ERR for a bug)");
|
||||
|
||||
(void)memset(&req, 0, sizeof(req));
|
||||
req.mask = ((uint64_t)1u << 63u); /* 第 63 项不存在 */
|
||||
CHECK(ui_data_param_request(&req) == -2, "TC-UIDATA-016c mask with bit63 == -2");
|
||||
req.mask = ((uint64_t)1u << (UI_DATA_SET_N - 1u)); /* 第 39 项 = 最后一项,合法 */
|
||||
CHECK(ui_data_param_request(&req) == 0, "TC-UIDATA-016d last legal bit accepted");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-016e drained last-bit request");
|
||||
CHECK(s_fake_apply_calls == 1u, "TC-UIDATA-016f item 39 really reached the executor");
|
||||
|
||||
/* 空请求:不占槽、直接 OK("没改东西却按了保存"是正常操作) */
|
||||
fake_reset();
|
||||
req.mask = 0u;
|
||||
CHECK(ui_data_param_request(&req) == 0, "TC-UIDATA-016g empty request returns 0");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-016h empty request settles OK");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0, "TC-UIDATA-016i empty request consumed no slot");
|
||||
|
||||
/* 内存预算护栏:请求是 .bss 里的定长槽,168 B/格 × 4 格 = 672 B
|
||||
* (台账 docs/内存管理/RAM使用分析与优化方案.md;扩深度/加宽必须先过这条) */
|
||||
CHECK(sizeof(ui_data_param_req_t) == 168u, "TC-UIDATA-016j request is 168 bytes");
|
||||
CHECK(sizeof(s_pq) == (672u), "TC-UIDATA-016k queue .bss is 4 x 168 = 672 bytes");
|
||||
CHECK(sizeof(s_pq) <= 768u, "TC-UIDATA-016l queue .bss within budget (<=768B)");
|
||||
CHECK((uint32_t)UI_DATA_SET_N <= 64u, "TC-UIDATA-016m item count fits the 64-bit mask");
|
||||
}
|
||||
|
||||
static void tc_param_roundtrip(void)
|
||||
{
|
||||
const uint8_t ids[3] = { 2u, 3u, 7u };
|
||||
const int32_t vals[3] = { 12345, 60000, 7 };
|
||||
|
||||
fake_reset();
|
||||
CHECK(fake_submit(ids, vals, 3u) == 0, "TC-UIDATA-017a batch of three submitted");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_WAIT, "TC-UIDATA-017b WAIT before drain");
|
||||
CHECK(fake_val(2) == 2000, "TC-UIDATA-017c old value still in effect before drain");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-017d drained one request");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_OK, "TC-UIDATA-017e OK after drain");
|
||||
CHECK((fake_val(2) == 12345) && (fake_val(3) == 60000) && (fake_val(7) == 7),
|
||||
"TC-UIDATA-017f new values readable after roundtrip");
|
||||
/* 只提交改动项:mask 外的槽一位都不动(Q10 往返精度坑的机器防线) */
|
||||
CHECK((fake_val(0) == 0) && (fake_val(1) == 1000) && (fake_val(4) == 4000)
|
||||
&& (fake_val(6) == 6000) && (fake_val(8) == 8000)
|
||||
&& (fake_val(UI_DATA_SET_N - 1u) == (int32_t)(UI_DATA_SET_N - 1u) * 1000),
|
||||
"TC-UIDATA-017g untouched items are byte-identical (mask-only commit)");
|
||||
CHECK(s_fake_apply_calls == 3u, "TC-UIDATA-017h exactly the masked items were applied");
|
||||
|
||||
/* 回环的后半段:App 落值后重发参数帧 → 页面按新 seq 读到新值 */
|
||||
{
|
||||
ui_data_par_t frame;
|
||||
ui_data_par_t got;
|
||||
uint8_t i;
|
||||
|
||||
(void)memset(&frame, 0, sizeof(frame));
|
||||
frame.valid = 1u;
|
||||
frame.checksum = 0x1234u;
|
||||
frame.cal_block = 0u;
|
||||
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
|
||||
frame.q10[i] = s_fake_val[i];
|
||||
}
|
||||
CHECK(ui_data_publish_par(&frame) == 0, "TC-UIDATA-017i par frame republished");
|
||||
(void)memset(&got, 0, sizeof(got));
|
||||
CHECK((ui_data_par(&got) == 0) && (got.q10[2] == 12345) && (got.q10[7] == 7),
|
||||
"TC-UIDATA-017j page reads back the committed values");
|
||||
}
|
||||
}
|
||||
|
||||
static void tc_param_range_atomic(void)
|
||||
{
|
||||
/* 非 const:本 TC 复用同一组数组跑三种越界组合(改了再投) */
|
||||
uint8_t ids[2] = { 2u, 5u };
|
||||
int32_t vals[2] = { 12345, 99999 };
|
||||
|
||||
fake_reset();
|
||||
s_fake_hi[5] = 100; /* 第 5 项上限 100 → 99999 越界;第 2 项本身合法 */
|
||||
CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-018a out-of-range batch accepted for queue");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-018b drained");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_RANGE, "TC-UIDATA-018c state ERR_RANGE");
|
||||
CHECK(ui_data_param_err_mask() == ((uint64_t)1u << 5u),
|
||||
"TC-UIDATA-018d err_mask points at the offending item");
|
||||
CHECK((fake_val(2) == 2000) && (fake_val(5) == 5000),
|
||||
"TC-UIDATA-018e all-or-nothing: the legal item in the same batch NOT written");
|
||||
CHECK(s_fake_apply_calls == 0u, "TC-UIDATA-018f precheck blocks every apply call");
|
||||
|
||||
/* 多项同时越界:err_mask 全给(页面逐行反显,不是只报第一行) */
|
||||
fake_reset();
|
||||
s_fake_lo[6] = 500;
|
||||
s_fake_hi[5] = 100;
|
||||
ids[0] = 5u;
|
||||
ids[1] = 6u;
|
||||
vals[0] = 99999; /* 超上限 */
|
||||
vals[1] = 60; /* 低于下限 */
|
||||
CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-018g two-item bad batch queued");
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
CHECK(ui_data_param_err_mask() == (((uint64_t)1u << 5u) | ((uint64_t)1u << 6u)),
|
||||
"TC-UIDATA-018h err_mask lists every offending item (both bounds)");
|
||||
|
||||
/* 下一次成功落定 → err_mask 自动清零(否则页面把上次越界显示成这次) */
|
||||
ids[0] = 2u;
|
||||
vals[0] = 12345;
|
||||
CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-018i good request queued");
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
CHECK((ui_data_param_state() == UI_DATA_PARAM_ST_OK) && (ui_data_param_err_mask() == 0u),
|
||||
"TC-UIDATA-018j err_mask cleared on next successful commit");
|
||||
}
|
||||
|
||||
static void tc_param_locked(void)
|
||||
{
|
||||
const uint8_t ids[1] = { 2u };
|
||||
const int32_t vals[1] = { 12345 };
|
||||
|
||||
fake_reset();
|
||||
s_fake_locked = 1u;
|
||||
CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-019a request queued while locked");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-019b drained under lock");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_LOCKED, "TC-UIDATA-019c state ERR_LOCKED");
|
||||
CHECK((fake_val(2) == 2000) && (s_fake_apply_calls == 0u),
|
||||
"TC-UIDATA-019d locked commit writes nothing (no apply call)");
|
||||
CHECK(ui_data_param_err_mask() == 0u, "TC-UIDATA-019e lock failure is not a range failure");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 0,
|
||||
"TC-UIDATA-019f locked request is dropped, not queued waiting for unlock");
|
||||
|
||||
/* 解闭锁后重新提交 → OK(闭锁丢弃的是那一条,不是永久拉黑写路径) */
|
||||
s_fake_locked = 0u;
|
||||
CHECK(fake_submit(ids, vals, 1u) == 0, "TC-UIDATA-019g resubmit after unlock accepted");
|
||||
(void)ui_data_param_drain(&s_fake_ops);
|
||||
CHECK((ui_data_param_state() == UI_DATA_PARAM_ST_OK) && (fake_val(2) == 12345),
|
||||
"TC-UIDATA-019h write path healthy again after unlock");
|
||||
}
|
||||
|
||||
static void tc_param_apply_fail(void)
|
||||
{
|
||||
const uint8_t ids[2] = { 3u, 7u };
|
||||
const int32_t vals[2] = { 33333, 77777 };
|
||||
|
||||
fake_reset();
|
||||
s_fake_poison = 7u; /* 预检全过、执行侧仍拒第 7 项(两真源不一致的防御分支) */
|
||||
CHECK(fake_submit(ids, vals, 2u) == 0, "TC-UIDATA-020a request with poisoned item queued");
|
||||
CHECK(ui_data_param_drain(&s_fake_ops) == 1, "TC-UIDATA-020b drained");
|
||||
CHECK(ui_data_param_state() == UI_DATA_PARAM_ST_ERR_RANGE,
|
||||
"TC-UIDATA-020c apply failure reported as ERR_RANGE (not silent OK)");
|
||||
CHECK(ui_data_param_err_mask() == ((uint64_t)1u << 7u), "TC-UIDATA-020d failing item named");
|
||||
CHECK(fake_val(3) == 33333, "TC-UIDATA-020e earlier item already applied is NOT rolled back");
|
||||
CHECK(fake_val(7) == 7000, "TC-UIDATA-020f failing item itself unchanged");
|
||||
}
|
||||
|
||||
static void tc_param_frame_v2(void)
|
||||
{
|
||||
ui_data_par_t src;
|
||||
ui_data_par_t got;
|
||||
|
||||
/* 版本号是页面与写侧唯一对齐凭据;v2 = 尾追加 cal_block */
|
||||
CHECK(UI_DATA_VER == 2u, "TC-UIDATA-021a contract version is 2");
|
||||
/* v1 布局逐字节见证:字段只允许尾追加,任何插入/重排都会让下面四条红
|
||||
* (页面按名字取字段,错位是静默的——只有把偏移钉死才照得出来) */
|
||||
CHECK(offsetof(ui_data_par_t, seq) == 0u, "TC-UIDATA-021b seq offset unchanged");
|
||||
CHECK(offsetof(ui_data_par_t, bad_mask) == 16u, "TC-UIDATA-021c bad_mask offset unchanged");
|
||||
CHECK(offsetof(ui_data_par_t, q10) == 24u, "TC-UIDATA-021d q10 offset unchanged");
|
||||
CHECK(offsetof(ui_data_par_t, cal_block) == 184u,
|
||||
"TC-UIDATA-021e cal_block is the appended tail field (offset 184)");
|
||||
CHECK(sizeof(ui_data_par_t) == 192u, "TC-UIDATA-021f par frame is 192 B (+8 for v2)");
|
||||
/* 写侧组装缓冲是 rt/par/info 三帧共用的 union(App/app/ui_data_feed.c 的 s_scr):
|
||||
* rt 恒大于 par,所以给 par 追加字段**不会**连带抬升那块 .bss——加字段之前
|
||||
* 先确认这条,否则每次尾追加都悄悄多啃一份 RAM(台账只记总量,看不出是谁长的) */
|
||||
CHECK(sizeof(ui_data_rt_t) == 208u, "TC-UIDATA-021g rt frame is 208 B (union stays put)");
|
||||
CHECK(sizeof(ui_data_par_t) < sizeof(ui_data_rt_t),
|
||||
"TC-UIDATA-021h par still smaller than rt (shared scratch unchanged)");
|
||||
|
||||
/* 闭锁态:1 = 未标定(写侧按 UA/UN/IA 三路 VALID 现算,本层只存真值) */
|
||||
ui_data_init();
|
||||
(void)memset(&src, 0, sizeof(src));
|
||||
src.valid = 1u;
|
||||
src.locked = 0u;
|
||||
src.cal_block = 1u;
|
||||
CHECK(ui_data_publish_par(&src) == 0, "TC-UIDATA-021g publish blocked frame");
|
||||
(void)memset(&got, 0, sizeof(got));
|
||||
CHECK((ui_data_par(&got) == 0) && (got.cal_block == 1u) && (got.locked == 0u),
|
||||
"TC-UIDATA-021h cal_block=1 roundtrip, distinct from locked");
|
||||
|
||||
/* 解卡态:写侧填 0 后覆盖发布(同一份存储不会粘住旧值) */
|
||||
src.cal_block = 0u;
|
||||
(void)ui_data_publish_par(&src);
|
||||
(void)memset(&got, 0xAA, sizeof(got));
|
||||
CHECK((ui_data_par(&got) == 0) && (got.cal_block == 0u),
|
||||
"TC-UIDATA-021i cal_block=0 roundtrip (no stale block flag)");
|
||||
|
||||
/* 写侧默认值纪律:调用方清零后不填本字段 = 0(未闭锁),首帧不会假报徽标 */
|
||||
(void)memset(&src, 0, sizeof(src));
|
||||
src.valid = 1u;
|
||||
(void)ui_data_publish_par(&src);
|
||||
(void)ui_data_par(&got);
|
||||
CHECK(got.cal_block == 0u, "TC-UIDATA-021j zero-initialized frame means not blocked");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
|
||||
int main(void)
|
||||
@@ -551,6 +916,20 @@ int main(void)
|
||||
tc_soe_elms();
|
||||
printf("-- soe render --\n");
|
||||
tc_soe_render();
|
||||
printf("-- param write path: queue --\n");
|
||||
tc_param_queue();
|
||||
printf("-- param write path: request shape --\n");
|
||||
tc_param_request_shape();
|
||||
printf("-- param write path: roundtrip --\n");
|
||||
tc_param_roundtrip();
|
||||
printf("-- param write path: range atomic --\n");
|
||||
tc_param_range_atomic();
|
||||
printf("-- param write path: locked --\n");
|
||||
tc_param_locked();
|
||||
printf("-- param write path: apply failure --\n");
|
||||
tc_param_apply_fail();
|
||||
printf("-- param frame v2 (cal_block) --\n");
|
||||
tc_param_frame_v2();
|
||||
|
||||
printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
Reference in New Issue
Block a user