feat(scheduler-core): implement REPL breakpoints and state inspection (task 12.2)

Adds the interactive simulation REPL's breakpoint and state-inspection
commands (Req 5 AC 22): `break <event_type> [key=value ...]` / `info
break` / `delete <id>|all` on event and decision types, and `info
session/job/queue/worker/resource/summary`.

repl.c (370 lines after task 12.1) couldn't hold this task's full
surface under the project's 800-line cap, so it's split three ways
(matching the snapshot.c/snapshot_wire.c/snapshot_load.c precedent):
repl.c (execution control + dispatch), repl_break.c (matching/CRUD +
the three new driver-facing hooks), repl_info.c (info dumps), sharing
state via repl_internal.h. sim_loop.c gained 3 new bare hook calls
(repl.h inline wrappers, same pattern as task 12.1's two) at the
points design.md's pseudocode requires -- per decoded event before it
joins the batch, per session boundary, and per drained decision --
landing sim_loop.c at exactly 800/800 lines.

Two real bugs found and fixed during development (bug-1506, bug-1507):
an uninitialized xstr_table left breakpoint filter fields silently
NULL instead of failing loud, and repl_breakpoint_free() called a bare
free() on an RTTI-refcounted makeString() string, corrupting the heap.

Review found one gap: `info job` printed only a resreq presence flag
instead of the actual resreq text, when format_resreq() already
existed in this repo for exactly this purpose. Fixed to match.

Scheduler suite: 35/35 clean (was 34/34). RED-phase evidence (2 real
bugs plus 2 synthetic probes, all reverted with confirmed zero
residue) recorded in tasks.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPPXeKSUXUqcqvzNXY1rzJ
This commit is contained in:
Rongsong Shen 2026-08-07 09:46:16 +00:00
parent 02f7f48ff9
commit 2fecea3592
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
18 changed files with 3067 additions and 212 deletions

View file

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

View file

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

View file

@ -6,20 +6,29 @@
#include <editline/readline.h> /* 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 <EVENT_TYPE> [key=value ...] Set a breakpoint.\n");
printf(" info break List breakpoints.\n");
printf(" delete <id>|all Delete breakpoint(s).\n");
printf(" info session Simulation position.\n");
printf(" info job <jobid> Dump one job.\n");
printf(" info queue <name>|all Dump queue(s).\n");
printf(" info worker <name>|all Dump worker(s).\n");
printf(" info resource <name>|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)
{

View file

@ -0,0 +1,669 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* 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 <TOKEN>` 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 <id>`. 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 <event_type> [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 <EVENT_TYPE> [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 <id>": "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 <id>|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);
}
}

View file

@ -0,0 +1,668 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <jobid>: 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 <jobid>": "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 <name>": "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 <name|all>` 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 <name>": "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 <name|all>` 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 <name>": "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 <name|all>` 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 "
"<break|session|job|queue|worker|resource|summary> [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 <jobid>\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 <name>|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 <name>|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 <name>|all\n");
} else {
repl_info_resource(sim, tokens[2]);
}
} else {
printf("[sim] info: unrecognized subcommand '%s'\n", tokens[1]);
}
return false;
}

View file

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

View file

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

View file

@ -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'])

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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 <errno.h>
@ -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"

View file

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

View file

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

View file

@ -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: "<name> 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;
}

View file

@ -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 <errno.h>
#include <limits.h>
@ -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"

View file

@ -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 <errno.h>
#include <limits.h>
@ -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"