/* * test_driver_session_ordering.c - the safety-property proof referenced by * name in core.h's xce_core_begin_session()/xce_core_signal_go() doc * comment, driver.h's xce_sched_driver_run_normal_mode_round() doc comment, * and driver.c's own header comment (scheduler-core spec, task 5.2, Req 1 * AC 7 / Req 5 AC 9). * * The property under test: a normal-mode scheduling round MUST call * plugin_begin_session()/sched_account_jobs() strictly AFTER * xce_core_begin_session()'s resval reset and strictly BEFORE * xce_core_signal_go() wakes the picker thread -- otherwise the picker * could observe a session whose resource-in-use accounting was reset to * zero but not yet re-summed from currently running jobs, letting the * picker/allocator over-allocate resources already consumed by running * jobs. plugin_end_session() must likewise actually run, once per * completed round. * * Function under test: the REAL xce_sched_driver_run_normal_mode_round() * (driver.c) -- NOT a hand-copied reimplementation of its call sequence. * That function started life as a `static` helper inside main.c's loop * body, unreachable from any test binary; it was relocated (same file * content, task 5.2's own scope) into driver.c and exported via driver.h * specifically so this safety property could be proven against the real * production code path instead of a copy that could silently drift from * it over time -- see driver.h's declaration comment for the full * rationale. This file white-box #includes ../src/driver.c (same * convention as test_driver_translate.c/test_driver_decision_cb.c) to pull * that real function into this TU. * * Design: real picker thread + real stand-in allocator thread, NOT a * sequential drain in the test's own thread -- same reasoning * test_core_full_cycle.c's own header comment gives for its identical * design choice, and the same reason applies with EXTRA force here: * xce_sched_driver_run_normal_mode_round() is now a single BLOCKING call * (it does not return control to its caller between xce_core_signal_go() * and the decision-drain loop the way test_core_full_cycle.c's own test * function -- which plays driver AND allocator itself -- can), so the * calling (main/test) thread cannot ALSO act as the stand-in allocator the * way test_core_full_cycle.c's test function does. This test therefore * spawns two collaborator threads: * * - picker_thread_fn(): reproduces sched_job_picker()'s real per-session * body verbatim (identical to test_core_full_cycle.c's * test_picker_thread_fn), single-shot for this test's one session. * This fixture pushes no jobs, so the picker's own next_job() returns * NULL immediately -- but the wbuf rendezvous (cbuf_get(&core->wbuf)) * this thread blocks on before doing ANYTHING else is exactly the * cross-thread synchronization point that makes the ordering * assertions below sound: whatever the driver did in its own thread * BEFORE xce_core_signal_go()'s cbuf_put(&core->wbuf) is guaranteed * visible-before whatever this thread does AFTER its cbuf_get(&wbuf) * returns (the SAME reasoning test_core_full_cycle.c's own header * comment documents for its analogous claim). * * - stand_in_allocator_thread_fn(): drains the picker's jbuf * end-of-session sentinel (jobid == 0 -- the only jbuf item this * fixture ever produces, since no jobs are pushed) and pushes the * matching NULL end-of-session marker onto core->decisions, exactly * as sched_allocator() (scheduler.c) does for a session that elects no * jobs. This is what unblocks xce_sched_driver_run_normal_mode_round()'s * own decision-drain loop so the round can complete and reach * plugin_end_session()/xce_core_end_session(). * * plugin_begin_session()/plugin_end_session() are the REAL production * functions (white-box #include of ../src/plugin.c), driven through a * genuinely test-registered xce_sched_plugin_t (g_observer_plugin below) * whose begin_session_fn/end_session_fn set an atomic counter + log an * event -- per this task's own suggested technique, this is a stronger * proof than spying on plugin_begin_session() itself: it proves BOTH that * xce_sched_driver_run_normal_mode_round() calls the real * plugin_begin_session()/plugin_end_session() symbols AND that their real * iterate-registered-plugins logic fires a genuinely registered plugin's * hooks when driven this way. plugin.c is not otherwise exercised by any * existing test file in this directory (codegraph's own blast-radius * report flags plugin_begin_session/plugin_end_session/sched_account_jobs * as having "no covering tests found" before this file), so this is * incidental first coverage for plugin_begin_session()/plugin_end_session() * as a byproduct of this task's own target. * * sched_account_jobs() (scheduler.c) is instead a SPY test double (this * file never links scheduler.c) -- same rationale * test_driver_decision_cb.c/test_driver_translate.c already established * for sched_publish_dispatch()/sched_publish_pend_reason(): it pulls in * resource.c's sched_charge_account() machinery, whose OWN resource- * accounting correctness is out of THIS task's scope (this test's target * is the DRIVER's ordering discipline -- does it call sched_account_jobs() * at all, and in the right window -- not whether sched_account_jobs() * itself charges resources correctly, which is scheduler-allocation spec * territory, plus test_resource_accounting.c's own coverage as of * scheduler-sim-decision-parity task 1.1). As of that same task, * sched_account_jobs() takes xce_sched_core_t* (relocated from * xce_scheduler_t*) -- this spy's signature matches. * * White-box #include of ../src/core.c, ../src/core_clock.c, * ../src/core_events.c, ../src/core_session.c, ../src/decision.c, * ../src/driver.c (same file set as test_core_full_cycle.c, needed for the * real xce_sched_core_init()/xce_core_begin_session()/xce_core_signal_go()/ * xce_core_end_session()/xce_core_set_decision_cb() under test, transitively * required by driver.c) PLUS ../src/plugin.c (new to this file, for the * real plugin_begin_session()/plugin_end_session() -- see above). * * Stubs below: the usual kill_sched_* / sched_worker_release_host_ctx / * sched_resolve_job_picker set (same rationale as test_core_full_cycle.c's * own identical stubs -- this fixture never tears down, never adds a * worker, and needs its own real reimplementation of schedobj.c's * sched_resolve_job_picker since schedobj.c itself is not linked) PLUS * sched_publish_dispatch()/sched_publish_pend_reason() link-satisfying * no-op stubs (driver.c's xce_sched_driver_on_decision references them; * never actually invoked here since this fixture produces no DISPATCH/PEND * decisions) PLUS sched_fill_limit_requests() (resource.c) -- plugin.c's * plugin_prepare_allocation() references it; never actually invoked here * either (this test never calls plugin_prepare_allocation()), a pure * link-satisfying no-op like the others. Unlike every other file in this * directory, plugin_unregister() is NOT stubbed here -- it is the REAL * plugin.c symbol (needed as core.c's plugins-kvmgr datakiller), never * actually invoked at runtime since this fixture, like every other file's, * is never torn down. * * CONCERN surfaced during this test's construction (documented here, not * silently worked around): plugin_end_session() (plugin.c, pre-existing, * untouched by this task's diff) has a latent bug -- its per-plugin guard * checks `plugin->begin_session_fn != NULL` but then calls * `plugin->end_session_fn(plugin)`. A plugin registering ONLY * end_session_fn (no begin_session_fn) would silently never have its * end_session_fn invoked; a plugin registering ONLY begin_session_fn (no * end_session_fn) would crash via a NULL function-pointer call the first * time a round ends. g_observer_plugin below deliberately sets BOTH * fields, so this test's own result is unaffected by the bug -- but the * bug is real, pre-existing, and out of this task's scope to fix (it is * not part of task 5.2's diff and not something * xce_sched_driver_run_normal_mode_round() can compensate for from the * outside); flagged here for the reviewer rather than silently working around * it. */ #include #include #include #include #include #include #include #include "controller.capnp.h" #include "kvmgr.h" #include "preason.h" #include "scheduler.h" #include "xstring.h" /* Pull in the real xce_sched_core_init()/xce_core_begin_session()/ * xce_core_signal_go()/xce_core_end_session()/xce_core_set_decision_cb()/ * xce_sched_driver_run_normal_mode_round()/plugin_begin_session()/ * plugin_end_session() under test into this TU (white-box #include, same * convention as test_core_full_cycle.c/test_driver_translate.c/ * test_driver_decision_cb.c). */ #include "../src/core.c" #include "../src/core_clock.c" #include "../src/core_events.c" #include "../src/core_session.c" #include "../src/decision.c" #include "../src/driver.c" #include "../src/plugin.c" void kill_sched_job(void* arg) { (void)arg; } void kill_sched_queue(void* arg) { (void)arg; } void kill_sched_worker(void* arg) { (void)arg; } void kill_sched_user(void* arg) { (void)arg; } void kill_sched_project(void* arg) { (void)arg; } void kill_sched_resval(xce_val_t* val) { (void)val; } /* resource-accounting.md's reconciliation (resource.c, not linked here -- * this file only #includes core_events.c). core_events.c's * XCE_CORE_EVENT_JOB_RUSAGE handler calls this whenever job->accounts != * NULL; link-satisfying stub only, same rationale as every other stub in * this block (see test_job_update.c's identical stub). */ int sched_reconcile_job_resource_usage(xce_sched_core_t* c, xce_job_account_t* a, xce_job_rusages_t* r) { (void)c; (void)a; (void)r; return 0; } /* dup_val - real reimplementation of snapshot_wire.c's function of the same * name (verbatim logic), needed because core_events.c's RESVALS_FULL/ * RESVALS_DELTA handlers now call it (bug-1585 fix) and this file's * white-box #include of ../src/core_events.c does not pull in * snapshot_wire.c (same "reimplement the one small function instead of the * whole file" rationale this file's own sched_resolve_job_picker copy * already documents). This file's own kill_sched_resval is a no-op stub, so * the deep copy's contents are never inspected here -- only link-time * presence matters for this file's own tests. */ xce_val_t* dup_val(const xce_val_t* src) { xce_val_t* v; if (src == NULL) { return NULL; } v = (xce_val_t*)calloc(1, sizeof(*v)); if (v == NULL) { return NULL; } v->which = src->which; if (src->which == XCE_VAL_STRING) { v->u.svalue = (src->u.svalue != NULL) ? makeString(src->u.svalue) : NULL; } else { v->u = src->u; } return v; } /* core_reset_workers()'s (core_session.c) linker dependency -- see file * header comment; never invoked (no worker is ever added here). */ void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; } /* Link-satisfying stub for scheduler.c's sched_publish_dispatch()/ * sched_publish_pend_reason() -- see file header comment. Never actually * invoked at runtime: this fixture pushes no jobs, so the driver's * decision-drain loop never observes a non-NULL decision, so * xce_sched_driver_on_decision() (driver.c) is never called here. */ int sched_publish_dispatch(xce_scheduler_t* sched, xce_job_dispatch_t* d) { (void)sched; (void)d; return 0; } int sched_publish_pend_reason(xce_scheduler_t* sched, xce_preason_set_t* r) { (void)sched; (void)r; return 0; } /* Link-satisfying stub for resource.c's sched_fill_limit_requests() -- * plugin.c's plugin_prepare_allocation() references it; never actually * invoked here (this test never calls plugin_prepare_allocation()). See * file header comment. */ int sched_fill_limit_requests(xce_sched_alloc_req_t* alloc_req, xlist_t* limits, xlist_t* rusages, int nhosts, int nslots) { (void)alloc_req; (void)limits; (void)rusages; (void)nhosts; (void)nslots; return 0; } /* Link-satisfying stubs for recording.c/recording_events.c/ * recording_session.c's real functions -- as of scheduler-core spec task * 9.2, driver.c's (real, #included above) * xce_sched_driver_translate_pending_events()/ * xce_sched_driver_run_normal_mode_round() reference these * unconditionally (the `sched.recording`/`record_ctx != NULL` gates that * decide whether they do real work are runtime checks INSIDE these * functions, not something that removes the call site itself -- the * linker needs the symbol regardless). This file's fixture never sets * `sched.recording` (memset(&sched, 0, ...) below leaves it false), so at * runtime xce_sched_driver_translate_pending_events() calls this stub with * a non-NULL `sched` (record_ctx == &sched, always non-NULL here) but * xce_sched_driver_run_normal_mode_round() never calls * sched_record_session_begin() with `sched.recording == true` -- either * way, these stubs never need to do real work for this file's ordering * proof, which is not about recording. Never links recording.c's real * implementations (they need xce_sched_core_save_snapshot(), snapshot.c, * an even bigger dependency surface this file's own fixture doesn't * otherwise need). */ int sched_record_write_events(xce_scheduler_t* sched, xce_core_event_t* events, int n) { (void)sched; (void)events; (void)n; return 0; } /* bug-1852: driver.c now checkpoints separately, after applying a batch. */ int sched_record_checkpoint_after_apply(xce_scheduler_t* sched) { (void)sched; return 0; } /* sched_record_start/sched_record_stop (scheduler-core spec, task 9.4) -- * driver.c's handle_deferred_admin_record() (new this task) calls both; * this file never enqueues an XCE_SCHED_OP_ADMIN_RECORD request, so * neither is actually invoked at runtime, same rationale as * sched_record_write_events above. */ int sched_record_start(xce_scheduler_t* sched, const char* record_file) { (void)sched; (void)record_file; return 0; } int sched_record_stop(xce_scheduler_t* sched) { (void)sched; return 0; } sched_record_session_ctx_t* sched_record_session_begin(xce_scheduler_t* sched, uint64_t begin_round_ts) { (void)sched; (void)begin_round_ts; return NULL; } void sched_record_session_add_decision(sched_record_session_ctx_t* ctx, uint64_t ts, xce_decision_t* decision) { (void)ctx; (void)ts; (void)decision; } int sched_record_session_end(xce_scheduler_t* sched, sched_record_session_ctx_t* ctx, uint64_t end_ts) { (void)sched; (void)ctx; (void)end_ts; return 0; } /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- see * file header comment (same rationale/algorithm as test_core_full_cycle.c's * own copy). */ int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out) { xce_sched_plugin_t* plugin = NULL; int rc = 0; if ((core == NULL) || (out == NULL)) { return -EINVAL; } *out = NULL; if (core->job_pick_plugin == NULL) { return -EINVAL; } lock(core->plugins, RTTI_LOCK_READ); rc = key_find(core->plugins, core->job_pick_plugin, (void**)&plugin); unlock(core->plugins); if (rc <= 0) { return (rc < 0) ? rc : -ENOENT; } if ((plugin == NULL) || (plugin->picker_iface == NULL) || (plugin->picker_iface->next_job == NULL) || (plugin->picker_iface->begin_session == NULL) || (plugin->picker_iface->end_session == NULL) || (plugin->picker_iface->on_dispatch == NULL) || (plugin->picker_iface->on_pend == NULL)) { return -EINVAL; } *out = plugin; return 0; } /* sched_account_jobs() itself is defined further below (SPY test double * section) -- its prototype already comes from core.h (included above via * scheduler.h). */ void setUp(void) { return; } void tearDown(void) { return; } /* ---------------------------------------------------------------------- */ /* Event log: records hook/thread firing order across all three threads */ /* involved (main/driver thread, picker thread, stand-in allocator thread) */ /* via a single atomically-claimed index per entry -- same pattern/ */ /* thread-safety argument as test_core_full_cycle.c's own g_events. */ /* ---------------------------------------------------------------------- */ typedef enum { EVT_OBSERVER_BEGIN_SESSION, EVT_ACCOUNT_JOBS, EVT_PICKER_BEGIN_SESSION, EVT_PICKER_NEXT_JOB_NULL, EVT_PICKER_END_SESSION, EVT_ALLOCATOR_DRAINED_JBUF_SENTINEL, EVT_ALLOCATOR_PUSHED_END_DECISION, EVT_OBSERVER_END_SESSION, } ordering_event_tag_t; #define MAX_EVENTS 32 typedef struct { ordering_event_tag_t tags[MAX_EVENTS]; _Atomic int n; } event_log_t; static event_log_t g_events; static void log_event(ordering_event_tag_t tag) { int idx = atomic_fetch_add(&(g_events.n), 1); if (idx < MAX_EVENTS) { g_events.tags[idx] = tag; } } static int find_log_index(ordering_event_tag_t tag) { int n = atomic_load(&(g_events.n)); int cap = (n > MAX_EVENTS) ? MAX_EVENTS : n; int i; for (i = 0; i < cap; i++) { if (g_events.tags[i] == tag) { return i; } } return -1; } /* ---------------------------------------------------------------------- */ /* sched_account_jobs() SPY -- see file header comment. */ /* ---------------------------------------------------------------------- */ static _Atomic int g_account_jobs_calls; int sched_account_jobs(xce_sched_core_t* core) { (void)core; atomic_fetch_add(&g_account_jobs_calls, 1); log_event(EVT_ACCOUNT_JOBS); return 0; } /* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision * Reconciliation capacity gate): link-satisfying stubs, mirroring this * file's existing sched_account_jobs() no-op precedent immediately above -- * sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls * sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to * re-validate a recorded decision against live capacity, and * sched_charge_account() (resource.c) to keep that check's own view of * capacity up to date across multiple candidates in the same round. This * file never exercises real resource contention (same rationale as the * existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so * the satisfy checks trivially always succeed and the charge call is a * no-op. */ int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w, xvec_t* uncomitted, xvec_idval_t* wanted, xvec_idval_t* unmeet) { (void)core; (void)w; (void)uncomitted; (void)wanted; (void)unmeet; return 1; } int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted, xvec_idval_t* wanted, xvec_idval_t* unmeet) { (void)core; (void)uncomitted; (void)wanted; (void)unmeet; return 1; } int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct) { (void)core; (void)acct; return 0; } int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct) { (void)core; (void)acct; return 0; } /* ---------------------------------------------------------------------- */ /* Observer plugin: OLD begin_session_fn/end_session_fn hook category */ /* (plugin.h) -- genuinely registered into core->plugins and driven by */ /* the REAL plugin_begin_session()/plugin_end_session() (plugin.c). See */ /* file header comment for why both fields are set (sidesteps the */ /* documented plugin_end_session() guard-field bug). */ /* ---------------------------------------------------------------------- */ static _Atomic int g_observer_begin_calls; static _Atomic int g_observer_end_calls; static int observer_begin_session_fn(void* arg) { (void)arg; atomic_fetch_add(&g_observer_begin_calls, 1); log_event(EVT_OBSERVER_BEGIN_SESSION); return 0; } static int observer_end_session_fn(void* arg) { (void)arg; atomic_fetch_add(&g_observer_end_calls, 1); log_event(EVT_OBSERVER_END_SESSION); return 0; } static xce_sched_plugin_t g_observer_plugin = { .name = "test_ordering_observer", .begin_session_fn = observer_begin_session_fn, .end_session_fn = observer_end_session_fn }; /* ---------------------------------------------------------------------- */ /* Stub picker plugin: NEW picker_iface category (plugin.h) -- resolved by */ /* sched_resolve_job_picker() and driven by a real picker pthread below. */ /* This fixture never pushes any jobs, so next_job() always returns NULL */ /* immediately -- the picker's SOLE purpose here is to prove the wbuf */ /* rendezvous timing (see file header comment), not to exercise job flow. */ /* ---------------------------------------------------------------------- */ static int stub_picker_begin_session(void* picker) { (void)picker; log_event(EVT_PICKER_BEGIN_SESSION); return 0; } static xce_sched_job_t* stub_picker_next_job(void* picker) { (void)picker; log_event(EVT_PICKER_NEXT_JOB_NULL); return NULL; } static int stub_picker_end_session(void* picker) { (void)picker; log_event(EVT_PICKER_END_SESSION); return 0; } static int stub_picker_jobid_noop(void* picker, uint64_t jobid) { (void)picker; (void)jobid; return 0; } static int stub_picker_queue_update_noop(void* picker, xce_sched_queue_t* q) { (void)picker; (void)q; return 0; } static int stub_picker_job_noop(void* picker, xce_sched_job_t* job) { (void)picker; (void)job; return 0; } static int stub_picker_job_state_change_noop(void* picker, xce_sched_job_t* job, uint32_t state) { (void)picker; (void)job; (void)state; return 0; } static xce_job_picker_iface_t g_stub_picker_iface = { .init = NULL, .fini = NULL, .begin_session = stub_picker_begin_session, .next_job = stub_picker_next_job, .end_session = stub_picker_end_session, .on_dispatch = stub_picker_jobid_noop, .on_pend = stub_picker_jobid_noop, .on_queue_update = stub_picker_queue_update_noop, .on_job_push = stub_picker_job_noop, .on_job_state_change = stub_picker_job_state_change_noop, .on_job_update = stub_picker_job_noop, .on_config = NULL, .on_job_rusage = NULL }; static xce_sched_plugin_t g_stub_picker_plugin = { .name = "test_stub_picker", .picker_iface = &g_stub_picker_iface }; /* ---------------------------------------------------------------------- */ /* Picker thread: reproduces sched_job_picker()'s real per-session body */ /* verbatim (identical to test_core_full_cycle.c's own test_picker_thread_fn), */ /* single-shot -- see file header comment. */ /* ---------------------------------------------------------------------- */ static void* picker_thread_fn(void* arg) { xce_sched_core_t* core = (xce_sched_core_t*)arg; xce_sched_plugin_t* job_picker = NULL; int rc; int go = 0; uint64_t jobid; rc = sched_resolve_job_picker(core, &job_picker); if (rc < 0) { return NULL; } cbuf_get(&(core->wbuf), &go); job_picker->picker_iface->begin_session(job_picker); for (;;) { xce_sched_job_t* job = job_picker->picker_iface->next_job(job_picker); if (job == NULL) { break; } jobid = job->jobid; cbuf_put(&(core->jbuf), &jobid); } jobid = 0; cbuf_put(&(core->jbuf), &jobid); job_picker->picker_iface->end_session(job_picker); return NULL; } /* ---------------------------------------------------------------------- */ /* Stand-in allocator thread -- see file header comment for why this must */ /* be a real, separate thread here (unlike test_core_full_cycle.c, whose */ /* test function itself can play this role sequentially). */ /* ---------------------------------------------------------------------- */ static void* stand_in_allocator_thread_fn(void* arg) { xce_sched_core_t* core = (xce_sched_core_t*)arg; uint64_t jobid; xce_decision_t* decision = NULL; cbuf_get(&(core->jbuf), &jobid); (void)jobid; /* always 0 here -- this fixture pushes no jobs */ log_event(EVT_ALLOCATOR_DRAINED_JBUF_SENTINEL); cbuf_put(&(core->decisions), &decision); log_event(EVT_ALLOCATOR_PUSHED_END_DECISION); return NULL; } /* ---------------------------------------------------------------------- */ /* Driver-side decision callback spy -- must never actually fire in this */ /* test (this fixture produces no DISPATCH/PEND decisions), asserted below.*/ /* ---------------------------------------------------------------------- */ typedef struct { int calls; } test_driver_ctx_t; static int test_on_decision(void* driver_ctx, xce_decision_t* decision) { test_driver_ctx_t* ctx = (test_driver_ctx_t*)driver_ctx; (void)decision; ctx->calls++; return 0; } /* ---------------------------------------------------------------------- */ /* The contract test. */ /* ---------------------------------------------------------------------- */ /* * test_round_runs_plugin_begin_and_account_jobs_before_picker_signaled - * drives the REAL xce_sched_driver_run_normal_mode_round() through one * complete round against a real picker thread + a real stand-in allocator * thread, and proves (see file header comment for the full design): * * 1. plugin_begin_session() (observer) and sched_account_jobs() (spy) * both actually fire, exactly once each. * 2. Both complete strictly BEFORE the picker thread's begin_session * hook runs -- i.e. strictly before xce_core_signal_go()'s * cbuf_put(&core->wbuf) is observed by the picker thread's * cbuf_get(&wbuf). This is the central safety property (Req 1 AC 7 / * Req 5 AC 9): "called in the right window", not just "eventually * called". * 3. plugin_end_session() (observer) fires exactly once, after the * round's decision-drain loop genuinely receives the end-of-session * marker (proven via the stand-in allocator's own cbuf_put -> * cbuf_get cross-thread edge). * 4. xce_core_end_session() genuinely clears session_active. * 5. No decision ever reaches the registered on_decision callback (this * fixture pushes no jobs -- a sanity check that the ordering proof * above isn't accidentally relying on unrelated activity). */ static void test_round_runs_plugin_begin_and_account_jobs_before_picker_signaled(void) { xce_scheduler_t sched; pthread_t picker_tid; pthread_t allocator_tid; test_driver_ctx_t driver_ctx; int rc; int idx_observer_begin; int idx_account_jobs; int idx_picker_begin; int idx_picker_next_null; int idx_picker_end; int idx_allocator_sentinel; int idx_allocator_pushed_end; int idx_observer_end; memset(&g_events, 0, sizeof(g_events)); memset(&sched, 0, sizeof(sched)); memset(&driver_ctx, 0, sizeof(driver_ctx)); atomic_store(&g_account_jobs_calls, 0); atomic_store(&g_observer_begin_calls, 0); atomic_store(&g_observer_end_calls, 0); rc = xce_sched_core_init(&(sched.core)); TEST_ASSERT_EQUAL_INT(0, rc); /* Register both plugins directly into core->plugins (bypassing * plugin_register() -- same convention test_core_full_cycle.c's * fixture_init() uses: this test does not need plugin_register()'s * own init_fn/legacy-wrapper logic, only the registered-plugin * iteration plugin_begin_session()/plugin_end_session()/ * sched_resolve_job_picker() perform). Must run AFTER * xce_sched_core_init() (which memsets the whole core, including * job_pick_plugin, back to zero). */ lock(sched.core.plugins, RTTI_LOCK_WRITE); rc = key_add(sched.core.plugins, (void*)g_observer_plugin.name, (void*)&g_observer_plugin); TEST_ASSERT_GREATER_OR_EQUAL(0, rc); rc = key_add(sched.core.plugins, (void*)g_stub_picker_plugin.name, (void*)&g_stub_picker_plugin); TEST_ASSERT_GREATER_OR_EQUAL(0, rc); unlock(sched.core.plugins); sched.core.job_pick_plugin = g_stub_picker_plugin.name; rc = xce_core_set_decision_cb(&(sched.core), test_on_decision, &driver_ctx); TEST_ASSERT_EQUAL_INT(0, rc); rc = pthread_create(&picker_tid, NULL, picker_thread_fn, &(sched.core)); TEST_ASSERT_EQUAL_INT(0, rc); rc = pthread_create(&allocator_tid, NULL, stand_in_allocator_thread_fn, &(sched.core)); TEST_ASSERT_EQUAL_INT(0, rc); /* The function under test: the REAL production * xce_sched_driver_run_normal_mode_round(), driven exactly the way * main.c's while(1) loop drives it. Blocks until the round completes * (the stand-in allocator thread above pushes the end-of-session NULL * decision, unblocking this function's own decision-drain loop). */ xce_sched_driver_run_normal_mode_round(&sched); rc = pthread_join(picker_tid, NULL); TEST_ASSERT_EQUAL_INT(0, rc); rc = pthread_join(allocator_tid, NULL); TEST_ASSERT_EQUAL_INT(0, rc); /* --- plugin_begin_session()/sched_account_jobs()/plugin_end_session() * genuinely ran, exactly once each --- */ TEST_ASSERT_EQUAL_INT(1, atomic_load(&g_observer_begin_calls)); TEST_ASSERT_EQUAL_INT(1, atomic_load(&g_account_jobs_calls)); TEST_ASSERT_EQUAL_INT(1, atomic_load(&g_observer_end_calls)); /* --- No decision ever reached the driver callback --- */ TEST_ASSERT_EQUAL_INT(0, driver_ctx.calls); /* --- xce_core_end_session() genuinely cleared session_active --- */ TEST_ASSERT_EQUAL_INT(0, atomic_load_explicit(&(sched.core.session_active), memory_order_acquire)); /* --- Ordering, from the event log --- */ idx_observer_begin = find_log_index(EVT_OBSERVER_BEGIN_SESSION); idx_account_jobs = find_log_index(EVT_ACCOUNT_JOBS); idx_picker_begin = find_log_index(EVT_PICKER_BEGIN_SESSION); idx_picker_next_null = find_log_index(EVT_PICKER_NEXT_JOB_NULL); idx_picker_end = find_log_index(EVT_PICKER_END_SESSION); idx_allocator_sentinel = find_log_index(EVT_ALLOCATOR_DRAINED_JBUF_SENTINEL); idx_allocator_pushed_end = find_log_index(EVT_ALLOCATOR_PUSHED_END_DECISION); idx_observer_end = find_log_index(EVT_OBSERVER_END_SESSION); TEST_ASSERT_TRUE(idx_observer_begin >= 0); TEST_ASSERT_TRUE(idx_account_jobs >= 0); TEST_ASSERT_TRUE(idx_picker_begin >= 0); TEST_ASSERT_TRUE(idx_picker_next_null >= 0); TEST_ASSERT_TRUE(idx_picker_end >= 0); TEST_ASSERT_TRUE(idx_allocator_sentinel >= 0); TEST_ASSERT_TRUE(idx_allocator_pushed_end >= 0); TEST_ASSERT_TRUE(idx_observer_end >= 0); /* THE central safety-property assertion (Req 1 AC 7 / Req 5 AC 9): both * plugin_begin_session() and sched_account_jobs() complete strictly * BEFORE the picker thread's begin_session runs -- proven via the wbuf * cross-thread handoff (see file header comment). Not "eventually * called" -- "called in the right window". */ TEST_ASSERT_TRUE(idx_observer_begin < idx_picker_begin); TEST_ASSERT_TRUE(idx_account_jobs < idx_picker_begin); /* Documents the actual call order xce_sched_driver_run_normal_mode_round() * uses today (plugin_begin_session() then sched_account_jobs()) -- per * driver.c's own header comment this relative order is NOT itself * safety-critical (neither has a data dependency on the other), but it * is what genuinely happens, so it is asserted here too. */ TEST_ASSERT_TRUE(idx_observer_begin < idx_account_jobs); /* plugin_end_session() runs strictly AFTER the round's decision-drain * loop genuinely observed the end-of-session marker -- proven via the * allocator thread's cbuf_put(&core->decisions)/main thread's * cbuf_get(&core->decisions) cross-thread edge, the same style of proof * as the wbuf handoff above. */ TEST_ASSERT_TRUE(idx_allocator_pushed_end < idx_observer_end); } /* * test_null_driver_ctx_is_safe_noop - xce_sched_driver_run_normal_mode_round() * guards `driver_ctx == NULL` before touching anything (driver.c's own * `if (sched == NULL) { return; }`) -- must not crash, must not call any * of the observer/spy hooks above. */ static void test_null_driver_ctx_is_safe_noop(void) { memset(&g_events, 0, sizeof(g_events)); atomic_store(&g_account_jobs_calls, 0); atomic_store(&g_observer_begin_calls, 0); atomic_store(&g_observer_end_calls, 0); xce_sched_driver_run_normal_mode_round(NULL); TEST_ASSERT_EQUAL_INT(0, atomic_load(&g_observer_begin_calls)); TEST_ASSERT_EQUAL_INT(0, atomic_load(&g_account_jobs_calls)); TEST_ASSERT_EQUAL_INT(0, atomic_load(&g_observer_end_calls)); } int main(void) { UNITY_BEGIN(); RUN_TEST( test_round_runs_plugin_begin_and_account_jobs_before_picker_signaled); RUN_TEST(test_null_driver_ctx_is_safe_noop); return UNITY_END(); }