/* test_ctrlstatus.c — unit tests for the controller status handler * (controller-status spec, task 2.1). * * Exercises ctrlstatus_fill(), the pure local-state snapshot used by * handle_ctrlstatus(). Splitting the snapshot from the socket send (mirroring * ha_nop_reply_state_report) makes the field sourcing unit-testable without an * endpoint. Covers Requirements 1.2, 1.3, 2.1, 2.2, 2.3, 3.1, 3.2, 4.1, 4.2, * 5.1, 5.2, 5.3, 6.1, 6.2, 6.3, 6.4. */ #include #include #include #include #include "ha.h" #include "include/cluster.h" #include "myconf.h" #include "persist.h" #include "xce/cluster_def.h" /* ── helpers ────────────────────────────────────────────────── */ static ha_config_t cs_valid_cfg(void) { ha_config_t cfg = { .heartbeat_interval_ms = 3000, .heartbeat_timeout_ms = 10000, .election_timeout_ms = 2000, .probe_connect_timeout_ms = 500, .max_promotion_retries = 5, }; return cfg; } /* Build a minimal cluster_t whose ha/myconf fields the handler reads. * Only the fields ctrlstatus_fill() touches are initialised. */ static void cs_setup_cluster(cluster_t* c, myconf_t* mc) { memset(c, 0, sizeof(*c)); memset(mc, 0, sizeof(*mc)); ha_config_t cfg = cs_valid_cfg(); TEST_ASSERT_EQUAL_INT(0, ha_init(&c->ha, &cfg, 0)); c->myconf = mc; mc->name = "ctrlA"; mc->cluster_config = NULL; /* cluster name read from peers/own identity */ /* own identity (xce_peerinfo_t.name is a char*, not a buffer) */ c->myhost_info.name = "ctrlA"; c->myhost = &c->myhost_info; /* loaded config version + checksum */ mc->config_version_tag = 0xAABBCCDDEE001122ULL; for (int i = 0; i < 32; i++) mc->config_sha256[i] = (uint8_t)(i + 1); /* configured controllers (peers) */ mc->peers_cfg.candidates_count = 3; strncpy(mc->peers_cfg.candidates[0].name, "ctrlA", sizeof(mc->peers_cfg.candidates[0].name) - 1); strncpy(mc->peers_cfg.candidates[0].hostname, "10.0.0.1", sizeof(mc->peers_cfg.candidates[0].hostname) - 1); mc->peers_cfg.candidates[0].port = 7001; strncpy(mc->peers_cfg.candidates[1].name, "ctrlB", sizeof(mc->peers_cfg.candidates[1].name) - 1); strncpy(mc->peers_cfg.candidates[1].hostname, "10.0.0.2", sizeof(mc->peers_cfg.candidates[1].hostname) - 1); mc->peers_cfg.candidates[1].port = 7002; strncpy(mc->peers_cfg.candidates[2].name, "ctrlC", sizeof(mc->peers_cfg.candidates[2].name) - 1); strncpy(mc->peers_cfg.candidates[2].hostname, "10.0.0.3", sizeof(mc->peers_cfg.candidates[2].hostname) - 1); mc->peers_cfg.candidates[2].port = 7003; } static void cs_teardown_cluster(cluster_t* c) { ha_fini(&c->ha); } /* ── tests ──────────────────────────────────────────────────── */ /* (a) PRIMARY: responderRole==PRIMARY, primaryName==self, peers/version * populated; committed/applied/term reflect the atomics. (Req 2.1, 3.1, 3.2, * 4.1, 4.2, 5.1, 6.1, 6.2, 6.4) */ static void test_ctrlstatus_primary(void) { cluster_t c; myconf_t mc; cs_setup_cluster(&c, &mc); atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY, memory_order_release); atomic_store_explicit(&c.ha.term, 7, memory_order_release); atomic_store_explicit(&c.ha.applied_lsn, 42, memory_order_release); atomic_store_explicit(&c.ha.committed_config_tag, 0xAABBCCDDEE001122ULL, memory_order_release); atomic_store_explicit(&c.persist.committed_lsn, 41, memory_order_release); /* A primary knows itself as the primary */ strncpy(c.ha.primary_host, "ctrlA", sizeof(c.ha.primary_host) - 1); c.ha.primary_port = 7001; xce_ctrlstatus_t st; ctrlstatus_fill(&c, &st); TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_PRIMARY, st.responderRole); TEST_ASSERT_NOT_NULL(st.responderName); TEST_ASSERT_EQUAL_STRING("ctrlA", st.responderName); TEST_ASSERT_NOT_NULL(st.primaryName); TEST_ASSERT_EQUAL_STRING("ctrlA", st.primaryName); TEST_ASSERT_EQUAL_UINT64(7, st.term); TEST_ASSERT_EQUAL_UINT64(42, st.appliedLsn); TEST_ASSERT_EQUAL_UINT64(41, st.committedLsn); TEST_ASSERT_EQUAL_UINT64(0xAABBCCDDEE001122ULL, st.configVersion); TEST_ASSERT_EQUAL_UINT64(0xAABBCCDDEE001122ULL, st.committedConfigVersion); /* configChecksum: 64-char lowercase-hex of config_sha256[i]=(i+1), i.e. * bytes 0x01..0x20. (Req 5.2 content checksum, 5.3 divergence detection) */ TEST_ASSERT_NOT_NULL(st.configChecksum); TEST_ASSERT_EQUAL_UINT(64u, (unsigned)strlen(st.configChecksum)); TEST_ASSERT_EQUAL_STRING( "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", st.configChecksum); TEST_ASSERT_EQUAL_UINT32(3, st.peerCount); TEST_ASSERT_EQUAL_INT(3, st.peers.count); TEST_ASSERT_NOT_NULL(st.peers.list); TEST_ASSERT_EQUAL_STRING("ctrlB", st.peers.list[1]->name); TEST_ASSERT_EQUAL_STRING("10.0.0.2", st.peers.list[1]->host); TEST_ASSERT_EQUAL_UINT32(7002, st.peers.list[1]->port); TEST_ASSERT_EQUAL_UINT(0, st.fenced); ctrlstatus_free_fields(&st); cs_teardown_cluster(&c); printf("test_ctrlstatus_primary: PASS\n"); } /* (b) STANDBY with known primary: reports that primary, role==STANDBY. * (Req 2.2, 3.1, 3.2) */ static void test_ctrlstatus_standby_known_primary(void) { cluster_t c; myconf_t mc; cs_setup_cluster(&c, &mc); atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY, memory_order_release); strncpy(c.ha.primary_host, "ctrlB", sizeof(c.ha.primary_host) - 1); c.ha.primary_port = 7002; /* All-0xFF digest → 64 'f' chars: a distinct content checksum from the * primary case proves Req 5.3 — same version tag but differing config bytes * produce a differing hex; identical bytes would yield identical hex. */ for (int i = 0; i < 32; i++) mc.config_sha256[i] = 0xFFu; xce_ctrlstatus_t st; ctrlstatus_fill(&c, &st); TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_STANDBY, st.responderRole); TEST_ASSERT_EQUAL_STRING("ctrlA", st.responderName); TEST_ASSERT_NOT_NULL(st.primaryName); TEST_ASSERT_EQUAL_STRING("ctrlB", st.primaryName); TEST_ASSERT_NOT_NULL(st.configChecksum); TEST_ASSERT_EQUAL_UINT(64u, (unsigned)strlen(st.configChecksum)); TEST_ASSERT_EQUAL_STRING( "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", st.configChecksum); ctrlstatus_free_fields(&st); cs_teardown_cluster(&c); printf("test_ctrlstatus_standby_known_primary: PASS\n"); } /* (c) No known primary → empty primaryName (client renders "unknown"). * (Req 1.3, 2.3) */ static void test_ctrlstatus_unknown_primary(void) { cluster_t c; myconf_t mc; cs_setup_cluster(&c, &mc); atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY, memory_order_release); /* primary_host left empty by ha_init → unknown */ xce_ctrlstatus_t st; ctrlstatus_fill(&c, &st); TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_STANDBY, st.responderRole); TEST_ASSERT_NOT_NULL(st.primaryName); TEST_ASSERT_EQUAL_STRING("", st.primaryName); /* empty ⇒ "unknown" */ ctrlstatus_free_fields(&st); cs_teardown_cluster(&c); printf("test_ctrlstatus_unknown_primary: PASS\n"); } /* (d) fenced reflects in_catch_up. (Req 6.3) */ static void test_ctrlstatus_fenced_when_in_catch_up(void) { cluster_t c; myconf_t mc; cs_setup_cluster(&c, &mc); atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY, memory_order_release); atomic_store_explicit(&c.ha.in_catch_up, 1, memory_order_release); xce_ctrlstatus_t st; ctrlstatus_fill(&c, &st); TEST_ASSERT_EQUAL_UINT(1u, st.fenced ? 1u : 0u); ctrlstatus_free_fields(&st); cs_teardown_cluster(&c); printf("test_ctrlstatus_fenced_when_in_catch_up: PASS\n"); } /* (e) No-mutation: ctrlstatus_fill() must not change any HA/config state. * Snapshot the relevant fields, call fill, assert they are unchanged. (Req 1.2) */ static void test_ctrlstatus_no_mutation(void) { cluster_t c; myconf_t mc; cs_setup_cluster(&c, &mc); atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY, memory_order_release); atomic_store_explicit(&c.ha.term, 9, memory_order_release); atomic_store_explicit(&c.ha.applied_lsn, 100, memory_order_release); atomic_store_explicit(&c.persist.committed_lsn, 99, memory_order_release); atomic_store_explicit(&c.ha.committed_config_tag, 0x1234ULL, memory_order_release); atomic_store_explicit(&c.ha.in_catch_up, 0, memory_order_release); strncpy(c.ha.primary_host, "ctrlA", sizeof(c.ha.primary_host) - 1); c.ha.primary_port = 7001; int32_t role0 = atomic_load_explicit(&c.ha.role, memory_order_acquire); uint64_t term0 = atomic_load_explicit(&c.ha.term, memory_order_acquire); uint64_t applied0 = atomic_load_explicit(&c.ha.applied_lsn, memory_order_acquire); uint64_t committed0 = atomic_load_explicit(&c.persist.committed_lsn, memory_order_acquire); uint64_t cfgtag0 = atomic_load_explicit(&c.ha.committed_config_tag, memory_order_acquire); int catchup0 = atomic_load_explicit(&c.ha.in_catch_up, memory_order_acquire); char primary0[XCE_MAXNAME_LEN]; strncpy(primary0, c.ha.primary_host, sizeof(primary0)); uint32_t port0 = c.ha.primary_port; uint64_t vertag0 = mc.config_version_tag; xce_ctrlstatus_t st; ctrlstatus_fill(&c, &st); ctrlstatus_free_fields(&st); TEST_ASSERT_EQUAL_INT(role0, atomic_load_explicit(&c.ha.role, memory_order_acquire)); TEST_ASSERT_EQUAL_UINT64(term0, atomic_load_explicit(&c.ha.term, memory_order_acquire)); TEST_ASSERT_EQUAL_UINT64(applied0, atomic_load_explicit(&c.ha.applied_lsn, memory_order_acquire)); TEST_ASSERT_EQUAL_UINT64(committed0, atomic_load_explicit(&c.persist.committed_lsn, memory_order_acquire)); TEST_ASSERT_EQUAL_UINT64(cfgtag0, atomic_load_explicit(&c.ha.committed_config_tag, memory_order_acquire)); TEST_ASSERT_EQUAL_INT(catchup0, atomic_load_explicit(&c.ha.in_catch_up, memory_order_acquire)); TEST_ASSERT_EQUAL_STRING(primary0, c.ha.primary_host); TEST_ASSERT_EQUAL_UINT32(port0, c.ha.primary_port); TEST_ASSERT_EQUAL_UINT64(vertag0, mc.config_version_tag); cs_teardown_cluster(&c); printf("test_ctrlstatus_no_mutation: PASS\n"); } /* ── suite entry point ─────────────────────────────────────── */ void test_ctrlstatus(void) { RUN_TEST(test_ctrlstatus_primary); RUN_TEST(test_ctrlstatus_standby_known_primary); RUN_TEST(test_ctrlstatus_unknown_primary); RUN_TEST(test_ctrlstatus_fenced_when_in_catch_up); RUN_TEST(test_ctrlstatus_no_mutation); }