/* * 模块名称:保护定值模型(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 PASS(main 返回 0)。 * 创建日期:2026-10-11 */ #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() == (1uL << 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 <= 32u, "TC-PS-006 item count fits bad_mask bitmap"); } /* 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() == (1uL << 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"); } 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(); printf("\n=== Prot Set Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; }