/* * test_ha_start_election_async.c — Unity tests for task 2.4: * ha_start_election_async() non-blocking election initiator. * * Strategy: * - n_peers=1 shortcircuit: no iomgr needed; election completes synchronously * via ha_elect_evaluate_and_signal immediately after ha_start_election_async * returns (role flips to CTRL_PRIMARY_COMMITTING; task 4.4 committing timeout * will later promote to CTRL_PRIMARY for degraded/single-node mode). * - Function returns in < 5ms (observable via clock_gettime). * - n_peers=1 shortcircuit stores n_peers in ha->n_peers. * * Observable: function returns in < 5ms; n_peers stored; shortcircuit fires * ha_elect_evaluate_and_signal directly (no iomgr/delaymgr needed for n_peers=1). * * Requirements: 1.3 (non-blocking), 2.1 (single-node shortcircuit), 2.4 (vote send) * Boundary: ha.c (election orchestration) */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include "ha.h" #include "netio.h" #include "xce/cluster_def.h" #include "delay.h" /* Forward declaration for function under test */ extern void ha_start_election_async(ha_state_t *ha, cluster_t *c, int n_peers, xce_candidate_t **peers, uint64_t my_config_tag, uint64_t my_lsn, const char *my_name); /* ── Minimal ha_config_t for tests ─────────────────────────────────────── */ static ha_config_t g_async_cfg = { .heartbeat_interval_ms = 3000, .heartbeat_timeout_ms = 10000, .election_timeout_ms = 2000, .probe_connect_timeout_ms = 500, .max_promotion_retries = 5, .election_jitter_max_ms = 0, .election_priority_step_ms = 500, .committing_timeout_ms = 10000, }; /* ── Helper: elapsed ms since a timespec ───────────────────────────────── */ static long elapsed_ms(const struct timespec *start) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); long sec_diff = (long)(now.tv_sec - start->tv_sec); long nsec_diff = (long)(now.tv_nsec - start->tv_nsec); return sec_diff * 1000L + nsec_diff / 1000000L; } /* ── Test 1: single-node shortcircuit returns in < 5ms ──────────────────── */ static void test_single_node_returns_quickly(void) { ha_state_t ha; ha_init(&ha, &g_async_cfg, 0); ha.phase2_done = 0; /* first boot */ /* n_peers=1: shortcircuit, no iomgr or delaymgr needed */ struct timespec t0; clock_gettime(CLOCK_MONOTONIC, &t0); ha_start_election_async(&ha, NULL, 1, NULL, 100ULL, 50ULL, "ctrl-a"); long ms = elapsed_ms(&t0); TEST_ASSERT_LESS_OR_EQUAL_INT_MESSAGE(5, (int)ms, "ha_start_election_async(n_peers=1) must return in < 5ms"); ha_fini(&ha); printf("test_single_node_returns_quickly: PASS (elapsed=%ldms)\n", ms); } /* ── Test 2: single-node shortcircuit stores n_peers in ha->n_peers ──────── */ static void test_single_node_stores_n_peers(void) { ha_state_t ha; ha_init(&ha, &g_async_cfg, 0); ha.n_peers = 0; ha_start_election_async(&ha, NULL, 1, NULL, 0ULL, 0ULL, "ctrl-a"); TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.n_peers, "ha->n_peers must be set to n_peers argument (1 for single-node)"); ha_fini(&ha); printf("test_single_node_stores_n_peers: PASS\n"); } /* ── Test 3: single-node shortcircuit enters CTRL_PRIMARY_COMMITTING ──────── */ static void test_single_node_shortcircuit_evaluates_immediately(void) { ha_state_t ha; ha_init(&ha, &g_async_cfg, 0); ha.phase2_done = 0; /* Start in CTRL_ELECTING state so role transition is visible */ atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release); /* n_peers=1: must call ha_elect_evaluate_and_signal immediately. * Fix 1 (CRITICAL): single-node shortcircuit must enter CTRL_PRIMARY_COMMITTING, * not CTRL_PRIMARY. Task 4.4 committing timeout will promote to CTRL_PRIMARY * once the committing phase completes (Req 8.4 / degraded-single-node path). */ ha_start_election_async(&ha, NULL, 1, NULL, 100ULL, 50ULL, "ctrl-a"); ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire); TEST_ASSERT_EQUAL_INT_MESSAGE((int)CTRL_PRIMARY_COMMITTING, (int)role, "single-node shortcircuit must enter CTRL_PRIMARY_COMMITTING (not CTRL_PRIMARY)"); ha_fini(&ha); printf("test_single_node_shortcircuit_evaluates_immediately: PASS (role=%d == CTRL_PRIMARY_COMMITTING)\n", (int)role); } /* ── Test 6: single-node shortcircuit does NOT go directly to CTRL_PRIMARY ── */ static void test_single_node_not_primary_directly(void) { ha_state_t ha; ha_init(&ha, &g_async_cfg, 0); ha_start_election_async(&ha, NULL, 1, NULL, 0ULL, 0ULL, "ctrl-a"); ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire); TEST_ASSERT_NOT_EQUAL_MESSAGE((int)CTRL_PRIMARY, (int)role, "single-node shortcircuit must NOT jump directly to CTRL_PRIMARY; " "must go through CTRL_PRIMARY_COMMITTING first"); ha_fini(&ha); printf("test_single_node_not_primary_directly: PASS (role=%d)\n", (int)role); } /* ── Test 4: ha->n_peers set for multi-peer count too ───────────────────── */ static void test_n_peers_stored_for_multi_peer(void) { /* We can't test actual multi-peer endpoint setup without iomgr, * but we can verify n_peers is stored correctly for any value. * Pass peers=NULL to avoid actual connect attempts; the function * should skip peer on connect failure (design spec). */ ha_state_t ha; ha_init(&ha, &g_async_cfg, 0); ha.n_peers = 0; /* Passing n_peers=3 with peers=NULL: each peer would fail connect, * but n_peers must be stored before attempting connects. */ ha_start_election_async(&ha, NULL, 3, NULL, 100ULL, 50ULL, "ctrl-a"); TEST_ASSERT_EQUAL_INT_MESSAGE(3, ha.n_peers, "ha->n_peers must be set to n_peers argument even for multi-peer"); ha_fini(&ha); printf("test_n_peers_stored_for_multi_peer: PASS\n"); } /* ── Test 5: single-node election increments term ───────────────────────── */ static void test_single_node_increments_term(void) { ha_state_t ha; ha_init(&ha, &g_async_cfg, 0); uint64_t initial_term = atomic_load_explicit(&ha.term, memory_order_acquire); ha_start_election_async(&ha, NULL, 1, NULL, 0ULL, 0ULL, "ctrl-a"); uint64_t final_term = atomic_load_explicit(&ha.term, memory_order_acquire); TEST_ASSERT_GREATER_THAN_UINT64_MESSAGE(initial_term, final_term, "single-node election must increment ha->term"); ha_fini(&ha); printf("test_single_node_increments_term: PASS (term: %"PRIu64" → %"PRIu64")\n", initial_term, final_term); } /* ── Suite entry point ───────────────────────────────────────────────────── */ void test_ha_start_election_async(void) { RUN_TEST(test_single_node_returns_quickly); RUN_TEST(test_single_node_stores_n_peers); RUN_TEST(test_single_node_shortcircuit_evaluates_immediately); RUN_TEST(test_single_node_not_primary_directly); RUN_TEST(test_n_peers_stored_for_multi_peer); RUN_TEST(test_single_node_increments_term); }