feat(scheduler-sim-decision-parity): add reconciliation trigger and lookup (task 2.2)
Adds sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t*, replay_session_t*) to sim_loop.c -- not yet wired into the round boundary (task 3). Once per call, finds every RUN-state job with job->accounts == NULL (the signature of a job already resolved before this replay's own live decision-making ever considered it) and searches this round's decoded recorded-decision stream for a matching DISPATCH entry by jobid. This task implements only the trigger + lookup + not-found path: when no match exists, the job's allocation is left unknown (not fabricated), no error is raised, and the job is safe to re-check next round (Requirement 4.1, 4.2). Match-found reconstruction (ownership transfer, freeing/ compacting the consumed entry, reporting) is tasks 2.3/2.4, not implemented here -- decoded_session's decisions are read but not yet mutated. 6 new tests cover: not-found leaves job unaccounted, cross-round safety, already-accounted jobs are skipped, a same-jobid PEND entry is correctly not mistaken for a DISPATCH match, non-RUN-state jobs are gated out, and NULL-argument safety. RED phase: compile-error-before/link-succeeds-after, the correct RED variant for genuinely new code with no prior behavior. 46/47 scheduler tests pass; the 1 failure is pre-existing bug-1560 (unrelated). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
134380b871
commit
81358b5231
4 changed files with 618 additions and 1 deletions
|
|
@ -27,7 +27,7 @@
|
|||
- _Depends: 1.1_
|
||||
- _Boundary: Sim Driver Round-Boundary Accounting_
|
||||
|
||||
- [ ] 2.2 Detect and locate already-resolved jobs needing reconciliation
|
||||
- [x] 2.2 Detect and locate already-resolved jobs needing reconciliation
|
||||
- Identify, once per round, every currently-running job that has no known resource allocation yet (this is the signature of a job that was already resolved earlier in the original recording, before this replay run's own live decision-making ever considered it)
|
||||
- For each such job, search this round's decoded recorded-decision data for the matching originating decision by job identity
|
||||
- When no matching recorded decision is found, leave the job's allocation unknown, report its usage as unavailable rather than fabricating a value, and continue processing the rest of the replay without treating this as an error
|
||||
|
|
|
|||
|
|
@ -484,6 +484,120 @@ static void* sim_picker_thread_fn(void* arg)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* sim_reconcile_unaccounted_jobs - Recorded-Decision Reconciliation
|
||||
* (scheduler-sim-decision-parity spec, design.md's "Recorded-Decision
|
||||
* Reconciliation" component; tasks 2.2-2.4). THIS function, as it stands
|
||||
* right now, implements only task 2.2's slice: the once-per-round trigger,
|
||||
* the jobid-based lookup, and the "not found" path (Requirement 4.1, 4.2).
|
||||
* Tasks 2.3 (allocation reconstruction, ownership transfer, array
|
||||
* compaction) and 2.4 (feeding the reconstructed decision through the
|
||||
* decision-reporting channel) build on top of the FOUND branch below,
|
||||
* deliberately left as a no-op marker for now -- see the comment inside
|
||||
* that branch. Not yet called from anywhere (task 3 wires this into
|
||||
* sched_sim_driver_run()'s round-boundary sequence, before
|
||||
* sched_account_jobs() -- see that call site's own comment above) --
|
||||
* standalone and directly unit-testable for now, per design.md's own
|
||||
* Testing Strategy ("Recorded-Decision Reconciliation leaves a job
|
||||
* unaccounted, without fabricating data or erroring, when no matching
|
||||
* recorded decision exists").
|
||||
*
|
||||
* Trigger condition: mirrors sched_account_jobs()'s own `job->accounts ==
|
||||
* NULL` guard (scheduler.c) -- reused here as both the trigger AND (once
|
||||
* tasks 2.3/2.4 populate job->accounts) the double-application guard, per
|
||||
* design.md's Implementation Notes. A RUN-state job in `sim->core.jobs`
|
||||
* with `job->accounts == NULL` is the signature of a job already resolved
|
||||
* earlier in the original recording, before this replay run's own live
|
||||
* decision-making ever considered it.
|
||||
*
|
||||
* For each such job, searches `decoded_session->decisions` for a matching
|
||||
* DISPATCH entry by jobid -- a job can only reach RUN-state via a recorded
|
||||
* DISPATCH decision, so a PEND/PREEMPT entry (even one that happens to
|
||||
* carry the same jobid) is never treated as a match (see design.md's
|
||||
* Implementation Notes for why PEND/PREEMPT are structurally impossible to
|
||||
* legitimately match here).
|
||||
*
|
||||
* If no match is found: `job->accounts`/`job->allocs` are left exactly as
|
||||
* they were (still NULL) -- not an error, not fabricated. Nothing here
|
||||
* needs to separately record "usage unavailable": leaving `job->accounts ==
|
||||
* NULL` IS the unavailable state -- sched_account_jobs() (scheduler.c)
|
||||
* already skips exactly this job shape when it re-derives
|
||||
* resInUse/sharedInUse, so no usage value is ever fabricated for it. The
|
||||
* same `job->accounts == NULL` trigger naturally re-fires next round with
|
||||
* no extra bookkeeping required, so calling this again unconditionally on a
|
||||
* later round is always safe (Requirement 4.1, 4.2).
|
||||
*
|
||||
* NULL `sim`/`decoded_session` is a safe no-op, matching this codebase's
|
||||
* established "never load-bearing" REPL-hook convention (design.md's
|
||||
* Service Interface preconditions).
|
||||
*/
|
||||
static void sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t* sim,
|
||||
replay_session_t* decoded_session)
|
||||
{
|
||||
iterator_t it;
|
||||
int rc;
|
||||
|
||||
if ((sim == NULL) || (decoded_session == NULL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
rc = iter_make(&it, sim->core.jobs);
|
||||
if (rc < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
xce_sched_job_t* job = NULL;
|
||||
int i;
|
||||
|
||||
rc = iter_next(&it, (void**)&job);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((job == NULL) || !JOB_IS_RUN(job->state)
|
||||
|| (job->accounts != NULL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (i = 0; i < decoded_session->n_decisions; i++) {
|
||||
replay_decision_t* entry = decoded_session->decisions[i];
|
||||
|
||||
if ((entry == NULL)
|
||||
|| (entry->body_which != ReplayDecision_body_dispatch)
|
||||
|| (entry->body.dispatch == NULL)
|
||||
|| (entry->body.dispatch->alloc == NULL)
|
||||
|| (entry->body.dispatch->alloc->jobid != job->jobid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* FOUND -- tasks 2.3/2.4 (NOT this task) reconstruct
|
||||
* job->allocs/job->accounts from `entry` here (move-then-null-
|
||||
* then-free ownership transfer, then array compaction) and feed
|
||||
* the result through the decision-reporting channel. Task 2.2
|
||||
* does nothing further for a found match: the job simply
|
||||
* remains job->accounts == NULL for now, which
|
||||
* sched_account_jobs() already tolerates safely (same skip path
|
||||
* as the "not found" case below). */
|
||||
break;
|
||||
}
|
||||
|
||||
if (i == decoded_session->n_decisions) {
|
||||
char jobidStr[64];
|
||||
|
||||
/* Requirement 4.1/4.2: no matching recorded decision this round
|
||||
* -- not an error, nothing fabricated, keep going. */
|
||||
fmt_jobid(jobidStr, sizeof(jobidStr), job->jobid);
|
||||
TRACE("%s no matching recorded DISPATCH decision this round for "
|
||||
"RUN-state jobid=%s -- accounts left unknown, usage "
|
||||
"unavailable, will re-check next round",
|
||||
__func__, jobidStr);
|
||||
}
|
||||
}
|
||||
|
||||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
/*
|
||||
* sched_sim_driver_run() -- Req 5 AC 3 (design review follow-up,
|
||||
* 2026-08-19): sched_sim_driver_start() (sim.c) activates tlog virtual-
|
||||
|
|
|
|||
|
|
@ -752,6 +752,23 @@ sched_sim_session_ordering_test = executable(
|
|||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Recorded-Decision Reconciliation trigger/lookup/not-found contract
|
||||
# (scheduler-sim-decision-parity spec, task 2.2, Req 4.1/4.2, "Recorded-
|
||||
# Decision Reconciliation" component). Calls the new sim_loop.c-local
|
||||
# sim_reconcile_unaccounted_jobs() directly (it is `static` and not yet
|
||||
# wired into any call site -- task 3's job) against hand-built
|
||||
# xce_sched_core_t/replay_session_t fixtures (core_build_job()+
|
||||
# sched_add_job(), no `.replay` file or capnp round-trip needed). Same
|
||||
# white-box #include/stub set as sched_sim_event_loop_test -- sim_loop.c's
|
||||
# compiled TU unconditionally references every one of those externs
|
||||
# regardless of which functions this file's own tests call.
|
||||
sched_sim_reconcile_unaccounted_test = executable(
|
||||
'sched_sim_reconcile_unaccounted_test',
|
||||
['test_sim_reconcile_unaccounted.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Resource-contention replay fixture (scheduler-sim-decision-parity spec,
|
||||
# task 1.2, Req 2.1/2.2): existing fixtures in this directory never have
|
||||
# scarce capacity, so none can exercise or prove capacity-check correctness.
|
||||
|
|
@ -1102,6 +1119,9 @@ test('scheduler sim equivalence test',
|
|||
test('scheduler sim session ordering test',
|
||||
sched_sim_session_ordering_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler sim reconcile unaccounted test',
|
||||
sched_sim_reconcile_unaccounted_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler sim resource contention test',
|
||||
sched_sim_resource_contention_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
483
src/scheduler/testsuite/test_sim_reconcile_unaccounted.c
Normal file
483
src/scheduler/testsuite/test_sim_reconcile_unaccounted.c
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
/*
|
||||
* test_sim_reconcile_unaccounted.c - contract test for
|
||||
* sim_reconcile_unaccounted_jobs() (sim_loop.c, scheduler-sim-decision-
|
||||
* parity spec, task 2.2, "Recorded-Decision Reconciliation" component,
|
||||
* design.md; Requirement 4.1, 4.2).
|
||||
*
|
||||
* Scope (task 2.2 only, per design.md's Implementation Notes and this
|
||||
* task's own text): the once-per-round trigger (RUN-state job with
|
||||
* job->accounts == NULL), the jobid-based search through this round's
|
||||
* decoded_session->decisions, and the "not found" path -- leave the job
|
||||
* unaccounted, report usage unavailable (== job->accounts staying NULL,
|
||||
* which sched_account_jobs() already tolerates safely), never treat this as
|
||||
* an error, and stay safe to re-check next round. Allocation reconstruction
|
||||
* for a FOUND match (ownership transfer, array compaction, decision
|
||||
* reporting) is deliberately NOT exercised here -- that is tasks 2.3/2.4's
|
||||
* own test coverage; this file's "found" test (see below) only asserts
|
||||
* that a match does not corrupt an already-accounted job, not that a match
|
||||
* gets reconstructed.
|
||||
*
|
||||
* sim_reconcile_unaccounted_jobs() is `static` and not yet called from
|
||||
* anywhere in production code (task 3 wires it into
|
||||
* sched_sim_driver_run()'s round-boundary sequence) -- called directly here
|
||||
* instead, standalone, per design.md's own Testing Strategy.
|
||||
*
|
||||
* White-box #include set and stub set: IDENTICAL to
|
||||
* test_sim_event_loop.c's own (same rationale -- sim_loop.c's compiled TU
|
||||
* unconditionally references every one of these as externs, whether or not
|
||||
* any single test here calls sched_sim_driver_run() itself). This file
|
||||
* never calls sched_sim_driver_run()/spawns the picker thread, so (unlike
|
||||
* test_sim_event_loop.c) no `static` storage workaround is needed for its
|
||||
* own `xce_sched_sim_driver_t` fixtures.
|
||||
*
|
||||
* Fixture technique: xce_sched_core_init() plus core_build_job()/
|
||||
* sched_add_job() (core_events.c/core.c, both white-box #included) to place
|
||||
* a job directly into sim.core.jobs -- same technique
|
||||
* write_header_and_snapshot_with_pending_job() (test_sim_event_loop.c)
|
||||
* already establishes for baking a job straight into a core without a real
|
||||
* `.replay` file. `decoded_session` (replay_session_t) and its
|
||||
* `decisions[]` (replay_decision_t*) are hand-built directly in memory
|
||||
* (calloc), not round-tripped through capnp -- sim_reconcile_unaccounted_
|
||||
* jobs() only ever reads these plain heap structures, so no encode/decode
|
||||
* step is needed to exercise it. Fixtures are not torn down (matches this
|
||||
* whole suite's own "detect_leaks=0" convention, meson.build).
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "controller.capnp.h"
|
||||
#include "kvmgr.h"
|
||||
#include "scheduler.h"
|
||||
#include "xstring.h"
|
||||
|
||||
/* Pull in the real functions under test into this TU (white-box #include,
|
||||
* same combined set test_sim_event_loop.c's own header comment documents
|
||||
* and justifies, symbol by symbol). */
|
||||
#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/plugin.c"
|
||||
#include "../src/plugins/builtin_fcfs.c"
|
||||
#include "../src/recording.c"
|
||||
#include "../src/recording_session.c"
|
||||
#include "../src/repl.c"
|
||||
#include "../src/repl_break.c"
|
||||
#include "../src/repl_info.c"
|
||||
#include "../src/repl_params.c"
|
||||
#include "../src/sim.c"
|
||||
#include "../src/sim_loop.c"
|
||||
#include "../src/sim_picker.c" /* bug-1567: sim_loop.c calls sim_bootstrap_picker() (sim_loop.c-cap sibling) as an extern -- undefined reference at link time otherwise */
|
||||
#include "../src/sim_record.c"
|
||||
#include "../src/sim_verify.c"
|
||||
#include "../src/snapshot.c"
|
||||
#include "../src/snapshot_load.c"
|
||||
#include "../src/snapshot_wire.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; }
|
||||
|
||||
/* core_reset_workers()'s (core_session.c) linker dependency -- never
|
||||
* invoked (no worker is ever added here). */
|
||||
void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; }
|
||||
|
||||
/* plugin_prepare_allocation()'s (plugin.c, the "limit" plugin category)
|
||||
* linker dependency -- never invoked (this file never drives real
|
||||
* allocation). */
|
||||
int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
|
||||
xlist_t* limits)
|
||||
{
|
||||
(void)sched;
|
||||
(void)vec;
|
||||
(void)limits;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* sched_sim_driver_run()'s (sim_loop.c) round-boundary call to the
|
||||
* relocated sched_account_jobs() (core.h) -- a no-op stand-in here since
|
||||
* this file does not link scheduler.c/resource.c and never calls
|
||||
* sched_sim_driver_run() itself; needed purely to satisfy the linker
|
||||
* (sim_loop.c's compiled TU references it unconditionally). */
|
||||
int sched_account_jobs(xce_sched_core_t* core)
|
||||
{
|
||||
(void)core;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* copy every white-box test in this directory that does not link
|
||||
* schedobj.c already establishes. */
|
||||
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;
|
||||
}
|
||||
|
||||
void setUp(void) { return; }
|
||||
void tearDown(void) { return; }
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Fixture helpers */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void fixture_init(xce_sched_sim_driver_t* sim)
|
||||
{
|
||||
int rc;
|
||||
|
||||
memset(sim, 0, sizeof(*sim));
|
||||
rc = xce_sched_core_init(&(sim->core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
}
|
||||
|
||||
/* Bakes one job directly into `core->jobs` (no `.replay` file, no capnp --
|
||||
* same core_build_job()+sched_add_job() technique test_sim_event_loop.c's
|
||||
* own write_header_and_snapshot_with_pending_job() establishes). Passing a
|
||||
* non-NULL `allocs` reproduces the "already accounted" job shape (mirrors
|
||||
* core_build_job()'s own `if (j->allocs != NULL) { ...; job->accounts =
|
||||
* make_job_account(job->allocs); }` branch, core_events.c) -- pass NULL for
|
||||
* the ordinary "unaccounted RUN-state job" shape this file's central tests
|
||||
* target. */
|
||||
static xce_sched_job_t* add_job(xce_sched_core_t* core, uint64_t jobid,
|
||||
unsigned int state, xce_job_alloc_t* allocs)
|
||||
{
|
||||
xce_queue_t wire_queue;
|
||||
xce_sched_queue_t* q;
|
||||
job_submit_data_t submit;
|
||||
xce_job_t wire_job;
|
||||
xce_sched_job_t* job;
|
||||
|
||||
memset(&wire_queue, 0, sizeof(wire_queue));
|
||||
wire_queue.queue = (char*)"normal";
|
||||
wire_queue.status = QUEUE_STAT_OPEN;
|
||||
wire_queue.priority = 1;
|
||||
q = core_build_queue(&wire_queue);
|
||||
TEST_ASSERT_NOT_NULL(q);
|
||||
TEST_ASSERT_GREATER_OR_EQUAL(0, sched_add_queue(core, q));
|
||||
|
||||
memset(&submit, 0, sizeof(submit));
|
||||
submit.queue = (char*)"normal";
|
||||
submit.user = (char*)"alice";
|
||||
submit.mintasks = 1;
|
||||
submit.maxtasks = 1;
|
||||
|
||||
memset(&wire_job, 0, sizeof(wire_job));
|
||||
wire_job.jobid = jobid;
|
||||
wire_job.state = state;
|
||||
wire_job.submit = &submit;
|
||||
wire_job.allocs = allocs;
|
||||
|
||||
job = core_build_job(core, &wire_job);
|
||||
TEST_ASSERT_NOT_NULL(job);
|
||||
TEST_ASSERT_GREATER_OR_EQUAL(0, sched_add_job(core, job));
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
static replay_decision_t* make_dispatch_entry(uint64_t jobid)
|
||||
{
|
||||
xce_job_alloc_t* alloc = make_job_alloc(jobid, 0, NULL);
|
||||
sched_dispatch_decision_t* dispatch
|
||||
= (sched_dispatch_decision_t*)calloc(1, sizeof(*dispatch));
|
||||
replay_decision_t* rd = (replay_decision_t*)calloc(1, sizeof(*rd));
|
||||
|
||||
dispatch->alloc = alloc;
|
||||
|
||||
rd->body_which = ReplayDecision_body_dispatch;
|
||||
rd->body.dispatch = dispatch;
|
||||
|
||||
return rd;
|
||||
}
|
||||
|
||||
static replay_decision_t* make_pend_entry(uint64_t jobid)
|
||||
{
|
||||
sched_pend_decision_t* pend
|
||||
= (sched_pend_decision_t*)calloc(1, sizeof(*pend));
|
||||
replay_decision_t* rd = (replay_decision_t*)calloc(1, sizeof(*rd));
|
||||
|
||||
pend->jobid = jobid;
|
||||
pend->reason = NULL;
|
||||
|
||||
rd->body_which = ReplayDecision_body_pend;
|
||||
rd->body.pend = pend;
|
||||
|
||||
return rd;
|
||||
}
|
||||
|
||||
static replay_session_t* make_decoded_session(uint64_t begin_round,
|
||||
replay_decision_t* entry)
|
||||
{
|
||||
replay_session_t* s = (replay_session_t*)calloc(1, sizeof(*s));
|
||||
|
||||
s->begin_round = begin_round;
|
||||
s->summary = NULL;
|
||||
|
||||
if (entry == NULL) {
|
||||
s->decisions = NULL;
|
||||
s->n_decisions = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
s->decisions = (replay_decision_t**)calloc(1, sizeof(*(s->decisions)));
|
||||
s->decisions[0] = entry;
|
||||
s->n_decisions = 1;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 1 (the task's own primary Observable): a RUN-state job with no */
|
||||
/* known allocation and no matching recorded decision in this round's */
|
||||
/* window is left unaccounted (usage unavailable), and the call returns */
|
||||
/* normally -- no error, nothing fabricated (Requirement 4.1, 4.2). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void
|
||||
test_reconcile_leaves_run_job_unaccounted_when_no_recorded_decision(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
replay_session_t* decoded;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
/* RUN-state, no known allocation -- e.g. a job dispatched before the
|
||||
* snapshot this (hypothetical) replay started from, exactly the
|
||||
* scenario this task's own Observable Completion text describes. */
|
||||
job = add_job(&(sim.core), 7001ULL, JOB_STAT_RUN, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
TEST_ASSERT_NULL(job->allocs);
|
||||
|
||||
/* This round's decoded recorded-decision data has nothing for this
|
||||
* jobid at all -- the replay's own window never captured it. */
|
||||
decoded = make_decoded_session(1000ULL, NULL);
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded);
|
||||
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
job->accounts,
|
||||
"no matching recorded decision -- job->accounts must stay NULL "
|
||||
"(usage unavailable), never fabricated");
|
||||
TEST_ASSERT_NULL_MESSAGE(job->allocs, "job->allocs must likewise stay "
|
||||
"NULL -- nothing to reconstruct "
|
||||
"from");
|
||||
|
||||
printf("test_reconcile_leaves_run_job_unaccounted_when_no_recorded_"
|
||||
"decision: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 2: a job left unresolved this round is safe to re-check again on */
|
||||
/* a later round, with no adverse effect (task's own explicit text). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_reconcile_is_safe_to_recheck_on_a_later_round(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
replay_session_t* decoded_round1;
|
||||
replay_session_t* decoded_round2;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
job = add_job(&(sim.core), 7002ULL, JOB_STAT_RUN, NULL);
|
||||
|
||||
decoded_round1 = make_decoded_session(1000ULL, NULL);
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded_round1);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
|
||||
/* Round 2: still no matching recorded decision -- calling again must
|
||||
* not error, crash, or otherwise behave differently just because this
|
||||
* is a repeat check of the same job. */
|
||||
decoded_round2 = make_decoded_session(2000ULL, NULL);
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded_round2);
|
||||
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
job->accounts,
|
||||
"re-checking an unresolved job on a later round must remain safe "
|
||||
"and leave it unaccounted, with no adverse effect");
|
||||
|
||||
printf(
|
||||
"test_reconcile_is_safe_to_recheck_on_a_later_round: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 3: a job that already has known accounts (either reconciled */
|
||||
/* earlier or freshly dispatched by the live path) is never re-touched -- */
|
||||
/* the trigger condition (job->accounts == NULL) must gate correctly even */
|
||||
/* when a matching recorded DISPATCH entry for the same jobid IS present */
|
||||
/* this round (task 2.2 does not implement match-handling yet, but the */
|
||||
/* trigger itself must still never fire for an already-accounted job). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_reconcile_skips_job_that_already_has_accounts(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
xce_job_account_t* accounts_before;
|
||||
replay_session_t* decoded;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
job = add_job(&(sim.core), 7003ULL, JOB_STAT_RUN,
|
||||
make_job_alloc(7003ULL, 0, NULL));
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
job->accounts, "fixture setup: job must already have accounts");
|
||||
accounts_before = job->accounts;
|
||||
|
||||
decoded = make_decoded_session(1000ULL, make_dispatch_entry(7003ULL));
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded);
|
||||
|
||||
TEST_ASSERT_EQUAL_PTR_MESSAGE(
|
||||
accounts_before, job->accounts,
|
||||
"an already-accounted job must never be re-touched, even when a "
|
||||
"matching recorded decision exists this round (Requirement 3.3's "
|
||||
"double-application guard)");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, decoded->n_decisions,
|
||||
"task 2.2 does not consume/compact recorded entries -- that is "
|
||||
"task 2.3's job");
|
||||
|
||||
printf("test_reconcile_skips_job_that_already_has_accounts: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 4: a recorded PEND entry sharing the same jobid must never be */
|
||||
/* mistaken for a match -- only a DISPATCH entry can legitimately account */
|
||||
/* for a RUN-state job (design.md's Implementation Notes). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_reconcile_ignores_non_dispatch_entry_with_matching_jobid(
|
||||
void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
replay_session_t* decoded;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
job = add_job(&(sim.core), 7004ULL, JOB_STAT_RUN, NULL);
|
||||
|
||||
decoded = make_decoded_session(1000ULL, make_pend_entry(7004ULL));
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded);
|
||||
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
job->accounts,
|
||||
"a recorded PEND entry must never be treated as a DISPATCH match, "
|
||||
"even when its jobid matches -- job stays unaccounted");
|
||||
|
||||
printf("test_reconcile_ignores_non_dispatch_entry_with_matching_jobid:"
|
||||
" PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 5: the trigger only fires for a RUN-state job -- a PEND-state job */
|
||||
/* with no accounts must never be touched here (the live picker re-derives */
|
||||
/* PEND decisions fresh every round, per design.md's Revalidation */
|
||||
/* Triggers section). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_reconcile_skips_non_run_state_job(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
replay_session_t* decoded;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
job = add_job(&(sim.core), 7005ULL, JOB_STAT_PEND, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
|
||||
/* A matching recorded DISPATCH entry is present -- but must not be
|
||||
* consulted at all, since the trigger's RUN-state gate must reject this
|
||||
* job before any lookup happens. */
|
||||
decoded = make_decoded_session(1000ULL, make_dispatch_entry(7005ULL));
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded);
|
||||
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
job->accounts,
|
||||
"a non-RUN-state job must never be reconciled here, regardless of "
|
||||
"what recorded data exists for it this round");
|
||||
|
||||
printf("test_reconcile_skips_non_run_state_job: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 6: NULL `sim`/`decoded_session` is a safe no-op (design.md's */
|
||||
/* Service Interface preconditions -- "never load-bearing" convention). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_reconcile_null_args_are_a_safe_noop(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
replay_session_t* decoded;
|
||||
|
||||
fixture_init(&sim);
|
||||
decoded = make_decoded_session(1000ULL, NULL);
|
||||
|
||||
sim_reconcile_unaccounted_jobs(NULL, decoded);
|
||||
sim_reconcile_unaccounted_jobs(&sim, NULL);
|
||||
sim_reconcile_unaccounted_jobs(NULL, NULL);
|
||||
|
||||
/* Reaching this line without a crash IS the assertion. */
|
||||
TEST_ASSERT_TRUE(1);
|
||||
|
||||
printf("test_reconcile_null_args_are_a_safe_noop: PASS\n");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(
|
||||
test_reconcile_leaves_run_job_unaccounted_when_no_recorded_decision);
|
||||
RUN_TEST(test_reconcile_is_safe_to_recheck_on_a_later_round);
|
||||
RUN_TEST(test_reconcile_skips_job_that_already_has_accounts);
|
||||
RUN_TEST(test_reconcile_ignores_non_dispatch_entry_with_matching_jobid);
|
||||
RUN_TEST(test_reconcile_skips_non_run_state_job);
|
||||
RUN_TEST(test_reconcile_null_args_are_a_safe_noop);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue