test(scheduler-allocator-runner): characterize the seqjob allocator (task 2.1)

12 tests drive the real seqjob plugin through job_alloc_fn and record
today's results, reasons and debits, with switch-stable OOM injection
(sched_make_allocs stub, --wrap of make_host_alloc/make_job_alloc).
Assertions the runner changes on purpose sit in *_today tests.

The characterization showed bug-1893's real effect: vget() returns 0 on
success, so the shared-reason loop (rc <= 0 skip) never records a shared
NO_RESOURCE reason. Bug record and spec wording corrected; the runner
skips only on rc < 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-30 08:11:26 +00:00
parent 31ad79d325
commit 91a8580948
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
8 changed files with 835 additions and 16 deletions

View file

@ -1714,6 +1714,21 @@ sched_seqjob_allocator_test = executable(
dependencies: [scheduler_test_deps],
install: false)
# scheduler-allocator-runner task 2.1 (Req 4.5, 5.4, 5.8, 7.2): seqjob
# characterization through the plugin's registered job_alloc_fn, against
# today's allocator and (from task 4.2) the framework runner. Same white-box
# stack as sched_seqjob_allocator_test. `--wrap` on libxce's
# make_host_alloc/make_job_alloc gives the test fail-on-call-N out-of-memory
# injection points that stay the same across the switch.
sched_alloc_equivalence_seqjob_test = executable(
'sched_alloc_equivalence_seqjob_test',
['test_alloc_equivalence_seqjob.c'],
include_directories:[incdir],
dependencies: [scheduler_test_deps],
link_args: ['-Wl,--wrap=make_host_alloc',
'-Wl,--wrap=make_job_alloc'],
install: false)
# Task 3.2 (scheduler-allocation spec, Req 9.1): the dedicated pre/post-
# rewrap host-selection EQUIVALENCE proof for seqjob_allocator.c's
# my_alloc() -- fixture states task 3.1's own smoke test does not cover
@ -1980,6 +1995,9 @@ test('scheduler sim cache clear isolation test',
test('scheduler seqjob allocator test',
sched_seqjob_allocator_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])
# Leak detection stays on: every fixture tears down its ctx, core and plugin.
test('scheduler alloc equivalence seqjob test',
sched_alloc_equivalence_seqjob_test, suite: 'scheduler')
test('scheduler seqjob rewrap equivalence test',
sched_seqjob_rewrap_equivalence_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -0,0 +1,794 @@
/*
* test_alloc_equivalence_seqjob.c - scheduler-allocator-runner task 2.1:
* characterization of the sequential-job allocator (Req 4.5, 5.4, 5.8,
* 7.2), driven only through the registered plugin->job_alloc_fn of
* seqjob_allocator_new_plugin(), never the static routine by name. The
* expectations must still hold once task 4.2 replaces that routine with the
* framework runner (one more white-box include below). Each fixture asserts
* result code, hosts, per-host records, shared records, per-host and shared
* uncommitted amounts and pend reasons.
*
* Out-of-memory injection (task 2.2 copies it), only at points that stay
* the same across the switch: the sched_make_allocs() stub failing on call
* N (1 = claimed host's records, 2 = shared records) and -Wl,--wrap of
* libxce make_host_alloc/make_job_alloc (meson.build) failing on call N.
* The host-array allocation is not injected (its origin changes; task 3.3).
*
* Kept out on purpose: multi-host pending (Req 3.7), more shared requests
* than short ones (bug-1893), COMPOUND layouts where the positional
* uncommitted read (bug-1892) matters. Values the switch changes on purpose
* (Req 5.5 host-stage debit, Req 2.2 shared reasons) sit in their own
* `_today` test functions.
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "preason.h"
#include "resobj.h"
#include "resreq.h"
#include "sched/alloc_iter.h"
#include "scheduler.h"
#include "xcealloc.h"
#include "xstring.h"
/* White-box sources under test. Task 4.2 adds the framework runner
* ("../src/alloc_runner.c") as one more line of this list. */
#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 (same convention as test_seqjob_allocator.c) */
/* ---------------------------------------------------------------------- */
int plugin_register(void* sched, void* plugin)
{
(void)sched;
(void)plugin;
return 0;
}
void plugin_unregister(void* arg) { (void)arg; }
/* Stands in for plugin.c's fan-out with exactly one plugin (seqjob's own
* prepare hook): reset the request vectors exactly as plugin.c's
* plugin_prepare_allocation() does (job-level pass: .shared and .host,
* bug-1883; per-host pass: .host), then call the hook. */
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);
xvec_idval_fini(&(ctx->alloc_req.host));
xvec_idval_init(&(ctx->alloc_req.host), 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;
fake_plugin.core = sched;
return seqjob_prepare_allocation(&fake_plugin, ctx);
}
int plugin_job_update(void* s, xce_sched_job_t* j)
{
return (void)s, (void)j, 0;
}
int plugin_queue_update(void* s, xce_sched_queue_t* q)
{
return (void)s, (void)q, 0;
}
int plugin_job_rusage_update(void* s, xce_sched_job_t* j, xce_job_rusages_t* r)
{
return (void)s, (void)j, (void)r, 0;
}
/* ---------------------------------------------------------------------- */
/* Out-of-memory injection */
/* ---------------------------------------------------------------------- */
/* Each counter counts every call; a fail_on value N > 0 makes call N fail
* (NULL, errno ENOMEM). 0 disarms. oom_injection_reset() clears all. */
static int g_make_allocs_calls = 0;
static int g_make_allocs_fail_on = 0;
static int g_host_alloc_calls = 0;
static int g_host_alloc_fail_on = 0;
static int g_job_alloc_calls = 0;
static int g_job_alloc_fail_on = 0;
static void oom_injection_reset(void)
{
g_make_allocs_calls = 0;
g_make_allocs_fail_on = 0;
g_host_alloc_calls = 0;
g_host_alloc_fail_on = 0;
g_job_alloc_calls = 0;
g_job_alloc_fail_on = 0;
}
/* Stub for scheduler.c's sched_make_allocs(): builds real records with
* make_alloc() (resource name from the resmgr, the requested value, empty
* reservation name, duration -1 as for a NULL duration array), and
* returns NULL for nres <= 0 as the real one does. instid is 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;
g_make_allocs_calls++;
if ((g_make_allocs_fail_on > 0)
&& (g_make_allocs_calls == g_make_allocs_fail_on)) {
errno = ENOMEM;
return NULL;
}
if ((sched == NULL) || (nres <= 0) || (idxs == NULL) || (vals == NULL)) {
errno = EINVAL;
return NULL;
}
allocs = (xce_alloc_t**)calloc(nres, sizeof(*allocs));
if (allocs == NULL) {
return NULL;
}
for (i = 0; i < nres; i++) {
xce_resource_t* r = NULL;
vget(sched->core.resmgr, idxs[i], (void**)&r);
if (r == NULL) {
continue;
}
allocs[i] = make_alloc(r->name, reserve, 0, vals[i],
(duration != NULL) ? duration[i] : -1);
if (allocs[i] == NULL) {
free_ResAlloc_list(nres, allocs);
errno = ENOMEM;
return NULL;
}
}
return allocs;
}
xce_host_alloc_t* __real_make_host_alloc(char*, int, xce_alloc_t**);
xce_job_alloc_t* __real_make_job_alloc(uint64_t, int, xce_host_alloc_t**);
xce_host_alloc_t* __wrap_make_host_alloc(char* host, int nalloc,
xce_alloc_t** allocs)
{
g_host_alloc_calls++;
if ((g_host_alloc_fail_on > 0)
&& (g_host_alloc_calls == g_host_alloc_fail_on)) {
errno = ENOMEM;
return NULL;
}
return __real_make_host_alloc(host, nalloc, allocs);
}
xce_job_alloc_t* __wrap_make_job_alloc(uint64_t jobid, int nhost,
xce_host_alloc_t** hallocs)
{
g_job_alloc_calls++;
if ((g_job_alloc_fail_on > 0)
&& (g_job_alloc_calls == g_job_alloc_fail_on)) {
errno = ENOMEM;
return NULL;
}
return __real_make_job_alloc(jobid, nhost, hallocs);
}
void setUp(void) { oom_injection_reset(); }
void tearDown(void) { oom_injection_reset(); }
/* ---------------------------------------------------------------------- */
/* Fixture */
/* ---------------------------------------------------------------------- */
#define HOST_A "eq-host-a"
#define HOST_B "eq-host-b"
typedef struct {
xce_scheduler_t sched;
xce_resource_t* slots;
xce_resource_t* mem;
xce_resource_t* rack;
xce_resource_t* lic; /* shared (XCE_RESF_GLOBAL) */
xce_sched_plugin_t* plugin;
xce_sched_obj_t obj;
xce_primary_schedobj_t primary_obj;
xce_sched_ctx_t ctx;
int ctx_ready;
} eq_fixture_t;
/* One primary request: `mem` per task, `lic` per job (0 = not requested),
* optional `rack == N` select. */
typedef struct {
xce_primary_resreq_t req;
rusage_value_t mem_rusage;
rusage_value_t lic_rusage;
xce_select_t select;
xce_select_node_t sel_res, sel_num, sel_eq;
} eq_primary_t;
static xce_resource_t* eq_register_resource(var resmgr, const char* name,
int flags)
{
xce_resource_t res;
memset(&res, 0, sizeof(res));
res.name = (char*)name;
res.flags = flags;
return makeResource(resmgr, &res);
}
/* Shared pool "licenses" has `lic_total` capacity, none in use. */
static void eq_fixture_init(eq_fixture_t* f, float lic_total)
{
int idx[1];
float val[1];
memset(f, 0, sizeof(*f));
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(f->sched.core)));
/* xce_sched_core_fini() finalizes these; both drivers calloc them */
for (idx[0] = 0; idx[0] < 2; idx[0]++) {
f->sched.core.pend_updates[idx[0]]
= (dataq_t*)calloc(1, sizeof(dataq_t));
TEST_ASSERT_NOT_NULL(f->sched.core.pend_updates[idx[0]]);
dataq_init(f->sched.core.pend_updates[idx[0]], NULL);
}
f->slots = eq_register_resource(f->sched.core.resmgr, "slots", 0);
f->mem = eq_register_resource(f->sched.core.resmgr, "mem", 0);
f->rack = eq_register_resource(f->sched.core.resmgr, "rack", 0);
f->lic = eq_register_resource(f->sched.core.resmgr, "licenses",
XCE_RESF_GLOBAL);
TEST_ASSERT_NOT_NULL(f->slots);
TEST_ASSERT_NOT_NULL(f->mem);
TEST_ASSERT_NOT_NULL(f->rack);
TEST_ASSERT_NOT_NULL(f->lic);
idx[0] = f->lic->idx;
val[0] = lic_total;
put_resvals(&(f->sched.core.sharedInst), 1, idx, val);
f->plugin = seqjob_allocator_new_plugin();
TEST_ASSERT_NOT_NULL(f->plugin);
TEST_ASSERT_NOT_NULL(f->plugin->job_alloc_fn);
f->plugin->sched = &(f->sched);
f->plugin->core = &(f->sched.core);
}
static xce_sched_worker_t* eq_add_host(eq_fixture_t* f, const char* name,
float slots, float mem, float rack)
{
xce_sched_worker_t* w;
int idx[3] = { f->slots->idx, f->mem->idx, f->rack->idx };
float total[3] = { slots, mem, rack };
float inuse[2] = { 0.0f, 0.0f };
w = (xce_sched_worker_t*)calloc(1, sizeof(*w));
TEST_ASSERT_NOT_NULL(w);
w->name = makeString((char*)name);
TEST_ASSERT_NOT_NULL(w->name);
zarray_init(&(w->resvals), 8, (zarray_item_killer_t)kill_sched_resval);
zarray_init(&(w->resInUse), 8, (zarray_item_killer_t)kill_sched_resval);
put_resvals(&(w->resvals), 3, idx, total);
put_resvals(&(w->resInUse), 2, idx, inuse);
TEST_ASSERT_TRUE(sched_add_worker(&(f->sched.core), w) >= 0);
return w;
}
static void eq_primary_init(eq_primary_t* p, int max_tasks, float mem,
float lic, const eq_fixture_t* f, int rack)
{
memset(p, 0, sizeof(*p));
p->req.minTasks = (max_tasks > 0) ? 1 : 0;
p->req.maxTasks = max_tasks;
xlist_init(&(p->req.rusages), 4);
if (mem > 0.0f) {
p->mem_rusage.res = (char*)"mem";
p->mem_rusage.value = mem;
p->mem_rusage.per_flag = PER_TASK;
xlist_store(&(p->req.rusages), &(p->mem_rusage));
}
if (lic > 0.0f) {
p->lic_rusage.res = (char*)"licenses";
p->lic_rusage.value = lic;
p->lic_rusage.per_flag = PER_JOB;
xlist_store(&(p->req.rusages), &(p->lic_rusage));
}
if (rack > 0) {
p->sel_res.type = SEL_RESNAME;
p->sel_res.u.resname.res_idx = f->rack->idx;
p->sel_res.u.resname.res_type = RES_TYPE_NUMERIC;
p->sel_num.type = SEL_NUM;
p->sel_num.u.numval = (double)rack;
p->sel_eq.type = SEL_EQ;
p->sel_eq.u.binop.left = &(p->sel_res);
p->sel_eq.u.binop.right = &(p->sel_num);
p->select.root = &(p->sel_eq);
p->req.select = &(p->select);
}
}
/* ctx over a PRIMARY obj for `p`, job-level prepare pass included
* (sched_ctx_set_primary() -> plugin_prepare_allocation(), ctx->worker
* NULL), exactly as sched_make_primary_allocation() drives it. */
static void eq_ctx_begin_primary(eq_fixture_t* f, eq_primary_t* p)
{
f->primary_obj.resreq = &(p->req);
f->obj.tag = XCE_RESREQ_PRIMARY;
f->obj.u.primary = &(f->primary_obj);
TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&(f->ctx), &(f->sched.core),
&(f->obj)));
f->ctx_ready = 1;
sched_ctx_set_primary(&(f->ctx), &(p->req));
}
static int eq_alloc(eq_fixture_t* f)
{
return f->plugin->job_alloc_fn(f->plugin, &(f->ctx));
}
static void eq_fixture_fini(eq_fixture_t* f)
{
if (f->ctx_ready) {
free_JobAlloc_ptr(&(f->ctx.alloc));
free_PendReason_ptr(&(f->ctx.reason));
sched_ctx_fini(&(f->ctx));
}
f->plugin->fini_fn(f->plugin);
xce_sched_core_fini(&(f->sched.core));
/* xce_sched_core_init() creates the process-wide string table on every
* call and xce_sched_core_fini() leaves it; release it per fixture */
xstring_table_fini();
}
/* ---------------------------------------------------------------------- */
/* Assertion helpers */
/* ---------------------------------------------------------------------- */
static float eq_host_uncomit(eq_fixture_t* f, xce_sched_worker_t* w,
xce_resource_t* r)
{
xvec_t* v = (xvec_t*)zarray_get(&(f->ctx.uncomitted.hosts), w->idx);
return (v == NULL) ? 0.0f : ((float*)v->data)[r->idx];
}
static float eq_shared_uncomit(eq_fixture_t* f)
{
return ((float*)f->ctx.uncomitted.shared->data)[f->lic->idx];
}
static void eq_assert_record(xce_alloc_t* a, const char* resname, float value)
{
TEST_ASSERT_NOT_NULL(a);
TEST_ASSERT_EQUAL_STRING(resname, a->resname);
TEST_ASSERT_EQUAL_STRING("", a->reserve);
TEST_ASSERT_EQUAL_FLOAT(value, a->value);
TEST_ASSERT_EQUAL_INT(-1, a->duration);
}
/* One host allocation carrying mem then slots (seqjob's per-host prepare
* computes the rusage wants first, then stores slots). */
static void eq_assert_host_mem_slots(xce_host_alloc_t* ha, const char* host,
float mem)
{
TEST_ASSERT_NOT_NULL(ha);
TEST_ASSERT_EQUAL_STRING(host, ha->host);
TEST_ASSERT_EQUAL_INT(2, ha->nallocs);
TEST_ASSERT_NOT_NULL(ha->allocs);
eq_assert_record(ha->allocs[0], "mem", mem);
eq_assert_record(ha->allocs[1], "slots", 1.0f);
}
static void eq_assert_reason_counts(eq_fixture_t* f, int main_reason,
int nhost, int nres)
{
TEST_ASSERT_NOT_NULL(f->ctx.reason);
TEST_ASSERT_NOT_NULL(f->ctx.reason->reasons);
TEST_ASSERT_EQUAL_INT(main_reason, f->ctx.reason->reasons->main_reason);
TEST_ASSERT_EQUAL_INT(nhost, f->ctx.reason->reasons->nhost_reasons);
TEST_ASSERT_EQUAL_INT(nres, f->ctx.reason->reasons->nres_reasons);
}
static void eq_assert_res_reason(eq_fixture_t* f, int i, const char* host,
const char* res)
{
xce_res_reason_t* rr = f->ctx.reason->reasons->res_based_reasons[i];
TEST_ASSERT_NOT_NULL(rr);
TEST_ASSERT_EQUAL_STRING(host, rr->host);
TEST_ASSERT_EQUAL_STRING(res, rr->resource);
TEST_ASSERT_EQUAL_INT(PREASON_NO_RESOURCE, rr->reason);
}
static void eq_primary_fini(eq_primary_t* p) { xlist_fini(&(p->req.rusages)); }
static void eq_assert_calls(int make_allocs, int host_alloc, int job_alloc)
{
TEST_ASSERT_EQUAL_INT(make_allocs, g_make_allocs_calls);
TEST_ASSERT_EQUAL_INT(host_alloc, g_host_alloc_calls);
TEST_ASSERT_EQUAL_INT(job_alloc, g_job_alloc_calls);
}
/* The standard satisfiable job: one host (4 slots, 8 mem, rack 1), 10
* shared licenses; the job wants 2 mem per task and 3 licenses per job.
* Returns the job_alloc_fn result; the fixture is left for the caller. */
static int eq_run_standard(eq_fixture_t* f, eq_primary_t* p,
xce_sched_worker_t** host)
{
eq_fixture_init(f, 10.0f);
*host = eq_add_host(f, HOST_A, 4.0f, 8.0f, 1.0f);
eq_primary_init(p, 1, 2.0f, 3.0f, f, 0);
eq_ctx_begin_primary(f, p);
return eq_alloc(f);
}
static void eq_finish(eq_fixture_t* f, eq_primary_t* p)
{
eq_fixture_fini(f);
eq_primary_fini(p);
}
/* ---------------------------------------------------------------------- */
/* Fixtures */
/* ---------------------------------------------------------------------- */
/* 1. Success: one host allocation (mem, slots) and one shared record. */
static void test_seqjob_success_one_host_with_shared(void)
{
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
TEST_ASSERT_EQUAL_INT(1, eq_run_standard(&f, &p, &a));
TEST_ASSERT_NOT_NULL(f.ctx.alloc);
TEST_ASSERT_EQUAL_UINT64(0, f.ctx.alloc->jobid);
TEST_ASSERT_EQUAL_INT(1, f.ctx.alloc->nhosts);
eq_assert_host_mem_slots(f.ctx.alloc->allocs[0], HOST_A, 2.0f);
TEST_ASSERT_EQUAL_INT(1, f.ctx.alloc->nres_shared);
eq_assert_record(f.ctx.alloc->allocs_shared[0], "licenses", 3.0f);
TEST_ASSERT_EQUAL_FLOAT(2.0f, eq_host_uncomit(&f, a, f.mem));
TEST_ASSERT_EQUAL_FLOAT(1.0f, eq_host_uncomit(&f, a, f.slots));
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
eq_assert_reason_counts(&f, 0, 0, 0);
eq_assert_calls(2, 1, 1);
eq_finish(&f, &p);
}
/* 2. Pending on the one shared resource the job requests, which is short
* (2 licenses, 3 wanted): no host is tried and nothing is counted. */
static int eq_run_short_shared(eq_fixture_t* f, eq_primary_t* p,
xce_sched_worker_t** host)
{
eq_fixture_init(f, 2.0f);
*host = eq_add_host(f, HOST_A, 4.0f, 8.0f, 1.0f);
eq_primary_init(p, 1, 2.0f, 3.0f, f, 0);
eq_ctx_begin_primary(f, p);
return eq_alloc(f);
}
static void test_seqjob_pending_on_short_shared_resource(void)
{
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
TEST_ASSERT_EQUAL_INT(0, eq_run_short_shared(&f, &p, &a));
TEST_ASSERT_NULL(f.ctx.alloc);
TEST_ASSERT_NOT_NULL(f.ctx.reason);
TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->main_reason);
TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->nhost_reasons);
TEST_ASSERT_NULL(zarray_get(&(f.ctx.uncomitted.hosts), a->idx));
TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_shared_uncomit(&f));
eq_assert_calls(0, 0, 0);
eq_finish(&f, &p);
}
/* 2, resource reasons, TODAY's behaviour: none. The reason loop skips
* every unmet entry because vget() returns 0 on success and the loop
* tests `rc <= 0`. Requirement 2.2 changes this in task 4.2 (one host-less
* NO_RESOURCE reason naming "licenses"). */
static void test_seqjob_pending_on_short_shared_no_reason_today(void)
{
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
TEST_ASSERT_EQUAL_INT(0, eq_run_short_shared(&f, &p, &a));
eq_assert_reason_counts(&f, 0, 0, 0);
eq_finish(&f, &p);
}
/* 3. Pending on hosts: the only candidate has a free slot but too little
* mem. The pre-filter state (slots only) lets it through, and the per-
* host check records NO_RESOURCE for (host, mem). The met shared request
* stays counted as uncommitted. */
static void test_seqjob_pending_single_host_short_of_mem(void)
{
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
eq_fixture_init(&f, 10.0f);
a = eq_add_host(&f, HOST_A, 4.0f, 1.0f, 1.0f);
eq_primary_init(&p, 1, 4.0f, 3.0f, &f, 0);
eq_ctx_begin_primary(&f, &p);
TEST_ASSERT_EQUAL_INT(0, eq_alloc(&f));
TEST_ASSERT_NULL(f.ctx.alloc);
eq_assert_reason_counts(&f, 0, 0, 1);
eq_assert_res_reason(&f, 0, HOST_A, "mem");
TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, a, f.mem));
TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, a, f.slots));
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
eq_assert_calls(0, 0, 0);
eq_finish(&f, &p);
}
/* 4a. Out of memory building the claimed host's allocation records
* (sched_make_allocs() call 1). Per-host debit: see the _debit_today test. */
static void test_seqjob_oom_host_records_returns_enomem(void)
{
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
g_make_allocs_fail_on = 1;
TEST_ASSERT_EQUAL_INT(-ENOMEM, eq_run_standard(&f, &p, &a));
TEST_ASSERT_NULL(f.ctx.alloc);
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
eq_assert_reason_counts(&f, 0, 0, 0);
eq_assert_calls(1, 0, 0);
eq_finish(&f, &p);
}
/* 4b. Out of memory in make_host_alloc(). Per-host debit: see the
* _debit_today test. */
static void test_seqjob_oom_host_alloc_returns_enomem(void)
{
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
g_host_alloc_fail_on = 1;
TEST_ASSERT_EQUAL_INT(-ENOMEM, eq_run_standard(&f, &p, &a));
TEST_ASSERT_NULL(f.ctx.alloc);
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
eq_assert_reason_counts(&f, 0, 0, 0);
eq_assert_calls(1, 1, 0);
eq_finish(&f, &p);
}
/* 4a/4b per-host debit, TODAY's behaviour: the claimed host's debit is
* left in the attempt context. Requirement 5.5 changes this in task 4.2
* (the debit of a claim not turned into a host allocation is released). */
static void test_seqjob_oom_host_stage_leaves_debit_today(void)
{
int stage;
for (stage = 0; stage < 2; stage++) {
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
oom_injection_reset();
if (stage == 0) {
g_make_allocs_fail_on = 1; /* host allocation records */
} else {
g_host_alloc_fail_on = 1; /* make_host_alloc() */
}
TEST_ASSERT_EQUAL_INT(-ENOMEM, eq_run_standard(&f, &p, &a));
TEST_ASSERT_EQUAL_FLOAT(2.0f, eq_host_uncomit(&f, a, f.mem));
TEST_ASSERT_EQUAL_FLOAT(1.0f, eq_host_uncomit(&f, a, f.slots));
eq_finish(&f, &p);
}
}
/* 4c. Out of memory in make_job_alloc(); the debit stays (also after the
* switch: the host allocation was built). */
static void test_seqjob_oom_job_alloc_returns_enomem_debit_stays(void)
{
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
g_job_alloc_fail_on = 1;
TEST_ASSERT_EQUAL_INT(-ENOMEM, eq_run_standard(&f, &p, &a));
TEST_ASSERT_NULL(f.ctx.alloc);
TEST_ASSERT_EQUAL_FLOAT(2.0f, eq_host_uncomit(&f, a, f.mem));
TEST_ASSERT_EQUAL_FLOAT(1.0f, eq_host_uncomit(&f, a, f.slots));
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
eq_assert_reason_counts(&f, 0, 0, 0);
eq_assert_calls(1, 1, 1);
eq_finish(&f, &p);
}
/* 4d. Out of memory building the shared records (sched_make_allocs()
* call 2); the debit stays. */
static void test_seqjob_oom_shared_records_returns_enomem_debit_stays(void)
{
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
g_make_allocs_fail_on = 2;
TEST_ASSERT_EQUAL_INT(-ENOMEM, eq_run_standard(&f, &p, &a));
TEST_ASSERT_NULL(f.ctx.alloc);
TEST_ASSERT_EQUAL_FLOAT(2.0f, eq_host_uncomit(&f, a, f.mem));
TEST_ASSERT_EQUAL_FLOAT(1.0f, eq_host_uncomit(&f, a, f.slots));
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
eq_assert_reason_counts(&f, 0, 0, 0);
eq_assert_calls(2, 1, 1);
eq_finish(&f, &p);
}
/* 5. A task count other than 1 reaching seqjob directly: its prepare hook
* is a no-op for maxTasks != 1, so both request vectors stay empty and the
* job is placed on the first host with no resource entries at all. */
static void eq_check_task_count(int max_tasks)
{
eq_fixture_t f;
eq_primary_t p;
xce_sched_worker_t* a;
eq_fixture_init(&f, 10.0f);
a = eq_add_host(&f, HOST_A, 4.0f, 8.0f, 1.0f);
eq_primary_init(&p, max_tasks, 2.0f, 3.0f, &f, 0);
eq_ctx_begin_primary(&f, &p);
TEST_ASSERT_EQUAL_INT(1, eq_alloc(&f));
TEST_ASSERT_NOT_NULL(f.ctx.alloc);
TEST_ASSERT_EQUAL_INT(1, f.ctx.alloc->nhosts);
TEST_ASSERT_NOT_NULL(f.ctx.alloc->allocs[0]);
TEST_ASSERT_EQUAL_STRING(HOST_A, f.ctx.alloc->allocs[0]->host);
TEST_ASSERT_EQUAL_INT(0, f.ctx.alloc->allocs[0]->nallocs);
TEST_ASSERT_NULL(f.ctx.alloc->allocs[0]->allocs);
TEST_ASSERT_EQUAL_INT(0, f.ctx.alloc->nres_shared);
TEST_ASSERT_NULL(f.ctx.alloc->allocs_shared);
TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, a, f.mem));
TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, a, f.slots));
TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_shared_uncomit(&f));
eq_assert_reason_counts(&f, 0, 0, 0);
eq_assert_calls(1, 1, 1);
eq_finish(&f, &p);
}
static void test_seqjob_task_count_zero_reaches_plugin(void)
{
eq_check_task_count(0);
}
static void test_seqjob_task_count_two_reaches_plugin(void)
{
eq_check_task_count(2);
}
/* 6. COMPOUND: one ctx, two primaries allocated in turn as
* sched_make_compound_alloc_() does. The selects (rack 1 / rack 2) keep
* the second primary off the host the first claimed, so the positional
* uncommitted read (bug-1892) cannot touch the expected values. Each
* primary is sized from its own request (Req 5.8, bug-1768). */
static void test_seqjob_compound_second_primary_sized_from_own_request(void)
{
eq_fixture_t f;
eq_primary_t p1;
eq_primary_t p2;
xce_compound_resreq_t cres;
xce_compound_schedobj_t cobj;
xce_job_alloc_t* first = NULL;
xce_sched_worker_t* a;
xce_sched_worker_t* b;
eq_fixture_init(&f, 10.0f);
a = eq_add_host(&f, HOST_A, 1.0f, 8.0f, 1.0f);
b = eq_add_host(&f, HOST_B, 1.0f, 8.0f, 2.0f);
eq_primary_init(&p1, 1, 2.0f, 3.0f, &f, 1);
eq_primary_init(&p2, 1, 3.0f, 0.0f, &f, 2);
xlist_init(&(cres.primaries), 2);
xlist_store(&(cres.primaries), &(p1.req));
xlist_store(&(cres.primaries), &(p2.req));
memset(&cobj, 0, sizeof(cobj));
cobj.owner = &(f.obj);
cobj.resreq = &cres;
f.obj.tag = XCE_RESREQ_COMPOUND;
f.obj.u.compound = &cobj;
TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&(f.ctx), &(f.sched.core),
&(f.obj)));
f.ctx_ready = 1;
sched_ctx_set_primary(&(f.ctx), &(p1.req));
TEST_ASSERT_EQUAL_INT(1, eq_alloc(&f));
MOVE_PTR(first, f.ctx.alloc);
sched_ctx_set_primary(&(f.ctx), &(p2.req));
TEST_ASSERT_EQUAL_INT(1, eq_alloc(&f));
TEST_ASSERT_NOT_NULL(first);
TEST_ASSERT_EQUAL_INT(1, first->nhosts);
eq_assert_host_mem_slots(first->allocs[0], HOST_A, 2.0f);
TEST_ASSERT_EQUAL_INT(1, first->nres_shared);
eq_assert_record(first->allocs_shared[0], "licenses", 3.0f);
TEST_ASSERT_NOT_NULL(f.ctx.alloc);
TEST_ASSERT_EQUAL_INT(1, f.ctx.alloc->nhosts);
eq_assert_host_mem_slots(f.ctx.alloc->allocs[0], HOST_B, 3.0f);
TEST_ASSERT_EQUAL_INT(0, f.ctx.alloc->nres_shared);
TEST_ASSERT_NULL(f.ctx.alloc->allocs_shared);
TEST_ASSERT_EQUAL_FLOAT(2.0f, eq_host_uncomit(&f, a, f.mem));
TEST_ASSERT_EQUAL_FLOAT(1.0f, eq_host_uncomit(&f, a, f.slots));
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_host_uncomit(&f, b, f.mem));
TEST_ASSERT_EQUAL_FLOAT(1.0f, eq_host_uncomit(&f, b, f.slots));
TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f));
eq_assert_reason_counts(&f, 0, 0, 0);
eq_assert_calls(3, 2, 2);
free_JobAlloc_ptr(&first);
xlist_fini(&(cres.primaries));
eq_fixture_fini(&f);
eq_primary_fini(&p1);
eq_primary_fini(&p2);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_seqjob_success_one_host_with_shared);
RUN_TEST(test_seqjob_pending_on_short_shared_resource);
RUN_TEST(test_seqjob_pending_on_short_shared_no_reason_today);
RUN_TEST(test_seqjob_pending_single_host_short_of_mem);
RUN_TEST(test_seqjob_oom_host_records_returns_enomem);
RUN_TEST(test_seqjob_oom_host_alloc_returns_enomem);
RUN_TEST(test_seqjob_oom_host_stage_leaves_debit_today);
RUN_TEST(test_seqjob_oom_job_alloc_returns_enomem_debit_stays);
RUN_TEST(test_seqjob_oom_shared_records_returns_enomem_debit_stays);
RUN_TEST(test_seqjob_task_count_zero_reaches_plugin);
RUN_TEST(test_seqjob_task_count_two_reaches_plugin);
RUN_TEST(test_seqjob_compound_second_primary_sized_from_own_request);
return UNITY_END();
}