diff --git a/.kiro/specs/scheduler-sim-repl-verbosity/design.md b/.kiro/specs/scheduler-sim-repl-verbosity/design.md index 071228d9..3687b8b1 100644 --- a/.kiro/specs/scheduler-sim-repl-verbosity/design.md +++ b/.kiro/specs/scheduler-sim-repl-verbosity/design.md @@ -114,9 +114,12 @@ flowchart TD - `src/scheduler/include/repl.h` — no functional change expected; existing inline wrappers already cover the gating points this feature needs. ### New/Modified Test Files -**Correction (post-review, task 2.1)**: `test_repl_execution_control.c` was already over this project's 800-line cap (937 lines) before this feature's work began. Landing task 2.1's Round Rollup tests there per the original plan below pushed it to 1563 lines — nearly double the cap, and past the ~1000-line threshold this exact spec family already treats as a split trigger (`test_repl_snapshot_params.c` was split out of `test_repl_breakpoints_inspection.c` for precisely this reason; see that file's own meson.build comment). Round Rollup's tests (task 2.1, and any Round-Rollup-internal portion of later tasks, e.g. Requirement 3.5's flush-on-structural-event/breakpoint interaction) now live in a new `src/scheduler/testsuite/test_repl_round_rollup.c` instead, duplicating (not sharing) whatever pre-existing static fixture helpers it needs — the established convention this same file pair (`test_repl_breakpoints_inspection.c`/`test_repl_snapshot_params.c`) already follows, rather than factoring out a shared test-helper header. `test_repl_execution_control.c` remains the target for Requirement 1's Event Echo Gate tests, the `sim_print_decision` gate tests, the Requirement 5 no-regression checks, the new prompt-content tests (Requirement 5.5), and `help`-listing structure tests (Requirement 7.1-7.4) — but re-check its line count before landing each of those; if it approaches the cap again, split further rather than growing past it. -- `src/scheduler/testsuite/test_repl_execution_control.c` — child-process output-capture tests covering Requirement 1, the Requirement 5 no-regression checks (this file already drives `sched_sim_driver_run()` end-to-end and captures real process output via `child_continue_then_inspect`-style helpers); also gains the new prompt-content tests (Requirement 5.5) and `help`-listing structure tests (Requirement 7.1-7.4) — the natural home given it already exercises `readline()`/prompt-adjacent behavior and no `help` test exists anywhere today. -- `src/scheduler/testsuite/test_repl_round_rollup.c` (new, per the Correction above) — child-process output-capture tests for Requirements 2 and 3 (per-round decision summary, collapsing, the shared flush entry point, and its call from `xce_repl_on_replay_complete`). +**Correction (post-review, task 2.1)**: `test_repl_execution_control.c` was already over this project's 800-line cap (937 lines) before this feature's work began. Landing task 2.1's Round Rollup tests there per the original plan below pushed it to 1563 lines — nearly double the cap, and past the ~1000-line threshold this exact spec family already treats as a split trigger (`test_repl_snapshot_params.c` was split out of `test_repl_breakpoints_inspection.c` for precisely this reason; see that file's own meson.build comment). Round Rollup's tests (task 2.1, and any Round-Rollup-internal portion of later tasks, e.g. Requirement 3.5's flush-on-structural-event/breakpoint interaction) now live in a new `src/scheduler/testsuite/test_repl_round_rollup.c` instead, duplicating (not sharing) whatever pre-existing static fixture helpers it needs — the established convention this same file pair (`test_repl_breakpoints_inspection.c`/`test_repl_snapshot_params.c`) already follows, rather than factoring out a shared test-helper header. `test_repl_execution_control.c` remains at its restored 937-line pre-feature size (still over cap on its own, a pre-existing condition this feature doesn't need to fix) and is the target only for the Requirement 5 no-regression checks, the new prompt-content tests (Requirement 5.5), and `help`-listing structure tests (Requirement 7.1-7.4) — re-check its line count before landing each of those; if it approaches ~1000+ lines again, split further rather than growing past it. + +**Correction (post-review, task 3.1)**: applying the same reasoning that split Round Rollup's tests out in task 2.1 — Requirement 1's Event Echo Gate tests are comparable in scope (4 behavioral cases: routine-event suppression, step-mode-unaffected, structural-event-ends-range, breakpoint-ends-range) and would push the already-over-cap `test_repl_execution_control.c` (937 lines) well past 1300+ lines. They instead live in a new `src/scheduler/testsuite/test_repl_event_echo.c`, duplicating pre-existing fixture helpers per this file family's established convention, decided proactively before implementation rather than after a review round. +- `src/scheduler/testsuite/test_repl_execution_control.c` — the Requirement 5 no-regression checks (this file already drives `sched_sim_driver_run()` end-to-end and captures real process output via `child_continue_then_inspect`-style helpers); also gains the new prompt-content tests (Requirement 5.5) and `help`-listing structure tests (Requirement 7.1-7.4) — the natural home given it already exercises `readline()`/prompt-adjacent behavior and no `help` test exists anywhere today. +- `src/scheduler/testsuite/test_repl_round_rollup.c` (new, per the task 2.1 Correction above) — child-process output-capture tests for Requirements 2 and 3 (per-round decision summary, collapsing, the shared flush entry point, and its call from `xce_repl_on_replay_complete`). +- `src/scheduler/testsuite/test_repl_event_echo.c` (new, per the task 3.1 Correction above) — child-process output-capture tests for Requirement 1 (mode-aware event echo classification, ending an active collapsed range on structural events/breakpoint hits). - `src/scheduler/testsuite/test_repl_output_compression.c` (new) — focused unit tests for the jobid-range-compression helper (Requirement 4): a pure function, tested without the process harness. - `src/scheduler/testsuite/test_repl_resource_formatting.c` (new) — focused unit tests for the byte/megabyte classification and adaptive human-scaling helpers (Requirement 6.1, 6.2, 6.5): pure functions, tested without the process harness, mirroring `test_repl_output_compression.c`'s pattern. - `src/scheduler/testsuite/test_repl_breakpoints_inspection.c` — extended with `info worker`/`info resource` table-layout and units-mode-display tests (Requirement 6.6, alongside its existing `info worker`/`info resource` tests) and `break`'s enriched unrecognized-event-type error test (Requirement 7.5, alongside the existing `test_break_unrecognized_event_type_rejected`). diff --git a/.kiro/specs/scheduler-sim-repl-verbosity/tasks.md b/.kiro/specs/scheduler-sim-repl-verbosity/tasks.md index f5c17e6d..94a035d7 100644 --- a/.kiro/specs/scheduler-sim-repl-verbosity/tasks.md +++ b/.kiro/specs/scheduler-sim-repl-verbosity/tasks.md @@ -29,7 +29,7 @@ - _Boundary: Round Rollup_ - [ ] 3. (P) Core: mode-aware event echo, including ending an active collapsed range on structural events or breakpoint hits — Depends: 2.2 -- [ ] 3.1 (P) Write failing tests for suppressing routine event chatter and for ending an active collapsed range at the right moments +- [x] 3.1 (P) Write failing tests for suppressing routine event chatter and for ending an active collapsed range at the right moments - Cover: a routine per-tick event (a job status change, job progress update, job resource-usage update, queue update, or partial/delta resource-value update) produces no line under continuous execution; the same event still produces a line while single-stepping; a structural change (new jobs, workers, or resources arriving, a full resource-value snapshot arriving, or configuration pushed) still produces a line under continuous execution and ends any active collapsed range first; an event matching a breakpoint still interrupts continuous execution regardless of its classification, and ends any active collapsed range before the interruption is shown - Observable: the new test cases fail against today's unconditional per-event echo, which has no classification or range-ending behavior - _Requirements: 1.1, 1.2, 1.3, 1.4, 3.5, 3.6_ diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 4f7c864d..c78aa01d 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -934,6 +934,34 @@ sched_repl_round_rollup_test = executable( dependencies: [scheduler_test_deps, libedit_dep], install: false) +# Contract test for the interactive simulation REPL's Event Echo Gate +# component (scheduler-sim-repl-verbosity spec, task 3.1, Requirements 1 and +# 3.5/3.6 "mode-aware event echo"/"ending an active collapsed range"): a +# routine per-tick event (e.g. QUEUE_UPDATE) produces no line under +# continue-mode execution but still prints while single-stepping; a +# structural event (e.g. WORKERS_PUSH) still prints under continue and +# flushes any active Round Rollup collapsed range first; a breakpoint hit +# still interrupts continue-mode execution regardless of the matched +# event's classification, and likewise flushes any active collapsed range +# before the interruption is shown. Same white-box #include pattern as +# sched_repl_round_rollup_test above (core.c, core_clock.c, core_events.c, +# core_session.c, decision.c, plugin.c, plugins/builtin_fcfs.c, recording.c, +# recording_session.c, sim.c, sim_loop.c, sim_record.c, sim_verify.c, +# snapshot.c/snapshot_wire.c/snapshot_load.c, repl.c, repl_rollup.c, +# repl_break.c, repl_info.c, repl_params.c). Split into its own file rather +# than appended to test_repl_execution_control.c -- that file was already +# 937 lines (pre-existing, over this project's 800-line cap) before this +# feature began, and this task's own four behavioral cases would have +# pushed it well past 1300 lines, the same reasoning that already split +# test_repl_round_rollup.c out (task 2.1) -- see design.md's own +# "**Correction (post-review, task 3.1)**" note. +sched_repl_event_echo_test = executable( + 'sched_repl_event_echo_test', + ['test_repl_event_echo.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps, libedit_dep], + install: false) + # Contract test for the interactive simulation REPL's breakpoint and # state-inspection slice (scheduler-core spec, task 12.2, Req 5 AC 22 # "Breakpoints"/"State inspection"): setting a breakpoint on a specific @@ -1387,6 +1415,9 @@ test('scheduler repl execution control test', test('scheduler repl round rollup test', sched_repl_round_rollup_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) +test('scheduler repl event echo test', + sched_repl_event_echo_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0']) test('scheduler repl breakpoints inspection test', sched_repl_breakpoints_inspection_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_repl_event_echo.c b/src/scheduler/testsuite/test_repl_event_echo.c new file mode 100644 index 00000000..65fbfff3 --- /dev/null +++ b/src/scheduler/testsuite/test_repl_event_echo.c @@ -0,0 +1,704 @@ +/* + * test_repl_event_echo.c - contract test for the interactive simulation + * REPL's Event Echo Gate component (scheduler-sim-repl-verbosity spec, task + * 3.1, Requirements 1 and 3.5/3.6): mode-aware suppression of routine + * per-event chatter during `continue`, and ending an active Round Rollup + * collapsed range before a structural event's own line or a breakpoint + * interruption is shown. + * + * Split into its own file rather than appended to + * test_repl_execution_control.c -- see design.md's own + * "**Correction (post-review, task 3.1)**" note in its "New/Modified Test + * Files" section: that file was already 937 lines (over this project's + * 800-line cap) before this feature began, and landing this task's four + * behavioral cases there per the original plan would have pushed it well + * past 1300 lines -- the same reasoning that already split Round Rollup's + * own tests into test_repl_round_rollup.c (task 2.1). Following that same + * established convention, this file duplicates (rather than shares via a + * new header) whatever pre-existing static fixture helpers it needs from + * test_repl_execution_control.c/test_repl_round_rollup.c. + * + * Fixture strategy and white-box #include set: identical to + * test_repl_round_rollup.c/test_repl_execution_control.c (see those files' + * own header comments) -- this file exercises the same real + * sched_sim_driver_run() end-to-end, forked-child, real-stdout-capture + * technique. + * + * bug-1595 jobid-safety discipline: NONE of this file's fixtures push, + * mutate, or reference any job or jobid at all. Every event used below + * (QUEUE_UPDATE, WORKERS_PUSH) is deliberately job-free, so the specific + * "a filler event's jobid accidentally mutates the real job under test" + * failure mode bug-1595 logged has no surface to occur on here at all -- + * a stronger guarantee than merely using a distinct filler jobid. + * + * THIS TASK (3.1) IS RED-PHASE ONLY: the classification/range-ending + * behavior under test does not exist yet in xce_repl_on_event() (repl_break.c) + * today -- that unconditionally echoes every event before its breakpoint + * check (bug-1595's own fix), with no reference to sim->repl_state->mode and + * no call to repl_round_collapse_flush() anywhere in repl_break.c (task 3.2, + * a separate later task, adds both). Unlike test_repl_round_rollup.c's own + * RED-phase story (which referenced a not-yet-defined extern to force a + * link-time failure), every symbol this file references already exists + * today (repl_round_collapse_flush() was implemented by task 2.2) -- so + * every test below builds and links cleanly today but FAILS ITS OWN RUNTIME + * ASSERTIONS, because today's REPL echoes unconditionally in both modes and + * never calls repl_round_collapse_flush() from repl_break.c at all. Verified + * concretely (not assumed) against a real incremental build before this + * task was reported done -- see this task's own Status Report. + * + * Assumed output contract this file pins (matches design.md's Event Echo + * Gate component and test_repl_round_rollup.c's own "Assumed output + * contract" precedent): + * - Routine events (RESVALS_DELTA, JOB_STATE_CHANGE, JOB_UPDATE, + * QUEUE_UPDATE, JOB_RUSAGE) print their existing "[sim] event: + * ..." line (repl_print_event_summary(), repl_break.c) ONLY while + * sim->repl_state->mode == XCE_REPL_MODE_STEP; under + * XCE_REPL_MODE_CONTINUE that line must not appear at all. + * - Structural events (CONFIG_PUSH, JOBS_PUSH, WORKERS_PUSH, + * RESOURCES_PUSH, RESVALS_FULL) print that line in EVERY mode, and -- + * before printing it -- the Event Echo Gate calls + * repl_round_collapse_flush() so any active Round Rollup collapsed + * range is closed and printed first (Requirement 3.5). + * - A breakpoint match (checked on every event, any classification) still + * interrupts continue-mode execution and prints "[sim] hit breakpoint + * ..." (repl_break.c's existing format), and -- before that line -- + * also calls repl_round_collapse_flush() first (Requirement 3.6). + * - Tests use strstr()/pointer-order comparisons on captured real stdout, + * the same technique test_repl_round_rollup.c's own tests establish. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "controller.capnp.h" +#include "kvmgr.h" +#include "scheduler.h" +#include "xstring.h" + +/* Pull in the real functions under test into this TU (white-box #include, + * same convention as test_repl_round_rollup.c/test_repl_execution_control.c/ + * test_repl_breakpoints_inspection.c). */ +#include "../src/core.c" +#include "../src/core_clock.c" +#include "../src/core_events.c" +#include "../src/core_session.c" +#include "../src/decision.c" +#include "../src/plugin.c" +#include "../src/plugins/builtin_fcfs.c" +#include "../src/recording.c" +#include "../src/recording_session.c" +#include "../src/repl.c" +#include "../src/repl_rollup.c" +#include "../src/repl_break.c" +#include "../src/repl_info.c" +#include "../src/repl_params.c" +#include "../src/sim.c" +#include "../src/sim_loop.c" +#include "../src/sim_picker.c" /* bug-1567: sched_sim_driver_run() now calls sim_bootstrap_picker() (sim_loop.c-cap sibling) as an extern -- undefined reference at link time otherwise */ +#include "../src/sim_record.c" +#include "../src/sim_verify.c" +#include "../src/snapshot.c" +#include "../src/snapshot_load.c" +#include "../src/snapshot_wire.c" + +void kill_sched_job(void* arg) { (void)arg; } +void kill_sched_queue(void* arg) { (void)arg; } +void kill_sched_worker(void* arg) { (void)arg; } +void kill_sched_user(void* arg) { (void)arg; } +void kill_sched_project(void* arg) { (void)arg; } +void kill_sched_resval(xce_val_t* val) { (void)val; } + +/* resource-accounting.md's reconciliation (resource.c, not linked here -- + * this file only #includes core_events.c). core_events.c's + * XCE_CORE_EVENT_JOB_RUSAGE handler calls this whenever job->accounts != + * NULL; link-satisfying stub only, same rationale as every other stub in + * this block (see test_job_update.c's identical stub). */ +int sched_reconcile_job_resource_usage(xce_sched_core_t* c, + xce_job_account_t* a, + xce_job_rusages_t* r) +{ + (void)c; (void)a; (void)r; return 0; +} + +void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; } + +int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, + xlist_t* limits) +{ + (void)sched; + (void)vec; + (void)limits; + return 0; +} + +/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task + * 2.1) new round-boundary call to the relocated sched_account_jobs() + * (core.h) -- a no-op stand-in here since this file does not link + * scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy + * documents. */ +int sched_account_jobs(xce_sched_core_t* core) +{ + (void)core; + return 0; +} + +/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same + * copy test_sim_alt_events.c/test_sim_verify.c/test_sim_event_loop.c/ + * test_repl_execution_control.c/test_repl_round_rollup.c each carry + * (schedobj.c is not linked here: it would need plugin_register(), which + * requires a genuine xce_scheduler_t* neither this file's fixtures nor + * sim_loop.c's own core-scoped registration path have, Req 5 AC 17). */ +int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out) +{ + xce_sched_plugin_t* plugin = NULL; + int rc = 0; + + if ((core == NULL) || (out == NULL)) { + return -EINVAL; + } + + *out = NULL; + + if (core->job_pick_plugin == NULL) { + return -EINVAL; + } + + lock(core->plugins, RTTI_LOCK_READ); + rc = key_find(core->plugins, core->job_pick_plugin, (void**)&plugin); + unlock(core->plugins); + + if (rc <= 0) { + return (rc < 0) ? rc : -ENOENT; + } + + if ((plugin == NULL) || (plugin->picker_iface == NULL) + || (plugin->picker_iface->next_job == NULL) + || (plugin->picker_iface->begin_session == NULL) + || (plugin->picker_iface->end_session == NULL) + || (plugin->picker_iface->on_dispatch == NULL) + || (plugin->picker_iface->on_pend == NULL)) { + return -EINVAL; + } + + *out = plugin; + + return 0; +} + +void setUp(void) { return; } +void tearDown(void) { return; } + +/* ---------------------------------------------------------------------- */ +/* Fixture helpers -- duplicated from test_repl_round_rollup.c/ */ +/* test_repl_execution_control.c (same techniques test_sim_alt_events.c/ */ +/* test_sim_verify.c already establish), per this file family's established*/ +/* split convention (see header comment above): copied, not shared via a */ +/* new header. */ +/* ---------------------------------------------------------------------- */ + +static void make_tmp_dir(char* out, size_t out_len, const char* suffix) +{ + char tmpl[PATH_MAX]; + + snprintf(tmpl, sizeof(tmpl), "/tmp/sched_replexec%s_XXXXXX", suffix); + TEST_ASSERT_NOT_NULL(mkdtemp(tmpl)); + TEST_ASSERT_TRUE(strlen(tmpl) < out_len); + strcpy(out, tmpl); +} + +static void 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* 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; + + make_tmp_dir(record_dir, sizeof(record_dir), suffix); + 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 append_queue_update_event(FILE* f, uint64_t ts, + const char* queue_name) +{ + 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 = 1; + + 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); +} + +/* append_workers_push_event - a structural event (WORKERS_PUSH, per + * design.md's Event Echo Gate classification list), encoded the same + * minimal-zeroed-wire-struct way append_jobs_push_event() + * (test_repl_round_rollup.c) builds a JOBS_PUSH: only the fields + * core_upsert_worker()/core_build_worker() (core_events.c) actually read + * (w->name, w->status, w->nresvals_to_sched) are set; everything else stays + * zeroed/NULL, which every encode_*_ptr() step along this path (encode_ + * Worker(), encode_HostAttrs_ptr(), encode_TimeWindow_ptr(), encode_ + * Threshold_list(), encode_NameValue_list() -- controller.capnp.c) already + * tolerates by design (same null-safety this file family's job/queue + * fixtures already rely on). Deliberately job-free -- see this file's own + * header comment on bug-1595 jobid safety. */ +static void append_workers_push_event(FILE* f, uint64_t ts, + const char* worker_name) +{ + xce_worker_t worker; + xce_worker_t* workers[1]; + xce_workers_data_t workers_data; + replay_event_t src; + struct capn c; + struct capn_segment* cs; + ReplayEvent_ptr p; + + memset(&worker, 0, sizeof(worker)); + worker.name = (char*)worker_name; + + workers[0] = &worker; + + memset(&workers_data, 0, sizeof(workers_data)); + workers_data.n_total = 1; + workers_data.count = 1; + workers_data.workers = workers; + + memset(&src, 0, sizeof(src)); + src.ts = ts; + src.body_which = ReplayEvent_body_workersPush; + src.body.workers_push = &workers_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 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 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 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 char* read_whole_file(const char* path) +{ + FILE* f = fopen(path, "rb"); + long len; + char* buf; + + TEST_ASSERT_NOT_NULL(f); + TEST_ASSERT_EQUAL_INT(0, fseek(f, 0, SEEK_END)); + len = ftell(f); + TEST_ASSERT_TRUE(len >= 0); + rewind(f); + + buf = (char*)malloc((size_t)len + 1); + TEST_ASSERT_NOT_NULL(buf); + if (len > 0) { + TEST_ASSERT_EQUAL_INT((int)len, (int)fread(buf, 1, (size_t)len, f)); + } + buf[len] = '\0'; + fclose(f); + + return buf; +} + +/* build_round_marker - the bounds+count PREFIX of the Round Rollup's own + * pinned collapsed-range format (test_repl_round_rollup.c's own header + * comment documents the full format this file only needs the prefix of), + * e.g. build_round_marker(buf, sz, 1, 2) -> "round 1-2 (2 round(s)". Used + * as a strstr() needle to locate a flushed collapsed range in captured + * output and compare its position against another line's. */ +static void build_round_marker(char* out, size_t outsz, uint64_t start_round, + uint64_t end_round) +{ + snprintf(out, outsz, "round %llu-%llu (%llu round(s)", + (unsigned long long)start_round, (unsigned long long)end_round, + (unsigned long long)(end_round - start_round + 1)); +} + +/* child_run_repl_and_exit / run_repl_fixture_and_capture - the same fork/ + * freopen/waitpid/capture shape test_repl_round_rollup.c's own pair + * establishes (itself following child_continue_then_inspect()/Test 2 in + * test_repl_execution_control.c): run a scripted command sequence to + * completion against a real sched_sim_driver_run(), capturing real stdout. */ +static void child_run_repl_and_exit(const char* replay_path, + const char* script_path, + const char* capture_path) +{ + xce_sched_sim_driver_t sim; + sched_sim_args_t args; + + if (freopen(script_path, "r", stdin) == NULL) { + _exit(120); + } + if (freopen(capture_path, "w", stdout) == NULL) { + _exit(121); + } + + memset(&sim, 0, sizeof(sim)); + memset(&args, 0, sizeof(args)); + args.replay_path = replay_path; + args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + args.sim_speed = 0; + args.interactive = true; + + (void)sched_sim_driver_run(&sim, &args); + + fflush(stdout); + _exit(111); /* unreachable -- the script's `quit` must already have + * exited */ +} + +static char* run_repl_fixture_and_capture(const char* replay_path, + const char* script, + const char* aux_suffix) +{ + char aux_dir[PATH_MAX]; + char script_path[PATH_MAX]; + char capture_path[PATH_MAX]; + pid_t pid; + int status; + + make_tmp_dir(aux_dir, sizeof(aux_dir), aux_suffix); + snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir); + snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir); + write_text_file(script_path, script); + + pid = fork(); + TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed"); + + if (pid == 0) { + child_run_repl_and_exit(replay_path, script_path, capture_path); + _exit(112); /* unreachable -- child_run_repl_and_exit never returns */ + } + + 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)"); + TEST_ASSERT_EQUAL_INT(0, WEXITSTATUS(status)); + + return read_whole_file(capture_path); +} + +/* ---------------------------------------------------------------------- */ +/* Test 1: a routine per-tick event (QUEUE_UPDATE) produces NO individual */ +/* echo line while the REPL is executing in continue mode (Req 1.1). */ +/* ---------------------------------------------------------------------- */ + +static void test_routine_event_produces_no_line_under_continue_mode(void) +{ + char replay_path[PATH_MAX]; + FILE* f; + char* captured; + + f = write_header_and_snapshot(replay_path, sizeof(replay_path), + "_eechoroutine"); + TEST_ASSERT_NOT_NULL(f); + + append_queue_update_event(f, 1000ULL, "normal"); /* routine event */ + append_replay_session(f, 2000000000ULL); /* round 1: idle */ + append_end_of_stream_marker(f); + fclose(f); + + captured = run_repl_fixture_and_capture(replay_path, "continue\nquit\n", + "_eechoroutineaux"); + + TEST_ASSERT_NULL_MESSAGE( + strstr(captured, "[sim] event: QUEUE_UPDATE"), + "a routine per-tick event (QUEUE_UPDATE) must produce NO individual " + "line while the REPL is executing in continue mode (Requirement " + "1.1)"); + + free(captured); + + printf( + "test_routine_event_produces_no_line_under_continue_mode: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 2: the SAME routine event type still produces a line while */ +/* single-stepping -- step mode's per-event detail is unaffected by */ +/* continue-mode's suppression (Req 1.2; doubles as a regression check). */ +/* ---------------------------------------------------------------------- */ + +static void test_routine_event_still_prints_in_step_mode(void) +{ + char replay_path[PATH_MAX]; + FILE* f; + char* captured; + + f = write_header_and_snapshot(replay_path, sizeof(replay_path), + "_eechostep"); + TEST_ASSERT_NOT_NULL(f); + + append_queue_update_event(f, 1000ULL, "normal"); /* same routine event */ + append_replay_session(f, 2000000000ULL); /* round 1 */ + append_end_of_stream_marker(f); + fclose(f); + + captured = run_repl_fixture_and_capture(replay_path, "step\nquit\n", + "_eechostepaux"); + + TEST_ASSERT_NOT_NULL_MESSAGE( + strstr(captured, "[sim] event: QUEUE_UPDATE"), + "the SAME routine event type must still produce its individual " + "echo line while single-stepping -- step mode's per-event detail " + "is unaffected by continue-mode's suppression (Requirement 1.2)"); + + free(captured); + + printf("test_routine_event_still_prints_in_step_mode: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 3: a structural change (WORKERS_PUSH) still prints a line under */ +/* continue-mode execution (Req 1.3), and -- critically -- flushes any */ +/* active Round Rollup collapsed range FIRST, so the range's summary */ +/* appears in captured output BEFORE the structural event's own line */ +/* (Req 3.5). */ +/* ---------------------------------------------------------------------- */ + +static void +test_structural_event_prints_and_flushes_active_collapsed_range(void) +{ + char replay_path[PATH_MAX]; + FILE* f; + char* captured; + char idle_marker[64]; + char* idle_pos; + char* structural_pos; + + f = write_header_and_snapshot(replay_path, sizeof(replay_path), + "_eechostruct"); + TEST_ASSERT_NOT_NULL(f); + + append_queue_update_event(f, 1000ULL, "normal"); + append_replay_session(f, 2000000000ULL); /* round 1: idle */ + append_replay_session(f, 3000000000ULL); /* round 2: identical (idle) + * outcome -- silently extends + * the active range to rounds + * 1-2 (not yet printed) */ + /* Structural event (WORKERS_PUSH), decoded mid-round-3 -- BEFORE round + * 3's own ReplaySession boundary is even reached. Must flush the + * still-active rounds-1-2 idle range and print it BEFORE this event's + * own echoed line (Requirement 3.5). Deliberately job-free (see this + * file's own header comment on bug-1595 jobid safety). */ + append_workers_push_event(f, 3500000000ULL, "workerA"); + append_replay_session(f, 4000000000ULL); /* round 3: idle again -- a + * fresh active range of its + * own, alone at replay + * completion; per design.md's + * Round Rollup, a lone idle + * round is never printed, so + * this produces no further + * output to confound the + * assertions below */ + append_end_of_stream_marker(f); + fclose(f); + + captured = run_repl_fixture_and_capture(replay_path, "continue\nquit\n", + "_eechostructaux"); + + build_round_marker(idle_marker, sizeof(idle_marker), 1, 2); + idle_pos = strstr(captured, idle_marker); + structural_pos = strstr(captured, "[sim] event: WORKERS_PUSH"); + + TEST_ASSERT_NOT_NULL_MESSAGE( + structural_pos, + "a structural change (WORKERS_PUSH) must still print a line " + "regardless of execution mode, including continue (Requirement " + "1.3)"); + TEST_ASSERT_NOT_NULL_MESSAGE( + idle_pos, + "the active collapsed range (rounds 1-2, idle) must be flushed and " + "reported once the structural event ends it (Requirement 3.5)"); + TEST_ASSERT_TRUE_MESSAGE( + idle_pos < structural_pos, + "the flushed collapsed range must be printed BEFORE the " + "structural event's own line -- a stale collapsed range and a " + "structural event must never interleave out of order (Requirement " + "3.5)"); + + free(captured); + + printf( + "test_structural_event_prints_and_flushes_active_collapsed_range: " + "PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 4: an event matching a breakpoint still interrupts continue-mode */ +/* execution regardless of its routine/structural classification (Req */ +/* 1.4), and ends any active collapsed range BEFORE the interruption is */ +/* shown to the operator (Req 3.6). */ +/* ---------------------------------------------------------------------- */ + +static void +test_breakpoint_hit_ends_active_collapsed_range_before_interruption(void) +{ + char replay_path[PATH_MAX]; + FILE* f; + char* captured; + char idle_marker[64]; + char* idle_pos; + char* breakpoint_pos; + + f = write_header_and_snapshot(replay_path, sizeof(replay_path), + "_eechobp"); + TEST_ASSERT_NOT_NULL(f); + + /* round 1 setup event -- queue "normal" never matches the breakpoint's + * queue=special filter below, so it never itself triggers an + * interruption. */ + append_queue_update_event(f, 1000ULL, "normal"); + append_replay_session(f, 2000000000ULL); /* round 1: idle */ + append_replay_session(f, 3000000000ULL); /* round 2: identical (idle) + * outcome -- silently extends + * the active range to rounds + * 1-2 (not yet printed) */ + /* This SECOND queue_update, decoded mid-round-3, is the one the + * breakpoint below actually matches (queue=special). QUEUE_UPDATE is a + * ROUTINE event type (design.md's Event Echo Gate classification) -- + * using it here directly proves Requirement 1.4's "regardless of + * whether the event is routine or structural". */ + append_queue_update_event(f, 3500000000ULL, "special"); + append_replay_session(f, 4000000000ULL); /* never reached -- the + * breakpoint above interrupts + * execution first */ + append_end_of_stream_marker(f); + fclose(f); + + captured = run_repl_fixture_and_capture( + replay_path, "break QUEUE_UPDATE queue=special\ncontinue\nquit\n", + "_eechobpaux"); + + build_round_marker(idle_marker, sizeof(idle_marker), 1, 2); + idle_pos = strstr(captured, idle_marker); + breakpoint_pos = strstr(captured, "[sim] hit breakpoint"); + + TEST_ASSERT_NOT_NULL_MESSAGE( + breakpoint_pos, + "an event matching a breakpoint must still interrupt continue-mode " + "execution regardless of its routine/structural classification " + "(Requirement 1.4)"); + TEST_ASSERT_NOT_NULL_MESSAGE( + idle_pos, + "the active collapsed range (rounds 1-2, idle) must be flushed and " + "reported before the breakpoint interruption is shown to the " + "operator (Requirement 3.6)"); + TEST_ASSERT_TRUE_MESSAGE( + idle_pos < breakpoint_pos, + "the flushed collapsed range must be printed BEFORE the " + "breakpoint-hit line -- the operator must see the collapsed " + "range's final state before the interactive prompt returns " + "(Requirement 3.6)"); + + free(captured); + + printf( + "test_breakpoint_hit_ends_active_collapsed_range_before_" + "interruption: PASS\n"); +} + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_routine_event_produces_no_line_under_continue_mode); + RUN_TEST(test_routine_event_still_prints_in_step_mode); + RUN_TEST( + test_structural_event_prints_and_flushes_active_collapsed_range); + RUN_TEST( + test_breakpoint_hit_ends_active_collapsed_range_before_interruption); + + return UNITY_END(); +}