myxce/src/controller/testsuite/test_worker_join.c
Rongsong Shen e3b3ffaa97
fix(controller,xceadmin): fix zero-slot scheduling block, misleading resval logs, byte-unit display
Three bugs found while diagnosing a live controller log, all in the
worker resource-value path:

1. Every host relying on the documented hosts.yaml auto-detect default
   (no max_slot, meaning "use CPU count") got 0 scheduling slots,
   permanently blocking the scheduler from placing jobs on it. mxj is
   UInt32 on the wire and can't carry the schema's -1 sentinel; mapping.py
   worked around this by leaving it unset, which defaults to 0 -- and 0 is
   never < 0, so apply_join_state()'s auto-detect fallback never fired.
   Fixed by encoding the sentinel as 0xFFFFFFFF, which the controller's
   `int mxj` field reinterprets back to -1 via its existing implicit
   uint32_t->int conversion. No C-side or wire schema change needed for
   this part; the fallback itself was already correct but had zero test
   coverage for a negative mxj, now added.

2. update_resvals()/update_resval_list() logged the failure reason via
   PERROR, which reads process-global errno -- but update_resval()'s
   failure paths return raw negative codes directly and never touch
   errno, so the controller log filled with misleading "update_resval:
   Success" / "update_resval: Resource temporarily unavailable" lines
   instead of the real reason. Fixed to decode the actual rc via
   xce_strerror_r and name the failing resource.

3. maxmem is canonically stored/wired as raw bytes but had no unit
   metadata, so resval/workerlist printed it as a bare number that read
   as garbage (e.g. workerlist's fixed-width columns visually smeared a
   multi-GB byte count across the MaxCPU/MaxMem headers). Added an
   optional `unit` field to the Resource/ResourceVal capnp structs (and
   xce_resource_t/xce_resval_t/resources.yaml) so resval and workerlist
   can render a human-readable value (e.g. "30.90 GB") while the stored
   value stays raw bytes -- display-only, every other resource unaffected.

Regenerated the xceadmin golden fixture (fixtures/full/expected.bin)
twice: once for the mxj sentinel encoding change, once for the new
Resource.unit field growing every resource's wire size by one pointer
slot. Both diffs verified byte-for-byte isolated to exactly the expected
change before regenerating.

Verified: 1095/1095 controller_unit_test (stable across repeated runs),
229/229 xceadmin pytest, clean rebuild of lib/controller/xceadmin/xce_test.
Two related findings logged to .wolf/buglog.json but deliberately not
fixed here (out of scope per discussion): a pre-existing flaky segfault
in the combined controller_unit_test binary, and maxswap/maxtmp being
hardcoded stub values on the worker side.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 08:12:09 +00:00

719 lines
24 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* test_worker_join.c — TDD tests for worker join refactor
*
* Task 1.1: Failing tests for apply_join_state (shared status/resource update)
* Task 1.2: Failing tests for init_worker (first-time join path)
* Task 1.3: Failing tests for rejoin_worker (reconnect update with admin-bit preservation)
*
* RED phase: apply_join_state / init_worker / rejoin_worker are stubs that
* return 0 without modifying state, so all assertions below fail until
* Tasks 2–3 provide real implementations.
*/
/* rtti.h must come first — it defines 'var' and 'typeinfo_t' used by kvmgr.h */
#include <rtti.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#include "cluster.h"
#include "kvmgr.h"
#include "resobj.h"
#include "wobj.h"
#include "worker.h"
#include "xce/cluster_def.h"
#include "xce/types.h"
#include "xstring.h"
#include "zarray.h"
/* ── Stubs for controller functions not under test ───────────────────── */
/*
* collect_confvars is called by on_worker_join → worker_push_config.
* Stubbed here so worker.c links without pulling in the full dataex.c.
*/
void collect_confvars(cluster_t* c, collect_check_fn_t fn, xlist_t* l)
{
(void)c;
(void)fn;
(void)l;
}
/* ── Forward declarations for functions under test ──────────────────── */
/*
* apply_join_state is non-static in worker.c when XCE_TESTING is defined.
*
* RED: the current stub body does nothing and returns 0, so assertions
* that check the modified state will fail.
*/
int apply_join_state(cluster_t* c, worker_t* w, endpoint_t* ep,
xce_hostconfig_t* hconfig);
/*
* init_worker and rejoin_worker are exported from worker.c.
*
* RED: stubs return 0 without registering or modifying any entry.
*/
int init_worker(cluster_t* c, endpoint_t* ep, const char* host,
hostattrs_t* attrs, xce_hostconfig_t* hconfig);
int rejoin_worker(cluster_t* c, worker_t* w, endpoint_t* ep,
hostattrs_t* attrs, xce_hostconfig_t* hconfig);
/* ── Test fixture ────────────────────────────────────────────────────── */
/* Resource names and value types matching add_hostattrs_resvals in worker.c */
#define N_RES 9
static const char* g_res_names[N_RES] = {
"type", "model", "cpuf", "maxcpus", "maxmem",
"maxswap", "maxtmp", "ndisks", "slots"
};
static const int g_res_valtypes[N_RES] = {
XCE_VAL_STRING, XCE_VAL_STRING,
XCE_VAL_NUMERIC, XCE_VAL_NUMERIC, XCE_VAL_NUMERIC,
XCE_VAL_NUMERIC, XCE_VAL_NUMERIC, XCE_VAL_NUMERIC,
XCE_VAL_NUMERIC
};
typedef struct {
cluster_t c;
var workermgr;
var resmgr;
} test_cluster_t;
static test_cluster_t g_tc;
static void setup_test_cluster(void)
{
kvmgr_params_t str_params = {
.key_type = KVMGR_KEY_STRING,
.nslots = 64,
.delta = 16,
.datakiller = NULL,
};
memset(&g_tc, 0, sizeof(g_tc));
g_tc.workermgr = makeInstance(&xkvmgr_type, &str_params);
TEST_ASSERT_NOT_NULL(g_tc.workermgr);
g_tc.resmgr = makeInstance(&xkvmgr_type, &str_params);
TEST_ASSERT_NOT_NULL(g_tc.resmgr);
for (int i = 0; i < N_RES; i++) {
xce_resource_t rdef;
memset(&rdef, 0, sizeof(rdef));
rdef.name = (char*)g_res_names[i];
rdef.valtype = g_res_valtypes[i];
rdef.flags = 0;
rdef.idx = -1;
xce_resource_t* r = makeResource(g_tc.resmgr, &rdef);
TEST_ASSERT_NOT_NULL_MESSAGE(r, g_res_names[i]);
}
g_tc.c.workermgr = g_tc.workermgr;
g_tc.c.resmgr = g_tc.resmgr;
zarray_init(&g_tc.c.sharedinst, 64, NULL);
}
static void teardown_test_cluster(void)
{
zarray_fini(&g_tc.c.sharedinst);
if (g_tc.resmgr) dereference(g_tc.resmgr);
if (g_tc.workermgr) dereference(g_tc.workermgr);
memset(&g_tc, 0, sizeof(g_tc));
}
/* Create a worker in the test workermgr; attrs strings must be makeString'd. */
static worker_t* make_test_worker(const char* name, hostattrs_t* attrs)
{
xce_worker_t host;
memset(&host, 0, sizeof(host));
host.name = (char*)name;
host.attrs = attrs;
return makeWorker(g_tc.workermgr, &host);
}
/* Retrieve a numeric resource value from a worker's resvals by resource name */
static float get_worker_res_fval(worker_t* w, const char* name)
{
xce_resource_t* r = NULL;
lock(g_tc.c.resmgr, RTTI_LOCK_READ);
key_find(g_tc.c.resmgr, (void*)name, (void**)&r);
unlock(g_tc.c.resmgr);
if (r == NULL || r->idx < 0 || r->idx >= w->host.resvals.caps)
return -1.0f;
xce_val_t* v = (xce_val_t*)w->host.resvals.data[r->idx];
return (v != NULL) ? v->u.fvalue : -1.0f;
}
/*
* Capture whatever update_resvals()/update_resval_list() print during a
* failing update_resval() call, as raw runtime output rather than a
* source-text scan. This test binary never calls mylog_init()/tlog_init()
* (only the real daemon main()s do -- see test_apply_correctness_
* verification.c's own header comment, and the identical established
* precedent in scheduler/testsuite/test_scheduler_connect.c's
* capture_stdout_during_sched_init()), so tlog's internal root log handle
* stays NULL and every ERROR()/WARN() call falls through tlog's early-
* print path, which writes the fully formatted line directly to
* STDOUT_FILENO via a raw write() -- genuine runtime log capture, not a
* guess about which stream PERROR/ERROR happens to use.
*/
static char* capture_stdout_during(void (*fn)(void* ctx), void* ctx)
{
char tmp[] = "/tmp/xce_worker_join_logcap_XXXXXX";
int fd = mkstemp(tmp);
TEST_ASSERT_TRUE_MESSAGE(fd >= 0,
"fixture: mkstemp for stdout capture must succeed");
fflush(stdout);
int saved_stdout = dup(STDOUT_FILENO);
TEST_ASSERT_TRUE_MESSAGE(saved_stdout >= 0,
"fixture: dup(STDOUT_FILENO) must succeed");
TEST_ASSERT_TRUE_MESSAGE(dup2(fd, STDOUT_FILENO) >= 0,
"fixture: dup2 redirect of STDOUT_FILENO must "
"succeed");
fn(ctx);
fflush(stdout);
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdout);
off_t len = lseek(fd, 0, SEEK_CUR);
TEST_ASSERT_TRUE_MESSAGE(len >= 0,
"fixture: lseek on capture file must succeed");
char* buf = (char*)calloc(1, (size_t)len + 1);
TEST_ASSERT_NOT_NULL(buf);
if (len > 0) {
TEST_ASSERT_TRUE_MESSAGE(lseek(fd, 0, SEEK_SET) == 0,
"fixture: rewind of capture file must "
"succeed");
ssize_t nread = read(fd, buf, (size_t)len);
TEST_ASSERT_TRUE_MESSAGE(nread == len,
"fixture: reading back captured buffer "
"must succeed");
}
close(fd);
unlink(tmp);
return buf;
}
typedef struct {
cluster_t* c;
worker_t* w;
xce_resval_t* val;
} update_resvals_call_ctx_t;
static void call_update_resvals_1(void* ctx_)
{
update_resvals_call_ctx_t* ctx = (update_resvals_call_ctx_t*)ctx_;
update_resvals(ctx->c, ctx->w, 1, ctx->val);
}
/*
* update_resval() returns its failure reason as a raw negative code
* directly (key_find/zarray_put convention -- never via errno), so
* update_resvals()/update_resval_list() logging it via PERROR (which reads
* the process-global errno) reported whatever errno happened to be left
* over from an unrelated earlier call: "Success" if nothing had touched
* it, or a stray EAGAIN/etc from some other syscall in the same event-loop
* iteration -- never the real reason a resource update failed. This test
* reproduces that exact symptom (ambient errno=EAGAIN, unrelated to the
* genuine failure) and proves the fix decodes the actual rc instead.
*/
static void test_update_resvals_logs_actual_rc_not_stale_errno(void)
{
setup_test_cluster();
hostattrs_t attrs;
memset(&attrs, 0, sizeof(attrs));
attrs.hosttype = makeString("LINUX");
attrs.hostmodel = makeString("x86");
worker_t* w = make_test_worker("host-resval-log", &attrs);
TEST_ASSERT_NOT_NULL(w);
xce_val_t fval;
memset(&fval, 0, sizeof(fval));
fval.which = Value_u_fvalue;
fval.u.fvalue = 1.0f;
xce_resval_t val;
val.name = "does-not-exist-in-resmgr";
val.val = &fval;
/* Simulate the exact reported symptom: ambient errno unrelated to the
* real failure, left over from some other unrelated call. */
errno = EAGAIN;
update_resvals_call_ctx_t ctx = { .c = &g_tc.c, .w = w, .val = &val };
char* captured = capture_stdout_during(call_update_resvals_1, &ctx);
TEST_ASSERT_NOT_NULL(captured);
TEST_ASSERT_NULL_MESSAGE(
strstr(captured, "Resource temporarily unavailable"),
"must not log the ambient errno's string (EAGAIN) -- that is not "
"why update_resval failed");
TEST_ASSERT_NULL_MESSAGE(
strstr(captured, "Success"),
"must not log \"Success\" for a call that returned rc<0");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "does-not-exist-in-resmgr"),
"must name which resource failed");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "No such file or directory"),
"must log the real reason (-ENOENT, from key_find on an "
"unregistered resource name)");
free(captured);
teardown_test_cluster();
printf("test_update_resvals_logs_actual_rc_not_stale_errno: PASS\n");
}
/* ── Task 1.1: Tests for apply_join_state ──────────────────────────── */
/*
* After apply_join_state, XCE_WORKER_UNAVAIL must be cleared. Req 4.1
*/
static void test_apply_join_state_clears_unavail(void)
{
setup_test_cluster();
hostattrs_t attrs;
memset(&attrs, 0, sizeof(attrs));
attrs.hosttype = makeString("LINUX");
attrs.hostmodel = makeString("x86");
attrs.cpufactor = 1.0f;
attrs.maxcpus = 4;
attrs.maxmem = 16;
attrs.maxswap = 32;
attrs.maxtmp = 64;
attrs.ndisks = 1;
worker_t* w = make_test_worker("host-ajs-1", &attrs);
TEST_ASSERT_NOT_NULL(w);
lock(w, RTTI_LOCK_WRITE);
w->host.status |= XCE_WORKER_UNAVAIL;
unlock(w);
xce_hostconfig_t hconfig;
memset(&hconfig, 0, sizeof(hconfig));
hconfig.hosttype = (char*)"!"; /* no type override */
hconfig.hostmodel = (char*)"!"; /* no model override */
hconfig.mxj = 2;
endpoint_t ep;
memset(&ep, 0, sizeof(ep));
lock(w, RTTI_LOCK_WRITE);
int rc = apply_join_state(&g_tc.c, w, &ep, &hconfig);
unlock(w);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_FALSE_MESSAGE(w->host.status & XCE_WORKER_UNAVAIL,
"UNAVAIL must be cleared after apply_join_state");
teardown_test_cluster();
printf("test_apply_join_state_clears_unavail: PASS\n");
}
/*
* When hconfig provides non-"!" overrides, hosttype and hostmodel must
* match those values after apply_join_state. Req 4.2
*/
static void test_apply_join_state_applies_hosttype_override(void)
{
setup_test_cluster();
hostattrs_t attrs;
memset(&attrs, 0, sizeof(attrs));
attrs.hosttype = makeString("OLD_TYPE");
attrs.hostmodel = makeString("OLD_MODEL");
attrs.cpufactor = 1.0f;
attrs.maxcpus = 2;
attrs.maxmem = 8;
attrs.maxswap = 16;
attrs.maxtmp = 32;
attrs.ndisks = 1;
worker_t* w = make_test_worker("host-ajs-2", &attrs);
TEST_ASSERT_NOT_NULL(w);
xce_hostconfig_t hconfig;
memset(&hconfig, 0, sizeof(hconfig));
hconfig.hosttype = (char*)"LINUX64";
hconfig.hostmodel = (char*)"x86_64";
hconfig.mxj = 4;
endpoint_t ep;
memset(&ep, 0, sizeof(ep));
lock(w, RTTI_LOCK_WRITE);
int rc = apply_join_state(&g_tc.c, w, &ep, &hconfig);
unlock(w);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_STRING_MESSAGE("LINUX64", w->host.attrs->hosttype,
"hosttype must be overridden from hconfig");
TEST_ASSERT_EQUAL_STRING_MESSAGE("x86_64", w->host.attrs->hostmodel,
"hostmodel must be overridden from hconfig");
teardown_test_cluster();
printf("test_apply_join_state_applies_hosttype_override: PASS\n");
}
/*
* After apply_join_state, 'slots' equals hconfig.mxj and numeric host-attr
* resources match the worker's attrs. Req 4.2, 4.3
*/
static void test_apply_join_state_updates_resource_values(void)
{
setup_test_cluster();
hostattrs_t attrs;
memset(&attrs, 0, sizeof(attrs));
attrs.hosttype = makeString("LINUX");
attrs.hostmodel = makeString("x86");
attrs.cpufactor = 3.0f;
attrs.maxcpus = 8;
attrs.maxmem = 64;
attrs.maxswap = 128;
attrs.maxtmp = 256;
attrs.ndisks = 4;
worker_t* w = make_test_worker("host-ajs-3", &attrs);
TEST_ASSERT_NOT_NULL(w);
xce_hostconfig_t hconfig;
memset(&hconfig, 0, sizeof(hconfig));
hconfig.hosttype = (char*)"!";
hconfig.hostmodel = (char*)"!";
hconfig.mxj = 6;
endpoint_t ep;
memset(&ep, 0, sizeof(ep));
lock(w, RTTI_LOCK_WRITE);
int rc = apply_join_state(&g_tc.c, w, &ep, &hconfig);
unlock(w);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(6.0f, get_worker_res_fval(w, "slots"),
"slots must equal hconfig.mxj");
TEST_ASSERT_EQUAL_FLOAT(3.0f, get_worker_res_fval(w, "cpuf"));
TEST_ASSERT_EQUAL_FLOAT(8.0f, get_worker_res_fval(w, "maxcpus"));
TEST_ASSERT_EQUAL_FLOAT(64.0f, get_worker_res_fval(w, "maxmem"));
TEST_ASSERT_EQUAL_FLOAT(128.0f, get_worker_res_fval(w, "maxswap"));
TEST_ASSERT_EQUAL_FLOAT(256.0f, get_worker_res_fval(w, "maxtmp"));
TEST_ASSERT_EQUAL_FLOAT(4.0f, get_worker_res_fval(w, "ndisks"));
teardown_test_cluster();
printf("test_apply_join_state_updates_resource_values: PASS\n");
}
/*
* hconfig.mxj == -1 is the schema's documented "auto-detect" sentinel
* (hosts.schema.json: "max_slot": {"minimum": -1, "description": "-1 means
* auto-detect"}) -- apply_join_state must fall back to the worker's own
* reported maxcpus, NOT persist -1 (or, before the xceadmin mapping fix,
* silently ship 0 and permanently disable scheduling on that host: mxj is
* UInt32 on the wire, so an unset/absent max_slot used to decode as 0,
* which is not < 0 and so never reached this fallback -- see
* xcecommon.mapping.HOSTCONFIG_MXJ_AUTO_DETECT). This is the ONLY test in
* this file that exercises the nslots<0 branch; every sibling test above
* uses a positive hconfig.mxj and so cannot catch a regression here.
*/
static void test_apply_join_state_mxj_negative_one_falls_back_to_maxcpus(void)
{
setup_test_cluster();
hostattrs_t attrs;
memset(&attrs, 0, sizeof(attrs));
attrs.hosttype = makeString("LINUX");
attrs.hostmodel = makeString("x86");
attrs.cpufactor = 1.0f;
attrs.maxcpus = 12;
attrs.maxmem = 64;
attrs.maxswap = 128;
attrs.maxtmp = 256;
attrs.ndisks = 2;
worker_t* w = make_test_worker("host-ajs-autodetect", &attrs);
TEST_ASSERT_NOT_NULL(w);
xce_hostconfig_t hconfig;
memset(&hconfig, 0, sizeof(hconfig));
hconfig.hosttype = (char*)"!";
hconfig.hostmodel = (char*)"!";
hconfig.mxj = -1;
endpoint_t ep;
memset(&ep, 0, sizeof(ep));
lock(w, RTTI_LOCK_WRITE);
int rc = apply_join_state(&g_tc.c, w, &ep, &hconfig);
unlock(w);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
12.0f, get_worker_res_fval(w, "slots"),
"mxj=-1 must auto-detect slots from the worker's reported maxcpus, "
"not persist -1 or silently leave slots at 0");
teardown_test_cluster();
printf("test_apply_join_state_mxj_negative_one_falls_back_to_maxcpus: "
"PASS\n");
}
/* ── Task 1.2: Tests for init_worker ────────────────────────────────── */
/*
* After init_worker for a new hostname, key_find must find the entry and
* return a non-negative index. Req 2.1, 2.3
*/
static void test_init_worker_creates_new_entry(void)
{
setup_test_cluster();
hostattrs_t attrs;
memset(&attrs, 0, sizeof(attrs));
attrs.hosttype = makeString("LINUX");
attrs.hostmodel = makeString("x86");
attrs.cpufactor = 1.0f;
attrs.maxcpus = 4;
attrs.maxmem = 16;
attrs.maxswap = 32;
attrs.maxtmp = 64;
attrs.ndisks = 1;
xce_hostconfig_t hconfig;
memset(&hconfig, 0, sizeof(hconfig));
hconfig.hosttype = (char*)"!";
hconfig.hostmodel = (char*)"!";
hconfig.mxj = 4;
endpoint_t ep;
memset(&ep, 0, sizeof(ep));
int idx = init_worker(&g_tc.c, &ep, "new-host-1", &attrs, &hconfig);
TEST_ASSERT_GREATER_OR_EQUAL_MESSAGE(0, idx,
"init_worker must return non-negative index");
worker_t* w = NULL;
lock(g_tc.c.workermgr, RTTI_LOCK_READ);
int rc = key_find(g_tc.c.workermgr, (void*)"new-host-1", (void**)&w);
unlock(g_tc.c.workermgr);
TEST_ASSERT_GREATER_THAN_MESSAGE(-1, rc,
"worker must be registered in workermgr after init");
TEST_ASSERT_NOT_NULL_MESSAGE(w, "key_find must return a valid worker pointer");
teardown_test_cluster();
printf("test_init_worker_creates_new_entry: PASS\n");
}
/*
* The index returned by init_worker must match w->idx. Req 2.3
*/
static void test_init_worker_returns_correct_idx(void)
{
setup_test_cluster();
hostattrs_t attrs;
memset(&attrs, 0, sizeof(attrs));
attrs.hosttype = makeString("LINUX");
attrs.hostmodel = makeString("x86");
attrs.cpufactor = 1.0f;
attrs.maxcpus = 2;
attrs.maxmem = 8;
attrs.maxswap = 16;
attrs.maxtmp = 32;
attrs.ndisks = 1;
xce_hostconfig_t hconfig;
memset(&hconfig, 0, sizeof(hconfig));
hconfig.hosttype = (char*)"!";
hconfig.hostmodel = (char*)"!";
hconfig.mxj = 2;
endpoint_t ep;
memset(&ep, 0, sizeof(ep));
int idx = init_worker(&g_tc.c, &ep, "new-host-2", &attrs, &hconfig);
TEST_ASSERT_GREATER_OR_EQUAL(0, idx);
worker_t* w = NULL;
lock(g_tc.c.workermgr, RTTI_LOCK_READ);
key_find(g_tc.c.workermgr, (void*)"new-host-2", (void**)&w);
unlock(g_tc.c.workermgr);
TEST_ASSERT_NOT_NULL(w);
TEST_ASSERT_EQUAL_INT_MESSAGE(idx, w->idx,
"returned idx must match the worker's idx field");
teardown_test_cluster();
printf("test_init_worker_returns_correct_idx: PASS\n");
}
/* ── Task 1.3: Tests for rejoin_worker ──────────────────────────────── */
/*
* rejoin_worker must update hostattrs and ep on the existing entry. Req 3.1, 3.2
*/
static void test_rejoin_worker_updates_attrs_and_ep(void)
{
setup_test_cluster();
hostattrs_t old_attrs;
memset(&old_attrs, 0, sizeof(old_attrs));
old_attrs.hosttype = makeString("OLD_TYPE");
old_attrs.hostmodel = makeString("OLD_MODEL");
old_attrs.cpufactor = 1.0f;
old_attrs.maxcpus = 2;
old_attrs.maxmem = 8;
old_attrs.maxswap = 16;
old_attrs.maxtmp = 32;
old_attrs.ndisks = 1;
worker_t* w = make_test_worker("rejoin-host-1", &old_attrs);
TEST_ASSERT_NOT_NULL(w);
hostattrs_t new_attrs;
memset(&new_attrs, 0, sizeof(new_attrs));
new_attrs.hosttype = makeString("NEW_TYPE");
new_attrs.hostmodel = makeString("NEW_MODEL");
new_attrs.cpufactor = 2.0f;
new_attrs.maxcpus = 8;
new_attrs.maxmem = 64;
new_attrs.maxswap = 128;
new_attrs.maxtmp = 256;
new_attrs.ndisks = 2;
xce_hostconfig_t hconfig;
memset(&hconfig, 0, sizeof(hconfig));
hconfig.hosttype = (char*)"!";
hconfig.hostmodel = (char*)"!";
hconfig.mxj = 4;
endpoint_t ep;
memset(&ep, 0, sizeof(ep));
int idx = rejoin_worker(&g_tc.c, w, &ep, &new_attrs, &hconfig);
TEST_ASSERT_GREATER_OR_EQUAL_MESSAGE(0, idx,
"rejoin_worker must return non-negative index");
TEST_ASSERT_EQUAL_STRING_MESSAGE("NEW_TYPE", w->host.attrs->hosttype,
"hosttype must be updated to new attrs value");
TEST_ASSERT_EQUAL_STRING_MESSAGE("NEW_MODEL", w->host.attrs->hostmodel,
"hostmodel must be updated to new attrs value");
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(2.0f, w->host.attrs->cpufactor,
"cpufactor must be updated to new value");
TEST_ASSERT_EQUAL_PTR_MESSAGE(&ep, w->ep,
"ep must be updated to the new endpoint");
/* Release the test's own references to the new strings */
dereference(new_attrs.hosttype);
dereference(new_attrs.hostmodel);
teardown_test_cluster();
printf("test_rejoin_worker_updates_attrs_and_ep: PASS\n");
}
/*
* Admin-set status bits must survive a rejoin_worker call. Req 3.3
*/
static void test_rejoin_worker_preserves_admin_bits(void)
{
setup_test_cluster();
hostattrs_t attrs;
memset(&attrs, 0, sizeof(attrs));
attrs.hosttype = makeString("LINUX");
attrs.hostmodel = makeString("x86");
attrs.cpufactor = 1.0f;
attrs.maxcpus = 4;
attrs.maxmem = 16;
attrs.maxswap = 32;
attrs.maxtmp = 64;
attrs.ndisks = 1;
worker_t* w = make_test_worker("rejoin-host-2", &attrs);
TEST_ASSERT_NOT_NULL(w);
/* Set all admin bits before the rejoin */
lock(w, RTTI_LOCK_WRITE);
w->host.status |= (XCE_WORKER_LOCKEDU | XCE_WORKER_CLOSED
| XCE_WORKER_CLOSED_BY_ADMIN);
unlock(w);
hostattrs_t new_attrs;
memset(&new_attrs, 0, sizeof(new_attrs));
new_attrs.hosttype = makeString("LINUX");
new_attrs.hostmodel = makeString("x86");
new_attrs.cpufactor = 2.0f;
new_attrs.maxcpus = 8;
new_attrs.maxmem = 32;
new_attrs.maxswap = 64;
new_attrs.maxtmp = 128;
new_attrs.ndisks = 2;
xce_hostconfig_t hconfig;
memset(&hconfig, 0, sizeof(hconfig));
hconfig.hosttype = (char*)"!";
hconfig.hostmodel = (char*)"!";
hconfig.mxj = 4;
endpoint_t ep;
memset(&ep, 0, sizeof(ep));
rejoin_worker(&g_tc.c, w, &ep, &new_attrs, &hconfig);
TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_LOCKEDU,
"LOCKEDU must be preserved after rejoin");
TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED,
"CLOSED must be preserved after rejoin");
TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED_BY_ADMIN,
"CLOSED_BY_ADMIN must be preserved after rejoin");
dereference(new_attrs.hosttype);
dereference(new_attrs.hostmodel);
teardown_test_cluster();
printf("test_rejoin_worker_preserves_admin_bits: PASS\n");
}
/* ── Dispatch ───────────────────────────────────────────────────────── */
void test_worker_join(void)
{
UnitySetTestFile(__FILE__);
printf("\n>> worker join refactor tests\n\n");
/* xstring_table_init must be called once before any makeString usage */
xstring_table_init(139);
RUN_TEST(test_update_resvals_logs_actual_rc_not_stale_errno);
/* Task 1.1: apply_join_state (shared status/resource update) */
RUN_TEST(test_apply_join_state_clears_unavail);
RUN_TEST(test_apply_join_state_applies_hosttype_override);
RUN_TEST(test_apply_join_state_updates_resource_values);
RUN_TEST(test_apply_join_state_mxj_negative_one_falls_back_to_maxcpus);
/* Task 1.2: init_worker (first-time join) */
RUN_TEST(test_init_worker_creates_new_entry);
RUN_TEST(test_init_worker_returns_correct_idx);
/* Task 1.3: rejoin_worker (reconnect with admin-bit preservation) */
RUN_TEST(test_rejoin_worker_updates_attrs_and_ep);
RUN_TEST(test_rejoin_worker_preserves_admin_bits);
}