/* * 模块名称:保护定值模型(P1)主机侧单元测试 * 模块功能:在 PC 上对 App/algo/prot_set.c 做离线验证。 * * 被测对象:真实源文件(#include 原样纳入)。纯逻辑,无需桩。 * * 覆盖点(docs/算法/继电保护功能复现规划.md §5.2 / §3.4 L5-1): * TC-PS-001 init:默认值装载、越界位图 0、selfcheck 通过、未闭锁 * TC-PS-002 set:范围内写入生效;越界拒写且原值不动 * TC-PS-003 校验和封存语义:set 后未 seal → selfcheck 失败并闭锁; * seal → 通过并解锁(L5-1 核心闭环) * TC-PS-004 load_raw:合法整表装载并封存;含越界项 → 整表不装载 + * bad_mask 指认越界项(定值一致性:宁可全旧不可半新) * TC-PS-005 Q10 往返精度(±1 LSB) * TC-PS-006 元数据表自检:min<=max、投退项 0..1、非零通讯地址不重复 * TC-PS-007 越界编号守卫(get/set) * TC-PS-008 全局软压板 PS_GLOBAL_EN(B7):默认投入、set_sealed 原子写 * 不破坏 selfcheck、脏标志三件套(peek/take/mark) * TC-PS-009 持久化 blob 往返:serialize → init 清场 → parse 恢复 * (位精确 + 封存自洽 + parse 不置脏) * TC-PS-010 损坏 blob 整表拒绝(all-or-nothing):值区位翻转/坏 magic/ * 截断/超长/坏版本/伪造自洽越界 → 全部拒绝且活跃值区一字不动 * TC-PS-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 #include 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-PS-001 */ static void tc_init(void) { prot_set_init(); CHECK(prot_set_bad_mask() == 0u, "TC-PS-001 init: no bad items"); CHECK(prot_set_selfcheck() == 0, "TC-PS-001 init: selfcheck pass"); CHECK(prot_set_locked() == 0u, "TC-PS-001 init: not locked"); CHECK(prot_set_get_f(PS_OC1_CUR) > 9.99f && prot_set_get_f(PS_OC1_CUR) < 10.01f, "TC-PS-001 default OC1 current = 10.0 A"); CHECK(prot_set_get_ctl(PS_OC3_TRIP) == 0u, "TC-PS-001 default OC3 = alarm (trip off)"); } /* TC-PS-002 */ static void tc_set_range(void) { prot_set_init(); CHECK(prot_set_set(PS_OC2_CUR, PROT_SET_Q10(7.5)) == 0, "TC-PS-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-PS-002 value readable after set"); CHECK(prot_set_set(PS_OC2_CUR, PROT_SET_Q10(101.0)) == -1, "TC-PS-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-PS-002 rejected set leaves value unchanged"); CHECK(prot_set_set(PS_OC1_EN, PROT_SET_Q10(2)) == -1, "TC-PS-002 ctl item rejects value 2"); } /* TC-PS-003 */ static void tc_seal_selfcheck(void) { prot_set_init(); CHECK(prot_set_set(PS_OC1_CUR, PROT_SET_Q10(8.0)) == 0, "TC-PS-003 set ok"); CHECK(prot_set_selfcheck() == -1, "TC-PS-003 set-without-seal => selfcheck FAIL (tamper detect)"); CHECK(prot_set_locked() == 1u, "TC-PS-003 selfcheck failure latches lock"); prot_set_seal(); CHECK(prot_set_selfcheck() == 0, "TC-PS-003 seal => selfcheck pass"); CHECK(prot_set_locked() == 0u, "TC-PS-003 seal clears lock"); } /* TC-PS-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-PS-004 valid table loads"); CHECK(prot_set_get_f(PS_OL_CUR) > 1.49f && prot_set_get_f(PS_OL_CUR) < 1.51f, "TC-PS-004 loaded value visible"); CHECK(prot_set_selfcheck() == 0, "TC-PS-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-PS-004 bad table rejected"); CHECK(prot_set_bad_mask() == (1uLL << PS_OC3_DLY), "TC-PS-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-PS-004 rejected load keeps OLD table (all-or-nothing)"); CHECK(prot_set_load_raw(0) == -1, "TC-PS-004 NULL table rejected"); } /* TC-PS-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-PS-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-PS-005 Q10 roundtrip error < 1 LSB"); } } /* TC-PS-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-PS-006 meta: min<=def<=max for all items"); CHECK(ok_ctl, "TC-PS-006 meta: ctl items are 0..1"); CHECK(ok_addr, "TC-PS-006 meta: comm addresses unique"); CHECK((uint32_t)PS_ID_COUNT <= 64u, "TC-PS-006 item count fits 64-bit bad_mask (B8: 40)"); } /* TC-PS-007 */ static void tc_id_guard(void) { prot_set_init(); CHECK(prot_set_get(PS_ID_COUNT) == 0, "TC-PS-007 get out-of-range id -> 0"); CHECK(prot_set_set(PS_ID_COUNT, 0) == -1, "TC-PS-007 set out-of-range id -> -1"); CHECK(prot_set_set_sealed(PS_ID_COUNT, 0) == -1, "TC-PS-007 set_sealed id guard"); } /* TC-PS-008(B7:全局软压板 + 原子写 + 脏标志) */ static void tc_global_plate_dirty(void) { prot_set_init(); CHECK(prot_set_dirty_peek() == 0u, "TC-PS-008 init not dirty"); CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-PS-008 global plate default IN"); CHECK(prot_set_set(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PS-008 plate rejects 2"); CHECK(prot_set_set_sealed(PS_GLOBAL_EN, 0) == 0, "TC-PS-008 plate OUT via set_sealed"); CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-008 plate OUT readable"); CHECK(prot_set_selfcheck() == 0, "TC-PS-008 set_sealed leaves selfcheck PASS (no unsealed window)"); CHECK(prot_set_locked() == 0u, "TC-PS-008 set_sealed no lock"); CHECK(prot_set_dirty_peek() == 1u, "TC-PS-008 set_sealed marks dirty"); CHECK(prot_set_dirty_take() == 1u, "TC-PS-008 dirty take consumes"); CHECK(prot_set_dirty_peek() == 0u, "TC-PS-008 dirty cleared after take"); prot_set_dirty_mark(); CHECK(prot_set_dirty_peek() == 1u, "TC-PS-008 dirty mark re-arms (write-fail retry)"); CHECK(prot_set_set_sealed(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PS-008 set_sealed range check"); CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-008 rejected set_sealed keeps value"); } /* TC-PS-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-PS-009 serialize ok"); CHECK(len == (uint32_t)PROT_SET_BLOB_SZ, "TC-PS-009 blob exact size"); prot_set_init(); CHECK(prot_set_get(PS_OC1_CUR) != cur, "TC-PS-009 init wiped to defaults"); CHECK(prot_set_store_parse(blob, len) == 0, "TC-PS-009 parse ok"); CHECK(prot_set_get(PS_OC1_CUR) == cur, "TC-PS-009 value round-trip bit-exact"); CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-009 plate state round-trip"); CHECK(prot_set_selfcheck() == 0, "TC-PS-009 parsed table sealed"); CHECK(prot_set_dirty_peek() == 0u, "TC-PS-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-PS-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-PS-010 serialize ok"); blob[PROT_SET_BLOB_HDR_SZ] ^= 0x01u; /* 值区一位翻转:sum 对不符 */ CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 flipped value byte rejected (sum pair)"); CHECK(prot_set_get(PS_OC2_CUR) == keep, "TC-PS-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-PS-010 bad magic rejected"); (void)prot_set_store_serialize(blob, sizeof(blob), &len); CHECK(prot_set_store_parse(blob, len - 4u) == -1, "TC-PS-010 truncated blob rejected"); CHECK(prot_set_store_parse(blob, len + 4u) == -1, "TC-PS-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-PS-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-PS-010 forged consistent out-of-range rejected"); CHECK(prot_set_bad_mask() == (1uLL << PS_OC1_CUR), "TC-PS-010 bad_mask points at item"); CHECK(prot_set_get(PS_OC1_CUR) == PROT_SET_Q10(10.0), "TC-PS-010 defaults still intact"); CHECK(prot_set_selfcheck() == 0, "TC-PS-010 active table still self-consistent after all rejects"); } /* TC-PS-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-PS-011 PS_ID_COUNT == 40 (32 + 8 OF/UF)"); CHECK(PROT_SET_BLOB_VER == 2u, "TC-PS-011 blob version bumped to 2 (B8)"); CHECK((uint32_t)PROT_SET_BLOB_SZ == (PROT_SET_BLOB_HDR_SZ + 4u * 40u + 8u), "TC-PS-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-PS-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-PS-011 EN items are ctl, HZ/DLY/RET are Q10 analog"); /* 出厂默认(拟定值)与范围 */ CHECK(prot_set_get(PS_OF_EN) == PROT_SET_Q10(1.0), "TC-PS-011 OF EN default 1"); CHECK(prot_set_get(PS_UF_EN) == PROT_SET_Q10(1.0), "TC-PS-011 UF EN default 1"); CHECK(prot_set_get(PS_OF_HZ) == PROT_SET_Q10(51.0), "TC-PS-011 OF default 51.0 Hz"); CHECK(prot_set_get(PS_UF_HZ) == PROT_SET_Q10(49.0), "TC-PS-011 UF default 49.0 Hz"); CHECK(prot_set_get(PS_OF_DLY) == PROT_SET_Q10(1000.0), "TC-PS-011 OF default 1000 ms"); CHECK(prot_set_get(PS_UF_DLY) == PROT_SET_Q10(1000.0), "TC-PS-011 UF default 1000 ms"); CHECK(prot_set_get(PS_RET_OF) == PROT_SET_Q10(0.99), "TC-PS-011 RET_OF default 0.99"); CHECK(prot_set_get(PS_RET_UF) == PROT_SET_Q10(1.01), "TC-PS-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-PS-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-PS-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-PS-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-PS-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-PS-011 OF EN set_sealed OK"); CHECK(prot_set_get_ctl(PS_OF_EN) == 0u, "TC-PS-011 OF EN readable after seal"); CHECK(prot_set_set_sealed(PS_OF_EN, PROT_SET_Q10(2)) == -1, "TC-PS-011 OF EN rejects Q10(2)"); CHECK(prot_set_selfcheck() == 0, "TC-PS-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-PS-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-PS-011 out-of-range RET_UF rejected"); CHECK(prot_set_bad_mask() == (1uLL << PS_RET_UF), "TC-PS-011 bad_mask bit 39 addressable (64-bit mask, B8)"); CHECK(prot_set_get(PS_RET_UF) == PROT_SET_Q10(1.01), "TC-PS-011 default RET_UF intact"); /* 换代语义:B7 旧档(ver=1 / 32 项 = 144 B)在新固件必被拒 -> 出厂默认 */ CHECK(prot_set_store_parse(blob, 8u + 4u * 32u + 8u) == -1, "TC-PS-011 legacy 32-item blob length rejected"); CHECK(prot_set_selfcheck() == 0, "TC-PS-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; }