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