Retypes xce_sched_ctx_t.sched (sched/types.h) from void* to xce_sched_core_t* outright, and sched_try_alloc_on_host_() (resource.c) to take xce_sched_core_t* directly -- completing what tasks 1.2/1.3 left as temporary &(sched->core) call-site wraps. Removes the two resulting struct-prefix casts entirely (schedobj.c's sched_ctx_set_primary(), host_iter.c's sched_host_iter_next()): both now pass core state through honestly, with no cast. Updates every sched_ctx_init()/sched_try_alloc_on_host_() call site tree-wide (scheduler.c, parjob_allocator.c, seqjob_allocator.c) with zero change to host_iter/parjob/seqjob's own host-iteration/ bucket-construction algorithm. Adds a driver-parity test for sched_try_alloc_on_host_() proving identical grant/deny results from normal-mode-shaped and simulation-mode-shaped core state. Flags (for task 2.3) a confirmed-dead, type-confused write at plugins/limit.c:319 (ctx.sched = plugin->sched) -- never read today, left untouched since limit.c is task 2.3's boundary, not this one's. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
375 lines
14 KiB
C
375 lines
14 KiB
C
/*
|
|
* 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 <errno.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#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();
|
|
}
|