/* * test_host_iter.c - contract + behavioral test for the one shared * xce_host_iter_t implementation (scheduler-allocation spec, task 2.1, * Req 3 AC 1-6). * * Unlike test_alloc_plan_iter.c (task 1.1), which proved the CONTRACT * SHAPE compiles and is usable via trivial no-op stub implementations, * this file drives the REAL host_iter.c against real fixture state: * real xce_sched_core_t workers (../src/core.c), the real select- * expression evaluator + sched_check_host_satisfy() (../src/resource.c), * and the real sched_ctx_get_uncomit_host() accessor (../src/schedobj.c) * -- the exact same white-box #include combination * test_resource_accounting.c already established for this fixture shape * (core.c + resource.c + schedobj.c), plus the new ../src/host_iter.c * under test. * * Covers: * - select-expression filtering (a host failing the select expression * is never returned) -- Req 3 AC 2. * - availability filtering (a host without enough resources is never * returned) -- Req 3 AC 2. * - exhaustion, stable across repeated calls -- Req 3 AC 3. * - the fresh-per-bucket adversarial case (Req 3 AC 4, Req 4 AC 2): a * host too small for one candidate search remains a valid candidate * for a later, smaller search using a freshly-initialized host_iter * instance over the SAME underlying worker set -- proving this * implementation carries no persistent "consumed"/"rejected" state * across instances. This is the single most important test in this * file (design.md's own words for this exact case). * - Req 3 AC 6: next() calls pthread_testcancel() (or an equivalent * real cancellation point) -- proved behaviorally by cancelling a * thread parked in a tight next()-calling loop. * * plan->ctx (the small, documented addition to xce_alloc_plan_t this * task made -- see alloc_iter.h) is how each test wires a scheduler + * select expression + resource want into the plan host_iter consumes; * see host_iter.c's own file header comment for why init()'s existing * three parameters cannot reach that state on their own. */ #include #include #include #include #include #include #include "resreq.h" #include "sched/alloc_iter.h" #include "scheduler.h" /* scheduler-sim-live-allocation spec, task 2.1: sim.h only depends on * core.h (+ stdbool/stdint/stdio, per its own header comment), so it is * safe to pull in here just for xce_sched_sim_driver_t -- needed to build * a simulation-mode-shaped container for the driver-parity test added by * this task (mirrors test.c's own test_core_safe_allocation_chain_driver_ * parity(), task 1.3). */ #include "sim.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 evaluator + availability check under test), the real * sched_ctx_init()/sched_ctx_get_uncomit_host() (schedobj.c), and the * new host_iter.c implementation under test into this TU -- same * white-box convention test_resource_accounting.c already established * for this exact core.c+resource.c+schedobj.c combination. */ #include "../src/core.c" #include "../src/resource.c" #include "../src/schedobj.c" #include "../src/host_iter.c" /* Link-satisfying stubs -- identical set to test_resource_accounting.c's * own (see that file's header comment for the full rationale). None of * these are ever actually invoked by this file's tests: this file never * calls sched_ctx_set_primary() (it sets ctx.primary directly, a plain * field assignment, to avoid the plugin_prepare_allocation() side effect * entirely) nor sched_try_alloc_on_hosts()/sched_update_existing_*(). */ void plugin_unregister(void* arg) { (void)arg; } int plugin_prepare_allocation(xce_sched_core_t* s, xce_sched_ctx_t* c) { (void)s; (void)c; return 0; } 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; } void setUp(void) { return; } void tearDown(void) { return; } /* ---------------------------------------------------------------------- */ /* Fixture helpers */ /* ---------------------------------------------------------------------- */ #define SLOTS_RESIDX 0 #define RACK_RESIDX 1 /* * add_worker - a real sched_add_worker() (core.c) worker with a "slots" * resource (total = slots_total, in-use = slots_inuse) and a "rack" * resource (numeric), both set via the real put_resvals() (resource.c). */ static void add_worker(xce_sched_core_t* core, const char* name, float slots_total, float slots_inuse, float rack) { 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_RESIDX; val[0] = slots_total; put_resvals(&(w->resvals), 1, idx, val); val[0] = slots_inuse; put_resvals(&(w->resInUse), 1, idx, val); idx[0] = RACK_RESIDX; val[0] = rack; put_resvals(&(w->resvals), 1, idx, val); rc = sched_add_worker(core, w); TEST_ASSERT_TRUE(rc >= 0); } /* make_rack_eq_select - builds `rack == literal`, a real, evaluator- * exercised select expression (SEL_EQ over a numeric SEL_RESNAME leaf and * a SEL_NUM literal) -- no parser needed, eval_select_simple() (used via * sched_meet_host_selection()'s no-sel_eval_ctx fallback path, since this * fixture's xce_scheduler_t never sets sched.core.sel_eval_ctx) walks * this hand-built tree directly. */ 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) { memset(resname, 0, sizeof(*resname)); resname->type = SEL_RESNAME; resname->u.resname.res_idx = RACK_RESIDX; 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; } /* init_ctx - a real sched_ctx_init() (schedobj.c), with ctx->primary set * by plain field assignment (not sched_ctx_set_primary(), whose * plugin_prepare_allocation() side effect this file's tests don't need * or want -- see the stub's own comment above). */ static void init_ctx(xce_sched_ctx_t* ctx, xce_scheduler_t* sched, xce_sched_obj_t* obj, xce_primary_resreq_t* primary) { /* scheduler-sim-live-allocation spec, task 2.1: ctx->sched is now * honestly typed xce_sched_core_t* -- pass &(sched->core), not sched. */ int rc = sched_ctx_init(ctx, &(sched->core), obj); TEST_ASSERT_EQUAL_INT(0, rc); ctx->primary = primary; } static void init_primary_obj(xce_sched_obj_t* obj) { memset(obj, 0, sizeof(*obj)); obj->tag = XCE_RESREQ_PRIMARY; } /* set_wanted_slots - stages a "slots" resource want into ctx->alloc_req.host * -- the vector whichever allocator's prepare_allocation_hook would * normally populate; this file populates it directly since no allocator * plugin is wired in this fixture (host_iter.c never populates it * itself, by design). */ static void set_wanted_slots(xce_sched_ctx_t* ctx, float slots_wanted) { int rc = xvec_idval_store(&(ctx->alloc_req.host), SLOTS_RESIDX, slots_wanted); TEST_ASSERT_TRUE(rc >= 0); } static void bind_host_iter(xce_host_iter_t* iter) { memset(iter, 0, sizeof(*iter)); iter->init = sched_host_iter_init; iter->next = sched_host_iter_next; iter->fini = sched_host_iter_fini; } #define MAX_COLLECT 16 static int collect_all(xce_host_iter_t* iter, char names[][64]) { int n = 0; xce_sched_worker_t* w; while ((n < MAX_COLLECT) && ((w = iter->next(iter)) != NULL)) { strncpy(names[n], w->name, 63); names[n][63] = '\0'; n++; } return n; } static int contains(char names[][64], int n, const char* target) { int i; for (i = 0; i < n; i++) { if (strcmp(names[i], target) == 0) { return 1; } } return 0; } /* ---------------------------------------------------------------------- */ /* Tests */ /* ---------------------------------------------------------------------- */ /* * Req 3 AC 2 (select half): a host failing the select expression is never * returned, regardless of having plenty of available slots. */ static void test_select_expression_filtering_excludes_non_matching_host(void) { xce_scheduler_t sched; xce_sched_ctx_t ctx; xce_sched_obj_t obj; xce_primary_resreq_t primary; xce_select_node_t resname, num, eq; xce_select_t select; xce_host_iter_t host_iter_storage; xce_host_iter_t* host_iter = &host_iter_storage; int bucket_slots[1] = { 1 }; xce_alloc_plan_t plan; char names[MAX_COLLECT][64]; int n; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); add_worker(&(sched.core), "rack-match", 4.0f, 0.0f, 1.0); add_worker(&(sched.core), "rack-nomatch", 4.0f, 0.0f, 2.0); make_rack_eq_select(&resname, &num, &eq, &select, 1.0); memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = &select; init_primary_obj(&obj); init_ctx(&ctx, &sched, &obj, &primary); /* No slots requirement staged -- isolates select filtering from * availability filtering for this test. */ memset(&plan, 0, sizeof(plan)); plan.nbuckets = 1; plan.bucket_slots = bucket_slots; plan.ctx = &ctx; bind_host_iter(host_iter); TEST_ASSERT_EQUAL_INT( 0, host_iter->init(host_iter, &plan, sched.core.workers)); n = collect_all(host_iter, names); TEST_ASSERT_TRUE(contains(names, n, "rack-match")); TEST_ASSERT_FALSE(contains(names, n, "rack-nomatch")); host_iter->fini(host_iter); TEST_ASSERT_NULL(host_iter->private); } /* * Req 3 AC 2 (availability half): a host without enough available slots * is never returned, even when it passes the select expression cleanly * (no select set at all, here). */ static void test_availability_filtering_excludes_too_small_host(void) { xce_scheduler_t sched; xce_sched_ctx_t ctx; xce_sched_obj_t obj; xce_primary_resreq_t primary; xce_host_iter_t host_iter_storage; xce_host_iter_t* host_iter = &host_iter_storage; int bucket_slots[1] = { 3 }; xce_alloc_plan_t plan; char names[MAX_COLLECT][64]; int n; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); add_worker(&(sched.core), "big", 4.0f, 0.0f, 0.0); add_worker(&(sched.core), "small", 1.0f, 0.0f, 0.0); memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = NULL; /* no select filter -- isolates availability */ init_primary_obj(&obj); init_ctx(&ctx, &sched, &obj, &primary); set_wanted_slots(&ctx, 3.0f); /* "small" (1 free) cannot satisfy this */ memset(&plan, 0, sizeof(plan)); plan.nbuckets = 1; plan.bucket_slots = bucket_slots; plan.ctx = &ctx; bind_host_iter(host_iter); TEST_ASSERT_EQUAL_INT( 0, host_iter->init(host_iter, &plan, sched.core.workers)); n = collect_all(host_iter, names); TEST_ASSERT_TRUE(contains(names, n, "big")); TEST_ASSERT_FALSE(contains(names, n, "small")); host_iter->fini(host_iter); } /* * Req 3 AC 3: next() returns NULL once exhausted, and this signal is * stable across repeated calls on the same instance. */ static void test_exhaustion_is_stable_across_repeated_calls(void) { xce_scheduler_t sched; xce_sched_ctx_t ctx; xce_sched_obj_t obj; xce_primary_resreq_t primary; xce_host_iter_t host_iter_storage; xce_host_iter_t* host_iter = &host_iter_storage; int bucket_slots[1] = { 1 }; xce_alloc_plan_t plan; xce_sched_worker_t* w; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); add_worker(&(sched.core), "only", 4.0f, 0.0f, 0.0); memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = NULL; init_primary_obj(&obj); init_ctx(&ctx, &sched, &obj, &primary); set_wanted_slots(&ctx, 1.0f); memset(&plan, 0, sizeof(plan)); plan.nbuckets = 1; plan.bucket_slots = bucket_slots; plan.ctx = &ctx; bind_host_iter(host_iter); TEST_ASSERT_EQUAL_INT( 0, host_iter->init(host_iter, &plan, sched.core.workers)); w = host_iter->next(host_iter); TEST_ASSERT_NOT_NULL(w); TEST_ASSERT_EQUAL_STRING("only", w->name); TEST_ASSERT_NULL(host_iter->next(host_iter)); TEST_ASSERT_NULL(host_iter->next(host_iter)); TEST_ASSERT_NULL(host_iter->next(host_iter)); host_iter->fini(host_iter); } /* * THE critical adversarial case (Req 3 AC 4, Req 4 AC 2; design.md calls * this "the single most important test in this spec"): a host too small * for one search remains a valid candidate for a LATER, smaller search * using a freshly-initialized xce_host_iter_t instance over the SAME * underlying worker set -- proving this implementation carries no * persistent "consumed"/"rejected" state across instances (which is * exactly what would happen if a plan's buckets wrongly shared one * cursor instead of each getting a fresh host_iter, per requirements.md's * Composition Algorithm). */ static void test_host_too_small_for_larger_search_remains_candidate_for_smaller_search( void) { xce_scheduler_t sched; char names_large[MAX_COLLECT][64]; char names_small[MAX_COLLECT][64]; int n_large, n_small; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); /* "big" has 4 free slots, "small" has only 1 -- the same underlying * worker set is reused, unmodified, across both searches below. */ add_worker(&(sched.core), "big", 4.0f, 0.0f, 0.0); add_worker(&(sched.core), "small", 1.0f, 0.0f, 0.0); /* --- Search 1: a LARGE want (3 slots) -- "small" must be rejected --- */ { xce_sched_ctx_t ctx1; xce_sched_obj_t obj1; xce_primary_resreq_t primary1; xce_host_iter_t host_iter_storage; xce_host_iter_t* host_iter = &host_iter_storage; int bucket_slots[1] = { 3 }; xce_alloc_plan_t plan; memset(&primary1, 0, sizeof(primary1)); primary1.minTasks = 1; primary1.maxTasks = 1; primary1.select = NULL; init_primary_obj(&obj1); init_ctx(&ctx1, &sched, &obj1, &primary1); set_wanted_slots(&ctx1, 3.0f); memset(&plan, 0, sizeof(plan)); plan.nbuckets = 1; plan.bucket_slots = bucket_slots; plan.ctx = &ctx1; bind_host_iter(host_iter); TEST_ASSERT_EQUAL_INT( 0, host_iter->init(host_iter, &plan, sched.core.workers)); n_large = collect_all(host_iter, names_large); host_iter->fini(host_iter); sched_ctx_fini(&ctx1); } TEST_ASSERT_TRUE(contains(names_large, n_large, "big")); TEST_ASSERT_FALSE(contains(names_large, n_large, "small")); /* --- Search 2: a fresh xce_host_iter_t instance, a SMALLER want (1 * slot), over the SAME sched.core.workers -- "small" must now be * found. If host_iter carried any persistent "rejected by an earlier * search" state, this would wrongly fail. --- */ { xce_sched_ctx_t ctx2; xce_sched_obj_t obj2; xce_primary_resreq_t primary2; xce_host_iter_t host_iter_storage; xce_host_iter_t* host_iter = &host_iter_storage; int bucket_slots[1] = { 1 }; xce_alloc_plan_t plan; memset(&primary2, 0, sizeof(primary2)); primary2.minTasks = 1; primary2.maxTasks = 1; primary2.select = NULL; init_primary_obj(&obj2); init_ctx(&ctx2, &sched, &obj2, &primary2); set_wanted_slots(&ctx2, 1.0f); memset(&plan, 0, sizeof(plan)); plan.nbuckets = 1; plan.bucket_slots = bucket_slots; plan.ctx = &ctx2; bind_host_iter(host_iter); TEST_ASSERT_EQUAL_INT( 0, host_iter->init(host_iter, &plan, sched.core.workers)); n_small = collect_all(host_iter, names_small); host_iter->fini(host_iter); sched_ctx_fini(&ctx2); } TEST_ASSERT_TRUE(contains(names_small, n_small, "small")); TEST_ASSERT_TRUE(contains(names_small, n_small, "big")); } /* * Req 3 AC 6: next() calls pthread_testcancel() (or an equivalent real * cancellation point) at least once per invocation. Proved behaviorally: * a worker thread parked in a tight loop calling next() repeatedly is * reliably torn down by pthread_cancel() from the main thread. If * host_iter_next() had no real cancellation point, this tight loop would * never yield to the pending cancellation and pthread_join() below would * hang (timing out the whole test binary) rather than return. */ typedef struct { xce_host_iter_t* host_iter; } cancel_thread_arg_t; static void* cancel_spin_thread(void* argp) { cancel_thread_arg_t* arg = (cancel_thread_arg_t*)argp; for (;;) { (void)arg->host_iter->next(arg->host_iter); } return NULL; /* unreachable */ } static void test_next_has_a_real_cancellation_point(void) { xce_scheduler_t sched; xce_sched_ctx_t ctx; xce_sched_obj_t obj; xce_primary_resreq_t primary; xce_host_iter_t host_iter_storage; xce_host_iter_t* host_iter = &host_iter_storage; int bucket_slots[1] = { 1 }; xce_alloc_plan_t plan; cancel_thread_arg_t arg; pthread_t tid; int rc; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); add_worker(&(sched.core), "only", 4.0f, 0.0f, 0.0); memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = NULL; init_primary_obj(&obj); init_ctx(&ctx, &sched, &obj, &primary); set_wanted_slots(&ctx, 1.0f); memset(&plan, 0, sizeof(plan)); plan.nbuckets = 1; plan.bucket_slots = bucket_slots; plan.ctx = &ctx; bind_host_iter(host_iter); TEST_ASSERT_EQUAL_INT( 0, host_iter->init(host_iter, &plan, sched.core.workers)); arg.host_iter = host_iter; rc = pthread_create(&tid, NULL, cancel_spin_thread, &arg); TEST_ASSERT_EQUAL_INT(0, rc); /* Let the spin thread actually get running before cancelling it. */ usleep(20000); rc = pthread_cancel(tid); TEST_ASSERT_EQUAL_INT(0, rc); rc = pthread_join(tid, NULL); TEST_ASSERT_EQUAL_INT(0, rc); /* Safe post-cancellation cleanup: pthread_testcancel() only ever * fires between complete logical steps (function entry, or right * before the next iter_next() call), never mid-mutation, so the * iterator's private state is always well-formed here regardless of * exactly where cancellation landed. */ host_iter->fini(host_iter); sched_ctx_fini(&ctx); } /* * init() defensively rejects a plan with no ctx -- this implementation's * only way of reaching the scheduler/select/resource-want state it needs * (see alloc_iter.h's xce_alloc_plan_t.ctx doc comment). */ static void test_init_rejects_missing_ctx(void) { xce_host_iter_t host_iter_storage; xce_host_iter_t* host_iter = &host_iter_storage; int bucket_slots[1] = { 1 }; xce_alloc_plan_t plan; memset(&plan, 0, sizeof(plan)); plan.nbuckets = 1; plan.bucket_slots = bucket_slots; plan.ctx = NULL; bind_host_iter(host_iter); TEST_ASSERT_EQUAL_INT(-EINVAL, host_iter->init(host_iter, &plan, NULL)); TEST_ASSERT_EQUAL_INT(-EINVAL, host_iter->init(host_iter, NULL, NULL)); } /* * scheduler-sim-live-allocation spec, task 2.1 (Core-Safe Allocation * Chain): sched_try_alloc_on_host_() (resource.c) -- the per-host * allocation-attempt leaf that both host_iter.c's own capacity checks * ultimately mirror and that parjob_allocator.c's/seqjob_allocator.c's * my_alloc() call once per candidate -- is now core-typed. This drives it * end to end (real sched_ctx_get_uncomit_host()/sched_check_host_satisfy(), * schedobj.c+resource.c, this file's own plugin_prepare_allocation() no-op * stub above) against BOTH a normal-mode-shaped (xce_scheduler_t) and a * simulation-mode-shaped (xce_sched_sim_driver_t) core container, proving * identical grant/deny results regardless of which driver's core state is * passed in -- mirrors test.c's own * test_core_safe_allocation_chain_driver_parity() (task 1.3) for the one * function that test doesn't itself cover. * * RED (before task 2.1): fails to compile -- sched_try_alloc_on_host_() * still takes xce_scheduler_t*, so passing xce_sched_core_t* directly is * an incompatible-pointer-type error (this toolchain treats * -Wincompatible-pointer-types as fatal, not a warning). */ static int try_alloc_once(xce_sched_core_t* core, xce_sched_worker_t* w, float host_wanted) { xce_sched_ctx_t ctx; xce_sched_obj_t obj; xce_primary_resreq_t primary; int rc; memset(&primary, 0, sizeof(primary)); primary.minTasks = 1; primary.maxTasks = 1; primary.select = NULL; init_primary_obj(&obj); TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&ctx, core, &obj)); ctx.primary = &primary; /* sched_try_alloc_on_host_()'s resource-unmet branch assumes * ctx->reason already exists (unlike its select-mismatch branch, which * lazily creates one) -- my_alloc() always creates one before ever * reaching this leaf in production; this test calls the leaf directly, * bypassing that step, so it must set one up itself. Same pattern * test_parjob_combinatorial.c's own direct parjob_try_plan_() call * already established for the identical reason. */ ctx.reason = make_pendreason(0); TEST_ASSERT_NOT_NULL(ctx.reason); set_wanted_slots(&ctx, host_wanted); rc = sched_try_alloc_on_host_(w, core, &ctx); sched_ctx_fini(&ctx); return rc; } static void test_try_alloc_on_host_grants_and_denies_identically_for_both_driver_shapes( void) { xce_scheduler_t sched_normal; xce_sched_sim_driver_t sim_shape; xce_sched_core_t* core_normal = &(sched_normal.core); xce_sched_core_t* core_sim = &(sim_shape.core); xce_sched_worker_t* w_normal; xce_sched_worker_t* w_sim; int rc_normal, rc_sim; memset(&sched_normal, 0, sizeof(sched_normal)); memset(&sim_shape, 0, sizeof(sim_shape)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(core_normal)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(core_sim)); add_worker(core_normal, "only", 4.0f, 0.0f, 0.0); add_worker(core_sim, "only", 4.0f, 0.0f, 0.0); w_normal = sched_get_worker(core_normal, "only"); w_sim = sched_get_worker(core_sim, "only"); TEST_ASSERT_NOT_NULL(w_normal); TEST_ASSERT_NOT_NULL(w_sim); /* Grant: wants 3 of 4 free slots. */ rc_normal = try_alloc_once(core_normal, w_normal, 3.0f); rc_sim = try_alloc_once(core_sim, w_sim, 3.0f); TEST_ASSERT_EQUAL_INT(1, rc_normal); TEST_ASSERT_EQUAL_INT_MESSAGE( rc_normal, rc_sim, "sched_try_alloc_on_host_() must grant identically for a " "normal-mode-shaped and a simulation-mode-shaped core"); /* Deny: wants 8, only 4 free -- the per-ctx uncommitted debit tracked * by the grant above never touches the worker's own resInUse, so this * second, fresh-ctx attempt against the SAME worker still sees all 4 * free, exactly like test.c's own driver-parity test reuses one * worker across a grant then a deny sub-case. */ rc_normal = try_alloc_once(core_normal, w_normal, 8.0f); rc_sim = try_alloc_once(core_sim, w_sim, 8.0f); TEST_ASSERT_EQUAL_INT(0, rc_normal); TEST_ASSERT_EQUAL_INT_MESSAGE( rc_normal, rc_sim, "sched_try_alloc_on_host_() must deny identically for a " "normal-mode-shaped and a simulation-mode-shaped core"); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_select_expression_filtering_excludes_non_matching_host); RUN_TEST(test_availability_filtering_excludes_too_small_host); RUN_TEST(test_exhaustion_is_stable_across_repeated_calls); RUN_TEST( test_host_too_small_for_larger_search_remains_candidate_for_smaller_search); RUN_TEST(test_next_has_a_real_cancellation_point); RUN_TEST(test_init_rejects_missing_ctx); RUN_TEST( test_try_alloc_on_host_grants_and_denies_identically_for_both_driver_shapes); return UNITY_END(); }