/* * test_repl_breakpoints_inspection.c - contract test for the interactive * simulation REPL's breakpoint and state-inspection slice (scheduler-core * spec, task 12.2, Req 5 AC 22). * * Observables under test (task 12.2's own text): * (a) setting a breakpoint on a specific event type and field value * pauses the REPL exactly when a matching event is about to be * processed; * (b) each inspection command reports state matching what a snapshot * dump of the same moment would show. * * Four groups, at increasing granularity (same "unit-level precision PLUS * integration-level end-to-end proof" split test_repl_execution_control.c's * own header comment establishes for task 12.1): * * Group A (unit, white-box): `break`/`delete`/`info break` command * parsing and listing, driven via repl_dispatch() directly, asserting on * sim.repl_state->breakpoints[]'s own fields and captured stdout. * * Group B (unit, white-box): the pure breakpoint-matching predicates * (repl_find_event_breakpoint()/repl_find_decision_breakpoint()/ * repl_find_begin_round_breakpoint(), repl_break.c) driven directly * against hand-built xce_core_event_t/xce_decision_t values -- the exact * "not a decoy, the real predicate" proof test_repl_execution_control.c's * own Test 1 (repl_should_pause()) already establishes for task 12.1. * * Group C (unit, white-box): state-inspection dumps (`info job`/`queue`/ * `summary`/`session`) driven via repl_dispatch() against a REAL * xce_sched_core_t populated through a genuine xce_core_push_events() * call (same in-memory QUEUE_UPDATE/JOBS_PUSH xce_core_event_t * construction test_core_events.c's own fixtures already establish -- no * capnp file needed for this granularity), captured via stdout and * asserted against the exact field values pushed in. `info worker`/ * `info resource` share the identical iter_make()/iter_next()/printf * code shape as `info queue` (same file, repl_info.c) and are not * separately re-proven here -- a deliberate, documented scope decision, * not an oversight. * * Group D (integration, single-process, scripted stdin): a REAL * sched_sim_driver_run() against a real `.replay` fixture, proving * Observable (a) end to end for both an event breakpoint * (JOB_STATE_CHANGE jobid=) and a decision breakpoint (DECISION type=), * by capturing stdout and asserting ordering the same way * test_repl_execution_control.c's own Test 2 does for the pre-replay * pause. * * White-box #include set: the SAME TU set test_repl_execution_control.c * establishes for task 12.1, PLUS the new ../src/repl_break.c/repl_info.c * under test (task 12.2's own 800-line-cap split of repl.c -- see * repl_internal.h's own "Why a split at all" section). */ #include #include #include #include #include #include #include #include #include #include #include #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= 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 " * 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= 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 `, 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 ' " "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(); }