diff --git a/.kiro/specs/scheduler-core/tasks.md b/.kiro/specs/scheduler-core/tasks.md index 3d5bd78d..37ffcd4e 100644 --- a/.kiro/specs/scheduler-core/tasks.md +++ b/.kiro/specs/scheduler-core/tasks.md @@ -362,11 +362,12 @@ - _Depends: 11.2_ - _Verified: RED-phase reproduction performed during review round 2 — temporarily changed `repl_should_pause()` (repl.c) from `return (rs->step_budget <= 0);` to `return false;`, rebuilt, ran `sched_repl_execution_control_test`: 2/4 tests failed exactly as expected (`test_should_pause_fires_on_exactly_the_nth_session`, `test_step_1_processes_exactly_one_session_then_quit_exits`), the 2 unrelated tests (`test_should_pause_never_fires_in_continue_mode`, `test_pre_replay_pause_blocks_before_any_event_is_processed`) stayed green. Reverted; rebuilt; full scheduler suite 34/34 GREEN._ -- [ ] 12.2 Implement REPL breakpoints and state inspection +- [x] 12.2 Implement REPL breakpoints and state inspection - Support setting/listing/deleting breakpoints on event type with optional field filters, and inspection commands reporting current simulation position, and job/queue/worker/resource/summary state - Observable: setting a breakpoint on a specific event type and field value pauses the REPL exactly when a matching event is about to be processed; each inspection command reports state matching what a snapshot dump of the same moment would show - _Requirements: 5.22_ - _Depends: 12.1_ + - _Verified: 2026-08-07 — repl.c (task 12.1, 370 lines) split three ways under this project's 800-line cap (task 12.2's own feature surface did not fit in one file): repl.c (416 lines, execution control + dispatch, unchanged in scope), repl_break.c (breakpoint kind/filter matching, breakpoint CRUD, and the three new driver-facing hooks xce_repl_on_event()/xce_repl_on_begin_round()/xce_repl_on_decision()), repl_info.c (`info job`/`queue`/`worker`/`resource`/`summary`/`session`/`break` dumps) -- all sharing private state via a new repl_internal.h (the full xce_sched_repl_state_s definition, moved out of repl.c). sim_loop.c gained exactly 3 new bare call expressions (xce_repl_check_event()/xce_repl_check_begin_round()/xce_repl_check_decision(), repl.h inline wrappers gated on sim->interactive, same pattern as task 12.1's two existing hooks) at the three points design.md's pseudocode requires: per decoded ReplayEvent before it joins the accumulated batch, per ReplaySession boundary before the batch is pushed, and per drained decision after sim_print_decision() -- landed sim_loop.c at exactly 800/800 lines (trimmed its own header comment to make room). repl_should_pause()'s stale doc comment (which wrongly predicted this task would extend it) was corrected: breakpoints are a separate mechanism, never folded into the `step N` pause path. test_sim_isolation.c (task 11.3, already extended by 12.1) extended again to scan repl_break.c/repl_info.c/repl_internal.h (8-file precedent). New test_repl_breakpoints_inspection.c (17 tests, 4 groups: command parsing/listing, pure matcher predicates, state-inspection dumps against a real xce_sched_core_t, full-integration scripted-stdin pause proof for both an event breakpoint and a decision breakpoint). RED-phase: two REAL bugs surfaced and fixed during development (bug-1506: test fixtures calling repl_dispatch()'s `break` command without first calling xce_sched_core_init() left makeString()'s backing xstr_table uninitialized, silently returning NULL breakpoint fields instead of failing loud; bug-1507: repl_breakpoint_free() called a bare free() on a makeString()-produced xstring -- RTTI reference-counted, not a plain malloc() chunk -- causing a heap-corruption abort, fixed by never freeing those fields at all, matching kill_sched_job()/kill_sched_queue()'s existing "makeString()'d fields live for the process's lifetime" precedent). PLUS two targeted synthetic RED-phase probes reproduced independently after both real bugs were fixed: (1) repl_find_event_breakpoint() hardcoded to always return NULL -- rebuilt, ran the suite, exactly 3 of 17 tests failed (test_event_breakpoint_matches_jobid_filter, test_event_breakpoint_matches_state_filter, test_event_breakpoint_pauses_repl_at_matching_event), all others (including decision/begin_round matching and every info dump) stayed green; (2) repl_job_state_name() hardcoded to return a fixed bogus string -- exactly 1 of 17 failed (test_info_job_reports_state_queue_priority). Both probes reverted (grep-confirmed zero residue), rebuilt, reconfirmed green. Final: 35/35 scheduler suite (was 34/34), whole-project build clean. Remediation (2026-08-07): review found `repl_info_job()`'s resreq= field reported only a `set`/`(none)` presence flag instead of real content, despite `format_resreq()` (resreq.h) already existing and already used one file away by `build_wire_job()` (snapshot_wire.c) for this exact purpose; fixed by calling `format_resreq()` in `repl_info_job()` (mirroring `build_wire_job()`'s WARN-and-fallback error handling, freed after printing since this dump is transient), corrected the function's stale doc-comment claim that no print-form existed, and extended `test_info_job_reports_state_queue_priority` to assert the real formatted text (`"Primary:"`/`"Tasks: min = 0, max = 0"`/`"Rusages:"`) appears instead of the old boolean flag. Still 35/35 scheduler suite. - [ ] 12.3 Implement REPL snapshot capture and simulation parameter commands - Support writing a full snapshot to a file at the current simulation position, and adjusting speed/output-file/verify-mode from the prompt without restarting diff --git a/.wolf/buglog.json b/.wolf/buglog.json index 37619ed3..50a1ee16 100644 --- a/.wolf/buglog.json +++ b/.wolf/buglog.json @@ -14840,5 +14840,48 @@ ], "occurrences": 1, "last_seen": "2026-08-07T08:10:00Z" + }, + { + "id": "bug-1506", + "timestamp": "2026-08-07T09:05:00Z", + "error_message": "sched_repl_breakpoints_inspection_test: repl_dispatch(sim, \"break JOB_STATE_CHANGE jobid=12345\") stored NULL for the breakpoint's token/filter key/value -- TEST_ASSERT_EQUAL_STRING(\"jobid\", ...) failed with 'Expected jobid Was NULL'.", + "file": "src/scheduler/testsuite/test_repl_breakpoints_inspection.c", + "root_cause": "makeString() (src/lib/xstring.c) is an interned-xstring allocator backed by a process-global xstr_table that must be initialized via xstring_table_init() before first use -- xce_sched_core_init() (core.c) is the only call site in the scheduler tree and does this transitively. New task 12.2 unit tests built a bare xce_sched_sim_driver_t via memset(0) and called repl_state_ensure()/repl_dispatch() directly for pure command-dispatch tests, without ever calling xce_sched_core_init() first (the real driver flow always does, via sched_sim_driver_start() before xce_repl_start() runs, so this precondition is always satisfied in production -- only the test fixtures skipped it). With xstr_table uninitialized (NULL), makeString()'s internal key_find()/key_add() against a NULL kvmgr container fail gracefully and return NULL rather than crash, so the bug surfaced as silently-NULL breakpoint fields, not an immediate crash.", + "fix": "Added xce_sched_core_init(&(sim.core)) as the first statement (right after memset) in every Group A/B test in test_repl_breakpoints_inspection.c that calls repl_dispatch()'s break/delete commands or the repl_break.c matcher functions -- matching the real driver's own precondition ordering.", + "tags": [ + "scheduler-core", + "task-12.2", + "repl", + "xstring", + "test-fixture", + "makeString" + ], + "related_bugs": [ + "bug-1507" + ], + "occurrences": 1, + "last_seen": "2026-08-07T09:05:00Z" + }, + { + "id": "bug-1507", + "timestamp": "2026-08-07T09:10:00Z", + "error_message": "sched_repl_breakpoints_inspection_test: 'free(): invalid pointer' SIGABRT inside repl_breakpoint_free(), reached from repl_cmd_break()'s malformed-filter rejection path (\"break JOB_PUSH nofiltermark\"). gdb backtrace: malloc_printerr <- repl_breakpoint_free (repl_break.c:375) <- repl_cmd_break <- repl_dispatch <- test_break_malformed_filter_rejected.", + "file": "src/scheduler/src/repl_break.c", + "root_cause": "repl_breakpoint_free() called FREEUP() (a bare free()) on bp->token and each filter's key/value, all of which are makeString()-produced xstrings -- RTTI reference-counted objects (src/lib/xstring.c: xstring_type has custom .reference/.dereference interface functions; the returned pointer is the object's DATA region inside an RTTI-managed allocation, not a plain malloc() chunk). Releasing one requires going through the RTTI dereference path (xstring_dereference(), which only fires the real free when the refcount hits zero via the owning kvtab's key_del), never a bare free(). This codebase's own kill_sched_job()/kill_sched_queue() (schedobj.c) establish the identical precedent for every other makeString()'d field in the scheduler tree (xce_sched_job_t.submit fields, xce_sched_queue_t.name, etc.): none of them are ever explicitly released either -- they live for the process's lifetime, matching this whole test suite's 'detect_leaks=0 by design' convention. My new repl_break.c code was the first place in this file family to call free() directly on a makeString() result.", + "fix": "Removed the FREEUP() calls on bp->token/filters[i].key/filters[i].value from repl_breakpoint_free() (repl_break.c) -- it now only frees the plain calloc'd bp->filters array and bp itself, matching kill_sched_job()/kill_sched_queue()'s established never-release-xstrings precedent. Updated repl_internal.h's xce_repl_filter_t doc comment to state this explicitly so a future maintainer doesn't reintroduce the same free() on an RTTI-managed pointer.", + "tags": [ + "scheduler-core", + "task-12.2", + "repl", + "xstring", + "heap-corruption", + "free", + "rtti" + ], + "related_bugs": [ + "bug-1506" + ], + "occurrences": 1, + "last_seen": "2026-08-07T09:10:00Z" } ] diff --git a/src/scheduler/include/repl.h b/src/scheduler/include/repl.h index 736236c2..556cda38 100644 --- a/src/scheduler/include/repl.h +++ b/src/scheduler/include/repl.h @@ -5,36 +5,39 @@ #include "sim.h" /* - * repl.h - interactive simulation REPL, execution-control slice (scheduler- - * core spec, task 12.1, Req 5 AC 22). New file per design.md's File - * Structure Plan ("repl.c -- NEW (Phase 3): interactive REPL command loop - * (Req 5 AC 22)"), Phase 3, sibling to sim.h/sim.c/sim_loop.c. + * repl.h - interactive simulation REPL (scheduler-core spec, Req 5 AC 22). + * New file per design.md's File Structure Plan ("repl.c -- NEW (Phase 3): + * interactive REPL command loop (Req 5 AC 22)"), Phase 3, sibling to + * sim.h/sim.c/sim_loop.c. Task 12.1 built execution control (step/continue/ + * quit, pre-replay pause); task 12.2 (this revision) adds breakpoints + * (event/BEGIN_ROUND/DECISION, with key=value filters) and the `info` + * state-inspection command family. * - * This header declares repl.c's two driver-facing entry points, plus two - * `static inline` wrappers that fold the `sim->interactive` gate INTO the - * header itself. That inline-wrapper indirection is a deliberate, judgment- - * based deviation from this file family's usual "extern declared where - * consumed, no dedicated header" convention (sim_verify.c/sim_record.c/ + * This header declares repl.c's driver-facing entry points, plus `static + * inline` wrappers that fold the `sim->interactive` gate INTO the header + * itself. That inline-wrapper indirection is a deliberate, judgment-based + * deviation from this file family's usual "extern declared where consumed, + * no dedicated header" convention (sim_verify.c/sim_record.c/ * recording_session.c's entry points are all extern-declared directly - * inside sim_loop.c -- see that file's own header comment): sim_loop.c was - * already at 796/800 lines (this project's per-file cap, CLAUDE.md) before - * this task, leaving no room for even one bare `if (sim->interactive) { ... - * }` block at each of the two call sites design.md's REPL integration - * pseudocode requires (pre-replay pause, post-session pause), let alone two. - * Moving the gate into these inline wrappers keeps sim_loop.c's own diff to - * two single-line, brace-free call expressions -- this task's own text - * anticipates exactly this fallback ("move the hook logic into a tiny - * static inline helper in repl.h, or extract something unrelated out of - * sim_loop.c"). The two real functions (`xce_repl_start`/ - * `xce_repl_post_session_check`) still carry the actual logic; a `static - * inline` here costs nothing at link time (each including TU gets its own - * copy, trivially small, no ODR risk -- sim_loop.c is the only production + * inside sim_loop.c -- see that file's own header comment): sim_loop.c sits + * right at its 800-line cap (CLAUDE.md), leaving no room for even one bare + * `if (sim->interactive) { ... }` block at any of the five call sites + * design.md's REPL integration pseudocode requires (pre-replay pause, + * post-session pause, per-event/per-BEGIN_ROUND/per-decision breakpoint + * checks). Moving the gate into these inline wrappers keeps sim_loop.c's + * own diff to five single-line, brace-free call expressions. The real + * functions below still carry the actual logic; a `static inline` here + * costs nothing at link time (each including TU gets its own copy, + * trivially small, no ODR risk -- sim_loop.c is the only production * consumer). * * Boundary (Req 5 AC 17, "Simulation isolation", same as sim.h): this header * only depends on sim.h (itself core.h + stdbool/stdint/stdio) -- no - * scheduler.h, no client.h, no capnp types. repl.c's own file header - * comment documents the same boundary for its implementation. + * scheduler.h, no client.h, no capnp types. xce_core_event_t (core.h) and + * xce_decision_t (decision.h, included transitively via core.h) are both + * already capnp-free, so the three new task 12.2 hooks below add no new + * dependency. repl.c's own file header comment documents the same boundary + * for its implementation. */ /* @@ -51,20 +54,47 @@ extern void xce_repl_start(xce_sched_sim_driver_t* sim); /* - * xce_repl_post_session_check - the per-session pause-decision point (Req 5 - * AC 22's interaction model: "blocks on stdin ... at each breakpoint hit"; - * task 12.1's own narrower scope: only a `step N` budget reaching zero, no - * breakpoints yet). Called once per completed ReplaySession, immediately - * after xce_core_end_session() (sim_loop.c), when sim->interactive is set. - * Internally decides whether to pause (a `step` budget hit zero; `continue` - * mode never pauses here in 12.1's scope) and, if so, re-enters the same - * blocking command loop xce_repl_start() uses. Task 12.2's own seam: it - * extends the INTERNAL pause decision (repl.c) to also check a breakpoint - * list -- this function's signature and sim_loop.c's own call site are not - * expected to change. + * xce_repl_post_session_check - the `step N` pause-decision point (Req 5 AC + * 22's interaction model: a `step` budget reaching zero). Called once per + * completed ReplaySession, immediately after xce_core_end_session() + * (sim_loop.c), when sim->interactive is set. This is a SEPARATE pause path + * from the breakpoint checks below -- it never inspects the breakpoint + * list (that would be too coarse: a breakpoint must interrupt mid-batch, at + * the exact matching event/decision, not merely once per completed + * session). See xce_repl_check_begin_round()'s own doc comment for why + * task 12.2 gave breakpoints their own, separately-triggered hooks instead + * of folding them into this function. */ extern void xce_repl_post_session_check(xce_sched_sim_driver_t* sim); +/* + * xce_repl_on_event / xce_repl_on_begin_round / xce_repl_on_decision (task + * 12.2, Req 5 AC 22 "Breakpoints" + "REPL integration with the simulation + * loop" pseudocode) - the three breakpoint-check hooks, one per granularity + * the pseudocode's `check_breakpoints(...)` calls require: per decoded + * ReplayEvent (before it is applied to core state), per ReplaySession + * boundary (BEGIN_ROUND, before the accumulated event batch is pushed), and + * per drained decision (DECISION, after verify/output-write, matching + * sim_loop.c's existing per-decision loop body). Each also does lightweight + * position bookkeeping (event_index/session_count/last-session decision + * tally) that `info session` reports -- folded in here rather than given + * separate hook call sites, since these are the only three places in + * sim_loop.c's loop where "one event/session/decision was just processed" + * is already known, and sim_loop.c has no line budget for a sixth call + * site. Implemented in repl.c; each checks sim->repl_state's breakpoint + * list for a match and, if found, prints a "[sim] hit breakpoint ..." line + * and re-enters the same blocking command loop xce_repl_start() uses. A + * NULL sim/ev/d, or a not-yet-allocated repl_state, is a safe no-op (same + * "never load-bearing, degrade to unattended" discipline + * repl_state_ensure()'s own doc comment establishes). + */ +extern void xce_repl_on_event(xce_sched_sim_driver_t* sim, + xce_core_event_t* ev); +extern void xce_repl_on_begin_round(xce_sched_sim_driver_t* sim, + uint64_t begin_round); +extern void xce_repl_on_decision(xce_sched_sim_driver_t* sim, + xce_decision_t* d); + static inline void xce_repl_pre_replay_pause(xce_sched_sim_driver_t* sim) { if (sim->interactive) { @@ -79,4 +109,28 @@ static inline void xce_repl_post_session_pause(xce_sched_sim_driver_t* sim) } } +static inline void xce_repl_check_event(xce_sched_sim_driver_t* sim, + xce_core_event_t* ev) +{ + if (sim->interactive) { + xce_repl_on_event(sim, ev); + } +} + +static inline void xce_repl_check_begin_round(xce_sched_sim_driver_t* sim, + uint64_t begin_round) +{ + if (sim->interactive) { + xce_repl_on_begin_round(sim, begin_round); + } +} + +static inline void xce_repl_check_decision(xce_sched_sim_driver_t* sim, + xce_decision_t* d) +{ + if (sim->interactive) { + xce_repl_on_decision(sim, d); + } +} + #endif diff --git a/src/scheduler/meson.build b/src/scheduler/meson.build index dd30e6c2..7aa94c95 100644 --- a/src/scheduler/meson.build +++ b/src/scheduler/meson.build @@ -19,6 +19,8 @@ scheduler_srcs = [ 'src/sim_verify.c', 'src/sim_record.c', 'src/repl.c', + 'src/repl_break.c', + 'src/repl_info.c', 'src/schedobj.c', 'src/plugin.c', 'src/resource.c', @@ -78,18 +80,23 @@ sched_core_isolation_lib = static_library( # Task 11.3 (scheduler-core spec, Req 5 AC 17): isolated compile target for # the simulation driver's own files, consumed by # testsuite/test_sim_isolation.c to prove at build time that -# sim.c/sim_loop.c/sim_record.c/repl.c reference zero controller-facing -# symbols (client/netio/xceapi/delay/idgen). All files are compiled together -# into ONE archive -- task 11.2 added sim_loop.c as a sibling file to task -# 11.1's sim.c, task 11.5 added sim_record.c as a third, and task 12.1 adds -# repl.c as a fourth; covering only a subset of them would repeat core.c's -# own siblings' (core_events.c/core_session.c/core_clock.c) +# sim.c/sim_loop.c/sim_record.c/repl.c/repl_break.c/repl_info.c reference +# zero controller-facing symbols (client/netio/xceapi/delay/idgen). All +# files are compiled together into ONE archive -- task 11.2 added +# sim_loop.c as a sibling file to task 11.1's sim.c, task 11.5 added +# sim_record.c as a third, task 12.1 added repl.c as a fourth, and task +# 12.2 adds repl_break.c/repl_info.c as a fifth/sixth (task 12.2's own +# 800-line-cap split of repl.c's breakpoint/state-inspection logic -- +# repl_internal.h's own "Why a split at all" section has the full +# rationale); covering only a subset of them would repeat core.c's own +# siblings' (core_events.c/core_session.c/core_clock.c) # never-mechanically-covered gap for the sim driver. Same "compiling to an # object is enough, no real linking needed" rationale as # sched_core_isolation_lib above. sched_sim_isolation_lib = static_library( 'sched_sim_isolation', - ['src/sim.c', 'src/sim_loop.c', 'src/sim_record.c', 'src/repl.c'], + ['src/sim.c', 'src/sim_loop.c', 'src/sim_record.c', 'src/repl.c', + 'src/repl_break.c', 'src/repl_info.c'], include_directories: incdir, dependencies: scheduler_deps, install: false) diff --git a/src/scheduler/src/repl.c b/src/scheduler/src/repl.c index 6081dcd6..e5fd32e1 100644 --- a/src/scheduler/src/repl.c +++ b/src/scheduler/src/repl.c @@ -6,20 +6,29 @@ #include /* libedit (BSD) -- readline()/add_history() */ #include "repl.h" +#include "repl_internal.h" #include "utils.h" /* - * repl.c - interactive simulation REPL, execution-control slice (scheduler- - * core spec, task 12.1, Req 5 AC 22). Implements a single input-loop REPL - * (NOT a separate thread, per this file family's own established - * single-threaded-driver discipline and design.md line 482/510) that pauses - * before the first event is processed and blocks the simulation loop while - * active. This task's own scope: `step [N]`/`continue`/`quit`/`help`, the - * pre-replay pause, and the blocking interaction model. Breakpoints, state - * inspection (`info job`/`info queue`/etc.), snapshot capture, and `set`/ - * `info config` are tasks 12.2/12.3's own jobs -- see repl_should_pause() - * below for the single seam left for 12.2 to extend without restructuring - * sim_loop.c's call sites (repl.h's own header comment). + * repl.c - interactive simulation REPL: command loop and execution control + * (scheduler-core spec, task 12.1, Req 5 AC 22). Implements a single + * input-loop REPL (NOT a separate thread, per this file family's own + * established single-threaded-driver discipline and design.md line + * 482/510) that pauses before the first event is processed and blocks the + * simulation loop while active. + * + * This file's own scope: `step [N]`/`continue`/`quit`/`help`, the + * pre-replay pause, the blocking interaction model, and repl_dispatch()'s + * command table (which now also hands off to repl_break.c's `break`/ + * `delete` and repl_info.c's `info` -- task 12.2, Req 5 AC 22 + * "Breakpoints"/"State inspection"). Breakpoint matching/storage and state- + * inspection dumps are NOT in this file -- task 12.2's full scope did not + * fit in one file under this project's 800-line cap, so it is split across + * repl.c (this file)/repl_break.c/repl_info.c, sharing private state via + * repl_internal.h -- see that header's own "Why a split at all" section for + * the full rationale, matching the precedent snapshot.c/snapshot_wire.c/ + * snapshot_load.c (task 8.1) already established for the identical + * problem. * * Sanctioned deviation from requirements.md's literal text (Req 5 AC 22, * "Implementation note": "The REPL is a single `fgets` loop"): this task's @@ -40,44 +49,19 @@ * the whole linked binary. Uses libedit (BSD/NetBSD license) instead, via * its editline/readline.h emulation header, which provides the identical * readline()/add_history() surface this file calls -- zero behavioral - * difference, no GPL exposure. src/controller/testsuite/test.c's own REPL - * was switched to the same libedit dependency for project-wide consistency - * (see src/controller/meson.build's `libedit_dep`), even though xce_test's - * standalone-binary status meant it was never GPL-exposed the way - * xce_scheduler would have been. + * difference, no GPL exposure. * * Boundary (Req 5 AC 17, "Simulation isolation", same as sim.h/sim.c/ - * sim_loop.c/sim_record.c): this file touches only xce_sched_sim_driver_t/ - * xce_sched_core_t (via repl.h -> sim.h -> core.h) plus stdio/libedit -- - * no xcecli_ctx_t, no xce_cluster_config_t, no capnp socket, no controller - * RPC. test_sim_isolation.c (task 11.3) is extended by this task to cover - * repl.c/repl.h too, matching the 3-file (now 5-file) precedent it already - * established for sim.c/sim_loop.c/sim_record.c. + * sim_loop.c/sim_record.c): this file (and its repl_break.c/repl_info.c + * siblings) touch only xce_sched_sim_driver_t/xce_sched_core_t (via repl.h/ + * repl_internal.h -> sim.h -> core.h) plus stdio/libedit -- no + * xcecli_ctx_t, no xce_cluster_config_t, no capnp socket, no controller + * RPC. test_sim_isolation.c (task 11.3, extended by task 12.1 and again by + * this task) scans all three .c files plus repl.h/repl_internal.h. */ #define XCE_REPL_PROMPT "(sim) " -typedef enum { - XCE_REPL_MODE_STEP = 0, /* pause once `step_budget` sessions complete */ - XCE_REPL_MODE_CONTINUE /* task 12.1 scope: run unattended until EOF -- - * no breakpoint mechanism exists yet */ -} xce_repl_run_mode_t; - -/* - * xce_sched_repl_state_s - the full definition of sim.h's opaque forward - * declaration. Deliberately private to this file (not exposed even in - * repl.h) -- sim_loop.c and every other production consumer touch it only - * through the pointer, never its fields, matching core.h's own - * xce_sched_core_t precedent for "opaque outside the owning module." - * White-box test files (this test suite's established convention -- see - * test_sim_isolation.c's own header comment on the genre) get full field - * access anyway via `#include "../src/repl.c"`. - */ -struct xce_sched_repl_state_s { - xce_repl_run_mode_t mode; - long step_budget; -}; - /* * repl_state_ensure - lazily allocates sim->repl_state on first use * (xce_repl_start() is always the first caller in the real driver flow, but @@ -88,7 +72,11 @@ struct xce_sched_repl_state_s { * correctness (design.md's "Deterministic replay is preserved" invariant -- * the REPL only ever controls WHEN execution pauses, never WHAT is * processed, so degrading to "never pauses" on OOM is safe, just less - * useful). + * useful). Only called here, at startup -- by the time repl_break.c's + * xce_repl_on_event()/on_begin_round()/on_decision() or any repl_dispatch() + * command runs, sim->repl_state is already guaranteed non-NULL (xce_repl_start() + * always runs first, sim_loop.c's pre-replay pause), so those files read + * sim->repl_state directly rather than needing this function too. */ static xce_sched_repl_state_t* repl_state_ensure(xce_sched_sim_driver_t* sim) { @@ -102,6 +90,44 @@ static xce_sched_repl_state_t* repl_state_ensure(xce_sched_sim_driver_t* sim) return sim->repl_state; } +/* + * repl_split_tokens - splits `line` on whitespace into up to `max_tokens` + * fixed-size tokens (repl_internal.h's own doc comment on why `break`/ + * `info`/`delete` all need more than a single token). Silently truncates an + * over-length token rather than erroring -- a malformed breakpoint filter/ + * name is caught downstream by the specific command handler (unrecognized + * event type, missing '=', etc.), matching this file's existing "fail loud + * with a specific message" style rather than a generic parse-error here. + * Returns the token count (0 if `line` is entirely whitespace). + */ +int repl_split_tokens(const char* line, char tokens[][XCE_REPL_TOKEN_LEN], + int max_tokens) +{ + const char* p = line; + int n = 0; + + while ((*p != '\0') && (n < max_tokens)) { + int i = 0; + + while (isspace((unsigned char)*p)) { + p++; + } + if (*p == '\0') { + break; + } + while ((*p != '\0') && !isspace((unsigned char)*p)) { + if (i < (XCE_REPL_TOKEN_LEN - 1)) { + tokens[n][i++] = *p; + } + p++; + } + tokens[n][i] = '\0'; + n++; + } + + return n; +} + /* * repl_count_jobs - counts `core->jobs` via the SAME iter_make()/iter_next() * walk core_session.c's core_gc_select_nodes()/scheduler.c's own job-listing @@ -159,14 +185,11 @@ static void repl_print_banner(const xce_sched_sim_driver_t* sim) /* * repl_print_help - Req 5 AC 22's "help" command: "Print available commands - * with one-line descriptions." Deliberately covers ONLY the commands this - * task implements (step/continue/quit/help) -- AC 22's full table also - * lists breakpoints, state inspection, snapshot, and `set`/`info config` - * commands that do not exist yet; listing them here would document - * commands repl_dispatch() below would then reject as unrecognized. Tasks - * 12.2/12.3 extend this listing as they add those commands, matching this - * task's own text ("Don't over-build entries for commands that don't exist - * yet"). + * with one-line descriptions." Covers execution control (task 12.1), + * breakpoints, and state inspection (task 12.2) -- AC 22's remaining rows + * (`snapshot`, `set`/`info config`) are task 12.3's job and stay unlisted + * until implemented, matching task 12.1's own "don't over-build entries for + * commands that don't exist yet" convention. */ static void repl_print_help(void) { @@ -175,6 +198,15 @@ static void repl_print_help(void) printf(" continue Run until next breakpoint hit or EOF.\n"); printf(" quit Exit the scheduler immediately.\n"); printf(" help Print this help listing.\n"); + printf(" break [key=value ...] Set a breakpoint.\n"); + printf(" info break List breakpoints.\n"); + printf(" delete |all Delete breakpoint(s).\n"); + printf(" info session Simulation position.\n"); + printf(" info job Dump one job.\n"); + printf(" info queue |all Dump queue(s).\n"); + printf(" info worker |all Dump worker(s).\n"); + printf(" info resource |all Dump resource(s).\n"); + printf(" info summary Quick overview.\n"); } /* @@ -257,10 +289,12 @@ static bool repl_cmd_step(xce_sched_sim_driver_t* sim, const char* line) * repl_dispatch - parses one readline()-returned command line (already * known non-blank by the caller) and executes it. Returns true if control * should return to the driver's event loop (`step`/`continue`), false if - * the REPL should re-prompt (`help`, an unrecognized command, or a `step` - * parse error). `quit` never returns from repl_cmd_quit() -- its own return - * value below is unreachable, kept only so every branch has a well-typed - * value. + * the REPL should re-prompt (`help`, `break`/`info`/`delete`, an + * unrecognized command, or a `step` parse error). `quit` never returns from + * repl_cmd_quit() -- its own return value below is unreachable, kept only + * so every branch has a well-typed value. `break`/`delete`/`info` are + * implemented in repl_break.c/repl_info.c (repl_internal.h's own extern + * declarations) -- this file only routes to them. */ static bool repl_dispatch(xce_sched_sim_driver_t* sim, const char* line) { @@ -285,6 +319,15 @@ static bool repl_dispatch(xce_sched_sim_driver_t* sim, const char* line) repl_print_help(); return false; } + if (strcmp(cmd, "break") == 0) { + return repl_cmd_break(sim, line); + } + if (strcmp(cmd, "delete") == 0) { + return repl_cmd_delete(sim, line); + } + if (strcmp(cmd, "info") == 0) { + return repl_cmd_info(sim, line); + } printf("[sim] unknown command: '%s' (type 'help' for a list)\n", cmd); @@ -299,9 +342,11 @@ static bool repl_dispatch(xce_sched_sim_driver_t* sim, const char* line) * from readline() (Ctrl-D / stdin EOF) is treated exactly like an explicit * `quit` -- the same convention test.c's own loop follows * (`execute_line("quit")` on a NULL readline() return) -- never returns in - * that case either. + * that case either. Non-static (repl_internal.h) -- repl_break.c's + * breakpoint-hit hooks re-enter it directly, the same way + * xce_repl_post_session_check() below already does for a `step N` pause. */ -static void repl_command_loop(xce_sched_sim_driver_t* sim) +void repl_command_loop(xce_sched_sim_driver_t* sim) { for (;;) { char* line = readline(XCE_REPL_PROMPT); @@ -337,13 +382,14 @@ void xce_repl_start(xce_sched_sim_driver_t* sim) } /* - * repl_should_pause - Req 5 AC 22's per-session pause decision, task 12.1's - * own scope: pause only when a `step N` budget reaches zero. `continue` - * mode (AC 22: "Run until next breakpoint hit or EOF") always returns false - * here today -- no breakpoint mechanism exists yet (task 12.2's job). This - * is the single seam task 12.2 extends (adding a breakpoint-list check - * alongside the step-budget check below) without touching - * xce_repl_post_session_check() or sim_loop.c's call site at all. + * repl_should_pause - Req 5 AC 22's `step N` pause decision: pause only + * when a `step N` budget reaches zero. `continue` mode always returns false + * here -- this function is deliberately NOT where breakpoints are checked + * (repl_break.c's xce_repl_on_event()/xce_repl_on_begin_round()/ + * xce_repl_on_decision() are: breakpoint matching happens at the exact + * event/session/decision granularity, not once per completed session -- + * see repl.h's own doc comment on those three functions for the three call + * sites in sim_loop.c). */ static bool repl_should_pause(xce_sched_sim_driver_t* sim) { diff --git a/src/scheduler/src/repl_break.c b/src/scheduler/src/repl_break.c new file mode 100644 index 00000000..35a13b57 --- /dev/null +++ b/src/scheduler/src/repl_break.c @@ -0,0 +1,669 @@ +#include +#include +#include +#include /* strcasecmp -- breakpoint filter value matching */ + +#include "repl.h" +#include "repl_internal.h" +#include "utils.h" +#include "xstring.h" /* makeString() -- breakpoint token/filter string copies */ + +/* + * repl_break.c - breakpoints (scheduler-core spec, task 12.2, Req 5 AC 22 + * "Breakpoints"). Sibling of repl.c/repl_info.c -- see repl_internal.h's + * own "Why a split at all" section for the full rationale behind this + * three-file layout. + * + * Owns: the `break`/`delete` command handlers, the breakpoint list (create/ + * find/remove/clear), event/decision key=value filter matching, and the + * three driver-facing breakpoint-check hooks repl.h declares + * (xce_repl_on_event()/xce_repl_on_begin_round()/xce_repl_on_decision()) -- + * called from sim_loop.c at the three points design.md's REPL integration + * pseudocode requires: per decoded ReplayEvent (before it is applied to + * core state), per ReplaySession boundary (BEGIN_ROUND, before the + * accumulated batch is pushed), and per drained decision (DECISION, after + * verify/output-write). See repl.h's own doc comment on those three + * functions for the exact call-site reasoning. + * + * These hooks are a SEPARATE mechanism from repl.c's repl_should_pause() + * (task 12.1's `step N` pause): repl_should_pause() only fires once per + * completed ReplaySession, too coarse for "pause AT the matching event", + * so breakpoints get their own hook sites instead of extending that + * function -- task 12.1's own doc comment on repl_should_pause() originally + * (wrongly) predicted the opposite; repl.c's current comment there + * corrects the record. + * + * Boundary (Req 5 AC 17): same as repl.c -- only xce_sched_sim_driver_t/ + * xce_sched_core_t (via repl.h/repl_internal.h -> sim.h -> core.h) plus + * stdio/string.h/strings.h/stdlib.h. sched_get_job() (core.h) is the only + * core.h accessor this file calls, for a `queue=` decision filter's job + * lookup. + */ + +/* + * BP_KIND_TABLE - the `break ` vocabulary (Req 5 AC 22 "Breakpoints" + * table's own examples: JOB_STATE_CHANGE, JOB_PUSH, BEGIN_ROUND, DECISION). + * The eight remaining *_PUSH, *_UPDATE, *_RUSAGE tokens complete the set + * against every xce_core_event_type_t member (core.h) + * sim_process_replay_event_message() (sim_loop.c) can actually decode, so + * no wire event type is unreachable from `break`. Token spelling + * intentionally does NOT always match the xce_core_event_type_t enumerator + * name (e.g. "JOB_PUSH" here vs. XCE_CORE_EVENT_JOBS_PUSH) -- AC 22's own + * example text is the authority on spelling, not the internal C enum. + */ +static const struct { + const char* token; + xce_repl_bp_kind_t kind; +} BP_KIND_TABLE[] = { + { "CONFIG_PUSH", XCE_REPL_BP_CONFIG_PUSH }, + { "JOB_PUSH", XCE_REPL_BP_JOB_PUSH }, + { "WORKERS_PUSH", XCE_REPL_BP_WORKERS_PUSH }, + { "RESOURCES_PUSH", XCE_REPL_BP_RESOURCES_PUSH }, + { "RESVALS_FULL", XCE_REPL_BP_RESVALS_FULL }, + { "RESVALS_DELTA", XCE_REPL_BP_RESVALS_DELTA }, + { "JOB_STATE_CHANGE", XCE_REPL_BP_JOB_STATE_CHANGE }, + { "JOB_UPDATE", XCE_REPL_BP_JOB_UPDATE }, + { "QUEUE_UPDATE", XCE_REPL_BP_QUEUE_UPDATE }, + { "JOB_RUSAGE", XCE_REPL_BP_JOB_RUSAGE }, + { "BEGIN_ROUND", XCE_REPL_BP_BEGIN_ROUND }, + { "DECISION", XCE_REPL_BP_DECISION }, +}; +#define NUM_BP_KINDS (sizeof(BP_KIND_TABLE) / sizeof(BP_KIND_TABLE[0])) + +static bool repl_bp_kind_from_token(const char* token, xce_repl_bp_kind_t* out) +{ + size_t i; + + for (i = 0; i < NUM_BP_KINDS; i++) { + if (strcasecmp(token, BP_KIND_TABLE[i].token) == 0) { + *out = BP_KIND_TABLE[i].kind; + return true; + } + } + + return false; +} + +/* repl_bp_kind_for_core_event - the reverse of BP_KIND_TABLE, keyed by + * xce_core_event_type_t (core.h) instead of a user-typed token. Used by + * xce_repl_on_event() (below) to find which breakpoints are even + * candidates for a given decoded event, before running any filter. */ +static xce_repl_bp_kind_t repl_bp_kind_for_core_event(xce_core_event_type_t t) +{ + switch (t) { + case XCE_CORE_EVENT_CONFIG_PUSH: + return XCE_REPL_BP_CONFIG_PUSH; + case XCE_CORE_EVENT_JOBS_PUSH: + return XCE_REPL_BP_JOB_PUSH; + case XCE_CORE_EVENT_WORKERS_PUSH: + return XCE_REPL_BP_WORKERS_PUSH; + case XCE_CORE_EVENT_RESOURCES_PUSH: + return XCE_REPL_BP_RESOURCES_PUSH; + case XCE_CORE_EVENT_RESVALS_FULL: + return XCE_REPL_BP_RESVALS_FULL; + case XCE_CORE_EVENT_RESVALS_DELTA: + return XCE_REPL_BP_RESVALS_DELTA; + case XCE_CORE_EVENT_JOB_STATE_CHANGE: + return XCE_REPL_BP_JOB_STATE_CHANGE; + case XCE_CORE_EVENT_JOB_UPDATE: + return XCE_REPL_BP_JOB_UPDATE; + case XCE_CORE_EVENT_QUEUE_UPDATE: + return XCE_REPL_BP_QUEUE_UPDATE; + case XCE_CORE_EVENT_JOB_RUSAGE: + return XCE_REPL_BP_JOB_RUSAGE; + default: + return (xce_repl_bp_kind_t)0; /* matches no BP_KIND_TABLE entry */ + } +} + +/* repl_job_state_matches - AC 22's `state=RUN`-style filter value, matched + * against the JOB_IS_*() bitmask macros (xce/types.h) every other live + * state check in this codebase already uses, rather than an exact bitwise + * equality (a job's `state` can carry secondary bits, e.g. PEND|PSUSP). */ +static bool repl_job_state_matches(uint32_t state, const char* name) +{ + if (strcasecmp(name, "RUN") == 0) { + return JOB_IS_RUN(state); + } + if (strcasecmp(name, "PEND") == 0) { + return JOB_IS_PEND(state); + } + if (strcasecmp(name, "EXIT") == 0) { + return JOB_IS_EXIT(state); + } + if (strcasecmp(name, "DONE") == 0) { + return JOB_IS_DONE(state); + } + + return false; +} + +/* repl_u64_str_eq - compares a uint64_t field against a filter's string + * value (every jobid= filter goes through this). */ +static bool repl_u64_str_eq(uint64_t v, const char* value) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), "%llu", (unsigned long long)v); + return strcmp(buf, value) == 0; +} + +/* + * repl_event_matches_filter - one key=value clause against one decoded + * xce_core_event_t (task 12.2, AC 22's `break JOB_STATE_CHANGE jobid=12345`/ + * `state=RUN` examples). A key not meaningful for this event's own type + * (e.g. "state=" against a QUEUE_UPDATE) fails closed (never matches) + * rather than erroring at match time -- `break` already validated the + * event-type token itself; an unsupported key on a supported type is a + * user typo this function has no channel to report from (called deep + * inside the driver loop, not at command-entry time). + */ +static bool repl_event_matches_filter(const xce_core_event_t* ev, + const char* key, const char* value) +{ + switch (ev->type) { + case XCE_CORE_EVENT_JOB_STATE_CHANGE: + if (strcmp(key, "jobid") == 0) { + return repl_u64_str_eq(ev->u.job_state_change.jobid, value); + } + if (strcmp(key, "state") == 0) { + return repl_job_state_matches(ev->u.job_state_change.new_state, + value); + } + return false; + case XCE_CORE_EVENT_JOB_UPDATE: + if (strcmp(key, "jobid") == 0) { + return repl_u64_str_eq(ev->u.job_update.jobid, value); + } + return false; + case XCE_CORE_EVENT_JOB_RUSAGE: + if (strcmp(key, "jobid") == 0) { + return repl_u64_str_eq(ev->u.job_rusage.jobid, value); + } + return false; + case XCE_CORE_EVENT_JOBS_PUSH: + if (strcmp(key, "jobid") == 0) { + int i; + + for (i = 0; i < ev->u.jobs_push.njobs; i++) { + xce_job_t* j = ev->u.jobs_push.jobs[i]; + + if ((j != NULL) && repl_u64_str_eq(j->jobid, value)) { + return true; + } + } + return false; + } + return false; + case XCE_CORE_EVENT_QUEUE_UPDATE: + if (strcmp(key, "queue") == 0) { + return (ev->u.queue_update.queue != NULL) + && (strcmp(ev->u.queue_update.queue->queue, value) == 0); + } + return false; + default: + return false; + } +} + +/* repl_breakpoint_matches_event - AND's every filter of `bp` against `ev`; + * a breakpoint with zero filters matches every event of its own kind. */ +static bool repl_breakpoint_matches_event(const xce_repl_breakpoint_t* bp, + const xce_core_event_t* ev) +{ + int i; + + if (bp->kind != repl_bp_kind_for_core_event(ev->type)) { + return false; + } + for (i = 0; i < bp->nfilters; i++) { + if (!repl_event_matches_filter(ev, bp->filters[i].key, + bp->filters[i].value)) { + return false; + } + } + + return true; +} + +/* decision_op_name - "DISPATCH"/"PEND"/"PREEMPT"/"NONE", the `type=` + * filter's own vocabulary (AC 22: `break DECISION type=DISPATCH`) and the + * same label sim_print_decision() (sim_loop.c) already prints per op. */ +static const char* decision_op_name(xce_decision_op_t op) +{ + switch (op) { + case XCE_DECISION_DISPATCH: + return "DISPATCH"; + case XCE_DECISION_PEND: + return "PEND"; + case XCE_DECISION_PREEMPT: + return "PREEMPT"; + default: + return "NONE"; + } +} + +/* decision_jobid - the one jobid every non-NONE xce_decision_t op carries, + * regardless of which union arm is active. */ +static uint64_t decision_jobid(const xce_decision_t* d) +{ + switch (d->op) { + case XCE_DECISION_DISPATCH: + return (d->u.alloc != NULL) ? d->u.alloc->jobid : 0; + case XCE_DECISION_PEND: + return (d->u.reason != NULL) ? d->u.reason->jobid : 0; + case XCE_DECISION_PREEMPT: + return (d->u.preempt != NULL) ? d->u.preempt->jobid : 0; + default: + return 0; + } +} + +/* + * repl_decision_matches_filter - one key=value clause against one drained + * xce_decision_t (task 12.2, AC 22's `break DECISION type=DISPATCH + * queue=normal` example). `queue=` requires a job lookup (a decision itself + * carries no queue field) -- sched_get_job() (core.h), the same accessor + * `info job` (repl_info.c) uses; a decision for a jobid no longer in + * core->jobs (freed after completion) never matches a queue= filter, a + * documented, narrow gap rather than a crash. + */ +static bool repl_decision_matches_filter(xce_sched_core_t* core, + const xce_decision_t* d, + const char* key, const char* value) +{ + if (strcmp(key, "type") == 0) { + return strcasecmp(decision_op_name(d->op), value) == 0; + } + if (strcmp(key, "jobid") == 0) { + return repl_u64_str_eq(decision_jobid(d), value); + } + if (strcmp(key, "queue") == 0) { + xce_sched_job_t* job = sched_get_job(core, decision_jobid(d)); + + return (job != NULL) && (job->queue != NULL) + && (strcmp(job->queue->name, value) == 0); + } + + return false; +} + +static bool repl_breakpoint_matches_decision(xce_sched_core_t* core, + const xce_repl_breakpoint_t* bp, + const xce_decision_t* d) +{ + int i; + + if (bp->kind != XCE_REPL_BP_DECISION) { + return false; + } + for (i = 0; i < bp->nfilters; i++) { + if (!repl_decision_matches_filter(core, d, bp->filters[i].key, + bp->filters[i].value)) { + return false; + } + } + + return true; +} + +/* repl_find_event_breakpoint / repl_find_begin_round_breakpoint / + * repl_find_decision_breakpoint - first-match scan of rs->breakpoints; + * returns the matched entry (for its id + token, used by the "[sim] hit + * breakpoint ..." message) or NULL. BEGIN_ROUND supports no filters today + * (AC 22 shows no BEGIN_ROUND filter example) -- a bare "break BEGIN_ROUND" + * matches every session boundary; any filters attached to one never match + * (fail closed, same discipline repl_event_matches_filter() uses for an + * unsupported key). */ +static xce_repl_breakpoint_t* +repl_find_event_breakpoint(xce_sched_repl_state_t* rs, + const xce_core_event_t* ev) +{ + int i; + + for (i = 0; i < rs->nbreakpoints; i++) { + if (repl_breakpoint_matches_event(rs->breakpoints[i], ev)) { + return rs->breakpoints[i]; + } + } + return NULL; +} + +static xce_repl_breakpoint_t* +repl_find_begin_round_breakpoint(xce_sched_repl_state_t* rs) +{ + int i; + + for (i = 0; i < rs->nbreakpoints; i++) { + xce_repl_breakpoint_t* bp = rs->breakpoints[i]; + + if ((bp->kind == XCE_REPL_BP_BEGIN_ROUND) && (bp->nfilters == 0)) { + return bp; + } + } + return NULL; +} + +static xce_repl_breakpoint_t* +repl_find_decision_breakpoint(xce_sched_core_t* core, + xce_sched_repl_state_t* rs, + const xce_decision_t* d) +{ + int i; + + for (i = 0; i < rs->nbreakpoints; i++) { + if (repl_breakpoint_matches_decision(core, rs->breakpoints[i], d)) { + return rs->breakpoints[i]; + } + } + return NULL; +} + +/* + * repl_breakpoint_free - releases one breakpoint's plain-malloc'd memory + * (bug-fix, discovered during this task's own RED/GREEN cycle: see + * .wolf/buglog.json). `bp->token` and each filter's key/value are + * makeString()-interned xstrings -- RTTI reference-counted objects whose + * returned pointer is NOT a plain malloc() chunk (xstring.c's own + * xstring_dereference() is the only correct release path, via the RTTI + * reference-counting interface, never a bare free()). This codebase's own + * kill_sched_job()/kill_sched_queue() (schedobj.c) establish the identical + * precedent for every OTHER makeString()'d field in this codebase + * (xce_sched_job_t/xce_sched_queue_t names): never explicitly released, + * living for the process's lifetime (matching this whole test suite's own + * "detect_leaks=0 by design" convention). Calling free() on one instead + * corrupts the heap allocator's bookkeeping ("free(): invalid pointer"); + * only the plain calloc'd `bp->filters` array and `bp` itself are + * genuinely owned, plain-malloc'd memory this function may free. + */ +static void repl_breakpoint_free(xce_repl_breakpoint_t* bp) +{ + if (bp == NULL) { + return; + } + free(bp->filters); + free(bp); +} + +/* repl_breakpoints_append - grows rs->breakpoints (a realloc'd pointer + * array, same growth idiom sim_loop.c's own buf_events[] uses) and appends + * `bp`. Returns false (and does NOT free `bp` -- the caller's job, so it + * can report the allocation failure with its own context) on OOM. */ +static bool repl_breakpoints_append(xce_sched_repl_state_t* rs, + xce_repl_breakpoint_t* bp) +{ + xce_repl_breakpoint_t** grown = (xce_repl_breakpoint_t**)realloc( + rs->breakpoints, + (size_t)(rs->nbreakpoints + 1) * sizeof(*(rs->breakpoints))); + + if (grown == NULL) { + return false; + } + + rs->breakpoints = grown; + rs->breakpoints[rs->nbreakpoints] = bp; + rs->nbreakpoints++; + + return true; +} + +/* repl_breakpoints_clear - `delete all`. Frees every entry, resets the + * array/count. `next_bp_id` is deliberately NOT reset -- ids are never + * reused within one REPL session, matching this codebase's general + * "monotonic id, never recycled" convention (e.g. core->nextid). */ +static void repl_breakpoints_clear(xce_sched_repl_state_t* rs) +{ + int i; + + for (i = 0; i < rs->nbreakpoints; i++) { + repl_breakpoint_free(rs->breakpoints[i]); + } + free(rs->breakpoints); + rs->breakpoints = NULL; + rs->nbreakpoints = 0; +} + +/* repl_breakpoint_remove - `delete `. Compacts the array on a match + * (memmove over the removed slot) so `nbreakpoints` always reflects the + * live count with no NULL holes -- every scan above (repl_find_*) relies on + * a hole-free array. Returns false if no breakpoint has `id`. */ +static bool repl_breakpoint_remove(xce_sched_repl_state_t* rs, int id) +{ + int i; + + for (i = 0; i < rs->nbreakpoints; i++) { + if (rs->breakpoints[i]->id == id) { + repl_breakpoint_free(rs->breakpoints[i]); + memmove(&(rs->breakpoints[i]), &(rs->breakpoints[i + 1]), + (size_t)(rs->nbreakpoints - i - 1) + * sizeof(*(rs->breakpoints))); + rs->nbreakpoints--; + return true; + } + } + + return false; +} + +/* + * repl_cmd_break - Req 5 AC 22's "break [key=value ...]": "Set + * breakpoint on event type ... Optional key=value filters narrow the + * match ... Multiple filters are AND'd." `line` (the full command line, + * including the "break" token) is re-tokenized via repl_split_tokens() + * (repl.c) rather than repl_dispatch()'s single-token sscanf(). Each filter + * token must contain exactly one '=' with a non-empty key -- a malformed + * filter aborts the WHOLE command (no partially-applied breakpoint), same + * "fail loud, no silent partial state" discipline repl.c's own + * repl_cmd_step() doc comment establishes for a bad `step` argument. + */ +bool repl_cmd_break(xce_sched_sim_driver_t* sim, const char* line) +{ + char tokens[XCE_REPL_MAX_TOKENS][XCE_REPL_TOKEN_LEN]; + int ntok = repl_split_tokens(line, tokens, + XCE_REPL_MAX_TOKENS); + xce_repl_bp_kind_t kind; + xce_repl_breakpoint_t* bp; + int i; + + if (ntok < 2) { + printf("[sim] break: usage: break [key=value ...]\n"); + return false; + } + if (!repl_bp_kind_from_token(tokens[1], &kind)) { + printf("[sim] break: unrecognized event type '%s'\n", tokens[1]); + return false; + } + + bp = (xce_repl_breakpoint_t*)calloc(1, sizeof(*bp)); + if (bp == NULL) { + printf("[sim] break: out of memory\n"); + return false; + } + bp->kind = kind; + bp->token = makeString(tokens[1]); + + for (i = 2; i < ntok; i++) { + char* eq = strchr(tokens[i], '='); + xce_repl_filter_t* grown; + + if ((eq == NULL) || (eq == tokens[i])) { + printf("[sim] break: malformed filter '%s' -- expected " + "key=value\n", + tokens[i]); + repl_breakpoint_free(bp); + return false; + } + *eq = '\0'; + + grown = (xce_repl_filter_t*)realloc( + bp->filters, (size_t)(bp->nfilters + 1) * sizeof(*(bp->filters))); + if (grown == NULL) { + printf("[sim] break: out of memory\n"); + repl_breakpoint_free(bp); + return false; + } + bp->filters = grown; + bp->filters[bp->nfilters].key = makeString(tokens[i]); + bp->filters[bp->nfilters].value = makeString(eq + 1); + bp->nfilters++; + } + + bp->id = ++(sim->repl_state->next_bp_id); + if (!repl_breakpoints_append(sim->repl_state, bp)) { + printf("[sim] break: out of memory\n"); + (sim->repl_state->next_bp_id)--; + repl_breakpoint_free(bp); + return false; + } + + printf("[sim] breakpoint %d set: %s", bp->id, bp->token); + for (i = 0; i < bp->nfilters; i++) { + printf(" %s=%s", bp->filters[i].key, bp->filters[i].value); + } + printf("\n"); + + return false; /* setting a breakpoint never hands control back */ +} + +/* + * repl_cmd_delete - Req 5 AC 22's "delete ": "Delete breakpoint by ID. + * `delete all` removes all breakpoints." + */ +bool repl_cmd_delete(xce_sched_sim_driver_t* sim, const char* line) +{ + char tokens[XCE_REPL_MAX_TOKENS][XCE_REPL_TOKEN_LEN]; + int ntok = repl_split_tokens(line, tokens, XCE_REPL_MAX_TOKENS); + + if (ntok < 2) { + printf("[sim] delete: usage: delete |all\n"); + return false; + } + + if (strcmp(tokens[1], "all") == 0) { + int n = sim->repl_state->nbreakpoints; + + repl_breakpoints_clear(sim->repl_state); + printf("[sim] deleted %d breakpoint(s)\n", n); + return false; + } + + { + int id = atoi(tokens[1]); + + if (!repl_breakpoint_remove(sim->repl_state, id)) { + printf("[sim] delete: no breakpoint with id %d\n", id); + } else { + printf("[sim] breakpoint %d deleted\n", id); + } + } + + return false; +} + +/* + * xce_repl_on_event - the per-decoded-ReplayEvent breakpoint-check hook + * (task 12.2, Req 5 AC 22's `check_breakpoints(event)` pseudocode call, + * BEFORE `core_push_events`). Called from sim_loop.c right after a + * translatable event is decoded, before it is appended to the accumulating + * batch -- so a matching breakpoint pauses the REPL with core state exactly + * as it stood before this event's effect, i.e. "about to be processed" + * (the event is not applied to core state until the next ReplaySession + * boundary's xce_core_push_events() call). Also increments `event_index`, + * the position `info session` (repl_info.c) reports -- see repl.h's own + * doc comment on this function for why no separate hook site exists for + * that bookkeeping. + */ +void xce_repl_on_event(xce_sched_sim_driver_t* sim, xce_core_event_t* ev) +{ + xce_sched_repl_state_t* rs; + xce_repl_breakpoint_t* bp; + + if ((sim == NULL) || (ev == NULL) || (sim->repl_state == NULL)) { + return; + } + rs = sim->repl_state; + + rs->event_index++; + + bp = repl_find_event_breakpoint(rs, ev); + if (bp != NULL) { + printf("[sim] hit breakpoint %d @ event #%llu: %s\n", bp->id, + (unsigned long long)rs->event_index, bp->token); + repl_command_loop(sim); + } +} + +/* + * xce_repl_on_begin_round - the BEGIN_ROUND breakpoint-check hook (task + * 12.2): BEGIN_ROUND is the ReplaySession boundary itself, so this is + * called from sim_loop.c right after `begin_round` is decoded, before the + * accumulated event batch is pushed -- matching the pseudocode's + * `check_breakpoints(event)` call ordering for the message that IS a + * ReplaySession (see repl.h's own doc comment). Also resets the + * last-session decision tally and increments `session_count`, both of + * which `info session` (repl_info.c) reports. + */ +void xce_repl_on_begin_round(xce_sched_sim_driver_t* sim, uint64_t begin_round) +{ + xce_sched_repl_state_t* rs; + xce_repl_breakpoint_t* bp; + + if ((sim == NULL) || (sim->repl_state == NULL)) { + return; + } + rs = sim->repl_state; + + rs->session_count++; + rs->last_begin_round = begin_round; + rs->last_n_dispatch = 0; + rs->last_n_pend = 0; + rs->last_n_preempt = 0; + + bp = repl_find_begin_round_breakpoint(rs); + if (bp != NULL) { + printf("[sim] hit breakpoint %d @ BEGIN_ROUND round=%llu\n", bp->id, + (unsigned long long)begin_round); + repl_command_loop(sim); + } +} + +/* + * xce_repl_on_decision - the per-drained-decision breakpoint-check hook + * (task 12.2): called from sim_loop.c's decision-drain loop right after + * sim_print_decision(d), matching the pseudocode's + * `check_breakpoints(decision)` ordering ("after verify/output-write, + * before moving to the next decision" -- see repl.h's own doc comment). + * Also accumulates the last-session dispatch/pend/preempt tally + * `info session` (repl_info.c) reports. + */ +void xce_repl_on_decision(xce_sched_sim_driver_t* sim, xce_decision_t* d) +{ + xce_sched_repl_state_t* rs; + xce_repl_breakpoint_t* bp; + + if ((sim == NULL) || (d == NULL) || (sim->repl_state == NULL)) { + return; + } + rs = sim->repl_state; + + switch (d->op) { + case XCE_DECISION_DISPATCH: + rs->last_n_dispatch++; + break; + case XCE_DECISION_PEND: + rs->last_n_pend++; + break; + case XCE_DECISION_PREEMPT: + rs->last_n_preempt++; + break; + default: + break; + } + + bp = repl_find_decision_breakpoint(&(sim->core), rs, d); + if (bp != NULL) { + printf("[sim] hit breakpoint %d @ decision: %s jobid=%llu\n", bp->id, + decision_op_name(d->op), (unsigned long long)decision_jobid(d)); + repl_command_loop(sim); + } +} diff --git a/src/scheduler/src/repl_info.c b/src/scheduler/src/repl_info.c new file mode 100644 index 00000000..6a901b52 --- /dev/null +++ b/src/scheduler/src/repl_info.c @@ -0,0 +1,668 @@ +#include +#include +#include + +#include "repl.h" +#include "repl_internal.h" +#include "resreq.h" +#include "utils.h" + +/* + * repl_info.c - state inspection (scheduler-core spec, task 12.2, Req 5 AC + * 22 "State inspection"). Sibling of repl.c/repl_break.c -- see + * repl_internal.h's own "Why a split at all" section for the full + * rationale behind this three-file layout. + * + * Owns: `info session`/`info job`/`info queue`/`info worker`/ + * `info resource`/`info summary`/`info break`, and the `repl_cmd_info()` + * dispatcher repl.c's repl_dispatch() hands off to (repl_internal.h's own + * extern declaration). + * + * Every dump below reads core->jobs/queues/workers/resmgr the SAME way + * repl.c's own repl_count_jobs() and snapshot.c's + * xce_sched_core_save_snapshot() already do -- no new traversal mechanism. + * Field selection is deliberately cross-checked against + * xce_sched_core_save_snapshot()'s own documented field set (snapshot.c's + * file header comment, "Field-by-field mapping") rather than invented + * fresh, matching AC 22's own text: inspection output should "report state + * matching what a snapshot dump of the same moment would show." Where a + * requested field genuinely does not exist in this codebase's data model + * yet (e.g. xce_sched_queue_t has no RUNLIMIT/MAX_RUNNING field), that gap + * is reported honestly ("n/a"), never fabricated -- the same discipline + * snapshot.c's own "Scope decisions" section already establishes for + * job-level priority. + * + * Boundary (Req 5 AC 17): same as repl.c/repl_break.c -- only + * xce_sched_sim_driver_t/xce_sched_core_t (via repl.h/repl_internal.h -> + * sim.h -> core.h) plus stdio/string.h/stdlib.h, plus resreq.h for + * format_resreq() (`info job`'s resreq= field, see repl_info_job() below). + * resreq.h carries no capnp/controller/client symbols of its own (its + * #include set is kvmgr.h/xlist.h/xvec.h plus libc headers) and is already + * part of this same isolation-boundary family -- snapshot_wire.c's own + * build_wire_job() (task 8.1) already includes it and calls format_resreq() + * for the identical purpose. + */ + +/* dup_val/arr_append/find_resource_name_by_idx -- implemented in + * snapshot_wire.c (task 8.1's own 800-line-cap split); redeclared here + * rather than pulling in a private header, same "extern declared where + * consumed" idiom snapshot.c itself already uses for these exact symbols. + * Only arr_append/find_resource_name_by_idx are needed here (`info worker`/ + * `info resource` resolve a resource idx to its name the SAME way + * xce_sched_core_save_snapshot() already does, per this file's own header + * comment). */ +extern int arr_append(void***, int*, int*, void*); +extern const char* find_resource_name_by_idx(xce_resource_t**, int, int); + +/* repl_job_state_name - a single readable label for `info job`'s state= + * field (task 12.2, AC 22's "info job : Dump one job: state, ..." + * row). Priority order matches this codebase's own convention for a job + * whose `state` bitmask carries more than one bit (e.g. PEND|PSUSP): the + * most "advanced" lifecycle bit wins, mirroring repl_break.c's own + * repl_job_state_matches() check order (used by `break ... state=` + * filters) so a filter and this display never disagree about which single + * word describes a given state. */ +static const char* repl_job_state_name(uint32_t state) +{ + if (JOB_IS_RUN(state)) { + return "RUN"; + } + if (JOB_IS_EXIT(state)) { + return "EXIT"; + } + if (JOB_IS_DONE(state)) { + return "DONE"; + } + if (JOB_IS_PEND(state)) { + return "PEND"; + } + return "UNKNOWN"; +} + +/* + * repl_info_job - AC 22's "info job ": "Dump one job: state, queue, + * priority, queue_time, resreq, allocations." Field set cross-checked + * against xce_sched_core_save_snapshot()'s own documented mapping + * (snapshot.c file header, "Field-by-field mapping", jobs row) rather than + * invented fresh -- e.g. `resreq` is formatted via format_resreq() + * (resreq.h), the SAME print-form snapshot_wire.c's own build_wire_job() + * (task 8.1) already calls to populate a restored job's `submit->resreq` + * wire field, not a fabricated format. (job-level priority remains the one + * genuine gap snapshot.c's own "Scope decisions" section documents -- no + * wire slot exists for it at all, unlike resreq.) sched_get_job() (core.h) + * is the exact accessor xce_core_push_events()'s own JOB_STATE_CHANGE/ + * JOB_UPDATE handling (core_events.c) already uses. + */ +static void repl_info_job(xce_sched_sim_driver_t* sim, const char* jobid_str) +{ + uint64_t jobid = strtoull(jobid_str, NULL, 10); + xce_sched_job_t* job = sched_get_job(&(sim->core), jobid); + char* resreq_str = NULL; + + if (job == NULL) { + printf("[sim] info job: no such job %llu\n", + (unsigned long long)jobid); + return; + } + + /* Same format_resreq() call shape/error-handling build_wire_job() + * (snapshot_wire.c) already uses to populate a restored job's + * `submit->resreq` wire field -- WARN + fall back to "(none)" on + * failure, never fabricate a format. Unlike build_wire_job() (whose + * caller owns the string via j->submit->resreq for the lifetime of the + * wire job), this dump is transient -- resreq_str is freed below right + * after printing. */ + if (job->resreq != NULL) { + int rc = format_resreq(&resreq_str, job->resreq); + + if (rc < 0) { + WARN("%s format_resreq failed for jobid %llu (rc=%d); resreq " + "will be reported as unavailable for this dump", + __func__, (unsigned long long)job->jobid, rc); + resreq_str = NULL; + } + } + + printf("[sim] job %llu: state=%s queue=%s priority=%d queue_time=%lld " + "resreq=%s allocs=%d\n", + (unsigned long long)job->jobid, repl_job_state_name(job->state), + (job->queue != NULL) ? job->queue->name : "(none)", job->priority, + (long long)job->queue_time, + (resreq_str != NULL) ? resreq_str : "(none)", + (job->allocs != NULL) ? job->allocs->nhosts : 0); + + FREEUP(resreq_str); +} + +/* + * repl_info_queue_one - AC 22's "info queue ": "Dump one queue: + * priority, state (open/closed), RUNLIMIT, MAX_RUNNING, job_pick_plugin, + * list of queued job IDs with priorities." xce_sched_queue_t (sched/types.h) + * has no RUNLIMIT/MAX_RUNNING field at all -- confirmed against both the + * live struct and its wire xce_queue_t counterpart -- the same + * pre-existing, documented gap snapshot.c's own "Scope decisions" section + * already notes for other AC 21/22-adjacent fields (e.g. job-level + * priority) that requirements.md names but no C struct in this codebase + * carries yet. Reported honestly as "n/a" rather than a fabricated number. + * "queued" below means JOB_IS_PEND() -- a running job is not queued. + */ +static void repl_info_queue_one(xce_sched_sim_driver_t* sim, + xce_sched_queue_t* q) +{ + iterator_t it; + xce_sched_job_t* job = NULL; + int rc; + + printf("[sim] queue %s: priority=%d status=%s RUNLIMIT=n/a " + "MAX_RUNNING=n/a job_pick_plugin=%s\n", + q->name, q->priority, + ((q->status & QUEUE_STAT_CLOSED) != 0) ? "closed" : "open", + (q->job_pick_plugin != NULL) ? q->job_pick_plugin : "(inherit)"); + + rc = iter_make(&it, sim->core.jobs); + if (rc < 0) { + return; + } + for (;;) { + rc = iter_next(&it, (void**)&job); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + if ((job->queue != NULL) && (strcmp(job->queue->name, q->name) == 0) + && JOB_IS_PEND(job->state)) { + printf("[sim] queued job: jobid=%llu priority=%d\n", + (unsigned long long)job->jobid, job->priority); + } + } + iter_destroy(&it); +} + +/* repl_info_queue - `info queue ` dispatch. "all" walks + * core->queues (same iter_make()/iter_next() idiom every other walk in this + * file uses); a specific name goes through sched_get_queue() (core.h). */ +static void repl_info_queue(xce_sched_sim_driver_t* sim, const char* arg) +{ + if (strcmp(arg, "all") == 0) { + iterator_t it; + xce_sched_queue_t* q = NULL; + int rc = iter_make(&it, sim->core.queues); + + if (rc < 0) { + return; + } + for (;;) { + rc = iter_next(&it, (void**)&q); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + repl_info_queue_one(sim, q); + } + iter_destroy(&it); + return; + } + + { + xce_sched_queue_t* q = sched_get_queue(&(sim->core), (char*)arg); + + if (q == NULL) { + printf("[sim] info queue: no such queue '%s'\n", arg); + return; + } + repl_info_queue_one(sim, q); + } +} + +/* + * repl_info_worker_one - AC 22's "info worker ": "Dump one worker: + * state, resources (total + available), assigned jobs." `w->resvals` holds + * currently-AVAILABLE values (the same zarray the picker/allocator + * accounting path reads/resets each session, core_events.c); `w->resInUse` + * holds the currently-committed amount at the same index -- "total" below + * is therefore computed as available+inUse, mirroring how this codebase's + * own resource accounting treats the pair as a conservation invariant + * (nothing elsewhere sums them into a literal "total" field to reuse). + * Resource names are resolved via find_resource_name_by_idx() against a + * resmgr walk built the same way xce_sched_core_save_snapshot() builds its + * own `res_arr` (snapshot.c) -- not a new lookup mechanism. "assigned jobs" + * scans core->jobs for any per-host allocation naming this worker + * (xce_job_alloc_t.allocs[]->host, xce/types.h). + */ +static void repl_info_worker_one(xce_sched_sim_driver_t* sim, + xce_sched_worker_t* w) +{ + xce_resource_t** res_arr = NULL; + int res_cap = 0; + int res_n = 0; + iterator_t it; + xce_resource_t* r = NULL; + xce_sched_job_t* job; + int rc; + int i; + + printf("[sim] worker %s: status=%s\n", w->name, + (IS_WORKER_UNAVAIL(w->status)) ? "unavail" : "online"); + + rc = iter_make(&it, sim->core.resmgr); + if (rc >= 0) { + for (;;) { + rc = iter_next(&it, (void**)&r); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + arr_append((void***)&res_arr, &res_cap, &res_n, r); + } + iter_destroy(&it); + } + + for (i = 0; i < w->resvals.count; i++) { + xce_val_t* avail_v = (xce_val_t*)zarray_get(&(w->resvals), i); + xce_val_t* used_v = (xce_val_t*)zarray_get(&(w->resInUse), i); + const char* rname = find_resource_name_by_idx(res_arr, res_n, i); + double avail; + double used; + + if ((avail_v == NULL) || (rname == NULL)) { + continue; + } + avail = (avail_v->which == XCE_VAL_NUMERIC) ? avail_v->u.fvalue : 0.0; + used = ((used_v != NULL) && (used_v->which == XCE_VAL_NUMERIC)) + ? used_v->u.fvalue + : 0.0; + printf("[sim] resource %s: total=%.2f available=%.2f\n", rname, + avail + used, avail); + } + free(res_arr); + + job = NULL; + rc = iter_make(&it, sim->core.jobs); + if (rc < 0) { + return; + } + for (;;) { + rc = iter_next(&it, (void**)&job); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + if (job->allocs == NULL) { + continue; + } + for (i = 0; i < job->allocs->nhosts; i++) { + xce_host_alloc_t* ha = job->allocs->allocs[i]; + + if ((ha != NULL) && (ha->host != NULL) + && (strcmp(ha->host, w->name) == 0)) { + printf("[sim] assigned job: jobid=%llu\n", + (unsigned long long)job->jobid); + break; + } + } + } + iter_destroy(&it); +} + +/* repl_info_worker - `info worker ` dispatch, same "all" vs. + * sched_get_worker() split as repl_info_queue() above. */ +static void repl_info_worker(xce_sched_sim_driver_t* sim, const char* arg) +{ + if (strcmp(arg, "all") == 0) { + iterator_t it; + xce_sched_worker_t* w = NULL; + int rc = iter_make(&it, sim->core.workers); + + if (rc < 0) { + return; + } + for (;;) { + rc = iter_next(&it, (void**)&w); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + repl_info_worker_one(sim, w); + } + iter_destroy(&it); + return; + } + + { + xce_sched_worker_t* w = sched_get_worker(&(sim->core), (char*)arg); + + if (w == NULL) { + printf("[sim] info worker: no such worker '%s'\n", arg); + return; + } + repl_info_worker_one(sim, w); + } +} + +/* + * repl_info_resource_one - AC 22's "info resource ": "Dump one + * resource: type, total capacity, available, allocation count." `r` is the + * DEFINITION object (xce/types.h's xce_resource_t -- valtype/ordtype/idx, + * no per-worker numbers of its own, per snapshot.c's own "resources -> + * core->resmgr ... already the wire type natively" note); total/available + * are aggregated across every worker's resvals/resInUse at `r->idx`, the + * same idx-indexed convention repl_info_worker_one() above already + * establishes. "allocation count" counts distinct jobs currently holding + * any per-host xce_alloc_t naming this resource (xce/types.h). + */ +static void repl_info_resource_one(xce_sched_sim_driver_t* sim, + xce_resource_t* r) +{ + iterator_t it; + xce_sched_worker_t* w = NULL; + xce_sched_job_t* job; + double total = 0.0; + double available = 0.0; + int nallocs = 0; + int rc; + + rc = iter_make(&it, sim->core.workers); + if (rc >= 0) { + for (;;) { + xce_val_t* avail_v; + xce_val_t* used_v; + + rc = iter_next(&it, (void**)&w); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + if (r->idx >= w->resvals.count) { + continue; + } + avail_v = (xce_val_t*)zarray_get(&(w->resvals), r->idx); + used_v = (xce_val_t*)zarray_get(&(w->resInUse), r->idx); + if ((avail_v != NULL) && (avail_v->which == XCE_VAL_NUMERIC)) { + available += avail_v->u.fvalue; + total += avail_v->u.fvalue; + } + if ((used_v != NULL) && (used_v->which == XCE_VAL_NUMERIC)) { + total += used_v->u.fvalue; + } + } + iter_destroy(&it); + } + + job = NULL; + rc = iter_make(&it, sim->core.jobs); + if (rc >= 0) { + for (;;) { + int i; + int matched = 0; + + rc = iter_next(&it, (void**)&job); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + if (job->allocs == NULL) { + continue; + } + for (i = 0; i < job->allocs->nhosts; i++) { + xce_host_alloc_t* ha = job->allocs->allocs[i]; + int j; + + if (ha == NULL) { + continue; + } + for (j = 0; j < ha->nallocs; j++) { + if ((ha->allocs[j] != NULL) + && (ha->allocs[j]->resname != NULL) + && (strcmp(ha->allocs[j]->resname, r->name) == 0)) { + matched = 1; + break; + } + } + if (matched) { + break; + } + } + if (matched) { + nallocs++; + } + } + iter_destroy(&it); + } + + printf("[sim] resource %s: valtype=%s total=%.2f available=%.2f " + "allocation_count=%d\n", + r->name, + (r->valtype == XCE_VAL_NUMERIC) + ? "numeric" + : (r->valtype == XCE_VAL_STRING) ? "string" : "boolean", + total, available, nallocs); +} + +/* repl_info_resource - `info resource ` dispatch. resmgr has no + * exported sched_get_*() accessor (unlike jobs/queues/workers, core.h) -- + * key_find() directly, the same accessor core_events.c's own + * QUEUE_UPDATE/RESOURCES_PUSH handling already uses against core->resmgr. */ +static void repl_info_resource(xce_sched_sim_driver_t* sim, const char* arg) +{ + if (strcmp(arg, "all") == 0) { + iterator_t it; + xce_resource_t* r = NULL; + int rc = iter_make(&it, sim->core.resmgr); + + if (rc < 0) { + return; + } + for (;;) { + rc = iter_next(&it, (void**)&r); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + repl_info_resource_one(sim, r); + } + iter_destroy(&it); + return; + } + + { + xce_resource_t* r = NULL; + int rc = key_find(sim->core.resmgr, (void*)arg, (void**)&r); + + if (rc <= 0) { + printf("[sim] info resource: no such resource '%s'\n", arg); + return; + } + repl_info_resource_one(sim, r); + } +} + +/* + * repl_info_summary - AC 22's "info summary": "Quick overview: N_jobs (M + * pending, K running), N_workers (M online), N_queues, N_resources." Four + * independent iter_make()/iter_next() walks, same idiom repl.c's own + * repl_count_jobs() already establishes -- no persistent counters, + * computed fresh each call. + */ +static void repl_info_summary(xce_sched_sim_driver_t* sim) +{ + iterator_t it; + int rc; + int n_jobs = 0, n_pending = 0, n_running = 0; + int n_workers = 0, n_online = 0; + int n_queues = 0, n_resources = 0; + xce_sched_job_t* job = NULL; + + rc = iter_make(&it, sim->core.jobs); + if (rc >= 0) { + for (;;) { + rc = iter_next(&it, (void**)&job); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + n_jobs++; + if (JOB_IS_PEND(job->state)) { + n_pending++; + } + if (JOB_IS_RUN(job->state)) { + n_running++; + } + } + iter_destroy(&it); + } + + { + xce_sched_worker_t* w = NULL; + + rc = iter_make(&it, sim->core.workers); + if (rc >= 0) { + for (;;) { + rc = iter_next(&it, (void**)&w); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + n_workers++; + if (!IS_WORKER_UNAVAIL(w->status)) { + n_online++; + } + } + iter_destroy(&it); + } + } + + { + void* tmp = NULL; + + rc = iter_make(&it, sim->core.queues); + if (rc >= 0) { + for (;;) { + rc = iter_next(&it, &tmp); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + n_queues++; + } + iter_destroy(&it); + } + + rc = iter_make(&it, sim->core.resmgr); + if (rc >= 0) { + for (;;) { + rc = iter_next(&it, &tmp); + if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) { + break; + } + n_resources++; + } + iter_destroy(&it); + } + } + + printf("[sim] summary: %d jobs (%d pending, %d running), %d workers " + "(%d online), %d queues, %d resources\n", + n_jobs, n_pending, n_running, n_workers, n_online, n_queues, + n_resources); +} + +/* + * repl_info_session - AC 22's "info session": "Show current simulation + * position: logical time (xce_core_now()), event index (Nth message + * processed), session count, last SessionSummary if available." The last- + * session dispatch/pend/preempt tally is this task's own stand-in for "last + * SessionSummary" -- no in-memory SessionSummary wire struct is decoded on + * the replay-read path (only written on the record path, + * recording_session.c), so this reports the same live decision tally + * repl_break.c's xce_repl_on_decision() already accumulates per session, + * strictly more current than a stale recorded summary would be for a + * replay in progress. + */ +static void repl_info_session(xce_sched_sim_driver_t* sim) +{ + xce_sched_repl_state_t* rs = sim->repl_state; + + printf("[sim] session: ts=%llu event_index=%llu sessions=%llu\n", + (unsigned long long)xce_core_now(&(sim->core)), + (unsigned long long)((rs != NULL) ? rs->event_index : 0), + (unsigned long long)((rs != NULL) ? rs->session_count : 0)); + + if ((rs != NULL) && (rs->session_count > 0)) { + printf("[sim] last session: round=%llu dispatch=%d pend=%d " + "preempt=%d\n", + (unsigned long long)rs->last_begin_round, rs->last_n_dispatch, + rs->last_n_pend, rs->last_n_preempt); + } else { + printf("[sim] last session: (none yet)\n"); + } +} + +/* repl_info_break - AC 22's "info break": "List all breakpoints with their + * IDs, event types, and filters." */ +static void repl_info_break(xce_sched_sim_driver_t* sim) +{ + xce_sched_repl_state_t* rs = sim->repl_state; + int i, j; + + if ((rs == NULL) || (rs->nbreakpoints == 0)) { + printf("[sim] no breakpoints set\n"); + return; + } + + printf("[sim] breakpoints:\n"); + for (i = 0; i < rs->nbreakpoints; i++) { + xce_repl_breakpoint_t* bp = rs->breakpoints[i]; + + printf("[sim] %d: %s", bp->id, bp->token); + for (j = 0; j < bp->nfilters; j++) { + printf(" %s=%s", bp->filters[j].key, bp->filters[j].value); + } + printf("\n"); + } +} + +/* + * repl_cmd_info - Req 5 AC 22's "info ..." family: dispatches on the second + * token to repl_info_break()/repl_info_session()/repl_info_job()/ + * repl_info_queue()/repl_info_worker()/repl_info_resource()/ + * repl_info_summary() (all above). No `info` subcommand ever hands control + * back to the driver loop -- inspecting state is never a reason to advance + * the simulation. repl.c's repl_dispatch() calls this (repl_internal.h's + * own extern declaration). + */ +bool repl_cmd_info(xce_sched_sim_driver_t* sim, const char* line) +{ + char tokens[XCE_REPL_MAX_TOKENS][XCE_REPL_TOKEN_LEN]; + int ntok = repl_split_tokens(line, tokens, XCE_REPL_MAX_TOKENS); + + if (ntok < 2) { + printf("[sim] info: usage: info " + " [arg]\n"); + return false; + } + + if (strcmp(tokens[1], "break") == 0) { + repl_info_break(sim); + } else if (strcmp(tokens[1], "session") == 0) { + repl_info_session(sim); + } else if (strcmp(tokens[1], "summary") == 0) { + repl_info_summary(sim); + } else if (strcmp(tokens[1], "job") == 0) { + if (ntok < 3) { + printf("[sim] info job: usage: info job \n"); + } else { + repl_info_job(sim, tokens[2]); + } + } else if (strcmp(tokens[1], "queue") == 0) { + if (ntok < 3) { + printf("[sim] info queue: usage: info queue |all\n"); + } else { + repl_info_queue(sim, tokens[2]); + } + } else if (strcmp(tokens[1], "worker") == 0) { + if (ntok < 3) { + printf("[sim] info worker: usage: info worker |all\n"); + } else { + repl_info_worker(sim, tokens[2]); + } + } else if (strcmp(tokens[1], "resource") == 0) { + if (ntok < 3) { + printf("[sim] info resource: usage: info resource |all\n"); + } else { + repl_info_resource(sim, tokens[2]); + } + } else { + printf("[sim] info: unrecognized subcommand '%s'\n", tokens[1]); + } + + return false; +} diff --git a/src/scheduler/src/repl_internal.h b/src/scheduler/src/repl_internal.h new file mode 100644 index 00000000..fd6efe2b --- /dev/null +++ b/src/scheduler/src/repl_internal.h @@ -0,0 +1,162 @@ +#if !defined(_REPL_INTERNAL_H_) + +#define _REPL_INTERNAL_H_ 1 + +#include "repl.h" + +/* + * repl_internal.h - private state and cross-file declarations shared by + * repl.c/repl_break.c/repl_info.c (scheduler-core spec, task 12.2). NOT + * installed, NOT included by anything outside src/scheduler/src/ -- the + * SAME "private header shared between sibling .c files, moved out of a + * single file that used to hold it alone" pattern src/lib/client_internal.h + * already establishes for client.c/client_failover.c (see that file's own + * header comment). + * + * Why a split at all: task 12.1's repl.c (execution control only) fit + * comfortably at 370/800 lines. Task 12.2's full scope -- breakpoints on 10 + * wire event kinds plus BEGIN_ROUND/DECISION, each with key=value filter + * matching, PLUS five separate `info` state-inspection dumps -- does not + * fit in one file under this project's 800-line-per-file cap + * (CLAUDE.md/coding-style.md). Split along the same domain boundaries + * task 8.1 already used for snapshot.c/snapshot_wire.c/snapshot_load.c + * (that file's own "Three-file split" header section is the precedent): + * + * - repl.c: the command loop itself (readline()/dispatch), execution + * control (`step`/`continue`/`quit`/`help`), and the two task-12.1 + * driver-facing entry points (xce_repl_start()/ + * xce_repl_post_session_check()). Unchanged in scope from task 12.1 + * except for three new repl_dispatch() branches that hand off to the + * other two files. + * - repl_break.c: breakpoint kind/filter matching, the breakpoint list + * (create/list/delete), and the three task-12.2 driver-facing hooks + * (xce_repl_on_event()/xce_repl_on_begin_round()/xce_repl_on_decision(), + * declared in repl.h, implemented here since they are breakpoint-domain + * logic through and through). + * - repl_info.c: the `info job`/`queue`/`worker`/`resource`/`summary`/ + * `session`/`break` state-inspection dumps. + * + * `struct xce_sched_repl_state_s` (the full definition of sim.h's opaque + * forward declaration) moves here from repl.c for the same reason: + * repl_break.c's breakpoint list and repl_info.c's `info session`/ + * `info break` both need direct field access, not just the opaque pointer + * sim_loop.c gets via sim.h. This does NOT weaken the "opaque outside the + * owning module" property that struct always had -- repl_internal.h is + * never included outside src/scheduler/src/repl*.c, so sim_loop.c and + * every other production consumer still see only the pointer (via sim.h), + * exactly as before this split. + */ + +typedef enum { + XCE_REPL_MODE_STEP = 0, /* pause once `step_budget` sessions complete */ + XCE_REPL_MODE_CONTINUE /* run to EOF or the next breakpoint hit */ +} xce_repl_run_mode_t; + +/* + * xce_repl_bp_kind_t (task 12.2, Req 5 AC 22 "Breakpoints") - the event + * types a `break` command may name. The ten *_PUSH, *_UPDATE, *_CHANGE, + * *_RUSAGE kinds map one-for-one onto core.h's xce_core_event_type_t + * (repl_break.c's repl_bp_kind_for_core_event() has the mapping, and + * documents why the token spelling sometimes differs, e.g. "JOB_PUSH" here + * vs. XCE_CORE_EVENT_JOBS_PUSH there -- matching requirements.md's own AC + * 22 example text verbatim). BEGIN_ROUND and DECISION are NOT + * xce_core_event_t members -- BEGIN_ROUND is the ReplaySession boundary + * itself, DECISION is a drained xce_decision_t -- each gets its own + * breakpoint-kind value and its own matching path (repl_break.c). + */ +typedef enum { + XCE_REPL_BP_CONFIG_PUSH = 1, + XCE_REPL_BP_JOB_PUSH, + XCE_REPL_BP_WORKERS_PUSH, + XCE_REPL_BP_RESOURCES_PUSH, + XCE_REPL_BP_RESVALS_FULL, + XCE_REPL_BP_RESVALS_DELTA, + XCE_REPL_BP_JOB_STATE_CHANGE, + XCE_REPL_BP_JOB_UPDATE, + XCE_REPL_BP_QUEUE_UPDATE, + XCE_REPL_BP_JOB_RUSAGE, + XCE_REPL_BP_BEGIN_ROUND, + XCE_REPL_BP_DECISION, +} xce_repl_bp_kind_t; + +/* xce_repl_filter_t - one AND'd `key=value` clause of a `break` command + * (task 12.2). Both strings are makeString()-interned xstrings (RTTI + * reference-counted, not plain malloc'd) -- never explicitly released, + * the same "makeString()'d fields live for the process's lifetime" + * precedent this codebase's own kill_sched_job()/kill_sched_queue() + * (schedobj.c) already establish for every other such field; see + * repl_break.c's repl_breakpoint_free() for the full rationale (a bare + * free() on one corrupts the heap allocator). */ +typedef struct { + char* key; + char* value; +} xce_repl_filter_t; + +/* xce_repl_breakpoint_t (task 12.2) - one entry set by `break`, listed by + * `info break`, removed by `delete`. `token` is the user-typed event-type + * text (e.g. "JOB_STATE_CHANGE"), kept verbatim for `info break`'s own + * listing rather than re-derived from `kind`, so listing output always + * echoes exactly what the operator typed. */ +typedef struct { + int id; + xce_repl_bp_kind_t kind; + char* token; + xce_repl_filter_t* filters; + int nfilters; +} xce_repl_breakpoint_t; + +/* + * xce_sched_repl_state_s - see this header's own "Why a split at all" + * section above for why the full definition lives here now instead of + * repl.c. Task 12.1 fields: mode/step_budget. Task 12.2 fields: + * `breakpoints`/`nbreakpoints`/`next_bp_id` back `break`/`info break`/ + * `delete` (a plain realloc'd array of pointers, same growth idiom + * sim_loop.c's own buf_events[] uses -- no new library dependency). + * `event_index`/`session_count`/`last_begin_round`/`last_n_dispatch`/ + * `last_n_pend`/`last_n_preempt` back `info session`'s position/summary + * reporting; updated by xce_repl_on_event()/xce_repl_on_begin_round()/ + * xce_repl_on_decision() (repl_break.c) as a side effect of the same + * per-event/per-session/per-decision call sim_loop.c already makes for + * breakpoint checking (repl.h's doc comment on those three functions + * explains why no separate hook site was needed for this bookkeeping). + */ +struct xce_sched_repl_state_s { + xce_repl_run_mode_t mode; + long step_budget; + xce_repl_breakpoint_t** breakpoints; + int nbreakpoints; + int next_bp_id; + uint64_t event_index; + uint64_t session_count; + uint64_t last_begin_round; + int last_n_dispatch; + int last_n_pend; + int last_n_preempt; +}; + +/* repl_split_tokens/XCE_REPL_MAX_TOKENS/XCE_REPL_TOKEN_LEN (repl.c) - + * shared whitespace tokenizer: `break`/`info`/`delete` (repl_break.c/ + * repl_info.c) all need more than repl_dispatch()'s own single-token + * `sscanf(" %31s")`. */ +#define XCE_REPL_MAX_TOKENS 16 +#define XCE_REPL_TOKEN_LEN 64 + +extern int repl_split_tokens(const char* line, + char tokens[][XCE_REPL_TOKEN_LEN], int max_tokens); + +/* repl_command_loop (repl.c) - the blocking prompt loop. Breakpoint hits + * (repl_break.c's xce_repl_on_event()/on_begin_round()/on_decision()) call + * back into it directly, the same re-entry xce_repl_post_session_check() + * (repl.c) already uses for a `step N` pause. */ +extern void repl_command_loop(xce_sched_sim_driver_t* sim); + +/* repl_cmd_break/repl_cmd_delete (repl_break.c) and repl_cmd_info + * (repl_info.c) - the three task-12.2 command handlers repl.c's + * repl_dispatch() hands off to. Same "returns false, re-prompt" contract + * repl_cmd_step()/repl_print_help() already follow (repl.c's own doc + * comment on repl_dispatch()). */ +extern bool repl_cmd_break(xce_sched_sim_driver_t* sim, const char* line); +extern bool repl_cmd_delete(xce_sched_sim_driver_t* sim, const char* line); +extern bool repl_cmd_info(xce_sched_sim_driver_t* sim, const char* line); + +#endif diff --git a/src/scheduler/src/sim_loop.c b/src/scheduler/src/sim_loop.c index 7814b385..79917216 100644 --- a/src/scheduler/src/sim_loop.c +++ b/src/scheduler/src/sim_loop.c @@ -20,41 +20,38 @@ * (sim.c/task 11.1 implements steps 1-4) and hosts sched_sim_driver_run() * itself -- the design.md-literal SimDriver entry point sim.h declares. * - * Task 11.5 (Req 5 AC 16/19, this revision) adds the --sim-alt-events event - * source switch (`event_src` in sched_sim_driver_run(), chosen once before - * the loop) and the --sim-output writer: header+snapshot (once, right - * after startup -- sim_record.c's sim_record_write_header_and_snapshot()), - * per-event pass-through (sim_record_write_event(), same file), and - * per-round ReplaySession accumulation (recording_session.c's - * scheduler_t-free sched_record_session_ctx_new()/_add_decision()/ - * _finalize(), externed directly -- no wrapper needed). sim_print_decision() - * remains this file's own unconditional Req 5 AC 19 default; it, --sim-output - * recording, and --sim-verify comparison are all independently gated per - * drained decision (AC 19 does not make them mutually exclusive). Task 11.4 - * owns comparing each drained decision against the recorded - * ReplaySession.decisions when --sim-verify is set -- see the ReplaySession - * branch below and sim_verify.c (sibling file) for that comparison logic. + * Task 11.5 (Req 5 AC 16/19) adds the --sim-alt-events event source switch + * (`event_src`, chosen once before the loop) and the --sim-output writer: + * header+snapshot (once, right after startup -- sim_record.c's + * sim_record_write_header_and_snapshot()), per-event pass-through + * (sim_record_write_event()), and per-round ReplaySession accumulation + * (recording_session.c's scheduler_t-free sched_record_session_ctx_new()/ + * _add_decision()/_finalize(), externed directly). sim_print_decision() + * remains this file's own unconditional Req 5 AC 19 default; it, + * --sim-output recording, and --sim-verify comparison are independently + * gated per drained decision. Task 11.4 owns comparing each drained + * decision against the recorded ReplaySession.decisions when --sim-verify + * is set -- see the ReplaySession branch below and sim_verify.c. * - * Picker-driving thread (sim_picker_thread_fn, below): Req 5 AC 18's - * pseudocode calls `core_go()` then blocks draining `core->decisions` until - * the end-of-session NULL marker -- exactly like the normal-mode driver's - * own protocol (core_session.c's doc comments on xce_core_go()/ - * xce_core_end_session()), which only works because sched_job_picker()/ - * sched_allocator() (scheduler.c) are two real, persistent threads started - * once at sched_init() and living for the scheduler's whole lifetime. The - * simulation driver (xce_sched_sim_driver_t, sim.h) deliberately has no - * xce_scheduler_t* (Req 5 AC 17) and therefore cannot use either of those - * xce_scheduler_t*-typed thread entry points -- without SOME equivalent - * thread, the very first ReplaySession boundary in any fixture would call - * xce_core_go() and then block forever on cbuf_get(&core->decisions), since - * nothing would ever produce the closing NULL marker. sim_picker_thread_fn() - * below is that equivalent: core-scoped (xce_sched_core_t*, not - * xce_scheduler_t*), spawned once here, living for the simulation driver's - * whole run -- see that function's own doc comment for why it also stands - * in for the allocator role (no separate thread/jbuf hand-off), and for the - * documented, out-of-this-task's-scope reason every candidate job it sees - * is conservatively PENDed rather than DISPATCHed with a fabricated - * allocation. + * Task 12.2 (Req 5 AC 22 "Breakpoints") adds three bare hook calls below -- + * xce_repl_check_event()/xce_repl_check_begin_round()/xce_repl_check_decision() + * (repl.h) -- at the three points design.md's REPL integration pseudocode + * requires: per decoded event (before it joins the accumulated batch), per + * ReplaySession boundary (before the batch is pushed), and per drained + * decision (after sim_print_decision()). Each is a single call expression, + * no branching -- the `sim->interactive` gate and all breakpoint-matching + * logic live in repl.h/repl_break.c, this file having zero line-budget + * headroom for either (see repl_internal.h's own "Why a split at all"). + * + * Picker-driving thread (sim_picker_thread_fn, below): stands in for BOTH + * the normal-mode picker AND allocator threads (scheduler.c), which the + * simulation driver cannot use directly (no xce_scheduler_t*, Req 5 AC 17) + * -- without it, the first ReplaySession would block forever on + * cbuf_get(&core->decisions) with nothing to ever produce the closing NULL + * marker. Conservatively PENDs every candidate job (real resource-matching + * allocation belongs to the not-yet-designed scheduler-allocation spec) via + * the real on_pend()/make_pend_decision()/make_pendreason() production + * calls -- see that function's own doc comment for the full reasoning. */ /* Shared with sim.c (task 11.1) -- see that file's own file header comment @@ -595,6 +592,8 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim, } if (translated > 0) { + xce_repl_check_event(sim, &ev); + if (buf_n == buf_cap) { int newcap = buf_cap ? (buf_cap * 2) : 16; xce_core_event_t* tmp = (xce_core_event_t*)realloc( @@ -630,6 +629,7 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim, sp.p = root; begin_round = ReplaySession_get_beginRound(sp); + xce_repl_check_begin_round(sim, begin_round); /* Req 5 AC 15/18 (task 11.4): decode the FULL session -- * including its `decisions` list -- before the capnp arena is @@ -722,6 +722,7 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim, record_ctx, xce_core_now(&(sim->core)), d); } sim_print_decision(d); + xce_repl_check_decision(sim, d); } if (sim->verify && (decoded_session != NULL) diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 1c125816..b2340651 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -551,6 +551,15 @@ sched_sim_isolation_test = executable( '-DXCE_SIM_REPL_H_PATH="' + (meson.current_source_dir() / '..' / 'include' / 'repl.h') + '"', + '-DXCE_SIM_REPL_BREAK_C_PATH="' + + (meson.current_source_dir() / '..' / 'src' / 'repl_break.c') + + '"', + '-DXCE_SIM_REPL_INFO_C_PATH="' + + (meson.current_source_dir() / '..' / 'src' / 'repl_info.c') + + '"', + '-DXCE_SIM_REPL_INTERNAL_H_PATH="' + + (meson.current_source_dir() / '..' / 'src' / 'repl_internal.h') + + '"', '-DXCE_SIM_LIB_PATH="' + sched_sim_isolation_lib.full_path() + '"'], @@ -660,7 +669,13 @@ sched_sim_equivalence_test = executable( # (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) PLUS -# the new ../src/repl.c under test. Needs libedit_dep (parent meson.build) +# ../src/repl.c under test. Also pulls in ../src/repl_break.c/repl_info.c +# (task 12.2's 800-line-cap split of repl.c -- repl_internal.h's own "Why a +# split at all") -- required at link time since sim_loop.c's new breakpoint +# hooks (repl.h) reference symbols repl_break.c defines, even though this +# file's own tests exercise only the execution-control slice; task 12.2's +# own breakpoint/inspection tests live in the sibling +# test_repl_breakpoints_inspection.c. Needs libedit_dep (parent meson.build) # at link time -- repl.c's real readline()/add_history() calls are pulled # into this TU by the white-box #include. sched_repl_execution_control_test = executable( @@ -670,6 +685,29 @@ sched_repl_execution_control_test = executable( 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 +# event type and field value pauses the REPL exactly when a matching event +# is about to be processed, and each `info` command reports state matching +# a snapshot dump of the same moment. Same white-box #include pattern as +# sched_repl_execution_control_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_break.c, +# repl_info.c). Split into its own file rather than appended to +# test_repl_execution_control.c -- that file was already 644 lines after +# task 12.1, and this task's own fixture/assertion surface (breakpoints on +# multiple event kinds plus five separate `info` dumps) does not fit +# alongside it under this project's 800-line cap. +sched_repl_breakpoints_inspection_test = executable( + 'sched_repl_breakpoints_inspection_test', + ['test_repl_breakpoints_inspection.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps, libedit_dep], + install: false) + # Register as meson tests so `meson test --suite scheduler` runs them all. # detect_leaks=0 matches the suite convention (fixtures are not torn down). test('scheduler test', scheduler_test, suite: 'scheduler', @@ -763,3 +801,6 @@ test('scheduler sim equivalence test', test('scheduler repl execution control test', sched_repl_execution_control_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_breakpoints_inspection.c b/src/scheduler/testsuite/test_repl_breakpoints_inspection.c new file mode 100644 index 00000000..e12c0a50 --- /dev/null +++ b/src/scheduler/testsuite/test_repl_breakpoints_inspection.c @@ -0,0 +1,1063 @@ +/* + * test_repl_breakpoints_inspection.c - contract test for the interactive + * simulation REPL's breakpoint and state-inspection slice (scheduler-core + * spec, task 12.2, Req 5 AC 22). + * + * Observables under test (task 12.2's own text): + * (a) setting a breakpoint on a specific event type and field value + * pauses the REPL exactly when a matching event is about to be + * processed; + * (b) each inspection command reports state matching what a snapshot + * dump of the same moment would show. + * + * Four groups, at increasing granularity (same "unit-level precision PLUS + * integration-level end-to-end proof" split test_repl_execution_control.c's + * own header comment establishes for task 12.1): + * + * Group A (unit, white-box): `break`/`delete`/`info break` command + * parsing and listing, driven via repl_dispatch() directly, asserting on + * sim.repl_state->breakpoints[]'s own fields and captured stdout. + * + * Group B (unit, white-box): the pure breakpoint-matching predicates + * (repl_find_event_breakpoint()/repl_find_decision_breakpoint()/ + * repl_find_begin_round_breakpoint(), repl_break.c) driven directly + * against hand-built xce_core_event_t/xce_decision_t values -- the exact + * "not a decoy, the real predicate" proof test_repl_execution_control.c's + * own Test 1 (repl_should_pause()) already establishes for task 12.1. + * + * Group C (unit, white-box): state-inspection dumps (`info job`/`queue`/ + * `summary`/`session`) driven via repl_dispatch() against a REAL + * xce_sched_core_t populated through a genuine xce_core_push_events() + * call (same in-memory QUEUE_UPDATE/JOBS_PUSH xce_core_event_t + * construction test_core_events.c's own fixtures already establish -- no + * capnp file needed for this granularity), captured via stdout and + * asserted against the exact field values pushed in. `info worker`/ + * `info resource` share the identical iter_make()/iter_next()/printf + * code shape as `info queue` (same file, repl_info.c) and are not + * separately re-proven here -- a deliberate, documented scope decision, + * not an oversight. + * + * Group D (integration, single-process, scripted stdin): a REAL + * sched_sim_driver_run() against a real `.replay` fixture, proving + * Observable (a) end to end for both an event breakpoint + * (JOB_STATE_CHANGE jobid=) and a decision breakpoint (DECISION type=), + * by capturing stdout and asserting ordering the same way + * test_repl_execution_control.c's own Test 2 does for the pre-replay + * pause. + * + * White-box #include set: the SAME TU set test_repl_execution_control.c + * establishes for task 12.1, PLUS the new ../src/repl_break.c/repl_info.c + * under test (task 12.2's own 800-line-cap split of repl.c -- see + * repl_internal.h's own "Why a split at all" section). + */ +#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_execution_control.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_break.c" +#include "../src/repl_info.c" +#include "../src/sim.c" +#include "../src/sim_loop.c" +#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; } + +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; +} + +/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same + * copy test_repl_execution_control.c/test_sim_alt_events.c/etc. each carry + * (schedobj.c is not linked here). */ +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 -- Group C: in-memory core-state construction (no capnp, */ +/* same technique test_core_events.c's own make_job()/fixture_init() */ +/* establish). */ +/* ---------------------------------------------------------------------- */ + +static xce_job_t* make_job(uint64_t jobid, uint32_t state, char* queue, + char* user, char* project) +{ + xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j)); + + j->jobid = jobid; + j->state = state; + j->sub_time = 1000; + j->queue_time = 2000; + j->start_time = 0; + j->submit = (job_submit_data_t*)calloc(1, sizeof(*(j->submit))); + j->submit->queue = queue; + j->submit->user = user; + j->submit->project_name = project; + j->submit->resreq = "type == any"; + j->submit->mintasks = 1; + j->submit->maxtasks = 1; + + return j; +} + +/* Builds a real xce_sched_core_t (no picker plugin registered -- core + * state mutation does not depend on one, core_events.c's own + * xce_core_push_events() doc comment) with one queue ("normal", priority + * 5, open) and one job (jobid, PEND, in "normal") pushed through a + * genuine xce_core_push_events() call. */ +static void build_fixture_core(xce_sched_core_t* core, uint64_t jobid) +{ + xce_core_event_t events[2]; + xce_queue_t wire_queue; + xce_job_t* job1; + int rc; + + rc = xce_sched_core_init(core); + TEST_ASSERT_EQUAL_INT(0, rc); + + memset(&wire_queue, 0, sizeof(wire_queue)); + wire_queue.queue = "normal"; + wire_queue.priority = 5; + wire_queue.status = QUEUE_STAT_OPEN; + + job1 = make_job(jobid, JOB_STAT_PEND, "normal", "alice", "proj1"); + + memset(events, 0, sizeof(events)); + events[0].type = XCE_CORE_EVENT_QUEUE_UPDATE; + events[0].u.queue_update.queue = &wire_queue; + events[1].type = XCE_CORE_EVENT_JOBS_PUSH; + events[1].u.jobs_push.njobs = 1; + events[1].u.jobs_push.jobs = &job1; + + rc = xce_core_push_events(core, events, 2); + TEST_ASSERT_EQUAL_INT(0, rc); +} + +/* Same dup()/dup2()-around-STDOUT_FILENO capture technique + * test_repl_execution_control.c's own capture_stdout_around() establishes. */ +static char* capture_stdout_around(void (*fn)(void)) +{ + char tmp[] = "/tmp/xce_replbpi_stdoutcap_XXXXXX"; + int fd = mkstemp(tmp); + int saved_stdout; + off_t len; + char* buf; + + TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "mkstemp for stdout capture must " + "succeed"); + + fflush(stdout); + saved_stdout = dup(STDOUT_FILENO); + TEST_ASSERT_TRUE_MESSAGE(saved_stdout >= 0, "dup(STDOUT_FILENO) must " + "succeed"); + TEST_ASSERT_TRUE_MESSAGE(dup2(fd, STDOUT_FILENO) >= 0, + "dup2 redirect of STDOUT_FILENO must succeed"); + + fn(); + + fflush(stdout); + dup2(saved_stdout, STDOUT_FILENO); + close(saved_stdout); + + len = lseek(fd, 0, SEEK_CUR); + TEST_ASSERT_TRUE(len >= 0); + + buf = (char*)calloc(1, (size_t)len + 1); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0); + if (len > 0) { + ssize_t nread = read(fd, buf, (size_t)len); + TEST_ASSERT_TRUE(nread == len); + } + close(fd); + unlink(tmp); + + return buf; +} + +/* Captures the output of dispatching one REPL command line against `sim` + * -- used by both Group A (break/delete/info break) and Group C (info + * job/queue/summary/session). Never a `step`/`continue`/`quit` line (this + * helper does not expect repl_dispatch() to return true). */ +static xce_sched_sim_driver_t* g_dispatch_sim; +static const char* g_dispatch_line; + +static void run_dispatch_for_capture(void) +{ + (void)repl_dispatch(g_dispatch_sim, g_dispatch_line); +} + +static char* capture_dispatch(xce_sched_sim_driver_t* sim, const char* line) +{ + g_dispatch_sim = sim; + g_dispatch_line = line; + return capture_stdout_around(run_dispatch_for_capture); +} + +/* ---------------------------------------------------------------------- */ +/* Group A: `break`/`delete`/`info break` command parsing and listing. */ +/* ---------------------------------------------------------------------- */ + +static void test_break_creates_breakpoint_with_filters(void) +{ + xce_sched_sim_driver_t sim; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + + TEST_ASSERT_FALSE(repl_dispatch(&sim, "break JOB_STATE_CHANGE jobid=12345")); + + TEST_ASSERT_EQUAL_INT(1, sim.repl_state->nbreakpoints); + TEST_ASSERT_EQUAL_INT(XCE_REPL_BP_JOB_STATE_CHANGE, + sim.repl_state->breakpoints[0]->kind); + TEST_ASSERT_EQUAL_INT(1, sim.repl_state->breakpoints[0]->nfilters); + TEST_ASSERT_EQUAL_STRING("jobid", + sim.repl_state->breakpoints[0]->filters[0].key); + TEST_ASSERT_EQUAL_STRING("12345", + sim.repl_state->breakpoints[0]->filters[0].value); + + printf("test_break_creates_breakpoint_with_filters: PASS\n"); +} + +static void test_break_unrecognized_event_type_rejected(void) +{ + xce_sched_sim_driver_t sim; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + + TEST_ASSERT_FALSE(repl_dispatch(&sim, "break BOGUS_EVENT_TYPE")); + TEST_ASSERT_EQUAL_INT(0, sim.repl_state->nbreakpoints); + + printf("test_break_unrecognized_event_type_rejected: PASS\n"); +} + +static void test_break_malformed_filter_rejected(void) +{ + xce_sched_sim_driver_t sim; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + + /* "nofiltermark" has no '=' -- the whole command must be rejected, no + * partially-applied breakpoint. */ + TEST_ASSERT_FALSE(repl_dispatch(&sim, "break JOB_PUSH nofiltermark")); + TEST_ASSERT_EQUAL_INT(0, sim.repl_state->nbreakpoints); + + printf("test_break_malformed_filter_rejected: PASS\n"); +} + +static void test_delete_by_id_removes_one_breakpoint(void) +{ + xce_sched_sim_driver_t sim; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + + TEST_ASSERT_FALSE(repl_dispatch(&sim, "break JOB_PUSH")); + TEST_ASSERT_FALSE(repl_dispatch(&sim, "break BEGIN_ROUND")); + TEST_ASSERT_EQUAL_INT(2, sim.repl_state->nbreakpoints); + + TEST_ASSERT_FALSE(repl_dispatch(&sim, "delete 1")); + TEST_ASSERT_EQUAL_INT(1, sim.repl_state->nbreakpoints); + TEST_ASSERT_EQUAL_INT(2, sim.repl_state->breakpoints[0]->id); + + printf("test_delete_by_id_removes_one_breakpoint: PASS\n"); +} + +static void test_delete_all_removes_all_breakpoints(void) +{ + xce_sched_sim_driver_t sim; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + + TEST_ASSERT_FALSE(repl_dispatch(&sim, "break JOB_PUSH")); + TEST_ASSERT_FALSE(repl_dispatch(&sim, "break BEGIN_ROUND")); + TEST_ASSERT_EQUAL_INT(2, sim.repl_state->nbreakpoints); + + TEST_ASSERT_FALSE(repl_dispatch(&sim, "delete all")); + TEST_ASSERT_EQUAL_INT(0, sim.repl_state->nbreakpoints); + + printf("test_delete_all_removes_all_breakpoints: PASS\n"); +} + +static void test_info_break_lists_breakpoints_with_filters(void) +{ + xce_sched_sim_driver_t sim; + char* captured; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + + TEST_ASSERT_FALSE( + repl_dispatch(&sim, "break DECISION type=DISPATCH queue=normal")); + + captured = capture_dispatch(&sim, "info break"); + + TEST_ASSERT_NOT_NULL(strstr(captured, "DECISION")); + TEST_ASSERT_NOT_NULL(strstr(captured, "type=DISPATCH")); + TEST_ASSERT_NOT_NULL(strstr(captured, "queue=normal")); + + free(captured); + + printf("test_info_break_lists_breakpoints_with_filters: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Group B: pure breakpoint-matching predicates (repl_break.c), the */ +/* precise "not N-1, not N+1"-grade proof, same genre */ +/* test_repl_execution_control.c's own repl_should_pause() unit test. */ +/* ---------------------------------------------------------------------- */ + +static void test_event_breakpoint_matches_jobid_filter(void) +{ + xce_sched_sim_driver_t sim; + xce_core_event_t ev; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + TEST_ASSERT_FALSE( + repl_dispatch(&sim, "break JOB_STATE_CHANGE jobid=42")); + + memset(&ev, 0, sizeof(ev)); + ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE; + ev.u.job_state_change.jobid = 42; + ev.u.job_state_change.new_state = JOB_STAT_RUN; + + TEST_ASSERT_NOT_NULL_MESSAGE( + repl_find_event_breakpoint(sim.repl_state, &ev), + "jobid=42 must match an event whose own jobid is 42"); + + ev.u.job_state_change.jobid = 99; + TEST_ASSERT_NULL_MESSAGE( + repl_find_event_breakpoint(sim.repl_state, &ev), + "jobid=42 must NOT match an event whose own jobid is 99"); + + printf("test_event_breakpoint_matches_jobid_filter: PASS\n"); +} + +static void test_event_breakpoint_matches_state_filter(void) +{ + xce_sched_sim_driver_t sim; + xce_core_event_t ev; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + TEST_ASSERT_FALSE( + repl_dispatch(&sim, "break JOB_STATE_CHANGE state=RUN")); + + memset(&ev, 0, sizeof(ev)); + ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE; + ev.u.job_state_change.jobid = 1; + ev.u.job_state_change.new_state = JOB_STAT_RUN; + + TEST_ASSERT_NOT_NULL_MESSAGE( + repl_find_event_breakpoint(sim.repl_state, &ev), + "state=RUN must match a JOB_STATE_CHANGE whose new_state is RUN"); + + ev.u.job_state_change.new_state = JOB_STAT_PEND; + TEST_ASSERT_NULL_MESSAGE( + repl_find_event_breakpoint(sim.repl_state, &ev), + "state=RUN must NOT match a JOB_STATE_CHANGE whose new_state is " + "PEND"); + + printf("test_event_breakpoint_matches_state_filter: PASS\n"); +} + +static void test_decision_breakpoint_matches_type_filter(void) +{ + xce_sched_sim_driver_t sim; + xce_pendreason_t reason; + xce_decision_t d_pend; + xce_decision_t d_dispatch; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + TEST_ASSERT_FALSE(repl_dispatch(&sim, "break DECISION type=PEND")); + + memset(&reason, 0, sizeof(reason)); + reason.jobid = 7; + + memset(&d_pend, 0, sizeof(d_pend)); + d_pend.op = XCE_DECISION_PEND; + d_pend.u.reason = &reason; + + memset(&d_dispatch, 0, sizeof(d_dispatch)); + d_dispatch.op = XCE_DECISION_DISPATCH; + + TEST_ASSERT_NOT_NULL_MESSAGE( + repl_find_decision_breakpoint(&(sim.core), sim.repl_state, &d_pend), + "type=PEND must match a PEND decision"); + TEST_ASSERT_NULL_MESSAGE( + repl_find_decision_breakpoint(&(sim.core), sim.repl_state, + &d_dispatch), + "type=PEND must NOT match a DISPATCH decision (u.alloc is NULL " + "here, decision_jobid() must not dereference it)"); + + printf("test_decision_breakpoint_matches_type_filter: PASS\n"); +} + +static void test_begin_round_breakpoint_bare_matches_filtered_does_not(void) +{ + xce_sched_sim_driver_t sim; + + memset(&sim, 0, sizeof(sim)); + /* xce_sched_core_init() is the same precondition xce_repl_start() + * always has satisfied for it in the real driver flow (sched_sim_driver_start() + * runs first) -- it also transitively calls xstring_table_init() + * (core.c), which makeString() (breakpoint token/filter string copies, + * repl_break.c) requires to not silently return NULL. */ + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core))); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + TEST_ASSERT_FALSE(repl_dispatch(&sim, "break BEGIN_ROUND")); + + TEST_ASSERT_NOT_NULL_MESSAGE( + repl_find_begin_round_breakpoint(sim.repl_state), + "a bare 'break BEGIN_ROUND' (no filters) must match every session " + "boundary"); + + /* Hand-attach a filter this codebase's `break` command never lets an + * operator type for BEGIN_ROUND (AC 22 shows no BEGIN_ROUND filter + * example) -- proves the fail-closed contract directly, matching + * repl_break.c's own doc comment on repl_find_begin_round_breakpoint(). */ + sim.repl_state->breakpoints[0]->nfilters = 1; + TEST_ASSERT_NULL_MESSAGE( + repl_find_begin_round_breakpoint(sim.repl_state), + "a BEGIN_ROUND breakpoint carrying any filter must never match " + "(fail closed)"); + + printf("test_begin_round_breakpoint_bare_matches_filtered_does_not: " + "PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Group C: state-inspection dumps against a REAL xce_sched_core_t. */ +/* ---------------------------------------------------------------------- */ + +static void test_info_job_reports_state_queue_priority(void) +{ + xce_sched_sim_driver_t sim; + char* captured; + + memset(&sim, 0, sizeof(sim)); + build_fixture_core(&(sim.core), 777); + + captured = capture_dispatch(&sim, "info job 777"); + + TEST_ASSERT_NOT_NULL(strstr(captured, "job 777")); + TEST_ASSERT_NOT_NULL(strstr(captured, "state=PEND")); + TEST_ASSERT_NOT_NULL(strstr(captured, "queue=normal")); + TEST_ASSERT_NOT_NULL(strstr(captured, "queue_time=2000")); + + /* resreq= must carry format_resreq()'s REAL formatted text (the same + * print-form build_wire_job()/snapshot_wire.c already calls for a + * restored job's wire resreq), not a "set"/"(none)" presence flag -- + * this is the review remediation for task 12.2's `info job`. make_job() + * above gives this job's wire submit->resreq = "type == any", which + * core_build_job() (core_events.c) parses into job->resreq via + * parse_resreq() against build_fixture_core()'s empty core->resmgr (no + * "type" resource registered); print_primary_resreq() (resreq.c) always + * emits a "Primary:"/"Tasks: min = %d, max = %d"/"Rusages:" skeleton for + * any parsed primary resreq regardless of select-clause content (the + * select tree itself has no print-form yet -- resreq.c's own + * print_primary_resreq() leaves that "Add later"), so this exact + * skeleton is the real, deterministic format_resreq() output for this + * fixture -- not a placeholder standing in for it. */ + TEST_ASSERT_NOT_NULL(strstr(captured, "resreq=Primary:")); + TEST_ASSERT_NOT_NULL(strstr(captured, "Tasks: min = 0, max = 0")); + TEST_ASSERT_NOT_NULL(strstr(captured, "Rusages:")); + TEST_ASSERT_NULL(strstr(captured, "resreq=set")); + TEST_ASSERT_NULL(strstr(captured, "resreq=(none)")); + + free(captured); + + printf("test_info_job_reports_state_queue_priority: PASS\n"); +} + +static void test_info_job_unknown_reports_not_found(void) +{ + xce_sched_sim_driver_t sim; + char* captured; + + memset(&sim, 0, sizeof(sim)); + build_fixture_core(&(sim.core), 777); + + captured = capture_dispatch(&sim, "info job 999999"); + + TEST_ASSERT_NOT_NULL(strstr(captured, "no such job")); + + free(captured); + + printf("test_info_job_unknown_reports_not_found: PASS\n"); +} + +static void test_info_queue_reports_status_and_queued_jobs(void) +{ + xce_sched_sim_driver_t sim; + char* captured; + + memset(&sim, 0, sizeof(sim)); + build_fixture_core(&(sim.core), 4242); + + captured = capture_dispatch(&sim, "info queue normal"); + + TEST_ASSERT_NOT_NULL(strstr(captured, "queue normal")); + TEST_ASSERT_NOT_NULL(strstr(captured, "priority=5")); + TEST_ASSERT_NOT_NULL(strstr(captured, "status=open")); + TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, "jobid=4242"), + "the fixture's own PEND job must appear " + "in the queue's queued-job listing"); + + free(captured); + + printf("test_info_queue_reports_status_and_queued_jobs: PASS\n"); +} + +static void test_info_summary_reports_counts(void) +{ + xce_sched_sim_driver_t sim; + char* captured; + + memset(&sim, 0, sizeof(sim)); + build_fixture_core(&(sim.core), 555); + + captured = capture_dispatch(&sim, "info summary"); + + TEST_ASSERT_NOT_NULL(strstr(captured, "1 jobs (1 pending, 0 running)")); + TEST_ASSERT_NOT_NULL(strstr(captured, "0 workers (0 online)")); + TEST_ASSERT_NOT_NULL(strstr(captured, "1 queues")); + TEST_ASSERT_NOT_NULL(strstr(captured, "0 resources")); + + free(captured); + + printf("test_info_summary_reports_counts: PASS\n"); +} + +static void test_info_session_reports_position_after_synthetic_events(void) +{ + xce_sched_sim_driver_t sim; + xce_core_event_t ev; + char* captured; + + memset(&sim, 0, sizeof(sim)); + build_fixture_core(&(sim.core), 1); + sim.repl_state = repl_state_ensure(&sim); + TEST_ASSERT_NOT_NULL(sim.repl_state); + + /* No breakpoints registered -- xce_repl_on_event()/on_begin_round() + * only update position counters here, never block on stdin. */ + memset(&ev, 0, sizeof(ev)); + ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE; + ev.u.job_state_change.jobid = 1; + xce_repl_on_event(&sim, &ev); + xce_repl_on_event(&sim, &ev); + xce_repl_on_begin_round(&sim, 9000ULL); + + captured = capture_dispatch(&sim, "info session"); + + TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, "event_index=2"), + "two xce_repl_on_event() calls must " + "advance event_index to exactly 2"); + TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, "sessions=1"), + "one xce_repl_on_begin_round() call must " + "advance session_count to exactly 1"); + TEST_ASSERT_NOT_NULL(strstr(captured, "round=9000")); + + free(captured); + + printf("test_info_session_reports_position_after_synthetic_events: " + "PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Group D: full-integration pause proof (scripted stdin + capture, same */ +/* technique test_repl_execution_control.c's own Test 2 establishes). */ +/* ---------------------------------------------------------------------- */ + +static void make_tmp_dir(char* out, size_t out_len, const char* suffix) +{ + char tmpl[PATH_MAX]; + + snprintf(tmpl, sizeof(tmpl), "/tmp/sched_replbpi%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); +} + +static void 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); +} + +/* jobStateChange payload used purely as a cheap, already-proven-safe + * event shape (a plain value struct, no nested pointers) -- same technique + * and same rationale test_sim_event_loop.c's own + * append_jobstatechange_event() establishes: core_events.c WARN+skips an + * unknown jobid rather than fail, so neither breakpoint test below needs a + * real job to exist in the loaded snapshot. */ +static void append_jobstatechange_event(FILE* f, uint64_t ts, uint64_t jobid) +{ + job_state_change_event_t change; + replay_event_t src; + struct capn c; + struct capn_segment* cs; + ReplayEvent_ptr p; + + memset(&change, 0, sizeof(change)); + change.jobid = jobid; + change.old_state = JOB_STAT_PEND; + change.new_state = JOB_STAT_RUN; + change.exit_code = 0; + + memset(&src, 0, sizeof(src)); + src.ts = ts; + src.body_which = ReplayEvent_body_jobStateChange; + src.body.job_state_change = &change; + + 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 xce_sched_sim_driver_t g_d_sim; +static sched_sim_args_t g_d_args; +static int g_d_rc; + +static void run_driver_for_group_d_capture(void) +{ + g_d_rc = sched_sim_driver_run(&g_d_sim, &g_d_args); +} + +static void test_event_breakpoint_pauses_repl_at_matching_event(void) +{ + char replay_path[PATH_MAX]; + char script_dir[PATH_MAX]; + char script_path[PATH_MAX]; + FILE* f; + char* captured; + char* bp_pos; + char* pend_pos; + + f = write_header_and_snapshot(replay_path, sizeof(replay_path), + "_evbp"); + TEST_ASSERT_NOT_NULL(f); + + append_queue_update_event(f, 1000ULL, "normal"); + append_jobs_push_event(f, 1500ULL, 4242ULL, "normal"); + append_jobstatechange_event(f, 1600ULL, 111ULL); /* event #3: no match */ + append_jobstatechange_event(f, 1700ULL, 222ULL); /* event #4: MATCH */ + append_replay_session(f, 2000ULL); + append_end_of_stream_marker(f); + fclose(f); + + make_tmp_dir(script_dir, sizeof(script_dir), "_evbpscript"); + snprintf(script_path, sizeof(script_path), "%s/script.txt", script_dir); + write_text_file(script_path, + "break JOB_STATE_CHANGE jobid=222\ncontinue\ncontinue\n"); + + TEST_ASSERT_NOT_NULL(freopen(script_path, "r", stdin)); + + memset(&g_d_sim, 0, sizeof(g_d_sim)); + memset(&g_d_args, 0, sizeof(g_d_args)); + g_d_args.replay_path = replay_path; + g_d_args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + g_d_args.sim_speed = 0; + g_d_args.interactive = true; + + captured = capture_stdout_around(run_driver_for_group_d_capture); + + TEST_ASSERT_EQUAL_INT(0, g_d_rc); + + bp_pos = strstr(captured, "hit breakpoint 1 @ event #4: JOB_STATE_CHANGE"); + pend_pos = strstr(captured, "PEND jobid=4242"); + + TEST_ASSERT_NOT_NULL_MESSAGE( + bp_pos, "the breakpoint must fire exactly at event #4 (jobid=222), " + "not event #3 (jobid=111)"); + TEST_ASSERT_NULL_MESSAGE( + strstr(captured, "@ event #3"), + "the breakpoint must NOT have fired on event #3 (jobid=111)"); + TEST_ASSERT_NOT_NULL_MESSAGE(pend_pos, + "the session's own PEND decision must " + "still have run after the resumed " + "breakpoint pause"); + TEST_ASSERT_TRUE_MESSAGE(bp_pos < pend_pos, + "the event breakpoint (mid-batch, before " + "push_events) must be reported BEFORE the " + "session's own decision output"); + + free(captured); + + printf("test_event_breakpoint_pauses_repl_at_matching_event: PASS\n"); +} + +static void test_decision_breakpoint_pauses_after_decision_output(void) +{ + char replay_path[PATH_MAX]; + char script_dir[PATH_MAX]; + char script_path[PATH_MAX]; + FILE* f; + char* captured; + char* pend_pos; + char* bp_pos; + + f = write_header_and_snapshot(replay_path, sizeof(replay_path), + "_decbp"); + TEST_ASSERT_NOT_NULL(f); + + append_queue_update_event(f, 1000ULL, "normal"); + append_jobs_push_event(f, 1500ULL, 555ULL, "normal"); + append_replay_session(f, 2000ULL); + append_end_of_stream_marker(f); + fclose(f); + + make_tmp_dir(script_dir, sizeof(script_dir), "_decbpscript"); + snprintf(script_path, sizeof(script_path), "%s/script.txt", script_dir); + write_text_file(script_path, "break DECISION type=PEND\ncontinue\ncontinue\n"); + + TEST_ASSERT_NOT_NULL(freopen(script_path, "r", stdin)); + + memset(&g_d_sim, 0, sizeof(g_d_sim)); + memset(&g_d_args, 0, sizeof(g_d_args)); + g_d_args.replay_path = replay_path; + g_d_args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + g_d_args.sim_speed = 0; + g_d_args.interactive = true; + + captured = capture_stdout_around(run_driver_for_group_d_capture); + + TEST_ASSERT_EQUAL_INT(0, g_d_rc); + + pend_pos = strstr(captured, "PEND jobid=555"); + bp_pos = strstr(captured, "hit breakpoint 1 @ decision: PEND jobid=555"); + + TEST_ASSERT_NOT_NULL_MESSAGE(pend_pos, "the fixture's own PEND decision " + "must have run"); + TEST_ASSERT_NOT_NULL_MESSAGE(bp_pos, "the decision breakpoint must " + "have fired for that decision"); + TEST_ASSERT_TRUE_MESSAGE(pend_pos < bp_pos, + "the decision breakpoint check happens AFTER " + "sim_print_decision() (AC 22's 'after " + "verify/output-write' ordering)"); + + free(captured); + + printf("test_decision_breakpoint_pauses_after_decision_output: PASS\n"); +} + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_break_creates_breakpoint_with_filters); + RUN_TEST(test_break_unrecognized_event_type_rejected); + RUN_TEST(test_break_malformed_filter_rejected); + RUN_TEST(test_delete_by_id_removes_one_breakpoint); + RUN_TEST(test_delete_all_removes_all_breakpoints); + RUN_TEST(test_info_break_lists_breakpoints_with_filters); + + RUN_TEST(test_event_breakpoint_matches_jobid_filter); + RUN_TEST(test_event_breakpoint_matches_state_filter); + RUN_TEST(test_decision_breakpoint_matches_type_filter); + RUN_TEST(test_begin_round_breakpoint_bare_matches_filtered_does_not); + + RUN_TEST(test_info_job_reports_state_queue_priority); + RUN_TEST(test_info_job_unknown_reports_not_found); + RUN_TEST(test_info_queue_reports_status_and_queued_jobs); + RUN_TEST(test_info_summary_reports_counts); + RUN_TEST(test_info_session_reports_position_after_synthetic_events); + + RUN_TEST(test_event_breakpoint_pauses_repl_at_matching_event); + RUN_TEST(test_decision_breakpoint_pauses_after_decision_output); + + return UNITY_END(); +} diff --git a/src/scheduler/testsuite/test_repl_execution_control.c b/src/scheduler/testsuite/test_repl_execution_control.c index ce9240ee..e4c72d9a 100644 --- a/src/scheduler/testsuite/test_repl_execution_control.c +++ b/src/scheduler/testsuite/test_repl_execution_control.c @@ -94,6 +94,8 @@ #include "../src/recording.c" #include "../src/recording_session.c" #include "../src/repl.c" +#include "../src/repl_break.c" +#include "../src/repl_info.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_sim_alt_events.c b/src/scheduler/testsuite/test_sim_alt_events.c index fe21865d..9519632a 100644 --- a/src/scheduler/testsuite/test_sim_alt_events.c +++ b/src/scheduler/testsuite/test_sim_alt_events.c @@ -46,13 +46,16 @@ * White-box #include of the same TU set test_sim_verify.c uses (see that * file's own header comment) -- 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/ + * recording_session.c/repl.c/sim.c/sim_loop.c/sim_record.c/sim_verify.c/ * snapshot.c/snapshot_load.c/snapshot_wire.c. sim_verify.c AND - * recording_session.c/sim_record.c are all required at link time + * recording_session.c/sim_record.c/repl.c are all required at link time * (sched_sim_driver_run() externs sim_decision_eq()/sim_log_mismatch() and * sched_record_session_ctx_new()/_finalize()/sim_record_write_header_and_ - * snapshot()/sim_record_write_event() unconditionally) even though this - * file never sets --sim-verify or --sim-output, same reason + * snapshot()/sim_record_write_event() unconditionally, and repl.h's task + * 12.1 inline wrappers -- pulled in transitively by sim_loop.c's own + * `#include "repl.h"` -- reference xce_repl_start()/ + * xce_repl_post_session_check() unconditionally too) even though this file + * never sets --sim-verify/--sim-output/--interactive, same reason * test_sim_event_loop.c's own header comment documents. */ #include @@ -83,6 +86,9 @@ #include "../src/plugins/builtin_fcfs.c" #include "../src/recording.c" #include "../src/recording_session.c" +#include "../src/repl.c" +#include "../src/repl_break.c" +#include "../src/repl_info.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_sim_equivalence.c b/src/scheduler/testsuite/test_sim_equivalence.c index 2f2058a1..2f0b3a09 100644 --- a/src/scheduler/testsuite/test_sim_equivalence.c +++ b/src/scheduler/testsuite/test_sim_equivalence.c @@ -100,9 +100,11 @@ * recording.c, recording_events.c, recording_session.c, snapshot.c, * snapshot_wire.c) and test_sim_record_output.c's own set (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, + * plugins/builtin_fcfs.c, recording.c, recording_session.c, repl.c, sim.c, * sim_loop.c, sim_record.c, sim_verify.c, snapshot.c, snapshot_load.c, - * snapshot_wire.c) -- no prior test file combines both the normal-mode + * snapshot_wire.c -- repl.c added by task 12.1, required at link time by + * sim_loop.c's own repl.h inline wrappers) -- no prior test file combines + * both the normal-mode * driver stack and the full simulation stack in one TU; this task is the * first to genuinely need both halves of the pipeline live simultaneously. * Verified no static-symbol collisions exist across the combined file set @@ -153,6 +155,9 @@ #include "../src/recording.c" #include "../src/recording_events.c" #include "../src/recording_session.c" +#include "../src/repl.c" +#include "../src/repl_break.c" +#include "../src/repl_info.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_sim_event_loop.c b/src/scheduler/testsuite/test_sim_event_loop.c index c99645ef..c1e4fc86 100644 --- a/src/scheduler/testsuite/test_sim_event_loop.c +++ b/src/scheduler/testsuite/test_sim_event_loop.c @@ -66,7 +66,12 @@ * sched_record_session_ctx_new()/_finalize()/sim_record_write_header_and_ * snapshot()/sim_record_write_event() as extern functions, undefined * references at link time otherwise, even though this file never sets - * --sim-output either) PLUS + * --sim-output either) PLUS ../src/repl.c (task 12.1's sibling file -- same + * reasoning again: sim_loop.c now `#include`s repl.h, whose task 12.1 + * inline wrappers unconditionally reference xce_repl_start()/ + * xce_repl_post_session_check() as extern functions, undefined references + * at link time otherwise, even though this file never sets --interactive) + * PLUS * ../src/snapshot.c/snapshot_wire.c/snapshot_load.c (task 8.1, needed by * both the fixture-building and sim.c's own snapshot load). */ @@ -96,6 +101,9 @@ #include "../src/plugins/builtin_fcfs.c" #include "../src/recording.c" #include "../src/recording_session.c" +#include "../src/repl.c" +#include "../src/repl_break.c" +#include "../src/repl_info.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_sim_isolation.c b/src/scheduler/testsuite/test_sim_isolation.c index 11e3f6ef..705a65b4 100644 --- a/src/scheduler/testsuite/test_sim_isolation.c +++ b/src/scheduler/testsuite/test_sim_isolation.c @@ -4,27 +4,30 @@ * or structural check confirms no controller-facing symbol is reachable * from the simulation driver's compiled object" -- plus the AC's own * "confirmed by the struct definition itself, not runtime checks alone" - * clause. Scope: sim.h + sim.c + sim_loop.c + sim_record.c + repl.c/repl.h - * together (task 11.1 + task 11.2's sibling file + task 11.5's sibling - * file, the --sim-output writer + task 12.1's sibling files, the - * interactive REPL) -- NOT a re-audit of core.c (task 4.1's own territory, - * test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c (shared - * normal-mode infra, out of this task's boundary). sim_verify.c (task 11.4) - * is likewise out of scope here -- it is never compiled into + * clause. Scope: sim.h + sim.c + sim_loop.c + sim_record.c + repl.c/repl.h/ + * repl_break.c/repl_info.c/repl_internal.h together (task 11.1 + task + * 11.2's sibling file + task 11.5's sibling file, the --sim-output writer + + * task 12.1's REPL files + task 12.2's own repl_break.c/repl_info.c/ + * repl_internal.h, its 800-line-cap split of repl.c -- see repl_internal.h's + * own "Why a split at all" section) -- NOT a re-audit of core.c (task 4.1's + * own territory, test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c + * (shared normal-mode infra, out of this task's boundary). sim_verify.c + * (task 11.4) is likewise out of scope here -- it is never compiled into * sched_sim_isolation_lib below and carries no isolation-sensitive logic of * its own (pure struct comparison, no I/O); extending this guard to it was - * not part of task 11.4's own text and is left as-is. repl.c/repl.h are - * added here per task 12.1's own text (extending this exact 3-file - * precedent to a 5-file one) -- the REPL reads/writes stdin/stdout and - * touches xce_sched_sim_driver_t directly, the same category of - * driver-level code this guard already covers. + * not part of task 11.4's own text and is left as-is. repl.c/repl.h were + * added per task 12.1's own text (extending this exact 3-file precedent to + * a 5-file one); repl_break.c/repl_info.c/repl_internal.h extend it again + * (task 12.2, to an 8-file one) -- all touch xce_sched_sim_driver_t + * directly or read/write stdin/stdout, the same category of driver-level + * code this guard already covers. * * Three layers, extending test_core_isolation.c's two-layer pattern (same * genre: source-text scans of the real, current on-disk files, not a frozen * snapshot): * * 1. Source-text scan of sim.h/sim.c/sim_loop.c/sim_record.c/repl.c/ - * repl.h: no #include of + * repl.h/repl_break.c/repl_info.c/repl_internal.h: no #include of * client.h/netio.h/xceapi.h/delay.h/idgen.h/xce/cluster.h; no call-site * substring matching a symbol exported only by those libraries (the * exact FORBIDDEN_SYMBOLS list test_core_isolation.c already @@ -97,9 +100,21 @@ #error \ "XCE_SIM_REPL_H_PATH must be defined by meson.build to repl.h's absolute path" #endif +#ifndef XCE_SIM_REPL_BREAK_C_PATH +#error \ + "XCE_SIM_REPL_BREAK_C_PATH must be defined by meson.build to repl_break.c's absolute path" +#endif +#ifndef XCE_SIM_REPL_INFO_C_PATH +#error \ + "XCE_SIM_REPL_INFO_C_PATH must be defined by meson.build to repl_info.c's absolute path" +#endif +#ifndef XCE_SIM_REPL_INTERNAL_H_PATH +#error \ + "XCE_SIM_REPL_INTERNAL_H_PATH must be defined by meson.build to repl_internal.h's absolute path" +#endif #ifndef XCE_SIM_LIB_PATH #error \ - "XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c+sim_record.c+repl.c's isolated static_library() archive" + "XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c+sim_record.c+repl.c+repl_break.c+repl_info.c's isolated static_library() archive" #endif /* Forbidden headers (design.md "Allowed Dependencies", same list @@ -369,12 +384,15 @@ void tearDown(void) { return; } * failure mode this whole test exists to avoid). */ static void test_sim_source_paths_are_readable(void) { - char* h = read_whole_file(XCE_SIM_H_PATH); - char* c = read_whole_file(XCE_SIM_C_PATH); - char* loop = read_whole_file(XCE_SIM_LOOP_C_PATH); - char* record = read_whole_file(XCE_SIM_RECORD_C_PATH); - char* repl_c = read_whole_file(XCE_SIM_REPL_C_PATH); - char* repl_h = read_whole_file(XCE_SIM_REPL_H_PATH); + char* h = read_whole_file(XCE_SIM_H_PATH); + char* c = read_whole_file(XCE_SIM_C_PATH); + char* loop = read_whole_file(XCE_SIM_LOOP_C_PATH); + char* record = read_whole_file(XCE_SIM_RECORD_C_PATH); + char* repl_c = read_whole_file(XCE_SIM_REPL_C_PATH); + char* repl_h = read_whole_file(XCE_SIM_REPL_H_PATH); + char* repl_break_c = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); + char* repl_info_c = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); + char* repl_int_h = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_SIM_H_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_SIM_C_PATH); @@ -382,6 +400,9 @@ static void test_sim_source_paths_are_readable(void) TEST_ASSERT_NOT_NULL_MESSAGE(record, XCE_SIM_RECORD_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(repl_c, XCE_SIM_REPL_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(repl_h, XCE_SIM_REPL_H_PATH); + TEST_ASSERT_NOT_NULL_MESSAGE(repl_break_c, XCE_SIM_REPL_BREAK_C_PATH); + TEST_ASSERT_NOT_NULL_MESSAGE(repl_info_c, XCE_SIM_REPL_INFO_C_PATH); + TEST_ASSERT_NOT_NULL_MESSAGE(repl_int_h, XCE_SIM_REPL_INTERNAL_H_PATH); free(h); free(c); @@ -389,22 +410,31 @@ static void test_sim_source_paths_are_readable(void) free(record); free(repl_c); free(repl_h); + free(repl_break_c); + free(repl_info_c); + free(repl_int_h); } static void test_sim_never_includes_forbidden_headers(void) { - char* h_raw = read_whole_file(XCE_SIM_H_PATH); - char* c_raw = read_whole_file(XCE_SIM_C_PATH); - char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH); - char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); - char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); - char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); + char* h_raw = read_whole_file(XCE_SIM_H_PATH); + char* c_raw = read_whole_file(XCE_SIM_C_PATH); + char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH); + char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); + char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); + char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); + char* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); + char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); + char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); char* h; char* c; char* loop; char* record; char* repl_c; char* repl_h; + char* repl_break_c; + char* repl_info_c; + char* repl_int_h; TEST_ASSERT_NOT_NULL(h_raw); TEST_ASSERT_NOT_NULL(c_raw); @@ -412,19 +442,28 @@ static void test_sim_never_includes_forbidden_headers(void) TEST_ASSERT_NOT_NULL(record_raw); TEST_ASSERT_NOT_NULL(repl_c_raw); TEST_ASSERT_NOT_NULL(repl_h_raw); + TEST_ASSERT_NOT_NULL(repl_break_c_raw); + TEST_ASSERT_NOT_NULL(repl_info_c_raw); + TEST_ASSERT_NOT_NULL(repl_int_h_raw); - h = strip_c_comments(h_raw); - c = strip_c_comments(c_raw); - loop = strip_c_comments(loop_raw); - record = strip_c_comments(record_raw); - repl_c = strip_c_comments(repl_c_raw); - repl_h = strip_c_comments(repl_h_raw); + h = strip_c_comments(h_raw); + c = strip_c_comments(c_raw); + loop = strip_c_comments(loop_raw); + record = strip_c_comments(record_raw); + repl_c = strip_c_comments(repl_c_raw); + repl_h = strip_c_comments(repl_h_raw); + repl_break_c = strip_c_comments(repl_break_c_raw); + repl_info_c = strip_c_comments(repl_info_c_raw); + repl_int_h = strip_c_comments(repl_int_h_raw); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(loop); TEST_ASSERT_NOT_NULL(record); TEST_ASSERT_NOT_NULL(repl_c); TEST_ASSERT_NOT_NULL(repl_h); + TEST_ASSERT_NOT_NULL(repl_break_c); + TEST_ASSERT_NOT_NULL(repl_info_c); + TEST_ASSERT_NOT_NULL(repl_int_h); assert_no_forbidden_includes(h, "sim.h"); assert_no_forbidden_includes(c, "sim.c"); @@ -432,6 +471,9 @@ static void test_sim_never_includes_forbidden_headers(void) assert_no_forbidden_includes(record, "sim_record.c"); assert_no_forbidden_includes(repl_c, "repl.c"); assert_no_forbidden_includes(repl_h, "repl.h"); + assert_no_forbidden_includes(repl_break_c, "repl_break.c"); + assert_no_forbidden_includes(repl_info_c, "repl_info.c"); + assert_no_forbidden_includes(repl_int_h, "repl_internal.h"); free(h_raw); free(c_raw); @@ -439,28 +481,40 @@ static void test_sim_never_includes_forbidden_headers(void) free(record_raw); free(repl_c_raw); free(repl_h_raw); + free(repl_break_c_raw); + free(repl_info_c_raw); + free(repl_int_h_raw); free(h); free(c); free(loop); free(record); free(repl_c); free(repl_h); + free(repl_break_c); + free(repl_info_c); + free(repl_int_h); } static void test_sim_never_calls_forbidden_symbols(void) { - char* h_raw = read_whole_file(XCE_SIM_H_PATH); - char* c_raw = read_whole_file(XCE_SIM_C_PATH); - char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH); - char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); - char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); - char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); + char* h_raw = read_whole_file(XCE_SIM_H_PATH); + char* c_raw = read_whole_file(XCE_SIM_C_PATH); + char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH); + char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); + char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); + char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); + char* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); + char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); + char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); char* h; char* c; char* loop; char* record; char* repl_c; char* repl_h; + char* repl_break_c; + char* repl_info_c; + char* repl_int_h; TEST_ASSERT_NOT_NULL(h_raw); TEST_ASSERT_NOT_NULL(c_raw); @@ -468,19 +522,28 @@ static void test_sim_never_calls_forbidden_symbols(void) TEST_ASSERT_NOT_NULL(record_raw); TEST_ASSERT_NOT_NULL(repl_c_raw); TEST_ASSERT_NOT_NULL(repl_h_raw); + TEST_ASSERT_NOT_NULL(repl_break_c_raw); + TEST_ASSERT_NOT_NULL(repl_info_c_raw); + TEST_ASSERT_NOT_NULL(repl_int_h_raw); - h = strip_c_comments(h_raw); - c = strip_c_comments(c_raw); - loop = strip_c_comments(loop_raw); - record = strip_c_comments(record_raw); - repl_c = strip_c_comments(repl_c_raw); - repl_h = strip_c_comments(repl_h_raw); + h = strip_c_comments(h_raw); + c = strip_c_comments(c_raw); + loop = strip_c_comments(loop_raw); + record = strip_c_comments(record_raw); + repl_c = strip_c_comments(repl_c_raw); + repl_h = strip_c_comments(repl_h_raw); + repl_break_c = strip_c_comments(repl_break_c_raw); + repl_info_c = strip_c_comments(repl_info_c_raw); + repl_int_h = strip_c_comments(repl_int_h_raw); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(loop); TEST_ASSERT_NOT_NULL(record); TEST_ASSERT_NOT_NULL(repl_c); TEST_ASSERT_NOT_NULL(repl_h); + TEST_ASSERT_NOT_NULL(repl_break_c); + TEST_ASSERT_NOT_NULL(repl_info_c); + TEST_ASSERT_NOT_NULL(repl_int_h); assert_no_forbidden_symbols(h, "sim.h"); assert_no_forbidden_symbols(c, "sim.c"); @@ -488,6 +551,9 @@ static void test_sim_never_calls_forbidden_symbols(void) assert_no_forbidden_symbols(record, "sim_record.c"); assert_no_forbidden_symbols(repl_c, "repl.c"); assert_no_forbidden_symbols(repl_h, "repl.h"); + assert_no_forbidden_symbols(repl_break_c, "repl_break.c"); + assert_no_forbidden_symbols(repl_info_c, "repl_info.c"); + assert_no_forbidden_symbols(repl_int_h, "repl_internal.h"); free(h_raw); free(c_raw); @@ -495,12 +561,18 @@ static void test_sim_never_calls_forbidden_symbols(void) free(record_raw); free(repl_c_raw); free(repl_h_raw); + free(repl_break_c_raw); + free(repl_info_c_raw); + free(repl_int_h_raw); free(h); free(c); free(loop); free(record); free(repl_c); free(repl_h); + free(repl_break_c); + free(repl_info_c); + free(repl_int_h); } /* @@ -544,15 +616,14 @@ static void test_sim_driver_struct_has_no_forbidden_fields(void) /* * test_sim_object_has_no_forbidden_undefined_symbols - the literal - * build-time/link-time proof: sim.c + sim_loop.c + sim_record.c, compiled - * together in isolation into sched_sim_isolation_lib (meson.build), must - * not reference - * (as an undefined external symbol in the compiled archive) anything - * matching the forbidden prefixes above. Independent of what any header - * transitively declares -- reflects only what this driver's compiled code - * actually calls. Same technique and same any_undefined_seen - * vacuous-pass guard as test_core_isolation.c's - * test_core_object_has_no_forbidden_undefined_symbols(). + * build-time/link-time proof: sim.c + sim_loop.c + sim_record.c + repl.c + + * repl_break.c + repl_info.c, compiled together in isolation into + * sched_sim_isolation_lib (meson.build), must not reference (as an + * undefined external symbol in the compiled archive) anything matching the + * forbidden prefixes above. Independent of what any header transitively + * declares -- reflects only what this driver's compiled code actually + * calls. Same technique and same any_undefined_seen vacuous-pass guard as + * test_core_isolation.c's test_core_object_has_no_forbidden_undefined_symbols(). */ static void test_sim_object_has_no_forbidden_undefined_symbols(void) { @@ -575,8 +646,9 @@ static void test_sim_object_has_no_forbidden_undefined_symbols(void) /* posix format for an undefined symbol: " U" (no * value/size columns). Skip anything that doesn't parse as * exactly that -- e.g. a blank line, or an archive member header - * some nm versions print (this archive has four member objects, - * sim.c.o, sim_loop.c.o, sim_record.c.o, and repl.c.o). */ + * some nm versions print (this archive has six member objects, + * sim.c.o, sim_loop.c.o, sim_record.c.o, repl.c.o, repl_break.c.o, + * and repl_info.c.o). */ if (sscanf(line, "%511s %c", symbol, &type) != 2) { continue; } diff --git a/src/scheduler/testsuite/test_sim_record_output.c b/src/scheduler/testsuite/test_sim_record_output.c index f283cfa3..3b547818 100644 --- a/src/scheduler/testsuite/test_sim_record_output.c +++ b/src/scheduler/testsuite/test_sim_record_output.c @@ -40,9 +40,10 @@ * already establish. * * White-box #include of the same TU set test_sim_verify.c uses (see that - * file's own header comment) PLUS ../src/recording_session.c (the - * scheduler_t-free session-accumulator entry points sim_loop.c now calls) - * PLUS the new ../src/sim_record.c (the code under test). + * file's own header comment, now including task 12.1's repl.c too) PLUS + * ../src/recording_session.c (the scheduler_t-free session-accumulator + * entry points sim_loop.c now calls) PLUS the new ../src/sim_record.c (the + * code under test). */ #include #include @@ -72,6 +73,9 @@ #include "../src/plugins/builtin_fcfs.c" #include "../src/recording.c" #include "../src/recording_session.c" +#include "../src/repl.c" +#include "../src/repl_break.c" +#include "../src/repl_info.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" diff --git a/src/scheduler/testsuite/test_sim_verify.c b/src/scheduler/testsuite/test_sim_verify.c index 7097cbc2..5a4f86bd 100644 --- a/src/scheduler/testsuite/test_sim_verify.c +++ b/src/scheduler/testsuite/test_sim_verify.c @@ -59,10 +59,10 @@ * * White-box #include of the same TU set test_sim_event_loop.c uses (see * that file's own header comment, including task 11.5's - * recording_session.c/sim_record.c -- required at link time by - * sched_sim_driver_run()'s unconditional externs regardless of whether - * --sim-output is ever set in a given test) PLUS ../src/sim_verify.c (the - * code under test). + * recording_session.c/sim_record.c AND task 12.1's repl.c -- all required at + * link time by sched_sim_driver_run()'s/repl.h's unconditional externs + * regardless of whether --sim-output/--interactive is ever set in a given + * test) PLUS ../src/sim_verify.c (the code under test). */ #include #include @@ -93,6 +93,9 @@ #include "../src/plugins/builtin_fcfs.c" #include "../src/recording.c" #include "../src/recording_session.c" +#include "../src/repl.c" +#include "../src/repl_break.c" +#include "../src/repl_info.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c"