test(scheduler-sim-decision-parity): regression suite for existing guarantees (task 4.2)

Four checks in one run: (a) REPL-vs-batch decision equivalence unmodified
and still passing; (b) --sim-verify reports no false mismatch for a
recording with multiple already-resolved jobs across rounds (non-adjacent
memmove compaction, no cross-round bleed); (c) a replay starting from a
snapshot with a job already RUN-state and no in-window recorded decision
degrades gracefully (no crash, no fabricated value, a second ordinary job
unaffected) -- the first test to exercise reconciliation's "not found"
path through the real sched_sim_driver_run()/xce_sched_core_load_snapshot()
path rather than a hand-built core; (d) normal-mode behavior is byte-for-
byte unchanged (zero production diff, the strongest possible evidence).

Found and fixed a real test-authoring gap while building (b): builtin_fcfs
re-offers a still-PEND job as a fresh PEND every round (confirmed
pre-existing, unrelated, also visible in test_repl_equivalence.c's own
fixture) -- the first draft's round-2 fixture omitted this recurrence.

No production code touched (task 3 already completed the feature) --
git diff on sim_loop.c/resource.c/scheduler.c/driver.c/sim_verify.c is
empty. 49/50 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 11:40:14 +00:00
parent b31907324c
commit 88bed529a7
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 803 additions and 1 deletions

View file

