Files
STM32F4-Base/test/host/prot_set/prot_set_test.c
T
wandering1 b7c0889cc7 refactor(编号·批次二): 代码与测试侧 215 处保护功能号改成功能名,零行为改动
用户裁定「不要用字母加数字的编号,语义标识是最好的」后的批次二代码半(道 ui-param 执行,逐文件站点数与
自证见其交付)。唯一口径 = 映射表 ci-artifacts/lprot_map.tsv 的功能名列原文,**零自造同义词**;L2.5 与
L-TEST 经实测各 0 处(不是本族),TC 族一字未动。

固件 22 文件 128 处(adc_task.c 21、phasor.c 22、phasor.h 13…),主机测试 13 文件 87 处(pha
sor_test.c
57…);设计文档指针新增 19 行,另 16 只文件本来就写着规划文档 ⇒ 不重复加。

**两条只有真改过 215 处才会踩到的坑,都写进注释规范实践**:
· 指针**不能接在原句尾部**——那会替原文一个未闭合的半括号封口,实测咬坏 App/algo/sample_ring.h 这类
  跨行句 ⇒ 指针一律**单独成行**;
· 号删掉会留下**叠词**(「自检失败闭锁 闭锁判定」「定值校验和 定值巡检」)⇒ 复扫并收口 4 处,措辞只用
  映射名本身;具体对象需要留住时写成「自检失败闭锁(采样故障闭锁)」。

自证(不靠嘴说):改前把 35 只文件原文备份到仓外 temp,逐文件比 `/*`、`*/` 计数、三类括号计数、行数增量
恰等于指针行数、全角括号增删相等、残留号 0 ⇒ **35/35 通过**;Lead 复核后跑权威 @build **0 错 0 警**,
Program Size `Code=233900 RO=26104 RW=1536 ZI=136808` —— **与三条道各自改前改后测的四数逐
字节同数**,
这就是「只动注释与文本」的机器证据;八套主机单测 exit 0,cppcheck 按钩子参数零发现,coding-standard 0。

文件头「修改记录」里的号**一并换名、不还原**(Lead 裁定):这些号是全仓退役的,留一个查无此号的引用比
留一条改过名的条目更坏——「冻结历史不回改」保护的是**别替历史改口**,不是保护一个再也查不到的字符串;
id-check 对变更记录行本就豁免,两种选择都不影响门禁。
2026-10-27 08:59:26 +08:00

