/* * test_picker_fcfs_session.c - white-box tests for builtin_fcfs's session * hooks (begin_session/next_job/end_session) and post-allocation hooks * (on_dispatch/on_pend) -- scheduler-core spec, Req 3 AC 2's corresponding * hook-table rows, Req 3 AC 4/AC 6. * * Split out of test_picker_fcfs.c (task 2.3, tasks.md): that file already * sits at ~765 lines after tasks 2.1/2.2 (lifecycle + state-change hooks), * against this project's 800-line ceiling (typical 200-400) -- adding this * task's session/post-allocation coverage on top would push meaningfully * past 800. This file is therefore a separate, cohesive translation unit * covering exactly task 2.3's hooks, registered as its own meson test * executable (testsuite/meson.build) alongside (not instead of) * test_picker_fcfs.c. * * RED: before task 2.3, plugins/builtin_fcfs.c did not define * builtin_fcfs_begin_session/builtin_fcfs_next_job/ * builtin_fcfs_end_session/builtin_fcfs_on_dispatch/ * builtin_fcfs_on_pend -- this file failed to compile (implicit * declaration of every symbol under test). * GREEN: the hooks implement the walking/cursor/no-op contract Req 3 AC 2 * specifies, verified here by driving them directly (no live * core) and asserting on next_job()'s actual return sequence. * * #include's the real plugins/builtin_fcfs.c (white-box, same convention as * test_picker_fcfs.c) so this test exercises the real cursor logic and the * real private struct layout, not a re-implementation of either. * * builtin_fcfs.c's next_job() calls sched_queue_is_ready()/ * sched_job_is_ready() (declared in scheduler.h, defined in scheduler.c -- * design.md: "called as-is by both today's fcfs_elect_job and the new * builtin_fcfs"). This translation unit does not link scheduler.c (same as * test_picker_fcfs.c, which never needed these symbols since tasks 2.1/2.2 * never call them), so this file supplies its own test-double definitions * below -- which doubles as the only way to unit-test the "skip a not-ready * queue/job" branch, since scheduler.c's real definitions are permanently * `return 1;` stubs (scheduler.c:1267/1270, FIXME) that could never * exercise the skip path. */ #include #include #include #include #include #include "plugin.h" #include "sched/types.h" /* ---------------------------------------------------------------------- */ /* Test doubles for sched_queue_is_ready()/sched_job_is_ready() -- see file * header. Each test that needs to force a skip points the relevant global * at the queue/job under test before calling next_job(), and resets it * (via fixture_init, below) before the next test runs. */ static xce_sched_queue_t* g_not_ready_queue = NULL; static xce_sched_job_t* g_not_ready_job = NULL; int sched_queue_is_ready(xce_sched_queue_t* q) { return (q != g_not_ready_queue); } int sched_job_is_ready(xce_sched_job_t* j) { return (j != g_not_ready_job); } /* 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). * Stub to satisfy the linker -- this file's tests (task 2.3's session/ * post-allocation hooks) don't exercise registration; see * test_picker_wiring.c (task 2.4) for coverage 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 -- same convention as test_picker_fcfs.c's fixture_init: "picker" * is passed to every hook as this struct's address. Also resets the * sched_queue_is_ready()/sched_job_is_ready() test doubles to "everything * ready", so a skip forced by one test never leaks into the next. Nothing * is torn down between tests (suite convention, see meson.build: * detect_leaks=0). */ static void fixture_init(xce_sched_plugin_t* plugin) { memset(plugin, 0, sizeof(*plugin)); g_not_ready_queue = NULL; g_not_ready_job = NULL; } /* ---------------------------------------------------------------------- */ /* Task 2.3 Observable: "a full begin_session -> repeated next_job -> * end_session cycle against a fixed candidate set returns jobs in (queue * priority desc, job priority desc, queue_time asc) order and returns NULL * exactly once all candidates are exhausted." * * Fixture: three queues (mid=5, high=9, low=1, pushed out of priority * order) each with two-or-three jobs, including a same-queue priority tie * broken by queue_time asc. Expected next_job() sequence, fully spelled * out against the documented ordering rule: * high queue (prio 9): hA (prio 9), hB (prio 3) * mid queue (prio 5): mTieEarly (prio 4, t=100), mTieLate (prio 4, * t=200), mLow (prio 1) * low queue (prio 1): lOnly (prio 1) */ static void test_full_session_cycle_returns_documented_order_then_null(void) { xce_sched_plugin_t plugin; xce_sched_queue_t qmid = { .name = "mid", .priority = 5 }; xce_sched_queue_t qhigh = { .name = "high", .priority = 9 }; xce_sched_queue_t qlow = { .name = "low", .priority = 1 }; xce_sched_job_t hA = { .jobid = 101, .priority = 9, .queue_time = 10, .state = JOB_STAT_PEND, .queue = &qhigh }; xce_sched_job_t hB = { .jobid = 102, .priority = 3, .queue_time = 20, .state = JOB_STAT_PEND, .queue = &qhigh }; xce_sched_job_t mTieLate = { .jobid = 201, .priority = 4, .queue_time = 200, .state = JOB_STAT_PEND, .queue = &qmid }; xce_sched_job_t mLow = { .jobid = 202, .priority = 1, .queue_time = 5, .state = JOB_STAT_PEND, .queue = &qmid }; xce_sched_job_t mTieEarly = { .jobid = 203, .priority = 4, .queue_time = 100, .state = JOB_STAT_PEND, .queue = &qmid }; xce_sched_job_t lOnly = { .jobid = 301, .priority = 1, .queue_time = 1, .state = JOB_STAT_PEND, .queue = &qlow }; uint64_t expected[6] = { 101, 102, 203, 201, 202, 301 }; size_t i; xce_sched_job_t* got; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); /* pushed out of priority order on purpose */ TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qmid)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qhigh)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qlow)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &hA)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &hB)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &mTieLate)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &mLow)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &mTieEarly)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &lOnly)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_begin_session(&plugin)); for (i = 0; i < 6; i++) { got = builtin_fcfs_next_job(&plugin); TEST_ASSERT_NOT_NULL(got); TEST_ASSERT_EQUAL_UINT64(expected[i], got->jobid); } /* NULL-on-exhaustion, and stays NULL on a repeated call (task * Observable: "returns NULL exactly once all candidates are * exhausted"). */ TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_end_session(&plugin)); builtin_fcfs_fini(&plugin); } /* begin_session() with no queues registered at all: next_job() must return * NULL immediately, not crash walking a NULL priv->queues.head. */ static void test_next_job_returns_null_when_no_queues_registered(void) { xce_sched_plugin_t plugin; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_begin_session(&plugin)); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); builtin_fcfs_fini(&plugin); } /* A registered queue with an empty pendJobs must be walked past (not * returned as a job, not crashed on) straight to NULL. */ static void test_next_job_returns_null_when_queue_has_no_jobs(void) { xce_sched_plugin_t plugin; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_begin_session(&plugin)); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "next_job" row: "skip if sched_queue_is_ready() returns * false". Two queues, one forced not-ready via the test double -- its job * must never be returned; the other queue's job still is. */ static void test_next_job_skips_not_ready_queue(void) { xce_sched_plugin_t plugin; xce_sched_queue_t qready = { .name = "ready", .priority = 1 }; xce_sched_queue_t qnotready = { .name = "notready", .priority = 9 }; xce_sched_job_t jready = { .jobid = 1, .priority = 1, .queue_time = 1, .state = JOB_STAT_PEND, .queue = &qready }; xce_sched_job_t jnotready = { .jobid = 2, .priority = 1, .queue_time = 1, .state = JOB_STAT_PEND, .queue = &qnotready }; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qready)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &qnotready)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &jready)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &jnotready)); g_not_ready_queue = &qnotready; /* higher priority, would sort first */ TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_begin_session(&plugin)); TEST_ASSERT_EQUAL_UINT64(1, builtin_fcfs_next_job(&plugin)->jobid); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "next_job" row: "Return first job where sched_job_is_ready() * returns true" -- a single not-ready job within an otherwise-ready queue * must be skipped, not stop iteration of the rest of that queue's jobs. */ static void test_next_job_skips_not_ready_job(void) { xce_sched_plugin_t plugin; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; /* jskip outranks jok on priority, so it would be returned FIRST if it * weren't forced not-ready. */ xce_sched_job_t jskip = { .jobid = 1, .priority = 9, .queue_time = 1, .state = JOB_STAT_PEND, .queue = &q1 }; xce_sched_job_t jok = { .jobid = 2, .priority = 1, .queue_time = 1, .state = JOB_STAT_PEND, .queue = &q1 }; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_queue_update(&plugin, &q1)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &jskip)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_job_push(&plugin, &jok)); g_not_ready_job = &jskip; TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_begin_session(&plugin)); TEST_ASSERT_EQUAL_UINT64(2, builtin_fcfs_next_job(&plugin)->jobid); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); builtin_fcfs_fini(&plugin); } /* Defensive-argument contract, matching every other hook in this file: NULL * or never-init()'d pickers report -EINVAL/NULL rather than crash. */ static void test_session_hooks_reject_invalid_picker(void) { xce_sched_plugin_t plugin; fixture_init(&plugin); /* plugin.data == NULL, never init()'d */ TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_begin_session(NULL)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_begin_session(&plugin)); TEST_ASSERT_NULL(builtin_fcfs_next_job(NULL)); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_end_session(NULL)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_end_session(&plugin)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_on_dispatch(NULL, 1)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_on_dispatch(&plugin, 1)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_on_pend(NULL, 1)); TEST_ASSERT_EQUAL_INT(-EINVAL, builtin_fcfs_on_pend(&plugin, 1)); } /* Req 3 AC 2 "on_dispatch" row: a dispatched job is permanently gone -- a * later session (begin_session -> next_job) never returns it again, while * the queue's remaining candidate still comes back. */ static void test_on_dispatch_removes_job_permanently(void) { xce_sched_plugin_t plugin; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 5, .queue_time = 1, .state = JOB_STAT_PEND, .queue = &q1 }; xce_sched_job_t j2 = { .jobid = 2, .priority = 1, .queue_time = 1, .state = JOB_STAT_PEND, .queue = &q1 }; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); 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)); /* session 1: j1 elected (higher priority), allocator dispatches it */ TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_begin_session(&plugin)); TEST_ASSERT_EQUAL_UINT64(1, builtin_fcfs_next_job(&plugin)->jobid); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_dispatch(&plugin, 1)); TEST_ASSERT_EQUAL_UINT64(2, builtin_fcfs_next_job(&plugin)->jobid); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_end_session(&plugin)); /* session 2: j1 must not reappear; j2 (never dispatched) still does */ TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_begin_session(&plugin)); TEST_ASSERT_EQUAL_UINT64(2, builtin_fcfs_next_job(&plugin)->jobid); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_end_session(&plugin)); builtin_fcfs_fini(&plugin); } /* Req 3 AC 2 "on_pend" row: a true no-op -- the job reappears in the SAME * position on the very next session (not moved, not dropped, not * duplicated). */ static void test_on_pend_is_true_noop_job_reappears_same_position(void) { xce_sched_plugin_t plugin; xce_sched_queue_t q1 = { .name = "q1", .priority = 1 }; xce_sched_job_t j1 = { .jobid = 1, .priority = 5, .queue_time = 1, .state = JOB_STAT_PEND, .queue = &q1 }; xce_sched_job_t j2 = { .jobid = 2, .priority = 1, .queue_time = 1, .state = JOB_STAT_PEND, .queue = &q1 }; fixture_init(&plugin); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_init(&plugin, NULL)); 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)); /* session 1: j1 elected first, allocator can't place it, pends it */ TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_begin_session(&plugin)); TEST_ASSERT_EQUAL_UINT64(1, builtin_fcfs_next_job(&plugin)->jobid); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_on_pend(&plugin, 1)); TEST_ASSERT_EQUAL_UINT64(2, builtin_fcfs_next_job(&plugin)->jobid); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_end_session(&plugin)); /* session 2: j1 still comes back first, in the same position */ TEST_ASSERT_EQUAL_INT(0, builtin_fcfs_begin_session(&plugin)); TEST_ASSERT_EQUAL_UINT64(1, builtin_fcfs_next_job(&plugin)->jobid); TEST_ASSERT_EQUAL_UINT64(2, builtin_fcfs_next_job(&plugin)->jobid); TEST_ASSERT_NULL(builtin_fcfs_next_job(&plugin)); builtin_fcfs_fini(&plugin); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_full_session_cycle_returns_documented_order_then_null); RUN_TEST(test_next_job_returns_null_when_no_queues_registered); RUN_TEST(test_next_job_returns_null_when_queue_has_no_jobs); RUN_TEST(test_next_job_skips_not_ready_queue); RUN_TEST(test_next_job_skips_not_ready_job); RUN_TEST(test_session_hooks_reject_invalid_picker); RUN_TEST(test_on_dispatch_removes_job_permanently); RUN_TEST(test_on_pend_is_true_noop_job_reappears_same_position); return UNITY_END(); }