/* * 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 #include #include #include #include #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); } /* bug-1858: q->resreq is an interned makeString() instance, so replacing * it must dereference() the old value -- a plain free() is an ASAN * bad-free (pointer is past the xstring object header). */ static void test_update_existing_queue_replaces_interned_resreq(void) { xce_scheduler_t sched; fixture_init(&sched); xce_sched_queue_t* q = make_test_queue("q1", 1); q->resreq = makeString("type == any"); xce_queue_t upd; memset(&upd, 0, sizeof(upd)); upd.queue = makeString("q1"); upd.resreq = makeString("type == local"); TEST_ASSERT_EQUAL_INT(0, sched_update_existing_queue(&sched, &upd)); TEST_ASSERT_EQUAL_STRING("type == local", q->resreq); } 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_existing_queue_replaces_interned_resreq, 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(); }