test(scheduler-sim-diagnostics): verify info limit is read-only and deterministic
Four new tests in test_repl_limit_table.c close out Requirement 5: - test_info_limit_never_mutates_global_limit_config_or_accounting_state / ..._host_scoped_... : snapshot a matched limit's config fields and its vres capacity/in-use zarray entries (both pointer identity and value) before and after repeated info limit calls; asserts byte-identical state and byte-identical repeated stdout output. - test_info_limit_on_empty_registry_is_repeatable_and_never_registers_plugin: confirms info limit never causes the plugin to become spuriously registered as a side effect of being queried. - test_repl_step_continue_with_info_limit_interleaved_matches_baseline_decision_sequence: a step/continue sequence run twice (once with info limit interleaved, once without) produces an identical decision sequence. Verified via real mutation testing: three targeted fake mutations were temporarily applied to repl_info.c, confirmed each new test fails exactly as predicted, then cleanly reverted (repl_info.c shows zero diff from HEAD). Zero production code changed -- this is a test-only validation task. Full 14-binary regression sweep + 78/78 meson aggregate confirm zero impact on any pre-existing info subcommand (Requirement 5.3). This closes out scheduler-sim-diagnostics' first capability (limit inspection, Requirements 1-5) end to end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
parent
afe22d3122
commit
97c182cbe4
2 changed files with 797 additions and 1 deletions
|
|
@ -49,7 +49,7 @@
|
|||
- _Requirements: 4.1, 4.2_
|
||||
- _Depends: 2.1, 3.1, 3.2_
|
||||
|
||||
- [ ] 5. Verify no mutation, replay determinism, and existing-subcommand regression
|
||||
- [x] 5. Verify no mutation, replay determinism, and existing-subcommand regression
|
||||
- Test: displaying limit information never alters configuration, accounting state, or scheduling decisions (assert config/accounting untouched before vs. after)
|
||||
- Test: a replay run twice — once with `info limit` issued mid-session, once without — produces identical scheduling decisions and identical final accounted state
|
||||
- Full existing `info` subcommand test suite (job/queue/worker/resource/summary) re-run to confirm zero output changes from this addition
|
||||
|
|
|
|||
|
|
@ -1607,6 +1607,794 @@ test_info_limit_dispatch_all_reaches_repl_info_limit_via_real_dispatch(void)
|
|||
"dispatch: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Task 5 (Requirement Set 1's own Validation task, Requirement 5): proves */
|
||||
/* -- rather than merely asserting by construction, as every prior task's */
|
||||
/* own tests above already do -- that displaying limit information never */
|
||||
/* mutates configuration or accounting state (5.1), that issuing it mid- */
|
||||
/* replay never perturbs scheduling decisions (5.2), and (via the full */
|
||||
/* regression sweep reported in this task's own status report, not a new */
|
||||
/* test here) that every pre-existing `info` subcommand's behavior is */
|
||||
/* unchanged (5.3). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Requirement 5.1 (GLOBAL/SHARED path): snapshots the matched limit's own
|
||||
* configured fields (val, all four consumers) BY VALUE, and the matched
|
||||
* vres's capacity/in-use zarray slots BY POINTER IDENTITY (a `put_resvals`-
|
||||
* style accidental write swaps in a NEW xce_val_t* at that index even if
|
||||
* the new value happened to coincide numerically, so pointer identity is a
|
||||
* strictly stronger check than value equality alone) -- then calls
|
||||
* `info limit` repeatedly, in every arg form this fixture can produce a
|
||||
* match or non-match for, through BOTH the direct entry point
|
||||
* (repl_info_limit(), task 2's own call shape) and the real repl_dispatch()
|
||||
* entry point (task 4's own call shape), and re-checks every snapshot
|
||||
* afterward.
|
||||
*/
|
||||
static void
|
||||
test_info_limit_never_mutates_global_limit_config_or_accounting_state(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_plugin_t plugin;
|
||||
xce_scheduler_t group_sched;
|
||||
xce_limit_t* matched = NULL;
|
||||
var limits_handle;
|
||||
xce_vres_t* vres;
|
||||
xce_val_t* cap_before;
|
||||
xce_val_t* used_before;
|
||||
xce_limit_val_t val_before;
|
||||
char* name_before;
|
||||
xce_consumer_t consumer_before[CONSUMER_MAX];
|
||||
char* captured1;
|
||||
char* captured2;
|
||||
char* captured3;
|
||||
char* captured4;
|
||||
char* captured5;
|
||||
int rc;
|
||||
int i;
|
||||
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
rc = xce_sched_core_init(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
register_per_host_resource(&sim, "mem");
|
||||
register_limit_plugin(&sim, &plugin, &group_sched);
|
||||
add_configured_limit(&plugin, "guarded_limit");
|
||||
|
||||
limits_handle = limit_get_configured_limits(&(sim.core));
|
||||
rc = key_find(limits_handle, (void*)"guarded_limit", (void**)&matched);
|
||||
TEST_ASSERT_TRUE(rc > 0);
|
||||
TEST_ASSERT_NOT_NULL(matched);
|
||||
|
||||
vres = instantiate_and_register_global_vres(&sim, &plugin, matched);
|
||||
put_shared_fvalue(&(sim.core.sharedInst), vres->vres->idx, 10.0);
|
||||
put_shared_fvalue(&(sim.core.sharedInUse), vres->vres->idx, 4.0);
|
||||
|
||||
/* ---- Snapshot BEFORE. ---- */
|
||||
name_before = matched->name;
|
||||
val_before = matched->val;
|
||||
for (i = 0; i < CONSUMER_MAX; i++) {
|
||||
if (matched->consumers[i] != NULL) {
|
||||
consumer_before[i] = *(matched->consumers[i]);
|
||||
} else {
|
||||
memset(&consumer_before[i], 0, sizeof(consumer_before[i]));
|
||||
}
|
||||
}
|
||||
cap_before
|
||||
= (xce_val_t*)zarray_get(&(sim.core.sharedInst), vres->vres->idx);
|
||||
used_before
|
||||
= (xce_val_t*)zarray_get(&(sim.core.sharedInUse), vres->vres->idx);
|
||||
TEST_ASSERT_NOT_NULL(cap_before);
|
||||
TEST_ASSERT_NOT_NULL(used_before);
|
||||
|
||||
/* ---- Exercise `info limit` repeatedly, in every arg form this
|
||||
* fixture supports (named hit, "all", named miss), both via the direct
|
||||
* call and via the real repl_dispatch() entry point -- Requirement 5.1
|
||||
* must hold for every one of these call shapes. ---- */
|
||||
captured1 = capture_info_limit(&sim, "guarded_limit");
|
||||
captured2 = capture_info_limit(&sim, "guarded_limit");
|
||||
captured3 = capture_info_limit(&sim, "all");
|
||||
captured4 = capture_dispatch(&sim, "info limit guarded_limit");
|
||||
captured5 = capture_info_limit(&sim, "does_not_exist");
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE(
|
||||
captured1, captured2,
|
||||
"Requirement 5.2 (no intervening step): 'info limit <name>' issued "
|
||||
"twice in a row must produce byte-identical output");
|
||||
|
||||
/* ---- Snapshot AFTER -- must be byte-for-byte/pointer-for-pointer
|
||||
* identical to BEFORE. ---- */
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
matched->name == name_before,
|
||||
"Requirement 5.1: the matched limit's own name pointer must not be "
|
||||
"reallocated/reassigned by displaying it");
|
||||
TEST_ASSERT_EQUAL_MEMORY_MESSAGE(
|
||||
&val_before, &(matched->val), sizeof(val_before),
|
||||
"Requirement 5.1: the matched limit's configured value (resname/"
|
||||
"percent/val) must be bit-for-bit unchanged after displaying it");
|
||||
for (i = 0; i < CONSUMER_MAX; i++) {
|
||||
if (matched->consumers[i] != NULL) {
|
||||
TEST_ASSERT_EQUAL_MEMORY_MESSAGE(
|
||||
&consumer_before[i], matched->consumers[i],
|
||||
sizeof(consumer_before[i]),
|
||||
"Requirement 5.1: each consumer's per_consumer flag/group "
|
||||
"pointer must be unchanged after displaying the limit");
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
cap_before
|
||||
== (xce_val_t*)zarray_get(&(sim.core.sharedInst),
|
||||
vres->vres->idx),
|
||||
"Requirement 5.1: the capacity zarray slot must still hold the "
|
||||
"SAME xce_val_t* -- no accidental write/reallocation");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
used_before
|
||||
== (xce_val_t*)zarray_get(&(sim.core.sharedInUse),
|
||||
vres->vres->idx),
|
||||
"Requirement 5.1: the in-use zarray slot must still hold the SAME "
|
||||
"xce_val_t* -- no accidental charge/write");
|
||||
TEST_ASSERT_EQUAL_FLOAT(10.0f, (float)cap_before->u.fvalue);
|
||||
TEST_ASSERT_EQUAL_FLOAT(4.0f, (float)used_before->u.fvalue);
|
||||
|
||||
free(captured1);
|
||||
free(captured2);
|
||||
free(captured3);
|
||||
free(captured4);
|
||||
free(captured5);
|
||||
|
||||
printf("test_info_limit_never_mutates_global_limit_config_or_"
|
||||
"accounting_state: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* Requirement 5.1, host-scoped path: mirrors the GLOBAL/SHARED no-mutation
|
||||
* test above, but for the per-worker fan-out path
|
||||
* (repl_print_limit_usage_per_host()) -- a host-scoped limit's capacity/
|
||||
* in-use zarray entries live on EACH worker's OWN resvals/resInUse, not on
|
||||
* core.sharedInst/sharedInUse, so this is a genuinely distinct read path
|
||||
* needing its own no-mutation proof.
|
||||
*/
|
||||
static void
|
||||
test_info_limit_never_mutates_host_scoped_limit_accounting_state(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_plugin_t plugin;
|
||||
xce_scheduler_t group_sched;
|
||||
xce_limit_t* matched = NULL;
|
||||
var limits_handle;
|
||||
xce_vres_t* vres;
|
||||
xce_sched_worker_t* wA;
|
||||
xce_sched_worker_t* wB;
|
||||
xce_val_t* capA_before;
|
||||
xce_val_t* usedA_before;
|
||||
xce_val_t* capB_before;
|
||||
xce_val_t* usedB_before;
|
||||
char* captured1;
|
||||
char* captured2;
|
||||
char* captured3;
|
||||
int rc;
|
||||
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
rc = xce_sched_core_init(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
register_per_host_resource(&sim, "mem");
|
||||
register_limit_plugin(&sim, &plugin, &group_sched);
|
||||
add_configured_percent_limit(&plugin, "guarded_host_limit", "mem");
|
||||
|
||||
limits_handle = limit_get_configured_limits(&(sim.core));
|
||||
rc = key_find(limits_handle, (void*)"guarded_host_limit",
|
||||
(void**)&matched);
|
||||
TEST_ASSERT_TRUE(rc > 0);
|
||||
TEST_ASSERT_NOT_NULL(matched);
|
||||
|
||||
vres = instantiate_and_register_host_vres(&sim, &plugin, matched);
|
||||
wA = build_bare_worker(&sim, "hostA");
|
||||
wB = build_bare_worker(&sim, "hostB");
|
||||
|
||||
put_shared_fvalue(&(wA->resvals), vres->vres->idx, 5.0);
|
||||
put_shared_fvalue(&(wA->resInUse), vres->vres->idx, 2.0);
|
||||
put_shared_fvalue(&(wB->resvals), vres->vres->idx, 5.0);
|
||||
put_shared_fvalue(&(wB->resInUse), vres->vres->idx, 3.0);
|
||||
|
||||
capA_before = (xce_val_t*)zarray_get(&(wA->resvals), vres->vres->idx);
|
||||
usedA_before = (xce_val_t*)zarray_get(&(wA->resInUse), vres->vres->idx);
|
||||
capB_before = (xce_val_t*)zarray_get(&(wB->resvals), vres->vres->idx);
|
||||
usedB_before = (xce_val_t*)zarray_get(&(wB->resInUse), vres->vres->idx);
|
||||
TEST_ASSERT_NOT_NULL(capA_before);
|
||||
TEST_ASSERT_NOT_NULL(usedA_before);
|
||||
TEST_ASSERT_NOT_NULL(capB_before);
|
||||
TEST_ASSERT_NOT_NULL(usedB_before);
|
||||
|
||||
captured1 = capture_info_limit(&sim, "guarded_host_limit");
|
||||
captured2 = capture_info_limit(&sim, "guarded_host_limit");
|
||||
captured3 = capture_dispatch(&sim, "info limit guarded_host_limit");
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE(
|
||||
captured1, captured2,
|
||||
"Requirement 5.2: 'info limit <name>' issued twice in a row with "
|
||||
"no intervening step must produce byte-identical output for a "
|
||||
"host-scoped limit too");
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
capA_before
|
||||
== (xce_val_t*)zarray_get(&(wA->resvals), vres->vres->idx),
|
||||
"Requirement 5.1: hostA's capacity zarray slot must still hold the "
|
||||
"SAME xce_val_t*");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
usedA_before
|
||||
== (xce_val_t*)zarray_get(&(wA->resInUse), vres->vres->idx),
|
||||
"Requirement 5.1: hostA's in-use zarray slot must still hold the "
|
||||
"SAME xce_val_t*");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
capB_before
|
||||
== (xce_val_t*)zarray_get(&(wB->resvals), vres->vres->idx),
|
||||
"Requirement 5.1: hostB's capacity zarray slot must still hold the "
|
||||
"SAME xce_val_t*");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
usedB_before
|
||||
== (xce_val_t*)zarray_get(&(wB->resInUse), vres->vres->idx),
|
||||
"Requirement 5.1: hostB's in-use zarray slot must still hold the "
|
||||
"SAME xce_val_t*");
|
||||
TEST_ASSERT_EQUAL_FLOAT(5.0f, (float)capA_before->u.fvalue);
|
||||
TEST_ASSERT_EQUAL_FLOAT(2.0f, (float)usedA_before->u.fvalue);
|
||||
TEST_ASSERT_EQUAL_FLOAT(5.0f, (float)capB_before->u.fvalue);
|
||||
TEST_ASSERT_EQUAL_FLOAT(3.0f, (float)usedB_before->u.fvalue);
|
||||
|
||||
free(captured1);
|
||||
free(captured2);
|
||||
free(captured3);
|
||||
|
||||
printf("test_info_limit_never_mutates_host_scoped_limit_accounting_"
|
||||
"state: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* Requirement 5.1/5.2, the degenerate "nothing configured at all" case:
|
||||
* calling `info limit` (named and "all") against a sim whose limit plugin
|
||||
* was never registered must be safely repeatable and must not, e.g.,
|
||||
* spuriously register/create the plugin as a side effect of merely being
|
||||
* asked about it -- key_find(core->plugins, "xce_sched_limit", ...) must
|
||||
* keep missing after these calls, exactly as it did before.
|
||||
*/
|
||||
static void
|
||||
test_info_limit_on_empty_registry_is_repeatable_and_never_registers_plugin(
|
||||
void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_plugin_t* found_before = NULL;
|
||||
xce_sched_plugin_t* found_after = NULL;
|
||||
char* captured1;
|
||||
char* captured2;
|
||||
char* captured3;
|
||||
char* captured4;
|
||||
int rc_before;
|
||||
int rc_after;
|
||||
int rc;
|
||||
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
rc = xce_sched_core_init(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc_before = key_find(sim.core.plugins, "xce_sched_limit",
|
||||
(void**)&found_before);
|
||||
|
||||
captured1 = capture_info_limit(&sim, "all");
|
||||
captured2 = capture_info_limit(&sim, "all");
|
||||
captured3 = capture_info_limit(&sim, "anything");
|
||||
captured4 = capture_dispatch(&sim, "info limit all");
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE(
|
||||
captured1, captured2,
|
||||
"Requirement 5.2: repeated 'info limit all' calls against a never-"
|
||||
"registered plugin must produce byte-identical output");
|
||||
|
||||
rc_after = key_find(sim.core.plugins, "xce_sched_limit",
|
||||
(void**)&found_after);
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
rc_before <= 0,
|
||||
"sanity: the limit plugin must genuinely be absent before any "
|
||||
"'info limit' call");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
rc_before, rc_after,
|
||||
"Requirement 5.1: 'info limit' must never spuriously register the "
|
||||
"limit plugin as a side effect of being asked about it");
|
||||
TEST_ASSERT_NULL_MESSAGE(
|
||||
found_after,
|
||||
"Requirement 5.1: the limit plugin must still be absent after "
|
||||
"every form of 'info limit' has been called against it");
|
||||
|
||||
free(captured1);
|
||||
free(captured2);
|
||||
free(captured3);
|
||||
free(captured4);
|
||||
|
||||
printf("test_info_limit_on_empty_registry_is_repeatable_and_never_"
|
||||
"registers_plugin: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Requirement 5.2 (determinism): `info limit` issued mid-replay must not */
|
||||
/* perturb the replay's own scheduling decisions -- extends the */
|
||||
/* established REPL-vs-batch equivalence discipline */
|
||||
/* (test_repl_equivalence.c) to this new read path specifically, per */
|
||||
/* design.md's own Integration Tests bullet and */
|
||||
/* scheduler-driver-equivalence.md's testing discipline. Every helper */
|
||||
/* below is duplicated (not shared) from test_repl_equivalence.c, per this */
|
||||
/* file family's own established "duplicate, don't share" convention (no */
|
||||
/* shared test-utility header exists across this directory) -- prefixed */
|
||||
/* det_ to avoid any symbol collision within this TU. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void det_make_tmp_dir(char* out, size_t out_len, const char* suffix)
|
||||
{
|
||||
char tmpl[PATH_MAX];
|
||||
|
||||
snprintf(tmpl, sizeof(tmpl), "/tmp/sched_repllimitdet%s_XXXXXX", suffix);
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
|
||||
TEST_ASSERT_TRUE(strlen(tmpl) < out_len);
|
||||
strcpy(out, tmpl);
|
||||
}
|
||||
|
||||
static void det_build_fixture_scheduler(xce_scheduler_t* sched,
|
||||
char* record_dir)
|
||||
{
|
||||
int rc;
|
||||
|
||||
memset(sched, 0, sizeof(*sched));
|
||||
mylock_init(&(sched->record_lock));
|
||||
|
||||
rc = xce_sched_core_init(&(sched->core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
sched->core.job_pick_plugin = (char*)"builtin_fcfs";
|
||||
sched->record_dir = record_dir;
|
||||
}
|
||||
|
||||
static FILE* det_write_header_and_snapshot(char* out_path,
|
||||
size_t out_path_len,
|
||||
const char* suffix)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
int rc;
|
||||
|
||||
det_make_tmp_dir(record_dir, sizeof(record_dir), suffix);
|
||||
det_build_fixture_scheduler(&sched, record_dir);
|
||||
|
||||
rc = sched_record_start(&sched, "fixture.replay");
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_path_len);
|
||||
strcpy(out_path, sched.record_file_path);
|
||||
|
||||
return sched.record_file;
|
||||
}
|
||||
|
||||
static void det_append_queue_update_event(FILE* f, uint64_t ts,
|
||||
const char* queue_name,
|
||||
int32_t priority)
|
||||
{
|
||||
xce_queue_t queue;
|
||||
replay_event_t src;
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplayEvent_ptr p;
|
||||
|
||||
memset(&queue, 0, sizeof(queue));
|
||||
queue.queue = (char*)queue_name;
|
||||
queue.status = QUEUE_STAT_OPEN;
|
||||
queue.priority = priority;
|
||||
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.ts = ts;
|
||||
src.body_which = ReplayEvent_body_queueUpdate;
|
||||
src.body.queue_update = &queue;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
encode_ReplayEvent_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
static void det_append_jobs_push_event(FILE* f, uint64_t ts, uint64_t jobid,
|
||||
const char* queue_name)
|
||||
{
|
||||
job_submit_data_t submit;
|
||||
xce_job_t job;
|
||||
xce_job_t* jobs[1];
|
||||
xce_jobs_data_t jobs_data;
|
||||
replay_event_t src;
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplayEvent_ptr p;
|
||||
|
||||
memset(&submit, 0, sizeof(submit));
|
||||
submit.queue = (char*)queue_name;
|
||||
submit.user = (char*)"alice";
|
||||
submit.mintasks = 1;
|
||||
submit.maxtasks = 1;
|
||||
|
||||
memset(&job, 0, sizeof(job));
|
||||
job.jobid = jobid;
|
||||
job.state = JOB_STAT_PEND;
|
||||
job.submit = &submit;
|
||||
|
||||
jobs[0] = &job;
|
||||
|
||||
memset(&jobs_data, 0, sizeof(jobs_data));
|
||||
jobs_data.n_total = 1;
|
||||
jobs_data.count = 1;
|
||||
jobs_data.jobs = jobs;
|
||||
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.ts = ts;
|
||||
src.body_which = ReplayEvent_body_jobsPush;
|
||||
src.body.jobs_push = &jobs_data;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
encode_ReplayEvent_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
static void det_append_replay_session(FILE* f, uint64_t begin_round)
|
||||
{
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplaySession_ptr sp;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
sp = new_ReplaySession(cs);
|
||||
ReplaySession_set_beginRound(sp, begin_round);
|
||||
capn_setp(capn_root(&c), 0, sp.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
static void det_append_end_of_stream_marker(FILE* f)
|
||||
{
|
||||
replay_event_t src;
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplayEvent_ptr p;
|
||||
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.body_which = ReplayEvent_body_endSession;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
encode_ReplayEvent_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
static void det_write_text_file(const char* path, const char* content)
|
||||
{
|
||||
FILE* f = fopen(path, "w");
|
||||
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
TEST_ASSERT_TRUE(fputs(content, f) >= 0);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void det_decode_all_sessions(const char* path, replay_session_t*** out,
|
||||
int* out_count)
|
||||
{
|
||||
FILE* f = fopen(path, "r");
|
||||
replay_session_t** sessions = NULL;
|
||||
int count = 0;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
|
||||
for (;;) {
|
||||
struct capn c;
|
||||
capn_ptr root;
|
||||
|
||||
if (capn_init_fp(&c, f, 0) != 0) {
|
||||
break; /* EOF */
|
||||
}
|
||||
|
||||
if (msg_root_has_shape(&c, &root, ReplaySession_word_count,
|
||||
ReplaySession_pointer_count)) {
|
||||
ReplaySession_ptr sp;
|
||||
replay_session_t* decoded = NULL;
|
||||
replay_session_t** grown;
|
||||
|
||||
sp.p = root;
|
||||
decode_ReplaySession_ptr(&decoded, sp);
|
||||
capn_free(&c);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(decoded);
|
||||
|
||||
grown = (replay_session_t**)realloc(
|
||||
sessions, (size_t)(count + 1) * sizeof(*sessions));
|
||||
TEST_ASSERT_NOT_NULL(grown);
|
||||
sessions = grown;
|
||||
sessions[count] = decoded;
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
|
||||
*out = sessions;
|
||||
*out_count = count;
|
||||
}
|
||||
|
||||
static void det_free_all_sessions(replay_session_t** sessions, int count)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
free_ReplaySession_ptr(&(sessions[i]));
|
||||
}
|
||||
free(sessions);
|
||||
}
|
||||
|
||||
static int det_replay_decision_content_eq(const replay_decision_t* a,
|
||||
const replay_decision_t* b)
|
||||
{
|
||||
if ((a == NULL) || (b == NULL)) {
|
||||
return 0;
|
||||
}
|
||||
if (a->body_which != b->body_which) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (a->body_which) {
|
||||
case ReplayDecision_body_dispatch:
|
||||
if ((a->body.dispatch == NULL) || (b->body.dispatch == NULL)) {
|
||||
return 0;
|
||||
}
|
||||
return sim_verify_job_alloc_eq(a->body.dispatch->alloc,
|
||||
b->body.dispatch->alloc);
|
||||
|
||||
case ReplayDecision_body_pend:
|
||||
if ((a->body.pend == NULL) || (b->body.pend == NULL)) {
|
||||
return 0;
|
||||
}
|
||||
return (a->body.pend->jobid == b->body.pend->jobid)
|
||||
&& sim_verify_job_preason_eq(a->body.pend->reason,
|
||||
b->body.pend->reason);
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* det_run_scripted_repl_session - runs sched_sim_driver_run() against
|
||||
* `fixture_path` in interactive mode, driven by `script_content` fed as
|
||||
* stdin, writing its own decision stream to a fresh temp file whose path is
|
||||
* returned in `out_path`. Forked, matching test_repl_equivalence.c's own
|
||||
* rationale verbatim: `continue` re-enters the REPL at replay completion
|
||||
* (bug-1594) rather than returning, so the script's trailing `quit` always
|
||||
* exit(0)s -- this must happen in a child, or it would tear down this
|
||||
* whole test binary.
|
||||
*/
|
||||
static void det_run_scripted_repl_session(const char* fixture_path,
|
||||
const char* script_content,
|
||||
char* out_path,
|
||||
size_t out_path_len)
|
||||
{
|
||||
char script_dir[PATH_MAX];
|
||||
char script_path[PATH_MAX];
|
||||
int out_fd;
|
||||
sched_sim_args_t args;
|
||||
pid_t pid;
|
||||
int status;
|
||||
|
||||
snprintf(out_path, out_path_len, "/tmp/xce_repllimitdet_out_XXXXXX");
|
||||
out_fd = mkstemp(out_path);
|
||||
TEST_ASSERT_TRUE(out_fd >= 0);
|
||||
close(out_fd);
|
||||
|
||||
det_make_tmp_dir(script_dir, sizeof(script_dir), "_script");
|
||||
snprintf(script_path, sizeof(script_path), "%s/script.txt", script_dir);
|
||||
det_write_text_file(script_path, script_content);
|
||||
|
||||
memset(&args, 0, sizeof(args));
|
||||
args.replay_path = (char*)fixture_path;
|
||||
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
|
||||
args.sim_speed = 0;
|
||||
args.output_path = out_path;
|
||||
args.interactive = true;
|
||||
|
||||
pid = fork();
|
||||
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed");
|
||||
|
||||
if (pid == 0) {
|
||||
xce_sched_sim_driver_t sim;
|
||||
|
||||
if (freopen(script_path, "r", stdin) == NULL) {
|
||||
_exit(120);
|
||||
}
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
(void)sched_sim_driver_run(&sim, &args);
|
||||
_exit(111); /* unreachable -- quit must already have exited */
|
||||
}
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(pid, waitpid(pid, &status, 0),
|
||||
"waitpid must reap the REPL child");
|
||||
TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status),
|
||||
"the REPL child must exit normally via quit's "
|
||||
"exit(0), not crash/signal");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, WEXITSTATUS(status),
|
||||
"quit's exit(0) is the only correct exit "
|
||||
"path");
|
||||
}
|
||||
|
||||
/*
|
||||
* THE central determinism assertion (Requirement 5.2, design.md's own
|
||||
* Integration Tests bullet): the SAME `.replay` fixture, driven through the
|
||||
* SAME `step 1`/`continue` script, must produce byte-identical scheduling
|
||||
* decisions whether or not `info limit` is issued at several points along
|
||||
* the way. No limit plugin is registered for this fixture (this task's own
|
||||
* scope is proving that ISSUING the command has zero effect on the replay,
|
||||
* not re-proving the config/usage display correctness the no-mutation
|
||||
* tests above and tasks 2-3's own tests already cover) -- every `info
|
||||
* limit` call below still reaches the real repl_info_limit()/
|
||||
* limit_get_configured_limits() code path and resolves through its genuine
|
||||
* "no limits configured"/"no such limit" short-circuits, so this proves
|
||||
* THOSE paths are exactly as non-perturbing as the matched-limit path.
|
||||
*/
|
||||
static void
|
||||
test_repl_step_continue_with_info_limit_interleaved_matches_baseline_decision_sequence(
|
||||
void)
|
||||
{
|
||||
char fixture_path[PATH_MAX];
|
||||
char baseline_out_path[PATH_MAX];
|
||||
char infolimit_out_path[PATH_MAX];
|
||||
FILE* f;
|
||||
replay_session_t** baseline_sessions = NULL;
|
||||
replay_session_t** infolimit_sessions = NULL;
|
||||
int n_baseline = 0;
|
||||
int n_infolimit = 0;
|
||||
int total_baseline_decisions;
|
||||
int saw_jobid_501;
|
||||
int i;
|
||||
int j;
|
||||
|
||||
/* Same fixture shape test_repl_equivalence.c's own decision-parity test
|
||||
* establishes (two queues at different priorities, four jobs split
|
||||
* across three ReplaySession boundaries) -- reused verbatim rather than
|
||||
* re-derived, since this task validates an ADDITIONAL invariant (info
|
||||
* limit's own non-interference) against an already-proven decision-
|
||||
* producing fixture shape, not a new one. */
|
||||
f = det_write_header_and_snapshot(fixture_path, sizeof(fixture_path),
|
||||
"_fixture");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
|
||||
det_append_queue_update_event(f, 1000ULL, "high", 5);
|
||||
det_append_queue_update_event(f, 1001ULL, "normal", 1);
|
||||
|
||||
det_append_jobs_push_event(f, 1500ULL, 501ULL, "high");
|
||||
det_append_jobs_push_event(f, 1501ULL, 502ULL, "normal");
|
||||
det_append_replay_session(f, 2000ULL); /* session 1 */
|
||||
|
||||
det_append_jobs_push_event(f, 2500ULL, 503ULL, "high");
|
||||
det_append_replay_session(f, 3000ULL); /* session 2 */
|
||||
|
||||
det_append_jobs_push_event(f, 3500ULL, 504ULL, "normal");
|
||||
det_append_replay_session(f, 4000ULL); /* session 3 */
|
||||
|
||||
det_append_end_of_stream_marker(f);
|
||||
fclose(f);
|
||||
|
||||
/* ---- Run 1: baseline -- step/continue to completion, info limit
|
||||
* never issued. ---- */
|
||||
det_run_scripted_repl_session(fixture_path, "step 1\ncontinue\nquit\n",
|
||||
baseline_out_path,
|
||||
sizeof(baseline_out_path));
|
||||
|
||||
/* ---- Run 2: the SAME fixture and SAME step/continue script, but with
|
||||
* `info limit` issued before the pre-replay pause, between step and
|
||||
* continue, and after continue completes every remaining session --
|
||||
* every position an operator could type it at, in both arg forms this
|
||||
* feature accepts (a name and "all"). ---- */
|
||||
det_run_scripted_repl_session(
|
||||
fixture_path,
|
||||
"info limit all\nstep 1\ninfo limit all\ncontinue\ninfo limit "
|
||||
"ug1_slots\nquit\n",
|
||||
infolimit_out_path, sizeof(infolimit_out_path));
|
||||
|
||||
det_decode_all_sessions(baseline_out_path, &baseline_sessions,
|
||||
&n_baseline);
|
||||
det_decode_all_sessions(infolimit_out_path, &infolimit_sessions,
|
||||
&n_infolimit);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(3, n_baseline,
|
||||
"the fixture pushes exactly 3 "
|
||||
"ReplaySession boundaries");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
n_baseline, n_infolimit,
|
||||
"Requirement 5.2: issuing 'info limit' at several points in the "
|
||||
"REPL session must not change how many ReplaySession boundaries "
|
||||
"the replay produces");
|
||||
|
||||
/* Non-tautology guard #1 (mirrors test_repl_equivalence.c's own): an
|
||||
* unambiguous floor independent of any 'info limit' interference --
|
||||
* session 1 always has exactly 2 pending candidates. */
|
||||
TEST_ASSERT_EQUAL_INT(2, baseline_sessions[0]->n_decisions);
|
||||
|
||||
/* Non-tautology guard #2: pin real decision CONTENT on the baseline
|
||||
* side, not merely "both sides agree" (a shared bug could satisfy that
|
||||
* too). */
|
||||
saw_jobid_501 = 0;
|
||||
for (j = 0; j < baseline_sessions[0]->n_decisions; j++) {
|
||||
replay_decision_t* d = baseline_sessions[0]->decisions[j];
|
||||
|
||||
TEST_ASSERT_EQUAL_INT((int)ReplayDecision_body_pend,
|
||||
(int)d->body_which);
|
||||
if ((d->body.pend != NULL) && (d->body.pend->jobid == 501ULL)) {
|
||||
saw_jobid_501 = 1;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_TRUE_MESSAGE(saw_jobid_501,
|
||||
"session 1's baseline decisions must "
|
||||
"genuinely include job 501");
|
||||
|
||||
/* Non-tautology guard #3: a genuinely non-trivial total decision
|
||||
* count. */
|
||||
total_baseline_decisions = 0;
|
||||
for (i = 0; i < n_baseline; i++) {
|
||||
total_baseline_decisions += baseline_sessions[i]->n_decisions;
|
||||
}
|
||||
TEST_ASSERT_TRUE_MESSAGE(total_baseline_decisions >= 4,
|
||||
"the fixture must produce a genuinely "
|
||||
"non-trivial decision sequence");
|
||||
|
||||
/* ---- The central assertion: per session, per decision, the
|
||||
* info-limit-interleaved run must match the baseline exactly. ---- */
|
||||
for (i = 0; i < n_baseline; i++) {
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
baseline_sessions[i]->n_decisions,
|
||||
infolimit_sessions[i]->n_decisions,
|
||||
"Requirement 5.2: session decision COUNT must match whether or "
|
||||
"not 'info limit' was issued during the replay");
|
||||
|
||||
for (j = 0; j < baseline_sessions[i]->n_decisions; j++) {
|
||||
replay_decision_t* bd = baseline_sessions[i]->decisions[j];
|
||||
replay_decision_t* id = infolimit_sessions[i]->decisions[j];
|
||||
|
||||
TEST_ASSERT_EQUAL_INT((int)ReplayDecision_body_pend,
|
||||
(int)bd->body_which);
|
||||
TEST_ASSERT_EQUAL_INT((int)ReplayDecision_body_pend,
|
||||
(int)id->body_which);
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
det_replay_decision_content_eq(bd, id),
|
||||
"Requirement 5.2: 'info limit' issued mid-session must "
|
||||
"produce a byte-identical decision to the baseline at this "
|
||||
"session and position");
|
||||
}
|
||||
}
|
||||
|
||||
det_free_all_sessions(baseline_sessions, n_baseline);
|
||||
det_free_all_sessions(infolimit_sessions, n_infolimit);
|
||||
|
||||
unlink(baseline_out_path);
|
||||
unlink(infolimit_out_path);
|
||||
|
||||
printf(
|
||||
"test_repl_step_continue_with_info_limit_interleaved_matches_"
|
||||
"baseline_decision_sequence: PASS\n");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
|
|
@ -1647,5 +2435,13 @@ int main(void)
|
|||
test_info_limit_dispatch_named_reaches_repl_info_limit_via_real_dispatch);
|
||||
RUN_TEST(
|
||||
test_info_limit_dispatch_all_reaches_repl_info_limit_via_real_dispatch);
|
||||
RUN_TEST(
|
||||
test_info_limit_never_mutates_global_limit_config_or_accounting_state);
|
||||
RUN_TEST(
|
||||
test_info_limit_never_mutates_host_scoped_limit_accounting_state);
|
||||
RUN_TEST(
|
||||
test_info_limit_on_empty_registry_is_repeatable_and_never_registers_plugin);
|
||||
RUN_TEST(
|
||||
test_repl_step_continue_with_info_limit_interleaved_matches_baseline_decision_sequence);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue