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().
409 lines
18 KiB
C
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();
|
|
}
|