@ -831,6 +831,106 @@ static void append_replay_session_with_dispatch_and_pend(
capn_free(&c);
}
/* Appends one ReplaySession carrying THREE recorded decisions -- a DISPATCH
* for `dispatch_alloc_a`'s jobid, a PEND for `pend_jobid`, and a DISPATCH
* for `dispatch_alloc_c`'s jobid (in that order: the still-PEND job's own
* recorded PEND sits BETWEEN the two DISPATCH entries) -- scheduler-sim-
* decision-parity spec, task 4.2 regression coverage (Req 5.1, 5.2): proves
* sim_reconcile_unaccounted_jobs()'s memmove-based compaction correctly
* consumes TWO, non-adjacent entries from the same round's
* decoded_session->decisions (task 3's own
* append_replay_session_with_dispatch_and_pend() above only ever needed to
* consume a single, leading entry), leaving the middle PEND entry correctly
* repositioned for the positional --sim-verify walk. Reuses
* make_replay_dispatch_decision()/make_replay_pend_decision() the same way
* append_replay_session_with_dispatch_and_pend() does. */
static void append_replay_session_with_two_dispatches_and_pend(
FILE* f, uint64_t begin_round, xce_job_alloc_t* dispatch_alloc_a,
uint64_t pend_jobid, int pend_main_reason,
xce_job_alloc_t* dispatch_alloc_c)
{
replay_decision_t d_dispatch_a;
replay_decision_t d_pend;
replay_decision_t d_dispatch_c;
replay_decision_t* decisions[3];
replay_session_t session;
struct capn c;
struct capn_segment* cs;
ReplaySession_ptr sp;
d_dispatch_a = make_replay_dispatch_decision(dispatch_alloc_a);
d_dispatch_a.ts = begin_round;
d_pend = make_replay_pend_decision(pend_jobid, pend_main_reason);
d_pend.ts = begin_round;
d_dispatch_c = make_replay_dispatch_decision(dispatch_alloc_c);
d_dispatch_c.ts = begin_round;
decisions[0] = &d_dispatch_a;
decisions[1] = &d_pend;
decisions[2] = &d_dispatch_c;
memset(&session, 0, sizeof(session));
session.begin_round = begin_round;
session.decisions = decisions;
session.n_decisions = 3;
session.summary = NULL;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_ReplaySession_ptr(cs, &sp, &session);
capn_setp(capn_root(&c), 0, sp.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
/* Appends one ReplaySession carrying TWO recorded PendDecisions -- scheduler-
* sim-decision-parity spec, task 4.2 regression coverage: a still-genuinely-
* pending job is re-offered (and re-PENDed) by the real picker EVERY round
* it remains unresolved, not just the round it first arrives in
* (builtin_fcfs.c's own pendJobs discipline: "A non-pending job is left
* removed and not reinserted" -- the converse holds too, a job that stays
* PEND is never removed from its queue's pendJobs, so next_job() keeps
* offering it every subsequent round). Used for this file's own second
* round, where the still-PEND job from round 1 recurs alongside a fresh
* job. */
static void append_replay_session_with_two_pends(FILE* f, uint64_t begin_round,
uint64_t jobid1,
int main_reason1,
uint64_t jobid2,
int main_reason2)
{
replay_decision_t d1;
replay_decision_t d2;
replay_decision_t* decisions[2];
replay_session_t session;
struct capn c;
struct capn_segment* cs;
ReplaySession_ptr sp;
d1 = make_replay_pend_decision(jobid1, main_reason1);
d1.ts = begin_round;
d2 = make_replay_pend_decision(jobid2, main_reason2);
d2.ts = begin_round;
decisions[0] = &d1;
decisions[1] = &d2;
memset(&session, 0, sizeof(session));
session.begin_round = begin_round;
session.decisions = decisions;
session.n_decisions = 2;
session.summary = NULL;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_ReplaySession_ptr(cs, &sp, &session);
capn_setp(capn_root(&c), 0, sp.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
/* Runs sched_sim_driver_run() against `path` with --sim-verify set to
* `verify`, capturing everything written to stdout during the call (the
* same dup()/dup2() technique capture_stdout_around() above uses, inlined
@ -1076,6 +1176,105 @@ static void test_verify_with_already_resolved_job_reports_no_mismatch(void)
"PASS\n");
}
/*
* test_verify_with_multiple_already_resolved_jobs_across_rounds_reports_no_mismatch
* -- scheduler-sim-decision-parity spec, task 4.2 (Req 5.1, 5.2): extends
* task 3's own single-already-resolved-job proof
* (test_verify_with_already_resolved_job_reports_no_mismatch above) to a
* MULTI-job, MULTI-round scenario:
*
* Round 1: TWO already-resolved jobs (A=501, C=503) plus ONE genuinely
* still-PEND job (B=502). The round's recorded decisions are
* [DISPATCH(A), PEND(B), DISPATCH(C)] -- the still-pending job's own
* recorded PEND sits BETWEEN the two DISPATCH entries reconciliation
* must consume, exercising the memmove-based compaction's non-adjacent-
* removal path (task 3's own test only ever consumed a single, leading
* entry).
*
* Round 2: one ordinary, freshly-arriving job (D=504), no reconciliation
* needed at all -- proves the PRIOR round's compaction has no
* cross-round effect (design.md's Implementation Notes: "decoded_session
* is decoded fresh once per round ... no cross-round effect") and that
* --sim-verify's positional walk resumes correctly at di=0 for the new
* round.
*
* Zero mismatches across both rounds is the Observable.
*/
static void
test_verify_with_multiple_already_resolved_jobs_across_rounds_reports_no_mismatch(
void)
{
char path[PATH_MAX];
FILE* f;
int rc;
char* captured;
xce_alloc_t* res_a[1];
xce_host_alloc_t* host_a[1];
xce_job_alloc_t* alloc_a;
xce_alloc_t* res_c[1];
xce_host_alloc_t* host_c[1];
xce_job_alloc_t* alloc_c;
f = write_header_and_snapshot(path, sizeof(path), "_reconciled_multi");
TEST_ASSERT_NOT_NULL(f);
append_queue_update_event(f, 1000ULL, "normal");
/* Jobs A (501) and C (503): both already resolved before this round's
* live picker ever considers them. Job B (502): genuinely still PEND
* -- the real picker independently re-pends it. */
append_jobs_push_event(f, 1500ULL, 501ULL, "normal");
append_jobs_push_event(f, 1500ULL, 502ULL, "normal");
append_jobs_push_event(f, 1500ULL, 503ULL, "normal");
append_job_state_change_event(f, 1800ULL, 501ULL, JOB_STAT_PEND,
JOB_STAT_RUN);
append_job_state_change_event(f, 1801ULL, 503ULL, JOB_STAT_PEND,
JOB_STAT_RUN);
res_a[0] = make_test_alloc("slots", "run", 1, 4.0, 0, 0);
host_a[0] = make_test_host_alloc("hostA", res_a, 1);
alloc_a = make_test_job_alloc(501ULL, host_a, 1);
res_c[0] = make_test_alloc("slots", "run", 1, 2.0, 0, 0);
host_c[0] = make_test_host_alloc("hostC", res_c, 1);
alloc_c = make_test_job_alloc(503ULL, host_c, 1);
/* Recorded order: DISPATCH(A), PEND(B), DISPATCH(C) -- B's entry is
* sandwiched between the two entries reconciliation consumes. */
append_replay_session_with_two_dispatches_and_pend(f, 2000ULL, alloc_a,
502ULL, 0, alloc_c);
/* Round 2: job B (502) is still genuinely PEND (never dispatched, never
* state-changed) so builtin_fcfs's pendJobs discipline re-offers it to
* the live picker again this round -- it is NOT an already-resolved
* job, so reconciliation has nothing to do with it; it simply recurs
* as an ordinary live PEND, same as job D (504), the fresh arrival.
* Recorded order [502, 504] matches queue_time ordering (502 queued at
* ts=1500, before 504's ts=2500) -- no already-resolved job is in the
* mix at all this round, proving no cross-round bleed from round 1's
* own compaction. */
append_jobs_push_event(f, 2500ULL, 504ULL, "normal");
append_replay_session_with_two_pends(f, 3000ULL, 502ULL, 0, 504ULL, 0);
append_end_of_stream_marker(f);
fclose(f);
captured = run_sim_and_capture_stdout(path, true, &rc);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_NULL_MESSAGE(
strstr(captured, "MISMATCH"),
"reconciling TWO already-resolved jobs in the same round (with a "
"genuinely-pending job's own recorded decision sandwiched between "
"them), followed by an ordinary second round with no reconciliation "
"needed, must not produce a false --sim-verify mismatch anywhere "
"(Requirement 5.1, 5.2)");
free(captured);
printf("test_verify_with_multiple_already_resolved_jobs_across_rounds_"
"reports_no_mismatch: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
@ -1095,6 +1294,8 @@ int main(void)
RUN_TEST(test_verify_against_altered_copy_reports_exact_mismatch);
RUN_TEST(test_verify_disabled_reports_no_mismatches_even_when_altered);
RUN_TEST(test_verify_with_already_resolved_job_reports_no_mismatch);
RUN_TEST(
test_verify_with_multiple_already_resolved_jobs_across_rounds_reports_no_mismatch);
return UNITY_END();
}