/* * test_repl_majority.c — RED-phase tests for majority-commit replication. * * Tests for: * repl_wait_majority_ack(replicator_t *, uint32_t timeout_ms, uint8_t ack_op) * repl_broadcast majority semantics (pending_ack_count, over-limit eviction) * * These functions/fields do not exist yet (task 7.2, 7.3). * This file will fail to link until they are implemented. * * Requirements: 5.1, 5.4, 5.5, 5.8, 8.2 */ #include #include #include #include #include #include #include #include #include "repl.h" #include "config.h" #include "controller.capnp.h" /* OPCode_walAck, OPCode_upgradeConfigAck */ /* MAX_PENDING_ACKS: defined in repl.h after task 7.2; fallback for RED phase */ #ifndef MAX_PENDING_ACKS #define MAX_PENDING_ACKS 16 #endif /* ── Forward declarations of functions-under-test (not yet implemented) ───── */ /* * repl_wait_majority_ack: wait until majority of active standbys have ACKed * for the given ack_op. Returns 0 on quorum, -ETIMEDOUT on timeout. * Single-node (no active standbys) returns 0 immediately. */ extern int repl_wait_majority_ack(replicator_t *r, uint32_t timeout_ms, uint8_t ack_op); /* ── Helpers ───────────────────────────────────────────────────────────────── */ /* Dummy endpoints — repl.c only compares pointers */ static int ep0, ep1, ep2; /* ACK a standby from another thread */ struct ack_args { replicator_t *r; void *ep; uint64_t lsn; uint32_t delay_ms; }; static void *ack_thread(void *arg) { struct ack_args *a = arg; if (a->delay_ms > 0) { struct timespec ts = { .tv_sec = 0, .tv_nsec = (long)a->delay_ms * 1000000L }; nanosleep(&ts, NULL); } repl_on_ack(a->r, a->ep, a->lsn); return NULL; } /* ── Test 1: 2/3 ACKs → majority, returns 0 ───────────────────────────────── */ /* Static so ack_args survive Unity longjmp if assert fires before join */ static replicator_t g_r1; static struct ack_args g_a1_0, g_a1_1; static void test_wait_majority_ack_two_of_three_returns_zero(void) { repl_init(&g_r1); repl_add_standby(&g_r1, &ep0, "sb0", 7000); repl_add_standby(&g_r1, &ep1, "sb1", 7001); repl_add_standby(&g_r1, &ep2, "sb2", 7002); /* 2 standbys ACK — majority of 3 active standbys is ceil((3+1)/2) = 2 ACKs + self = quorum */ pthread_t t0, t1; g_a1_0 = (struct ack_args){ &g_r1, &ep0, 1, 10 }; g_a1_1 = (struct ack_args){ &g_r1, &ep1, 1, 20 }; pthread_create(&t0, NULL, ack_thread, &g_a1_0); pthread_create(&t1, NULL, ack_thread, &g_a1_1); int rc = repl_wait_majority_ack(&g_r1, 2000, (uint8_t)OPCode_walAck); /* Join threads before asserting — prevents use-after-return if assert longjmps */ pthread_join(t0, NULL); pthread_join(t1, NULL); TEST_ASSERT_EQUAL_INT(0, rc); repl_fini(&g_r1); printf("test_wait_majority_ack_two_of_three_returns_zero: PASS\n"); } /* ── Test 2: timeout before majority ACKs → -ETIMEDOUT ────────────────────── */ static void test_wait_majority_ack_timeout_returns_etimedout(void) { replicator_t r; repl_init(&r); repl_add_standby(&r, &ep0, "sb0", 7000); repl_add_standby(&r, &ep1, "sb1", 7001); repl_add_standby(&r, &ep2, "sb2", 7002); /* No ACKs arrive — should time out */ int rc = repl_wait_majority_ack(&r, 50, (uint8_t)OPCode_walAck); TEST_ASSERT_EQUAL_INT(-ETIMEDOUT, rc); repl_fini(&r); printf("test_wait_majority_ack_timeout_returns_etimedout: PASS\n"); } /* ── Test 3: single-node (no standbys) → returns 0 immediately ────────────── */ static void test_wait_majority_ack_no_standbys_returns_zero(void) { replicator_t r; repl_init(&r); /* No standbys added — primary alone is majority */ int rc = repl_wait_majority_ack(&r, 100, (uint8_t)OPCode_walAck); TEST_ASSERT_EQUAL_INT(0, rc); repl_fini(&r); printf("test_wait_majority_ack_no_standbys_returns_zero: PASS\n"); } /* ── Test 4: in_catch_up standby excluded from quorum denominator ──────────── */ static replicator_t g_r4; static struct ack_args g_a4_0; static void test_wait_majority_ack_catchup_excluded_from_quorum(void) { repl_init(&g_r4); repl_add_standby(&g_r4, &ep0, "sb0", 7000); /* active */ repl_add_standby(&g_r4, &ep1, "sb1", 7001); /* will be put in catch-up */ repl_add_standby(&g_r4, &ep2, "sb2", 7002); /* active */ repl_start_catchup(&g_r4, &ep1, 0); pthread_t t0; g_a4_0 = (struct ack_args){ &g_r4, &ep0, 1, 10 }; pthread_create(&t0, NULL, ack_thread, &g_a4_0); int rc = repl_wait_majority_ack(&g_r4, 2000, (uint8_t)OPCode_walAck); pthread_join(t0, NULL); TEST_ASSERT_EQUAL_INT(0, rc); repl_fini(&g_r4); printf("test_wait_majority_ack_catchup_excluded_from_quorum: PASS\n"); } /* ── Test 5: OP_walAck vs OP_upgradeConfigAck dispatched independently ─────── */ static replicator_t g_r5; static struct ack_args g_a5_0; static void test_wait_majority_ack_ack_op_discriminates(void) { repl_init(&g_r5); repl_add_standby(&g_r5, &ep0, "sb0", 7000); pthread_t t0; g_a5_0 = (struct ack_args){ &g_r5, &ep0, 1, 10 }; pthread_create(&t0, NULL, ack_thread, &g_a5_0); int rc_wal = repl_wait_majority_ack(&g_r5, 2000, (uint8_t)OPCode_walAck); pthread_join(t0, NULL); TEST_ASSERT_EQUAL_INT(0, rc_wal); /* upgradeConfigAck with no pending ACKs → timeout */ int rc_cfg = repl_wait_majority_ack(&g_r5, 50, (uint8_t)OPCode_upgradeConfigAck); TEST_ASSERT_EQUAL_INT(-ETIMEDOUT, rc_cfg); repl_fini(&g_r5); printf("test_wait_majority_ack_ack_op_discriminates: PASS\n"); } /* ── Test 6: pending_ack_count > max removes standby from active set ──────── */ static void test_broadcast_over_limit_removes_standby(void) { replicator_t r; repl_init(&r); repl_add_standby(&r, &ep0, "sb0", 7000); /* Artificially inflate pending_ack_count past the limit. * After overflow the standby should be evicted (in_catch_up=1 or removed). */ for (int i = 0; i < r.standbys[0].ep != NULL ? 1 : 0; i++) { /* Access the pending_ack_count field — this will fail to compile if * the field does not exist on standby_conn_t */ r.standbys[0].pending_ack_count = MAX_PENDING_ACKS + 1; } /* Also verify the field exists via a direct write */ r.standbys[0].pending_ack_count = MAX_PENDING_ACKS + 1; /* Broadcast a wal entry — the over-limit standby should be evicted */ wal_entry_hdr_t hdr = { .type = WAL_JOB_SUBMIT, .lsn = 1 }; int rc = repl_broadcast(&r, &hdr, NULL, 0); /* repl_broadcast itself may return 0 even after eviction */ (void)rc; /* After broadcast with pending_ack_count > max, standby should be in catch-up */ int evicted = 0; for (int i = 0; i < MAX_STANDBYS; i++) { if (r.standbys[i].ep == &ep0) { if (atomic_load_explicit(&r.standbys[i].in_catch_up, memory_order_acquire)) evicted = 1; } } /* Alternatively the standby may have been fully removed */ if (!evicted) { evicted = (r.nstandbys == 0); } TEST_ASSERT_TRUE(evicted); repl_fini(&r); printf("test_broadcast_over_limit_removes_standby: PASS\n"); } /* ── Test group entry point ────────────────────────────────────────────────── */ void test_repl_majority(void) { UNITY_BEGIN(); RUN_TEST(test_wait_majority_ack_two_of_three_returns_zero); RUN_TEST(test_wait_majority_ack_timeout_returns_etimedout); RUN_TEST(test_wait_majority_ack_no_standbys_returns_zero); RUN_TEST(test_wait_majority_ack_catchup_excluded_from_quorum); RUN_TEST(test_wait_majority_ack_ack_op_discriminates); RUN_TEST(test_broadcast_over_limit_removes_standby); UNITY_END(); }