342 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:保护定值模型(P1)主机侧单元测试
* 模块功能:在 PC 上对 App/algo/prot_set.c 做离线验证。
*
* 被测对象:真实源文件(#include 原样纳入)。纯逻辑,无需桩。
*
* 覆盖点(docs/算法/继电保护功能复现规划.md §5.2 / §3.4 定值校验和):
* TC-PROTSET-001 init:默认值装载、越界位图 0、selfcheck 通过、未闭锁
* TC-PROTSET-002 set:范围内写入生效;越界拒写且原值不动
* TC-PROTSET-003 校验和封存语义:set 后未 seal → selfcheck 失败并闭锁;
* seal → 通过并解锁(定值校验和 核心闭环)
* TC-PROTSET-004 load_raw:合法整表装载并封存;含越界项 → 整表不装载 +
* bad_mask 指认越界项(定值一致性:宁可全旧不可半新)
* TC-PROTSET-005 Q10 往返精度(±1 LSB)
* TC-PROTSET-006 元数据表自检:min<=max、投退项 0..1、非零通讯地址不重复
* TC-PROTSET-007 越界编号守卫(get/set)
* TC-PROTSET-008 全局软压板 PS_GLOBAL_EN(B7):默认投入、set_sealed 原子写
* 不破坏 selfcheck、脏标志三件套(peek/take/mark)
* TC-PROTSET-009 持久化 blob 往返:serialize → init 清场 → parse 恢复
* (位精确 + 封存自洽 + parse 不置脏)
* TC-PROTSET-010 损坏 blob 整表拒绝(all-or-nothing):值区位翻转/坏 magic/
* 截断/超长/坏版本/伪造自洽越界 → 全部拒绝且活跃值区一字不动
* TC-PROTSET-011 OF/UF 定值行(B8):40 项 / blob 版本 2 / 本地项地址 0 /
* 默认值与范围 / 返回系数独立性 / 64 位 bad_mask(bit 39)/
* B7 旧档(32 项)换代拒绝
*
* 通过:全部 TC PASS(main 返回 0)。
* 创建日期:2026-10-11;2026-10-12 扩 OF/UF(B8)
*/
#include <stdint.h>
#include <stdio.h>
static int s_total = 0, s_passed = 0, s_failed = 0;
#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)
#include "prot_set.c"
/* TC-PROTSET-001 */
static void tc_init(void) {
prot_set_init();
CHECK(prot_set_bad_mask() == 0u, "TC-PROTSET-001 init: no bad items");
CHECK(prot_set_selfcheck() == 0, "TC-PROTSET-001 init: selfcheck pass");
CHECK(prot_set_locked() == 0u, "TC-PROTSET-001 init: not locked");
CHECK(prot_set_get_f(PS_OC1_CUR) > 9.99f && prot_set_get_f(PS_OC1_CUR) < 10.01f,
"TC-PROTSET-001 default OC1 current = 10.0 A");
CHECK(prot_set_get_ctl(PS_OC3_TRIP) == 0u,
"TC-PROTSET-001 default OC3 = alarm (trip off)");
}
/* TC-PROTSET-002 */
static void tc_set_range(void) {
prot_set_init();
CHECK(prot_set_set(PS_OC2_CUR, PROT_SET_Q10(7.5)) == 0,
"TC-PROTSET-002 in-range set accepted");
CHECK(prot_set_get_f(PS_OC2_CUR) > 7.49f && prot_set_get_f(PS_OC2_CUR) < 7.51f,
"TC-PROTSET-002 value readable after set");
CHECK(prot_set_set(PS_OC2_CUR, PROT_SET_Q10(101.0)) == -1,
"TC-PROTSET-002 over-max set rejected");
CHECK(prot_set_get_f(PS_OC2_CUR) > 7.49f && prot_set_get_f(PS_OC2_CUR) < 7.51f,
"TC-PROTSET-002 rejected set leaves value unchanged");
CHECK(prot_set_set(PS_OC1_EN, PROT_SET_Q10(2)) == -1,
"TC-PROTSET-002 ctl item rejects value 2");
}
/* TC-PROTSET-003 */
static void tc_seal_selfcheck(void) {
prot_set_init();
CHECK(prot_set_set(PS_OC1_CUR, PROT_SET_Q10(8.0)) == 0, "TC-PROTSET-003 set ok");
CHECK(prot_set_selfcheck() == -1,
"TC-PROTSET-003 set-without-seal => selfcheck FAIL (tamper detect)");
CHECK(prot_set_locked() == 1u, "TC-PROTSET-003 selfcheck failure latches lock");
prot_set_seal();
CHECK(prot_set_selfcheck() == 0, "TC-PROTSET-003 seal => selfcheck pass");
CHECK(prot_set_locked() == 0u, "TC-PROTSET-003 seal clears lock");
}
/* TC-PROTSET-004 */
static void tc_load_raw(void) {
int32_t tbl[PS_ID_COUNT];
uint32_t i;
prot_set_init();
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
tbl[i] = prot_set_get((prot_set_id_t)i);
}
tbl[PS_OL_CUR] = PROT_SET_Q10(1.5);
CHECK(prot_set_load_raw(tbl) == 0, "TC-PROTSET-004 valid table loads");
CHECK(prot_set_get_f(PS_OL_CUR) > 1.49f && prot_set_get_f(PS_OL_CUR) < 1.51f,
"TC-PROTSET-004 loaded value visible");
CHECK(prot_set_selfcheck() == 0, "TC-PROTSET-004 load_raw auto-seals");
tbl[PS_OC3_DLY] = PROT_SET_Q10(60001.0); /* 越上限 */
tbl[PS_OL_CUR] = PROT_SET_Q10(2.5); /* 合法改动 */
CHECK(prot_set_load_raw(tbl) == -1, "TC-PROTSET-004 bad table rejected");
CHECK(prot_set_bad_mask() == (1uLL << PS_OC3_DLY),
"TC-PROTSET-004 bad_mask points at the offending item");
CHECK(prot_set_get_f(PS_OL_CUR) > 1.49f && prot_set_get_f(PS_OL_CUR) < 1.51f,
"TC-PROTSET-004 rejected load keeps OLD table (all-or-nothing)");
CHECK(prot_set_load_raw(0) == -1, "TC-PROTSET-004 NULL table rejected");
}
/* TC-PROTSET-005 */
static void tc_q10_roundtrip(void) {
float v = 12.345f;
int32_t q;
prot_set_init();
q = PROT_SET_Q10(v);
CHECK(prot_set_set(PS_OC1_CUR, q) == 0, "TC-PROTSET-005 set ok");
{
float back = prot_set_get_f(PS_OC1_CUR);
float err = back - v;
if (err < 0.0f) { err = -err; }
CHECK(err < 0.001f, "TC-PROTSET-005 Q10 roundtrip error < 1 LSB");
}
}
/* TC-PROTSET-006 */
static void tc_meta_sanity(void) {
uint32_t i, j;
int ok_range = 1, ok_ctl = 1, ok_addr = 1;
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
const prot_set_meta_t *m = &g_prot_set_meta[i];
if (m->min > m->max) { ok_range = 0; }
if ((m->def < m->min) || (m->def > m->max)) { ok_range = 0; }
if ((m->is_ctl != 0u) &&
((m->min != 0) || (m->max != PROT_SET_Q10(1)))) { ok_ctl = 0; }
if (m->comm_addr == 0u) { continue; }
for (j = i + 1u; j < (uint32_t)PS_ID_COUNT; j++) {
if (g_prot_set_meta[j].comm_addr == m->comm_addr) { ok_addr = 0; }
}
}
CHECK(ok_range, "TC-PROTSET-006 meta: min<=def<=max for all items");
CHECK(ok_ctl, "TC-PROTSET-006 meta: ctl items are 0..1");
CHECK(ok_addr, "TC-PROTSET-006 meta: comm addresses unique");
CHECK((uint32_t)PS_ID_COUNT <= 64u, "TC-PROTSET-006 item count fits 64-bit bad_mask (B8: 40)");
}
/* TC-PROTSET-007 */
static void tc_id_guard(void) {
prot_set_init();
CHECK(prot_set_get(PS_ID_COUNT) == 0, "TC-PROTSET-007 get out-of-range id -> 0");
CHECK(prot_set_set(PS_ID_COUNT, 0) == -1, "TC-PROTSET-007 set out-of-range id -> -1");
CHECK(prot_set_set_sealed(PS_ID_COUNT, 0) == -1, "TC-PROTSET-007 set_sealed id guard");
}
/* TC-PROTSET-008(B7:全局软压板 + 原子写 + 脏标志) */
static void tc_global_plate_dirty(void) {
prot_set_init();
CHECK(prot_set_dirty_peek() == 0u, "TC-PROTSET-008 init not dirty");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-PROTSET-008 global plate default IN");
CHECK(prot_set_set(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PROTSET-008 plate rejects 2");
CHECK(prot_set_set_sealed(PS_GLOBAL_EN, 0) == 0, "TC-PROTSET-008 plate OUT via set_sealed");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PROTSET-008 plate OUT readable");
CHECK(prot_set_selfcheck() == 0, "TC-PROTSET-008 set_sealed leaves selfcheck PASS (no unsealed window)");
CHECK(prot_set_locked() == 0u, "TC-PROTSET-008 set_sealed no lock");
CHECK(prot_set_dirty_peek() == 1u, "TC-PROTSET-008 set_sealed marks dirty");
CHECK(prot_set_dirty_take() == 1u, "TC-PROTSET-008 dirty take consumes");
CHECK(prot_set_dirty_peek() == 0u, "TC-PROTSET-008 dirty cleared after take");
prot_set_dirty_mark();
CHECK(prot_set_dirty_peek() == 1u, "TC-PROTSET-008 dirty mark re-arms (write-fail retry)");
CHECK(prot_set_set_sealed(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PROTSET-008 set_sealed range check");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PROTSET-008 rejected set_sealed keeps value");
}
/* TC-PROTSET-009(B7:blob 往返) */
static void tc_store_roundtrip(void) {
uint8_t blob[PROT_SET_BLOB_SZ];
uint32_t len = 0u;
int32_t cur;
prot_set_init();
(void)prot_set_set_sealed(PS_OC1_CUR, PROT_SET_Q10(12.5));
(void)prot_set_set_sealed(PS_GLOBAL_EN, 0);
(void)prot_set_dirty_take();
cur = prot_set_get(PS_OC1_CUR);
CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PROTSET-009 serialize ok");
CHECK(len == (uint32_t)PROT_SET_BLOB_SZ, "TC-PROTSET-009 blob exact size");
prot_set_init();
CHECK(prot_set_get(PS_OC1_CUR) != cur, "TC-PROTSET-009 init wiped to defaults");
CHECK(prot_set_store_parse(blob, len) == 0, "TC-PROTSET-009 parse ok");
CHECK(prot_set_get(PS_OC1_CUR) == cur, "TC-PROTSET-009 value round-trip bit-exact");
CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PROTSET-009 plate state round-trip");
CHECK(prot_set_selfcheck() == 0, "TC-PROTSET-009 parsed table sealed");
CHECK(prot_set_dirty_peek() == 0u, "TC-PROTSET-009 parse does not mark dirty");
}
/* 伪造辅助:按 blob 值区重算尾部 sum/sumn(构造"自洽但越界"的对抗样本) */
static void s_forge_tail(uint8_t *b, uint32_t len) {
uint32_t sum = 0u;
uint32_t inv;
uint32_t i;
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
sum += s_get32(b + PROT_SET_BLOB_HDR_SZ + 4u * i);
}
inv = ~sum;
s_put32(b + len - 8u, sum);
s_put32(b + len - 4u, inv);
}
/* TC-PROTSET-010(B7:损坏 blob 整表拒绝,all-or-nothing) */
static void tc_store_corrupt(void) {
uint8_t blob[PROT_SET_BLOB_SZ];
uint32_t len = 0u;
int32_t keep;
prot_set_init();
(void)prot_set_set_sealed(PS_OC2_CUR, PROT_SET_Q10(6.0));
keep = prot_set_get(PS_OC2_CUR);
CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PROTSET-010 serialize ok");
blob[PROT_SET_BLOB_HDR_SZ] ^= 0x01u; /* 值区一位翻转:sum 对不符 */
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PROTSET-010 flipped value byte rejected (sum pair)");
CHECK(prot_set_get(PS_OC2_CUR) == keep, "TC-PROTSET-010 reject leaves values untouched");
(void)prot_set_store_serialize(blob, sizeof(blob), &len);
blob[0] ^= 0xFFu; /* magic 破坏 */
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PROTSET-010 bad magic rejected");
(void)prot_set_store_serialize(blob, sizeof(blob), &len);
CHECK(prot_set_store_parse(blob, len - 4u) == -1, "TC-PROTSET-010 truncated blob rejected");
CHECK(prot_set_store_parse(blob, len + 4u) == -1, "TC-PROTSET-010 oversized blob rejected");
(void)prot_set_store_serialize(blob, sizeof(blob), &len);
blob[5] ^= 0x02u; /* version 高字节:1 -> 0x200 */
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PROTSET-010 version mismatch rejected");
/* 对抗样本:越限定值 + 重算 sum 对(校验全过、范围必拦) */
(void)prot_set_store_serialize(blob, sizeof(blob), &len);
s_put32(blob + PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_OC1_CUR,
(uint32_t)(g_prot_set_meta[PS_OC1_CUR].max + 1));
s_forge_tail(blob, len);
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PROTSET-010 forged consistent out-of-range rejected");
CHECK(prot_set_bad_mask() == (1uLL << PS_OC1_CUR), "TC-PROTSET-010 bad_mask points at item");
CHECK(prot_set_get(PS_OC1_CUR) == PROT_SET_Q10(10.0), "TC-PROTSET-010 defaults still intact");
CHECK(prot_set_selfcheck() == 0, "TC-PROTSET-010 active table still self-consistent after all rejects");
}
/* TC-PROTSET-011(B8:OF/UF 定值行 + 40 项 + 64 位 bad_mask + 换代拒绝) */
static void tc_of_uf_meta(void) {
uint8_t blob[PROT_SET_BLOB_SZ];
uint32_t len = 0u;
prot_set_init();
/* 结构:40 项、blob 版本 2、尺寸随项数自适应 */
CHECK((uint32_t)PS_ID_COUNT == 40u, "TC-PROTSET-011 PS_ID_COUNT == 40 (32 + 8 OF/UF)");
CHECK(PROT_SET_BLOB_VER == 2u, "TC-PROTSET-011 blob version bumped to 2 (B8)");
CHECK((uint32_t)PROT_SET_BLOB_SZ == (PROT_SET_BLOB_HDR_SZ + 4u * 40u + 8u),
"TC-PROTSET-011 blob size = hdr + 4*40 + tail");
/* 地址:参考逐项地址不在本仓库 -> 8 项全部本地项(comm_addr=0,104 不可寻址) */
CHECK((g_prot_set_meta[PS_OF_EN].comm_addr == 0u)
&& (g_prot_set_meta[PS_OF_HZ].comm_addr == 0u)
&& (g_prot_set_meta[PS_OF_DLY].comm_addr == 0u)
&& (g_prot_set_meta[PS_UF_EN].comm_addr == 0u)
&& (g_prot_set_meta[PS_UF_HZ].comm_addr == 0u)
&& (g_prot_set_meta[PS_UF_DLY].comm_addr == 0u)
&& (g_prot_set_meta[PS_RET_OF].comm_addr == 0u)
&& (g_prot_set_meta[PS_RET_UF].comm_addr == 0u),
"TC-PROTSET-011 OF/UF items all local (comm_addr=0, pending reference table)");
/* 投退 = ctl 项(0/1),其余 = 模拟量 Q10 */
CHECK((g_prot_set_meta[PS_OF_EN].is_ctl == 1u) && (g_prot_set_meta[PS_UF_EN].is_ctl == 1u)
&& (g_prot_set_meta[PS_OF_HZ].is_ctl == 0u) && (g_prot_set_meta[PS_RET_UF].is_ctl == 0u),
"TC-PROTSET-011 EN items are ctl, HZ/DLY/RET are Q10 analog");
/* 出厂默认(拟定值)与范围 */
CHECK(prot_set_get(PS_OF_EN) == PROT_SET_Q10(1.0), "TC-PROTSET-011 OF EN default 1");
CHECK(prot_set_get(PS_UF_EN) == PROT_SET_Q10(1.0), "TC-PROTSET-011 UF EN default 1");
CHECK(prot_set_get(PS_OF_HZ) == PROT_SET_Q10(51.0), "TC-PROTSET-011 OF default 51.0 Hz");
CHECK(prot_set_get(PS_UF_HZ) == PROT_SET_Q10(49.0), "TC-PROTSET-011 UF default 49.0 Hz");
CHECK(prot_set_get(PS_OF_DLY) == PROT_SET_Q10(1000.0), "TC-PROTSET-011 OF default 1000 ms");
CHECK(prot_set_get(PS_UF_DLY) == PROT_SET_Q10(1000.0), "TC-PROTSET-011 UF default 1000 ms");
CHECK(prot_set_get(PS_RET_OF) == PROT_SET_Q10(0.99), "TC-PROTSET-011 RET_OF default 0.99");
CHECK(prot_set_get(PS_RET_UF) == PROT_SET_Q10(1.01), "TC-PROTSET-011 RET_UF default 1.01");
CHECK((g_prot_set_meta[PS_OF_HZ].min == PROT_SET_Q10(50.0))
&& (g_prot_set_meta[PS_OF_HZ].max == PROT_SET_Q10(55.0)),
"TC-PROTSET-011 OF range 50.0~55.0 Hz");
CHECK((g_prot_set_meta[PS_UF_HZ].min == PROT_SET_Q10(45.0))
&& (g_prot_set_meta[PS_UF_HZ].max == PROT_SET_Q10(50.0)),
"TC-PROTSET-011 UF range 45.0~50.0 Hz");
/* 频率返回系数与电气量系数独立:返回**门槛** = 定值×系数(Q10 乘积
* /1024)——OF 门槛 < 动作门槛、UF 门槛 > 动作门槛(方向相反) */
CHECK(((int64_t)prot_set_get(PS_OF_HZ) * (int64_t)prot_set_get(PS_RET_OF)) / 1024
< (int64_t)prot_set_get(PS_OF_HZ),
"TC-PROTSET-011 OF return threshold (set x 0.99) below pickup");
CHECK(((int64_t)prot_set_get(PS_UF_HZ) * (int64_t)prot_set_get(PS_RET_UF)) / 1024
> (int64_t)prot_set_get(PS_UF_HZ),
"TC-PROTSET-011 UF return threshold (set x 1.01) above pickup");
/* 投退可经 set_sealed 投/退(YK 口径同 GLOBAL_EN;Q10(2)=2048 越界拒) */
CHECK(prot_set_set_sealed(PS_OF_EN, 0) == 0, "TC-PROTSET-011 OF EN set_sealed OK");
CHECK(prot_set_get_ctl(PS_OF_EN) == 0u, "TC-PROTSET-011 OF EN readable after seal");
CHECK(prot_set_set_sealed(PS_OF_EN, PROT_SET_Q10(2)) == -1, "TC-PROTSET-011 OF EN rejects Q10(2)");
CHECK(prot_set_selfcheck() == 0, "TC-PROTSET-011 no lock after OF/UF seal ops");
/* 64 位 bad_mask:末项(bit 39 > 31)越界可指认 */
(void)prot_set_set_sealed(PS_OC1_CUR, PROT_SET_Q10(11.0));
(void)prot_set_dirty_take();
CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PROTSET-011 serialize ok");
s_put32(blob + PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_RET_UF,
(uint32_t)(g_prot_set_meta[PS_RET_UF].max + 1));
s_forge_tail(blob, len);
CHECK(prot_set_store_parse(blob, len) == -1, "TC-PROTSET-011 out-of-range RET_UF rejected");
CHECK(prot_set_bad_mask() == (1uLL << PS_RET_UF),
"TC-PROTSET-011 bad_mask bit 39 addressable (64-bit mask, B8)");
CHECK(prot_set_get(PS_RET_UF) == PROT_SET_Q10(1.01), "TC-PROTSET-011 default RET_UF intact");
/* 换代语义:B7 旧档(ver=1 / 32 项 = 144 B)在新固件必被拒 -> 出厂默认 */
CHECK(prot_set_store_parse(blob, 8u + 4u * 32u + 8u) == -1,
"TC-PROTSET-011 legacy 32-item blob length rejected");
CHECK(prot_set_selfcheck() == 0, "TC-PROTSET-011 active table intact after legacy reject");
}
int main(void) {
printf("=== Prot Set Tests ===\n");
tc_init();
tc_set_range();
tc_seal_selfcheck();
tc_load_raw();
tc_q10_roundtrip();
tc_meta_sanity();
tc_id_guard();
tc_global_plate_dirty();
tc_store_roundtrip();
tc_store_corrupt();
tc_of_uf_meta();
printf("\n=== Prot Set Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}