forked from lei.chen/myxce
test(ha-config-bootstrap): add failing dispatch gate tests (task 3.1)
RED-phase tests for ha_dispatch_gate() and FSM init state: - CTRL_INIT + non-election ops -> XCE_ERR_NOT_READY - CTRL_INIT/ELECTING + OP_requestVote/OP_probeConfigTag -> 0 (allowed) - CTRL_STANDBY + primary-only ops -> XCE_ERR_STANDBY - CTRL_PRIMARY + any op -> 0 - heartbeat_seen semaphore usable after ha_init() ha_dispatch_gate, XCE_ERR_NOT_READY, XCE_ERR_STANDBY are all undefined -> linker errors confirm RED phase
This commit is contained in:
parent
488fd1bf49
commit
0a881804a0
2 changed files with 234 additions and 9 deletions
|
|
@ -1,18 +1,26 @@
|
|||
/* test_ha_startup.c — Unity RED-phase tests for startup validation
|
||||
/* test_ha_startup.c — Unity RED-phase tests for startup validation and
|
||||
* dispatch gate / FSM init state.
|
||||
*
|
||||
* Tests verify: even-N peer count rejection, cert pre-flight -ENOENT with path
|
||||
* in error message, --vcpus 0 rejection, and vcpus key in config logs WARN
|
||||
* (via caller-supplied warn_buf) without failing.
|
||||
* Tests verify:
|
||||
* Req 4.1-4.4: even-N peer count rejection, cert pre-flight -ENOENT with
|
||||
* path in error message, --vcpus 0 rejection, and vcpus key in
|
||||
* config logs WARN (via caller-supplied warn_buf) without failing.
|
||||
* Req 1.3-1.4: dispatch gate rejects non-vote/non-probe ops in CTRL_INIT
|
||||
* and CTRL_ELECTING states with XCE_ERR_NOT_READY; returns
|
||||
* XCE_ERR_STANDBY for primary-only ops in CTRL_STANDBY state;
|
||||
* Req 1.x: heartbeat_seen semaphore is initialized in ha_init().
|
||||
*
|
||||
* Requirements: 4.1, 4.2, 4.3, 4.4
|
||||
* Functions under test: validate_peer_count(), validate_cert_files(),
|
||||
* validate_vcpus(), validate_startup_config()
|
||||
* These functions do NOT exist yet — this file establishes the contract
|
||||
* for tasks 2.2 and 2.3.
|
||||
* validate_vcpus(), validate_startup_config(),
|
||||
* ha_dispatch_gate()
|
||||
* These functions do NOT all exist yet — this file establishes contracts for
|
||||
* tasks 2.2/2.3 (validation) and 3.3 (dispatch gate).
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <semaphore.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
|
@ -22,6 +30,9 @@
|
|||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "controller.capnp.h"
|
||||
#include "ha.h"
|
||||
|
||||
/* Functions under test — declared here; implementations come in tasks 2.2/2.3 */
|
||||
int validate_peer_count(int n_peers);
|
||||
int validate_cert_files(const char *cert_path, const char *key_path,
|
||||
|
|
@ -30,6 +41,40 @@ int validate_vcpus(int vcpus);
|
|||
int validate_startup_config(const char *config_path,
|
||||
char *warn_buf, size_t warn_len);
|
||||
|
||||
/* ── Dispatch gate (task 3.3) — does NOT exist yet; linker failure expected ──
|
||||
*
|
||||
* ha_dispatch_gate: role-based operation filter for the vtaskmgr dispatch loop.
|
||||
*
|
||||
* Parameters:
|
||||
* ha - pointer to the controller's ha_state_t (role is read atomically)
|
||||
* opcode - the OPCode integer value from the incoming request
|
||||
*
|
||||
* Returns:
|
||||
* 0 — operation is permitted for the current role
|
||||
* -XCE_ERR_NOT_READY — role is CTRL_INIT or CTRL_ELECTING and opcode is not
|
||||
* OP_requestVote or OP_probeConfigTag
|
||||
* -XCE_ERR_STANDBY — role is CTRL_STANDBY and opcode is a primary-only op
|
||||
*
|
||||
* Design ref: design.md "Dispatch gate" section (req 1.3, 1.4).
|
||||
* This declaration exists to make the RED-phase tests compile and fail to link.
|
||||
*/
|
||||
int ha_dispatch_gate(ha_state_t *ha, int opcode);
|
||||
|
||||
/* ── Error codes (task 3.3) — do NOT exist in errnox.h yet ──────────────────
|
||||
*
|
||||
* XCE_ERR_NOT_READY: returned when controller is in CTRL_INIT or CTRL_ELECTING
|
||||
* state and receives an opcode that is not allowed during
|
||||
* startup (req 1.3).
|
||||
* XCE_ERR_STANDBY: returned when controller is in CTRL_STANDBY state and
|
||||
* receives a primary-only operation (req 1.4).
|
||||
*
|
||||
* These are declared as extern int to force a linker error when the
|
||||
* corresponding symbols are not yet defined in any translation unit.
|
||||
* Tasks 3.3 implementation will add them to errnox.h and provide definitions.
|
||||
*/
|
||||
extern int XCE_ERR_NOT_READY;
|
||||
extern int XCE_ERR_STANDBY;
|
||||
|
||||
/* ── helpers ──────────────────────────────────────────────────────────── */
|
||||
|
||||
static char g_tmpdir[256];
|
||||
|
|
@ -218,6 +263,171 @@ static void test_config_without_vcpus_key_has_no_warning(void)
|
|||
printf("test_config_without_vcpus_key_has_no_warning: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Req 1.3: dispatch gate — CTRL_INIT blocks non-election opcodes ──────── */
|
||||
|
||||
/* Shared ha_config_t for dispatch gate tests — minimal valid config */
|
||||
static ha_config_t g_dispatch_cfg = {
|
||||
.heartbeat_interval_ms = 3000,
|
||||
.heartbeat_timeout_ms = 10000,
|
||||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
};
|
||||
|
||||
static void test_dispatch_gate_init_blocks_non_election_ops(void)
|
||||
{
|
||||
/* Req 1.3: When role is CTRL_INIT, any opcode that is not OP_requestVote
|
||||
* or OP_probeConfigTag must receive XCE_ERR_NOT_READY.
|
||||
*
|
||||
* OPCode_jobsub (11) is a representative primary-only opcode. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_INIT, memory_order_release);
|
||||
|
||||
int rc = ha_dispatch_gate(&ha, OPCode_jobsub);
|
||||
TEST_ASSERT_EQUAL_INT(-XCE_ERR_NOT_READY, rc);
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_init_blocks_non_election_ops: PASS\n");
|
||||
}
|
||||
|
||||
static void test_dispatch_gate_electing_blocks_non_election_ops(void)
|
||||
{
|
||||
/* Req 1.3: Same gate applies when role is CTRL_ELECTING.
|
||||
* OPCode_get (5) is another representative non-election opcode. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release);
|
||||
|
||||
int rc = ha_dispatch_gate(&ha, OPCode_get);
|
||||
TEST_ASSERT_EQUAL_INT(-XCE_ERR_NOT_READY, rc);
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_electing_blocks_non_election_ops: PASS\n");
|
||||
}
|
||||
|
||||
static void test_dispatch_gate_init_allows_request_vote(void)
|
||||
{
|
||||
/* Req 1.3: OP_requestVote must pass through in CTRL_INIT state.
|
||||
* Returns 0 (permitted). */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_INIT, memory_order_release);
|
||||
|
||||
int rc = ha_dispatch_gate(&ha, OPCode_requestVote);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_init_allows_request_vote: PASS\n");
|
||||
}
|
||||
|
||||
static void test_dispatch_gate_init_allows_probe_config_tag(void)
|
||||
{
|
||||
/* Req 1.3: OP_probeConfigTag must pass through in CTRL_INIT state.
|
||||
* Returns 0 (permitted). */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_INIT, memory_order_release);
|
||||
|
||||
int rc = ha_dispatch_gate(&ha, OPCode_probeConfigTag);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_init_allows_probe_config_tag: PASS\n");
|
||||
}
|
||||
|
||||
static void test_dispatch_gate_electing_allows_request_vote(void)
|
||||
{
|
||||
/* Req 1.3: OP_requestVote must pass through in CTRL_ELECTING state. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release);
|
||||
|
||||
int rc = ha_dispatch_gate(&ha, OPCode_requestVote);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_electing_allows_request_vote: PASS\n");
|
||||
}
|
||||
|
||||
static void test_dispatch_gate_electing_allows_probe_config_tag(void)
|
||||
{
|
||||
/* Req 1.3: OP_probeConfigTag must pass through in CTRL_ELECTING state. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release);
|
||||
|
||||
int rc = ha_dispatch_gate(&ha, OPCode_probeConfigTag);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_electing_allows_probe_config_tag: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Req 1.4: dispatch gate — CTRL_STANDBY blocks primary-only ops ───────── */
|
||||
|
||||
static void test_dispatch_gate_standby_blocks_primary_only_ops(void)
|
||||
{
|
||||
/* Req 1.4: When role is CTRL_STANDBY, primary-only operations must
|
||||
* receive XCE_ERR_STANDBY.
|
||||
*
|
||||
* OPCode_jobsub (11) is a representative primary-only opcode.
|
||||
* The implementation populates redirect_host/port when a primary is known
|
||||
* (tested separately in join tests); here we only verify the error code. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_STANDBY, memory_order_release);
|
||||
|
||||
int rc = ha_dispatch_gate(&ha, OPCode_jobsub);
|
||||
TEST_ASSERT_EQUAL_INT(-XCE_ERR_STANDBY, rc);
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_standby_blocks_primary_only_ops: PASS\n");
|
||||
}
|
||||
|
||||
static void test_dispatch_gate_primary_allows_all_ops(void)
|
||||
{
|
||||
/* Sanity: when role is CTRL_PRIMARY, all ops including primary-only ones
|
||||
* must be permitted (return 0). */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY, memory_order_release);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_dispatch_gate(&ha, OPCode_jobsub));
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_dispatch_gate(&ha, OPCode_get));
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_dispatch_gate(&ha, OPCode_requestVote));
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_primary_allows_all_ops: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Req 1.x: ha_init() initializes heartbeat_seen semaphore ────────────── */
|
||||
|
||||
static void test_ha_init_initializes_heartbeat_semaphore(void)
|
||||
{
|
||||
/* Req 1: ha_init() must initialize ha->heartbeat_seen so that
|
||||
* ha_wait_for_heartbeat_or_timeout() can safely call sem_timedwait on it.
|
||||
*
|
||||
* Verify the semaphore is initialized at value 0 (no heartbeat yet seen):
|
||||
* sem_trywait() on a zero-value semaphore must return -1 with errno=EAGAIN.
|
||||
* Also verify sem_post() + sem_wait() round-trip works (semaphore is live). */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_dispatch_cfg, 0);
|
||||
|
||||
/* sem_trywait on a freshly initialized semaphore (value=0) must fail with
|
||||
* EAGAIN — confirming it was initialized at 0, not garbage. */
|
||||
int rc = sem_trywait(&ha.heartbeat_seen);
|
||||
TEST_ASSERT_EQUAL_INT(-1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(EAGAIN, errno);
|
||||
|
||||
/* Round-trip: post then wait must succeed immediately */
|
||||
TEST_ASSERT_EQUAL_INT(0, sem_post(&ha.heartbeat_seen));
|
||||
TEST_ASSERT_EQUAL_INT(0, sem_wait(&ha.heartbeat_seen));
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_ha_init_initializes_heartbeat_semaphore: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test suite entry point ───────────────────────────────────────────── */
|
||||
|
||||
void test_ha_startup(void)
|
||||
|
|
@ -241,4 +451,19 @@ void test_ha_startup(void)
|
|||
/* Req 4.4 — vcpus key WARN via warn_buf; no failure */
|
||||
RUN_TEST(test_vcpus_key_in_config_warns_but_does_not_fail);
|
||||
RUN_TEST(test_config_without_vcpus_key_has_no_warning);
|
||||
|
||||
/* Req 1.3 — dispatch gate: CTRL_INIT and CTRL_ELECTING block non-election ops */
|
||||
RUN_TEST(test_dispatch_gate_init_blocks_non_election_ops);
|
||||
RUN_TEST(test_dispatch_gate_electing_blocks_non_election_ops);
|
||||
RUN_TEST(test_dispatch_gate_init_allows_request_vote);
|
||||
RUN_TEST(test_dispatch_gate_init_allows_probe_config_tag);
|
||||
RUN_TEST(test_dispatch_gate_electing_allows_request_vote);
|
||||
RUN_TEST(test_dispatch_gate_electing_allows_probe_config_tag);
|
||||
|
||||
/* Req 1.4 — dispatch gate: CTRL_STANDBY blocks primary-only ops */
|
||||
RUN_TEST(test_dispatch_gate_standby_blocks_primary_only_ops);
|
||||
RUN_TEST(test_dispatch_gate_primary_allows_all_ops);
|
||||
|
||||
/* Req 1.x — ha_init() initializes heartbeat_seen semaphore */
|
||||
RUN_TEST(test_ha_init_initializes_heartbeat_semaphore);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue