myxce/src/scheduler/testsuite/test_seqjob_allocator.c
Rongsong Shen 65679285ba
feat(scheduler-allocator-runner): seqjob allocates through the framework runner (task 4.2)
seqjob registers sched_allocator_run with seqjob_alloc_iface (no accepts
guard, plus its one-bucket plan iterator); its private my_alloc() is
gone. New fixtures pin the intended changes: a reason for every short
host, the bug-1893 shared reason, the host-stage OOM debit release, and
-EINVAL for missing inputs; seqjob's plan is asserted to be one bucket of
one slot. No other test expectation changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 09:53:14 +00:00

448 lines
17 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/alloc_runner.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;
/* scheduler-sim-live-allocation spec, task 4.3: seqjob_prepare_
* allocation() now reads plugin->core (honestly typed, task 1.2)
* instead of plugin->sched -- `sched` here is already an
* xce_sched_core_t* (this stub's own signature), so no cast is
* needed. */
fake_plugin.core = 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. */
/* bug-1882: set to make the stub fail like the real sched_make_allocs()
* does when make_alloc() cannot build a record. */
static int g_make_allocs_fail = 0;
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 (g_make_allocs_fail) {
errno = ENOMEM;
return NULL;
}
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) { g_make_allocs_fail = 0; }
void tearDown(void) { g_make_allocs_fail = 0; }
/* ---------------------------------------------------------------------- */
/* 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;
/* scheduler-sim-live-allocation spec, task 4.3: my_alloc() now reads
* plugin->core (honestly typed, task 1.2) for every core-safe call,
* alongside the still-needed plugin->sched (sched_make_allocs(),
* scheduler.c, unchanged). */
plugin.core = &(sched.core);
plugin.allocator_iface = &seqjob_alloc_iface;
rc = sched_allocator_run(&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;
/* scheduler-sim-live-allocation spec, task 4.3: seqjob_prepare_
* allocation() now reads plugin->core (honestly typed, task 1.2). */
plugin.core = &(sched.core);
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);
}
/*
* bug-1882: when the allocation records cannot be built (sched_make_allocs()
* returns NULL), my_alloc() must report -ENOMEM and leave ctx->alloc NULL
* -- sched_make_primary_allocation() moves ctx.alloc onto the job whatever
* the rc, so a half-built allocation there would be dispatched.
*/
static void test_my_alloc_reports_enomem_when_allocs_cannot_be_built(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;
init_primary_obj(&obj, &primary_obj, &primary);
TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&ctx, &(sched.core), &obj));
sched_ctx_set_primary(&ctx, &primary);
memset(&plugin, 0, sizeof(plugin));
plugin.sched = &sched;
plugin.core = &(sched.core);
plugin.allocator_iface = &seqjob_alloc_iface;
g_make_allocs_fail = 1;
rc = sched_allocator_run(&plugin, &ctx);
TEST_ASSERT_EQUAL_INT(-ENOMEM, rc);
TEST_ASSERT_NULL(ctx.alloc);
sched_ctx_fini(&ctx);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_my_alloc_reports_enomem_when_allocs_cannot_be_built);
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();
}