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.
273 lines
12 KiB
C
273 lines
12 KiB
C
/* test_ha_vote_floor_heartbeat.c — characterization test for
|
|
* ha-heartbeat-committed-propagation task 3.2 (Req 3.2).
|
|
*
|
|
* SAFETY PROPERTY UNDER TEST (no production change in this task):
|
|
* The EXISTING vote-freshness floor is `max(durable_floor, last_known_committed_lsn)`
|
|
* (computed by ha_vote_self_freshness; design "Security Considerations"). This
|
|
* feature only RAISES `last_known_committed_lsn` via a heartbeat
|
|
* (ha_observe_heartbeat_committed). We prove that raising it through a
|
|
* heartbeat:
|
|
* 1. flows UNCHANGED into the floor (floor == max(durable_floor, RAISED)),
|
|
* 2. REJECTS a vote candidate whose last-log LSN is below the raised floor,
|
|
* 3. ACCEPTS a candidate at/above it,
|
|
* 4. never LOWERS the floor below the durable floor (monotone strengthening).
|
|
*
|
|
* This is a VERIFICATION/CHARACTERIZATION test: it must PASS against the
|
|
* already-landed code with NO production edit. If it failed, the safety
|
|
* property would NOT hold — a spec finding, not a green-on-new-code cycle.
|
|
*
|
|
* The gate functions exercised are the SAME ones the existing vote tests use
|
|
* (handle_request_vote + ha_vote_self_freshness in ha.c) — no new gate invented.
|
|
*
|
|
* Covers Requirement 3.2 (advisory committed must not LOWER/WEAKEN the
|
|
* durability-backed vote floor; raising it only strengthens Leader Completeness).
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "ha.h"
|
|
#include "xce/cluster_def.h" /* full cluster_t (embedded ha_state_t) */
|
|
|
|
/* Functions under test — the EXISTING vote gate (declared to match ha.c). */
|
|
int handle_request_vote(ha_state_t *ha,
|
|
uint64_t term,
|
|
const char *candidate_id,
|
|
uint64_t config_tag,
|
|
uint64_t last_lsn,
|
|
uint64_t my_config_tag,
|
|
uint64_t my_last_lsn,
|
|
int in_catch_up_flag);
|
|
|
|
void ha_vote_self_freshness(ha_state_t *ha,
|
|
uint64_t local_config_tag,
|
|
uint64_t local_applied_lsn,
|
|
uint64_t *out_tag,
|
|
uint64_t *out_lsn);
|
|
|
|
/* ── shared config ──────────────────────────────────────────────── */
|
|
|
|
static ha_config_t vff_cfg(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,
|
|
};
|
|
return cfg;
|
|
}
|
|
|
|
/* Open the role gate so handle_request_vote falls through to the freshness
|
|
* floor: STANDBY, no live primary connection (fd<0), stale heartbeat. */
|
|
static void vff_open_role_gate(ha_state_t *ha)
|
|
{
|
|
atomic_store_explicit(&ha->role, (int32_t)CTRL_STANDBY, memory_order_release);
|
|
atomic_store_explicit(&ha->term, 1ULL, memory_order_release);
|
|
ha->voted_for_term = 0;
|
|
ha->primary_ctrl_fd = -1;
|
|
|
|
struct timespec now;
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
ha->last_activity = now.tv_sec - 60; /* 60 s ago → stale */
|
|
}
|
|
|
|
/* The durable floor enters the gate as the `local_applied_lsn` / `my_last_lsn`
|
|
* argument (production cluster.c feeds persist_durable_floor there — see
|
|
* test_ha_vote_gate.c task-4.2 tests). We model that durable floor as a plain
|
|
* constant input so this test stays focused on the heartbeat→floor flow without
|
|
* a real persist fixture. */
|
|
#define DURABLE_FLOOR 5ULL
|
|
#define INITIAL_LEARNED 3ULL /* < DURABLE_FLOOR: floor starts at durable */
|
|
#define RAISED_LSN 9ULL /* heartbeat raises learned ABOVE durable */
|
|
|
|
/* ── (1) the heartbeat value flows into the floor and RAISES it ──────────── */
|
|
|
|
static void test_heartbeat_raises_vote_floor_to_max(void)
|
|
{
|
|
cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
ha_config_t cfg = vff_cfg();
|
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&c.ha, &cfg, 0));
|
|
|
|
/* Initial learned-committed is BELOW the durable floor → floor==durable. */
|
|
atomic_store_explicit(&c.ha.last_known_committed_lsn, INITIAL_LEARNED,
|
|
memory_order_release);
|
|
|
|
uint64_t tag0 = 0, lsn0 = 0;
|
|
ha_vote_self_freshness(&c.ha, 0ULL /*local tag*/, DURABLE_FLOOR, &tag0, &lsn0);
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(DURABLE_FLOOR, lsn0,
|
|
"before the heartbeat the floor must be max(durable=5, learned=3) = 5");
|
|
|
|
/* A heartbeat raises the learned committed ABOVE the durable floor. The tag
|
|
* argument is the heartbeat's config_tag (advisory, orthogonal here). */
|
|
ha_observe_heartbeat_committed(&c, RAISED_LSN, 0xAB);
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(RAISED_LSN,
|
|
atomic_load_explicit(&c.ha.last_known_committed_lsn, memory_order_acquire),
|
|
"heartbeat must have raised last_known_committed_lsn to 9");
|
|
|
|
/* The EXISTING floor now consumes the raised value: max(5, 9) = 9. */
|
|
uint64_t tag1 = 0, lsn1 = 0;
|
|
ha_vote_self_freshness(&c.ha, 0ULL, DURABLE_FLOOR, &tag1, &lsn1);
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(RAISED_LSN, lsn1,
|
|
"after the heartbeat the floor must be max(durable=5, learned=9) = 9 — "
|
|
"the raised last_known_committed_lsn flowed in UNCHANGED (Req 3.2)");
|
|
|
|
/* Monotone strengthening: the floor was RAISED (9 > 5), never lowered. */
|
|
TEST_ASSERT_TRUE_MESSAGE(lsn1 >= lsn0,
|
|
"the heartbeat may only RAISE the floor, never lower it (Req 3.2)");
|
|
TEST_ASSERT_TRUE_MESSAGE(lsn1 >= DURABLE_FLOOR,
|
|
"the floor must never drop below the durable floor (Req 3.2)");
|
|
|
|
ha_fini(&c.ha);
|
|
printf("test_heartbeat_raises_vote_floor_to_max: PASS\n");
|
|
}
|
|
|
|
/* ── (2) a candidate BELOW the raised floor is REJECTED ──────────────────── */
|
|
|
|
static void test_candidate_below_raised_floor_rejected(void)
|
|
{
|
|
cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
ha_config_t cfg = vff_cfg();
|
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&c.ha, &cfg, 0));
|
|
vff_open_role_gate(&c.ha);
|
|
|
|
atomic_store_explicit(&c.ha.last_known_committed_lsn, INITIAL_LEARNED,
|
|
memory_order_release);
|
|
/* Heartbeat raises the learned floor to 9 (> durable 5). */
|
|
ha_observe_heartbeat_committed(&c, RAISED_LSN, 0xAB);
|
|
|
|
/* Candidate's last-log LSN (7) is ABOVE the durable floor (5) and ABOVE the
|
|
* stale initial learned (3) but BELOW the raised floor (9). WITHOUT the
|
|
* heartbeat raising the floor this candidate would have been fresh enough;
|
|
* because the floor now consumes the raised learned value, it is DENIED.
|
|
* config_tag axis is held equal (0) so only the LSN axis decides. */
|
|
int granted = handle_request_vote(&c.ha,
|
|
1ULL, /* term */
|
|
"ctrl-laggard",
|
|
0ULL, /* cand config_tag == floor tag */
|
|
7ULL, /* cand last_lsn: 5 < 7 < 9 */
|
|
0ULL, /* my local config_tag */
|
|
DURABLE_FLOOR, /* my local (durable) lsn = 5 */
|
|
0);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, granted,
|
|
"a candidate (lsn=7) below the heartbeat-raised floor (9) must be DENIED "
|
|
"— the raised last_known_committed_lsn strengthened the gate (Req 3.2)");
|
|
|
|
ha_fini(&c.ha);
|
|
printf("test_candidate_below_raised_floor_rejected: PASS\n");
|
|
}
|
|
|
|
/* ── (3) a candidate AT or ABOVE the raised floor is ACCEPTED ────────────── */
|
|
|
|
static void test_candidate_at_or_above_raised_floor_accepted(void)
|
|
{
|
|
/* Candidate AT the raised floor (lsn == 9) → GRANT. */
|
|
cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
ha_config_t cfg = vff_cfg();
|
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&c.ha, &cfg, 0));
|
|
vff_open_role_gate(&c.ha);
|
|
|
|
atomic_store_explicit(&c.ha.last_known_committed_lsn, INITIAL_LEARNED,
|
|
memory_order_release);
|
|
ha_observe_heartbeat_committed(&c, RAISED_LSN, 0xAB);
|
|
|
|
int granted_at = handle_request_vote(&c.ha,
|
|
1ULL,
|
|
"ctrl-fresh",
|
|
0ULL,
|
|
RAISED_LSN, /* cand lsn == floor (9) */
|
|
0ULL,
|
|
DURABLE_FLOOR,
|
|
0);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, granted_at,
|
|
"a candidate AT the heartbeat-raised floor (lsn=9) must be GRANTED — "
|
|
"the floor is satisfiable, no liveness wedge (Req 3.2)");
|
|
ha_fini(&c.ha);
|
|
|
|
/* Candidate ABOVE the raised floor (lsn == 12) → GRANT. */
|
|
cluster_t c2;
|
|
memset(&c2, 0, sizeof(c2));
|
|
ha_config_t cfg2 = vff_cfg();
|
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&c2.ha, &cfg2, 0));
|
|
vff_open_role_gate(&c2.ha);
|
|
|
|
atomic_store_explicit(&c2.ha.last_known_committed_lsn, INITIAL_LEARNED,
|
|
memory_order_release);
|
|
ha_observe_heartbeat_committed(&c2, RAISED_LSN, 0xAB);
|
|
|
|
int granted_above = handle_request_vote(&c2.ha,
|
|
1ULL,
|
|
"ctrl-fresher",
|
|
0ULL,
|
|
12ULL, /* cand lsn > floor (9) */
|
|
0ULL,
|
|
DURABLE_FLOOR,
|
|
0);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, granted_above,
|
|
"a candidate ABOVE the heartbeat-raised floor (lsn=12) must be GRANTED");
|
|
|
|
ha_fini(&c2.ha);
|
|
printf("test_candidate_at_or_above_raised_floor_accepted: PASS\n");
|
|
}
|
|
|
|
/* ── (4) a non-advancing heartbeat NEVER lowers the floor ────────────────── */
|
|
|
|
static void test_lower_heartbeat_never_lowers_floor(void)
|
|
{
|
|
/* Guards the "never weaken" half of Req 3.2: once the floor is raised to 9,
|
|
* a later heartbeat carrying a LOWER committed (4) must NOT pull the floor
|
|
* back down — the candidate that was denied (7 < 9) stays denied. */
|
|
cluster_t c;
|
|
memset(&c, 0, sizeof(c));
|
|
ha_config_t cfg = vff_cfg();
|
|
TEST_ASSERT_EQUAL_INT(0, ha_init(&c.ha, &cfg, 0));
|
|
vff_open_role_gate(&c.ha);
|
|
|
|
atomic_store_explicit(&c.ha.last_known_committed_lsn, INITIAL_LEARNED,
|
|
memory_order_release);
|
|
ha_observe_heartbeat_committed(&c, RAISED_LSN, 0xAB); /* floor → 9 */
|
|
|
|
/* A lagging/out-of-order heartbeat carrying committed=4 (< current 9). */
|
|
ha_observe_heartbeat_committed(&c, 4ULL, 0xCD);
|
|
|
|
uint64_t tag = 0, lsn = 0;
|
|
ha_vote_self_freshness(&c.ha, 0ULL, DURABLE_FLOOR, &tag, &lsn);
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(RAISED_LSN, lsn,
|
|
"a lower/out-of-order heartbeat must NOT lower the floor below 9 (Req 3.2)");
|
|
|
|
/* The below-floor candidate (7) is still DENIED — the floor held. */
|
|
int granted = handle_request_vote(&c.ha,
|
|
1ULL,
|
|
"ctrl-laggard",
|
|
0ULL,
|
|
7ULL,
|
|
0ULL,
|
|
DURABLE_FLOOR,
|
|
0);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, granted,
|
|
"after a lower heartbeat the raised floor must still DENY the lagging "
|
|
"candidate (the floor was never weakened) (Req 3.2)");
|
|
|
|
ha_fini(&c.ha);
|
|
printf("test_lower_heartbeat_never_lowers_floor: PASS\n");
|
|
}
|
|
|
|
/* ── suite entry point ───────────────────────────────────────────── */
|
|
|
|
void test_ha_vote_floor_heartbeat(void)
|
|
{
|
|
UnitySetTestFile(__FILE__);
|
|
RUN_TEST(test_heartbeat_raises_vote_floor_to_max);
|
|
RUN_TEST(test_candidate_below_raised_floor_rejected);
|
|
RUN_TEST(test_candidate_at_or_above_raised_floor_accepted);
|
|
RUN_TEST(test_lower_heartbeat_never_lowers_floor);
|
|
}
|