/* * 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 #include #include #include #include #include #include #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); }