Task 1.6: the 27 tests that #include a hooked scheduler source now also include sched_trace.c and sched_trace_capture.c. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
745 lines
26 KiB
C
745 lines
26 KiB
C
/*
|
|
* 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 <errno.h>
|
|
#include <pthread.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#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"
|
|
/* Hooked sources compile SCHED_TRACE bodies (scheduler-sim-decision-trace). */
|
|
#include "../src/sched_trace.c"
|
|
#include "../src/sched_trace_capture.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 the framework routine sched_allocator_run()
|
|
* calls 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) -- the runner 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 sched_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();
|
|
}
|