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:
Rongsong Shen 2026-09-17 15:23:49 +00:00
parent afe22d3122
commit 97c182cbe4
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
2 changed files with 797 additions and 1 deletions

View file

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

View file

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