Retypes xce_sched_ctx_t.sched (sched/types.h) from void* to xce_sched_core_t* outright, and sched_try_alloc_on_host_() (resource.c) to take xce_sched_core_t* directly -- completing what tasks 1.2/1.3 left as temporary &(sched->core) call-site wraps. Removes the two resulting struct-prefix casts entirely (schedobj.c's sched_ctx_set_primary(), host_iter.c's sched_host_iter_next()): both now pass core state through honestly, with no cast. Updates every sched_ctx_init()/sched_try_alloc_on_host_() call site tree-wide (scheduler.c, parjob_allocator.c, seqjob_allocator.c) with zero change to host_iter/parjob/seqjob's own host-iteration/ bucket-construction algorithm. Adds a driver-parity test for sched_try_alloc_on_host_() proving identical grant/deny results from normal-mode-shaped and simulation-mode-shaped core state. Flags (for task 2.3) a confirmed-dead, type-confused write at plugins/limit.c:319 (ctx.sched = plugin->sched) -- never read today, left untouched since limit.c is task 2.3's boundary, not this one's. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
423 lines
13 KiB
C
423 lines
13 KiB
C
/*
|
|
* test_job_update.c - white-box tests for the notify single-job in-place
|
|
* update path (sched_update_existing_job), remediation of bug-765 / gap G-8+G-4.
|
|
*
|
|
* Strategy: pull the REAL sched_update_existing_job into this TU by including
|
|
* schedobj.c, then drive it against a REAL sched->jobs kvmgr seeded with a
|
|
* REAL xce_sched_job_t. The scheduler-internal externs schedobj.c references
|
|
* but which are NOT under test (and live in other .c files not linked here)
|
|
* are stubbed below. sched_get_job/sched_add_job semantics are exercised for
|
|
* real via the kvmgr key_find/key_add path.
|
|
*
|
|
* Asserts:
|
|
* - existing jobid : fields mutate IN PLACE (same object, runtime state kept)
|
|
* - unknown jobid : -ENOENT and NO job created
|
|
* - NULL payload : -EINVAL
|
|
*/
|
|
#define UNIT_TEST 1
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "kvmgr.h"
|
|
#include "scheduler.h"
|
|
#include "xstring.h"
|
|
|
|
/* Pull the real implementation under test into this TU. */
|
|
#include "../src/schedobj.c"
|
|
|
|
/* Stubs for scheduler-internal externs schedobj.c references but which are
|
|
* defined in other .c files not linked into this test. (make_sched_obj and
|
|
* kill_sched_job are static in schedobj.c and come in via the #include.)
|
|
* sched_get_user/sched_get_project/sched_get_queue/sched_add_user/
|
|
* sched_add_project/sched_add_job/sched_get_job relocated to core.h
|
|
* (scheduler-core spec, task 4.1) -- now operate on xce_sched_core_t*, not
|
|
* xce_scheduler_t*. */
|
|
xce_sched_user_t* sched_get_user(xce_sched_core_t* s, char* n)
|
|
{
|
|
(void)s; (void)n; return NULL;
|
|
}
|
|
xce_sched_project_t* sched_get_project(xce_sched_core_t* s, char* n)
|
|
{
|
|
(void)s; (void)n; return NULL;
|
|
}
|
|
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;
|
|
}
|
|
xce_job_account_t* make_job_account(xce_job_alloc_t* a) { (void)a; return NULL; }
|
|
void free_job_account(xce_job_account_t* a) { (void)a; }
|
|
/* resource-accounting.md's reconciliation (resource.c, not linked here --
|
|
* this file only #includes schedobj.c). Never actually reached: make_job_
|
|
* account() above always returns NULL, so every test job's job->accounts
|
|
* stays NULL and sched_apply_job_rusage()'s own NULL-guard skips the call
|
|
* entirely -- link-satisfying stub only, same rationale as every other
|
|
* stub in this block. */
|
|
int sched_reconcile_job_resource_usage(xce_sched_core_t* c,
|
|
xce_job_account_t* a,
|
|
xce_job_rusages_t* r)
|
|
{
|
|
(void)c; (void)a; (void)r; return 0;
|
|
}
|
|
int sched_add_user(xce_sched_core_t* s, xce_sched_user_t* u)
|
|
{
|
|
(void)s;
|
|
(void)u;
|
|
return 0;
|
|
}
|
|
int sched_add_project(xce_sched_core_t* s, xce_sched_project_t* p)
|
|
{
|
|
(void)s;
|
|
(void)p;
|
|
return 0;
|
|
}
|
|
void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; }
|
|
int plugin_prepare_allocation(xce_sched_core_t* s, xce_sched_ctx_t* c) { (void)s; (void)c; return 0; }
|
|
|
|
/* sched_get_queue: a tiny by-name lookup over a fixed test queue set. */
|
|
static xce_sched_queue_t* g_test_queues[8];
|
|
static int g_test_nqueues;
|
|
|
|
xce_sched_queue_t* sched_get_queue(xce_sched_core_t* s, char* name)
|
|
{
|
|
int i;
|
|
(void)s;
|
|
if (name == NULL) {
|
|
return NULL;
|
|
}
|
|
for (i = 0; i < g_test_nqueues; i++) {
|
|
if (g_test_queues[i] != NULL && g_test_queues[i]->name != NULL
|
|
&& strcmp(g_test_queues[i]->name, name) == 0) {
|
|
return g_test_queues[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* REAL kvmgr-backed job add/get - same rtti path the production code uses. */
|
|
int sched_add_job(xce_sched_core_t* core, xce_sched_job_t* j)
|
|
{
|
|
int rc;
|
|
lock(core->jobs, RTTI_LOCK_WRITE);
|
|
rc = key_add(core->jobs, (void*)j->jobid, j);
|
|
unlock(core->jobs);
|
|
return rc;
|
|
}
|
|
|
|
xce_sched_job_t* sched_get_job(xce_sched_core_t* core, uint64_t jobid)
|
|
{
|
|
int rc;
|
|
xce_sched_job_t* job = NULL;
|
|
lock(core->jobs, RTTI_LOCK_READ);
|
|
rc = key_find(core->jobs, (void*)jobid, (void**)&job);
|
|
unlock(core->jobs);
|
|
if (rc <= 0) {
|
|
return NULL;
|
|
}
|
|
return job;
|
|
}
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void)
|
|
{
|
|
printf("\n-----------------------------------------------------\n");
|
|
return;
|
|
}
|
|
|
|
/* Test fixture helpers */
|
|
|
|
static void fixture_init(xce_scheduler_t* sched)
|
|
{
|
|
kvmgr_params_t jobs_params = { .key_type = KVMGR_KEY_INT,
|
|
.nslots = 59,
|
|
.delta = 16,
|
|
.datakiller = kill_sched_job };
|
|
kvmgr_params_t res_params = { .key_type = KVMGR_KEY_STRING,
|
|
.nslots = 16,
|
|
.delta = 8,
|
|
.datakiller = NULL };
|
|
|
|
memset(sched, 0, sizeof(*sched));
|
|
resreq_init(1);
|
|
xstring_table_init(1024 * 16);
|
|
|
|
sched->core.jobs = makeInstance(&xkvmgr_type, &jobs_params);
|
|
sched->core.resmgr = makeInstance(&xkvmgr_type, &res_params);
|
|
|
|
g_test_nqueues = 0;
|
|
memset(g_test_queues, 0, sizeof(g_test_queues));
|
|
}
|
|
|
|
static xce_sched_queue_t* make_test_queue(const char* name, int32_t prio)
|
|
{
|
|
xce_sched_queue_t* q = (xce_sched_queue_t*)calloc(1, sizeof(*q));
|
|
q->name = makeString((char*)name);
|
|
q->priority = prio;
|
|
g_test_queues[g_test_nqueues++] = q;
|
|
return q;
|
|
}
|
|
|
|
/* A seeded existing job carrying live runtime state we must not lose. */
|
|
static xce_sched_job_t* seed_job(xce_scheduler_t* sched, uint64_t jobid)
|
|
{
|
|
xce_sched_job_t* job = (xce_sched_job_t*)calloc(1, sizeof(*job));
|
|
job->jobid = jobid;
|
|
job->state = JOB_STAT_PEND;
|
|
job->priority = 1;
|
|
job->resreq = (xce_resreq_t*)calloc(1, sizeof(*job->resreq));
|
|
/* schedobj is live runtime state that MUST survive an in-place update */
|
|
job->schedobj = (xce_sched_obj_t*)calloc(1, sizeof(*job->schedobj));
|
|
sched_add_job(&(sched->core), job);
|
|
return job;
|
|
}
|
|
|
|
static xce_job_t* make_update_payload(uint64_t jobid, unsigned int state,
|
|
const char* queue)
|
|
{
|
|
xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j));
|
|
j->jobid = jobid;
|
|
j->state = state;
|
|
j->submit = (job_submit_data_t*)calloc(1, sizeof(*j->submit));
|
|
if (queue != NULL) {
|
|
j->submit->queue = makeString((char*)queue);
|
|
}
|
|
return j;
|
|
}
|
|
|
|
/* Tests */
|
|
|
|
static void
|
|
test_update_existing_job_mutates_in_place(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
make_test_queue("normal", 5);
|
|
xce_sched_job_t* job = seed_job(&sched, 42);
|
|
xce_sched_obj_t* schedobj = job->schedobj; /* capture live runtime ptr */
|
|
|
|
xce_job_t* upd = make_update_payload(42, JOB_STAT_RUN, "normal");
|
|
|
|
int rc = sched_update_existing_job(&sched, upd);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
/* SAME object mutated in place, not replaced */
|
|
TEST_ASSERT_EQUAL_PTR(job, sched_get_job(&sched.core, 42));
|
|
/* mutable fields updated */
|
|
TEST_ASSERT_EQUAL_UINT(JOB_STAT_RUN, job->state);
|
|
TEST_ASSERT_EQUAL_PTR(g_test_queues[0], job->queue);
|
|
TEST_ASSERT_EQUAL_INT(5, job->priority);
|
|
/* live runtime state preserved (object identity, not a fresh calloc) */
|
|
TEST_ASSERT_EQUAL_PTR(schedobj, job->schedobj);
|
|
|
|
/* upd->submit->queue is a makeString()-interned string owned by the
|
|
* xstring table — not caller-freeable; only free the calloc'd structs. */
|
|
free(upd->submit);
|
|
free(upd);
|
|
}
|
|
|
|
static void
|
|
test_update_unknown_job_returns_enoent_no_create(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
/* No job seeded for jobid 999 */
|
|
xce_job_t* upd = make_update_payload(999, JOB_STAT_RUN, NULL);
|
|
|
|
int rc = sched_update_existing_job(&sched, upd);
|
|
|
|
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
|
|
/* And NO job was created via the update path */
|
|
TEST_ASSERT_NULL(sched_get_job(&sched.core, 999));
|
|
|
|
free(upd->submit);
|
|
free(upd);
|
|
}
|
|
|
|
static void
|
|
test_update_null_payload_returns_einval(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
int rc = sched_update_existing_job(&sched, NULL);
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
|
}
|
|
|
|
/* Job rusage storage (Req 6 / bug-765 G-5) */
|
|
|
|
static xce_job_rusages_t* make_rusages(uint64_t jobid)
|
|
{
|
|
/* count==0 / usages==NULL keeps free_job_rusages a clean structural free. */
|
|
xce_job_rusages_t* r = (xce_job_rusages_t*)calloc(1, sizeof(*r));
|
|
r->jobid = jobid;
|
|
return r;
|
|
}
|
|
|
|
static void
|
|
test_rusage_stored_on_existing_job(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
xce_sched_job_t* job = seed_job(&sched, 42);
|
|
xce_job_rusages_t* ru = make_rusages(42);
|
|
|
|
int rc = sched_apply_job_rusage(&sched, ru);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
/* stored ON the job (ownership transferred to the job) */
|
|
TEST_ASSERT_EQUAL_PTR(ru, job->rusages);
|
|
/* the job object itself is unchanged identity */
|
|
TEST_ASSERT_EQUAL_PTR(job, sched_get_job(&sched.core, 42));
|
|
/* job owns ru now; kill_sched_job/free_job_rusages will release it */
|
|
}
|
|
|
|
static void
|
|
test_rusage_replace_frees_prior(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
xce_sched_job_t* job = seed_job(&sched, 7);
|
|
xce_job_rusages_t* ru1 = make_rusages(7);
|
|
xce_job_rusages_t* ru2 = make_rusages(7);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, sched_apply_job_rusage(&sched, ru1));
|
|
TEST_ASSERT_EQUAL_PTR(ru1, job->rusages);
|
|
/* second report replaces the first (prior freed, no leak/crash) */
|
|
TEST_ASSERT_EQUAL_INT(0, sched_apply_job_rusage(&sched, ru2));
|
|
TEST_ASSERT_EQUAL_PTR(ru2, job->rusages);
|
|
}
|
|
|
|
static void
|
|
test_rusage_unknown_job_returns_enoent(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
xce_job_rusages_t* ru = make_rusages(999); /* no such job */
|
|
|
|
int rc = sched_apply_job_rusage(&sched, ru);
|
|
|
|
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
|
|
/* on -ENOENT the caller retains ownership */
|
|
free_JobRusages_ptr(&ru);
|
|
}
|
|
|
|
static void
|
|
test_rusage_null_returns_einval(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_apply_job_rusage(&sched, NULL));
|
|
}
|
|
|
|
/* Queue in-place update (Req 4 / task 9.5 — REAL, exercises the linked
|
|
* schedobj.c sched_update_existing_queue, not a local stub) */
|
|
|
|
static void
|
|
test_update_existing_queue_real(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
xce_sched_queue_t* q = make_test_queue("q1", 1);
|
|
|
|
xce_queue_t upd;
|
|
memset(&upd, 0, sizeof(upd));
|
|
upd.queue = makeString("q1");
|
|
upd.status = 2;
|
|
upd.priority = 9;
|
|
upd.accept_interval = 5;
|
|
upd.sched_delay = 3;
|
|
upd.resreq = NULL; /* skip the resreq makeString branch */
|
|
|
|
int rc = sched_update_existing_queue(&sched, &upd);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_EQUAL_UINT(2, q->status);
|
|
TEST_ASSERT_EQUAL_INT(9, q->priority);
|
|
TEST_ASSERT_EQUAL_INT(5, q->accept_interval);
|
|
TEST_ASSERT_EQUAL_INT(3, q->sched_delay);
|
|
}
|
|
|
|
static void
|
|
test_update_unknown_queue_returns_enoent(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
xce_queue_t upd;
|
|
memset(&upd, 0, sizeof(upd));
|
|
upd.queue = makeString("nope"); /* no such queue seeded */
|
|
|
|
TEST_ASSERT_EQUAL_INT(-ENOENT, sched_update_existing_queue(&sched, &upd));
|
|
}
|
|
|
|
static void
|
|
test_update_queue_null_returns_einval(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_update_existing_queue(&sched, NULL));
|
|
}
|
|
|
|
/* Allocation-mode extension point (scheduler-allocation spec, task 1.3,
|
|
* Req 9.5). Pulls the REAL sched_ctx_init() (via the schedobj.c #include
|
|
* above) to prove memset-zero-init alone defaults a freshly-initialized
|
|
* xce_sched_ctx_t's alloc_mode to XCE_ALLOC_NORMAL -- no explicit
|
|
* assignment needed in sched_ctx_init() since XCE_ALLOC_NORMAL is the
|
|
* enum's zero value. */
|
|
|
|
static void
|
|
test_ctx_init_defaults_alloc_mode_to_normal(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
fixture_init(&sched);
|
|
|
|
/* XCE_ALLOC_NORMAL must be exactly 0 for memset-zero-init to default
|
|
* to it -- pin the enum value itself, not just the observed field. */
|
|
TEST_ASSERT_EQUAL_INT(0, XCE_ALLOC_NORMAL);
|
|
|
|
xce_sched_obj_t obj;
|
|
memset(&obj, 0, sizeof(obj));
|
|
obj.tag = XCE_RESREQ_PRIMARY;
|
|
|
|
xce_sched_ctx_t ctx;
|
|
/* 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);
|
|
TEST_ASSERT_EQUAL_INT(XCE_ALLOC_NORMAL, ctx.alloc_mode);
|
|
|
|
sched_ctx_fini(&ctx);
|
|
}
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
(void)argc; (void)argv;
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_update_existing_job_mutates_in_place, 0);
|
|
RUN_TEST(test_update_unknown_job_returns_enoent_no_create, 0);
|
|
RUN_TEST(test_update_null_payload_returns_einval, 0);
|
|
RUN_TEST(test_rusage_stored_on_existing_job, 0);
|
|
RUN_TEST(test_rusage_replace_frees_prior, 0);
|
|
RUN_TEST(test_rusage_unknown_job_returns_enoent, 0);
|
|
RUN_TEST(test_rusage_null_returns_einval, 0);
|
|
RUN_TEST(test_update_existing_queue_real, 0);
|
|
RUN_TEST(test_update_unknown_queue_returns_enoent, 0);
|
|
RUN_TEST(test_update_queue_null_returns_einval, 0);
|
|
RUN_TEST(test_ctx_init_defaults_alloc_mode_to_normal, 0);
|
|
return UNITY_END();
|
|
}
|