diff --git a/src/controller/meson.build b/src/controller/meson.build index 79d4cbf..08c3aa6 100644 --- a/src/controller/meson.build +++ b/src/controller/meson.build @@ -86,6 +86,7 @@ controller_unit_test_src = [ 'testsuite/test_ha_role.c', 'testsuite/test_ha_cluster.c', 'testsuite/test_ha_redirect.c', + 'testsuite/test_p1_integration.c', 'persist.c', 'ha.c', ] diff --git a/src/controller/testsuite/controller_test.c b/src/controller/testsuite/controller_test.c index a582387..db8f201 100644 --- a/src/controller/testsuite/controller_test.c +++ b/src/controller/testsuite/controller_test.c @@ -22,6 +22,7 @@ extern void test_e2e_capnp(void); extern void test_ha_role(void); extern void test_ha_cluster(void); extern void test_ha_redirect(void); +extern void test_p1_integration(void); void setUp(void) { return; } @@ -113,6 +114,7 @@ int main(int argc, char* argv[]) TEST(test_ha_role); TEST(test_ha_cluster); TEST(test_ha_redirect); + TEST(test_p1_integration); return UNITY_END(); } diff --git a/src/controller/testsuite/test_p1_integration.c b/src/controller/testsuite/test_p1_integration.c new file mode 100644 index 0000000..8cc5f3c --- /dev/null +++ b/src/controller/testsuite/test_p1_integration.c @@ -0,0 +1,151 @@ +/* P1-7: HA Foundation integration tests — assembled system */ +#include +#include +#include +#include + +#include + +#include "ha.h" + +/* Test 1: Single-node startup: after ha_init + ha_do_promote(priority=0), + * role must be PRIMARY */ +static void test_single_node_starts_as_primary(void) +{ + ha_state_t ha; + 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, + }; + ha_init(&ha, &cfg, 0 /* priority 0 = highest */); + /* Simulate am_i_primary() decision for priority-0 node */ + ha_do_promote(&ha); + TEST_ASSERT_EQUAL_INT(CTRL_PRIMARY, + (int)atomic_load_explicit(&ha.role, memory_order_acquire)); + ha_fini(&ha); + printf("test_single_node_starts_as_primary: PASS\n"); +} + +/* Test 2: Term fencing — promote increments term; second promote increments again */ +static void test_term_fencing_monotonic(void) +{ + ha_state_t ha; + 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, + }; + ha_init(&ha, &cfg, 0); + uint64_t t0 = atomic_load_explicit(&ha.term, memory_order_acquire); + ha_do_promote(&ha); + uint64_t t1 = atomic_load_explicit(&ha.term, memory_order_acquire); + ha_do_demote(&ha); + ha_do_promote(&ha); + uint64_t t2 = atomic_load_explicit(&ha.term, memory_order_acquire); + TEST_ASSERT_TRUE(t1 > t0); + TEST_ASSERT_TRUE(t2 > t1); + ha_fini(&ha); + printf("test_term_fencing_monotonic: PASS\n"); +} + +/* Test 3: Watchdog start+stop cycle — verify clean teardown */ +static void test_watchdog_lifecycle_clean(void) +{ + ha_state_t ha; + 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, + }; + ha_init(&ha, &cfg, 0); + TEST_ASSERT_EQUAL_INT(0, ha_start_watchdog(&ha)); + struct timespec ts = { 0, 10 * 1000000L }; /* 10 ms */ + nanosleep(&ts, NULL); + ha_stop_watchdog(&ha); + /* After stop, watchdog_thread must be reset to 0 */ + TEST_ASSERT_EQUAL_INT(0, (int)ha.watchdog_thread); + ha_fini(&ha); + printf("test_watchdog_lifecycle_clean: PASS\n"); +} + +/* Test 4: Redirect guard — primary never redirects; standby with known + * primary redirects; standby with no known primary reports nomaster */ +static void test_redirect_guard_integration(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, + }; + + /* Primary: no redirect */ + ha_state_t ha_primary; + ha_init(&ha_primary, &cfg, 0); + atomic_store_explicit(&ha_primary.role, (int32_t)CTRL_PRIMARY, + memory_order_release); + TEST_ASSERT_EQUAL_INT(0, ha_should_redirect(&ha_primary)); + + /* Standby with known primary: redirect */ + ha_state_t ha_standby; + ha_init(&ha_standby, &cfg, 1); + strncpy(ha_standby.primary_host, "10.0.0.1", + sizeof(ha_standby.primary_host) - 1); + ha_standby.primary_port = 7000; + TEST_ASSERT_EQUAL_INT(1, ha_should_redirect(&ha_standby)); + TEST_ASSERT_EQUAL_INT(0, ha_no_primary_known(&ha_standby)); + + /* Standby with no known primary: no redirect, nomaster */ + ha_state_t ha_cold; + ha_init(&ha_cold, &cfg, 2); + TEST_ASSERT_EQUAL_INT(0, ha_should_redirect(&ha_cold)); + TEST_ASSERT_EQUAL_INT(1, ha_no_primary_known(&ha_cold)); + + ha_fini(&ha_primary); + ha_fini(&ha_standby); + ha_fini(&ha_cold); + printf("test_redirect_guard_integration: PASS\n"); +} + +/* Test 5: Hook idempotency — double-promote does not register hooks twice + * (full hook registration needs a cluster_t — this tests the hooks_registered + * guard) */ +static void test_hooks_registered_guard_integration(void) +{ + ha_state_t ha; + 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, + }; + ha_init(&ha, &cfg, 0); + TEST_ASSERT_EQUAL_INT(0, ha.hooks_registered); + /* Simulate idempotency: hooks_registered = 1 after first promote */ + ha.hooks_registered = 1; + /* Second promote should see guard and skip re-registration */ + TEST_ASSERT_TRUE(ha.hooks_registered == 1); /* guard still set */ + /* Demote resets guard */ + ha_do_demote(&ha); + TEST_ASSERT_EQUAL_INT(0, ha.hooks_registered); + ha_fini(&ha); + printf("test_hooks_registered_guard_integration: PASS\n"); +} + +void test_p1_integration(void) +{ + RUN_TEST(test_single_node_starts_as_primary); + RUN_TEST(test_term_fencing_monotonic); + RUN_TEST(test_watchdog_lifecycle_clean); + RUN_TEST(test_redirect_guard_integration); + RUN_TEST(test_hooks_registered_guard_integration); +}