myxce/src/controller/testsuite/test_ha_vote_floor_heartbeat.c
Rongsong Shen a81f2a249c fix(controller-tests): make Unity test-file attribution and counters consistent suite-wide
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.
2026-07-17 14:49:12 +00:00

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