fix(scheduler): clock/resval/jobid-format fixes plus REPL live-operator gaps (task 12.5)

Bugs fixed, discovered across a day of live-operator testing against a
real deployed cluster:

- bug-1584: clock anchors never initialized to real wall-clock values,
  timestamps showed system uptime instead of Unix epoch.
- bug-1585: heap-use-after-free in the normal-mode driver's periodic
  resval update path -- core_events.c's RESVALS_FULL/_DELTA handlers do a
  shallow zarray_put() of the wire-decoded xce_val_t*, but driver.c's
  release_translated_payload() unconditionally freed the same decoded
  array right after, so the next periodic report for the same
  worker+resource triggered a UAF read via kill_sched_resval(). New
  regression test: test_resval_ownership.c.
- bug-1587/1588/1591: raw-ns/raw-uint64 jobid/timestamp prints across the
  REPL/SIM family converted to the same human-readable formatting already
  used elsewhere (fmt_jobid(), timestamp formatting).
- bug-1590: repl_info.c's XCE_VAL_NUMERIC checks corrected to
  Value_u_fvalue, fixing `info worker`/`info resource` falling back to
  0.00 for real numeric resvals.

task 12.5 (Req 5.22 amendment) -- five REPL gaps surfaced by a live
operator session, each with its own bug id:
- bug-1594: `continue`/`step` reaching EOF with no breakpoint hit now
  re-enters the REPL via xce_repl_on_replay_complete() instead of exiting
  the process; step/continue issued after replay is already complete
  re-prompt instead of silently exiting.
- bug-1595: `step`/`continue` now echo each input event (not just
  decisions) via repl_print_event_summary(), in real event/decision
  order.
- bug-1593: `info resource <name>|all` now reports resource definition
  metadata restricted to XCE_RESF_GLOBAL/XCE_RESF_SHARED resources, read
  from core->sharedInst, instead of aggregating host-based resources
  across every worker.
- bug-1596: repl_split_tokens() strips one matching pair of surrounding
  quote characters from each token.

See requirements.md's Req 5 AC 22 "Amendment (2026-08-19)" for the full
rationale on each of the five task 12.5 gaps.

Full scheduler suite green (41/42; the one remaining failure,
test_recordstart_symlink_escape_replies_error, is the pre-existing
unrelated bug-1560).
This commit is contained in:
Rongsong Shen 2026-08-19 09:48:46 +00:00
parent 515f4ca098
commit c9acd502c0
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
32 changed files with 2547 additions and 295 deletions

View file

@ -52,6 +52,7 @@
*/
#include <errno.h>
#include <limits.h>
#include <sys/wait.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
@ -638,6 +639,8 @@ static void test_info_job_unknown_reports_not_found(void)
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));
@ -645,10 +648,16 @@ static void test_info_queue_reports_status_and_queued_jobs(void)
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=4242"),
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, jobid_marker),
"the fixture's own PEND job must appear "
"in the queue's queued-job listing");
@ -736,6 +745,316 @@ static void test_info_worker_reports_resource_values(void)
printf("test_info_worker_reports_resource_values: 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-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 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");
}
/*
* 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 "
"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");
}
/*
* 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;
@ -989,20 +1308,102 @@ static void write_text_file(const char* path, const char* content)
fclose(f);
}
static xce_sched_sim_driver_t g_d_sim;
static sched_sim_args_t g_d_args;
static int g_d_rc;
static void run_driver_for_group_d_capture(void)
static char* read_whole_file(const char* path)
{
g_d_rc = sched_sim_driver_run(&g_d_sim, &g_d_args);
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 script_dir[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* bp_pos;
@ -1020,26 +1421,25 @@ static void test_event_breakpoint_pauses_repl_at_matching_event(void)
append_end_of_stream_marker(f);
fclose(f);
make_tmp_dir(script_dir, sizeof(script_dir), "_evbpscript");
snprintf(script_path, sizeof(script_path), "%s/script.txt", script_dir);
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\n");
"break JOB_STATE_CHANGE jobid=222\ncontinue\ncontinue\nquit\n");
TEST_ASSERT_NOT_NULL(freopen(script_path, "r", stdin));
captured = run_group_d_and_capture(replay_path, script_path, capture_path);
memset(&g_d_sim, 0, sizeof(g_d_sim));
memset(&g_d_args, 0, sizeof(g_d_args));
g_d_args.replay_path = replay_path;
g_d_args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
g_d_args.sim_speed = 0;
g_d_args.interactive = true;
captured = capture_stdout_around(run_driver_for_group_d_capture);
TEST_ASSERT_EQUAL_INT(0, g_d_rc);
/* bug-1591: sim_print_decision() (sim_loop.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(pend_marker, sizeof(pend_marker), "PEND jobid=%s", jobid_buf);
bp_pos = strstr(captured, "hit breakpoint 1 @ event #4: JOB_STATE_CHANGE");
pend_pos = strstr(captured, "PEND jobid=4242");
pend_pos = strstr(captured, pend_marker);
TEST_ASSERT_NOT_NULL_MESSAGE(
bp_pos, "the breakpoint must fire exactly at event #4 (jobid=222), "
@ -1064,8 +1464,12 @@ static void test_event_breakpoint_pauses_repl_at_matching_event(void)
static void test_decision_breakpoint_pauses_after_decision_output(void)
{
char replay_path[PATH_MAX];
char script_dir[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;
@ -1081,25 +1485,26 @@ static void test_decision_breakpoint_pauses_after_decision_output(void)
append_end_of_stream_marker(f);
fclose(f);
make_tmp_dir(script_dir, sizeof(script_dir), "_decbpscript");
snprintf(script_path, sizeof(script_path), "%s/script.txt", script_dir);
write_text_file(script_path, "break DECISION type=PEND\ncontinue\ncontinue\n");
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);
/* task 12.5 (bug-1594): trailing `quit` -- see the event-breakpoint
* test above for why. */
write_text_file(script_path,
"break DECISION type=PEND\ncontinue\ncontinue\nquit\n");
TEST_ASSERT_NOT_NULL(freopen(script_path, "r", stdin));
captured = run_group_d_and_capture(replay_path, script_path, capture_path);
memset(&g_d_sim, 0, sizeof(g_d_sim));
memset(&g_d_args, 0, sizeof(g_d_args));
g_d_args.replay_path = replay_path;
g_d_args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
g_d_args.sim_speed = 0;
g_d_args.interactive = true;
/* 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);
captured = capture_stdout_around(run_driver_for_group_d_capture);
TEST_ASSERT_EQUAL_INT(0, g_d_rc);
pend_pos = strstr(captured, "PEND jobid=555");
bp_pos = strstr(captured, "hit breakpoint 1 @ decision: PEND jobid=555");
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");
@ -1115,6 +1520,84 @@ static void test_decision_breakpoint_pauses_after_decision_output(void)
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)");
free(captured);
printf("test_step_echoes_events_before_session_decisions: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
@ -1135,11 +1618,17 @@ int main(void)
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_string_and_boolean_resource_values);
RUN_TEST(test_info_resource_reports_string_value);
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_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);
return UNITY_END();
}