/* * test_seqjob_rewrap_equivalence.c - task 3.2 (scheduler-allocation spec): * the dedicated pre/post-rewrap host-selection EQUIVALENCE proof for * seqjob allocation (Req 9.1) -- then seqjob_allocator.c's my_alloc(), * today the framework routine sched_allocator_run() (alloc_runner.c). * * task 3.1's own test_seqjob_allocator.c only proves the rewrapped * allocator still runs end to end through the shared plan_iter/host_iter * composition (a single-obvious-host smoke case) and that the new * maxTasks==1 guard (Req 4.6) works. Req 9.1's actual claim is narrower * and harder to prove: "which host a job lands on, given the same * worker/resource state" must be UNCHANGED by the rewrap. A test that only * exercises the single-obvious-host case cannot distinguish "the new * composition happens to work" from "the new composition picks the same * host the old sched_collect_workers()+xlist_find() loop would have" -- * this file's whole job is to close that gap with fixture states rich * enough that a plausible regression in the rewrap would actually show up. * * How equivalence is established (see this task's own status report for * the full rationale): rather than literally resurrecting the deleted * pre-rewrap sched_collect_workers()/sched_try_alloc_on_hosts() code (git * history: commit b18c5f78^) and running it side by side, this file hand- * derives the expected pick from the two well-defined, already-unit-tested * primitives the composition is built from -- sched_meet_host_selection() * and sched_check_host_satisfy() (both resource.c, unmodified by the * rewrap) -- and asserts the allocator's real behavior against that * derivation. This is valid because both the old and new code paths are * confirmed (by reading resource.c) to funnel every host decision through * the exact same leaf, sched_try_alloc_on_host_(): pre-rewrap it was * `static`/va_list-based and reached via sched_try_alloc_on_hosts(); post- * rewrap (task 3.1) it is the same logic, exported with real parameters. * Byte-for-byte identical body (confirmed via `git show b18c5f78^: * src/scheduler/src/resource.c` against the current file) -- the ONLY * thing that changed is how candidate hosts are fed into it: an inline * sched_collect_workers()+xlist_find() scan before, xce_host_iter_t's * lazy iter_make()/iter_next()-driven scan (the SAME underlying RTTI * primitive over the SAME sched->core.workers var -- see host_iter.c's own * file header) after. So "identical host selection" reduces exactly to * "the new host_iter-driven loop still visits candidates in * iter_make()/iter_next() order and still picks the first one * sched_try_alloc_on_host_() accepts" -- which is what each test below * independently drives and checks, without assuming it. * * Covers (see design.md's Testing Strategy for * test_seqjob_rewrap_equivalence.c): * 1. Multiple candidate hosts, only one resource-satisfies -- the * correct one is chosen, and ONLY it carries a provisional debit. * 2. A host that fails the SELECT filter specifically, despite having * abundant free resources -- must still be skipped. * 3. Iteration-order sensitivity: with two equally-valid hosts, the pick * must match whichever host sched->core.workers' own * iter_make()/iter_next() (the identical primitive both the deleted * pre-rewrap loop and today's host_iter.c use) visits first -- * derived directly from that primitive in the test itself, not * assumed. * 4. A select-failing host, then a resource-failing host, then a * succeeding host, in that order -- proves the loop doesn't * short-circuit or mis-skip when back-to-back failures of two * DIFFERENT kinds precede the eventual success. * * White-box #include convention: identical combination * test_seqjob_allocator.c (task 3.1) already established (core.c + * resource.c + schedobj.c + host_iter.c + plugins/seqjob_allocator.c), * plus directly for iter_make()/iter_next()/iter_destroy() (test * 3's own grounding of "iteration order"), and test_host_iter.c's * make_rack_eq_select() helper (adapted for a resmgr-assigned residx * rather than a hardcoded one, since this file -- like * test_seqjob_allocator.c, unlike test_host_iter.c -- runs the REAL * seqjob_prepare_allocation(), whose key_find(resmgr, "slots", ...) call * needs a genuinely registered "slots" resource). */ #include #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 -- same * combination test_seqjob_allocator.c (task 3.1) already established. */ #include "../src/core.c" #include "../src/resource.c" #include "../src/schedobj.c" #include "../src/host_iter.c" #include "../src/alloc_runner.c" /* Hooked sources compile SCHED_TRACE bodies (scheduler-sim-decision-trace). */ #include "../src/sched_trace.c" #include "../src/sched_trace_scan.c" #include "../src/sched_trace_capture.c" #include "../src/sched_trace_outcome.c" #include "../src/plugins/seqjob_allocator.c" /* ---------------------------------------------------------------------- */ /* Link-satisfying stubs (identical set/rationale to test_seqjob_allocator.c) */ /* ---------------------------------------------------------------------- */ int plugin_register(void* sched, void* plugin) { (void)sched; (void)plugin; return 0; } void plugin_unregister(void* arg) { (void)arg; } /* 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 -- exactly one (seqjob's own) in this file's * fixtures. See test_seqjob_allocator.c's identical stub for the full * xvec_idval_fini()/init() reset-before-fan-out rationale. */ 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; } /* Same rationale as test_seqjob_allocator.c's identical stub: avoids * white-box-including the rest of scheduler.c just to build the leaf * xce_alloc_t records the runner's success path constructs. */ 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 */ /* ---------------------------------------------------------------------- */ /* register_slots_resource - the real makeResource() (resobj.c), giving * "slots" a resmgr-assigned idx -- identical to test_seqjob_allocator.c's * own helper. */ 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). * Identical to test_seqjob_allocator.c's own 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; } /* add_worker_with_slots_and_rack - same as above, plus a second, numeric * "rack" resource value (never staged as a want -- consumed only by the * select expression, matching test_host_iter.c's own add_worker() shape) * so select-filter tests can distinguish hosts by something other than * resource availability. */ static xce_sched_worker_t* add_worker_with_slots_and_rack(xce_sched_core_t* core, const char* name, int slots_idx, float slots_total, int rack_idx, double rack) { xce_sched_worker_t* w = add_worker_with_slots(core, name, slots_idx, slots_total); int idx[1]; float val[1]; idx[0] = rack_idx; val[0] = (float)rack; put_resvals(&(w->resvals), 1, idx, val); return w; } /* make_rack_eq_select - builds `rack == literal` (SEL_EQ over a numeric * SEL_RESNAME leaf and a SEL_NUM literal), the same hand-built tree shape * test_host_iter.c's own helper uses, generalized to take a resmgr- * assigned rack_idx instead of a hardcoded one. */ static void make_rack_eq_select(xce_select_node_t* resname, xce_select_node_t* num, xce_select_node_t* eq, xce_select_t* select, double literal, int rack_idx) { memset(resname, 0, sizeof(*resname)); resname->type = SEL_RESNAME; resname->u.resname.res_idx = rack_idx; resname->u.resname.res_type = RES_TYPE_NUMERIC; memset(num, 0, sizeof(*num)); num->type = SEL_NUM; num->u.numval = literal; memset(eq, 0, sizeof(*eq)); eq->type = SEL_EQ; eq->u.binop.left = resname; eq->u.binop.right = num; select->root = eq; } 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; } /* drive_allocator - the exact production sequence a job's allocation * attempt runs: sched_ctx_init() -> sched_ctx_set_primary() (the real * call that stages ctx->alloc_req.shared via plugin_prepare_allocation()'s * ctx->worker==NULL branch, exactly as sched_make_primary_allocation() * does today) -> seqjob's registered entry point, the framework routine * sched_allocator_run(). Returns its return code; ctx is left * initialized (caller must sched_ctx_fini()). */ static int drive_allocator(xce_scheduler_t* sched, xce_sched_obj_t* obj, xce_primary_resreq_t* primary, xce_sched_ctx_t* ctx) { xce_sched_plugin_t plugin; /* 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)); sched_ctx_set_primary(ctx, primary); memset(&plugin, 0, sizeof(plugin)); plugin.sched = sched; /* the runner reads plugin->core (honestly typed) for every core-safe * call, plugin->sched (sched_make_allocs(), scheduler.c) and seqjob's * allocator contract. */ plugin.core = &(sched->core); plugin.allocator_iface = &seqjob_alloc_iface; return sched_allocator_run(&plugin, ctx); } /* assert_uncommitted_host_debit - reads ctx->uncomitted.hosts[w->idx] for * resource residx and asserts it equals `expected`. Same accessor pattern * test_resource_accounting.c's own debit test uses. */ static void assert_uncommitted_host_debit(xce_sched_ctx_t* ctx, xce_sched_worker_t* w, int residx, float expected) { xvec_t* uncomit = NULL; int rc; rc = sched_ctx_get_uncomit_host(&uncomit, ctx, w->idx); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_NOT_NULL(uncomit); TEST_ASSERT_EQUAL_FLOAT(expected, ((float*)uncomit->data)[residx]); } /* ---------------------------------------------------------------------- */ /* Tests */ /* ---------------------------------------------------------------------- */ /* * Req 9.1, fixture 1: three candidate hosts, two of which fail the * resource-availability check and one of which satisfies -- the * satisfying host must be chosen, and it alone carries the provisional * debit. Hosts are inserted in an order that requires the loop to try (and * correctly reject) two failing candidates before reaching the one that * works -- a loop that stopped after the first failure, or that debited * the wrong host, would fail this test. */ static void test_three_hosts_only_resource_satisfying_host_is_chosen_and_debited(void) { xce_scheduler_t sched; xce_resource_t* slots_res; xce_sched_worker_t* host_a; xce_sched_worker_t* host_b; xce_sched_worker_t* host_c; xce_sched_obj_t obj; xce_primary_schedobj_t primary_obj; xce_primary_resreq_t primary; xce_sched_ctx_t ctx; 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); host_a = add_worker_with_slots(&(sched.core), "host-a", slots_res->idx, 0.0f); /* no free slots -- resource-fails */ host_b = add_worker_with_slots(&(sched.core), "host-b", slots_res->idx, 0.0f); /* no free slots -- resource-fails */ host_c = add_worker_with_slots(&(sched.core), "host-c", slots_res->idx, 4.0f); /* plenty -- satisfies */ memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = NULL; init_primary_obj(&obj, &primary_obj, &primary); rc = drive_allocator(&sched, &obj, &primary, &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[0]); TEST_ASSERT_EQUAL_STRING("host-c", ctx.alloc->allocs[0]->host); assert_uncommitted_host_debit(&ctx, host_c, slots_res->idx, 1.0f); assert_uncommitted_host_debit(&ctx, host_a, slots_res->idx, 0.0f); assert_uncommitted_host_debit(&ctx, host_b, slots_res->idx, 0.0f); sched_ctx_fini(&ctx); } /* * Req 9.1, fixture 2: a host that fails the SELECT filter specifically * (not resource availability -- it has abundant free slots) must still be * skipped in favor of a host that both matches the select expression and * satisfies resources. This is the exact case task 3.2's own task text * and design.md's Testing Strategy call out by name: a test that never * exercises a select-filter failure would not actually prove equivalence. */ static void test_host_failing_select_filter_is_skipped_despite_plenty_of_resources(void) { xce_scheduler_t sched; xce_resource_t* slots_res; int rack_idx; xce_sched_worker_t* rack_nomatch; xce_sched_worker_t* rack_match; xce_sched_obj_t obj; xce_primary_schedobj_t primary_obj; xce_primary_resreq_t primary; xce_select_node_t resname, num, eq; xce_select_t select; xce_sched_ctx_t ctx; 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); rack_idx = slots_res->idx + 1; /* rack-nomatch: 8 free slots (far more than the 1-slot want a * maxTasks==1 job stages) but the wrong rack -- inserted FIRST so a * regression that checked resource availability before (or instead * of) the select expression would wrongly pick it. */ rack_nomatch = add_worker_with_slots_and_rack( &(sched.core), "rack-nomatch", slots_res->idx, 8.0f, rack_idx, 2.0); rack_match = add_worker_with_slots_and_rack( &(sched.core), "rack-match", slots_res->idx, 4.0f, rack_idx, 1.0); make_rack_eq_select(&resname, &num, &eq, &select, 1.0, rack_idx); memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = &select; init_primary_obj(&obj, &primary_obj, &primary); rc = drive_allocator(&sched, &obj, &primary, &ctx); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_NOT_NULL(ctx.alloc); TEST_ASSERT_EQUAL_STRING("rack-match", ctx.alloc->allocs[0]->host); assert_uncommitted_host_debit(&ctx, rack_match, slots_res->idx, 1.0f); assert_uncommitted_host_debit(&ctx, rack_nomatch, slots_res->idx, 0.0f); sched_ctx_fini(&ctx); } /* * Req 9.1, fixture 3 (iteration-order sensitivity): two hosts that are * BOTH select-matching and resource-satisfying -- the allocator must pick * whichever one sched->core.workers' own iteration mechanism visits * FIRST. That mechanism -- iter_make()/iter_next() over the RTTI `var` * sched->core.workers -- is the exact same primitive the deleted * pre-rewrap sched_collect_workers() used (confirmed via `git show * b18c5f78^:src/scheduler/src/scheduler.c`) and the one host_iter.c uses * today (host_iter.c's own file header comment): same underlying object, * same traversal call, so "first in that order" is definitionally * identical pre- and post-rewrap. This test derives that order directly * (an independent iter_make()/iter_next() scan below) rather than * assuming worker-insertion order happens to match it, then asserts * the allocator's pick against the derived value. */ static void test_pick_matches_first_host_in_the_shared_workers_iteration_order(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; iterator_t it; xce_sched_worker_t* first_in_order = NULL; 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); /* Two equally-valid hosts -- neither the select expression (unset) * nor resource availability (both have plenty) distinguishes them. */ add_worker_with_slots(&(sched.core), "host-1", slots_res->idx, 4.0f); add_worker_with_slots(&(sched.core), "host-2", slots_res->idx, 4.0f); /* Ground truth: the same RTTI iteration primitive host_iter.c (and, * pre-rewrap, sched_collect_workers()) uses over the same * sched.core.workers var. */ rc = iter_make(&it, sched.core.workers); TEST_ASSERT_TRUE(rc >= 0); rc = iter_next(&it, (void**)&first_in_order); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(first_in_order); iter_destroy(&it); memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = NULL; init_primary_obj(&obj, &primary_obj, &primary); rc = drive_allocator(&sched, &obj, &primary, &ctx); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_NOT_NULL(ctx.alloc); TEST_ASSERT_EQUAL_STRING(first_in_order->name, ctx.alloc->allocs[0]->host); sched_ctx_fini(&ctx); } /* * Req 9.1, fixture 4: a select-failing host, THEN a resource-failing * host, THEN a succeeding host, in that exact order -- proves the * composition correctly skips two DIFFERENT kinds of failure back to back * without short-circuiting (stopping early) or mis-skipping (e.g. * skipping past the eventual success too). */ static void test_select_fail_then_resource_fail_then_success_does_not_short_circuit(void) { xce_scheduler_t sched; xce_resource_t* slots_res; int rack_idx; xce_sched_worker_t* sel_fail; xce_sched_worker_t* res_fail; xce_sched_worker_t* success; xce_sched_obj_t obj; xce_primary_schedobj_t primary_obj; xce_primary_resreq_t primary; xce_select_node_t resname, num, eq; xce_select_t select; xce_sched_ctx_t ctx; 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); rack_idx = slots_res->idx + 1; sel_fail = add_worker_with_slots_and_rack(&(sched.core), "sel-fail", slots_res->idx, 8.0f, rack_idx, 2.0 /* wrong rack */); res_fail = add_worker_with_slots_and_rack(&(sched.core), "res-fail", slots_res->idx, 0.0f, rack_idx, 1.0 /* right rack, no slots */); success = add_worker_with_slots_and_rack(&(sched.core), "success", slots_res->idx, 4.0f, rack_idx, 1.0 /* right rack, plenty */); make_rack_eq_select(&resname, &num, &eq, &select, 1.0, rack_idx); memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = &select; init_primary_obj(&obj, &primary_obj, &primary); rc = drive_allocator(&sched, &obj, &primary, &ctx); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_NOT_NULL(ctx.alloc); TEST_ASSERT_EQUAL_STRING("success", ctx.alloc->allocs[0]->host); assert_uncommitted_host_debit(&ctx, success, slots_res->idx, 1.0f); assert_uncommitted_host_debit(&ctx, sel_fail, slots_res->idx, 0.0f); assert_uncommitted_host_debit(&ctx, res_fail, slots_res->idx, 0.0f); sched_ctx_fini(&ctx); } int main(void) { UNITY_BEGIN(); RUN_TEST( test_three_hosts_only_resource_satisfying_host_is_chosen_and_debited); RUN_TEST( test_host_failing_select_filter_is_skipped_despite_plenty_of_resources); RUN_TEST( test_pick_matches_first_host_in_the_shared_workers_iteration_order); RUN_TEST( test_select_fail_then_resource_fail_then_success_does_not_short_circuit); return UNITY_END(); }