From d9ef9830149a0da1289908e5c221ea751b2b4efc Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 30 Sep 2026 08:58:07 +0000 Subject: [PATCH] feat(scheduler-allocator-runner): runner guards and shared-resource precheck (task 3.1) New alloc_runner.c: sched_allocator_run() validates its inputs (-EINVAL without touching the pend reason), applies the plugin's optional accepts guard, creates the pend-reason record, then checks shared resources: one host-less NO_RESOURCE reason per unmet entry (skip only on vget < 0, fixing bug-1893) and the met amounts counted once as uncommitted. Plan search follows in task 3.2. Runner tests use a fake allocator contract; the pend-reason factory audit now scans the runner. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../specs/scheduler-allocator-runner/tasks.md | 5 +- src/scheduler/meson.build | 1 + src/scheduler/src/alloc_runner.c | 148 +++++ src/scheduler/testsuite/meson.build | 16 + src/scheduler/testsuite/test_alloc_runner.c | 556 ++++++++++++++++++ .../test_picker_pendreason_factory_audit.c | 1 + 6 files changed, 726 insertions(+), 1 deletion(-) create mode 100644 src/scheduler/src/alloc_runner.c create mode 100644 src/scheduler/testsuite/test_alloc_runner.c diff --git a/.kiro/specs/scheduler-allocator-runner/tasks.md b/.kiro/specs/scheduler-allocator-runner/tasks.md index fc29ede2..55107f10 100644 --- a/.kiro/specs/scheduler-allocator-runner/tasks.md +++ b/.kiro/specs/scheduler-allocator-runner/tasks.md @@ -29,7 +29,7 @@ - _Requirements: 4.5, 5.1, 5.2, 5.4, 5.8, 7.2_ - [ ] 3. Framework allocation routine -- [ ] 3.1 Build the routine's guards and shared-resource precheck, test first +- [x] 3.1 Build the routine's guards and shared-resource precheck, test first - Add the new runner source to the scheduler build and to the pend-reason factory audit's scanned files, and add a runner test target that uses a scripted fake allocator contract - Missing plugin, context, core, scheduler back-reference, primary request, contract or plan functions returns invalid-argument without creating a pend-reason record - A rejecting acceptance guard returns pending with no pend-reason record, no resource check and no plan initialisation; an absent guard accepts @@ -100,3 +100,6 @@ - Per-host reasons (decided 2026-09-30 after 2.2): the runner clears `alloc_req.host` before every candidate (start of bucket + after each rejected candidate), both allocators, so every selection-matching unclaimed short host gets reasons. Today parjob gives reasons only for a bucket search's first rejected candidate; the 2.2 file keeps those counts in `_today` tests that 4.1 rewrites. - `test_alloc_equivalence_seqjob.c` is at ~794/800 lines: 4.2's new fixtures go in a second file or after trimming. - 4.1 expectations for the parjob `_today` host-reason test under the per-candidate clear: two-bucket success 2 reasons (A slots, B slots); pending on hosts 6 (A mem, A slots, B mem, B slots, A mem, B mem); mid-plan rollback 5 slots reasons (A, B, C from plan [4]; B, C from plan [2,2] bucket 2). `eq_assert_reason_prefix` checks `>=`, so 4.1 must pin exact counts. The parjob file is at 799/800 lines. +- Req 2.4 error-path test scope (3.1 review): `make_pendreason` -ENOMEM is tested (`--wrap=make_pendreason`). The uncommitted-read, shared-check and uncommitted-add error paths are returned correctly but not tested: the read cannot fail past the input guard (`sched_ctx_get_uncomit_shared` only rejects NULL args), and the other two fail only on a libxce-internal allocation failure (would need malloc-level injection). +- The runner ignores `add_job_preason`'s return value, as the old allocators did (a failed add drops that reason). +- Incremental rebuilds in `src/build.alloc` can miss a file restored after a temporary RED edit if its mtime ends up older than the object; `touch` restored files before the GREEN run (seen in 3.1). diff --git a/src/scheduler/meson.build b/src/scheduler/meson.build index ea115c0b..9895ff9b 100644 --- a/src/scheduler/meson.build +++ b/src/scheduler/meson.build @@ -34,6 +34,7 @@ scheduler_srcs = [ 'src/resource.c', 'src/alloc_cache.c', 'src/host_iter.c', + 'src/alloc_runner.c', 'src/plugins/fcfs.c', 'src/plugins/builtin_fcfs.c', 'src/plugins/seqjob_allocator.c', diff --git a/src/scheduler/src/alloc_runner.c b/src/scheduler/src/alloc_runner.c new file mode 100644 index 00000000..b8c47670 --- /dev/null +++ b/src/scheduler/src/alloc_runner.c @@ -0,0 +1,148 @@ +/* + * alloc_runner.c - the framework allocation routine + * (scheduler-allocator-runner spec, Req 1, 2, 5). + * + * sched_allocator_run() is the job_alloc_fn of every allocator plugin. The + * plugin supplies only its policy through xce_sched_plugin_t's + * allocator_iface (an optional acceptance guard and a plan generator); this + * file runs the rest of one allocation attempt: + * + * 1. input guard: -EINVAL before ctx->reason is touched (Req 5.1) + * 2. acceptance guard: a rejected request pends with no reason record and + * no check (Req 1.3, 1.4) + * 3. the pend-reason record, created only after both guards (Req 2.1) + * 4. the shared-resource precheck (Req 2.2-2.4) + * 5. the plan generator (Req 3.4) + * + * Returns > 0 when satisfied (ctx->alloc set), 0 when pending, < 0 a + * negative errno. No plugin file or plugin-specific symbol is referenced + * here; plugins reach this file only through sched_allocator_run() and the + * iface type (alloc_iter.h). + */ +#include + +#include "preason.h" +#include "resobj.h" +#include "sched/alloc_iter.h" +#include "sched/types.h" +#include "scheduler.h" + +/* Creates the attempt's pend-reason record unless an earlier primary of + * the same COMPOUND/ALTER attempt already did. 0 or -ENOMEM (ctx->reason + * then stays NULL). */ +static int sched_alloc_ensure_reason_(xce_sched_ctx_t* ctx) +{ + if (ctx->reason != NULL) { + return 0; + } + + ctx->reason = make_pendreason(0); + if (ctx->reason == NULL) { + return -ENOMEM; + } + + return 0; +} + +/* Checks the job-level shared request (ctx->alloc_req.shared) against the + * shared pools less what the attempt has already counted as uncommitted. + * + * Met: counts the requested amounts as uncommitted, once for the attempt, + * and returns 1. The runner never subtracts them again -- the ctx that + * holds them is discarded by its caller or shared with the next primary of + * the same COMPOUND/ALTER alternative (Req 2.3). + * + * Unmet: adds one host-less NO_RESOURCE reason per entry of the unmet list + * the check returns, and returns 0 (Req 2.2). vget() returns 0 on success, + * so only a negative result skips an entry (bug-1893). + * + * Any failure returns its negative error code (Req 2.4). */ +static int sched_alloc_prepare_shared_(xce_sched_core_t* core, + xce_sched_ctx_t* ctx) +{ + xvec_t* uncomit = NULL; + int rc; + int i; + + rc = sched_ctx_get_uncomit_shared(&uncomit, ctx); + if (rc < 0) { + return rc; + } + + /* bug-1884: the check (re)initializes unmeet itself */ + xvec_idval_fini(&(ctx->unmeet)); + rc = sched_check_shared_satisfy(core, uncomit, &(ctx->alloc_req.shared), + &(ctx->unmeet)); + if (rc < 0) { + return rc; + } + + if (rc == 0) { + for (i = 0; i < ctx->unmeet.count; i++) { + xce_resource_t* r = NULL; + + rc = vget(core->resmgr, ((int*)ctx->unmeet.idxs.data)[i], + (void**)&r); + if (rc < 0) { + continue; + } + + add_job_preason(ctx->reason->reasons, PREASON_NO_RESOURCE, NULL, + r->name); + } + + return 0; + } + + rc = xvec_idval(uncomit, &(ctx->alloc_req.shared), XVEC_ADD_FLOAT); + if (rc < 0) { + return rc; + } + + return 1; +} + +int sched_allocator_run(void* plugin_arg, xce_sched_ctx_t* ctx) +{ + xce_sched_plugin_t* plugin = plugin_arg; + const xce_sched_allocator_iface_t* iface; + xce_alloc_plan_iter_t iter; + int rc; + + if ((plugin == NULL) || (ctx == NULL) || (plugin->core == NULL) + || (plugin->sched == NULL) || (ctx->primary == NULL)) { + return -EINVAL; + } + + iface = plugin->allocator_iface; + if ((iface == NULL) || (iface->plan_init == NULL) + || (iface->plan_next == NULL) || (iface->plan_fini == NULL)) { + return -EINVAL; + } + + if ((iface->accepts != NULL) && !iface->accepts(ctx->primary)) { + return 0; + } + + rc = sched_alloc_ensure_reason_(ctx); + if (rc < 0) { + return rc; + } + + rc = sched_alloc_prepare_shared_(plugin->core, ctx); + if (rc <= 0) { + return rc; + } + + /* A plan generator that cannot start is not a resource verdict: the + * attempt pends, with nothing for plan_fini to release. Otherwise no + * plan is searched, so the attempt pends once the generator is + * finished. */ + if (iface->plan_init(&iter, ctx->primary, plugin->core) != 0) { + return 0; + } + + iface->plan_fini(&iter); + + return 0; +} diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 1340d4a8..38fb3d76 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -1743,6 +1743,19 @@ sched_alloc_equivalence_parjob_test = executable( '-Wl,--wrap=make_job_alloc'], install: false) +# scheduler-allocator-runner task 3.1 (Req 1.3, 1.4, 2.1-2.4, 5.1): the +# framework allocation routine (alloc_runner.c) through sched_allocator_run() +# with an in-test fake allocator contract. Same white-box stack as the +# equivalence targets above; make_pendreason is wrapped for the reason-record +# out-of-memory test (the make_*_alloc wraps are not needed). +sched_alloc_runner_test = executable( + 'sched_alloc_runner_test', + ['test_alloc_runner.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps], + link_args: ['-Wl,--wrap=make_pendreason'], + 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 @@ -2015,6 +2028,9 @@ test('scheduler alloc equivalence seqjob test', # Leak detection stays on (as for the seqjob characterization above). test('scheduler alloc equivalence parjob test', sched_alloc_equivalence_parjob_test, suite: 'scheduler') +# Leak detection stays on: every fixture tears down its ctx and core. +test('scheduler alloc runner test', + sched_alloc_runner_test, suite: 'scheduler') test('scheduler seqjob rewrap equivalence test', sched_seqjob_rewrap_equivalence_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_alloc_runner.c b/src/scheduler/testsuite/test_alloc_runner.c new file mode 100644 index 00000000..e1d5cd1f --- /dev/null +++ b/src/scheduler/testsuite/test_alloc_runner.c @@ -0,0 +1,556 @@ +/* + * test_alloc_runner.c - scheduler-allocator-runner task 3.1: the framework + * allocation routine's guards and shared-resource precheck (Req 1.3, 1.4, + * 2.1-2.4, 5.1), driven only through sched_allocator_run() with an in-test + * fake allocator contract on a hand-built plugin. + * + * The fake contract counts every call: the acceptance guard returns a + * configurable verdict; plan_init returns a configurable result and + * plan_next yields no plan, so an attempt that gets past the shared + * precheck always ends pending. The job-level shared request is staged by + * the plugin_prepare_allocation() stub below from g_shared_want. + */ +#include +#include +#include + +#include + +#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 */ +#include "../src/alloc_runner.c" +#include "../src/core.c" +#include "../src/host_iter.c" +#include "../src/resource.c" +#include "../src/schedobj.c" + +/* ---------------------------------------------------------------------- */ +/* Link-satisfying stubs (same convention as test_alloc_equivalence_*.c) */ +/* ---------------------------------------------------------------------- */ + +int plugin_register(void* sched, void* plugin) +{ + return (void)sched, (void)plugin, 0; +} + +void plugin_unregister(void* arg) { (void)arg; } + +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; +} + +/* Never reached by these tests (no plan is ever satisfied); present only + * because the white-box sources reference it. */ +xce_alloc_t** sched_make_allocs(xce_scheduler_t* sched, char* reserve, int nres, + int* idxs, float* vals, int* duration) +{ + (void)sched, (void)reserve, (void)nres, (void)idxs, (void)vals; + (void)duration; + errno = ENOSYS; + return NULL; +} + +/* Shared request staged by the job-level prepare pass: resource index and + * amount per entry, g_shared_nwant entries. */ +#define MAX_WANT 4 +static int g_shared_nwant = 0; +static int g_shared_idx[MAX_WANT]; +static float g_shared_val[MAX_WANT]; + +/* Resets the request vectors as plugin.c does, then stages g_shared_want + * on the job-level pass (ctx->worker NULL). */ +int plugin_prepare_allocation(xce_sched_core_t* sched, xce_sched_ctx_t* ctx) +{ + int i; + + (void)sched; + xvec_idval_fini(&(ctx->alloc_req.host)); + xvec_idval_init(&(ctx->alloc_req.host), 8); + if (ctx->worker != NULL) { + return 0; + } + + xvec_idval_fini(&(ctx->alloc_req.shared)); + xvec_idval_init(&(ctx->alloc_req.shared), 8); + for (i = 0; i < g_shared_nwant; i++) { + TEST_ASSERT_TRUE(xvec_idval_store(&(ctx->alloc_req.shared), + g_shared_idx[i], g_shared_val[i]) + > 0); + } + + return 0; +} + +/* ---------------------------------------------------------------------- */ +/* Fake allocator contract */ +/* ---------------------------------------------------------------------- */ + +static int g_accepts_calls = 0; +static int g_accepts_result = 1; +static int g_init_calls = 0; +static int g_init_result = 0; +static int g_next_calls = 0; +static int g_fini_calls = 0; + +static int fake_accepts(const xce_primary_resreq_t* primary) +{ + (void)primary; + g_accepts_calls++; + return g_accepts_result; +} + +static int fake_plan_init(xce_alloc_plan_iter_t* iter, + xce_primary_resreq_t* resreq, xce_sched_core_t* core) +{ + (void)resreq, (void)core; + g_init_calls++; + iter->private = NULL; + return g_init_result; +} + +static xce_alloc_plan_t* fake_plan_next(xce_alloc_plan_iter_t* iter) +{ + (void)iter; + g_next_calls++; + return NULL; +} + +static void fake_plan_fini(xce_alloc_plan_iter_t* iter) +{ + (void)iter; + g_fini_calls++; +} + +/* Mutable so a test can drop one member; reset by setUp() */ +static xce_sched_allocator_iface_t g_iface; + +/* make_pendreason() failure injection (-Wl,--wrap=make_pendreason in + * meson.build): when armed, the next call fails with ENOMEM and disarms. */ +static int g_pendreason_fail = 0; + +xce_pendreason_t* __real_make_pendreason(uint64_t jobid); + +xce_pendreason_t* __wrap_make_pendreason(uint64_t jobid) +{ + if (g_pendreason_fail) { + g_pendreason_fail = 0; + errno = ENOMEM; + return NULL; + } + return __real_make_pendreason(jobid); +} + +/* Restores the full fake contract and clears its call counters */ +static void fake_contract_reset(void) +{ + g_accepts_calls = 0; + g_accepts_result = 1; + g_init_calls = 0; + g_init_result = 0; + g_next_calls = 0; + g_fini_calls = 0; + + g_iface.accepts = fake_accepts; + g_iface.plan_init = fake_plan_init; + g_iface.plan_next = fake_plan_next; + g_iface.plan_fini = fake_plan_fini; +} + +void setUp(void) +{ + g_shared_nwant = 0; + g_pendreason_fail = 0; + fake_contract_reset(); +} + +void tearDown(void) { } + +/* ---------------------------------------------------------------------- */ +/* Fixture */ +/* ---------------------------------------------------------------------- */ + +typedef struct { + xce_scheduler_t sched; + xce_resource_t* lic_a; /* shared, 10 in total */ + xce_resource_t* lic_b; /* shared, 2 in total */ + xce_sched_plugin_t plugin; + xce_primary_resreq_t req; + xce_sched_obj_t obj; + xce_primary_schedobj_t primary_obj; + xce_sched_ctx_t ctx; +} rt_fixture_t; + +static xce_resource_t* rt_register_shared(var resmgr, const char* name) +{ + xce_resource_t res; + + memset(&res, 0, sizeof(res)); + res.name = (char*)name; + res.flags = XCE_RESF_GLOBAL; + + return makeResource(resmgr, &res); +} + +/* Core with two shared pools, the hand-built plugin over the fake contract, + * and a ctx over one single-task primary with its job-level prepare pass + * run (the shared request staged from g_shared_want). */ +static void rt_fixture_init(rt_fixture_t* f) +{ + int i; + int idx[2]; + float total[2] = { 10.0f, 2.0f }; + + 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 (i = 0; i < 2; i++) { + f->sched.core.pend_updates[i] = (dataq_t*)calloc(1, sizeof(dataq_t)); + TEST_ASSERT_NOT_NULL(f->sched.core.pend_updates[i]); + dataq_init(f->sched.core.pend_updates[i], NULL); + } + + f->lic_a = rt_register_shared(f->sched.core.resmgr, "lic_a"); + f->lic_b = rt_register_shared(f->sched.core.resmgr, "lic_b"); + TEST_ASSERT_NOT_NULL(f->lic_a); + TEST_ASSERT_NOT_NULL(f->lic_b); + idx[0] = f->lic_a->idx; + idx[1] = f->lic_b->idx; + put_resvals(&(f->sched.core.sharedInst), 2, idx, total); + + f->plugin.name = (char*)"fake_allocator"; + f->plugin.sched = &(f->sched); + f->plugin.core = &(f->sched.core); + f->plugin.allocator_iface = &g_iface; + + f->req.minTasks = 1; + f->req.maxTasks = 1; + f->primary_obj.resreq = &(f->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))); + sched_ctx_set_primary(&(f->ctx), &(f->req)); +} + +static void rt_fixture_fini(rt_fixture_t* f) +{ + free_JobAlloc_ptr(&(f->ctx.alloc)); + free_PendReason_ptr(&(f->ctx.reason)); + sched_ctx_fini(&(f->ctx)); + 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(); +} + +/* Stages a shared request of `a` lic_a and `b` lic_b (0 = not requested) + * and reruns the job-level prepare pass. */ +static void rt_want_shared(rt_fixture_t* f, float a, float b) +{ + g_shared_nwant = 0; + if (a > 0.0f) { + g_shared_idx[g_shared_nwant] = f->lic_a->idx; + g_shared_val[g_shared_nwant++] = a; + } + if (b > 0.0f) { + g_shared_idx[g_shared_nwant] = f->lic_b->idx; + g_shared_val[g_shared_nwant++] = b; + } + sched_ctx_set_primary(&(f->ctx), &(f->req)); +} + +static int rt_run(rt_fixture_t* f) +{ + return sched_allocator_run(&(f->plugin), &(f->ctx)); +} + +static float rt_shared_uncomit(rt_fixture_t* f, xce_resource_t* r) +{ + return ((float*)f->ctx.uncomitted.shared->data)[r->idx]; +} + +static void rt_assert_no_shared_uncomit(rt_fixture_t* f) +{ + TEST_ASSERT_EQUAL_FLOAT(0.0f, rt_shared_uncomit(f, f->lic_a)); + TEST_ASSERT_EQUAL_FLOAT(0.0f, rt_shared_uncomit(f, f->lic_b)); +} + +static void rt_assert_calls(int accepts, int init, int fini) +{ + TEST_ASSERT_EQUAL_INT(accepts, g_accepts_calls); + TEST_ASSERT_EQUAL_INT(init, g_init_calls); + TEST_ASSERT_EQUAL_INT(fini, g_fini_calls); +} + +/* ---------------------------------------------------------------------- */ +/* Guards (Req 5.1, 1.3, 1.4, 2.1) */ +/* ---------------------------------------------------------------------- */ + +/* Missing plugin or ctx: -EINVAL, the ctx untouched, no contract call. */ +static void test_missing_plugin_or_ctx_is_einval(void) +{ + rt_fixture_t f; + + rt_fixture_init(&f); + TEST_ASSERT_EQUAL_INT(-EINVAL, sched_allocator_run(NULL, &(f.ctx))); + TEST_ASSERT_EQUAL_INT(-EINVAL, sched_allocator_run(&(f.plugin), NULL)); + TEST_ASSERT_NULL(f.ctx.reason); + rt_assert_calls(0, 0, 0); + rt_fixture_fini(&f); +} + +/* Each missing field (core, sched, primary, iface) in turn: -EINVAL, + * ctx->reason still NULL, no contract call. */ +static void test_missing_field_is_einval_without_reason(void) +{ + rt_fixture_t f; + int i; + + rt_fixture_init(&f); + rt_want_shared(&f, 3.0f, 0.0f); + for (i = 0; i < 4; i++) { + xce_sched_core_t* core = f.plugin.core; + void* sched = f.plugin.sched; + xce_primary_resreq_t* primary = f.ctx.primary; + const xce_sched_allocator_iface_t* iface = f.plugin.allocator_iface; + + f.plugin.core = (i == 0) ? NULL : core; + f.plugin.sched = (i == 1) ? NULL : sched; + f.ctx.primary = (i == 2) ? NULL : primary; + f.plugin.allocator_iface = (i == 3) ? NULL : iface; + + TEST_ASSERT_EQUAL_INT_MESSAGE(-EINVAL, rt_run(&f), "missing field"); + TEST_ASSERT_NULL(f.ctx.reason); + + f.plugin.core = core; + f.plugin.sched = sched; + f.ctx.primary = primary; + f.plugin.allocator_iface = iface; + } + rt_assert_calls(0, 0, 0); + rt_assert_no_shared_uncomit(&f); + rt_fixture_fini(&f); +} + +/* Each missing plan_* member in turn: -EINVAL, no reason, no call. */ +static void test_missing_plan_member_is_einval_without_reason(void) +{ + rt_fixture_t f; + int i; + + rt_fixture_init(&f); + for (i = 0; i < 3; i++) { + fake_contract_reset(); + g_iface.plan_init = (i == 0) ? NULL : fake_plan_init; + g_iface.plan_next = (i == 1) ? NULL : fake_plan_next; + g_iface.plan_fini = (i == 2) ? NULL : fake_plan_fini; + + TEST_ASSERT_EQUAL_INT_MESSAGE(-EINVAL, rt_run(&f), "missing plan_*"); + TEST_ASSERT_NULL(f.ctx.reason); + rt_assert_calls(0, 0, 0); + } + rt_fixture_fini(&f); +} + +/* A rejecting guard: pending, no reason record, no shared check or count, + * no plan generator (Req 1.4). */ +static void test_rejecting_accepts_pends_without_checks(void) +{ + rt_fixture_t f; + + rt_fixture_init(&f); + rt_want_shared(&f, 3.0f, 1.0f); + g_accepts_result = 0; + + TEST_ASSERT_EQUAL_INT(0, rt_run(&f)); + TEST_ASSERT_NULL(f.ctx.reason); + TEST_ASSERT_NULL(f.ctx.alloc); + rt_assert_calls(1, 0, 0); + rt_assert_no_shared_uncomit(&f); + rt_fixture_fini(&f); +} + +/* No guard accepts every request and reaches plan_init (Req 1.3). */ +static void test_null_accepts_reaches_plan_init(void) +{ + rt_fixture_t f; + + rt_fixture_init(&f); + g_iface.accepts = NULL; + + TEST_ASSERT_EQUAL_INT(0, rt_run(&f)); + TEST_ASSERT_NOT_NULL(f.ctx.reason); + rt_assert_calls(0, 1, 1); + rt_fixture_fini(&f); +} + +/* An accepting guard: the reason record is created after it (Req 2.1). */ +static void test_reason_created_after_accepting_guard(void) +{ + rt_fixture_t f; + + rt_fixture_init(&f); + + TEST_ASSERT_EQUAL_INT(0, rt_run(&f)); + TEST_ASSERT_NOT_NULL(f.ctx.reason); + TEST_ASSERT_NOT_NULL(f.ctx.reason->reasons); + TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->main_reason); + TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->nres_reasons); + rt_assert_calls(1, 1, 1); + rt_fixture_fini(&f); +} + +/* An existing reason record (e.g. from an earlier primary of the same + * COMPOUND job) is reused, not replaced. */ +static void test_existing_reason_is_reused(void) +{ + rt_fixture_t f; + xce_pendreason_t* before; + + rt_fixture_init(&f); + before = make_pendreason(0); + TEST_ASSERT_NOT_NULL(before); + f.ctx.reason = before; + + TEST_ASSERT_EQUAL_INT(0, rt_run(&f)); + TEST_ASSERT_EQUAL_PTR(before, f.ctx.reason); + rt_fixture_fini(&f); +} + +/* The reason record cannot be created: -ENOMEM with ctx->reason still + * NULL, no shared count and no plan generator (Req 2.4). */ +static void test_reason_oom_returns_enomem_without_checks(void) +{ + rt_fixture_t f; + + rt_fixture_init(&f); + rt_want_shared(&f, 3.0f, 1.0f); + g_pendreason_fail = 1; + + TEST_ASSERT_EQUAL_INT(-ENOMEM, rt_run(&f)); + TEST_ASSERT_NULL(f.ctx.reason); + TEST_ASSERT_NULL(f.ctx.alloc); + rt_assert_calls(1, 0, 0); + rt_assert_no_shared_uncomit(&f); + rt_fixture_fini(&f); +} + +/* ---------------------------------------------------------------------- */ +/* Shared precheck (Req 2.2, 2.3, 3.4) */ +/* ---------------------------------------------------------------------- */ + +/* bug-1893: two shared resources requested, only lic_b short (3 of 2): + * exactly one host-less NO_RESOURCE reason naming lic_b, nothing counted, + * no plan generator. */ +static void test_shared_short_one_reason_per_unmet_entry(void) +{ + rt_fixture_t f; + xce_res_reason_t* rr; + + rt_fixture_init(&f); + rt_want_shared(&f, 3.0f, 3.0f); + + TEST_ASSERT_EQUAL_INT(0, rt_run(&f)); + 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_EQUAL_INT(1, f.ctx.reason->reasons->nres_reasons); + rr = f.ctx.reason->reasons->res_based_reasons[0]; + TEST_ASSERT_NOT_NULL(rr); + TEST_ASSERT_EQUAL_STRING("", rr->host); + TEST_ASSERT_EQUAL_STRING("lic_b", rr->resource); + TEST_ASSERT_EQUAL_INT(PREASON_NO_RESOURCE, rr->reason); + + rt_assert_calls(1, 0, 0); + rt_assert_no_shared_uncomit(&f); + rt_fixture_fini(&f); +} + +/* Shared met, plan_init fails: pending, plan_fini not called, and the + * requested amounts stay counted at their resource indices. */ +static void test_shared_met_counted_and_kept_when_plan_init_fails(void) +{ + rt_fixture_t f; + + rt_fixture_init(&f); + rt_want_shared(&f, 3.0f, 1.0f); + g_init_result = -ENOMEM; + + TEST_ASSERT_EQUAL_INT(0, rt_run(&f)); + TEST_ASSERT_NULL(f.ctx.alloc); + TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->nres_reasons); + rt_assert_calls(1, 1, 0); + TEST_ASSERT_EQUAL_INT(0, g_next_calls); + TEST_ASSERT_EQUAL_FLOAT(3.0f, rt_shared_uncomit(&f, f.lic_a)); + TEST_ASSERT_EQUAL_FLOAT(1.0f, rt_shared_uncomit(&f, f.lic_b)); + rt_fixture_fini(&f); +} + +/* Shared met, plan_init succeeds with no plan: pending, plan_fini called + * exactly once, and the requested amounts stay counted. */ +static void test_shared_met_counted_and_kept_when_no_plan(void) +{ + rt_fixture_t f; + + rt_fixture_init(&f); + rt_want_shared(&f, 3.0f, 1.0f); + + TEST_ASSERT_EQUAL_INT(0, rt_run(&f)); + TEST_ASSERT_NULL(f.ctx.alloc); + rt_assert_calls(1, 1, 1); + TEST_ASSERT_EQUAL_FLOAT(3.0f, rt_shared_uncomit(&f, f.lic_a)); + TEST_ASSERT_EQUAL_FLOAT(1.0f, rt_shared_uncomit(&f, f.lic_b)); + rt_fixture_fini(&f); +} + +/* The helpers directly: a shared request of nothing is met and counts + * nothing; the reason helper keeps an existing record. */ +static void test_helpers_empty_shared_is_met(void) +{ + rt_fixture_t f; + + rt_fixture_init(&f); + TEST_ASSERT_EQUAL_INT(0, sched_alloc_ensure_reason_(&(f.ctx))); + TEST_ASSERT_NOT_NULL(f.ctx.reason); + TEST_ASSERT_EQUAL_INT( + 1, sched_alloc_prepare_shared_(&(f.sched.core), &(f.ctx))); + rt_assert_no_shared_uncomit(&f); + rt_fixture_fini(&f); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_missing_plugin_or_ctx_is_einval); + RUN_TEST(test_missing_field_is_einval_without_reason); + RUN_TEST(test_missing_plan_member_is_einval_without_reason); + RUN_TEST(test_rejecting_accepts_pends_without_checks); + RUN_TEST(test_null_accepts_reaches_plan_init); + RUN_TEST(test_reason_created_after_accepting_guard); + RUN_TEST(test_existing_reason_is_reused); + RUN_TEST(test_reason_oom_returns_enomem_without_checks); + RUN_TEST(test_shared_short_one_reason_per_unmet_entry); + RUN_TEST(test_shared_met_counted_and_kept_when_plan_init_fails); + RUN_TEST(test_shared_met_counted_and_kept_when_no_plan); + RUN_TEST(test_helpers_empty_shared_is_met); + return UNITY_END(); +} diff --git a/src/scheduler/testsuite/test_picker_pendreason_factory_audit.c b/src/scheduler/testsuite/test_picker_pendreason_factory_audit.c index 36122fe4..7bb0cb06 100644 --- a/src/scheduler/testsuite/test_picker_pendreason_factory_audit.c +++ b/src/scheduler/testsuite/test_picker_pendreason_factory_audit.c @@ -61,6 +61,7 @@ static const char* const SCANNED_RELATIVE_PATHS[] = { "include/plugin.h", "include/scheduler.h", "include/sched/types.h", + "src/alloc_runner.c", "src/core.c", "src/core_session.c", "src/decision.c",