Files
STM32F4-Base/test/host/prot_set/prot_set_test.c
T
wandering1 bda7ab2b50 refactor(编号): 16 个测试模块码单词化 + 门禁识别面连带放宽
批 2(用户授权改名 + 「新名要一眼看懂」裁定)
- 16 个有歧义模块码单词化,共 1,146 处随 scripts/rename_ids.py 改写:
  AI→ADCIN、AH→ADCHEALTH、PS→PROTSET、PE→PROTELEM、PH→PHASOR、
  TS→TIMESYNC、VL→VOLLOCK、NP→NANDPORT、CFG→CONFIG、CLK→SYSCLK、
  WD→WATCHDOG、STO→STORAGE、SH→SHA256、FA→FTPAUTH、FR→FREQ、104→IEC104
- 不动 NET/GD5F/RTC/SOE/UI/ADC/LOG/RS485/TOOL(已是单词、芯片名或业界通用词,
  改了纯属 churn)
- 别名按全局登记:这批字面量已完全限定(TC-<码>-<nnn>),不存在裸形多义
- 映射表存档 scripts/rename_maps/batch2.json 作审计轨迹;§6 模块码表同步

连带修门禁(关键教训)
- 模块码涨到 9 位(ADCHEALTH/WATCHDOG)后,候选正则与族正则的 {0,4} 上界
  会让改名后的号**整批从门禁眼里消失**:实测编号字面量从 8271 掉到 7315、
  悬空假降 490 处。已放宽到 {0,9} 并写进 §6 注记
- 结论:改名批次必须连带复核识别面,只改数据不改判据 = 静默放行;
  这次是靠「可判定比例跌破基线即失败」那条线拦下来的

现状与遗留
- 校验 1 未登记前缀 0 处;可判定比例 82.71%(基线 82.6%,只涨不跌成立)
- 遗留:7 个模块的「单元测试说明.md」仍缺(PROTSET 116、NANDPORT 109、
  PROTELEM 65、FREQ 48、FTPAUTH 45、SHA256 25、SOE 14 处悬空)——改名不解决
  缺文档,处置方式待议
- 遗留:T-2/T-3/T-4/T-6 历史任务号悬空(别名表未收全,属本工具批次漏项)
- 未改任何 C/H 可编译语义(仅注释与宏名内的编号),故不需 @build
2026-10-21 09:53:28 +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 L5-1):
* TC-PROTSET-001 init:默认值装载、越界位图 0、selfcheck 通过、未闭锁
* TC-PROTSET-002 set:范围内写入生效;越界拒写且原值不动
* TC-PROTSET-003 校验和封存语义:set 后未 seal → selfcheck 失败并闭锁;
* seal → 通过并解锁(L5-1 核心闭环)
* 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;
}