/* * test_upgrade_snapshot_restore.c — config-upgrade-snapshot task 3 * * TDD tests for restore_host_state() (Requirements 1.10, 1.11, 5.8): * restoring a host admin-state frame must work whether or not the target * host has ever joined this member (create-if-missing, since hosts are * join-provisioned, not config-provisioned -- Design Invariant 1), must be * idempotent under a retried restore (Design Invariant 3), must never * clobber a pre-existing worker's non-admin state on a hit, and the * placeholder it creates on a miss must be safe for the host's later real * join (routed through rejoin_worker by join.c's own existing * key_find-based dispatch, unmodified) to adopt -- admin status set on the * placeholder must still be set after that real join (Design Invariant 6). * * RED phase: restore_host_state does not exist yet, so this file fails to * compile/link until Task 3 adds it to worker.c + worker.h. * * Fixture mirrors test_worker_join.c's bare in-memory cluster_t (workermgr + * resmgr kvmgr instances, no disk/persist) -- restore_host_state only * touches c->workermgr directly, and the placeholder-then-real-join test * additionally needs the resmgr resource defs that apply_join_state's * add_hostattrs_resvals()/set_host_slots() (invoked via rejoin_worker) * require -- same resource-name list as test_worker_join.c. * * collect_confvars() (required for worker.c to link without pulling in the * full dataex.c) is intentionally NOT redefined here: test_worker_join.c * already provides it once for the whole controller_unit_test binary. */ #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" /* ── Test fixture ────────────────────────────────────────────────────── */ /* Resource names/types matching add_hostattrs_resvals/set_host_slots in * worker.c -- required so a real join (rejoin_worker) after a placeholder * restore succeeds instead of failing resource lookups. */ #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; } tur_cluster_t; static tur_cluster_t g_tur; static void tur_setup(void) { kvmgr_params_t str_params = { .key_type = KVMGR_KEY_STRING, .nslots = 64, .delta = 16, .datakiller = NULL, }; /* Idempotent guard: the xstring intern table is process-global and * shared across every test file in this binary (mirrors * test_apply_seam.c / test_replay_apply_seam.c's guard, safer than * test_worker_join.c's unconditional call since both files run in the * same controller_unit_test process). */ if (makeString((char*)"tur-probe") == NULL) xstring_table_init(139); memset(&g_tur, 0, sizeof(g_tur)); g_tur.workermgr = makeInstance(&xkvmgr_type, &str_params); TEST_ASSERT_NOT_NULL(g_tur.workermgr); g_tur.resmgr = makeInstance(&xkvmgr_type, &str_params); TEST_ASSERT_NOT_NULL(g_tur.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_tur.resmgr, &rdef); TEST_ASSERT_NOT_NULL_MESSAGE(r, g_res_names[i]); } g_tur.c.workermgr = g_tur.workermgr; g_tur.c.resmgr = g_tur.resmgr; zarray_init(&g_tur.c.sharedinst, 64, NULL); } static void tur_teardown(void) { zarray_fini(&g_tur.c.sharedinst); if (g_tur.resmgr) dereference(g_tur.resmgr); if (g_tur.workermgr) dereference(g_tur.workermgr); memset(&g_tur, 0, sizeof(g_tur)); } static worker_t* tur_find(const char* name) { worker_t* w = NULL; lock(g_tur.c.workermgr, RTTI_LOCK_READ); key_find(g_tur.c.workermgr, (void*)name, (void**)&w); unlock(g_tur.c.workermgr); return w; } /* ── Miss: create-if-missing placeholder (Req 1.10) ────────────────────── */ static void test_restore_host_state_creates_placeholder_for_unknown_host(void) { tur_setup(); int rc = restore_host_state(&g_tur.c, "never-joined-host", XCE_WORKER_CLOSED); TEST_ASSERT_EQUAL_INT(0, rc); worker_t* w = tur_find("never-joined-host"); TEST_ASSERT_NOT_NULL_MESSAGE(w, "restore for a host with no local record must create exactly one placeholder"); TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED, "restored admin bit (CLOSED) must be set on the placeholder"); TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_UNAVAIL, "placeholder must be unjoined/dead-until-connected"); TEST_ASSERT_NOT_NULL_MESSAGE(w->host.attrs, "placeholder hostattrs must be non-NULL (rejoin_worker " "dereferences it directly on the later real join)"); tur_teardown(); printf("test_restore_host_state_creates_placeholder_for_unknown_host: PASS\n"); } /* ── Idempotent retry after partial failure (Req 5.8) ──────────────────── */ static void test_restore_host_state_retry_is_idempotent(void) { tur_setup(); int rc1 = restore_host_state(&g_tur.c, "retry-host", XCE_WORKER_CLOSED); TEST_ASSERT_EQUAL_INT(0, rc1); worker_t* w1 = tur_find("retry-host"); TEST_ASSERT_NOT_NULL(w1); int idx1 = w1->idx; /* Simulate a retried restore (e.g. after a partial earlier failure) -- * same name/admin bits, must find-and-continue, never a duplicate * create or an -EEXIST-style conflict. */ int rc2 = restore_host_state(&g_tur.c, "retry-host", XCE_WORKER_CLOSED); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc2, "retried restore must not error"); worker_t* w2 = tur_find("retry-host"); TEST_ASSERT_NOT_NULL(w2); TEST_ASSERT_EQUAL_INT_MESSAGE(idx1, w2->idx, "retry must reuse the same entry, never create a second one"); TEST_ASSERT_TRUE(w2->host.status & XCE_WORKER_CLOSED); tur_teardown(); printf("test_restore_host_state_retry_is_idempotent: PASS\n"); } /* ── Hit on a real, already-joined worker: OR bits only (Req 1.10) ──────── */ static void test_restore_host_state_on_real_worker_only_ors_admin_bits(void) { tur_setup(); 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_tur.c, &ep, "real-host-1", &attrs, &hconfig); TEST_ASSERT_GREATER_OR_EQUAL(0, idx); worker_t* w = tur_find("real-host-1"); TEST_ASSERT_NOT_NULL(w); TEST_ASSERT_FALSE(w->host.status & XCE_WORKER_UNAVAIL); TEST_ASSERT_EQUAL_PTR(&ep, w->ep); int rc = restore_host_state(&g_tur.c, "real-host-1", XCE_WORKER_CLOSED); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED, "admin bit must be OR'd onto the real worker"); TEST_ASSERT_FALSE_MESSAGE(w->host.status & XCE_WORKER_UNAVAIL, "restore on a real, already-joined worker must not mark it UNAVAIL"); TEST_ASSERT_EQUAL_STRING_MESSAGE("LINUX", w->host.attrs->hosttype, "restore on a hit must not touch existing attrs"); TEST_ASSERT_EQUAL_PTR_MESSAGE(&ep, w->ep, "restore on a hit must not touch ep"); tur_teardown(); printf("test_restore_host_state_on_real_worker_only_ors_admin_bits: PASS\n"); } /* ── Observable (task 3's own line): placeholder -> later real join ────── */ static void test_restore_host_state_placeholder_then_real_join_preserves_admin_status(void) { tur_setup(); /* Host admin-closed while down, then a configuration upgrade restores * its admin-state frame on a member with no local record of the host's * join -- must create a placeholder carrying the CLOSED bit. */ int rc = restore_host_state(&g_tur.c, "join-later-host", XCE_WORKER_CLOSED); TEST_ASSERT_EQUAL_INT(0, rc); worker_t* w = tur_find("join-later-host"); TEST_ASSERT_NOT_NULL(w); TEST_ASSERT_TRUE(w->host.status & XCE_WORKER_CLOSED); TEST_ASSERT_TRUE(w->host.status & XCE_WORKER_UNAVAIL); /* The host's real first join. join.c's own dispatch (join.c:90-103) * does exactly: key_find() -> found w != NULL -> rejoin_worker() (never * init_worker()). Reproduced directly here (rather than going through * handle_worker_join) since that needs a live xce_request_t/endpoint_t/ * persist commit-barrier harness this unit test intentionally has none * of -- the dispatch decision itself is not under test, join.c's is * unmodified and already covered elsewhere; what's under test is that * THIS placeholder is a valid, non-crashing input to rejoin_worker(). */ hostattrs_t new_attrs; memset(&new_attrs, 0, sizeof(new_attrs)); new_attrs.hosttype = makeString("LINUX"); new_attrs.hostmodel = makeString("x86_64"); new_attrs.cpufactor = 1.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)); worker_t* found = NULL; int findrc = 0; lock(g_tur.c.workermgr, RTTI_LOCK_READ); findrc = key_find(g_tur.c.workermgr, (void*)"join-later-host", (void**)&found); unlock(g_tur.c.workermgr); TEST_ASSERT_GREATER_THAN_MESSAGE(-1, findrc, "join.c's dispatch must find the placeholder (hit, not miss)"); TEST_ASSERT_EQUAL_PTR(w, found); int idx = rejoin_worker(&g_tur.c, found, &ep, &new_attrs, &hconfig); TEST_ASSERT_GREATER_OR_EQUAL_MESSAGE(0, idx, "rejoin_worker must not fail on the placeholder"); TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED, "admin status set via restore must still be set after the real join"); TEST_ASSERT_FALSE_MESSAGE(w->host.status & XCE_WORKER_UNAVAIL, "the real join must clear UNAVAIL"); TEST_ASSERT_EQUAL_STRING("LINUX", w->host.attrs->hosttype); dereference(new_attrs.hosttype); dereference(new_attrs.hostmodel); tur_teardown(); printf("test_restore_host_state_placeholder_then_real_join_preserves_admin_status: PASS\n"); } /* ── Defense-in-depth: non-admin bits are masked out ────────────────────── */ static void test_restore_host_state_masks_non_admin_bits(void) { tur_setup(); int rc = restore_host_state(&g_tur.c, "masked-host", XCE_WORKER_CLOSED | XCE_WORKER_BUSY); TEST_ASSERT_EQUAL_INT(0, rc); worker_t* w = tur_find("masked-host"); TEST_ASSERT_NOT_NULL(w); TEST_ASSERT_TRUE_MESSAGE(w->host.status & XCE_WORKER_CLOSED, "admin bit must be applied"); TEST_ASSERT_FALSE_MESSAGE(w->host.status & XCE_WORKER_BUSY, "non-admin bit must be masked out even if a caller passes it"); tur_teardown(); printf("test_restore_host_state_masks_non_admin_bits: PASS\n"); } /* ── Input validation at the boundary ───────────────────────────────────── */ static void test_restore_host_state_rejects_empty_name(void) { tur_setup(); TEST_ASSERT_EQUAL_INT(-EINVAL, restore_host_state(&g_tur.c, NULL, XCE_WORKER_CLOSED)); TEST_ASSERT_EQUAL_INT(-EINVAL, restore_host_state(&g_tur.c, "", XCE_WORKER_CLOSED)); tur_teardown(); printf("test_restore_host_state_rejects_empty_name: PASS\n"); } /* ── Dispatch ───────────────────────────────────────────────────────── */ void test_upgrade_snapshot_restore(void) { UnitySetTestFile(__FILE__); printf("\n>> config-upgrade-snapshot host restore tests (task 3)\n\n"); RUN_TEST(test_restore_host_state_creates_placeholder_for_unknown_host); RUN_TEST(test_restore_host_state_retry_is_idempotent); RUN_TEST(test_restore_host_state_on_real_worker_only_ors_admin_bits); RUN_TEST(test_restore_host_state_placeholder_then_real_join_preserves_admin_status); RUN_TEST(test_restore_host_state_masks_non_admin_bits); RUN_TEST(test_restore_host_state_rejects_empty_name); }