myxce/src/scheduler/testsuite/test_repl_breakpoints_inspection.c
Rongsong Shen 9db99964f0
feat(scheduler-sim-repl-verbosity): task 8.2 - implement resource table rendering
Rewrites info worker's/info resource's resource-listing loops from a
flat, unconditional fprintf into a 3-pass, section-grouped table
(Consumable/Capacity, Load Metrics, Static Hardware Attributes),
units_mode-aware value rendering (raw/human/both, including swp's
MB->bytes conversion), whole-number formatting with no trailing .00
outside the consumable value=/used= pair, and jobid-range-compression
adoption for info worker's assigned-job listing (Requirements 6.4-6.6,
4.5). All 10 of task 8.1's tests pass. Preserves every named prior-bug
invariant (bug-1727's avail_v-as-total, bug-1613's consumable/
non-consumable split, bug-1593's GLOBAL/SHARED filter, bug-1688's
.caps-bound iteration).

Fixes one pre-existing test (bug-1688, predating this spec) whose
"value=5.00" assertion for a whole-number non-consumable resource
directly conflicts with Requirement 6.5's intentional no-trailing-.00
change - same class of fix as task 4.2's pre-existing-test updates.

repl_info.c grew from 985 to 1267 lines (58% over the 800-line cap,
worse than any other file this spec touched) - but unlike repl.c/
repl_break.c, it was already 23% over cap before this spec began.
Review empirically confirmed a narrow helper-only extraction doesn't
achieve compliance (still 41% over), and a compliance-achieving split
would mean restructuring ~690 lines of pre-existing display logic
this feature doesn't otherwise touch - a larger scheduler-core-level
decision, accepted as a residual rather than folded into this task.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-02 19:32:29 +00:00

2095 lines
80 KiB
C

