Adds pre_vote_ep_err_handler_for_test (XCE_TESTING-guarded) and two tests: PreVote post-connect failure reaches pre_vote_reply_dispatch's tally (Req 4.3), and a round mixing that failure mode with a delivered reply reaches its quorum decision via the fast all-replied path without the full round timeout (Req 4.5). Real-vote coverage (Req 4.2) was already proven by task 3.1's own test, cited not duplicated. pre_vote_reply_dispatch has no mock hook seam, so both tests assert directly on raft_state_t's tally fields, mirroring test_ha_prevote.c's existing verification technique. 1061 -> 1063 tests, 0 failures.
908 lines
37 KiB
C
908 lines
37 KiB
C
/*
|
|
* test_ha_vote_send.c — Unity tests for task 2.2: vote_ep_ctx_t and
|
|
* on_vote_connected callback (election-iomgr-refactor spec).
|
|
*
|
|
* Observable: unit test with mock endpoint verifies xce_request called with
|
|
* correct opcode and fields after on_vote_connected fires.
|
|
*
|
|
* Strategy: XCE_TESTING test seam — xce_vote_request_hook captures the call
|
|
* to xce_request from on_vote_connected, letting us inspect the xce_request_t
|
|
* without touching the real iomgr/endpoint_enq path.
|
|
*
|
|
* Req coverage: 2.3 (send vote after handshake), 1.2 (vote RPC via iomgr path)
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <stdatomic.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "ha.h"
|
|
#include "netio.h"
|
|
#include "controller.capnp.h"
|
|
|
|
/* ── Forward declarations for functions under test ─────────────────────────
|
|
*
|
|
* vote_ep_ctx_t, on_vote_connected, election_timeout_ctx_t are defined in
|
|
* ha.c (file-scoped under XCE_TESTING preprocessor for the hook; the structs
|
|
* and callback are static but the hook pointer is exposed).
|
|
*
|
|
* on_vote_connected is static — we call it indirectly by setting ep->on_connected
|
|
* and invoking it through the endpoint pointer, but since we can't extern a static
|
|
* function, we test via the hook seam exclusively: set xce_vote_request_hook and
|
|
* invoke on_vote_connected by address via the function pointer stored in ep->on_connected.
|
|
*
|
|
* vote_ep_ctx_t_build: test helper exposed only under XCE_TESTING that allocates
|
|
* and fills a vote_ep_ctx_t, sets ep->on_connected, ep->data, so the test can
|
|
* call ep->on_connected(ep) to drive on_vote_connected.
|
|
*/
|
|
|
|
/* Test seam: pointer is declared in ha.c under XCE_TESTING and exposed here */
|
|
extern int (*xce_vote_request_hook)(endpoint_t *ep, xce_request_t *req);
|
|
|
|
/* vote_ep_ctx_build: test helper exposed from ha.c under XCE_TESTING.
|
|
* Allocates a vote_ep_ctx_t, fills it from the parameters, sets ep->data and
|
|
* ep->on_connected. Caller owns the returned pointer. */
|
|
typedef struct vote_ep_ctx_s vote_ep_ctx_t; /* defined in ha.c — opaque here */
|
|
|
|
extern endpoint_t *vote_ep_ctx_build_for_test(endpoint_t *ep,
|
|
ha_state_t *ha,
|
|
ha_elect_ctx_t *ctx,
|
|
int peer_idx,
|
|
uint64_t my_term,
|
|
uint64_t my_tag,
|
|
uint64_t my_lsn,
|
|
const char *my_name);
|
|
|
|
/* ── Minimal ha_config_t for tests ─────────────────────────────────────── */
|
|
|
|
static ha_config_t g_vote_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,
|
|
};
|
|
|
|
/* ── Mock xce_request hook ──────────────────────────────────────────────── */
|
|
|
|
static int g_hook_call_count = 0;
|
|
static endpoint_t *g_hook_ep = NULL;
|
|
static uint32_t g_hook_opcode = 0;
|
|
static int g_hook_body_which = 0;
|
|
static uint64_t g_captured_term = 0;
|
|
static uint64_t g_captured_tag = 0;
|
|
static uint64_t g_captured_lsn = 0;
|
|
static char g_captured_name[256] = {0};
|
|
|
|
static int mock_vote_request_hook(endpoint_t *ep, xce_request_t *req)
|
|
{
|
|
g_hook_call_count++;
|
|
g_hook_ep = ep;
|
|
g_hook_opcode = req->opcode;
|
|
g_hook_body_which = req->body_which;
|
|
|
|
if (req->body_which == Request_body_requestvotemsg &&
|
|
req->body.requestvotemsg != NULL) {
|
|
g_captured_term = req->body.requestvotemsg->term;
|
|
g_captured_tag = req->body.requestvotemsg->config_tag;
|
|
g_captured_lsn = req->body.requestvotemsg->last_log_lsn;
|
|
if (req->body.requestvotemsg->candidate_id)
|
|
strncpy(g_captured_name, req->body.requestvotemsg->candidate_id,
|
|
sizeof(g_captured_name) - 1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void reset_mock(void)
|
|
{
|
|
g_hook_call_count = 0;
|
|
g_hook_ep = NULL;
|
|
g_hook_opcode = 0;
|
|
g_hook_body_which = 0;
|
|
g_captured_term = 0;
|
|
g_captured_tag = 0;
|
|
g_captured_lsn = 0;
|
|
memset(g_captured_name, 0, sizeof(g_captured_name));
|
|
xce_vote_request_hook = mock_vote_request_hook;
|
|
}
|
|
|
|
/* ── Helper: make a minimal zeroed endpoint_t on the stack ─────────────── */
|
|
|
|
static void make_endpoint(endpoint_t *ep)
|
|
{
|
|
memset(ep, 0, sizeof(*ep));
|
|
ep->fd = -1;
|
|
}
|
|
|
|
/* ── Helper: allocate a minimal ha_elect_ctx_t ─────────────────────────── */
|
|
|
|
static ha_elect_ctx_t *alloc_ctx_for_vote(int n_peers)
|
|
{
|
|
ha_elect_ctx_t *ctx = calloc(1, sizeof(*ctx) +
|
|
(size_t)n_peers * sizeof(ctx->votes[0]));
|
|
if (!ctx) { fprintf(stderr, "alloc_ctx_for_vote: calloc failed\n"); exit(1); }
|
|
ctx->n_peers = n_peers;
|
|
ctx->votes_granted = 1; /* self-vote */
|
|
sem_init(&ctx->done, 0, 0);
|
|
return ctx;
|
|
}
|
|
|
|
static void free_ctx_for_vote(ha_elect_ctx_t *ctx)
|
|
{
|
|
sem_destroy(&ctx->done);
|
|
free(ctx);
|
|
}
|
|
|
|
/* ── Test: on_vote_connected calls hook with OPCode_requestVote ─────────── */
|
|
|
|
static void test_on_vote_connected_calls_hook_with_correct_opcode(void)
|
|
{
|
|
reset_mock();
|
|
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_vote_cfg, 0);
|
|
atomic_store_explicit(&ha.term, 3ULL, memory_order_release);
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
|
|
endpoint_t ep;
|
|
make_endpoint(&ep);
|
|
|
|
/* vote_ep_ctx_build_for_test sets ep->data and ep->on_connected */
|
|
vote_ep_ctx_build_for_test(&ep, &ha, ctx,
|
|
0, /* peer_idx */
|
|
3ULL, /* my_term */
|
|
100ULL, /* my_tag */
|
|
50ULL, /* my_lsn */
|
|
"ctrl-a");
|
|
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(ep.on_connected,
|
|
"ep->on_connected must be set by vote_ep_ctx_build_for_test");
|
|
|
|
/* Fire on_vote_connected */
|
|
ep.on_connected(&ep);
|
|
|
|
/* Verify hook was called exactly once */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_hook_call_count,
|
|
"xce_vote_request_hook must be called exactly once by on_vote_connected");
|
|
|
|
/* Verify opcode */
|
|
TEST_ASSERT_EQUAL_UINT32_MESSAGE(OPCode_requestVote, g_hook_opcode,
|
|
"xce_request must be called with opcode OPCode_requestVote");
|
|
|
|
/* Verify body_which */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(Request_body_requestvotemsg, g_hook_body_which,
|
|
"body_which must be Request_body_requestvotemsg");
|
|
|
|
/* Verify endpoint pointer passed through */
|
|
TEST_ASSERT_EQUAL_PTR_MESSAGE(&ep, g_hook_ep,
|
|
"hook must receive the same endpoint pointer that on_vote_connected received");
|
|
|
|
/* Cleanup: ep.data was allocated by vote_ep_ctx_build_for_test; free it */
|
|
free(ep.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_on_vote_connected_calls_hook_with_correct_opcode: PASS\n");
|
|
}
|
|
|
|
/* ── Test: on_vote_connected encodes correct term, tag, lsn, name ────────── */
|
|
|
|
static void test_on_vote_connected_encodes_correct_fields(void)
|
|
{
|
|
reset_mock();
|
|
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_vote_cfg, 1);
|
|
atomic_store_explicit(&ha.term, 7ULL, memory_order_release);
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
|
|
endpoint_t ep;
|
|
make_endpoint(&ep);
|
|
|
|
vote_ep_ctx_build_for_test(&ep, &ha, ctx,
|
|
1, /* peer_idx */
|
|
7ULL, /* my_term */
|
|
200ULL, /* my_tag */
|
|
99ULL, /* my_lsn */
|
|
"ctrl-b");
|
|
|
|
ep.on_connected(&ep);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_hook_call_count,
|
|
"hook must be called exactly once");
|
|
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(7ULL, g_captured_term,
|
|
"request_vote_req_t.term must match my_term from vote_ep_ctx_t");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(200ULL, g_captured_tag,
|
|
"request_vote_req_t.config_tag must match my_tag from vote_ep_ctx_t");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(99ULL, g_captured_lsn,
|
|
"request_vote_req_t.last_log_lsn must match my_lsn from vote_ep_ctx_t");
|
|
TEST_ASSERT_EQUAL_STRING_MESSAGE("ctrl-b", g_captured_name,
|
|
"request_vote_req_t.candidate_id must match my_name from vote_ep_ctx_t");
|
|
|
|
free(ep.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_on_vote_connected_encodes_correct_fields: PASS\n");
|
|
}
|
|
|
|
/* ── Test: on_vote_connected sets ep->msg_handler after sending ─────────── */
|
|
|
|
static void test_on_vote_connected_sets_msg_handler(void)
|
|
{
|
|
reset_mock();
|
|
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_vote_cfg, 0);
|
|
atomic_store_explicit(&ha.term, 1ULL, memory_order_release);
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
|
|
endpoint_t ep;
|
|
make_endpoint(&ep);
|
|
|
|
vote_ep_ctx_build_for_test(&ep, &ha, ctx,
|
|
0,
|
|
1ULL,
|
|
100ULL,
|
|
50ULL,
|
|
"ctrl-a");
|
|
|
|
/* msg_handler starts NULL */
|
|
TEST_ASSERT_NULL_MESSAGE(ep.msg_handler,
|
|
"msg_handler must be NULL before on_vote_connected fires");
|
|
|
|
ep.on_connected(&ep);
|
|
|
|
/* After on_vote_connected fires, ep->msg_handler must be set to
|
|
* vote_reply_msg_handler (forward-declared in 2.3; non-NULL here) */
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(ep.msg_handler,
|
|
"on_vote_connected must set ep->msg_handler to vote_reply_msg_handler");
|
|
|
|
free(ep.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_on_vote_connected_sets_msg_handler: PASS\n");
|
|
}
|
|
|
|
/* ── Test: election_timeout_ctx_t cancelled flag prevents action ─────────── */
|
|
|
|
static void test_election_timeout_ctx_cancelled_flag(void)
|
|
{
|
|
/* election_timeout_ctx_t carries _Atomic int cancelled.
|
|
* When cancelled == 1, the delaymgr callback must be a no-op.
|
|
* We test this by directly calling the exported helper
|
|
* election_timeout_fn_for_test (exposed under XCE_TESTING). */
|
|
extern int election_timeout_fn_for_test(ha_state_t *ha, ha_elect_ctx_t *ctx,
|
|
int cancelled_val);
|
|
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_vote_cfg, 0);
|
|
atomic_store_explicit(&ha.term, 1ULL, memory_order_release);
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
/* Set up so if evaluate fires, we'd know (done posted changes result) */
|
|
ctx->votes_granted = 1; /* below quorum — would yield STANDBY */
|
|
ctx->n_replied = 2;
|
|
|
|
/* cancelled=1 → no evaluate → done NOT posted */
|
|
int rc = election_timeout_fn_for_test(&ha, ctx, /*cancelled=*/1);
|
|
TEST_ASSERT_EQUAL_INT(0, rc); /* must return 0 (no-op) */
|
|
|
|
int try_rc = sem_trywait(&ctx->done);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(-1, try_rc,
|
|
"cancelled election_timeout must NOT post ctx->done");
|
|
TEST_ASSERT_EQUAL_INT(EAGAIN, errno);
|
|
|
|
/* cancelled=0 → evaluate runs → done posted */
|
|
ctx->done_posted = 0; /* reset guard */
|
|
ctx->votes[0].my_tag = 100ULL;
|
|
ctx->votes[0].my_lsn = 50ULL;
|
|
rc = election_timeout_fn_for_test(&ha, ctx, /*cancelled=*/0);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
try_rc = sem_trywait(&ctx->done);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, try_rc,
|
|
"not-cancelled election_timeout must post ctx->done");
|
|
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_election_timeout_ctx_cancelled_flag: PASS\n");
|
|
}
|
|
|
|
/* ── Task 2.3: vote_reply_msg_handler tests ─────────────────────────────────
|
|
*
|
|
* Test seam: xce_vote_reply_hook captures decoded fields without real iomgr.
|
|
* vote_reply_build_pkt_for_test encodes a real capnp RequestVoteReply packet.
|
|
* vote_reply_msg_handler_for_test sets ep->fsm_in.pkt and calls the handler.
|
|
*
|
|
* ASAN: handler calls free_packet()+free(pkt) before accessing vr_raw fields,
|
|
* so there is no heap-use-after-free under AddressSanitizer.
|
|
*/
|
|
|
|
/* Forward declarations for task 2.3 test seams (defined in ha.c XCE_TESTING) */
|
|
extern int (*xce_vote_reply_hook)(ha_state_t *ha, ha_elect_ctx_t *ctx,
|
|
int peer_idx, uint64_t term, int vote_granted,
|
|
uint64_t peer_tag, uint64_t peer_lsn);
|
|
|
|
extern io_packet_t *vote_reply_build_pkt_for_test(uint64_t term, int vote_granted,
|
|
uint64_t peer_tag,
|
|
uint64_t peer_lsn);
|
|
|
|
extern int vote_reply_msg_handler_for_test(endpoint_t *ep, io_packet_t *pkt);
|
|
|
|
/* ── Mock vote_reply_hook ───────────────────────────────────────────────── */
|
|
|
|
static int g_reply_hook_calls = 0;
|
|
static uint64_t g_reply_term = 0;
|
|
static int g_reply_granted = -1;
|
|
static uint64_t g_reply_peer_tag = 0;
|
|
static uint64_t g_reply_peer_lsn = 0;
|
|
static int g_reply_peer_idx = -1;
|
|
|
|
static int mock_vote_reply_hook(ha_state_t *ha, ha_elect_ctx_t *ctx,
|
|
int peer_idx, uint64_t term, int vote_granted,
|
|
uint64_t peer_tag, uint64_t peer_lsn)
|
|
{
|
|
(void)ha; (void)ctx;
|
|
g_reply_hook_calls++;
|
|
g_reply_peer_idx = peer_idx;
|
|
g_reply_term = term;
|
|
g_reply_granted = vote_granted;
|
|
g_reply_peer_tag = peer_tag;
|
|
g_reply_peer_lsn = peer_lsn;
|
|
return 0;
|
|
}
|
|
|
|
static void reset_reply_mock(void)
|
|
{
|
|
g_reply_hook_calls = 0;
|
|
g_reply_term = 0;
|
|
g_reply_granted = -1;
|
|
g_reply_peer_tag = 0;
|
|
g_reply_peer_lsn = 0;
|
|
g_reply_peer_idx = -1;
|
|
xce_vote_reply_hook = mock_vote_reply_hook;
|
|
}
|
|
|
|
/* ── Helper: build a minimal vote endpoint with reply context ────────────── */
|
|
|
|
static void make_vote_reply_ep(endpoint_t *ep, ha_state_t *ha,
|
|
ha_elect_ctx_t *ctx, int peer_idx)
|
|
{
|
|
memset(ep, 0, sizeof(*ep));
|
|
ep->fd = -1;
|
|
/* Build and attach a vote_ep_ctx_t via the existing test helper */
|
|
vote_ep_ctx_build_for_test(ep, ha, ctx, peer_idx,
|
|
atomic_load_explicit(&ha->term, memory_order_acquire),
|
|
100ULL, 50ULL, "ctrl-a");
|
|
}
|
|
|
|
/* ── Test: handler decodes correct term, granted, tag, lsn ─────────────── */
|
|
|
|
static void test_vote_reply_handler_decodes_correct_fields(void)
|
|
{
|
|
reset_reply_mock();
|
|
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_vote_cfg, 0);
|
|
atomic_store_explicit(&ha.term, 5ULL, memory_order_release);
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
pthread_mutex_init(&ctx->lock, NULL);
|
|
|
|
endpoint_t ep;
|
|
make_vote_reply_ep(&ep, &ha, ctx, 0);
|
|
|
|
/* Build a real capnp packet with known fields */
|
|
io_packet_t *pkt = vote_reply_build_pkt_for_test(
|
|
5ULL, /* term */
|
|
1, /* vote_granted = true */
|
|
200ULL, /* peer_tag */
|
|
99ULL /* peer_lsn */);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(pkt, "vote_reply_build_pkt_for_test must succeed");
|
|
|
|
/* Call handler — it takes ownership of pkt; frees it internally */
|
|
int rc = vote_reply_msg_handler_for_test(&ep, pkt);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
/* Verify hook was called exactly once */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_reply_hook_calls,
|
|
"vote_reply_msg_handler must call hook exactly once");
|
|
|
|
/* Verify decoded fields */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_reply_peer_idx,
|
|
"peer_idx must match (0)");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(5ULL, g_reply_term,
|
|
"decoded term must be 5");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_reply_granted,
|
|
"decoded vote_granted must be 1");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(200ULL, g_reply_peer_tag,
|
|
"decoded peer_tag must be 200");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(99ULL, g_reply_peer_lsn,
|
|
"decoded peer_lsn must be 99");
|
|
|
|
/* ep->fsm_in.pkt is NOT freed here — handler already freed it */
|
|
|
|
free(ep.data); /* vote_ep_ctx_t allocated by build helper */
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_vote_reply_handler_decodes_correct_fields: PASS\n");
|
|
}
|
|
|
|
/* ── Test: vote_granted=false correctly decoded ──────────────────────────── */
|
|
|
|
static void test_vote_reply_handler_denied_vote(void)
|
|
{
|
|
reset_reply_mock();
|
|
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_vote_cfg, 0);
|
|
atomic_store_explicit(&ha.term, 3ULL, memory_order_release);
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
pthread_mutex_init(&ctx->lock, NULL);
|
|
|
|
endpoint_t ep;
|
|
make_vote_reply_ep(&ep, &ha, ctx, 1);
|
|
|
|
io_packet_t *pkt = vote_reply_build_pkt_for_test(
|
|
3ULL, /* term */
|
|
0, /* vote_granted = false */
|
|
50ULL, /* peer_tag */
|
|
20ULL /* peer_lsn */);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(pkt, "vote_reply_build_pkt_for_test must succeed");
|
|
|
|
int rc = vote_reply_msg_handler_for_test(&ep, pkt);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_reply_hook_calls,
|
|
"hook must be called once even for denied vote");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_reply_granted,
|
|
"vote_granted must be 0 for denied vote");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(3ULL, g_reply_term,
|
|
"term must be decoded correctly even when denied");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_reply_peer_idx,
|
|
"peer_idx must be 1");
|
|
|
|
free(ep.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_vote_reply_handler_denied_vote: PASS\n");
|
|
}
|
|
|
|
/* ── Test: term fencing — higher peer term updates ha->term atomically ───── */
|
|
|
|
static void test_vote_reply_handler_term_fencing(void)
|
|
{
|
|
reset_reply_mock();
|
|
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_vote_cfg, 0);
|
|
atomic_store_explicit(&ha.term, 3ULL, memory_order_release);
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
pthread_mutex_init(&ctx->lock, NULL);
|
|
|
|
endpoint_t ep;
|
|
make_vote_reply_ep(&ep, &ha, ctx, 0);
|
|
|
|
/* Peer claims term 10 — higher than our term 3 */
|
|
io_packet_t *pkt = vote_reply_build_pkt_for_test(
|
|
10ULL, /* peer term > our term */
|
|
0, /* denied */
|
|
0ULL,
|
|
0ULL);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(pkt, "vote_reply_build_pkt_for_test must succeed");
|
|
|
|
vote_reply_msg_handler_for_test(&ep, pkt);
|
|
|
|
/* ha->term must now be updated to 10 */
|
|
uint64_t updated_term = atomic_load_explicit(&ha.term, memory_order_acquire);
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(10ULL, updated_term,
|
|
"ha->term must be atomically updated to peer term when peer term > ours");
|
|
|
|
free(ep.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_vote_reply_handler_term_fencing: PASS\n");
|
|
}
|
|
|
|
/* ── Test: no UAF — pkt is freed before fields are used (ASAN observable) ── */
|
|
|
|
static void test_vote_reply_handler_no_uaf(void)
|
|
{
|
|
/* This test proves ASAN-safety: vote_reply_msg_handler reads ALL capnp
|
|
* scalars into vr_term/vr_granted/vr_tag/vr_lsn BEFORE calling
|
|
* free_packet()+free(pkt). Under ASAN (enabled in xce.build via
|
|
* -fsanitize=address), accessing freed memory would fault immediately.
|
|
* If this test passes under ASAN, there is no heap-use-after-free. */
|
|
reset_reply_mock();
|
|
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_vote_cfg, 0);
|
|
atomic_store_explicit(&ha.term, 2ULL, memory_order_release);
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
pthread_mutex_init(&ctx->lock, NULL);
|
|
|
|
endpoint_t ep;
|
|
make_vote_reply_ep(&ep, &ha, ctx, 0);
|
|
|
|
io_packet_t *pkt = vote_reply_build_pkt_for_test(
|
|
2ULL, 1, 777ULL, 888ULL);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(pkt, "pkt allocation must succeed");
|
|
|
|
/* Handler frees pkt internally; all field reads happen before the free */
|
|
int rc = vote_reply_msg_handler_for_test(&ep, pkt);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
/* Hook captured the fields — verify they are the values we encoded */
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(2ULL, g_reply_term, "term matches");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_reply_granted, "granted matches");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(777ULL, g_reply_peer_tag, "peer_tag matches");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(888ULL, g_reply_peer_lsn, "peer_lsn matches");
|
|
|
|
free(ep.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_vote_reply_handler_no_uaf: PASS\n");
|
|
}
|
|
|
|
/* ── bug-1417: middle-index (self_idx=1) real-vote index-mapping regression ──
|
|
*
|
|
* raft_cb_send_vote_request (ha.c:142) stores the COMPRESSED local votes_idx
|
|
* (self-excluded) in vctx->peer_idx: votes_idx = peer_idx < self_idx ?
|
|
* peer_idx : peer_idx - 1. vote_reply_dispatch (ha.c:5208) then passes that
|
|
* SAME compressed idx straight to raft_on_vote_reply, which indexes
|
|
* s->vote_replies[] in ABSOLUTE space (the space raft_start_election's
|
|
* self-mark and per-peer send loop both use, raft.c:113-153).
|
|
*
|
|
* For self_idx=1 (a 3-node cluster's middle node), the peer at absolute
|
|
* index 2 maps to compressed votes_idx=1 -- which COLLIDES with
|
|
* vote_replies[self_idx=1], the slot raft_start_election already marked as
|
|
* counted for the self-vote. raft_on_vote_reply's per-peer dedup guard
|
|
* (raft.c:477) then treats the genuine reply from peer 2 as a duplicate and
|
|
* silently drops it -- vote_replies[1] was already 1, votes_granted stays
|
|
* at 1 (self only), quorum (2) is never reached, and the election times out
|
|
* even though peer 2 genuinely granted.
|
|
*
|
|
* Peer 0 is unaffected (compressed votes_idx=0 == absolute 0, no collision),
|
|
* which is why sequential/all-peers-alive elections never surfaced this --
|
|
* only a 3-node cluster's middle-priority node losing exactly its
|
|
* higher-priority peer (index 0) and depending on its lower peer's (index 2)
|
|
* vote exposes the collision.
|
|
*/
|
|
|
|
static void make_three_node_self1(ha_state_t *ha)
|
|
{
|
|
ha->raft.n_peers = 3;
|
|
ha->raft.self_idx = ha->my_priority; /* my_priority == 1 for this test */
|
|
ha->raft.role = RAFT_FOLLOWER;
|
|
ha->n_peers = 3;
|
|
}
|
|
|
|
static void test_bug1417_middle_index_real_vote_from_peer2_not_dropped(void)
|
|
{
|
|
reset_reply_mock();
|
|
xce_vote_reply_hook = NULL; /* drive the REAL raft_on_vote_reply path, not the hook */
|
|
|
|
ha_state_t ha;
|
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_vote_cfg, /*my_priority=*/1));
|
|
make_three_node_self1(&ha);
|
|
|
|
/* Start a real election: term++, self-vote, vote_replies[self_idx=1]=1
|
|
* pre-marked (raft.c:122-123), CANDIDATE role, one on_send_vote_request
|
|
* call per non-self absolute peer (0 and 2) -- c==NULL so each send is a
|
|
* safe no-op (raft_cb_send_vote_request returns 1 immediately), exactly
|
|
* the same test-mode pattern test_ha_prevote.c uses for
|
|
* ha_start_prevote_async(&ha, NULL). */
|
|
int rc = raft_start_election(&ha.raft);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(RAFT_OK, rc,
|
|
"3-node multi-node election must return RAFT_OK (CANDIDATE transition)");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(RAFT_CANDIDATE, (int)ha.raft.role,
|
|
"raft role must be CANDIDATE after starting a 3-node election");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.votes_granted,
|
|
"self-vote must be counted (1) before any peer reply");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.vote_replies[1],
|
|
"self_idx's own vote_replies slot must be pre-marked");
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2); /* n_other = n_peers - 1 = 2 */
|
|
pthread_mutex_init(&ctx->lock, NULL);
|
|
|
|
uint64_t term = atomic_load_explicit(&ha.term, memory_order_acquire);
|
|
|
|
/* Peer at ABSOLUTE index 2 grants. raft_cb_send_vote_request's mapping
|
|
* (peer_idx=2 >= self_idx=1) computes votes_idx = 2 - 1 = 1 -- this IS
|
|
* the value production code stores in vctx->peer_idx for this peer, so
|
|
* the test reproduces the exact wire-level index the real send path
|
|
* would have used. */
|
|
endpoint_t ep2;
|
|
make_vote_reply_ep(&ep2, &ha, ctx, /*peer_idx (compressed votes_idx)=*/1);
|
|
|
|
io_packet_t *pkt2 = vote_reply_build_pkt_for_test(term, /*granted=*/1, 0ULL, 0ULL);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(pkt2, "vote_reply_build_pkt_for_test must succeed");
|
|
|
|
vote_reply_msg_handler_for_test(&ep2, pkt2);
|
|
|
|
/* This is the bug: without ha_repl_ack_abs_peer_idx remapping compressed
|
|
* votes_idx=1 back to absolute peer_idx=2 before calling
|
|
* raft_on_vote_reply, the reply lands in vote_replies[1] -- the
|
|
* self-vote's own slot, already 1 -- and is silently discarded as a
|
|
* duplicate. votes_granted stays at 1 (self only), never reaching the
|
|
* 2-vote quorum, even though peer 2 genuinely granted. */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(2, ha.raft.votes_granted,
|
|
"bug-1417: peer 2's genuine grant must be counted (votes_granted==2), "
|
|
"not silently dropped into the self-vote's slot");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(RAFT_LEADER, (int)ha.raft.role,
|
|
"bug-1417: with self-vote + peer 2's grant, quorum (2) is reached and "
|
|
"the node must become LEADER");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.election_decided,
|
|
"election must be decided (won) once quorum is reached");
|
|
|
|
free(ep2.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_bug1417_middle_index_real_vote_from_peer2_not_dropped: PASS\n");
|
|
}
|
|
|
|
/* ── raft-adapter-conformance task 3.1: post-connect vote failure routing ──
|
|
*
|
|
* vote_ep_err_handler's election-phase branch (the iomgr err_handler fired
|
|
* when an already-connected outgoing vote endpoint fails/closes before a
|
|
* reply arrives) must count that outcome through vote_reply_dispatch -- the
|
|
* SAME production tally path a connect-time failure (raft_cb_send_vote_request's
|
|
* own rc<0 branches, bug-1423) or a delivered denial (vote_reply_msg_handler)
|
|
* uses -- not the legacy handle_request_vote_reply, which bypasses
|
|
* raft_on_vote_reply entirely and, critically for this test, never reaches the
|
|
* xce_vote_reply_hook seam vote_reply_dispatch checks.
|
|
*
|
|
* Test seam: xce_vote_reply_hook (checked ONLY inside vote_reply_dispatch) lets
|
|
* us observe, without a live iomgr/raft election, whether the post-connect
|
|
* failure reached the production tally path. Pre-fix, this hook is never
|
|
* invoked by vote_ep_err_handler's election branch -- proving the bug.
|
|
*/
|
|
|
|
extern int vote_ep_err_handler_for_test(endpoint_t *ep);
|
|
|
|
static void test_post_connect_vote_failure_routes_through_production_tally(void)
|
|
{
|
|
reset_reply_mock();
|
|
|
|
ha_state_t ha;
|
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_vote_cfg, 0));
|
|
/* Election phase, not CTRL_PRIMARY -- exercises the "else" branch. */
|
|
atomic_store_explicit(&ha.role, CTRL_ELECTING, memory_order_release);
|
|
atomic_store_explicit(&ha.term, 4ULL, memory_order_release);
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
pthread_mutex_init(&ctx->lock, NULL);
|
|
|
|
endpoint_t ep;
|
|
make_endpoint(&ep);
|
|
|
|
/* Build a real-vote endpoint context as if raft_cb_send_vote_request had
|
|
* already connected it (on_vote_connected fired, request sent) -- then
|
|
* the connection fails before any reply arrives: iomgr invokes the
|
|
* registered err_handler on this same ep. */
|
|
vote_ep_ctx_build_for_test(&ep, &ha, ctx, /*peer_idx=*/0,
|
|
4ULL, 100ULL, 50ULL, "ctrl-a");
|
|
|
|
int rc = vote_ep_err_handler_for_test(&ep);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_reply_hook_calls,
|
|
"post-connect vote failure must reach vote_reply_dispatch's tally "
|
|
"(xce_vote_reply_hook), not the legacy handle_request_vote_reply path "
|
|
"which never invokes this hook");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_reply_granted,
|
|
"a post-connect failure must be counted as a denied vote (granted=0)");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(4ULL, g_reply_term,
|
|
"the synthesized denial must carry the candidate's own term");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_reply_peer_idx,
|
|
"peer_idx must match the failed endpoint's vote_ep_ctx_t peer_idx");
|
|
|
|
free(ep.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_post_connect_vote_failure_routes_through_production_tally: PASS\n");
|
|
}
|
|
|
|
/* ── raft-adapter-conformance task 3.2: post-connect PreVote failure routing ──
|
|
*
|
|
* pre_vote_ep_err_handler's election-phase branch (task 3.1) must count a
|
|
* post-connect PreVote endpoint failure through pre_vote_reply_dispatch --
|
|
* the SAME production tally path a connect-time failure or a delivered
|
|
* PreVote reply uses -- not a legacy no-op path. Requirement 4.3 (mirror of
|
|
* 4.2 for the PreVote endpoint kind).
|
|
*
|
|
* Test seam: pre_vote_reply_dispatch has no mock hook analogous to
|
|
* xce_vote_reply_hook (it calls raft_on_pre_vote_reply directly). Instead we
|
|
* assert directly on raft_state_t's own PreVote tally fields
|
|
* (pre_vote_replies[], pre_votes_granted, pre_vote_in_progress) -- the same
|
|
* fields test_ha_prevote.c already asserts on for the delivered-reply path
|
|
* (pre_vote_reply_dispatch_for_test). Pre-fix, pre_vote_ep_err_handler routed
|
|
* this failure through the legacy handle_request_vote_reply path, which never
|
|
* touches raft_on_pre_vote_reply or these fields -- so pre_vote_replies[1]
|
|
* would stay 0 and this test would fail against pre-fix code.
|
|
*/
|
|
|
|
extern int pre_vote_ep_err_handler_for_test(endpoint_t *ep);
|
|
|
|
extern void pre_vote_reply_dispatch_for_test(ha_state_t *ha, cluster_t *c,
|
|
int peer_idx, int granted);
|
|
|
|
/* A LOST round re-arms the liveness watchdog (ha_prevote_rearm_liveness).
|
|
* With the hook unset that falls through to the real
|
|
* ha_arm_heartbeat_timeout_vtask, which -- with c==NULL in unit-test mode --
|
|
* calloc's a ctx into the static s_liveness_ctx and never arms a delaymgr
|
|
* task to free it (delay_task_ms is gated on `c && c->delaymgr.running`),
|
|
* leaking it and leaving a stale pointer into this test's stack-local `ha`
|
|
* once the test returns. test_ha_prevote.c avoids this identical hazard on
|
|
* its own LOST-path tests via the same hook override -- mirrored here. */
|
|
extern int (*ha_prevote_rearm_liveness_hook)(ha_state_t *ha, cluster_t *c);
|
|
|
|
static int stub_prevote_rearm_liveness(ha_state_t *ha, cluster_t *c)
|
|
{
|
|
(void)ha; (void)c;
|
|
return 0;
|
|
}
|
|
|
|
/* Build a 3-node (self + 2 peers) PreVote round in progress, mirroring
|
|
* test_ha_prevote.c's make_three_node + ha_start_prevote_async(&ha, NULL)
|
|
* fixture: role stays CTRL_STANDBY (the real production role throughout a
|
|
* PreVote round -- a node only becomes CTRL_ELECTING after WINNING the
|
|
* round), self-grant is recorded by the real raft_begin_pre_vote, and both
|
|
* non-self peers (absolute index 1 and 2) start unreplied. */
|
|
static void start_three_node_prevote_round(ha_state_t *ha)
|
|
{
|
|
ha_do_demote(ha);
|
|
ha->raft.n_peers = 3;
|
|
ha->raft.self_idx = ha->my_priority; /* my_priority == 0 for this fixture */
|
|
ha->raft.role = RAFT_FOLLOWER;
|
|
ha->n_peers = 3;
|
|
|
|
ha_start_prevote_async(ha, NULL);
|
|
}
|
|
|
|
static void test_post_connect_prevote_failure_routes_through_production_tally(void)
|
|
{
|
|
ha_state_t ha;
|
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_vote_cfg, 0));
|
|
atomic_store_explicit(&ha.term, 4ULL, memory_order_release);
|
|
|
|
start_three_node_prevote_round(&ha);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.pre_vote_in_progress,
|
|
"round must be in progress before the failure arrives");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.pre_votes_granted,
|
|
"self-grant must already be recorded by raft_begin_pre_vote");
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
pthread_mutex_init(&ctx->lock, NULL);
|
|
|
|
endpoint_t ep;
|
|
make_endpoint(&ep);
|
|
|
|
/* Build a PreVote endpoint context as if raft_cb_send_pre_vote_request had
|
|
* already connected it (on_vote_connected fired, request sent) -- then
|
|
* the connection fails before any reply arrives: iomgr invokes the
|
|
* registered err_handler (pre_vote_ep_err_handler) on this same ep. */
|
|
vote_ep_ctx_build_for_test(&ep, &ha, ctx, /*peer_idx=*/1,
|
|
4ULL, 100ULL, 50ULL, "ctrl-a");
|
|
|
|
int rc = pre_vote_ep_err_handler_for_test(&ep);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.pre_vote_replies[1],
|
|
"post-connect PreVote failure must reach pre_vote_reply_dispatch's "
|
|
"tally (raft_on_pre_vote_reply), not a legacy no-op path");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.pre_votes_granted,
|
|
"a post-connect PreVote failure must be counted as NOT granted "
|
|
"(pre_votes_granted stays at the self-grant only)");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.pre_vote_in_progress,
|
|
"round stays pending -- only 1 of 2 non-self peers has been counted, "
|
|
"no quorum(2) or all-replied(2) condition met yet");
|
|
|
|
free(ep.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_post_connect_prevote_failure_routes_through_production_tally: PASS\n");
|
|
}
|
|
|
|
/* ── raft-adapter-conformance task 3.2: fast quorum decision (Req 4.5) ────────
|
|
*
|
|
* Once every peer in a PreVote round has had its outcome counted -- whether
|
|
* via a delivered reply or via this post-connect failure mode -- the round
|
|
* must reach its quorum decision immediately (synchronously inside the
|
|
* counting call), without waiting for the round's full timeout
|
|
* (ha_prevote_round_timeout_fn is never invoked in this test). Mixes both
|
|
* counting paths in the same round to prove the failure path is a full
|
|
* first-class participant in the fast "all replied" decision, not merely
|
|
* counted without unblocking the round.
|
|
*/
|
|
static void test_prevote_round_reaches_quorum_decision_via_fast_tally(void)
|
|
{
|
|
ha_state_t ha;
|
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_vote_cfg, 0));
|
|
atomic_store_explicit(&ha.term, 4ULL, memory_order_release);
|
|
|
|
start_three_node_prevote_round(&ha);
|
|
|
|
/* This test's mixed round decides LOST (below), which re-arms the
|
|
* liveness watchdog -- stub it out so the real (c==NULL) production path
|
|
* is never exercised here (see extern decl comment above). */
|
|
ha_prevote_rearm_liveness_hook = stub_prevote_rearm_liveness;
|
|
|
|
ha_elect_ctx_t *ctx = alloc_ctx_for_vote(2);
|
|
pthread_mutex_init(&ctx->lock, NULL);
|
|
|
|
/* Peer 1: post-connect endpoint failure -- counted via
|
|
* pre_vote_ep_err_handler (the fix under test). */
|
|
endpoint_t ep1;
|
|
make_endpoint(&ep1);
|
|
vote_ep_ctx_build_for_test(&ep1, &ha, ctx, /*peer_idx=*/1,
|
|
4ULL, 100ULL, 50ULL, "ctrl-a");
|
|
TEST_ASSERT_EQUAL_INT(0, pre_vote_ep_err_handler_for_test(&ep1));
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.pre_vote_in_progress,
|
|
"round must still be pending -- peer 2's outcome is not counted yet");
|
|
|
|
/* Peer 2: a normal delivered denial through the production reply-dispatch
|
|
* path -- the same tally the post-connect failure above also reaches. */
|
|
pre_vote_reply_dispatch_for_test(&ha, NULL, /*peer_idx=*/2, /*granted=*/0);
|
|
|
|
/* Every non-self peer's outcome is now counted (one via the post-connect
|
|
* failure path, one via a delivered reply) -- the round must reach its
|
|
* LOST decision immediately, without any call into
|
|
* ha_prevote_round_timeout_fn (Requirement 4.5's fast "all replied"
|
|
* path). */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, ha.raft.pre_vote_in_progress,
|
|
"Req 4.5: once every peer outcome is counted -- including a "
|
|
"post-connect failure -- the round must reach its quorum decision "
|
|
"without waiting for the round's full timeout");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.pre_votes_granted,
|
|
"neither non-self peer granted -- self-grant only, quorum(2) never "
|
|
"reached");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(CTRL_STANDBY,
|
|
(int)atomic_load_explicit(&ha.role, memory_order_acquire),
|
|
"a LOST PreVote round leaves the node CTRL_STANDBY (Req 1.4, "
|
|
"unchanged by this fix)");
|
|
|
|
ha_prevote_rearm_liveness_hook = NULL; /* reset -- do not leak into other tests */
|
|
|
|
free(ep1.data);
|
|
free_ctx_for_vote(ctx);
|
|
ha_fini(&ha);
|
|
printf("test_prevote_round_reaches_quorum_decision_via_fast_tally: PASS\n");
|
|
}
|
|
|
|
/* ── Suite entry point ───────────────────────────────────────────────────── */
|
|
|
|
void test_ha_vote_send(void)
|
|
{
|
|
UnitySetTestFile(__FILE__);
|
|
RUN_TEST(test_on_vote_connected_calls_hook_with_correct_opcode);
|
|
RUN_TEST(test_on_vote_connected_encodes_correct_fields);
|
|
RUN_TEST(test_on_vote_connected_sets_msg_handler);
|
|
RUN_TEST(test_election_timeout_ctx_cancelled_flag);
|
|
/* Task 2.3: vote_reply_msg_handler */
|
|
RUN_TEST(test_vote_reply_handler_decodes_correct_fields);
|
|
RUN_TEST(test_vote_reply_handler_denied_vote);
|
|
RUN_TEST(test_vote_reply_handler_term_fencing);
|
|
RUN_TEST(test_vote_reply_handler_no_uaf);
|
|
/* bug-1417 */
|
|
RUN_TEST(test_bug1417_middle_index_real_vote_from_peer2_not_dropped);
|
|
/* raft-adapter-conformance task 3.1 */
|
|
RUN_TEST(test_post_connect_vote_failure_routes_through_production_tally);
|
|
/* raft-adapter-conformance task 3.2 */
|
|
RUN_TEST(test_post_connect_prevote_failure_routes_through_production_tally);
|
|
RUN_TEST(test_prevote_round_reaches_quorum_decision_via_fast_tally);
|
|
}
|