test: P1-7 HA foundation integration tests — election, term fencing, watchdog, redirect (P1-7)

This commit is contained in:
Rongsong Shen 2026-05-07 15:22:50 +00:00
parent f5b0ac0af3
commit 05c00704d3
3 changed files with 154 additions and 0 deletions

View file

@ -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',
]

View file

@ -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();
}

View file

@ -0,0 +1,151 @@
/* P1-7: HA Foundation integration tests — assembled system */
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unity/unity.h>
#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);
}