/*
* test_repl_breakpoints_inspection.c - contract test for the interactive
* simulation REPL's breakpoint and state-inspection slice (scheduler-core
* spec, task 12.2, Req 5 AC 22).
*
* Observables under test (task 12.2's own text):
* (a) setting a breakpoint on a specific event type and field value
* pauses the REPL exactly when a matching event is about to be
* processed;
* (b) each inspection command reports state matching what a snapshot
* dump of the same moment would show.
*
* Four groups, at increasing granularity (same "unit-level precision PLUS
* integration-level end-to-end proof" split test_repl_execution_control.c's
* own header comment establishes for task 12.1):
*
* Group A (unit, white-box): `break`/`delete`/`info break` command
* parsing and listing, driven via repl_dispatch() directly, asserting on
* sim.repl_state->breakpoints[]'s own fields and captured stdout.
*
* Group B (unit, white-box): the pure breakpoint-matching predicates
* (repl_find_event_breakpoint()/repl_find_decision_breakpoint()/
* repl_find_begin_round_breakpoint(), repl_break.c) driven directly
* against hand-built xce_core_event_t/xce_decision_t values -- the exact
* "not a decoy, the real predicate" proof test_repl_execution_control.c's
* own Test 1 (repl_should_pause()) already establishes for task 12.1.
*
* Group C (unit, white-box): state-inspection dumps (`info job`/`queue`/
* `summary`/`session`) driven via repl_dispatch() against a REAL
* xce_sched_core_t populated through a genuine xce_core_push_events()
* call (same in-memory QUEUE_UPDATE/JOBS_PUSH xce_core_event_t
* construction test_core_events.c's own fixtures already establish -- no
* capnp file needed for this granularity), captured via stdout and
* asserted against the exact field values pushed in. `info worker`/
* `info resource` share the identical iter_make()/iter_next()/printf
* code shape as `info queue` (same file, repl_info.c) and are not
* separately re-proven here -- a deliberate, documented scope decision,
* not an oversight.
*
* Group D (integration, single-process, scripted stdin): a REAL
* sched_sim_driver_run() against a real `.replay` fixture, proving
* Observable (a) end to end for both an event breakpoint
* (JOB_STATE_CHANGE jobid=) and a decision breakpoint (DECISION type=),
* by capturing stdout and asserting ordering the same way
* test_repl_execution_control.c's own Test 2 does for the pre-replay
* pause.
*
* White-box #include set: the SAME TU set test_repl_execution_control.c
* establishes for task 12.1, PLUS the new ../src/repl_break.c/repl_info.c
* under test (task 12.2's own 800-line-cap split of repl.c -- see
* repl_internal.h's own "Why a split at all" section).
*/
#include <errno.h>
#include <limits.h>
#include <sys/wait.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#include "controller.capnp.h"
#include "kvmgr.h"
#include "scheduler.h"
#include "xstring.h"
/* Pull in the real functions under test into this TU (white-box #include,
* same convention as test_repl_execution_control.c). */
#include "../src/core.c"
#include "../src/core_clock.c"
#include "../src/core_events.c"
#include "../src/core_session.c"
#include "../src/decision.c"
#include "../src/plugin.c"
#include "../src/plugins/builtin_fcfs.c"
#include "../src/recording.c"
#include "../src/recording_session.c"
#include "../src/repl.c"
#include "../src/repl_fmt.c"
#include "../src/repl_rollup.c"
#include "../src/repl_break.c"
#include "../src/repl_info.c"
#include "../src/repl_params.c"
#include "../src/sim.c"
#include "../src/sim_loop.c"
#include "../src/sim_picker.c" /* bug-1567: sched_sim_driver_run() now calls sim_bootstrap_picker() (sim_loop.c-cap sibling) as an extern -- undefined reference at link time otherwise */
#include "../src/sim_record.c"
#include "../src/sim_verify.c"
#include "../src/snapshot.c"
#include "../src/snapshot_load.c"
#include "../src/snapshot_wire.c"
void kill_sched_job(void* arg) { (void)arg; }
void kill_sched_queue(void* arg) { (void)arg; }
void kill_sched_worker(void* arg) { (void)arg; }
void kill_sched_user(void* arg) { (void)arg; }
void kill_sched_project(void* arg) { (void)arg; }
void kill_sched_resval(xce_val_t* val) { (void)val; }
/* resource-accounting.md's reconciliation (resource.c, not linked here --
* this file only #includes core_events.c). core_events.c's
* XCE_CORE_EVENT_JOB_RUSAGE handler calls this whenever job->accounts !=
* NULL; link-satisfying stub only, same rationale as every other stub in
* this block (see test_job_update.c's identical stub). */
int sched_reconcile_job_resource_usage(xce_sched_core_t* c,
xce_job_account_t* a,
xce_job_rusages_t* r)
{
(void)c; (void)a; (void)r; return 0;
}
void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; }
int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
xlist_t* limits)
{
(void)sched;
(void)vec;
(void)limits;
return 0;
}
/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task
* 2.1) new round-boundary call to the relocated sched_account_jobs()
* (core.h) -- a no-op stand-in here since this file does not link
* scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* copy test_repl_execution_control.c/test_sim_alt_events.c/etc. each carry
* (schedobj.c is not linked here). */
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
{
xce_sched_plugin_t* plugin = NULL;
int rc = 0;
if ((core == NULL) || (out == NULL)) {
return -EINVAL;
}
*out = NULL;
if (core->job_pick_plugin == NULL) {
return -EINVAL;
}
lock(core->plugins, RTTI_LOCK_READ);
rc = key_find(core->plugins, core->job_pick_plugin, (void**)&plugin);
unlock(core->plugins);
if (rc <= 0) {
return (rc < 0) ? rc : -ENOENT;
}
if ((plugin == NULL) || (plugin->picker_iface == NULL)
|| (plugin->picker_iface->next_job == NULL)
|| (plugin->picker_iface->begin_session == NULL)
|| (plugin->picker_iface->end_session == NULL)
|| (plugin->picker_iface->on_dispatch == NULL)
|| (plugin->picker_iface->on_pend == NULL)) {
return -EINVAL;
}
*out = plugin;
return 0;
}
void setUp(void) { return; }
void tearDown(void) { return; }
/* ---------------------------------------------------------------------- */
/* Fixture helpers -- Group C: in-memory core-state construction (no capnp, */
/* same technique test_core_events.c's own make_job()/fixture_init() */
/* establish). */
/* ---------------------------------------------------------------------- */
static xce_job_t* make_job(uint64_t jobid, uint32_t state, char* queue,
char* user, char* project)
{
xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j));
j->jobid = jobid;
j->state = state;
j->sub_time = 1000;
j->queue_time = 2000;
j->start_time = 0;
j->submit = (job_submit_data_t*)calloc(1, sizeof(*(j->submit)));
j->submit->queue = queue;
j->submit->user = user;
j->submit->project_name = project;
j->submit->resreq = "type == any";
j->submit->mintasks = 1;
j->submit->maxtasks = 1;
return j;
}
/* Builds a real xce_sched_core_t (no picker plugin registered -- core
* state mutation does not depend on one, core_events.c's own
* xce_core_push_events() doc comment) with one queue ("normal", priority
* 5, open) and one job (jobid, PEND, in "normal") pushed through a
* genuine xce_core_push_events() call. */
static void build_fixture_core(xce_sched_core_t* core, uint64_t jobid)
{
xce_core_event_t events[2];
xce_queue_t wire_queue;
xce_job_t* job1;
int rc;
rc = xce_sched_core_init(core);
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&wire_queue, 0, sizeof(wire_queue));
wire_queue.queue = "normal";
wire_queue.priority = 5;
wire_queue.status = QUEUE_STAT_OPEN;
job1 = make_job(jobid, JOB_STAT_PEND, "normal", "alice", "proj1");
memset(events, 0, sizeof(events));
events[0].type = XCE_CORE_EVENT_QUEUE_UPDATE;
events[0].u.queue_update.queue = &wire_queue;
events[1].type = XCE_CORE_EVENT_JOBS_PUSH;
events[1].u.jobs_push.njobs = 1;
events[1].u.jobs_push.jobs = &job1;
rc = xce_core_push_events(core, events, 2);
TEST_ASSERT_EQUAL_INT(0, rc);
}
/* Same dup()/dup2()-around-STDOUT_FILENO capture technique
* test_repl_execution_control.c's own capture_stdout_around() establishes. */
static char* capture_stdout_around(void (*fn)(void))
{
char tmp[] = "/tmp/xce_replbpi_stdoutcap_XXXXXX";
int fd = mkstemp(tmp);
int saved_stdout;
off_t len;
char* buf;
TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "mkstemp for stdout capture must "
"succeed");
fflush(stdout);
saved_stdout = dup(STDOUT_FILENO);
TEST_ASSERT_TRUE_MESSAGE(saved_stdout >= 0, "dup(STDOUT_FILENO) must "
"succeed");
TEST_ASSERT_TRUE_MESSAGE(dup2(fd, STDOUT_FILENO) >= 0,
"dup2 redirect of STDOUT_FILENO must succeed");
fn();
fflush(stdout);
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdout);
len = lseek(fd, 0, SEEK_CUR);
TEST_ASSERT_TRUE(len >= 0);
buf = (char*)calloc(1, (size_t)len + 1);
TEST_ASSERT_NOT_NULL(buf);
TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0);
if (len > 0) {
ssize_t nread = read(fd, buf, (size_t)len);
TEST_ASSERT_TRUE(nread == len);
}
close(fd);
unlink(tmp);
return buf;
}
/* Captures the output of dispatching one REPL command line against `sim`
* -- used by both Group A (break/delete/info break) and Group C (info
* job/queue/summary/session). Never a `step`/`continue`/`quit` line (this
* helper does not expect repl_dispatch() to return true). */
static xce_sched_sim_driver_t* g_dispatch_sim;
static const char* g_dispatch_line;
static void run_dispatch_for_capture(void)
{
(void)repl_dispatch(g_dispatch_sim, g_dispatch_line);
}
static char* capture_dispatch(xce_sched_sim_driver_t* sim, const char* line)
{
g_dispatch_sim = sim;
g_dispatch_line = line;
return capture_stdout_around(run_dispatch_for_capture);
}
/* ---------------------------------------------------------------------- */
/* Group A: `break`/`delete`/`info break` command parsing and listing. */
/* ---------------------------------------------------------------------- */
static void test_break_creates_breakpoint_with_filters(void)
{
xce_sched_sim_driver_t sim;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break JOB_STATE_CHANGE jobid=12345"));
TEST_ASSERT_EQUAL_INT(1, sim.repl_state->nbreakpoints);
TEST_ASSERT_EQUAL_INT(XCE_REPL_BP_JOB_STATE_CHANGE,
sim.repl_state->breakpoints[0]->kind);
TEST_ASSERT_EQUAL_INT(1, sim.repl_state->breakpoints[0]->nfilters);
TEST_ASSERT_EQUAL_STRING("jobid",
sim.repl_state->breakpoints[0]->filters[0].key);
TEST_ASSERT_EQUAL_STRING("12345",
sim.repl_state->breakpoints[0]->filters[0].value);
printf("test_break_creates_breakpoint_with_filters: PASS\n");
}
static void test_break_unrecognized_event_type_rejected(void)
{
xce_sched_sim_driver_t sim;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break BOGUS_EVENT_TYPE"));
TEST_ASSERT_EQUAL_INT(0, sim.repl_state->nbreakpoints);
printf("test_break_unrecognized_event_type_rejected: PASS\n");
}
static void test_break_malformed_filter_rejected(void)
{
xce_sched_sim_driver_t sim;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
/* "nofiltermark" has no '=' -- the whole command must be rejected, no
* partially-applied breakpoint. */
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break JOB_PUSH nofiltermark"));
TEST_ASSERT_EQUAL_INT(0, sim.repl_state->nbreakpoints);
printf("test_break_malformed_filter_rejected: PASS\n");
}
static void test_delete_by_id_removes_one_breakpoint(void)
{
xce_sched_sim_driver_t sim;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break JOB_PUSH"));
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break BEGIN_ROUND"));
TEST_ASSERT_EQUAL_INT(2, sim.repl_state->nbreakpoints);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "delete 1"));
TEST_ASSERT_EQUAL_INT(1, sim.repl_state->nbreakpoints);
TEST_ASSERT_EQUAL_INT(2, sim.repl_state->breakpoints[0]->id);
printf("test_delete_by_id_removes_one_breakpoint: PASS\n");
}
static void test_delete_all_removes_all_breakpoints(void)
{
xce_sched_sim_driver_t sim;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break JOB_PUSH"));
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break BEGIN_ROUND"));
TEST_ASSERT_EQUAL_INT(2, sim.repl_state->nbreakpoints);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "delete all"));
TEST_ASSERT_EQUAL_INT(0, sim.repl_state->nbreakpoints);
printf("test_delete_all_removes_all_breakpoints: PASS\n");
}
static void test_delete_non_numeric_id_rejected(void)
{
xce_sched_sim_driver_t sim;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break JOB_PUSH"));
TEST_ASSERT_EQUAL_INT(1, sim.repl_state->nbreakpoints);
/* "abc" must be rejected outright, not silently coerced to id 0
* (atoi()'s old behavior) and reported as "no breakpoint with id 0". */
TEST_ASSERT_FALSE(repl_dispatch(&sim, "delete abc"));
TEST_ASSERT_EQUAL_INT(1, sim.repl_state->nbreakpoints);
/* "0" and negative ids can never be legitimately assigned
* (repl_cmd_break()'s pre-incremented next_bp_id starts at 1) -- also
* rejected as invalid, not treated as a well-formed-but-missing id. */
TEST_ASSERT_FALSE(repl_dispatch(&sim, "delete 0"));
TEST_ASSERT_EQUAL_INT(1, sim.repl_state->nbreakpoints);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "delete -1"));
TEST_ASSERT_EQUAL_INT(1, sim.repl_state->nbreakpoints);
/* trailing garbage after a numeric prefix ("1x") is also rejected, not
* truncated down to id 1. */
TEST_ASSERT_FALSE(repl_dispatch(&sim, "delete 1x"));
TEST_ASSERT_EQUAL_INT(1, sim.repl_state->nbreakpoints);
printf("test_delete_non_numeric_id_rejected: PASS\n");
}
static void test_info_break_lists_breakpoints_with_filters(void)
{
xce_sched_sim_driver_t sim;
char* captured;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(
repl_dispatch(&sim, "break DECISION type=DISPATCH queue=normal"));
captured = capture_dispatch(&sim, "info break");
TEST_ASSERT_NOT_NULL(strstr(captured, "DECISION"));
TEST_ASSERT_NOT_NULL(strstr(captured, "type=DISPATCH"));
TEST_ASSERT_NOT_NULL(strstr(captured, "queue=normal"));
free(captured);
printf("test_info_break_lists_breakpoints_with_filters: PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Group B: pure breakpoint-matching predicates (repl_break.c), the */
/* precise "not N-1, not N+1"-grade proof, same genre */
/* test_repl_execution_control.c's own repl_should_pause() unit test. */
/* ---------------------------------------------------------------------- */
static void test_event_breakpoint_matches_jobid_filter(void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t ev;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(
repl_dispatch(&sim, "break JOB_STATE_CHANGE jobid=42"));
memset(&ev, 0, sizeof(ev));
ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE;
ev.u.job_state_change.jobid = 42;
ev.u.job_state_change.new_state = JOB_STAT_RUN;
TEST_ASSERT_NOT_NULL_MESSAGE(
repl_find_event_breakpoint(sim.repl_state, &ev),
"jobid=42 must match an event whose own jobid is 42");
ev.u.job_state_change.jobid = 99;
TEST_ASSERT_NULL_MESSAGE(
repl_find_event_breakpoint(sim.repl_state, &ev),
"jobid=42 must NOT match an event whose own jobid is 99");
printf("test_event_breakpoint_matches_jobid_filter: PASS\n");
}
static void test_event_breakpoint_matches_state_filter(void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t ev;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(
repl_dispatch(&sim, "break JOB_STATE_CHANGE state=RUN"));
memset(&ev, 0, sizeof(ev));
ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE;
ev.u.job_state_change.jobid = 1;
ev.u.job_state_change.new_state = JOB_STAT_RUN;
TEST_ASSERT_NOT_NULL_MESSAGE(
repl_find_event_breakpoint(sim.repl_state, &ev),
"state=RUN must match a JOB_STATE_CHANGE whose new_state is RUN");
ev.u.job_state_change.new_state = JOB_STAT_PEND;
TEST_ASSERT_NULL_MESSAGE(
repl_find_event_breakpoint(sim.repl_state, &ev),
"state=RUN must NOT match a JOB_STATE_CHANGE whose new_state is "
"PEND");
printf("test_event_breakpoint_matches_state_filter: PASS\n");
}
static void test_decision_breakpoint_matches_type_filter(void)
{
xce_sched_sim_driver_t sim;
xce_pendreason_t reason;
xce_decision_t d_pend;
xce_decision_t d_dispatch;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break DECISION type=PEND"));
memset(&reason, 0, sizeof(reason));
reason.jobid = 7;
memset(&d_pend, 0, sizeof(d_pend));
d_pend.op = XCE_DECISION_PEND;
d_pend.u.reason = &reason;
memset(&d_dispatch, 0, sizeof(d_dispatch));
d_dispatch.op = XCE_DECISION_DISPATCH;
TEST_ASSERT_NOT_NULL_MESSAGE(
repl_find_decision_breakpoint(&(sim.core), sim.repl_state, &d_pend),
"type=PEND must match a PEND decision");
TEST_ASSERT_NULL_MESSAGE(
repl_find_decision_breakpoint(&(sim.core), sim.repl_state,
&d_dispatch),
"type=PEND must NOT match a DISPATCH decision (u.alloc is NULL "
"here, decision_jobid() must not dereference it)");
printf("test_decision_breakpoint_matches_type_filter: PASS\n");
}
static void test_begin_round_breakpoint_bare_matches_filtered_does_not(void)
{
xce_sched_sim_driver_t sim;
memset(&sim, 0, sizeof(sim));
/* xce_sched_core_init() is the same precondition xce_repl_start()
* always has satisfied for it in the real driver flow (sched_sim_driver_start()
* runs first) -- it also transitively calls xstring_table_init()
* (core.c), which makeString() (breakpoint token/filter string copies,
* repl_break.c) requires to not silently return NULL. */
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sim.core)));
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
TEST_ASSERT_FALSE(repl_dispatch(&sim, "break BEGIN_ROUND"));
TEST_ASSERT_NOT_NULL_MESSAGE(
repl_find_begin_round_breakpoint(sim.repl_state),
"a bare 'break BEGIN_ROUND' (no filters) must match every session "
"boundary");
/* Hand-attach a filter this codebase's `break` command never lets an
* operator type for BEGIN_ROUND (AC 22 shows no BEGIN_ROUND filter
* example) -- proves the fail-closed contract directly, matching
* repl_break.c's own doc comment on repl_find_begin_round_breakpoint(). */
sim.repl_state->breakpoints[0]->nfilters = 1;
TEST_ASSERT_NULL_MESSAGE(
repl_find_begin_round_breakpoint(sim.repl_state),
"a BEGIN_ROUND breakpoint carrying any filter must never match "
"(fail closed)");
printf("test_begin_round_breakpoint_bare_matches_filtered_does_not: "
"PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Group C: state-inspection dumps against a REAL xce_sched_core_t. */
/* ---------------------------------------------------------------------- */
static void test_info_job_reports_state_queue_priority(void)
{
xce_sched_sim_driver_t sim;
char* captured;
memset(&sim, 0, sizeof(sim));
build_fixture_core(&(sim.core), 777);
captured = capture_dispatch(&sim, "info job 777");
TEST_ASSERT_NOT_NULL(strstr(captured, "job 777"));
TEST_ASSERT_NOT_NULL(strstr(captured, "state=PEND"));
TEST_ASSERT_NOT_NULL(strstr(captured, "queue=normal"));
TEST_ASSERT_NOT_NULL(strstr(captured, "queue_time=2000"));
/* resreq= must carry format_resreq()'s REAL formatted text (the same
* print-form build_wire_job()/snapshot_wire.c already calls for a
* restored job's wire resreq), not a "set"/"(none)" presence flag --
* this is the review remediation for task 12.2's `info job`. make_job()
* above gives this job's wire submit->resreq = "type == any", which
* core_build_job() (core_events.c) parses into job->resreq via
* parse_resreq() against build_fixture_core()'s empty core->resmgr (no
* "type" resource registered); print_primary_resreq() (resreq.c) always
* emits a "Primary:"/"Tasks: min = %d, max = %d"/"Rusages:" skeleton for
* any parsed primary resreq regardless of select-clause content (the
* select tree itself has no print-form yet -- resreq.c's own
* print_primary_resreq() leaves that "Add later"), so this exact
* skeleton is the real, deterministic format_resreq() output for this
* fixture -- not a placeholder standing in for it. */
TEST_ASSERT_NOT_NULL(strstr(captured, "resreq=Primary:"));
TEST_ASSERT_NOT_NULL(strstr(captured, "Tasks: min = 0, max = 0"));
TEST_ASSERT_NOT_NULL(strstr(captured, "Rusages:"));
TEST_ASSERT_NULL(strstr(captured, "resreq=set"));
TEST_ASSERT_NULL(strstr(captured, "resreq=(none)"));
free(captured);
printf("test_info_job_reports_state_queue_priority: PASS\n");
}
static void test_info_job_unknown_reports_not_found(void)
{
xce_sched_sim_driver_t sim;
char* captured;
memset(&sim, 0, sizeof(sim));
build_fixture_core(&(sim.core), 777);
captured = capture_dispatch(&sim, "info job 999999");
TEST_ASSERT_NOT_NULL(strstr(captured, "no such job"));
free(captured);
printf("test_info_job_unknown_reports_not_found: PASS\n");
}
static void test_info_queue_reports_status_and_queued_jobs(void)
{
xce_sched_sim_driver_t sim;
char jobid_marker[80];
char jobid_buf[64];
char* captured;
memset(&sim, 0, sizeof(sim));
build_fixture_core(&(sim.core), 4242);
captured = capture_dispatch(&sim, "info queue normal");
/* bug-1591: repl_info_queue_one() (repl_info.c) now renders jobids
* via fmt_jobid(), not raw %llu -- this marker must match that same
* human-readable form. */
fmt_jobid(jobid_buf, sizeof(jobid_buf), 4242ULL);
snprintf(jobid_marker, sizeof(jobid_marker), "jobid=%s", jobid_buf);
TEST_ASSERT_NOT_NULL(strstr(captured, "queue normal"));
TEST_ASSERT_NOT_NULL(strstr(captured, "priority=5"));
TEST_ASSERT_NOT_NULL(strstr(captured, "status=open"));
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, jobid_marker),
"the fixture's own PEND job must appear "
"in the queue's queued-job listing");
free(captured);
printf("test_info_queue_reports_status_and_queued_jobs: PASS\n");
}
/*
* bug-1568 (display-layer piece): repl_info_worker_one()/
* repl_info_resource_one() (repl_info.c) checked
* `avail_v->which == XCE_VAL_NUMERIC` to decide whether a worker's
* xce_val_t resval held a real number -- but XCE_VAL_NUMERIC (xce/types.h,
* value 1) is a RESOURCE-DEFINITION value-type constant (xce_resource_t.
* valtype), a completely different enum from xce_val_t.which's own real
* domain, Value_u_which (controller.capnp.h) -- every producer in this
* codebase (update_val_res/core_apply_worker_resvals/snapshot_load.c/etc.)
* sets xce_val_t.which to Value_u_fvalue (value 5), never XCE_VAL_NUMERIC.
* The comparison was therefore ALWAYS false for a genuinely-numeric
* resval, so `info worker`/`info resource` printed total=0.00/
* available=0.00 for every worker resource, unconditionally -- live-
* confirmed against a real deployed cluster: the value survived the full
* controller-push -> scheduler-apply -> snapshot-save -> snapshot-load
* round trip completely intact (traced at every hop), yet still displayed
* as 0.00, isolating the bug to this display-layer type-tag mismatch
* alone.
*/
static void test_info_worker_reports_resource_values(void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t events[2];
xce_resource_t wire_resource;
xce_worker_t wire_worker;
xce_worker_t* worker_ptrs[1];
xce_val_t val;
xce_name_val_t nv;
xce_name_val_t* nv_ptrs[1];
char* captured;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&wire_resource, 0, sizeof(wire_resource));
wire_resource.name = "mem";
TEST_ASSERT_NOT_NULL(makeResource(sim.core.resmgr, &wire_resource));
memset(&val, 0, sizeof(val));
val.which = Value_u_fvalue;
val.u.fvalue = 42.5;
memset(&nv, 0, sizeof(nv));
nv.name = "mem";
nv.val = &val;
nv_ptrs[0] = &nv;
memset(&wire_worker, 0, sizeof(wire_worker));
wire_worker.name = "hostA";
wire_worker.status = 0;
wire_worker.nresvals_to_sched = 1;
wire_worker.resvals_to_sched = nv_ptrs;
worker_ptrs[0] = &wire_worker;
memset(events, 0, sizeof(events));
events[0].type = XCE_CORE_EVENT_WORKERS_PUSH;
events[0].u.workers_push.nworkers = 1;
events[0].u.workers_push.workers = worker_ptrs;
rc = xce_core_push_events(&(sim.core), events, 1);
TEST_ASSERT_EQUAL_INT(0, rc);
captured = capture_dispatch(&sim, "info worker hostA");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "resource mem: value=42.50"),
"a worker's real numeric resource value must be displayed, not "
"the XCE_VAL_NUMERIC/Value_u_fvalue type-tag mismatch's 0.00 "
"(bug-1568); a non-consumable resource (no flags set here) gets a "
"single value= line, not a value=/used= pair");
TEST_ASSERT_NULL_MESSAGE(strstr(captured, "value=0.00"),
"must not fall back to the zero default for a "
"genuinely-set numeric value");
free(captured);
printf("test_info_worker_reports_resource_values: PASS\n");
}
/*
* New requirement (2026-08-20): a per-worker resource additionally flagged
* XCE_RESF_CONSUMABLE must report both value (its raw total capacity) and
* used (inUse); a non-consumable numeric resource keeps a single value=
* line (test_info_worker_reports_resource_values above covers that case).
*
* bug-1727 (2026-08-30): corrected from the original "value=
* avail+used" expectation -- w->resvals[idx] (avail_v) is ALREADY the
* total capacity (never decremented as jobs consume it; see resource.c's
* unsatisfy_() admission formula), so adding `used` on top double-counted
* it. See repl_info_worker_one()'s updated doc comment (repl_info.c) for
* the full live-reproduced evidence.
*/
static void test_info_worker_reports_consumable_value_and_used(void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t events[1];
xce_resource_t wire_resource;
xce_resource_t* r;
xce_worker_t wire_worker;
xce_worker_t* worker_ptrs[1];
xce_val_t val;
xce_name_val_t nv;
xce_name_val_t* nv_ptrs[1];
xce_val_t used_val;
xce_sched_worker_t* sw;
char* captured;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&wire_resource, 0, sizeof(wire_resource));
wire_resource.name = "slots";
wire_resource.valtype = XCE_VAL_NUMERIC;
wire_resource.flags = XCE_RESF_CONSUMABLE;
r = makeResource(sim.core.resmgr, &wire_resource);
TEST_ASSERT_NOT_NULL(r);
memset(&val, 0, sizeof(val));
val.which = Value_u_fvalue;
val.u.fvalue = 5.0;
memset(&nv, 0, sizeof(nv));
nv.name = "slots";
nv.val = &val;
nv_ptrs[0] = &nv;
memset(&wire_worker, 0, sizeof(wire_worker));
wire_worker.name = "hostA";
wire_worker.status = 0;
wire_worker.nresvals_to_sched = 1;
wire_worker.resvals_to_sched = nv_ptrs;
worker_ptrs[0] = &wire_worker;
memset(events, 0, sizeof(events));
events[0].type = XCE_CORE_EVENT_WORKERS_PUSH;
events[0].u.workers_push.nworkers = 1;
events[0].u.workers_push.workers = worker_ptrs;
rc = xce_core_push_events(&(sim.core), events, 1);
TEST_ASSERT_EQUAL_INT(0, rc);
sw = sched_get_worker(&(sim.core), "hostA");
TEST_ASSERT_NOT_NULL(sw);
memset(&used_val, 0, sizeof(used_val));
used_val.which = Value_u_fvalue;
used_val.u.fvalue = 2.0;
zarray_put(&(sw->resInUse), r->idx, dup_val(&used_val));
captured = capture_dispatch(&sim, "info worker hostA");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "resource slots: value=5.00 used=2.00"),
"a CONSUMABLE worker resource must report value=<raw total, "
"not avail+used> and used=inUse (bug-1727)");
free(captured);
printf("test_info_worker_reports_consumable_value_and_used: PASS\n");
}
/*
* bug-1590: repl_info_worker_one() forced EVERY resource value through the
* numeric "total=%.2f available=%.2f" format, silently coercing any
* string- or boolean-valued resource (e.g. `type`/`model`, or any
* XCE_VAL_BOOLEAN resource) to 0.00 -- indistinguishable from a genuinely
* zero numeric value. Regression: a string and a boolean resource must
* each render their real value via a "value=..." line, never a fabricated
* total/available pair.
*/
static void test_info_worker_reports_string_and_boolean_resource_values(void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t events[2];
xce_resource_t wire_resource_str;
xce_resource_t wire_resource_bool;
xce_worker_t wire_worker;
xce_worker_t* worker_ptrs[1];
xce_val_t val_str;
xce_val_t val_bool;
xce_name_val_t nv_str;
xce_name_val_t nv_bool;
xce_name_val_t* nv_ptrs[2];
char* captured;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&wire_resource_str, 0, sizeof(wire_resource_str));
wire_resource_str.name = "type";
wire_resource_str.valtype = XCE_VAL_STRING;
TEST_ASSERT_NOT_NULL(makeResource(sim.core.resmgr, &wire_resource_str));
memset(&wire_resource_bool, 0, sizeof(wire_resource_bool));
wire_resource_bool.name = "dockerized";
wire_resource_bool.valtype = XCE_VAL_BOOLEAN;
TEST_ASSERT_NOT_NULL(makeResource(sim.core.resmgr, &wire_resource_bool));
memset(&val_str, 0, sizeof(val_str));
val_str.which = Value_u_svalue;
val_str.u.svalue = "linux86";
memset(&nv_str, 0, sizeof(nv_str));
nv_str.name = "type";
nv_str.val = &val_str;
memset(&val_bool, 0, sizeof(val_bool));
val_bool.which = Value_u_bvalue;
val_bool.u.bvalue = 1;
memset(&nv_bool, 0, sizeof(nv_bool));
nv_bool.name = "dockerized";
nv_bool.val = &val_bool;
nv_ptrs[0] = &nv_str;
nv_ptrs[1] = &nv_bool;
memset(&wire_worker, 0, sizeof(wire_worker));
wire_worker.name = "hostA";
wire_worker.status = 0;
wire_worker.nresvals_to_sched = 2;
wire_worker.resvals_to_sched = nv_ptrs;
worker_ptrs[0] = &wire_worker;
memset(events, 0, sizeof(events));
events[0].type = XCE_CORE_EVENT_WORKERS_PUSH;
events[0].u.workers_push.nworkers = 1;
events[0].u.workers_push.workers = worker_ptrs;
rc = xce_core_push_events(&(sim.core), events, 1);
TEST_ASSERT_EQUAL_INT(0, rc);
captured = capture_dispatch(&sim, "info worker hostA");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "resource type: value=linux86"),
"a string-valued resource must display its real string value, "
"not a fabricated total/available pair (bug-1590)");
TEST_ASSERT_NULL_MESSAGE(strstr(captured, "resource type: total="),
"a string-valued resource must never be "
"rendered with a total=/available= pair");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "resource dockerized: value=true"),
"a boolean-valued resource must display true/false, not a "
"fabricated total/available pair (bug-1590)");
TEST_ASSERT_NULL_MESSAGE(strstr(captured, "resource dockerized: total="),
"a boolean-valued resource must never be "
"rendered with a total=/available= pair");
free(captured);
printf("test_info_worker_reports_string_and_boolean_resource_values: "
"PASS\n");
}
/*
* bug-1688: repl_info_worker_one()'s resource-print loop used
* `for (i = 0; i < w->resvals.count; i++)` -- treating `.count` (number of
* currently-populated slots) as a safe index upper bound for iterating the
* zarray BY INDEX. `.count` can legitimately be less than the highest
* occupied index once entries are sparse/non-contiguous (zarray_put_ex()'s
* count bookkeeping, zarray.c, only tracks "how many slots read NULL at
* write time" -- it is not recomputed from the array's actual contents).
* `.caps` (allocated capacity, monotonic, never shrinks) is this codebase's
* own established convention for this identical structure -- see
* resource.c:228's `int num_res = (int)w->resvals.caps;`. Regression: a
* worker whose resvals zarray has TWO populated slots (idx 0 "mem", idx 1
* "r1m") but whose `.count` field has been driven down to 1 (the exact
* post-corruption shape bug-1688's OTHER root cause -- the NULL-nv->val
* WORKERS_PUSH overwrite, core_events.c -- can leave resvals in, and
* reproducible directly here without needing that whole mechanism) must
* still show BOTH resource lines, not silently truncate to the first one.
*/
static void test_info_worker_shows_all_populated_resources_when_count_is_stale(
void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t events[1];
xce_resource_t wire_mem;
xce_resource_t wire_r1m;
xce_worker_t wire_worker;
xce_worker_t* worker_ptrs[1];
xce_val_t val_mem;
xce_val_t val_r1m;
xce_name_val_t nv_mem;
xce_name_val_t nv_r1m;
xce_name_val_t* nv_ptrs[2];
xce_sched_worker_t* sw;
char* captured;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&wire_mem, 0, sizeof(wire_mem));
wire_mem.name = "mem";
TEST_ASSERT_NOT_NULL(makeResource(sim.core.resmgr, &wire_mem));
memset(&wire_r1m, 0, sizeof(wire_r1m));
wire_r1m.name = "r1m";
TEST_ASSERT_NOT_NULL(makeResource(sim.core.resmgr, &wire_r1m));
memset(&val_mem, 0, sizeof(val_mem));
val_mem.which = Value_u_fvalue;
val_mem.u.fvalue = 100.0;
memset(&nv_mem, 0, sizeof(nv_mem));
nv_mem.name = "mem";
nv_mem.val = &val_mem;
memset(&val_r1m, 0, sizeof(val_r1m));
val_r1m.which = Value_u_fvalue;
val_r1m.u.fvalue = 5.0;
memset(&nv_r1m, 0, sizeof(nv_r1m));
nv_r1m.name = "r1m";
nv_r1m.val = &val_r1m;
nv_ptrs[0] = &nv_mem;
nv_ptrs[1] = &nv_r1m;
memset(&wire_worker, 0, sizeof(wire_worker));
wire_worker.name = "hostA";
wire_worker.status = 0;
wire_worker.nresvals_to_sched = 2;
wire_worker.resvals_to_sched = nv_ptrs;
worker_ptrs[0] = &wire_worker;
memset(events, 0, sizeof(events));
events[0].type = XCE_CORE_EVENT_WORKERS_PUSH;
events[0].u.workers_push.nworkers = 1;
events[0].u.workers_push.workers = worker_ptrs;
rc = xce_core_push_events(&(sim.core), events, 1);
TEST_ASSERT_EQUAL_INT(0, rc);
sw = sched_get_worker(&(sim.core), "hostA");
TEST_ASSERT_NOT_NULL(sw);
TEST_ASSERT_EQUAL_INT(2, sw->resvals.count);
/* Directly force the sparse-vs-stale-count shape: both idx 0 ("mem")
* and idx 1 ("r1m") stay populated, but .count now under-reports the
* true occupied high-water mark -- exactly what a `.count`-bound loop
* gets wrong and a `.caps`-bound loop (this fix) does not. */
sw->resvals.count = 1;
captured = capture_dispatch(&sim, "info worker hostA");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "resource mem: value=100.00"),
"the first populated slot must still display");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "resource r1m: value=5"),
"a populated slot at an index >= the (stale) .count must still "
"display -- iterating by .count instead of .caps (bug-1688) "
"silently truncates output here");
free(captured);
printf("test_info_worker_shows_all_populated_resources_when_count_is_"
"stale: PASS\n");
}
/*
* bug-1590: repl_info_resource_one() (the "info resource <name>"
* aggregate-across-workers command) had the same defect one layer up --
* total/available aggregation silently excluded any non-numeric resource,
* while `valtype=string`/`valtype=boolean` was already labeled correctly
* two fields over. Regression: the resource's actual value must be
* reported, not silently dropped from the summary.
*/
static void test_info_resource_reports_string_value(void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t events[1];
xce_resource_t wire_resource_str;
xce_val_t val_str;
xce_resval_t rv_str;
xce_resval_t* rv_ptrs[1];
char* captured;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
/* bug-1593 (task 12.5): 'info resource' now reads a GLOBAL/SHARED
* resource's value from core->sharedInst -- pushed here via
* RESVALS_FULL (mirrors core_events.c's own RESVALS_FULL handler),
* not via a per-worker WORKERS_PUSH resval anymore. */
memset(&wire_resource_str, 0, sizeof(wire_resource_str));
wire_resource_str.name = "license_pool";
wire_resource_str.valtype = XCE_VAL_STRING;
wire_resource_str.flags = XCE_RESF_GLOBAL;
TEST_ASSERT_NOT_NULL(makeResource(sim.core.resmgr, &wire_resource_str));
memset(&val_str, 0, sizeof(val_str));
val_str.which = Value_u_svalue;
val_str.u.svalue = "linux86";
memset(&rv_str, 0, sizeof(rv_str));
rv_str.name = "license_pool";
rv_str.val = &val_str;
rv_ptrs[0] = &rv_str;
memset(events, 0, sizeof(events));
events[0].type = XCE_CORE_EVENT_RESVALS_FULL;
events[0].u.resvals_full.n = 1;
events[0].u.resvals_full.resvals = rv_ptrs;
rc = xce_core_push_events(&(sim.core), events, 1);
TEST_ASSERT_EQUAL_INT(0, rc);
captured = capture_dispatch(&sim, "info resource license_pool");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "resource license_pool: desc=(none) "
"valtype=string ordtype=incr flags=GLOBAL "
"interval=0"),
"a GLOBAL resource's definition metadata must be printed "
"(bug-1593)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "value=linux86"),
"a GLOBAL/SHARED string resource's real value must be reported, "
"read from core->sharedInst (bug-1590/bug-1593)");
TEST_ASSERT_NULL_MESSAGE(strstr(captured, "total="),
"a string resource must never be rendered "
"with a total=/available= pair");
TEST_ASSERT_NULL_MESSAGE(strstr(captured, "host-based resource"),
"a GLOBAL resource must never be reported as "
"host-based");
free(captured);
printf("test_info_resource_reports_string_value: PASS\n");
}
/*
* New requirement (2026-08-20): a GLOBAL/SHARED resource additionally
* flagged XCE_RESF_CONSUMABLE must report both its value (raw total
* capacity) and how much is currently in use, read from core->sharedInUse
* -- the cluster-wide mirror of repl_info_worker_one()'s resInUse-derived
* value/used split.
*
* bug-1727 (2026-08-30): corrected from the original "value=
* avail+used" expectation -- same double-count fix as the per-worker case
* above.
*/
static void test_info_resource_reports_consumable_value_and_used(void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t events[1];
xce_resource_t wire_resource;
xce_resource_t* r;
xce_val_t avail_val;
xce_val_t used_val;
xce_resval_t rv;
xce_resval_t* rv_ptrs[1];
char* captured;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&wire_resource, 0, sizeof(wire_resource));
wire_resource.name = "licenses";
wire_resource.valtype = XCE_VAL_NUMERIC;
wire_resource.flags = XCE_RESF_SHARED | XCE_RESF_CONSUMABLE;
r = makeResource(sim.core.resmgr, &wire_resource);
TEST_ASSERT_NOT_NULL(r);
memset(&avail_val, 0, sizeof(avail_val));
avail_val.which = Value_u_fvalue;
avail_val.u.fvalue = 8.0;
memset(&rv, 0, sizeof(rv));
rv.name = "licenses";
rv.val = &avail_val;
rv_ptrs[0] = &rv;
memset(events, 0, sizeof(events));
events[0].type = XCE_CORE_EVENT_RESVALS_FULL;
events[0].u.resvals_full.n = 1;
events[0].u.resvals_full.resvals = rv_ptrs;
rc = xce_core_push_events(&(sim.core), events, 1);
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&used_val, 0, sizeof(used_val));
used_val.which = Value_u_fvalue;
used_val.u.fvalue = 3.0;
zarray_put(&(sim.core.sharedInUse), r->idx, dup_val(&used_val));
captured = capture_dispatch(&sim, "info resource licenses");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "value=8.00 used=3.00"),
"a CONSUMABLE global resource must report value=<raw total, not "
"avail+used> and used=inUse, sourced from core->sharedInUse "
"(bug-1727)");
free(captured);
printf("test_info_resource_reports_consumable_value_and_used: PASS\n");
}
/*
* bug-1593 (task 12.5): `info resource all` must list only resources
* flagged XCE_RESF_GLOBAL/XCE_RESF_SHARED -- a host-based resource (no
* such flag, e.g. cpus/mem) is `info worker`'s job, never this command's.
*/
static void test_info_resource_all_lists_only_global_shared_resources(void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t events[1];
xce_resource_t wire_resource_shared;
xce_resource_t wire_resource_hostbased;
xce_val_t val;
xce_resval_t rv;
xce_resval_t* rv_ptrs[1];
char* captured;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&wire_resource_shared, 0, sizeof(wire_resource_shared));
wire_resource_shared.name = "licenses";
wire_resource_shared.valtype = XCE_VAL_NUMERIC;
wire_resource_shared.flags = XCE_RESF_SHARED;
TEST_ASSERT_NOT_NULL(
makeResource(sim.core.resmgr, &wire_resource_shared));
memset(&wire_resource_hostbased, 0, sizeof(wire_resource_hostbased));
wire_resource_hostbased.name = "mem";
wire_resource_hostbased.valtype = XCE_VAL_NUMERIC;
/* flags left 0 -- host-based, no GLOBAL/SHARED bit. */
TEST_ASSERT_NOT_NULL(
makeResource(sim.core.resmgr, &wire_resource_hostbased));
memset(&val, 0, sizeof(val));
val.which = Value_u_fvalue;
val.u.fvalue = 8.0;
memset(&rv, 0, sizeof(rv));
rv.name = "licenses";
rv.val = &val;
rv_ptrs[0] = &rv;
memset(events, 0, sizeof(events));
events[0].type = XCE_CORE_EVENT_RESVALS_FULL;
events[0].u.resvals_full.n = 1;
events[0].u.resvals_full.resvals = rv_ptrs;
rc = xce_core_push_events(&(sim.core), events, 1);
TEST_ASSERT_EQUAL_INT(0, rc);
captured = capture_dispatch(&sim, "info resource all");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "resource licenses:"),
"a SHARED resource must appear in 'info resource all' (bug-1593)");
TEST_ASSERT_NULL_MESSAGE(
strstr(captured, "resource mem:"),
"a host-based resource must NOT appear in 'info resource all' -- "
"that is info worker's job, not info resource's (bug-1593)");
free(captured);
printf("test_info_resource_all_lists_only_global_shared_resources: "
"PASS\n");
}
/*
* bug-1593 (task 12.5): a host-based resource's own NAMED lookup
* (`info resource <name>`, as opposed to `all`) still resolves it, so an
* operator can check its metadata/flags -- but reports no fabricated
* cluster-wide value, directing to `info worker` instead.
*/
static void test_info_resource_named_lookup_reports_host_based_note(void)
{
xce_sched_sim_driver_t sim;
xce_resource_t wire_resource;
char* captured;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&wire_resource, 0, sizeof(wire_resource));
wire_resource.name = "cpus";
wire_resource.valtype = XCE_VAL_NUMERIC;
TEST_ASSERT_NOT_NULL(makeResource(sim.core.resmgr, &wire_resource));
captured = capture_dispatch(&sim, "info resource cpus");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "resource cpus: desc=(none) valtype=numeric "
"ordtype=incr flags=(none) interval=0"),
"a host-based resource's metadata must still be reported by a "
"named lookup (bug-1593)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "host-based resource -- see 'info worker <name>' "
"for per-host values"),
"a host-based resource must direct the operator to 'info worker', "
"never a fabricated cluster-wide value (bug-1593)");
free(captured);
printf("test_info_resource_named_lookup_reports_host_based_note: "
"PASS\n");
}
/*
* 2026-08-20 (live-deployment finding): `info resource all` printed
* literally nothing when every registered resource was host-based (no
* GLOBAL/SHARED resource defined at all) -- correct per bug-1593's own
* filtering rule, but indistinguishable from a REPL/dispatch bug from the
* operator's side with no output at all. Must say so explicitly.
*/
static void test_info_resource_all_reports_when_none_are_global_shared(void)
{
xce_sched_sim_driver_t sim;
xce_resource_t wire_resource;
char* captured;
int rc;
memset(&sim, 0, sizeof(sim));
rc = xce_sched_core_init(&(sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
memset(&wire_resource, 0, sizeof(wire_resource));
wire_resource.name = "slots";
wire_resource.valtype = XCE_VAL_NUMERIC;
/* flags left 0 -- host-based, no GLOBAL/SHARED bit, same as
* test_info_resource_all_lists_only_global_shared_resources()'s own
* "mem" fixture above -- but here it is the ONLY resource registered. */
TEST_ASSERT_NOT_NULL(makeResource(sim.core.resmgr, &wire_resource));
captured = capture_dispatch(&sim, "info resource all");
TEST_ASSERT_NULL_MESSAGE(
strstr(captured, "resource slots:"),
"a host-based resource must never appear in 'info resource all' "
"(bug-1593, unchanged)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "no global/shared resources defined"),
"'info resource all' must say something when zero resources "
"qualify, not print nothing at all");
free(captured);
printf("test_info_resource_all_reports_when_none_are_global_shared: "
"PASS\n");
}
/*
* bug-1596 (task 12.5): repl_split_tokens() must strip one matching pair
* of surrounding quote characters -- an operator quoting an argument out
* of habit (e.g. pasted straight from another command's own output) must
* not get a silent wrong-answer failure.
*/
static void test_info_job_accepts_quoted_jobid_argument(void)
{
xce_sched_sim_driver_t sim;
char* captured_dquoted;
char* captured_squoted;
memset(&sim, 0, sizeof(sim));
build_fixture_core(&(sim.core), 777);
captured_dquoted = capture_dispatch(&sim, "info job \"777\"");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured_dquoted, "job 777"),
"a double-quoted jobid argument must resolve the same job as the "
"unquoted form (bug-1596)");
TEST_ASSERT_NULL_MESSAGE(
strstr(captured_dquoted, "no such job"),
"must not silently mis-parse the quote characters into jobid 0");
free(captured_dquoted);
captured_squoted = capture_dispatch(&sim, "info job '777'");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured_squoted, "job 777"),
"a single-quoted jobid argument must resolve the same job too");
TEST_ASSERT_NULL_MESSAGE(strstr(captured_squoted, "no such job"), "");
free(captured_squoted);
printf("test_info_job_accepts_quoted_jobid_argument: PASS\n");
}
static void test_info_summary_reports_counts(void)
{
xce_sched_sim_driver_t sim;
char* captured;
memset(&sim, 0, sizeof(sim));
build_fixture_core(&(sim.core), 555);
captured = capture_dispatch(&sim, "info summary");
TEST_ASSERT_NOT_NULL(strstr(captured, "1 jobs (1 pending, 0 running)"));
TEST_ASSERT_NOT_NULL(strstr(captured, "0 workers (0 online)"));
TEST_ASSERT_NOT_NULL(strstr(captured, "1 queues"));
TEST_ASSERT_NOT_NULL(strstr(captured, "0 resources"));
free(captured);
printf("test_info_summary_reports_counts: PASS\n");
}
static void test_info_session_reports_position_after_synthetic_events(void)
{
xce_sched_sim_driver_t sim;
xce_core_event_t ev;
char* captured;
memset(&sim, 0, sizeof(sim));
build_fixture_core(&(sim.core), 1);
sim.repl_state = repl_state_ensure(&sim);
TEST_ASSERT_NOT_NULL(sim.repl_state);
/* No breakpoints registered -- xce_repl_on_event()/on_begin_round()
* only update position counters here, never block on stdin. */
memset(&ev, 0, sizeof(ev));
ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE;
ev.u.job_state_change.jobid = 1;
xce_repl_on_event(&sim, &ev);
xce_repl_on_event(&sim, &ev);
xce_repl_on_begin_round(&sim, 9000ULL);
captured = capture_dispatch(&sim, "info session");
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, "event_index=2"),
"two xce_repl_on_event() calls must "
"advance event_index to exactly 2");
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, "sessions=1"),
"one xce_repl_on_begin_round() call must "
"advance session_count to exactly 1");
TEST_ASSERT_NOT_NULL(strstr(captured, "round=9000"));
free(captured);
printf("test_info_session_reports_position_after_synthetic_events: "
"PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Group D: full-integration pause proof (scripted stdin + capture, same */
/* technique test_repl_execution_control.c's own Test 2 establishes). */
/* ---------------------------------------------------------------------- */
static void make_tmp_dir(char* out, size_t out_len, const char* suffix)
{
char tmpl[PATH_MAX];
snprintf(tmpl, sizeof(tmpl), "/tmp/sched_replbpi%s_XXXXXX", suffix);
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
TEST_ASSERT_TRUE(strlen(tmpl) < out_len);
strcpy(out, tmpl);
}
static void build_fixture_scheduler(xce_scheduler_t* sched, char* record_dir)
{
int rc;
memset(sched, 0, sizeof(*sched));
mylock_init(&(sched->record_lock));
rc = xce_sched_core_init(&(sched->core));
TEST_ASSERT_EQUAL_INT(0, rc);
sched->core.job_pick_plugin = (char*)"builtin_fcfs";
sched->record_dir = record_dir;
}
static FILE* write_header_and_snapshot(char* out_path, size_t out_path_len,
const char* suffix)
{
char record_dir[PATH_MAX];
xce_scheduler_t sched;
int rc;
make_tmp_dir(record_dir, sizeof(record_dir), suffix);
build_fixture_scheduler(&sched, record_dir);
rc = sched_record_start(&sched, "fixture.replay");
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_path_len);
strcpy(out_path, sched.record_file_path);
return sched.record_file;
}
static void append_queue_update_event(FILE* f, uint64_t ts,
const char* queue_name)
{
xce_queue_t queue;
replay_event_t src;
struct capn c;
struct capn_segment* cs;
ReplayEvent_ptr p;
memset(&queue, 0, sizeof(queue));
queue.queue = (char*)queue_name;
queue.status = QUEUE_STAT_OPEN;
queue.priority = 1;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_queueUpdate;
src.body.queue_update = &queue;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_ReplayEvent_ptr(cs, &p, &src);
capn_setp(capn_root(&c), 0, p.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
static void append_jobs_push_event(FILE* f, uint64_t ts, uint64_t jobid,
const char* queue_name)
{
job_submit_data_t submit;
xce_job_t job;
xce_job_t* jobs[1];
xce_jobs_data_t jobs_data;
replay_event_t src;
struct capn c;
struct capn_segment* cs;
ReplayEvent_ptr p;
memset(&submit, 0, sizeof(submit));
submit.queue = (char*)queue_name;
submit.user = (char*)"alice";
submit.mintasks = 1;
submit.maxtasks = 1;
memset(&job, 0, sizeof(job));
job.jobid = jobid;
job.state = JOB_STAT_PEND;
job.submit = &submit;
jobs[0] = &job;
memset(&jobs_data, 0, sizeof(jobs_data));
jobs_data.n_total = 1;
jobs_data.count = 1;
jobs_data.jobs = jobs;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_jobsPush;
src.body.jobs_push = &jobs_data;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_ReplayEvent_ptr(cs, &p, &src);
capn_setp(capn_root(&c), 0, p.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
/* jobStateChange payload used purely as a cheap, already-proven-safe
* event shape (a plain value struct, no nested pointers) -- same technique
* and same rationale test_sim_event_loop.c's own
* append_jobstatechange_event() establishes: core_events.c WARN+skips an
* unknown jobid rather than fail, so neither breakpoint test below needs a
* real job to exist in the loaded snapshot. */
static void append_jobstatechange_event(FILE* f, uint64_t ts, uint64_t jobid)
{
job_state_change_event_t change;
replay_event_t src;
struct capn c;
struct capn_segment* cs;
ReplayEvent_ptr p;
memset(&change, 0, sizeof(change));
change.jobid = jobid;
change.old_state = JOB_STAT_PEND;
change.new_state = JOB_STAT_RUN;
change.exit_code = 0;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_jobStateChange;
src.body.job_state_change = &change;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_ReplayEvent_ptr(cs, &p, &src);
capn_setp(capn_root(&c), 0, p.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
static void append_replay_session(FILE* f, uint64_t begin_round)
{
struct capn c;
struct capn_segment* cs;
ReplaySession_ptr sp;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
sp = new_ReplaySession(cs);
ReplaySession_set_beginRound(sp, begin_round);
capn_setp(capn_root(&c), 0, sp.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
static void append_end_of_stream_marker(FILE* f)
{
replay_event_t src;
struct capn c;
struct capn_segment* cs;
ReplayEvent_ptr p;
memset(&src, 0, sizeof(src));
src.body_which = ReplayEvent_body_endSession;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_ReplayEvent_ptr(cs, &p, &src);
capn_setp(capn_root(&c), 0, p.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
static void write_text_file(const char* path, const char* content)
{
FILE* f = fopen(path, "w");
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_TRUE(fputs(content, f) >= 0);
fclose(f);
}
static char* read_whole_file(const char* path)
{
FILE* f = fopen(path, "rb");
long len;
char* buf;
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_EQUAL_INT(0, fseek(f, 0, SEEK_END));
len = ftell(f);
TEST_ASSERT_TRUE(len >= 0);
rewind(f);
buf = (char*)malloc((size_t)len + 1);
TEST_ASSERT_NOT_NULL(buf);
if (len > 0) {
TEST_ASSERT_EQUAL_INT((int)len, (int)fread(buf, 1, (size_t)len, f));
}
buf[len] = '\0';
fclose(f);
return buf;
}
/*
* task 12.5 (bug-1594): a completed --interactive run now re-enters the
* REPL at replay completion instead of returning, so any Group D script
* that runs to the fixture's true EOF needs an explicit trailing `quit`
* and must run in a FORKED child -- otherwise the implicit-EOF-quit (or
* the scripted `quit` itself) calls exit(0) inside this shared unity test
* binary. Same fork+capture-file pattern test_repl_execution_control.c's
* own Test 3 and test_repl_snapshot_params.c already establish.
*/
static void run_group_d_child(const char* replay_path,
const char* script_path,
const char* capture_path)
{
xce_sched_sim_driver_t sim;
sched_sim_args_t args;
if (freopen(script_path, "r", stdin) == NULL) {
_exit(120);
}
if (freopen(capture_path, "w", stdout) == NULL) {
_exit(121);
}
memset(&sim, 0, sizeof(sim));
memset(&args, 0, sizeof(args));
args.replay_path = replay_path;
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
args.sim_speed = 0;
args.interactive = true;
(void)sched_sim_driver_run(&sim, &args);
fflush(stdout);
_exit(111); /* unreachable -- the script's trailing `quit` must
* already have exited */
}
/* Forks run_group_d_child(), waits for it, and returns the captured
* stdout as a malloc'd buffer (caller frees). Asserts a clean exit(0). */
static char* run_group_d_and_capture(const char* replay_path,
const char* script_path,
const char* capture_path)
{
pid_t pid = fork();
int status;
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed");
if (pid == 0) {
run_group_d_child(replay_path, script_path, capture_path);
_exit(112); /* unreachable */
}
TEST_ASSERT_EQUAL_INT_MESSAGE(pid, waitpid(pid, &status, 0),
"waitpid must reap the REPL child");
TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status),
"the REPL child must exit normally via "
"quit's exit(0), not crash/signal");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, WEXITSTATUS(status),
"quit's exit(0) is the only correct "
"exit path");
return read_whole_file(capture_path);
}
static void test_event_breakpoint_pauses_repl_at_matching_event(void)
{
char replay_path[PATH_MAX];
char aux_dir[PATH_MAX];
char script_path[PATH_MAX];
char capture_path[PATH_MAX];
char round_marker[96];
char jobid_buf[64];
FILE* f;
char* captured;
char* bp_pos;
char* round_pos;
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
"_evbp");
TEST_ASSERT_NOT_NULL(f);
append_queue_update_event(f, 1000ULL, "normal");
append_jobs_push_event(f, 1500ULL, 4242ULL, "normal");
append_jobstatechange_event(f, 1600ULL, 111ULL); /* event #3: no match */
append_jobstatechange_event(f, 1700ULL, 222ULL); /* event #4: MATCH */
append_replay_session(f, 2000ULL);
append_end_of_stream_marker(f);
fclose(f);
make_tmp_dir(aux_dir, sizeof(aux_dir), "_evbpaux");
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
/* task 12.5 (bug-1594): trailing `quit` -- the second `continue` now
* runs this one-session fixture all the way to replay completion,
* which re-enters the REPL instead of returning. */
write_text_file(script_path,
"break JOB_STATE_CHANGE jobid=222\ncontinue\ncontinue\nquit\n");
captured = run_group_d_and_capture(replay_path, script_path, capture_path);
/* scheduler-sim-repl-verbosity task 4.2 (Req 2.3, bug-1610): under
* `continue` mode sim_print_decision()'s bare per-decision line no
* longer prints -- the Round Rollup (repl_rollup.c) reports the
* session's decision instead, deferred until the collapsed range
* ends. This fixture has exactly one round, so that range is only
* flushed (and printed) by xce_repl_on_replay_complete()'s
* repl_round_collapse_flush() call, triggered by the script's
* trailing `quit`. Look for that round-rollup summary's
* "jobids=<...>" field instead of the old bare "PEND jobid=<...>"
* line as evidence the session's decision was eventually processed
* -- bug-1591's fmt_jobid()-vs-raw-%llu fix applies equally to
* repl_fmt_jobid_range()'s single-jobid rendering (repl.c), which is
* byte-identical to fmt_jobid()'s own non-array format. */
fmt_jobid(jobid_buf, sizeof(jobid_buf), 4242ULL);
snprintf(round_marker, sizeof(round_marker), "jobids=%s", jobid_buf);
bp_pos = strstr(captured, "hit breakpoint 1 @ event #4: JOB_STATE_CHANGE");
round_pos = strstr(captured, round_marker);
TEST_ASSERT_NOT_NULL_MESSAGE(
bp_pos, "the breakpoint must fire exactly at event #4 (jobid=222), "
"not event #3 (jobid=111)");
TEST_ASSERT_NULL_MESSAGE(
strstr(captured, "@ event #3"),
"the breakpoint must NOT have fired on event #3 (jobid=111)");
TEST_ASSERT_NOT_NULL_MESSAGE(round_pos,
"the session's own PEND decision must "
"still have run (reported via the round "
"rollup summary) after the resumed "
"breakpoint pause");
TEST_ASSERT_TRUE_MESSAGE(bp_pos < round_pos,
"the event breakpoint (mid-batch, before "
"push_events) must be reported BEFORE the "
"session's own decision output");
free(captured);
printf("test_event_breakpoint_pauses_repl_at_matching_event: PASS\n");
}
static void test_decision_breakpoint_pauses_after_decision_output(void)
{
char replay_path[PATH_MAX];
char aux_dir[PATH_MAX];
char script_path[PATH_MAX];
char capture_path[PATH_MAX];
char pend_marker[80];
char bp_marker[112];
char jobid_buf[64];
FILE* f;
char* captured;
char* pend_pos;
char* bp_pos;
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
"_decbp");
TEST_ASSERT_NOT_NULL(f);
append_queue_update_event(f, 1000ULL, "normal");
append_jobs_push_event(f, 1500ULL, 555ULL, "normal");
append_replay_session(f, 2000ULL);
append_end_of_stream_marker(f);
fclose(f);
make_tmp_dir(aux_dir, sizeof(aux_dir), "_decbpaux");
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
/* scheduler-sim-repl-verbosity task 4.2 (Req 2.3, bug-1610): `continue`
* mode now suppresses sim_print_decision()'s bare per-decision line
* entirely (the Round Rollup reports it instead, deferred until the
* collapsed range ends), so this fixture's single round would leave no
* "PEND jobid=..." text to anchor the print-before-breakpoint-check
* ordering this test exists to prove. That ordering
* (sim_print_decision() then xce_repl_on_decision()'s breakpoint
* check, sim_loop.c) is a mode-independent call-sequence invariant --
* xce_repl_check_decision() (repl.h) gates only on `sim->interactive`,
* never on REPL mode -- so `step` mode observes it exactly as well as
* `continue` did, and `step` still prints the bare per-decision line
* unconditionally (Requirement 5.1, unaffected by task 4.2). `step 1`
* pauses after session 1 completes, but the decision breakpoint below
* fires and nests its own repl_command_loop() first, so the trailing
* `quit` is read from within THAT nested loop -- same "quit ends the
* child deterministically" requirement as the `continue` scripts above,
* just reached via a different pause mechanism. */
write_text_file(script_path, "break DECISION type=PEND\nstep 1\nquit\n");
captured = run_group_d_and_capture(replay_path, script_path, capture_path);
/* bug-1591: sim_print_decision() (sim_loop.c) and repl_break.c's
* breakpoint-hit message now render jobids via fmt_jobid(), not raw
* %llu -- these markers must match that same human-readable form. */
fmt_jobid(jobid_buf, sizeof(jobid_buf), 555ULL);
snprintf(pend_marker, sizeof(pend_marker), "PEND jobid=%s", jobid_buf);
snprintf(bp_marker, sizeof(bp_marker),
"hit breakpoint 1 @ decision: PEND jobid=%s", jobid_buf);
pend_pos = strstr(captured, pend_marker);
bp_pos = strstr(captured, bp_marker);
TEST_ASSERT_NOT_NULL_MESSAGE(pend_pos, "the fixture's own PEND decision "
"must have run");
TEST_ASSERT_NOT_NULL_MESSAGE(bp_pos, "the decision breakpoint must "
"have fired for that decision");
TEST_ASSERT_TRUE_MESSAGE(pend_pos < bp_pos,
"the decision breakpoint check happens AFTER "
"sim_print_decision() (AC 22's 'after "
"verify/output-write' ordering)");
free(captured);
printf("test_decision_breakpoint_pauses_after_decision_output: PASS\n");
}
/*
* bug-1595 (task 12.5): `step`/`continue` must echo each input event, in
* the order it is decoded, BEFORE the session's own resulting decision --
* v1 only ever printed decisions, leaving every event silent unless a
* breakpoint happened to match it.
*/
static void test_step_echoes_events_before_session_decisions(void)
{
char replay_path[PATH_MAX];
char aux_dir[PATH_MAX];
char script_path[PATH_MAX];
char capture_path[PATH_MAX];
char pend_marker[80];
char jobid_buf[64];
FILE* f;
char* captured;
char* jobpush_pos;
char* statechange_pos;
char* pend_pos;
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
"_evorder");
TEST_ASSERT_NOT_NULL(f);
append_queue_update_event(f, 1000ULL, "normal");
append_jobs_push_event(f, 1500ULL, 6161ULL, "normal");
/* Unrelated jobid (7171, not 6161) -- deliberately does NOT match the
* job just pushed above. append_jobstatechange_event() is used here
* purely as a second event type to verify echo ordering against
* (see its own header comment: core_events.c WARN+skips an unknown
* jobid rather than fail). Reusing 6161 here would actually transition
* the real pushed job PEND->RUN before the session runs, so the picker
* would never offer it as a PEND candidate and the session's own
* decision this test asserts on below would never be produced. */
append_jobstatechange_event(f, 1600ULL, 7171ULL);
append_replay_session(f, 2000ULL);
append_end_of_stream_marker(f);
fclose(f);
make_tmp_dir(aux_dir, sizeof(aux_dir), "_evorderaux");
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
/* "step 1" pauses right after session 1 completes -- never reaches
* true replay EOF, so a plain trailing `quit` is enough here (same
* precedent test_step_1_processes_exactly_one_session_then_quit_exits,
* test_repl_execution_control.c, already establishes). */
write_text_file(script_path, "step 1\nquit\n");
captured = run_group_d_and_capture(replay_path, script_path, capture_path);
fmt_jobid(jobid_buf, sizeof(jobid_buf), 6161ULL);
snprintf(pend_marker, sizeof(pend_marker), "PEND jobid=%s", jobid_buf);
jobpush_pos = strstr(captured, "[sim] event: JOB_PUSH");
statechange_pos = strstr(captured, "[sim] event: JOB_STATE_CHANGE");
pend_pos = strstr(captured, pend_marker);
TEST_ASSERT_NOT_NULL_MESSAGE(jobpush_pos,
"a JOBS_PUSH event must be echoed "
"(bug-1595)");
TEST_ASSERT_NOT_NULL_MESSAGE(statechange_pos,
"a JOB_STATE_CHANGE event must be echoed "
"(bug-1595)");
TEST_ASSERT_NOT_NULL_MESSAGE(pend_pos,
"the session's own decision must still "
"be printed");
TEST_ASSERT_TRUE_MESSAGE(jobpush_pos < statechange_pos,
"events must be echoed in the order they are "
"decoded");
TEST_ASSERT_TRUE_MESSAGE(statechange_pos < pend_pos,
"events must be echoed BEFORE the session's "
"resulting decision (bug-1595)");
{
char* ts_line_pos = strstr(captured, "[sim] t=");
TEST_ASSERT_NOT_NULL_MESSAGE(
ts_line_pos,
"each echoed event must be preceded by a t=.../session=... "
"line so an operator can tell when/which session it happened "
"in");
TEST_ASSERT_TRUE_MESSAGE(
ts_line_pos < jobpush_pos,
"the t=.../session=... line must precede the event it "
"describes");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "session=1"),
"the first (and only) session's events must be tagged "
"session=1 -- xce_repl_on_begin_round() increments "
"session_count before this session's event batch is pushed");
}
free(captured);
printf("test_step_echoes_events_before_session_decisions: PASS\n");
}
/*
* New requirement (2026-08-20): loading a replay file reports its total
* session count up front (sim.c's sched_sim_driver_start(), via
* sim_prescan_session_count()'s ftell/fseek pre-scan), so an operator knows
* how many `step`s exist before starting to walk through them.
*/
static void test_replay_load_reports_total_session_count(void)
{
char replay_path[PATH_MAX];
char aux_dir[PATH_MAX];
char script_path[PATH_MAX];
char capture_path[PATH_MAX];
FILE* f;
char* captured;
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
"_sessioncount");
TEST_ASSERT_NOT_NULL(f);
append_jobs_push_event(f, 1000ULL, 8181ULL, "normal");
append_replay_session(f, 1500ULL);
append_jobs_push_event(f, 2000ULL, 8182ULL, "normal");
append_replay_session(f, 2500ULL);
append_end_of_stream_marker(f);
fclose(f);
make_tmp_dir(aux_dir, sizeof(aux_dir), "_sessioncountaux");
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
write_text_file(script_path, "quit\n");
captured = run_group_d_and_capture(replay_path, script_path,
capture_path);
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "[sim] replay file contains 2 sessions"),
"loading a 2-session replay file must report the total session "
"count up front, before any step/continue command runs");
free(captured);
printf("test_replay_load_reports_total_session_count: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_break_creates_breakpoint_with_filters);
RUN_TEST(test_break_unrecognized_event_type_rejected);
RUN_TEST(test_break_malformed_filter_rejected);
RUN_TEST(test_delete_by_id_removes_one_breakpoint);
RUN_TEST(test_delete_all_removes_all_breakpoints);
RUN_TEST(test_delete_non_numeric_id_rejected);
RUN_TEST(test_info_break_lists_breakpoints_with_filters);
RUN_TEST(test_event_breakpoint_matches_jobid_filter);
RUN_TEST(test_event_breakpoint_matches_state_filter);
RUN_TEST(test_decision_breakpoint_matches_type_filter);
RUN_TEST(test_begin_round_breakpoint_bare_matches_filtered_does_not);
RUN_TEST(test_info_job_reports_state_queue_priority);
RUN_TEST(test_info_job_unknown_reports_not_found);
RUN_TEST(test_info_queue_reports_status_and_queued_jobs);
RUN_TEST(test_info_worker_reports_resource_values);
RUN_TEST(test_info_worker_reports_consumable_value_and_used);
RUN_TEST(test_info_worker_reports_string_and_boolean_resource_values);
RUN_TEST(test_info_worker_shows_all_populated_resources_when_count_is_stale);
RUN_TEST(test_info_resource_reports_string_value);
RUN_TEST(test_info_resource_reports_consumable_value_and_used);
RUN_TEST(test_info_resource_all_lists_only_global_shared_resources);
RUN_TEST(test_info_resource_named_lookup_reports_host_based_note);
RUN_TEST(test_info_resource_all_reports_when_none_are_global_shared);
RUN_TEST(test_info_job_accepts_quoted_jobid_argument);
RUN_TEST(test_info_summary_reports_counts);
RUN_TEST(test_info_session_reports_position_after_synthetic_events);
RUN_TEST(test_event_breakpoint_pauses_repl_at_matching_event);
RUN_TEST(test_decision_breakpoint_pauses_after_decision_output);
RUN_TEST(test_step_echoes_events_before_session_decisions);
RUN_TEST(test_replay_load_reports_total_session_count);
return UNITY_END();
}