myxce/src/scheduler/testsuite/test_picker_fcfs.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

801 lines
33 KiB
C

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