myxce/src/scheduler/testsuite/test_picker_fcfs_session.c
Rongsong Shen 7ee9a16f53
feat(scheduler-core): wire builtin_fcfs as registered default picker (task 2.4)
Registers builtin_fcfs at scheduler startup and makes it the default
job_pick_plugin, replacing the hardcoded "xce_sched_fcfs" literal in
sched_job_picker() with a fail-loud lookup through sched_resolve_job_picker().
The picker thread now branches legacy job_elect_fn plugins (delegated
unchanged, preserving their self-contained wbuf-driven loop) from native
xce_job_picker_iface_t pickers (driven by a new per-session loop), and the
allocator now calls on_dispatch/on_pend for every picker as Req 1 AC 4
requires. Closes task 2.3's flagged picker/allocator concurrency race with a
mutex on fcfs_plugin_data_t, and adds cancel points to next_job().
2026-08-03 08:51:45 +00:00

409 lines
18 KiB
C

/*
* 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#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();
}