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:
parent
b31907324c
commit
88bed529a7
4 changed files with 803 additions and 1 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue