forked from lei.chen/myxce
Only 21 of 86 test_*.c group functions called their own nested UNITY_BEGIN()/UNITY_END(). Each nested call reset Unity's global NumberOfTests/TestFailures counters, so controller_test.c's final `return UNITY_END();` -- the value meson test actually gates on -- only ever reflected whichever small window of tests ran since the LAST nested reset, silently discarding pass/fail evidence for everything before it (measured: 16 of 972 total tests in one run). The same nested-BEGIN calls also caused PASS/FAIL banners to show the wrong source file for tests in files that don't reset it themselves (Unity.TestFile just carries over from whichever group last called UNITY_BEGIN()). Fix: replace every nested UNITY_BEGIN() with UnitySetTestFile(__FILE__) (sets file attribution only, no counter reset) and drop the matching UNITY_END(); add the same UnitySetTestFile(__FILE__) as the first statement to the other group functions that had neither. The 2 genuinely standalone binaries (test_job_array_sched_push.c, test_job_array_apiserver_push.c -- each has its own int main(), not registered via controller_test.c's TEST() macro) are untouched. Verified: single consolidated summary now reads "972 Tests 0 Failures 0 Ignored OK", exactly matching the ground-truth RUN_TEST call-site count across all 83 registered group files (976 total minus 4 belonging to the 2 standalone binaries). Every PASS/FAIL banner now names its own true source file. Full xce-build.sh all regression pass clean (Ok: 9, Fail: 0). No production code touched -- test-harness plumbing only.
262 lines
11 KiB
C
262 lines
11 KiB
C
/*
|
|
* test_ha_elect_ctx_lifecycle.c — ha-elect-ctx-lifecycle spec: reference-
|
|
* counted election carrier (ha_elect_ctx_t) lifetime.
|
|
*
|
|
* Task 2.1: reference count field + single release helper.
|
|
* Task 2.2/2.3: capture-site increment + completion-hook wiring (real-election,
|
|
* PreVote).
|
|
* Task 2.4: supersession release at both round-start publish points.
|
|
*
|
|
* The carrier is deliberately leaked forever today (see ha.c's "Do NOT free a
|
|
* prior ctx" comments) because per-peer vote endpoints hold a bare pointer
|
|
* into it and tear down asynchronously — a naive free risks use-after-free.
|
|
* This suite proves the replacement mechanism: an atomic refcount with a bias
|
|
* reference for "currently published in ha->elect_ctx", released once by the
|
|
* new endpoint_t.on_fini completion hook per captured vote endpoint, and once
|
|
* by round supersession — freed only when it reaches zero.
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <pthread.h>
|
|
#include <semaphore.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
#include "ha.h"
|
|
#include "netio.h"
|
|
|
|
/* ── XCE_TESTING seams ────────────────────────────────────────────────────── */
|
|
extern void ha_elect_ctx_release_for_test(ha_elect_ctx_t *ctx);
|
|
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);
|
|
extern void vote_ep_wire_on_fini_for_test(endpoint_t *ep);
|
|
|
|
static ha_config_t g_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,
|
|
};
|
|
|
|
/* ── Fixture: minimal, correctly-initialised carrier ─────────────────────── */
|
|
|
|
static ha_elect_ctx_t *make_test_elect_ctx(int n_peers)
|
|
{
|
|
int n_other = (n_peers > 1) ? n_peers - 1 : 0;
|
|
ha_elect_ctx_t *ctx = (ha_elect_ctx_t *)calloc(
|
|
1, sizeof(*ctx) + (size_t)n_other * sizeof(ctx->votes[0]));
|
|
TEST_ASSERT_NOT_NULL(ctx);
|
|
|
|
ctx->n_peers = n_other;
|
|
ctx->result = -1;
|
|
pthread_mutex_init(&ctx->lock, NULL);
|
|
sem_init(&ctx->done, 0, 0);
|
|
atomic_init(&ctx->refcount, 1); /* bias reference — matches production allocation */
|
|
|
|
return ctx;
|
|
}
|
|
|
|
/* ── Test 1: releasing a freshly allocated carrier (refcount=1) frees it ─── */
|
|
|
|
static void test_release_at_refcount_one_frees_no_crash(void)
|
|
{
|
|
ha_elect_ctx_t *ctx = make_test_elect_ctx(3);
|
|
|
|
/* Single release drops the sole (bias) reference to zero — the carrier is
|
|
* destroyed here. Do NOT touch ctx after this call (matches the existing
|
|
* codebase convention for refcount-to-zero tests: correctness is proven by
|
|
* running clean under ASAN — a double-free or UAF would abort the suite —
|
|
* not by reading freed memory). */
|
|
ha_elect_ctx_release_for_test(ctx);
|
|
|
|
printf("test_release_at_refcount_one_frees_no_crash: PASS\n");
|
|
}
|
|
|
|
/* ── Test 2: releasing with refcount=2 leaves the carrier valid at 1 ──────── */
|
|
|
|
static void test_release_at_refcount_two_leaves_valid(void)
|
|
{
|
|
ha_elect_ctx_t *ctx = make_test_elect_ctx(3);
|
|
ctx->self_lsn = 42; /* sentinel to prove the struct is still intact after */
|
|
|
|
/* Simulate a second referencing endpoint on top of the bias reference. */
|
|
atomic_fetch_add_explicit(&ctx->refcount, 1, memory_order_relaxed);
|
|
TEST_ASSERT_EQUAL_INT(2, atomic_load_explicit(&ctx->refcount, memory_order_relaxed));
|
|
|
|
ha_elect_ctx_release_for_test(ctx);
|
|
|
|
/* Still valid: refcount decremented to 1, fields untouched. */
|
|
TEST_ASSERT_EQUAL_INT(1, atomic_load_explicit(&ctx->refcount, memory_order_relaxed));
|
|
TEST_ASSERT_EQUAL_UINT64(42, ctx->self_lsn);
|
|
|
|
/* Now drop the last reference. */
|
|
ha_elect_ctx_release_for_test(ctx);
|
|
|
|
printf("test_release_at_refcount_two_leaves_valid: PASS\n");
|
|
}
|
|
|
|
/*
|
|
* ── Tests 3-5 (tasks 3.1/3.2/3.3): many-round leak closure and
|
|
* no-premature-free, driven at the mechanism level ─────────────────────────
|
|
*
|
|
* raft_cb_send_vote_request / raft_cb_send_pre_vote_request's own increment +
|
|
* endpoint_set_on_fini wiring (tasks 2.2/2.3) require a real cluster_t/myconf
|
|
* to reach past their `if (!c || !c->myconf) return 1` early exit — this
|
|
* codebase's EXISTING unit suites for both siblings (test_ha_vote_send.c,
|
|
* test_ha_prevote.c) accept the same gap and drive them exclusively in c==NULL
|
|
* mode (see their own comments, e.g. test_ha_vote_send.c: "raft_cb_send_vote_request
|
|
* returns 1 immediately"). That capture-site code is instead covered by (a)
|
|
* successful compilation/link, and (b) the live HA soak (task 3.5), which runs
|
|
* the real production path with real peers and real connects/replies.
|
|
*
|
|
* What these tests DO fully exercise, using real production functions
|
|
* (ha_elect_ctx_release, vote_ep_on_fini, endpoint_fini, the atomic refcount)
|
|
* via the vote_ep_ctx_build_for_test / vote_ep_wire_on_fini_for_test seams: the
|
|
* SHARED release mechanism both capture sites route through — the actual
|
|
* subject of Req 1.1-1.3, 2.1-2.4, 3.1-3.2.
|
|
*/
|
|
|
|
/* Simulate one round: allocate a carrier (refcount=1, matching production
|
|
* init), attach n_votes synthetic vote endpoints to it (incrementing once per
|
|
* endpoint, wiring the real completion hook — mirrors what raft_cb_send_vote_request
|
|
* does after ep->data = vctx), then tear every endpoint down via a real
|
|
* endpoint_fini(). Returns the carrier so the caller decides when to
|
|
* supersede it. */
|
|
static ha_elect_ctx_t *simulate_round_with_n_votes(ha_state_t *ha, int n_votes)
|
|
{
|
|
ha_elect_ctx_t *ctx = make_test_elect_ctx(n_votes + 1);
|
|
|
|
endpoint_t *eps[8];
|
|
TEST_ASSERT_TRUE(n_votes <= 8);
|
|
for (int i = 0; i < n_votes; i++) {
|
|
endpoint_t *ep = endpoint_get();
|
|
TEST_ASSERT_NOT_NULL(ep);
|
|
vote_ep_ctx_build_for_test(ep, ha, ctx, i, 1, 0, 0, "ctrl-a");
|
|
atomic_fetch_add_explicit(&ctx->refcount, 1, memory_order_relaxed);
|
|
vote_ep_wire_on_fini_for_test(ep);
|
|
eps[i] = ep;
|
|
}
|
|
|
|
/* Tear every endpoint down for real — fires vote_ep_on_fini, which
|
|
* releases the reference this endpoint took above — then reclaim the
|
|
* heap-allocated endpoint_t itself (endpoint_fini frees ep->data but
|
|
* never the endpoint_t struct; endpoint_get() heap-allocated it). */
|
|
for (int i = 0; i < n_votes; i++) {
|
|
endpoint_fini(eps[i]);
|
|
free(eps[i]);
|
|
}
|
|
|
|
return ctx;
|
|
}
|
|
|
|
static void test_many_election_rounds_no_carrier_leak(void)
|
|
{
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_cfg, 0);
|
|
|
|
ha_elect_ctx_t *prev = NULL;
|
|
for (int round = 0; round < 20; round++) {
|
|
ha_elect_ctx_t *ctx = simulate_round_with_n_votes(&ha, 2);
|
|
/* Supersede: release the PRIOR round's carrier now that this round's
|
|
* carrier has published (mirrors ha_start_election_async's
|
|
* atomic_exchange_explicit + ha_elect_ctx_release at task 2.4). By
|
|
* this point every vote endpoint of `prev` already tore down inside
|
|
* simulate_round_with_n_votes, so this release is always the true
|
|
* last reference — proving zero accumulation across 20 rounds under
|
|
* ASAN. */
|
|
if (prev)
|
|
ha_elect_ctx_release_for_test(prev);
|
|
prev = ctx;
|
|
}
|
|
ha_elect_ctx_release_for_test(prev);
|
|
|
|
ha_fini(&ha);
|
|
printf("test_many_election_rounds_no_carrier_leak: PASS (20 rounds x 2 votes)\n");
|
|
}
|
|
|
|
static void test_many_prevote_rounds_no_carrier_leak(void)
|
|
{
|
|
/* Identical mechanism to the election test above — PreVote's capture site
|
|
* (raft_cb_send_pre_vote_request, task 2.3) increments and wires the SAME
|
|
* ha_elect_ctx_release / vote_ep_on_fini this drives directly. Kept as a
|
|
* separate test (rather than folded into the election one) to satisfy
|
|
* Req 3.1/3.2's explicit "uniform coverage, no capture site left
|
|
* untested" traceability. */
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_cfg, 0);
|
|
|
|
ha_elect_ctx_t *prev = NULL;
|
|
for (int round = 0; round < 20; round++) {
|
|
ha_elect_ctx_t *ctx = simulate_round_with_n_votes(&ha, 2);
|
|
if (prev)
|
|
ha_elect_ctx_release_for_test(prev);
|
|
prev = ctx;
|
|
}
|
|
ha_elect_ctx_release_for_test(prev);
|
|
|
|
ha_fini(&ha);
|
|
printf("test_many_prevote_rounds_no_carrier_leak: PASS (20 rounds x 2 votes)\n");
|
|
}
|
|
|
|
/* ── Test (task 3.3): carrier survives supersession while an endpoint is
|
|
* still outstanding, freed only once that endpoint's teardown completes ── */
|
|
|
|
static void test_carrier_survives_supersession_with_outstanding_endpoint(void)
|
|
{
|
|
ha_state_t ha;
|
|
ha_init(&ha, &g_cfg, 0);
|
|
|
|
ha_elect_ctx_t *round1 = make_test_elect_ctx(2);
|
|
|
|
/* One vote endpoint captures round1 and is NOT torn down yet. */
|
|
endpoint_t *ep = endpoint_get();
|
|
TEST_ASSERT_NOT_NULL(ep);
|
|
vote_ep_ctx_build_for_test(ep, &ha, round1, 0, 1, 0, 0, "ctrl-a");
|
|
atomic_fetch_add_explicit(&round1->refcount, 1, memory_order_relaxed);
|
|
vote_ep_wire_on_fini_for_test(ep);
|
|
TEST_ASSERT_EQUAL_INT(2, atomic_load_explicit(&round1->refcount, memory_order_relaxed));
|
|
|
|
/* A new round supersedes round1 (mirrors ha_start_election_async's
|
|
* exchange+release at task 2.4) — releases the BIAS reference only. */
|
|
ha_elect_ctx_t *round2 = make_test_elect_ctx(2);
|
|
ha_elect_ctx_release_for_test(round1); /* bias release: 2 -> 1 */
|
|
|
|
/* round1 must still be valid — one endpoint reference remains outstanding.
|
|
* (We do not read round1's fields here beyond the refcount we already
|
|
* captured before this point, matching this suite's convention of never
|
|
* dereferencing a carrier that could be at its last reference — but the
|
|
* key proof is that NOTHING crashes/UAFs between here and the endpoint's
|
|
* teardown below, verified by ASAN.) */
|
|
|
|
/* Now the outstanding endpoint tears down for real — this must be the
|
|
* TRUE last reference and free round1 cleanly. */
|
|
endpoint_fini(ep);
|
|
free(ep);
|
|
|
|
/* Clean up round2 (nothing superseded it). */
|
|
ha_elect_ctx_release_for_test(round2);
|
|
|
|
ha_fini(&ha);
|
|
printf("test_carrier_survives_supersession_with_outstanding_endpoint: PASS\n");
|
|
}
|
|
|
|
/* ── Test suite entry point ───────────────────────────────────────── */
|
|
|
|
void test_ha_elect_ctx_lifecycle(void)
|
|
{
|
|
UnitySetTestFile(__FILE__);
|
|
printf("\n>> ha_elect_ctx_t reference-counted lifecycle tests\n\n");
|
|
|
|
RUN_TEST(test_release_at_refcount_one_frees_no_crash);
|
|
RUN_TEST(test_release_at_refcount_two_leaves_valid);
|
|
RUN_TEST(test_many_election_rounds_no_carrier_leak);
|
|
RUN_TEST(test_many_prevote_rounds_no_carrier_leak);
|
|
RUN_TEST(test_carrier_survives_supersession_with_outstanding_endpoint);
|
|
}
|