forked from lei.chen/myxce
test(ha-single-leader-role): RED tests for WAL_NOOP entry type (task 1.1)
This commit is contained in:
parent
678b7f89d5
commit
6df1283f1a
4 changed files with 444 additions and 1 deletions
|
|
@ -89,6 +89,7 @@ controller_unit_test_src = [
|
|||
'testsuite/test_e2e_gc.c',
|
||||
'testsuite/test_e2e_capnp.c',
|
||||
'testsuite/test_ha_role.c',
|
||||
'testsuite/test_ha_role_lifecycle.c',
|
||||
'testsuite/test_ha_cluster.c',
|
||||
'testsuite/test_ha_redirect.c',
|
||||
'testsuite/test_p1_integration.c',
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ extern void test_e2e_hooks(void);
|
|||
extern void test_e2e_gc(void);
|
||||
extern void test_e2e_capnp(void);
|
||||
extern void test_ha_role(void);
|
||||
extern void test_ha_role_lifecycle(void);
|
||||
extern void test_ha_cluster(void);
|
||||
extern void test_ha_redirect(void);
|
||||
extern void test_p1_integration(void);
|
||||
|
|
@ -142,6 +143,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_e2e_gc);
|
||||
TEST(test_e2e_capnp);
|
||||
TEST(test_ha_role);
|
||||
TEST(test_ha_role_lifecycle);
|
||||
TEST(test_ha_cluster);
|
||||
TEST(test_ha_redirect);
|
||||
TEST(test_p1_integration);
|
||||
|
|
|
|||
440
src/controller/testsuite/test_ha_role_lifecycle.c
Normal file
440
src/controller/testsuite/test_ha_role_lifecycle.c
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
#define _GNU_SOURCE
|
||||
/*
|
||||
* test_ha_role_lifecycle.c — ha-single-leader-role lifecycle contract.
|
||||
*
|
||||
* Task 1.1 (TDD RED): the WAL_NOOP no-op entry type. Later tasks (2.1, 3.1)
|
||||
* add direct-promotion and own-term-gate lifecycle cases to this same file.
|
||||
*
|
||||
* RED-PHASE APPROACH (b): WAL_NOOP does not exist yet (lands in task 1.2),
|
||||
* so referencing the symbol would fail to COMPILE. Instead these cases use
|
||||
* the literal value 40 (the value WAL_NOOP will take in wal_entry_type, per
|
||||
* design.md §"WAL_NOOP Entry") so the file COMPILES and LINKS into the
|
||||
* controller test binary; the assertions then fail at RUNTIME because the
|
||||
* type-40 behavior (explicit replay no-op, RETAIN classification) is not
|
||||
* implemented. This satisfies task 1.1's "build succeeds + new cases FAIL".
|
||||
*
|
||||
* The classification case (wal_classify(40,...)) cannot run inline in the
|
||||
* current tree: the totality guard does `assert(0)` for any unclassified
|
||||
* type, which would ABORT the whole test binary. It is isolated via the
|
||||
* fork() abort-test pattern (cerebrum: child silences stderr + _exit, parent
|
||||
* inspects exit status) so the RED state is a deterministic, captured
|
||||
* assertion in the parent rather than a binary crash.
|
||||
*
|
||||
* AAA structure throughout.
|
||||
*/
|
||||
#include <assert.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <inttypes.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "persist.h"
|
||||
|
||||
/* The value WAL_NOOP will take in wal_entry_type (task 1.2). Using the
|
||||
* literal keeps THIS file compiling before the enum value exists; task 1.2
|
||||
* replaces these with the WAL_NOOP symbol once it lands. */
|
||||
#define WAL_NOOP_VALUE 40u
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test-local fixtures */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Spin up a real on-disk persist_t in a fresh tmpdir (no cluster wired —
|
||||
* apply_wal_entry's entity-restore path is skipped when p->cluster == NULL,
|
||||
* which is exactly the no-effect guarantee we assert). */
|
||||
static persist_t* make_lifecycle_persist(const char* suffix)
|
||||
{
|
||||
persist_t* p = calloc(1, sizeof(persist_t));
|
||||
if (!p)
|
||||
return NULL;
|
||||
|
||||
persist_config_t cfg = persist_config_defaults();
|
||||
cfg.wal_sync_ms = 5000; /* slow background fsync — no races in tests */
|
||||
|
||||
char tmpdir[256];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/xce_noop_%s_XXXXXX", suffix);
|
||||
if (!mkdtemp(tmpdir)) {
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
persist_set_workdir(p, tmpdir, "ctrl-test");
|
||||
if (persist_init(p, &cfg, NULL) != 0) {
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
if (persist_start(p) != 0) {
|
||||
persist_fini(p);
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
static void teardown_lifecycle_persist(persist_t* p)
|
||||
{
|
||||
if (!p)
|
||||
return;
|
||||
persist_stop(p);
|
||||
persist_fini(p);
|
||||
free(p);
|
||||
/* tmpdir left on disk — /tmp is ephemeral */
|
||||
}
|
||||
|
||||
/* Read the most-recently-appended WAL entry header back from the active
|
||||
* segment file. Returns 0 and fills *out on success, negative on error.
|
||||
* Reads the header at (seg_offset - sizeof(hdr)) — the last entry written. */
|
||||
static int read_last_wal_hdr(persist_t* p, wal_entry_hdr_t* out)
|
||||
{
|
||||
if (p->seg_offset < sizeof(wal_entry_hdr_t))
|
||||
return -ENOENT;
|
||||
|
||||
int fd = open(p->seg_path, O_RDONLY);
|
||||
if (fd < 0)
|
||||
return -errno;
|
||||
|
||||
off_t at = (off_t)(p->seg_offset - sizeof(wal_entry_hdr_t));
|
||||
ssize_t r = pread(fd, out, sizeof(*out), at);
|
||||
close(fd);
|
||||
|
||||
if (r != (ssize_t)sizeof(*out))
|
||||
return -EIO;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Evaluate wal_classify(WAL_NOOP, 0) in a forked child and report whether it
|
||||
* cleanly returned WAL_MODE_RETAIN. Returns 1 iff the child exited 0 (no
|
||||
* abort, RETAIN), 0 otherwise (abort/SIGABRT today, since WAL_NOOP is
|
||||
* unclassified → the totality fail-fast `assert(0)` fires). Isolating the
|
||||
* call protects the test binary from the abort (cerebrum fork pattern). */
|
||||
static int noop_classifies_retain(void)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0)
|
||||
return -1;
|
||||
if (pid == 0) {
|
||||
setenv("ASAN_OPTIONS", "detect_leaks=0", 1);
|
||||
int devnull = open("/dev/null", O_WRONLY);
|
||||
if (devnull >= 0) {
|
||||
dup2(devnull, STDERR_FILENO);
|
||||
dup2(devnull, STDOUT_FILENO);
|
||||
}
|
||||
wal_commit_mode_t mode = wal_classify(WAL_NOOP_VALUE, 0);
|
||||
_exit(mode == WAL_MODE_RETAIN ? 0 : 1);
|
||||
}
|
||||
int status = 0;
|
||||
if (waitpid(pid, &status, 0) != pid)
|
||||
return -1;
|
||||
return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Task 1.1 — WAL_NOOP no-op entry (Req 2.1) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Case 1: persist_wal_async(p, WAL_NOOP, NULL, 0) appends a header-only
|
||||
* entry (payload_len == 0) and broadcasts it through the production path.
|
||||
*
|
||||
* Observables:
|
||||
* - return 0 (append succeeded);
|
||||
* - the on-disk last entry has type == WAL_NOOP and payload_len == 0;
|
||||
* - last_replicated_crc advanced to this entry's CRC (the same field
|
||||
* persist_wal_async sets immediately alongside repl_broadcast,
|
||||
* persist.c:359 — proxy for "the entry was handed to the replicator").
|
||||
*
|
||||
* RED today: persist_wal_async will write a type-40 entry, but the spec
|
||||
* requires WAL_NOOP to be appended with payload_len 0 ONLY via this path;
|
||||
* the binding of the value-40 type to the WAL_NOOP no-op contract does not
|
||||
* exist until task 1.2 wires the explicit replay/classify handling. The
|
||||
* payload_len/type-round-trip assertion below pins the entry shape that 1.2
|
||||
* must produce; it is asserted now so 1.2 has a concrete target. */
|
||||
static void test_noop_append_is_header_only_and_broadcast(void)
|
||||
{
|
||||
/* Arrange */
|
||||
persist_t* p = make_lifecycle_persist("append");
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
uint64_t lsn_before = p->current_lsn;
|
||||
uint32_t crc_before = p->last_replicated_crc;
|
||||
|
||||
/* Act: append a no-op (header only, NULL payload, len 0) */
|
||||
int rc = persist_wal_async(p, WAL_NOOP_VALUE, NULL, 0);
|
||||
|
||||
/* Assert: append succeeded and consumed exactly one LSN */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
|
||||
"WAL_NOOP append must succeed via persist_wal_async");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(lsn_before + 1, p->current_lsn,
|
||||
"WAL_NOOP append must advance current_lsn by exactly one");
|
||||
|
||||
/* Assert: the on-disk entry is header-only of the WAL_NOOP type */
|
||||
wal_entry_hdr_t hdr;
|
||||
memset(&hdr, 0, sizeof(hdr));
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, read_last_wal_hdr(p, &hdr),
|
||||
"must be able to read the appended WAL_NOOP header back from disk");
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(WAL_NOOP_VALUE, hdr.type,
|
||||
"appended entry must be of type WAL_NOOP");
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0u, hdr.payload_len,
|
||||
"WAL_NOOP must be header-only (payload_len == 0)");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(lsn_before, hdr.lsn,
|
||||
"WAL_NOOP entry LSN must be the pre-append current_lsn");
|
||||
|
||||
/* Assert: the entry was broadcast (last_replicated_crc moved off its
|
||||
* pre-append value to this entry's crc — the replicate-path observable) */
|
||||
TEST_ASSERT_NOT_EQUAL_MESSAGE(crc_before, p->last_replicated_crc,
|
||||
"WAL_NOOP append must broadcast (last_replicated_crc advances)");
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(hdr.crc, p->last_replicated_crc,
|
||||
"broadcast CRC must match the appended WAL_NOOP entry CRC");
|
||||
|
||||
/* Assert: WAL_NOOP is a CLASSIFIED type. An entry appended through the
|
||||
* write path must be classifiable (the design lands the enum value and
|
||||
* its wal_classify row in the SAME change, task 1.2). Today WAL_NOOP is
|
||||
* unclassified, so this is RED: the classify call aborts under the
|
||||
* totality guard (observed via the forked-child helper). */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, noop_classifies_retain(),
|
||||
"an appended WAL_NOOP must be a classified type (wal_classify → RETAIN)");
|
||||
|
||||
teardown_lifecycle_persist(p);
|
||||
printf("test_noop_append_is_header_only_and_broadcast: PASS\n");
|
||||
}
|
||||
|
||||
/* Case 2: apply_wal_entry on a WAL_NOOP advances current_lsn past the
|
||||
* entry's LSN and touches NO entity state.
|
||||
*
|
||||
* With p->cluster == NULL the entity-restore switch is skipped entirely, so
|
||||
* "no entity state touched" is structurally guaranteed; the contract this
|
||||
* pins is the LSN advance via the common path (persist.c:719-723) for a
|
||||
* WAL_NOOP header-only entry.
|
||||
*
|
||||
* RED today: the entry applies (type 40 < WAL_ENTRY_TYPE_MAX, so the common
|
||||
* path advances LSN), BUT task 1.2 must add an EXPLICIT `case WAL_NOOP:
|
||||
* break;` so the no-op is a recognised type rather than falling through the
|
||||
* `default:` arm. We additionally require the apply to return 0 AND not
|
||||
* emit the unknown-type WARN path — pinned here as the apply contract 1.2
|
||||
* realises. The assertion that the entry is recognised (return 0 with a
|
||||
* header-only payload and exact LSN advance) is the RED carrier. */
|
||||
static void test_noop_apply_advances_lsn_no_entity_effect(void)
|
||||
{
|
||||
/* Arrange: a persist with NO cluster — entity restore is skipped, so any
|
||||
* observable effect would have to come from the WAL_NOOP arm itself */
|
||||
persist_t* p = make_lifecycle_persist("apply");
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
TEST_ASSERT_NULL_MESSAGE(p->cluster,
|
||||
"fixture must have no cluster wired (no-effect guarantee depends on it)");
|
||||
|
||||
/* Build a header-only WAL_NOOP entry at a known LSN beyond current */
|
||||
uint64_t noop_lsn = p->current_lsn + 5;
|
||||
wal_entry_hdr_t hdr;
|
||||
memset(&hdr, 0, sizeof(hdr));
|
||||
hdr.term = p->term;
|
||||
hdr.lsn = noop_lsn;
|
||||
hdr.timestamp = 0;
|
||||
hdr.type = WAL_NOOP_VALUE;
|
||||
hdr.payload_len = 0;
|
||||
hdr.crc = 0;
|
||||
hdr.prev_crc = 0;
|
||||
|
||||
/* Act: apply the no-op with a NULL payload (header-only) */
|
||||
int rc = apply_wal_entry(p, &hdr, NULL);
|
||||
|
||||
/* Assert: clean apply, LSN advanced past the entry, nothing else */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
|
||||
"applying a WAL_NOOP must succeed");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(noop_lsn + 1, p->current_lsn,
|
||||
"applying a WAL_NOOP must advance current_lsn to entry LSN + 1");
|
||||
/* No cluster → no entity state to inspect; the absence of effect is the
|
||||
* point. Re-applying must be idempotent on LSN (already advanced). */
|
||||
int rc2 = apply_wal_entry(p, &hdr, NULL);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc2,
|
||||
"re-applying a WAL_NOOP must remain a clean no-op");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(noop_lsn + 1, p->current_lsn,
|
||||
"re-applying a lower/equal LSN WAL_NOOP must not regress current_lsn");
|
||||
|
||||
/* Assert: WAL_NOOP is a recognised, classified entry type — not an
|
||||
* unknown type the apply/classify paths only tolerate by fallthrough.
|
||||
* RED today: wal_classify(WAL_NOOP) aborts (unclassified) per the
|
||||
* totality guard; task 1.2 adds the explicit case + RETAIN row. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, noop_classifies_retain(),
|
||||
"an applied WAL_NOOP must be a classified type (wal_classify → RETAIN)");
|
||||
|
||||
teardown_lifecycle_persist(p);
|
||||
printf("test_noop_apply_advances_lsn_no_entity_effect: PASS\n");
|
||||
}
|
||||
|
||||
/* Case 3: boot replay over a segment containing a WAL_NOOP replays cleanly
|
||||
* with the same no-effect guarantee, and the WAL_NOOP is a RECOGNISED type.
|
||||
*
|
||||
* Fixture shape (deliberate): append ONE ordinary, already-classified WAL
|
||||
* entry (WAL_JOB_START, header-only) FIRST so it takes lsn 0, THEN append the
|
||||
* WAL_NOOP so it lands at lsn 1. This matters because replay_wal skips any
|
||||
* entry with `hdr.lsn <= from_lsn` (persist.c:907) and a fresh persist recovers
|
||||
* with from_lsn == checkpoint_lsn == 0. A WAL_NOOP written as the FIRST entry
|
||||
* would sit at lsn 0 and be skipped as snapshot-covered — an lsn-0/checkpoint-
|
||||
* boundary FIXTURE ARTIFACT unrelated to the WAL_NOOP contract (it would fail
|
||||
* for ANY type at lsn 0 and would NOT be fixed by task 1.2). By seating the
|
||||
* noop at lsn > 0 we pin the actual boot-replay contract instead.
|
||||
*
|
||||
* The leading WAL_JOB_START is side-effect-free here: apply_wal_entry returns
|
||||
* early at the `if (!p->cluster)` guard (persist.c:727-728) before the entity-
|
||||
* restore switch, so neither the ordinary entry nor the noop touches entity
|
||||
* state during replay — preserving the no-effect guarantee.
|
||||
*
|
||||
* Observables that PIN THE CONTRACT (these hold both today and after task 1.2,
|
||||
* because WAL_NOOP_VALUE 40 <= WAL_ENTRY_TYPE_MAX 100 already routes through
|
||||
* apply_wal_entry, which advances current_lsn):
|
||||
* - persist_recover returns 0 (replay clean);
|
||||
* - current_lsn advances STRICTLY past the noop's lsn (>= noop_lsn + 1);
|
||||
* - no entity side effects (NULL cluster — structurally guaranteed).
|
||||
*
|
||||
* The RED PIN is the SHARED classify carrier noop_classifies_retain(): boot
|
||||
* replay of a WAL_NOOP is only correct when the type is a recognised, RETAIN-
|
||||
* classified entry — not an unclassified type tolerated by routing alone.
|
||||
* Today wal_classify(40) aborts under the totality guard (carrier returns 0);
|
||||
* task 1.2 lands the enum + `case WAL_NOOP: break;` apply arm + the RETAIN
|
||||
* classify row TOGETHER, flipping the carrier to 1 and this case to green. */
|
||||
static void test_noop_boot_replay_clean_no_effect(void)
|
||||
{
|
||||
char tmpdir[256];
|
||||
uint64_t noop_lsn;
|
||||
|
||||
/* Arrange: write [WAL_JOB_START (lsn 0), WAL_NOOP (lsn 1)] into a fresh
|
||||
* persist so the noop sits at lsn > 0 (escaping the lsn<=from_lsn skip),
|
||||
* capture the noop's LSN, flush, then tear down (keeping the on-disk WAL). */
|
||||
{
|
||||
persist_t* p = calloc(1, sizeof(persist_t));
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
persist_config_t cfg = persist_config_defaults();
|
||||
cfg.wal_sync_ms = 5000;
|
||||
strcpy(tmpdir, "/tmp/xce_noop_replay_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir));
|
||||
persist_set_workdir(p, tmpdir, "ctrl-test");
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_init(p, &cfg, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_start(p));
|
||||
|
||||
/* Leading ORDINARY entry — already classified, header-only, harmless
|
||||
* on replay under a NULL cluster. Takes lsn 0 so the noop gets lsn>0. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
persist_wal_async(p, WAL_JOB_START, NULL, 0),
|
||||
"leading ordinary WAL entry must append for the replay fixture");
|
||||
|
||||
noop_lsn = p->current_lsn; /* the LSN the noop is about to take (> 0) */
|
||||
TEST_ASSERT_TRUE_MESSAGE(noop_lsn > 0,
|
||||
"noop must sit at lsn > 0 so replay does not skip it as "
|
||||
"snapshot-covered (hdr.lsn <= from_lsn guard, persist.c:907)");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
persist_wal_async(p, WAL_NOOP_VALUE, NULL, 0),
|
||||
"WAL_NOOP must append for the replay fixture");
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
|
||||
|
||||
persist_stop(p);
|
||||
persist_fini(p);
|
||||
free(p);
|
||||
}
|
||||
|
||||
/* Act: recover a FRESH persist over the same workdir (boot replay).
|
||||
* persist_init does NOT scan segments / set current_lsn — recovery is
|
||||
* explicit via persist_recover, so current_lsn starts at the calloc'd 0. */
|
||||
persist_t* p2 = calloc(1, sizeof(persist_t));
|
||||
TEST_ASSERT_NOT_NULL(p2);
|
||||
persist_config_t cfg2 = persist_config_defaults();
|
||||
cfg2.wal_sync_ms = 5000;
|
||||
persist_set_workdir(p2, tmpdir, "ctrl-test");
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_init(p2, &cfg2, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_start(p2));
|
||||
TEST_ASSERT_NULL_MESSAGE(p2->cluster,
|
||||
"replay fixture must have no cluster (no-effect guarantee)");
|
||||
|
||||
int rc = persist_recover(p2);
|
||||
|
||||
/* Assert: replay clean, LSN advanced STRICTLY past the no-op (contract). */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
|
||||
"boot replay over a WAL_NOOP must succeed");
|
||||
TEST_ASSERT_TRUE_MESSAGE(p2->current_lsn > noop_lsn,
|
||||
"boot replay over a WAL_NOOP must advance current_lsn past the no-op");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(noop_lsn + 1, p2->current_lsn,
|
||||
"boot replay must leave current_lsn at one past the WAL_NOOP's LSN");
|
||||
|
||||
persist_stop(p2);
|
||||
persist_fini(p2);
|
||||
free(p2);
|
||||
|
||||
/* Assert (RED PIN): the replayed WAL_NOOP must be a recognised, RETAIN-
|
||||
* classified type — not merely tolerated by apply/replay routing. Today
|
||||
* wal_classify(WAL_NOOP) aborts (unclassified) per the totality guard, so
|
||||
* the forked carrier returns 0 and this case is RED for the WAL_NOOP
|
||||
* CONTRACT reason. Task 1.2's enum + apply arm + RETAIN classify row flips
|
||||
* it to 1, turning this case green. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, noop_classifies_retain(),
|
||||
"a replayed WAL_NOOP must be a classified type (wal_classify → RETAIN)");
|
||||
|
||||
printf("test_noop_boot_replay_clean_no_effect: PASS\n");
|
||||
}
|
||||
|
||||
/* Case 4: wal_classify(WAL_NOOP, 0) == WAL_MODE_RETAIN.
|
||||
*
|
||||
* This keeps barrier-rollout's classification-totality test meaningful: the
|
||||
* enum value and its classification row must land together (task 1.2).
|
||||
*
|
||||
* RED today: WAL_NOOP (40) is unclassified, so wal_classify hits the
|
||||
* totality fail-fast `assert(0)` under XCE_TESTING and ABORTS. Calling it
|
||||
* inline would crash the whole test binary, so it is evaluated in a forked
|
||||
* child (cerebrum fork abort-test pattern). The contract is RETAIN; the
|
||||
* child computes wal_classify(40,0) and exits 0 ONLY if it returns RETAIN
|
||||
* without aborting. Today the child aborts (SIGABRT), so the parent's
|
||||
* "exited cleanly with RETAIN" assertion FAILS — the captured RED state.
|
||||
*
|
||||
* detect_leaks=0 set in the child so ASAN does not flag the abort path. */
|
||||
static void test_noop_classify_is_retain(void)
|
||||
{
|
||||
pid_t pid;
|
||||
int status = 0;
|
||||
|
||||
/* Act: classify WAL_NOOP in a child (it may abort in the current tree) */
|
||||
pid = fork();
|
||||
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork must succeed");
|
||||
if (pid == 0) {
|
||||
/* Child: silence the expected assert/abort noise; ASAN-friendly. */
|
||||
setenv("ASAN_OPTIONS", "detect_leaks=0", 1);
|
||||
int devnull = open("/dev/null", O_WRONLY);
|
||||
if (devnull >= 0) {
|
||||
dup2(devnull, STDERR_FILENO);
|
||||
dup2(devnull, STDOUT_FILENO);
|
||||
}
|
||||
wal_commit_mode_t mode = wal_classify(WAL_NOOP_VALUE, 0);
|
||||
/* Reaching here means no abort; encode the result in the exit code:
|
||||
* exit 0 ⇔ RETAIN (the contract), exit 1 ⇔ classified-but-wrong. */
|
||||
_exit(mode == WAL_MODE_RETAIN ? 0 : 1);
|
||||
}
|
||||
|
||||
/* Assert: the child must exit cleanly with RETAIN. Today it aborts
|
||||
* (SIGABRT) because WAL_NOOP is unclassified — this assertion is RED. */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(pid, waitpid(pid, &status, 0),
|
||||
"waitpid must reap the classifier child");
|
||||
TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status),
|
||||
"wal_classify(WAL_NOOP) must NOT abort — it must classify the type");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, WEXITSTATUS(status),
|
||||
"wal_classify(WAL_NOOP, 0) must return WAL_MODE_RETAIN");
|
||||
|
||||
printf("test_noop_classify_is_retain: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Entry point (registered in controller_test.c / meson.build) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void test_ha_role_lifecycle(void)
|
||||
{
|
||||
/* Task 1.1 — WAL_NOOP no-op entry (Req 2.1) */
|
||||
RUN_TEST(test_noop_append_is_header_only_and_broadcast);
|
||||
RUN_TEST(test_noop_apply_advances_lsn_no_entity_effect);
|
||||
RUN_TEST(test_noop_boot_replay_clean_no_effect);
|
||||
RUN_TEST(test_noop_classify_is_retain);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue