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>
719 lines
24 KiB
C
719 lines
24 KiB
C
/*
|
||
* 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);
|
||
}
|