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:
Rongsong Shen 2026-08-22 07:53:53 +00:00
parent 134380b871
commit 81358b5231
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 618 additions and 1 deletions

View file

@ -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

View file

@ -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-

View file

@ -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'])

View 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();
}