/* * test_seqjob_allocator.c - task 3.1 (scheduler-allocation spec): tests * for seqjob_allocator.c's rewrap onto the shared xce_alloc_plan_iter_t / * xce_host_iter_t interfaces (Req 9.1) and the new maxTasks==1 guard on * seqjob_prepare_allocation() (Req 4.6). * * This is the task-3.1-level smoke coverage only -- proving the rewrapped * my_alloc() actually runs end to end through the shared iterators (not * just that it compiles) and that the guard closes the fan-out gap Req * 4.6 describes. The exhaustive pre/post-rewrap equivalence proof across * multiple candidate hosts (including a select-filter failure) is task * 3.2's dedicated job (test_seqjob_rewrap_equivalence.c) -- not * duplicated here. * * White-box #include convention: same combination test_host_iter.c (task * 2.1) already established (core.c + resource.c + schedobj.c + * host_iter.c) for a real worker/resmgr/select/availability fixture, plus * ../src/plugins/seqjob_allocator.c itself so this file can call its * `static` my_alloc()/seqjob_prepare_allocation() directly. * * seqjob_allocator.c's my_alloc() calls the real, exported * plugin_prepare_allocation() (plugin.c) to (re-)populate * ctx->alloc_req.host per candidate host -- plugin.c itself is NOT * white-box included here (it needs the full plugins kvmgr/registration * machinery, unrelated to what this file tests), so, like * test_host_iter.c's own link-satisfying stub, this file provides one -- * but unlike that file's no-op stub, this one delegates straight through * to the real seqjob_prepare_allocation() under test, standing in for a * fan-out of exactly the one plugin relevant here. */ #include #include #include #include #include "resobj.h" #include "resreq.h" #include "sched/alloc_iter.h" #include "scheduler.h" #include "xcealloc.h" /* Pull in the real xce_sched_core_init()/sched_add_worker() (core.c), * sched_meet_host_selection()/sched_check_host_satisfy()/put_resvals() * (resource.c), the real sched_ctx_init()/sched_ctx_get_uncomit_*() * (schedobj.c), the shared host_iter.c implementation, and * seqjob_allocator.c itself (the file under test) into this TU. */ #include "../src/core.c" #include "../src/resource.c" #include "../src/schedobj.c" #include "../src/host_iter.c" #include "../src/plugins/seqjob_allocator.c" /* ---------------------------------------------------------------------- */ /* Link-satisfying stubs */ /* ---------------------------------------------------------------------- */ /* plugin_register()/plugin_unregister() are referenced by * register_seqjob_allocator()/core.c respectively, but this file never * calls register_seqjob_allocator() (it calls my_alloc()/ * seqjob_prepare_allocation() directly, avoiding the full plugins kvmgr * this codebase's real plugin_register() needs) -- never actually * invoked by anything this file's tests exercise. */ int plugin_register(void* sched, void* plugin) { (void)sched; (void)plugin; return 0; } void plugin_unregister(void* arg) { (void)arg; } /* Unlike test_host_iter.c's no-op stub, this one delegates to the real * seqjob_prepare_allocation() under test -- standing in for * plugin_prepare_allocation()'s (plugin.c) real fan-out across every * registered plugin, here exactly one (seqjob's own), which is all this * file's fixtures ever register. Mirrors plugin.c:591's own reset-before- * fan-out step (xvec_idval_fini()+init() on whichever of * ctx->alloc_req.{shared,host} this call's ctx->worker selects) -- without * it, ctx->alloc_req.host would accumulate a second "slots" entry across * the real production call sequence's two prepare_allocation() calls per * host try (once at ctx->worker==NULL via sched_ctx_set_primary(), again * per host at ctx->worker!=NULL), since xvec_idval_store() never dedups. */ int plugin_prepare_allocation(xce_sched_core_t* sched, xce_sched_ctx_t* ctx) { xce_sched_plugin_t fake_plugin; if (ctx->worker == NULL) { xvec_idval_fini(&(ctx->alloc_req.shared)); xvec_idval_init(&(ctx->alloc_req.shared), 8); } else { xvec_idval_fini(&(ctx->alloc_req.host)); xvec_idval_init(&(ctx->alloc_req.host), 8); } memset(&fake_plugin, 0, sizeof(fake_plugin)); fake_plugin.sched = sched; return seqjob_prepare_allocation(&fake_plugin, ctx); } int plugin_job_update(void* s, xce_sched_job_t* j) { (void)s; (void)j; return 0; } int plugin_queue_update(void* s, xce_sched_queue_t* q) { (void)s; (void)q; return 0; } int plugin_job_rusage_update(void* s, xce_sched_job_t* j, xce_job_rusages_t* r) { (void)s; (void)j; (void)r; return 0; } /* Link-satisfying stub for sched_make_allocs() (scheduler.c) -- avoids * white-box-including the rest of that 1382-line file (job/queue/picker * machinery unrelated to seqjob_allocator.c) just to build the leaf * xce_alloc_t records my_alloc()'s success path constructs. Returns one * non-NULL, zeroed xce_alloc_t per requested resource index -- sufficient * for this file's assertions, which check WHICH host got allocated and * how many resource entries it carries, not the internal xce_alloc_t * field contents make_alloc() (xcealloc.c, unmodified, real) would fill * in for a live cluster. */ xce_alloc_t** sched_make_allocs(xce_scheduler_t* sched, char* reserve, int nres, int* idxs, float* vals, int* duration) { xce_alloc_t** allocs; int i; (void)sched; (void)reserve; (void)idxs; (void)vals; (void)duration; if (nres <= 0) { return NULL; } allocs = (xce_alloc_t**)calloc(nres, sizeof(*allocs)); if (allocs == NULL) { return NULL; } for (i = 0; i < nres; i++) { allocs[i] = (xce_alloc_t*)calloc(1, sizeof(xce_alloc_t)); } return allocs; } void setUp(void) { return; } void tearDown(void) { return; } /* ---------------------------------------------------------------------- */ /* Fixture helpers */ /* ---------------------------------------------------------------------- */ #define WORKER_NAME "seqjob-host" /* register_slots_resource - the real makeResource() (resobj.c), giving * "slots" a resmgr-assigned idx -- seqjob_prepare_allocation()'s own * key_find(sched->core.resmgr, "slots", ...) call needs this to be a * genuinely registered resource, unlike test_host_iter.c's fixture (which * stubs plugin_prepare_allocation() entirely and never needs resmgr * lookups to resolve). */ static xce_resource_t* register_slots_resource(var resmgr) { xce_resource_t res; memset(&res, 0, sizeof(res)); res.name = strdup("slots"); return makeResource(resmgr, &res); } /* add_worker_with_slots - a real sched_add_worker() (core.c) worker * carrying `slots_total` free "slots" (idx == the resmgr-assigned index), * mirroring test_host_iter.c's own add_worker() helper. */ static xce_sched_worker_t* add_worker_with_slots(xce_sched_core_t* core, const char* name, int slots_idx, float slots_total) { xce_sched_worker_t* w; int idx[1]; float val[1]; int rc; w = (xce_sched_worker_t*)calloc(1, sizeof(*w)); TEST_ASSERT_NOT_NULL(w); w->name = strdup(name); TEST_ASSERT_NOT_NULL(w->name); rc = zarray_init(&(w->resvals), 8, NULL); TEST_ASSERT_EQUAL_INT(0, rc); rc = zarray_init(&(w->resInUse), 8, NULL); TEST_ASSERT_EQUAL_INT(0, rc); idx[0] = slots_idx; val[0] = slots_total; put_resvals(&(w->resvals), 1, idx, val); val[0] = 0.0f; put_resvals(&(w->resInUse), 1, idx, val); rc = sched_add_worker(core, w); TEST_ASSERT_TRUE(rc >= 0); return w; } static void init_primary_obj(xce_sched_obj_t* obj, xce_primary_schedobj_t* primary_obj, xce_primary_resreq_t* primary) { memset(primary_obj, 0, sizeof(*primary_obj)); primary_obj->resreq = primary; memset(obj, 0, sizeof(*obj)); obj->tag = XCE_RESREQ_PRIMARY; obj->u.primary = primary_obj; } /* ---------------------------------------------------------------------- */ /* Tests */ /* ---------------------------------------------------------------------- */ /* * Req 9.1: a maxTasks==1 job with one available, matching host is still * allocated correctly through the REWRAPPED my_alloc() -- driven through * the real shared plan_iter -> host_iter composition (seqjob_plan_iter_*, * sched_host_iter_*), not a re-implementation of any of it. Proves the * rewrap actually runs end to end (host selected, resource debited, * ctx->alloc populated), not just that it compiles. */ static void test_my_alloc_allocates_single_matching_host_through_shared_iterators(void) { xce_scheduler_t sched; xce_resource_t* slots_res; xce_sched_obj_t obj; xce_primary_schedobj_t primary_obj; xce_primary_resreq_t primary; xce_sched_ctx_t ctx; xce_sched_plugin_t plugin; int rc; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); slots_res = register_slots_resource(sched.core.resmgr); TEST_ASSERT_NOT_NULL(slots_res); add_worker_with_slots(&(sched.core), WORKER_NAME, slots_res->idx, 4.0f); memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = NULL; /* matches every host */ init_primary_obj(&obj, &primary_obj, &primary); /* scheduler-sim-live-allocation spec, task 2.1: ctx->sched is now * honestly typed xce_sched_core_t* -- pass &(sched.core), not &sched. */ TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&ctx, &(sched.core), &obj)); /* Real call -- exercises plugin_prepare_allocation()'s ctx->worker == * NULL branch exactly as sched_make_primary_allocation() does today, * populating ctx->alloc_req.shared before my_alloc() ever runs. */ sched_ctx_set_primary(&ctx, &primary); memset(&plugin, 0, sizeof(plugin)); plugin.sched = &sched; rc = my_alloc(&plugin, &ctx); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_NOT_NULL(ctx.alloc); TEST_ASSERT_EQUAL_INT(1, ctx.alloc->nhosts); TEST_ASSERT_NOT_NULL(ctx.alloc->allocs); TEST_ASSERT_NOT_NULL(ctx.alloc->allocs[0]); TEST_ASSERT_EQUAL_STRING(WORKER_NAME, ctx.alloc->allocs[0]->host); /* "slots" is the one resource seqjob_prepare_allocation() always * stages -- proves ctx->alloc_req.host was populated by the real * per-host prepare cycle the try_alloc leaf runs, not left empty by * an under-wired iterator hookup. */ TEST_ASSERT_EQUAL_INT(1, ctx.alloc->allocs[0]->nallocs); sched_ctx_fini(&ctx); } /* * Req 4.6: seqjob_prepare_allocation() must never fire for maxTasks > 1 * jobs. Regression test against the exact fan-out corruption this guard * prevents: pre-stage a want entry into ctx->alloc_req.host as * parjob_prepare_allocation() would for its own bucket, then call * seqjob_prepare_allocation() for the SAME (maxTasks > 1) ctx and confirm * it returns immediately, WITHOUT xvec_idval_store()'s append-only * behavior adding a second, wrong entry alongside parjob's. */ static void test_seqjob_prepare_allocation_guards_against_maxtasks_gt_1_fanout(void) { xce_scheduler_t sched; xce_sched_obj_t obj; xce_primary_schedobj_t primary_obj; xce_primary_resreq_t primary; xce_sched_ctx_t ctx; xce_sched_plugin_t plugin; int rc; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 4; /* parjob's job, not seqjob's */ init_primary_obj(&obj, &primary_obj, &primary); /* scheduler-sim-live-allocation spec, task 2.1: ctx->sched is now * honestly typed xce_sched_core_t* -- pass &(sched.core), not &sched. */ TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&ctx, &(sched.core), &obj)); ctx.primary = &primary; /* Simulate parjob_prepare_allocation() having already staged this * bucket's real want (Req 4.6's own scenario) before seqjob's hook is * fanned out to for the SAME maxTasks > 1 job. */ rc = xvec_idval_store(&(ctx.alloc_req.host), 7 /* arbitrary residx */, 3.0f); TEST_ASSERT_TRUE(rc >= 0); memset(&plugin, 0, sizeof(plugin)); plugin.sched = &sched; ctx.worker = NULL; rc = seqjob_prepare_allocation(&plugin, &ctx); TEST_ASSERT_EQUAL_INT(0, rc); /* THE regression assertion: seqjob's hook must not have appended a * second, wrong entry via xvec_idval_store()'s append-only, no-dedup * behavior -- parjob's staged want is exactly as it left it. */ TEST_ASSERT_EQUAL_INT(1, ctx.alloc_req.host.count); TEST_ASSERT_EQUAL_INT(7, ((int*)ctx.alloc_req.host.idxs.data)[0]); TEST_ASSERT_EQUAL_FLOAT(3.0f, ((float*)ctx.alloc_req.host.vals.data)[0]); /* Same guard, the other prepare branch (ctx->worker != NULL, the * per-host pass a real bucket try would also trigger): still a no-op, * still untouched. */ ctx.worker = add_worker_with_slots(&(sched.core), "some-host", 9, 4.0f); rc = seqjob_prepare_allocation(&plugin, &ctx); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, ctx.alloc_req.host.count); TEST_ASSERT_EQUAL_INT(7, ((int*)ctx.alloc_req.host.idxs.data)[0]); sched_ctx_fini(&ctx); } int main(void) { UNITY_BEGIN(); RUN_TEST( test_my_alloc_allocates_single_matching_host_through_shared_iterators); RUN_TEST(test_seqjob_prepare_allocation_guards_against_maxtasks_gt_1_fanout); return UNITY_END(); }