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
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue