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
2095 lines
80 KiB
C
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();
|
|
}
|