/* * test_picker_fcfs.c - white-box tests for builtin_fcfs's * xce_job_picker_iface_t hooks (scheduler-core spec, Req 3/4). * * Task 2.1 (this file's first content): lifecycle hooks only -- * builtin_fcfs_init()/builtin_fcfs_fini() and the private ordered-queue * state they allocate/free. * RED: before task 2.1, plugins/builtin_fcfs.c did not exist -- this * file failed to compile (no such file/symbols). * GREEN: builtin_fcfs_init()/builtin_fcfs_fini() allocate/free the * picker's private ordered-queue state exactly as Req 3 AC 2's * init/fini hook-table rows specify, with no core-table queries * (Req 3 AC 3). * * This file is expected to grow across tasks 2.2/2.3 as the remaining * hooks (on_queue_update, on_job_push, next_job, ...) land -- matching * design.md's File Structure Plan, which names a single test_picker_fcfs.c * for all of Req 3/4's hook-by-hook and ordering-equivalence coverage. * * #include's the real plugins/builtin_fcfs.c (white-box, same convention * as test_plugin_picker_registration.c's #include "../src/plugin.c") so * this test exercises the real allocator/destructor and the real private * struct layout, not a re-implementation of either. */ #include #include #include #include #include #include "plugin.h" #include "sched/types.h" /* builtin_fcfs.c's next_job() (task 2.3) calls sched_queue_is_ready()/ * sched_job_is_ready() (declared in scheduler.h, defined in scheduler.c), * which this translation unit does not link -- same as every other * white-box picker test in this directory that #includes builtin_fcfs.c * directly. This file's own tests (tasks 2.1/2.2) never call next_job(), so * a simple always-ready stub (mirroring scheduler.c's own current stub, * scheduler.c:1267/1270) is enough to satisfy the linker; * test_picker_fcfs_session.c is where the real skip-when-not-ready behavior * is exercised, via controllable test doubles. */ int sched_queue_is_ready(xce_sched_queue_t* q) { (void)q; return 1; } int sched_job_is_ready(xce_sched_job_t* j) { (void)j; return 1; } /* Task 2.4 added register_builtin_fcfs() (bottom of builtin_fcfs.c), which * calls plugin_register() (plugin.c, not linked into this white-box TU -- * this file's own tests never registered anything before task 2.4 either). * Stub matching plugin.c's real signature/calling convention closely * enough to satisfy the linker; this file's tests (tasks 2.1/2.2) don't * exercise registration, so no behavior beyond "don't crash" is needed -- * test_picker_wiring.c (task 2.4) is where register_builtin_fcfs() is * exercised against the REAL plugin_register(). */ int plugin_register(void* arg1, void* arg2) { (void)arg1; (void)arg2; return 0; } #include "../src/plugins/builtin_fcfs.c" void setUp(void) { return; } void tearDown(void) { return; } /* ---------------------------------------------------------------------- */ /* Fixture: a bare xce_sched_plugin_t -- "picker" is passed to every hook * as this struct's address, the same convention the job_elect_fn * compatibility wrapper requires (plugin.c's legacy_picker_next_job casts * its `picker` argument straight to xce_sched_plugin_t*, confirmed live by * test_plugin_picker_registration.c's * `plugin.picker_iface->next_job(&plugin)`) and the only convention that * lets one uniform call site drive both legacy-wrapped and native pickers * without knowing which kind it holds. builtin_fcfs stores its private * fcfs_plugin_data_t* in plugin.data, mirroring today's * fcfs_init/fcfs_fini (plugins/fcfs.c) exactly. Nothing is torn down * between tests (suite convention, see meson.build: detect_leaks=0 for the * normal run; the leak-checked run is separate, see task 2.1's Validation). * ---------------------------------------------------------------------- */ static void fixture_init(xce_sched_plugin_t* plugin) { memset(plugin, 0, sizeof(*plugin)); } /* Req 3 AC 2 "init" row + AC 3: init() allocates fcfs_plugin_data_t, * initializes an EMPTY ordq_t queues, and sets the iteration cursor to * NULL -- the picker holds an empty private candidate set immediately * after init(), before any state-change hook has ever run. */ static void test_builtin_fcfs_init_allocates_empty_private_state(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; int rc; fixture_init(&plugin); rc = builtin_fcfs_init(&plugin, NULL); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_NOT_NULL(plugin.data); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_NULL(priv->queues.head); /* empty ordq_t queues */ TEST_ASSERT_NULL(priv->cursor); /* cursor reset to NULL */ TEST_ASSERT_EQUAL_PTR(fcfs_queue_cmp, priv->queues.cmp); TEST_ASSERT_EQUAL_PTR(fcfs_queue_killer, priv->queues.killer); /* Paired fini() -- every init()-calling test in this file balances it * with fini() so the whole binary is leak-clean under ASAN's default * leak detector (task 2.1 Validation: the leak-checked run proves * fini() releases picker-owned memory, which only means something if * every init() this suite performs is actually matched by a fini()). */ builtin_fcfs_fini(&plugin); } /* init() never dereferences its xce_sched_core_t* core argument (task 2.1 * scope note: xce_sched_core_t is still only forward-declared -- nothing is * defined on it until group 4). Passing an obviously-invalid non-NULL * pointer and getting a clean, correct init proves init() doesn't touch * it. */ static void test_builtin_fcfs_init_does_not_dereference_core(void) { xce_sched_plugin_t plugin; int rc; fixture_init(&plugin); rc = builtin_fcfs_init(&plugin, (xce_sched_core_t*)0x1); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_NOT_NULL(plugin.data); builtin_fcfs_fini(&plugin); /* see paired-fini note above */ } /* init() rejects a NULL picker rather than crashing -- every other * lifecycle hook in this codebase (fcfs_fini, plugin_register) validates * its handle before touching it. */ static void test_builtin_fcfs_init_rejects_null_picker(void) { int rc = builtin_fcfs_init(NULL, NULL); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } /* Req 3 AC 2 "fini" row: fini() frees plugin data even when the private * ordq_t queues is empty (the common case immediately after init(), before * any queue has ever been pushed), and clears plugin->data so a * use-after-fini is observable rather than silently reusing freed * memory. */ static void test_builtin_fcfs_fini_releases_empty_state(void) { xce_sched_plugin_t plugin; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); builtin_fcfs_fini(&plugin); TEST_ASSERT_NULL(plugin.data); } /* fini(NULL) and fini() on a plugin with no private state must not crash -- * defensive-null contract every fini_fn in this codebase follows (see * plugins/fcfs.c's fcfs_fini). */ static void test_builtin_fcfs_fini_handles_null_and_unset_picker(void) { xce_sched_plugin_t plugin; builtin_fcfs_fini(NULL); fixture_init(&plugin); /* plugin.data == NULL, never init()'d */ builtin_fcfs_fini(&plugin); TEST_ASSERT_NULL(plugin.data); } /* Req 3 AC 2 "fini" row, full cascade: "Free all per-queue ordq_t * pendJobs, the ordq_t queues, and plugin data." Manually populates the * private queues/pendJobs structures the same way a future * on_queue_update/on_job_push (task 2.2) would, then relies on fini()'s * ordq_fini cascade (via fcfs_queue_killer -> ordq_fini(&q->pendJobs) -> * fcfs_job_killer per entry) to release every layer. This test's own * assertions only confirm the call completes and plugin.data is cleared; * the actual "all picker-owned memory is released" proof for this * Observable comes from running this binary under ASAN's default leak * detector (task 2.1 Validation) -- not something a Unity assertion alone * can express. */ static void test_builtin_fcfs_fini_releases_populated_queues_and_jobs(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 5 }; xce_sched_queue_t q2 = { .name = "q2", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 3 }; xce_sched_job_t j2a = { .jobid = 2, .priority = 7 }; xce_sched_job_t j2b = { .jobid = 3, .priority = 4 }; fcfs_queue_t* fq1; fcfs_queue_t* fq2; fcfs_job_t* fj1; fcfs_job_t* fj2a; fcfs_job_t* fj2b; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; /* queue 1: one pending job */ fq1 = (fcfs_queue_t*)calloc(1, sizeof(*fq1)); fq1->queue = &q1; TEST_ASSERT_EQUAL_INT(0, ordq_init(&(fq1->pendJobs), (ordq_data_killer_t)fcfs_job_killer, (ordq_cmp_fn_t)fcfs_job_cmp)); fj1 = (fcfs_job_t*)calloc(1, sizeof(*fj1)); fj1->job = &j1; TEST_ASSERT_EQUAL_INT(0, ordq_put(&(fq1->pendJobs), fj1)); TEST_ASSERT_EQUAL_INT(0, ordq_put(&(priv->queues), fq1)); /* queue 2: two pending jobs */ fq2 = (fcfs_queue_t*)calloc(1, sizeof(*fq2)); fq2->queue = &q2; TEST_ASSERT_EQUAL_INT(0, ordq_init(&(fq2->pendJobs), (ordq_data_killer_t)fcfs_job_killer, (ordq_cmp_fn_t)fcfs_job_cmp)); fj2a = (fcfs_job_t*)calloc(1, sizeof(*fj2a)); fj2a->job = &j2a; fj2b = (fcfs_job_t*)calloc(1, sizeof(*fj2b)); fj2b->job = &j2b; TEST_ASSERT_EQUAL_INT(0, ordq_put(&(fq2->pendJobs), fj2a)); TEST_ASSERT_EQUAL_INT(0, ordq_put(&(fq2->pendJobs), fj2b)); TEST_ASSERT_EQUAL_INT(0, ordq_put(&(priv->queues), fq2)); builtin_fcfs_fini(&plugin); TEST_ASSERT_NULL(plugin.data); } /* ---------------------------------------------------------------------- */ /* Task 2.2: state-change hooks (Req 3 AC 2's on_queue_update/on_job_push/ * on_job_state_change/on_job_update rows). Every test below drives the * hooks directly against a real init()'d picker and then inspects the * underlying ordq_t structures (priv->queues, and each fcfs_queue_t's * pendJobs) by walking ->head chains -- next_job() doesn't exist yet * (task 2.3), so this is the only way to observe candidate-set order this * task can prove. * RED: before task 2.2, builtin_fcfs_on_queue_update/on_job_push/ * on_job_state_change/on_job_update do not exist -- this file * fails to compile (implicit declaration / undefined symbols). * GREEN: the hooks upsert/insert/remove/reposition exactly as Req 3 AC 2 * specifies, verified by walking the resulting ordq_t chains. * ---------------------------------------------------------------------- */ /* Walks an ordq_t of fcfs_queue_t entries and collects queue names in list * order, so a test can assert on priv->queues' actual ordering (descending * queue->priority, fcfs_queue_cmp) without depending on next_job(). */ static size_t collect_queue_names(ordq_t* q, const char** out, size_t max) { ordq_item_t* it = q->head; size_t n = 0; while ((it != NULL) && (n < max)) { fcfs_queue_t* fq = (fcfs_queue_t*)it->data; out[n++] = fq->queue->name; it = it->next; } return n; } /* Walks an ordq_t of fcfs_job_t entries and collects jobids in list order, * so a test can assert on a queue's pendJobs actual ordering (priority * desc, queue_time asc, fcfs_job_cmp). */ static size_t collect_jobids(ordq_t* q, uint64_t* out, size_t max) { ordq_item_t* it = q->head; size_t n = 0; while ((it != NULL) && (n < max)) { fcfs_job_t* fj = (fcfs_job_t*)it->data; out[n++] = fj->job->jobid; it = it->next; } return n; } /* Req 3 AC 2 "on_queue_update" row, unseen-name branch: allocates a new * fcfs_queue_t pointing at `queue`, with an empty pendJobs initialized on * fcfs_job_cmp/fcfs_job_killer -- ready for a later on_job_push, task 2.1's * own comparators reused rather than re-implemented. */ static void test_on_queue_update_inserts_new_queue_with_empty_pendjobs(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 5 }; fcfs_queue_t* fq; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_NOT_NULL(priv->queues.head); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_EQUAL_PTR(&q1, fq->queue); TEST_ASSERT_NULL(fq->pendJobs.head); TEST_ASSERT_EQUAL_PTR(fcfs_job_cmp, fq->pendJobs.cmp); TEST_ASSERT_EQUAL_PTR(fcfs_job_killer, fq->pendJobs.killer); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "on_queue_update" row + Req 3 AC 4 "inter-queue by descending * queue->priority": three queues pushed out of priority order must come * back out of priv->queues sorted highest-priority-first. */ static void test_on_queue_update_orders_queues_by_priority_desc(void) { xce_sched_plugin_t plugin; xce_sched_queue_t qlow = { .name = "low", .priority = 1 }; xce_sched_queue_t qhigh = { .name = "high", .priority = 9 }; xce_sched_queue_t qmid = { .name = "mid", .priority = 5 }; const char* names[3]; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qlow)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qhigh)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qmid)); TEST_ASSERT_EQUAL_UINT( 3, collect_queue_names(&(((fcfs_plugin_data_t*)plugin.data)->queues), names, 3)); TEST_ASSERT_EQUAL_STRING("high", names[0]); TEST_ASSERT_EQUAL_STRING("mid", names[1]); TEST_ASSERT_EQUAL_STRING("low", names[2]); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "on_queue_update" row, known-name branch: "If queue exists, * update its priority and reorder." Raising q1's priority above q2's (by * mutating the same xce_sched_queue_t in place, the way * sched_update_existing_queue does) and re-calling on_queue_update must * move q1 ahead of q2 -- without allocating a second fcfs_queue_t for * "q1" (checked by identity: the head entry's pointer is still &q1). */ static void test_on_queue_update_reorders_existing_queue_on_priority_change(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_queue_t q2 = { .name = "q2", .priority = 5 }; const char* names[2]; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q2)); TEST_ASSERT_EQUAL_UINT(2, collect_queue_names(&(priv->queues), names, 2)); TEST_ASSERT_EQUAL_STRING("q2", names[0]); /* q2 (5) ahead of q1 (1) */ q1.priority = 9; /* in-place mutation, mirrors the live core's update */ TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_UINT(2, collect_queue_names(&(priv->queues), names, 2)); TEST_ASSERT_EQUAL_STRING("q1", names[0]); /* now q1 (9) ahead of q2 (5) */ TEST_ASSERT_EQUAL_STRING("q2", names[1]); builtin_fcfs_fini(&plugin); } /* Defensive-argument contract: NULL picker, NULL queue, an unnamed queue, * and a picker that was never init()'d must all report -EINVAL rather than * crash -- same convention as builtin_fcfs_init's own NULL check. */ static void test_on_queue_update_rejects_invalid_args(void) { xce_sched_plugin_t plugin; xce_sched_queue_t q = { .name = "q1", .priority = 1 }; xce_sched_queue_t noname = { .name = NULL, .priority = 1 }; fixture_init(&plugin); /* plugin.data == NULL, never init()'d */ TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_on_queue_update(NULL, &q)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_on_queue_update(&plugin, NULL)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_on_queue_update(&plugin, &noname)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_on_queue_update(&plugin, &q)); } /* Req 3 AC 2 "on_job_push" row: a pending job is wrapped and inserted into * its own queue's pendJobs (found via job->queue->name). */ static void test_on_job_push_adds_pending_job_to_its_queue(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 42, .priority = 3, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &q1 }; fcfs_queue_t* fq; uint64_t jobids[1]; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j1)); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_EQUAL_UINT(1, collect_jobids(&(fq->pendJobs), jobids, 1)); TEST_ASSERT_EQUAL_UINT64(42, jobids[0]); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "on_job_push" row + Req 3 AC 4 "intra-queue by priority desc, * queue_time asc": three pending jobs pushed out of order come back sorted * by priority desc, then (for the priority tie) queue_time asc. */ static void test_on_job_push_orders_jobs_priority_desc_then_queue_time_asc(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; /* tie on priority (5): j_tie_late (queue_time 200) must sort AFTER * j_tie_early (queue_time 100) despite being pushed first. */ xce_sched_job_t j_tie_late = { .jobid = 1, .priority = 5, .queue_time = 200, .state = JOB_STAT_PEND, .queue = &q1 }; xce_sched_job_t j_low = { .jobid = 2, .priority = 1, .queue_time = 50, .state = JOB_STAT_PEND, .queue = &q1 }; xce_sched_job_t j_tie_early = { .jobid = 3, .priority = 5, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &q1 }; fcfs_queue_t* fq; uint64_t jobids[3]; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j_tie_late)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j_low)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j_tie_early)); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_EQUAL_UINT(3, collect_jobids(&(fq->pendJobs), jobids, 3)); TEST_ASSERT_EQUAL_UINT64(3, jobids[0]); /* j_tie_early: prio 5, t=100 */ TEST_ASSERT_EQUAL_UINT64(1, jobids[1]); /* j_tie_late: prio 5, t=200 */ TEST_ASSERT_EQUAL_UINT64(2, jobids[2]); /* j_low: prio 1 */ builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "on_job_push" row's JOB_STAT_PEND guard: a non-pending job * (state 0, no JOB_STAT_PEND bit) must not be added to any candidate set. */ static void test_on_job_push_skips_non_pending_job(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 3, .queue_time = 100, .state = 0, /* not pending */ .queue = &q1 }; fcfs_queue_t* fq; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j1)); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_NULL(fq->pendJobs.head); builtin_fcfs_fini(&plugin); } /* A pending job whose queue was never registered via on_queue_update has * nowhere to go -- -EINVAL, not a crash or a silent drop into the wrong * queue. */ static void test_on_job_push_rejects_unknown_queue(void) { xce_sched_plugin_t plugin; xce_sched_queue_t unregistered = { .name = "ghost", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 3, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &unregistered }; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_on_job_push(&plugin, &j1)); builtin_fcfs_fini(&plugin); } /* A duplicate JOBS_PUSH delivery for the same still-pending job must not * create a second candidate entry. */ static void test_on_job_push_ignores_duplicate_push_of_same_job(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 7, .priority = 3, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &q1 }; fcfs_queue_t* fq; uint64_t jobids[2]; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j1)); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_EQUAL_UINT(1, collect_jobids(&(fq->pendJobs), jobids, 2)); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "on_job_state_change" row: a job leaving pending state * (EXIT/FAIL/DONE) is removed from its queue's candidate set. */ static void test_on_job_state_change_removes_job_when_leaving_pending(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 3, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &q1 }; fcfs_queue_t* fq; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j1)); j1.state = JOB_STAT_DONE; TEST_ASSERT_EQUAL_INT( 0, builtin_fcfs_on_job_state_change(&plugin, &j1, JOB_STAT_DONE)); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_NULL(fq->pendJobs.head); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "on_job_state_change" row: a job returning to PEND is re-added * to its queue's candidate set, in its correctly ordered position among * jobs already there. */ static void test_on_job_state_change_readds_job_in_order_when_returning_to_pending(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t jlow = { .jobid = 1, .priority = 1, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &q1 }; xce_sched_job_t jresurrected = { .jobid = 2, .priority = 9, .queue_time = 50, .state = JOB_STAT_DONE, .queue = &q1 }; fcfs_queue_t* fq; uint64_t jobids[2]; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &jlow)); /* jresurrected starts DONE -- never pushed, not a candidate yet. */ jresurrected.state = JOB_STAT_PEND; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_state_change( &plugin, &jresurrected, JOB_STAT_PEND)); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_EQUAL_UINT(2, collect_jobids(&(fq->pendJobs), jobids, 2)); TEST_ASSERT_EQUAL_UINT64(2, jobids[0]); /* higher priority first */ TEST_ASSERT_EQUAL_UINT64(1, jobids[1]); builtin_fcfs_fini(&plugin); } /* Idempotency: on_job_state_change(..., PEND) called twice in a row for a * job that is already a candidate must not create a duplicate entry. */ static void test_on_job_state_change_is_idempotent_when_already_pending(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 3, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &q1 }; fcfs_queue_t* fq; uint64_t jobids[2]; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j1)); TEST_ASSERT_EQUAL_INT( 0, builtin_fcfs_on_job_state_change(&plugin, &j1, JOB_STAT_PEND)); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_EQUAL_UINT(1, collect_jobids(&(fq->pendJobs), jobids, 2)); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "on_job_update" row, priority-change case: raising j1's * priority above j2's (mutated in place, mirroring the live core) and * calling on_job_update must move j1 ahead of j2 within their shared * queue. */ static void test_on_job_update_repositions_job_within_same_queue_on_priority_change(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 1, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &q1 }; xce_sched_job_t j2 = { .jobid = 2, .priority = 5, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &q1 }; fcfs_queue_t* fq; uint64_t jobids[2]; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j2)); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_EQUAL_UINT(2, collect_jobids(&(fq->pendJobs), jobids, 2)); TEST_ASSERT_EQUAL_UINT64(2, jobids[0]); /* j2 (5) ahead of j1 (1) */ j1.priority = 9; /* in-place mutation, mirrors sched_update_existing_job */ TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_update(&plugin, &j1)); TEST_ASSERT_EQUAL_UINT(2, collect_jobids(&(fq->pendJobs), jobids, 2)); TEST_ASSERT_EQUAL_UINT64(1, jobids[0]); /* now j1 (9) ahead of j2 (5) */ TEST_ASSERT_EQUAL_UINT64(2, jobids[1]); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "on_job_update" row, queue-reassignment case: moving a job's * `queue` field to a different registered queue (mutated in place) and * calling on_job_update must remove it from the old queue's pendJobs and * insert it into the new queue's pendJobs. */ static void test_on_job_update_moves_job_to_new_queue_on_queue_reassignment(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t qa = { .name = "qa", .priority = 1 }; xce_sched_queue_t qb = { .name = "qb", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 3, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &qa }; fcfs_queue_t* fqa; fcfs_queue_t* fqb; uint64_t jobids[1]; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qa)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qb)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &j1)); j1.queue = &qb; /* in-place reassignment */ TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_update(&plugin, &j1)); fqa = fcfs_find_queue(priv, "qa"); fqb = fcfs_find_queue(priv, "qb"); TEST_ASSERT_NULL(fqa->pendJobs.head); /* gone from the old queue */ TEST_ASSERT_EQUAL_UINT(1, collect_jobids(&(fqb->pendJobs), jobids, 1)); TEST_ASSERT_EQUAL_UINT64(1, jobids[0]); /* present in the new queue */ builtin_fcfs_fini(&plugin); } /* on_job_update on a job that is not (or no longer) pending must not add * it to any queue's candidate set -- pendJobs holds only pending jobs. */ static void test_on_job_update_is_noop_for_non_pending_job(void) { xce_sched_plugin_t plugin; fcfs_plugin_data_t* priv; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 3, .queue_time = 100, .state = JOB_STAT_RUN, /* not pending */ .queue = &q1 }; fcfs_queue_t* fq; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); priv = (fcfs_plugin_data_t*)plugin.data; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_update(&plugin, &j1)); fq = (fcfs_queue_t*)priv->queues.head->data; TEST_ASSERT_NULL(fq->pendJobs.head); builtin_fcfs_fini(&plugin); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_builtin_fcfs_init_allocates_empty_private_state); RUN_TEST(test_builtin_fcfs_init_does_not_dereference_core); RUN_TEST(test_builtin_fcfs_init_rejects_null_picker); RUN_TEST(test_builtin_fcfs_fini_releases_empty_state); RUN_TEST(test_builtin_fcfs_fini_handles_null_and_unset_picker); RUN_TEST(test_builtin_fcfs_fini_releases_populated_queues_and_jobs); RUN_TEST(test_on_queue_update_inserts_new_queue_with_empty_pendjobs); RUN_TEST(test_on_queue_update_orders_queues_by_priority_desc); RUN_TEST(test_on_queue_update_reorders_existing_queue_on_priority_change); RUN_TEST(test_on_queue_update_rejects_invalid_args); RUN_TEST(test_on_job_push_adds_pending_job_to_its_queue); RUN_TEST(test_on_job_push_orders_jobs_priority_desc_then_queue_time_asc); RUN_TEST(test_on_job_push_skips_non_pending_job); RUN_TEST(test_on_job_push_rejects_unknown_queue); RUN_TEST(test_on_job_push_ignores_duplicate_push_of_same_job); RUN_TEST(test_on_job_state_change_removes_job_when_leaving_pending); RUN_TEST( test_on_job_state_change_readds_job_in_order_when_returning_to_pending); RUN_TEST(test_on_job_state_change_is_idempotent_when_already_pending); RUN_TEST( test_on_job_update_repositions_job_within_same_queue_on_priority_change); RUN_TEST(test_on_job_update_moves_job_to_new_queue_on_queue_reassignment); RUN_TEST(test_on_job_update_is_noop_for_non_pending_job); return UNITY_END(); }