feat(libxce-alloc-preason-equality): sim verify uses public job_alloc_eq and logs the first difference (task 4.3)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-28 10:58:41 +00:00
parent 69112c2d22
commit 24b057189f
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
4 changed files with 415 additions and 217 deletions

View file

@ -14,9 +14,11 @@
* Unit-level (white-box): calls sim_decision_eq()/sim_log_mismatch()
* (sim_verify.c, task 11.4's own sibling file to sim_loop.c) directly
* against hand-built replay_decision_t/xce_decision_t pairs -- DISPATCH
* match/mismatch (jobid, host, resource value), PEND match/mismatch
* match/mismatch (jobid, host, resource value, from), PEND match/mismatch
* (jobid, main_reason), a body_which/op type mismatch, and PREEMPT's
* documented always-false path. This is the only way to exercise DISPATCH
* documented always-false path -- each mismatch also asserting the
* first-difference description (libxce-alloc-preason-equality
* Requirement 5.3). This is the only way to exercise DISPATCH
* comparison at all: sim_loop.c's real picker thread
* (sim_picker_thread_fn) only ever emits PEND decisions (its own doc
* comment: "conservatively PENDs every candidate ... rather than
@ -558,8 +560,24 @@ static replay_decision_t make_replay_pend_decision(uint64_t jobid,
/* Unit-level tests: sim_decision_eq() directly. */
/* ---------------------------------------------------------------------- */
/* Asserts that sim_decision_eq()'s description (libxce-alloc-preason-
* equality Requirement 5.3) was produced and contains `needle` -- the name
* of the differing field and both of its values -- then frees it. */
static void assert_why_names(char* why, const char* needle)
{
char msg[512];
TEST_ASSERT_NOT_NULL_MESSAGE(why, "a mismatch must produce a "
"description of the difference");
snprintf(msg, sizeof(msg), "description \"%s\" must contain \"%s\"",
why, needle);
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(why, needle), msg);
free(why);
}
static void test_dispatch_decisions_with_identical_content_match(void)
{
char* why;
xce_alloc_t* res_a[1]
= { make_test_alloc("gpu", "resv1", 1, 2.0, 100, 1800) };
xce_host_alloc_t* host_a[1] = { make_test_host_alloc("hostA", res_a, 1) };
@ -577,7 +595,11 @@ static void test_dispatch_decisions_with_identical_content_match(void)
* trees: proves the comparison is real structural equality, not a
* pointer/memcmp shortcut (this file's own header comment). */
TEST_ASSERT_NOT_EQUAL(expected_alloc, actual_alloc);
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual));
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual, NULL));
why = (char*)"stale";
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual, &why));
TEST_ASSERT_NULL_MESSAGE(why, "an equal comparison must leave no "
"description");
printf("test_dispatch_decisions_with_identical_content_match: PASS\n");
}
@ -595,7 +617,17 @@ static void test_dispatch_decisions_differing_jobid_mismatch(void)
replay_decision_t expected = make_replay_dispatch_decision(expected_alloc);
xce_decision_t* actual = make_dispatch_decision(actual_alloc);
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual));
char ja[64];
char jb[64];
char needle[160];
char* why = NULL;
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual, NULL));
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual, &why));
fmt_jobid(ja, sizeof(ja), 42ULL);
fmt_jobid(jb, sizeof(jb), 43ULL);
snprintf(needle, sizeof(needle), "jobid: %s != %s", ja, jb);
assert_why_names(why, needle);
printf("test_dispatch_decisions_differing_jobid_mismatch: PASS\n");
}
@ -613,7 +645,11 @@ static void test_dispatch_decisions_differing_host_mismatch(void)
replay_decision_t expected = make_replay_dispatch_decision(expected_alloc);
xce_decision_t* actual = make_dispatch_decision(actual_alloc);
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual));
char* why = NULL;
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual, &why));
assert_why_names(why, "allocs[0](host=hostA).host: \"hostA\" != "
"\"hostB\"");
printf("test_dispatch_decisions_differing_host_mismatch: PASS\n");
}
@ -631,27 +667,30 @@ static void test_dispatch_decisions_differing_resource_value_mismatch(void)
replay_decision_t expected = make_replay_dispatch_decision(expected_alloc);
xce_decision_t* actual = make_dispatch_decision(actual_alloc);
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual));
char* why = NULL;
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual, &why));
assert_why_names(why, "allocs[0](host=hostA).allocs[0](resname=gpu)."
"value: 1 != 2");
printf("test_dispatch_decisions_differing_resource_value_mismatch: "
"PASS\n");
}
/*
* test_dispatch_decisions_differing_from_timestamp_still_match -
* bug-1838-followup regression: `from` (xce_alloc_t's wall-clock reservation-
* start stamp, set unconditionally by make_alloc() via time(NULL) --
* xcealloc.c) is real-OS-clock-derived, not part of a decision's semantic
* identity. `expected`'s `from` is whatever second the ORIGINAL recording
* ran in; `actual`'s is whatever second THIS run rebuilds the allocation in
* -- two independent real-world timestamps that will essentially never
* agree for any replay of a real recording, even when the decision itself
* (job/host/resource/value/duration) is byte-for-byte the one that was
* recorded. Uses two `from` values far enough apart (a Unix-epoch-plausible
* value vs. 1) that they could never coincidentally collide, proving this
* is deliberate exclusion, not a fluke of nearby clock reads.
* test_dispatch_decisions_differing_from_timestamp_mismatch -
* libxce-alloc-preason-equality Requirement 5.1 (inverts the former
* bug-1838-followup "from still matches" lock): `from` used to be excluded
* because make_alloc() stamped it with time(NULL), a real-OS-clock value
* that differed between a recording and its replay. Every allocation a
* session decides now carries that session's start (Requirement 4.1,
* scheduler.c's sched_make_primary_alloc_()), and a replayed session starts
* at its recorded beginRound, so the two sides agree for a faithful replay
* and `from` is compared (by libxce's job_alloc_eq()). A `from` difference
* is therefore a real divergence: it must be reported, and the description
* must name `from` with both values.
*/
static void test_dispatch_decisions_differing_from_timestamp_still_match(void)
static void test_dispatch_decisions_differing_from_timestamp_mismatch(void)
{
xce_alloc_t* res_a[1]
= { make_test_alloc("gpu", "resv1", 1, 2.0, 1790000000ULL, 1800) };
@ -665,10 +704,13 @@ static void test_dispatch_decisions_differing_from_timestamp_still_match(void)
replay_decision_t expected = make_replay_dispatch_decision(expected_alloc);
xce_decision_t* actual = make_dispatch_decision(actual_alloc);
char* why = NULL;
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual));
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual, &why));
assert_why_names(why, "allocs[0](host=hostA).allocs[0](resname=gpu)."
"from: 1790000000 != 1");
printf("test_dispatch_decisions_differing_from_timestamp_still_match: "
printf("test_dispatch_decisions_differing_from_timestamp_mismatch: "
"PASS\n");
}
@ -677,8 +719,10 @@ static void test_dispatch_decisions_differing_from_timestamp_still_match(void)
* regression: `instid` (xce_alloc_t's per-process monotonic allocation-
* instance counter, stamped from `sched->core.nextid++` at allocation-build
* time -- scheduler.c's sched_get_id()) is an execution-order bookkeeping
* value, not part of a decision's semantic identity, exactly like `from`
* above (bug-1838-followup). The ORIGINAL recording's counter may have been
* value, not part of a decision's semantic identity. It is the one field
* libxce's alloc_eq() excludes (`from`, unlike instid, is now compared: see
* test_dispatch_decisions_differing_from_timestamp_mismatch above). The
* ORIGINAL recording's counter may have been
* running, and building allocations, for far longer before this capture
* window started; a fresh replay's own counter starts whatever its loaded
* snapshot's nextid happens to be -- the two have no reason to reach the
@ -703,7 +747,7 @@ static void test_dispatch_decisions_differing_instid_still_match(void)
replay_decision_t expected = make_replay_dispatch_decision(expected_alloc);
xce_decision_t* actual = make_dispatch_decision(actual_alloc);
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual));
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual, NULL));
printf("test_dispatch_decisions_differing_instid_still_match: PASS\n");
}
@ -713,11 +757,14 @@ static void test_pend_decisions_with_identical_content_match(void)
replay_decision_t expected = make_replay_pend_decision(9001ULL, 3);
xce_pendreason_t* reason = make_pendreason(9001ULL);
xce_decision_t* actual;
char* why;
reason->reasons->main_reason = 3;
actual = make_pend_decision(reason);
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual));
why = (char*)"stale";
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual, &why));
TEST_ASSERT_NULL(why);
printf("test_pend_decisions_with_identical_content_match: PASS\n");
}
@ -727,7 +774,16 @@ static void test_pend_decisions_differing_jobid_mismatch(void)
replay_decision_t expected = make_replay_pend_decision(9001ULL, 0);
xce_decision_t* actual = make_pend_decision(make_pendreason(9002ULL));
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual));
char ja[64];
char jb[64];
char needle[160];
char* why = NULL;
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual, &why));
fmt_jobid(ja, sizeof(ja), 9001ULL);
fmt_jobid(jb, sizeof(jb), 9002ULL);
snprintf(needle, sizeof(needle), "jobid: %s != %s", ja, jb);
assert_why_names(why, needle);
printf("test_pend_decisions_differing_jobid_mismatch: PASS\n");
}
@ -739,7 +795,10 @@ static void test_pend_decisions_differing_main_reason_mismatch(void)
/* actual's main_reason stays 0 (make_pendreason()'s own fresh-calloc
* default) -- differs from expected's 3. */
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual));
char* why = NULL;
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual, &why));
assert_why_names(why, "main_reason: 3 != 0");
printf("test_pend_decisions_differing_main_reason_mismatch: PASS\n");
}
@ -789,7 +848,7 @@ static void test_pend_decisions_differing_reason_detail_still_match(void)
actual = make_pend_decision(reason);
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual));
TEST_ASSERT_TRUE(sim_decision_eq(&expected, actual, NULL));
printf("test_pend_decisions_differing_reason_detail_still_match: PASS\n");
}
@ -801,8 +860,12 @@ static void test_decision_kind_mismatch_reported(void)
xce_decision_t* actual = make_dispatch_decision(alloc);
/* Recorded PEND vs. live DISPATCH for the SAME jobid: still a
* mismatch -- op/body_which disagreement is reported, not ignored. */
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual));
* mismatch -- op/body_which disagreement is reported, not ignored.
* The description names the kinds, recorded side first. */
char* why = NULL;
TEST_ASSERT_FALSE(sim_decision_eq(&expected, actual, &why));
assert_why_names(why, "kind: PEND != DISPATCH");
printf("test_decision_kind_mismatch_reported: PASS\n");
}
@ -826,7 +889,10 @@ static void test_preempt_decision_always_reports_mismatch(void)
memset(&expected, 0, sizeof(expected));
expected.body_which = ReplayDecision_body_preempt;
TEST_ASSERT_FALSE(sim_decision_eq(&expected, &actual));
char* why = NULL;
TEST_ASSERT_FALSE(sim_decision_eq(&expected, &actual, &why));
assert_why_names(why, "PREEMPT comparison not supported");
printf("test_preempt_decision_always_reports_mismatch: PASS\n");
}
@ -836,9 +902,18 @@ static void test_decision_eq_defends_against_null_inputs(void)
xce_decision_t* actual = make_pend_decision(make_pendreason(1ULL));
replay_decision_t expected = make_replay_pend_decision(1ULL, 0);
TEST_ASSERT_FALSE(sim_decision_eq(NULL, actual));
TEST_ASSERT_FALSE(sim_decision_eq(&expected, NULL));
TEST_ASSERT_FALSE(sim_decision_eq(NULL, NULL));
char* why = NULL;
TEST_ASSERT_FALSE(sim_decision_eq(NULL, actual, NULL));
TEST_ASSERT_FALSE(sim_decision_eq(&expected, NULL, NULL));
TEST_ASSERT_FALSE(sim_decision_eq(NULL, NULL, NULL));
/* A missing side is named: which of the two has no decision. */
TEST_ASSERT_FALSE(sim_decision_eq(NULL, actual, &why));
assert_why_names(why, "no recorded decision at this index");
why = NULL;
TEST_ASSERT_FALSE(sim_decision_eq(&expected, NULL, &why));
assert_why_names(why, "no live decision");
printf("test_decision_eq_defends_against_null_inputs: PASS\n");
}
@ -892,7 +967,15 @@ static void log_one_mismatch_for_capture(void)
replay_decision_t expected = make_replay_pend_decision(9001ULL, 3);
xce_decision_t* actual = make_pend_decision(make_pendreason(9001ULL));
sim_log_mismatch(12345ULL, 2, &expected, actual);
sim_log_mismatch(12345ULL, 2, &expected, actual, "main_reason: 3 != 0");
}
static void log_one_mismatch_without_why_for_capture(void)
{
replay_decision_t expected = make_replay_pend_decision(9001ULL, 3);
xce_decision_t* actual = make_pend_decision(make_pendreason(9001ULL));
sim_log_mismatch(12345ULL, 2, &expected, actual, NULL);
}
static void test_log_mismatch_reports_logical_time_and_decision_detail(void)
@ -925,6 +1008,59 @@ static void test_log_mismatch_reports_logical_time_and_decision_detail(void)
"PASS\n");
}
/*
* Requirement 5.3: the mismatch line keeps its logical-time and decision
* context and appends the difference description it is given; with no
* description (only when allocating it failed) the line is logged without
* one, unchanged otherwise.
*/
static void test_log_mismatch_appends_provided_description(void)
{
char* buf = capture_stdout_around(log_one_mismatch_for_capture);
char* nodiff;
TEST_ASSERT_NOT_NULL(strstr(buf, "begin_round=12345"));
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(buf, " -- diff: main_reason: 3 != 0"),
"the mismatch line must append the provided description");
free(buf);
nodiff = capture_stdout_around(log_one_mismatch_without_why_for_capture);
TEST_ASSERT_NOT_NULL(strstr(nodiff, "MISMATCH"));
TEST_ASSERT_NOT_NULL(strstr(nodiff, "decision_index=2"));
TEST_ASSERT_NULL_MESSAGE(strstr(nodiff, "-- diff:"),
"with no description the line carries no "
"diff clause");
free(nodiff);
printf("test_log_mismatch_appends_provided_description: PASS\n");
}
/* Every line of `captured` that reports a MISMATCH also carries a
* ` -- diff: ` clause (Requirement 5.3: each mismatch names its
* difference). Returns the number of MISMATCH lines seen. */
static int assert_every_mismatch_line_has_diff(const char* captured)
{
const char* line = captured;
int n = 0;
while ((line != NULL) && (*line != '\0')) {
const char* eol = strchr(line, '\n');
size_t len = (eol != NULL) ? (size_t)(eol - line) : strlen(line);
char* one = strndup(line, len);
TEST_ASSERT_NOT_NULL(one);
if (strstr(one, "MISMATCH") != NULL) {
n++;
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(one, " -- diff: "), one);
}
free(one);
line = (eol != NULL) ? (eol + 1) : NULL;
}
return n;
}
/* ---------------------------------------------------------------------- */
/* Integration-level fixture helpers: a real .replay stream driving the */
/* REAL builtin_fcfs picker to a genuine live PEND decision. */
@ -1568,12 +1704,55 @@ static void test_verify_against_altered_copy_reports_exact_mismatch(void)
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, jobid_marker),
"the mismatch report must name the specific job");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, " -- diff: main_reason: 99 != 0"),
"the mismatch report must name the differing field and both values");
TEST_ASSERT_EQUAL_INT(1, assert_every_mismatch_line_has_diff(captured));
free(captured);
printf("test_verify_against_altered_copy_reports_exact_mismatch: "
"PASS\n");
}
/*
* A recorded decision left over after the live stream ends (recorded
* PEND(777), PEND(778); only job 777 was ever pushed, so the live picker
* emits only PEND(777)) is reported as a mismatch whose diff says which
* side is missing: "no live decision" (Requirement 5.3).
*/
static void test_verify_leftover_recorded_decision_names_missing_side(void)
{
char path[PATH_MAX];
FILE* f;
int rc;
char* captured;
f = write_header_and_snapshot(path, sizeof(path), "_leftover");
TEST_ASSERT_NOT_NULL(f);
append_queue_update_event(f, 1000ULL, "normal");
append_jobs_push_event(f, 1500ULL, 777ULL, "normal");
append_replay_session_with_two_pends(f, 2000ULL, 777ULL, 0, 778ULL, 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_NOT_NULL_MESSAGE(
strstr(captured, "decision_index=1"),
"the left-over recorded decision must be reported at its index");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, " -- diff: no live decision"),
"a left-over recorded decision's mismatch must name the missing "
"live side");
TEST_ASSERT_EQUAL_INT(1, assert_every_mismatch_line_has_diff(captured));
free(captured);
printf("test_verify_leftover_recorded_decision_names_missing_side: "
"PASS\n");
}
/*
* sim->verify == false must skip verification entirely, even against the
* SAME deliberately-altered fixture the previous test proves DOES mismatch
@ -1873,6 +2052,12 @@ test_reconciliation_no_longer_masks_a_pend_jobs_dispatch_mismatch(void)
"PEND-only live stub cannot reproduce it either, so --sim-verify "
"must correctly report a real mismatch here, not silently agree "
"with a decision nothing in this environment actually made");
/* Round 1: recorded DISPATCH vs live PEND; round 2: a live PEND with
* no recorded entry. Both lines name their difference. */
TEST_ASSERT_NOT_NULL(strstr(captured, " -- diff: kind: DISPATCH != PEND"));
TEST_ASSERT_NOT_NULL(
strstr(captured, " -- diff: no recorded decision at this index"));
TEST_ASSERT_TRUE(assert_every_mismatch_line_has_diff(captured) >= 2);
free(captured);
printf("test_reconciliation_no_longer_masks_a_pend_jobs_dispatch_"
@ -1887,7 +2072,7 @@ int main(void)
RUN_TEST(test_dispatch_decisions_differing_jobid_mismatch);
RUN_TEST(test_dispatch_decisions_differing_host_mismatch);
RUN_TEST(test_dispatch_decisions_differing_resource_value_mismatch);
RUN_TEST(test_dispatch_decisions_differing_from_timestamp_still_match);
RUN_TEST(test_dispatch_decisions_differing_from_timestamp_mismatch);
RUN_TEST(test_dispatch_decisions_differing_instid_still_match);
RUN_TEST(test_pend_decisions_with_identical_content_match);
RUN_TEST(test_pend_decisions_differing_jobid_mismatch);
@ -1897,8 +2082,10 @@ int main(void)
RUN_TEST(test_preempt_decision_always_reports_mismatch);
RUN_TEST(test_decision_eq_defends_against_null_inputs);
RUN_TEST(test_log_mismatch_reports_logical_time_and_decision_detail);
RUN_TEST(test_log_mismatch_appends_provided_description);
RUN_TEST(test_verify_against_self_reports_zero_mismatches);
RUN_TEST(test_verify_against_altered_copy_reports_exact_mismatch);
RUN_TEST(test_verify_leftover_recorded_decision_names_missing_side);
RUN_TEST(test_verify_disabled_reports_no_mismatches_even_when_altered);
RUN_TEST(test_verify_with_already_resolved_job_reports_no_mismatch);
RUN_TEST(