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:
parent
515f4ca098
commit
c9acd502c0
32 changed files with 2547 additions and 295 deletions
|
|
@ -455,25 +455,61 @@ static void test_should_pause_never_fires_in_continue_mode(void)
|
|||
/* -- Observable (a), proved by output ORDER, not merely presence. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static xce_sched_sim_driver_t g_t2_sim;
|
||||
static sched_sim_args_t g_t2_args;
|
||||
static int g_t2_rc;
|
||||
|
||||
static void run_driver_for_t2_capture(void)
|
||||
/*
|
||||
* task 12.5 (bug-1594): a completed --interactive run now re-enters the
|
||||
* REPL (xce_repl_on_replay_complete(), repl.c) instead of returning, so a
|
||||
* "continue"-to-EOF script that has no further scripted line will hit
|
||||
* stdin EOF inside that re-entered repl_command_loop() -- which
|
||||
* repl_command_loop() has always treated as an implicit `quit` (exit(0)).
|
||||
* Before task 12.5 this test ran the driver IN-PROCESS (capture_stdout_
|
||||
* around()) because sched_sim_driver_run() genuinely returned once EOF was
|
||||
* reached; now it must fork, exactly like Test 3 below, since exit(0)
|
||||
* would otherwise tear down this whole unity binary mid-suite.
|
||||
*/
|
||||
static void child_prepause_then_quit(const char* replay_path,
|
||||
const char* script_path,
|
||||
const char* capture_path)
|
||||
{
|
||||
g_t2_rc = sched_sim_driver_run(&g_t2_sim, &g_t2_args);
|
||||
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);
|
||||
|
||||
/* Unreachable in a correct implementation -- the script's trailing
|
||||
* `quit` (or, absent that, stdin EOF) must already have called
|
||||
* exit(0) from inside the post-replay-completion pause. */
|
||||
fflush(stdout);
|
||||
_exit(111);
|
||||
}
|
||||
|
||||
static void test_pre_replay_pause_blocks_before_any_event_is_processed(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;
|
||||
pid_t pid;
|
||||
int status;
|
||||
char* captured;
|
||||
char* ready_pos;
|
||||
char* pend_pos;
|
||||
FILE* saved_stdin_check;
|
||||
|
||||
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
|
||||
"_prepause");
|
||||
|
|
@ -485,30 +521,43 @@ static void test_pre_replay_pause_blocks_before_any_event_is_processed(void)
|
|||
append_end_of_stream_marker(f);
|
||||
fclose(f);
|
||||
|
||||
make_tmp_dir(script_dir, sizeof(script_dir), "_prepausescript");
|
||||
snprintf(script_path, sizeof(script_path), "%s/script.txt", script_dir);
|
||||
/* A single "continue" -- 12.1's own scope means continue never
|
||||
* re-pauses, so the run reaches EOF cleanly without reading stdin
|
||||
* again. */
|
||||
write_text_file(script_path, "continue\n");
|
||||
make_tmp_dir(aux_dir, sizeof(aux_dir), "_prepauseaux");
|
||||
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
|
||||
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
|
||||
/* "continue" runs to EOF (12.1's own scope: continue never re-pauses
|
||||
* mid-stream absent a breakpoint); task 12.5 then re-enters the REPL
|
||||
* at replay completion, so an explicit trailing `quit` is required to
|
||||
* end the child deterministically. */
|
||||
write_text_file(script_path, "continue\nquit\n");
|
||||
|
||||
saved_stdin_check = freopen(script_path, "r", stdin);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(saved_stdin_check,
|
||||
"freopen of scripted stdin must succeed");
|
||||
pid = fork();
|
||||
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed");
|
||||
|
||||
memset(&g_t2_sim, 0, sizeof(g_t2_sim));
|
||||
memset(&g_t2_args, 0, sizeof(g_t2_args));
|
||||
g_t2_args.replay_path = replay_path;
|
||||
g_t2_args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
|
||||
g_t2_args.sim_speed = 0;
|
||||
g_t2_args.interactive = true;
|
||||
if (pid == 0) {
|
||||
child_prepause_then_quit(replay_path, script_path, capture_path);
|
||||
_exit(112); /* unreachable -- child_prepause_then_quit never
|
||||
* returns */
|
||||
}
|
||||
|
||||
captured = capture_stdout_around(run_driver_for_t2_capture);
|
||||
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");
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, g_t2_rc);
|
||||
captured = read_whole_file(capture_path);
|
||||
|
||||
/* 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);
|
||||
|
||||
ready_pos = strstr(captured, "[sim] ready. type 'help' for commands.");
|
||||
pend_pos = strstr(captured, "PEND jobid=4242");
|
||||
pend_pos = strstr(captured, pend_marker);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(ready_pos,
|
||||
"pre-replay pause banner must be printed");
|
||||
|
|
@ -526,6 +575,204 @@ static void test_pre_replay_pause_blocks_before_any_event_is_processed(void)
|
|||
"PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* task 12.5 (bug-1594) regression: `continue` reaching EOF with no
|
||||
* breakpoints must NOT exit the process -- it must re-enter the REPL so
|
||||
* the operator can inspect final state, and only `quit` (or stdin EOF,
|
||||
* pre-existing documented behavior) may end it.
|
||||
*/
|
||||
static void child_continue_then_inspect(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 `quit` must already have
|
||||
* exited */
|
||||
}
|
||||
|
||||
static void test_continue_with_no_breakpoints_stays_alive_after_replay_complete(
|
||||
void)
|
||||
{
|
||||
char replay_path[PATH_MAX];
|
||||
char aux_dir[PATH_MAX];
|
||||
char script_path[PATH_MAX];
|
||||
char capture_path[PATH_MAX];
|
||||
FILE* f;
|
||||
pid_t pid;
|
||||
int status;
|
||||
char* captured;
|
||||
|
||||
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
|
||||
"_contnoexit");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
|
||||
append_queue_update_event(f, 1000ULL, "normal");
|
||||
append_jobs_push_event(f, 1500ULL, 9090ULL, "normal");
|
||||
append_replay_session(f, 2000ULL);
|
||||
append_end_of_stream_marker(f);
|
||||
fclose(f);
|
||||
|
||||
make_tmp_dir(aux_dir, sizeof(aux_dir), "_contnoexitaux");
|
||||
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
|
||||
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
|
||||
/* No breakpoints set. After `continue` runs to EOF, the REPL must
|
||||
* still be alive to process `info summary` before `quit`. */
|
||||
write_text_file(script_path, "continue\ninfo summary\nquit\n");
|
||||
|
||||
pid = fork();
|
||||
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed");
|
||||
|
||||
if (pid == 0) {
|
||||
child_continue_then_inspect(replay_path, script_path, capture_path);
|
||||
_exit(112);
|
||||
}
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(pid, waitpid(pid, &status, 0),
|
||||
"waitpid must reap the REPL child");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
WIFEXITED(status),
|
||||
"the child must exit normally via quit's exit(0) -- a hang or "
|
||||
"crash means the REPL did not survive replay completion");
|
||||
TEST_ASSERT_EQUAL_INT(0, WEXITSTATUS(status));
|
||||
|
||||
captured = read_whole_file(capture_path);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, "[sim] replay complete."),
|
||||
"replay completion must still print its banner");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, "1 jobs (1 pending, 0 running)"),
|
||||
"info summary issued AFTER replay completion must still execute "
|
||||
"against final state -- proves the REPL did not exit on EOF "
|
||||
"(bug-1594)");
|
||||
|
||||
free(captured);
|
||||
|
||||
printf("test_continue_with_no_breakpoints_stays_alive_after_replay_"
|
||||
"complete: PASS\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* task 12.5 (bug-1594) regression: `step`/`continue` issued AFTER replay
|
||||
* is already complete must re-prompt with an informational message, not
|
||||
* be treated as "hand control back to a driver loop" (there is none left).
|
||||
*/
|
||||
static void child_continue_twice_after_complete(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);
|
||||
}
|
||||
|
||||
static void test_continue_after_replay_complete_reprompts_instead_of_exiting(
|
||||
void)
|
||||
{
|
||||
char replay_path[PATH_MAX];
|
||||
char aux_dir[PATH_MAX];
|
||||
char script_path[PATH_MAX];
|
||||
char capture_path[PATH_MAX];
|
||||
FILE* f;
|
||||
pid_t pid;
|
||||
int status;
|
||||
char* captured;
|
||||
|
||||
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
|
||||
"_contafter");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
|
||||
append_queue_update_event(f, 1000ULL, "normal");
|
||||
append_jobs_push_event(f, 1500ULL, 8080ULL, "normal");
|
||||
append_replay_session(f, 2000ULL);
|
||||
append_end_of_stream_marker(f);
|
||||
fclose(f);
|
||||
|
||||
make_tmp_dir(aux_dir, sizeof(aux_dir), "_contafteraux");
|
||||
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
|
||||
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
|
||||
/* Second `continue`/`step` are issued AFTER replay is already
|
||||
* complete -- both must re-prompt, not attempt to resume anything. */
|
||||
write_text_file(script_path, "continue\ncontinue\nstep\nquit\n");
|
||||
|
||||
pid = fork();
|
||||
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed");
|
||||
|
||||
if (pid == 0) {
|
||||
child_continue_twice_after_complete(replay_path, script_path,
|
||||
capture_path);
|
||||
_exit(112);
|
||||
}
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(pid, waitpid(pid, &status, 0),
|
||||
"waitpid must reap the REPL child");
|
||||
TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status),
|
||||
"the child must exit normally via quit's "
|
||||
"exit(0)");
|
||||
TEST_ASSERT_EQUAL_INT(0, WEXITSTATUS(status));
|
||||
|
||||
captured = read_whole_file(capture_path);
|
||||
|
||||
{
|
||||
const char* needle = "[sim] replay already complete -- no more "
|
||||
"events to process. Use 'quit' to exit.";
|
||||
char* first = strstr(captured, needle);
|
||||
char* second = (first != NULL)
|
||||
? strstr(first + 1, needle)
|
||||
: NULL;
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
first, "a `continue` issued after replay completion must "
|
||||
"print the re-prompt message, not silently resume/exit "
|
||||
"(bug-1594)");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
second, "a `step` issued after replay completion must ALSO "
|
||||
"print the re-prompt message");
|
||||
}
|
||||
|
||||
free(captured);
|
||||
|
||||
printf("test_continue_after_replay_complete_reprompts_instead_of_"
|
||||
"exiting: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test 3: "step 1" against a three-session fixture processes EXACTLY one */
|
||||
/* session before re-pausing -- proved end to end via a forked child that */
|
||||
|
|
@ -572,6 +819,10 @@ static void test_step_1_processes_exactly_one_session_then_quit_exits(void)
|
|||
char aux_dir[PATH_MAX];
|
||||
char script_path[PATH_MAX];
|
||||
char capture_path[PATH_MAX];
|
||||
char pend_marker_111[80];
|
||||
char pend_marker_222[80];
|
||||
char pend_marker_333[80];
|
||||
char jobid_buf[64];
|
||||
FILE* f;
|
||||
pid_t pid;
|
||||
int status;
|
||||
|
|
@ -620,12 +871,25 @@ static void test_step_1_processes_exactly_one_session_then_quit_exits(void)
|
|||
|
||||
captured = read_whole_file(capture_path);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, "PEND jobid=111"),
|
||||
/* bug-1591: sim_print_decision() (sim_loop.c) now renders jobids via
|
||||
* fmt_jobid(), not raw %llu -- these markers must match that same
|
||||
* human-readable form. */
|
||||
fmt_jobid(jobid_buf, sizeof(jobid_buf), 111ULL);
|
||||
snprintf(pend_marker_111, sizeof(pend_marker_111), "PEND jobid=%s",
|
||||
jobid_buf);
|
||||
fmt_jobid(jobid_buf, sizeof(jobid_buf), 222ULL);
|
||||
snprintf(pend_marker_222, sizeof(pend_marker_222), "PEND jobid=%s",
|
||||
jobid_buf);
|
||||
fmt_jobid(jobid_buf, sizeof(jobid_buf), 333ULL);
|
||||
snprintf(pend_marker_333, sizeof(pend_marker_333), "PEND jobid=%s",
|
||||
jobid_buf);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(captured, pend_marker_111),
|
||||
"session 1's decision must have run");
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(captured, "PEND jobid=222"),
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(captured, pend_marker_222),
|
||||
"session 2 must NEVER have run -- step 1 "
|
||||
"must pause strictly after session 1");
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(captured, "PEND jobid=333"),
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(captured, pend_marker_333),
|
||||
"session 3 must NEVER have run either");
|
||||
|
||||
free(captured);
|
||||
|
|
@ -642,6 +906,8 @@ int main(void)
|
|||
RUN_TEST(test_should_pause_never_fires_in_continue_mode);
|
||||
RUN_TEST(test_pre_replay_pause_blocks_before_any_event_is_processed);
|
||||
RUN_TEST(test_step_1_processes_exactly_one_session_then_quit_exits);
|
||||
RUN_TEST(test_continue_with_no_breakpoints_stays_alive_after_replay_complete);
|
||||
RUN_TEST(test_continue_after_replay_complete_reprompts_instead_of_exiting);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue