forked from lei.chen/myxce
bug-1401: xce_free_ctrlstatus(s) was defined as free_CtrlStatus(s), which only frees the fields inside xce_ctrlstatus_t. The real client reply path (decode_CtrlStatus_ptr) calloc's the struct itself, so every ctrlstatus call leaked sizeof(xce_ctrlstatus_t) (120 bytes). Existing unit tests never caught it because they built/freed the struct via the raw stack-decode + bare free_CtrlStatus() helpers, never exercising the actual client macro. Fixed by switching the macro to free_CtrlStatus_ptr(&(s)) and added a regression test that exercises the real heap-decode path; RED confirmed the exact 120-byte/1-alloc LeakSanitizer signature before the fix. bug-1400: the originally reported SEGV in cmd_jobsub on malformed positional input is stale — NULLSTR guards added 2025-11-05 already reject it cleanly. The real residual defect was submit.start_info leaking on all 4 early-return validation paths; fixed by routing them through a shared `goto cleanup` (matches the existing goto-clean idiom in queue_ctrl). bug-1396 (job-count anomaly after cold-restart-all, not yet root-caused): instrumented test-commit-durability.sh's assert_job_count() to dump the live joblist and ACKED_FILE ledger on any count mismatch instead of only the count, per the bug's own recorded next step. Verified: controller ASAN build + full controller_unit_test green; lib xcelib_test 285/285 green, 0 ASAN leaks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SDWVNQLKxqTVHh42cH7rV3
463 lines
18 KiB
C
463 lines
18 KiB
C
/* test_ctrlstatus.c — Unity tests for the controller-status (ctrlstatus)
|
|
* libxce client reply path (controller-status task 3.1).
|
|
*
|
|
* Proves the reply path that xcecli_ctrlstatus relies on:
|
|
* 1. A true ENCODE→DECODE round-trip of xce_ctrlstatus_t through the
|
|
* generated Cap'n Proto codec (encode_CtrlStatus/write_CtrlStatus →
|
|
* bytes → read_CtrlStatus/decode_CtrlStatus): every status field,
|
|
* including the peers list and the configChecksum hex Text, survives
|
|
* the wire (Req 1.1 — the client decodes every field a handler emits).
|
|
* 2. xcecli_ctrlstatus(NULL, ...) returns a typed negative rather than a
|
|
* partial/empty success (Req 7.3 — no reachable controller ⇒ typed
|
|
* failure).
|
|
*
|
|
* A full live round-trip (client → running controller → handler reply)
|
|
* needs a controller and is the E2E task; here we prove the encode/decode
|
|
* round-trip + the no-context typed failure only.
|
|
*/
|
|
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unity/unity.h>
|
|
|
|
#include "client.h"
|
|
#include "config.h" /* XCE_MAXHOSTNAME_LEN */
|
|
#include "request.h"
|
|
#include "xce/types.h"
|
|
#include "xcectx.h" /* xce_ctx_t, xce_handle_reply (bug-1077 regression) */
|
|
|
|
/* generated Cap'n Proto codec for CtrlStatus / CtrlPeer */
|
|
#include "controller.capnp.h"
|
|
|
|
/* ── encode→decode round-trip of xce_ctrlstatus_t ───────────────────────── */
|
|
|
|
static void test_ctrlstatus_roundtrip_all_fields(void)
|
|
{
|
|
/* Arrange: a fully-populated status with two configured peers */
|
|
xce_ctrlpeer_t p0 = {
|
|
.name = "ctrl-a", .host = "10.0.0.1", .port = 9991
|
|
};
|
|
xce_ctrlpeer_t p1 = {
|
|
.name = "ctrl-b", .host = "10.0.0.2", .port = 9992
|
|
};
|
|
xce_ctrlpeer_t* peerlist[2] = { &p0, &p1 };
|
|
|
|
xce_ctrlstatus_t in;
|
|
memset(&in, 0, sizeof(in));
|
|
in.clusterName = "lab-cluster";
|
|
in.primaryName = "ctrl-a";
|
|
in.responderName = "ctrl-b";
|
|
in.responderRole = 3; /* STANDBY */
|
|
in.term = 42;
|
|
in.configVersion = 0x1122334455667788ULL;
|
|
in.configChecksum = "1122334455667788aabbccddeeff0011"
|
|
"2233445566778899aabbccddeeff0011";
|
|
in.committedConfigVersion = 0x1122334455667788ULL;
|
|
in.committedLsn = 5000;
|
|
in.appliedLsn = 4999;
|
|
in.fenced = 1;
|
|
in.peers.count = 2;
|
|
in.peers.list = peerlist;
|
|
in.peerCount = 2;
|
|
|
|
/* Act: encode into a capnp segment, serialize to bytes, then decode
|
|
* a fresh struct back out — mirrors xce_encode/xce_decode. */
|
|
struct capn c;
|
|
struct capn_segment* cs;
|
|
struct CtrlStatus cap_in;
|
|
CtrlStatus_ptr sp;
|
|
|
|
capn_init_malloc(&c);
|
|
cs = capn_root(&c).seg;
|
|
|
|
memset(&cap_in, 0, sizeof(cap_in));
|
|
encode_CtrlStatus(cs, &cap_in, &in);
|
|
|
|
sp = new_CtrlStatus(cs);
|
|
write_CtrlStatus(&cap_in, sp);
|
|
capn_setp(capn_root(&c), 0, sp.p);
|
|
|
|
size_t sz = capn_size(&c);
|
|
uint8_t* buf = (uint8_t*)malloc(sz);
|
|
TEST_ASSERT_NOT_NULL(buf);
|
|
TEST_ASSERT_EQUAL_size_t(sz, capn_write_mem(&c, buf, sz, 0));
|
|
capn_free(&c);
|
|
|
|
/* decode side */
|
|
struct capn dc;
|
|
struct CtrlStatus cap_out;
|
|
CtrlStatus_ptr dp;
|
|
xce_ctrlstatus_t out;
|
|
|
|
capn_init_mem(&dc, buf, sz, 0);
|
|
dp.p = capn_getp(capn_root(&dc), 0, 1);
|
|
read_CtrlStatus(&cap_out, dp);
|
|
memset(&out, 0, sizeof(out));
|
|
decode_CtrlStatus(&out, &cap_out);
|
|
|
|
/* Assert: every scalar/text field survived */
|
|
TEST_ASSERT_NOT_NULL(out.clusterName);
|
|
TEST_ASSERT_EQUAL_STRING("lab-cluster", out.clusterName);
|
|
TEST_ASSERT_NOT_NULL(out.primaryName);
|
|
TEST_ASSERT_EQUAL_STRING("ctrl-a", out.primaryName);
|
|
TEST_ASSERT_NOT_NULL(out.responderName);
|
|
TEST_ASSERT_EQUAL_STRING("ctrl-b", out.responderName);
|
|
TEST_ASSERT_EQUAL_UINT8(3, out.responderRole);
|
|
TEST_ASSERT_EQUAL_UINT64(42, out.term);
|
|
TEST_ASSERT_EQUAL_UINT64(0x1122334455667788ULL, out.configVersion);
|
|
TEST_ASSERT_NOT_NULL(out.configChecksum);
|
|
TEST_ASSERT_EQUAL_STRING(in.configChecksum, out.configChecksum);
|
|
TEST_ASSERT_EQUAL_UINT64(0x1122334455667788ULL, out.committedConfigVersion);
|
|
TEST_ASSERT_EQUAL_UINT64(5000, out.committedLsn);
|
|
TEST_ASSERT_EQUAL_UINT64(4999, out.appliedLsn);
|
|
TEST_ASSERT_EQUAL_UINT32(1, out.fenced);
|
|
TEST_ASSERT_EQUAL_UINT32(2, out.peerCount);
|
|
|
|
/* Assert: the peers list survived with both entries */
|
|
TEST_ASSERT_EQUAL_INT(2, out.peers.count);
|
|
TEST_ASSERT_NOT_NULL(out.peers.list);
|
|
TEST_ASSERT_NOT_NULL(out.peers.list[0]);
|
|
TEST_ASSERT_EQUAL_STRING("ctrl-a", out.peers.list[0]->name);
|
|
TEST_ASSERT_EQUAL_STRING("10.0.0.1", out.peers.list[0]->host);
|
|
TEST_ASSERT_EQUAL_UINT32(9991, out.peers.list[0]->port);
|
|
TEST_ASSERT_NOT_NULL(out.peers.list[1]);
|
|
TEST_ASSERT_EQUAL_STRING("ctrl-b", out.peers.list[1]->name);
|
|
TEST_ASSERT_EQUAL_STRING("10.0.0.2", out.peers.list[1]->host);
|
|
TEST_ASSERT_EQUAL_UINT32(9992, out.peers.list[1]->port);
|
|
|
|
free_CtrlStatus(&out);
|
|
capn_free(&dc);
|
|
free(buf);
|
|
|
|
printf("test_ctrlstatus_roundtrip_all_fields: PASS\n");
|
|
}
|
|
|
|
/* An "unknown" primary (empty primaryName) must round-trip as empty/NULL,
|
|
* never garbage — the client renders it as "unknown" (Req 1.3, 2.2). */
|
|
static void test_ctrlstatus_roundtrip_unknown_primary(void)
|
|
{
|
|
xce_ctrlstatus_t in;
|
|
memset(&in, 0, sizeof(in));
|
|
in.clusterName = "lab-cluster";
|
|
in.primaryName = ""; /* unknown */
|
|
in.responderName = "ctrl-a";
|
|
in.responderRole = 1; /* ELECTING */
|
|
in.peers.count = 0;
|
|
in.peers.list = NULL;
|
|
in.peerCount = 0;
|
|
|
|
struct capn c;
|
|
struct capn_segment* cs;
|
|
struct CtrlStatus cap_in;
|
|
CtrlStatus_ptr sp;
|
|
|
|
capn_init_malloc(&c);
|
|
cs = capn_root(&c).seg;
|
|
memset(&cap_in, 0, sizeof(cap_in));
|
|
encode_CtrlStatus(cs, &cap_in, &in);
|
|
sp = new_CtrlStatus(cs);
|
|
write_CtrlStatus(&cap_in, sp);
|
|
capn_setp(capn_root(&c), 0, sp.p);
|
|
|
|
size_t sz = capn_size(&c);
|
|
uint8_t* buf = (uint8_t*)malloc(sz);
|
|
TEST_ASSERT_NOT_NULL(buf);
|
|
capn_write_mem(&c, buf, sz, 0);
|
|
capn_free(&c);
|
|
|
|
struct capn dc;
|
|
struct CtrlStatus cap_out;
|
|
CtrlStatus_ptr dp;
|
|
xce_ctrlstatus_t out;
|
|
|
|
capn_init_mem(&dc, buf, sz, 0);
|
|
dp.p = capn_getp(capn_root(&dc), 0, 1);
|
|
read_CtrlStatus(&cap_out, dp);
|
|
memset(&out, 0, sizeof(out));
|
|
decode_CtrlStatus(&out, &cap_out);
|
|
|
|
TEST_ASSERT_EQUAL_STRING("ctrl-a", out.responderName);
|
|
TEST_ASSERT_EQUAL_UINT8(1, out.responderRole);
|
|
/* empty primaryName decodes to NULL or "" — not a stale/guessed value */
|
|
if (out.primaryName != NULL) {
|
|
TEST_ASSERT_EQUAL_STRING("", out.primaryName);
|
|
}
|
|
TEST_ASSERT_EQUAL_INT(0, out.peers.count);
|
|
TEST_ASSERT_EQUAL_UINT32(0, out.peerCount);
|
|
|
|
free_CtrlStatus(&out);
|
|
capn_free(&dc);
|
|
free(buf);
|
|
|
|
printf("test_ctrlstatus_roundtrip_unknown_primary: PASS\n");
|
|
}
|
|
|
|
/* bug-1401: xce_free_ctrlstatus() must free the heap-allocated xce_ctrlstatus_t
|
|
* itself, not just the fields inside it. decode_CtrlStatus_ptr() (the real
|
|
* client reply path, via xce_decode -> ... -> xce_ctrlstatus) calloc's the
|
|
* struct; free_CtrlStatus() alone only frees the inner strings/peers list and
|
|
* leaks the struct. This mirrors that allocation path (decode_CtrlStatus_ptr,
|
|
* not the stack-struct decode_CtrlStatus used by the roundtrip tests above)
|
|
* and relies on ASan leak detection to catch a regression. */
|
|
static void test_xce_free_ctrlstatus_frees_the_struct_itself(void)
|
|
{
|
|
xce_ctrlpeer_t p0 = { .name = "ctrl-a", .host = "10.0.0.1", .port = 9991 };
|
|
xce_ctrlpeer_t* peerlist[1] = { &p0 };
|
|
xce_ctrlstatus_t in;
|
|
memset(&in, 0, sizeof(in));
|
|
in.clusterName = "lab-cluster";
|
|
in.primaryName = "ctrl-a";
|
|
in.responderName = "ctrl-a";
|
|
in.configChecksum = "deadbeef";
|
|
in.peers.count = 1;
|
|
in.peers.list = peerlist;
|
|
in.peerCount = 1;
|
|
|
|
struct capn c;
|
|
struct capn_segment* cs;
|
|
struct CtrlStatus cap_in;
|
|
CtrlStatus_ptr sp;
|
|
|
|
capn_init_malloc(&c);
|
|
cs = capn_root(&c).seg;
|
|
memset(&cap_in, 0, sizeof(cap_in));
|
|
encode_CtrlStatus(cs, &cap_in, &in);
|
|
sp = new_CtrlStatus(cs);
|
|
write_CtrlStatus(&cap_in, sp);
|
|
capn_setp(capn_root(&c), 0, sp.p);
|
|
|
|
size_t sz = capn_size(&c);
|
|
uint8_t* buf = (uint8_t*)malloc(sz);
|
|
TEST_ASSERT_NOT_NULL(buf);
|
|
capn_write_mem(&c, buf, sz, 0);
|
|
capn_free(&c);
|
|
|
|
struct capn dc;
|
|
CtrlStatus_ptr dp;
|
|
capn_init_mem(&dc, buf, sz, 0);
|
|
dp.p = capn_getp(capn_root(&dc), 0, 1);
|
|
|
|
xce_ctrlstatus_t* out = NULL;
|
|
decode_CtrlStatus_ptr(&out, dp);
|
|
TEST_ASSERT_NOT_NULL(out);
|
|
|
|
xce_free_ctrlstatus(out);
|
|
|
|
capn_free(&dc);
|
|
free(buf);
|
|
|
|
printf("test_xce_free_ctrlstatus_frees_the_struct_itself: PASS\n");
|
|
}
|
|
|
|
/* ── no reachable controller ⇒ typed failure (Req 7.3) ──────────────────── */
|
|
|
|
static void test_xcecli_ctrlstatus_null_ctx_typed_failure(void)
|
|
{
|
|
xce_ctrlstatus_t* status = (xce_ctrlstatus_t*)0xdeadbeef;
|
|
int rc = xcecli_ctrlstatus(NULL, &status);
|
|
/* typed negative, not a partial/empty success */
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
|
printf("test_xcecli_ctrlstatus_null_ctx_typed_failure: PASS\n");
|
|
}
|
|
|
|
/* ── authoritative (primary-only) read: task 3.2 ─────────────────────────── */
|
|
|
|
/* The authoritative call must reject a NULL/unconnected ctx with the same
|
|
* typed negative as the any-node read — it must NEVER hand back a hint reply
|
|
* as the authoritative result (Req 9.2). The `status` out-param is left
|
|
* untouched on failure (it still holds the poisoned sentinel below). */
|
|
static void test_xcecli_ctrlstatus_primary_null_ctx_typed_failure(void)
|
|
{
|
|
xce_ctrlstatus_t* status = (xce_ctrlstatus_t*)0xdeadbeef;
|
|
int rc = xcecli_ctrlstatus_primary(NULL, &status);
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
|
/* never returned a hint reply as the authoritative result */
|
|
TEST_ASSERT_EQUAL_PTR((xce_ctrlstatus_t*)0xdeadbeef, status);
|
|
printf("test_xcecli_ctrlstatus_primary_null_ctx_typed_failure: PASS\n");
|
|
}
|
|
|
|
/* NULL status out-param is also a typed failure (boundary validation). */
|
|
static void test_xcecli_ctrlstatus_primary_null_out_typed_failure(void)
|
|
{
|
|
int rc = xcecli_ctrlstatus_primary(NULL, NULL);
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
|
printf("test_xcecli_ctrlstatus_primary_null_out_typed_failure: PASS\n");
|
|
}
|
|
|
|
/* The pure authoritative-decision helper: a reply is authoritative iff the
|
|
* responder's role is PRIMARY (ctrl_role_t PRIMARY == 0). A standby (1),
|
|
* init (2), or electing (3) responder is a non-authoritative HINT — the call
|
|
* must keep following the redirect (Req 9.1, 2.3, 3.2). */
|
|
static void test_ctrlstatus_is_authoritative_decision(void)
|
|
{
|
|
xce_ctrlstatus_t reply;
|
|
memset(&reply, 0, sizeof(reply));
|
|
|
|
reply.responderRole = 0; /* PRIMARY */
|
|
TEST_ASSERT_TRUE(xcecli_ctrlstatus_is_authoritative(&reply));
|
|
|
|
reply.responderRole = 1; /* STANDBY */
|
|
TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(&reply));
|
|
|
|
reply.responderRole = 2; /* INIT */
|
|
TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(&reply));
|
|
|
|
reply.responderRole = 3; /* ELECTING */
|
|
TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(&reply));
|
|
|
|
/* NULL reply is never authoritative (defensive). */
|
|
TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(NULL));
|
|
|
|
printf("test_ctrlstatus_is_authoritative_decision: PASS\n");
|
|
}
|
|
|
|
/* The pure next-target helper: given a standby's hint reply, map the reported
|
|
* primaryName to its host:port via the reply's peers list so the next query
|
|
* can target the primary directly (the redirect step of Req 9.1). */
|
|
static void test_ctrlstatus_primary_addr_from_peers(void)
|
|
{
|
|
xce_ctrlpeer_t p0 = { .name = "ctrl-a", .host = "10.0.0.1", .port = 9991 };
|
|
xce_ctrlpeer_t p1 = { .name = "ctrl-b", .host = "10.0.0.2", .port = 9992 };
|
|
xce_ctrlpeer_t* peerlist[2] = { &p0, &p1 };
|
|
|
|
xce_ctrlstatus_t hint;
|
|
memset(&hint, 0, sizeof(hint));
|
|
hint.responderName = "ctrl-a";
|
|
hint.responderRole = 1; /* STANDBY */
|
|
hint.primaryName = "ctrl-b"; /* the standby knows ctrl-b is primary */
|
|
hint.peers.count = 2;
|
|
hint.peers.list = peerlist;
|
|
hint.peerCount = 2;
|
|
|
|
char host[XCE_MAXHOSTNAME_LEN];
|
|
uint16_t port = 0;
|
|
int rc = xcecli_ctrlstatus_primary_addr(&hint, host, sizeof(host), &port);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_EQUAL_STRING("10.0.0.2", host);
|
|
TEST_ASSERT_EQUAL_UINT16(9992, port);
|
|
|
|
printf("test_ctrlstatus_primary_addr_from_peers: PASS\n");
|
|
}
|
|
|
|
/* When the standby reports no known primary (empty primaryName) or the named
|
|
* primary is not in the peers list, the address lookup fails (-ENOENT) so the
|
|
* caller falls back to plain master rotation rather than targeting a bogus
|
|
* address (Req 2.3 unknown-primary, redirect best-effort). */
|
|
static void test_ctrlstatus_primary_addr_unknown_or_missing(void)
|
|
{
|
|
xce_ctrlpeer_t p0 = { .name = "ctrl-a", .host = "10.0.0.1", .port = 9991 };
|
|
xce_ctrlpeer_t* peerlist[1] = { &p0 };
|
|
|
|
xce_ctrlstatus_t hint;
|
|
memset(&hint, 0, sizeof(hint));
|
|
hint.responderName = "ctrl-a";
|
|
hint.responderRole = 3; /* ELECTING */
|
|
hint.peers.count = 1;
|
|
hint.peers.list = peerlist;
|
|
hint.peerCount = 1;
|
|
|
|
char host[XCE_MAXHOSTNAME_LEN];
|
|
uint16_t port = 7;
|
|
|
|
/* unknown primary (empty) */
|
|
hint.primaryName = "";
|
|
TEST_ASSERT_EQUAL_INT(-ENOENT, xcecli_ctrlstatus_primary_addr(
|
|
&hint, host, sizeof(host), &port));
|
|
|
|
/* NULL primary */
|
|
hint.primaryName = NULL;
|
|
TEST_ASSERT_EQUAL_INT(-ENOENT, xcecli_ctrlstatus_primary_addr(
|
|
&hint, host, sizeof(host), &port));
|
|
|
|
/* named primary not present in peers */
|
|
hint.primaryName = "ctrl-z";
|
|
TEST_ASSERT_EQUAL_INT(-ENOENT, xcecli_ctrlstatus_primary_addr(
|
|
&hint, host, sizeof(host), &port));
|
|
|
|
/* NULL/zero args are rejected */
|
|
hint.primaryName = "ctrl-a";
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL, xcecli_ctrlstatus_primary_addr(
|
|
NULL, host, sizeof(host), &port));
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL, xcecli_ctrlstatus_primary_addr(
|
|
&hint, NULL, sizeof(host), &port));
|
|
TEST_ASSERT_EQUAL_INT(
|
|
-EINVAL, xcecli_ctrlstatus_primary_addr(&hint, host, 0, &port));
|
|
TEST_ASSERT_EQUAL_INT(
|
|
-EINVAL,
|
|
xcecli_ctrlstatus_primary_addr(&hint, host, sizeof(host), NULL));
|
|
|
|
printf("test_ctrlstatus_primary_addr_unknown_or_missing: PASS\n");
|
|
}
|
|
|
|
/* ── reply-union type-confusion regression (bug-1077) ───────────────────────
|
|
*
|
|
* ctx->reply.u is a single untagged union shared by every opcode (debugmsg /
|
|
* get / jobid / clusterconf / ctrlstatus all alias the same 8 bytes). A
|
|
* failover-aborted reslist/joblist leaves ctx->reply.u.get pointing at the
|
|
* caller's (now-dead) stack `getreply`. When the next ctrlstatus reply lands on
|
|
* a worker thread, handle_reply must NOT treat that foreign, non-NULL slot as a
|
|
* CtrlStatus* and free it — doing so dereferenced a dead stack address inside
|
|
* free_CtrlStatus and crashed (ASan stack-use-after-return at
|
|
* controller.capnp.c:11638).
|
|
*
|
|
* This drives xce_handle_reply directly with a poisoned union slot and proves
|
|
* the foreign occupant is left untouched (no free, no deref). Pre-fix, the
|
|
* ctrlstatus branch frees the sentinel and the explicit free() below is a
|
|
* double-free that ASan aborts on. */
|
|
static void test_handle_reply_ctrlstatus_does_not_free_foreign_slot(void)
|
|
{
|
|
xce_ctx_t ctx;
|
|
memset(&ctx, 0, sizeof(ctx));
|
|
|
|
/* A prior get/reslist leaked a non-CtrlStatus pointer into the shared slot.
|
|
* Sized as a zeroed xce_ctrlstatus_t so that, even on the buggy path,
|
|
* free_CtrlStatus(sentinel) only walks all-NULL fields (no wild deref) and
|
|
* the regression surfaces as a clean double-free rather than a hard crash. */
|
|
xce_ctrlstatus_t* sentinel = (xce_ctrlstatus_t*)calloc(1, sizeof(*sentinel));
|
|
TEST_ASSERT_NOT_NULL(sentinel);
|
|
ctx.reply.u.get = (dataset_t*)sentinel; /* same 8 bytes as u.ctrlstatus */
|
|
|
|
/* A genuine ctrlstatus reply arrives for this ctx. */
|
|
xce_ctrlstatus_t* cs = (xce_ctrlstatus_t*)calloc(1, sizeof(*cs));
|
|
TEST_ASSERT_NOT_NULL(cs);
|
|
cs->clusterName = strdup("eda1");
|
|
|
|
xce_reply_t reply;
|
|
memset(&reply, 0, sizeof(reply));
|
|
reply.opcode = OPCode_ctrlstatus;
|
|
reply.status = 0;
|
|
reply.data_which = RequestReply_data_ctrlstatus;
|
|
reply.data.ctrlstatus = cs;
|
|
|
|
int rc = xce_handle_reply(&ctx, &reply);
|
|
|
|
/* The genuine reply is now owned by the ctx (MOVE_PTR nulls the source) … */
|
|
TEST_ASSERT_EQUAL_INT(1, rc);
|
|
TEST_ASSERT_EQUAL_PTR(cs, ctx.reply.u.ctrlstatus);
|
|
TEST_ASSERT_NULL(reply.data.ctrlstatus);
|
|
|
|
/* … and the foreign sentinel was NOT freed by the reply handler. Freeing it
|
|
* here is the sole owner's free; on the buggy code it is the second free. */
|
|
free(sentinel);
|
|
|
|
free_CtrlStatus(cs);
|
|
free(cs);
|
|
|
|
printf("test_handle_reply_ctrlstatus_does_not_free_foreign_slot: PASS\n");
|
|
}
|
|
|
|
/* ── Suite entry point ──────────────────────────────────────────────────── */
|
|
|
|
void test_ctrlstatus(void)
|
|
{
|
|
RUN_TEST(test_handle_reply_ctrlstatus_does_not_free_foreign_slot);
|
|
RUN_TEST(test_ctrlstatus_roundtrip_all_fields);
|
|
RUN_TEST(test_ctrlstatus_roundtrip_unknown_primary);
|
|
RUN_TEST(test_xce_free_ctrlstatus_frees_the_struct_itself);
|
|
RUN_TEST(test_xcecli_ctrlstatus_null_ctx_typed_failure);
|
|
RUN_TEST(test_xcecli_ctrlstatus_primary_null_ctx_typed_failure);
|
|
RUN_TEST(test_xcecli_ctrlstatus_primary_null_out_typed_failure);
|
|
RUN_TEST(test_ctrlstatus_is_authoritative_decision);
|
|
RUN_TEST(test_ctrlstatus_primary_addr_from_peers);
|
|
RUN_TEST(test_ctrlstatus_primary_addr_unknown_or_missing);
|
|
}
|