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
|
|
@ -325,6 +325,21 @@ sched_driver_translate_test = executable(
|
|||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Regression test for bug-1585's true root cause: a live heap-use-after-free
|
||||
# in the RESVALS_FULL/RESVALS_DELTA event-application path (core_events.c),
|
||||
# triggered by every second periodic resval report for the same
|
||||
# worker+resource on ANY real deployment. Same white-box #include set as
|
||||
# sched_driver_translate_test (its sibling), but with a REAL (non-stubbed)
|
||||
# kill_sched_resval -- see test_resval_ownership.c's own header comment for
|
||||
# why that distinction is exactly what makes this test able to catch the bug
|
||||
# sched_driver_translate_test's own no-op stub cannot.
|
||||
sched_resval_ownership_test = executable(
|
||||
'sched_resval_ownership_test',
|
||||
['test_resval_ownership.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Contract test for the normal-mode driver's decision-publish callback
|
||||
# (scheduler-core spec, task 5.2, Req 5 AC 6 / AC 9's publishing half):
|
||||
# xce_sched_driver_on_decision() forwards DISPATCH/PEND decisions to
|
||||
|
|
@ -944,6 +959,9 @@ test('scheduler core full cycle test',
|
|||
test('scheduler driver translate test',
|
||||
sched_driver_translate_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler resval ownership test',
|
||||
sched_resval_ownership_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler driver decision cb test',
|
||||
sched_driver_decision_cb_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
|
|
@ -246,6 +246,53 @@ static void test_xce_sched_core_init_seeds_normal_mode_defaults(void)
|
|||
1, atomic_load_explicit(&(core.speed_den), memory_order_acquire));
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* xce_core_set_wallclock_anchor() -- normal-mode epoch seeding (bug-1584) */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static uint64_t test_epoch_ns(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
return ((uint64_t)ts.tv_sec * TEST_NSEC_PER_SEC) + (uint64_t)ts.tv_nsec;
|
||||
}
|
||||
|
||||
static void test_xce_core_set_wallclock_anchor_seeds_real_epoch_time(void)
|
||||
{
|
||||
/* bug-1584: xce_sched_core_init() alone leaves xce_core_now() returning
|
||||
* raw CLOCK_MONOTONIC-since-boot nanoseconds (see
|
||||
* test_xce_sched_core_init_seeds_normal_mode_defaults above) -- NOT
|
||||
* epoch time, which every real consumer of a normal-mode timestamp
|
||||
* needs (recording.c's record_start_ts, SchedAdminReply.startedAt,
|
||||
* documented in controller.capnp as "nanoseconds since epoch").
|
||||
* xce_core_set_wallclock_anchor() is the fix: bracket its effect on
|
||||
* xce_core_now() between two back-to-back CLOCK_REALTIME reads taken
|
||||
* immediately before/after the call -- deterministic (real time only
|
||||
* moves forward) and immune to sleep-based flakiness. Before this fix,
|
||||
* `result` would be a small monotonic-since-boot value, far below
|
||||
* `before` (a real epoch value ~1.7e18 ns as of 2026) -- this bracket
|
||||
* fails loudly on that regression rather than passing by coincidence. */
|
||||
xce_sched_core_t core;
|
||||
uint64_t before, result, after;
|
||||
|
||||
fixture_init(&core);
|
||||
|
||||
before = test_epoch_ns();
|
||||
xce_core_set_wallclock_anchor(&core);
|
||||
result = xce_core_now(&core);
|
||||
after = test_epoch_ns();
|
||||
|
||||
TEST_ASSERT_TRUE(result >= before);
|
||||
TEST_ASSERT_TRUE(result <= after);
|
||||
}
|
||||
|
||||
static void test_xce_core_set_wallclock_anchor_null_core_is_a_safe_noop(void)
|
||||
{
|
||||
/* Mirrors xce_core_now(NULL)'s defensive contract -- no crash. */
|
||||
xce_core_set_wallclock_anchor(NULL);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* tlog format integration (Req 5 AC 3) */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
|
@ -451,6 +498,8 @@ int main(void)
|
|||
RUN_TEST(test_xce_core_now_fast_simulation_scales_up_by_speed);
|
||||
RUN_TEST(test_xce_core_now_slow_simulation_scales_down_by_speed);
|
||||
RUN_TEST(test_xce_sched_core_init_seeds_normal_mode_defaults);
|
||||
RUN_TEST(test_xce_core_set_wallclock_anchor_seeds_real_epoch_time);
|
||||
RUN_TEST(test_xce_core_set_wallclock_anchor_null_core_is_a_safe_noop);
|
||||
RUN_TEST(test_tlog_format_shows_frozen_virtual_time_not_infotime);
|
||||
RUN_TEST(test_tlog_format_frozen_time_is_stable_across_calls);
|
||||
RUN_TEST(test_tlog_format_falls_back_to_infotime_when_no_active_core);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,39 @@ 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; }
|
||||
|
||||
/* dup_val - real reimplementation of snapshot_wire.c's function of the same
|
||||
* name (verbatim logic), needed because core_events.c's RESVALS_FULL/
|
||||
* RESVALS_DELTA handlers now call it (bug-1585 fix) and this file's
|
||||
* white-box #include of ../src/core_events.c does not pull in
|
||||
* snapshot_wire.c (same "reimplement the one small function instead of the
|
||||
* whole file" rationale this file's own sched_resolve_job_picker copy
|
||||
* already documents). This file's own kill_sched_resval is a no-op stub, so
|
||||
* the deep copy's contents are never inspected here -- only link-time
|
||||
* presence matters for this file's own tests. */
|
||||
xce_val_t* dup_val(const xce_val_t* src)
|
||||
{
|
||||
xce_val_t* v;
|
||||
|
||||
if (src == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v = (xce_val_t*)calloc(1, sizeof(*v));
|
||||
if (v == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v->which = src->which;
|
||||
if (src->which == XCE_VAL_STRING) {
|
||||
v->u.svalue
|
||||
= (src->u.svalue != NULL) ? makeString(src->u.svalue) : NULL;
|
||||
} else {
|
||||
v->u = src->u;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
void plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- see
|
||||
|
|
|
|||
|
|
@ -134,6 +134,39 @@ 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; }
|
||||
|
||||
/* dup_val - real reimplementation of snapshot_wire.c's function of the same
|
||||
* name (verbatim logic), needed because core_events.c's RESVALS_FULL/
|
||||
* RESVALS_DELTA handlers now call it (bug-1585 fix) and this file's
|
||||
* white-box #include of ../src/core_events.c does not pull in
|
||||
* snapshot_wire.c (same "reimplement the one small function instead of the
|
||||
* whole file" rationale this file's own sched_resolve_job_picker copy
|
||||
* already documents). This file's own kill_sched_resval is a no-op stub, so
|
||||
* the deep copy's contents are never inspected here -- only link-time
|
||||
* presence matters for this file's own tests. */
|
||||
xce_val_t* dup_val(const xce_val_t* src)
|
||||
{
|
||||
xce_val_t* v;
|
||||
|
||||
if (src == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v = (xce_val_t*)calloc(1, sizeof(*v));
|
||||
if (v == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v->which = src->which;
|
||||
if (src->which == XCE_VAL_STRING) {
|
||||
v->u.svalue
|
||||
= (src->u.svalue != NULL) ? makeString(src->u.svalue) : NULL;
|
||||
} else {
|
||||
v->u = src->u;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
void plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* core_reset_workers()'s (core_session.c) linker dependency -- see file
|
||||
|
|
|
|||
|
|
@ -67,6 +67,39 @@ 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; }
|
||||
|
||||
/* dup_val - real reimplementation of snapshot_wire.c's function of the same
|
||||
* name (verbatim logic), needed because core_events.c's RESVALS_FULL/
|
||||
* RESVALS_DELTA handlers now call it (bug-1585 fix) and this file's
|
||||
* white-box #include of ../src/core_events.c does not pull in
|
||||
* snapshot_wire.c (same "reimplement the one small function instead of the
|
||||
* whole file" rationale this file's own sched_resolve_job_picker copy
|
||||
* already documents). This file's own kill_sched_resval is a no-op stub, so
|
||||
* the deep copy's contents are never inspected here -- only link-time
|
||||
* presence matters for this file's own tests. */
|
||||
xce_val_t* dup_val(const xce_val_t* src)
|
||||
{
|
||||
xce_val_t* v;
|
||||
|
||||
if (src == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v = (xce_val_t*)calloc(1, sizeof(*v));
|
||||
if (v == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v->which = src->which;
|
||||
if (src->which == XCE_VAL_STRING) {
|
||||
v->u.svalue
|
||||
= (src->u.svalue != NULL) ? makeString(src->u.svalue) : NULL;
|
||||
} else {
|
||||
v->u = src->u;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
void plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* Link-satisfying stubs: as of scheduler-core spec task 5.2,
|
||||
|
|
|
|||
|
|
@ -165,6 +165,39 @@ 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; }
|
||||
|
||||
/* dup_val - real reimplementation of snapshot_wire.c's function of the same
|
||||
* name (verbatim logic), needed because core_events.c's RESVALS_FULL/
|
||||
* RESVALS_DELTA handlers now call it (bug-1585 fix) and this file's
|
||||
* white-box #include of ../src/core_events.c does not pull in
|
||||
* snapshot_wire.c (same "reimplement the one small function instead of the
|
||||
* whole file" rationale this file's own sched_resolve_job_picker copy
|
||||
* already documents). This file's own kill_sched_resval is a no-op stub, so
|
||||
* the deep copy's contents are never inspected here -- only link-time
|
||||
* presence matters for this file's own tests. */
|
||||
xce_val_t* dup_val(const xce_val_t* src)
|
||||
{
|
||||
xce_val_t* v;
|
||||
|
||||
if (src == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v = (xce_val_t*)calloc(1, sizeof(*v));
|
||||
if (v == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v->which = src->which;
|
||||
if (src->which == XCE_VAL_STRING) {
|
||||
v->u.svalue
|
||||
= (src->u.svalue != NULL) ? makeString(src->u.svalue) : NULL;
|
||||
} else {
|
||||
v->u = src->u;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
/* core_reset_workers()'s (core_session.c) linker dependency -- see file
|
||||
* header comment; never invoked (no worker is ever added here). */
|
||||
void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; }
|
||||
|
|
|
|||
|
|
@ -65,6 +65,39 @@ 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; }
|
||||
|
||||
/* dup_val - real reimplementation of snapshot_wire.c's function of the same
|
||||
* name (verbatim logic), needed because core_events.c's RESVALS_FULL/
|
||||
* RESVALS_DELTA handlers now call it (bug-1585 fix) and this file's
|
||||
* white-box #include of ../src/core_events.c does not pull in
|
||||
* snapshot_wire.c (same "reimplement the one small function instead of the
|
||||
* whole file" rationale this file's own sched_resolve_job_picker copy
|
||||
* already documents). This file's own kill_sched_resval is a no-op stub, so
|
||||
* the deep copy's contents are never inspected here -- only link-time
|
||||
* presence matters for this file's own tests. */
|
||||
xce_val_t* dup_val(const xce_val_t* src)
|
||||
{
|
||||
xce_val_t* v;
|
||||
|
||||
if (src == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v = (xce_val_t*)calloc(1, sizeof(*v));
|
||||
if (v == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v->which = src->which;
|
||||
if (src->which == XCE_VAL_STRING) {
|
||||
v->u.svalue
|
||||
= (src->u.svalue != NULL) ? makeString(src->u.svalue) : NULL;
|
||||
} else {
|
||||
v->u = src->u;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
void plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* Link-satisfying stubs for scheduler.c's sched_publish_dispatch()/
|
||||
|
|
@ -673,19 +706,29 @@ static void test_drain_translates_and_pushes_all_ten_op_types(void)
|
|||
r3->u.resourceupdate.resources = resources;
|
||||
enqueue(&core, r3);
|
||||
|
||||
/* 4. UPDATE_RESVALS_FULL -> RESVALS_FULL. NOTE: core_events.c's
|
||||
* RESVALS_FULL case does `zarray_put(&core->sharedInst, res->idx,
|
||||
* rv->val)` -- a SHALLOW store of the SAME `val` pointer (verified by
|
||||
* reading zarray_put_ex()'s source, src/lib/zarray.c) -- and this
|
||||
* function's cleanup then frees `full_resval` (and, via
|
||||
* free_ResourceVal->free_Value_ptr, `full_val` too), exactly mirroring
|
||||
* scheduler.c's existing sched_pend_update_killer()'s identical
|
||||
* RESVALS_FULL cleanup. This is a PRE-EXISTING production ownership
|
||||
* characteristic (the same hazard already exists in sched_update()'s
|
||||
* own live path today, untouched by this task) -- so after the drain
|
||||
* below, core->sharedInst's stored pointer for "cpu" is only safe to
|
||||
* check for non-NULL (the slot was populated), never to dereference. */
|
||||
full_val = (xce_val_t*)calloc(1, sizeof(*full_val));
|
||||
/* 4. UPDATE_RESVALS_FULL -> RESVALS_FULL. NOTE (bug-1585, FIXED): this
|
||||
* comment used to document a PRE-EXISTING production ownership hazard --
|
||||
* core_events.c's RESVALS_FULL case used to do `zarray_put(&core->
|
||||
* sharedInst, res->idx, rv->val)`, a SHALLOW store of the SAME `val`
|
||||
* pointer this function's cleanup then frees via
|
||||
* free_ResourceVal->free_Value_ptr, exactly mirroring scheduler.c's
|
||||
* sched_pend_update_killer()'s identical (also now-fixed) RESVALS_FULL
|
||||
* cleanup -- meaning core->sharedInst's stored pointer for "cpu" used to
|
||||
* be a dangling pointer the moment this drain returned, unsafe to
|
||||
* dereference (only a non-NULL check was safe). core_events.c's
|
||||
* RESVALS_FULL/RESVALS_DELTA handlers now `dup_val()` before
|
||||
* `zarray_put()`, so the core owns an independent copy and this
|
||||
* function's unconditional free of `full_val`/`delta_val` below is
|
||||
* exactly what should happen -- core->sharedInst's stored value is now
|
||||
* genuinely safe to read back and assert on (see
|
||||
* test_resval_ownership.c for the dedicated regression test proving
|
||||
* this end-to-end under a REAL, non-stubbed kill_sched_resval). `which`
|
||||
* must be set explicitly here (XCE_VAL_NUMERIC) -- dup_val() now
|
||||
* actually inspects it, and a bare calloc() leaves it 0 (==
|
||||
* XCE_VAL_STRING), which would make dup_val() misread `u.ivalue32`'s
|
||||
* bit pattern as a `char*` and crash in makeString(). */
|
||||
full_val = (xce_val_t*)calloc(1, sizeof(*full_val));
|
||||
full_val->which = XCE_VAL_NUMERIC;
|
||||
full_val->u.ivalue32 = 16;
|
||||
full_resval = (xce_resval_t*)calloc(1, sizeof(*full_resval));
|
||||
full_resval->name = strdup("cpu");
|
||||
|
|
@ -698,9 +741,10 @@ static void test_drain_translates_and_pushes_all_ten_op_types(void)
|
|||
enqueue(&core, r4);
|
||||
|
||||
/* 5. UPDATE_RESVALS -> RESVALS_DELTA: attributes to worker "hostA".
|
||||
* Same shallow-store-then-free hazard as 4 above (per-worker resvals
|
||||
* zarray instead of core->sharedInst) -- same "non-NULL only" caveat. */
|
||||
delta_val = (xce_val_t*)calloc(1, sizeof(*delta_val));
|
||||
* Same fix as 4 above (per-worker resvals zarray instead of
|
||||
* core->sharedInst) -- `which` set explicitly for the same reason. */
|
||||
delta_val = (xce_val_t*)calloc(1, sizeof(*delta_val));
|
||||
delta_val->which = XCE_VAL_NUMERIC;
|
||||
delta_val->u.ivalue32 = 4;
|
||||
delta_resval = (xce_resval_t*)calloc(1, sizeof(*delta_resval));
|
||||
delta_resval->name = strdup("cpu");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
|
@ -571,12 +572,14 @@ test_repl_step_continue_to_completion_matches_batch_decision_sequence(void)
|
|||
|
||||
make_tmp_dir(script_dir, sizeof(script_dir), "_script");
|
||||
snprintf(script_path, sizeof(script_path), "%s/script.txt", script_dir);
|
||||
write_text_file(script_path, "step 1\ncontinue\n");
|
||||
/* task 12.5 (bug-1594): trailing `quit` -- `continue` now re-enters
|
||||
* the REPL at replay completion instead of returning, so this side
|
||||
* must run in a FORKED child (matching test_repl_execution_control.c's
|
||||
* own Test 3 pattern); otherwise quit's exit(0) would tear down this
|
||||
* whole test binary before the batch-vs-REPL comparison below ever
|
||||
* runs. */
|
||||
write_text_file(script_path, "step 1\ncontinue\nquit\n");
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(freopen(script_path, "r", stdin),
|
||||
"freopen of scripted stdin must succeed");
|
||||
|
||||
memset(&sim_repl, 0, sizeof(sim_repl));
|
||||
memset(&args_repl, 0, sizeof(args_repl));
|
||||
args_repl.replay_path = fixture_path;
|
||||
args_repl.sim_start_ts = SCHED_SIM_START_TS_LATEST;
|
||||
|
|
@ -584,8 +587,30 @@ test_repl_step_continue_to_completion_matches_batch_decision_sequence(void)
|
|||
args_repl.output_path = repl_out_path;
|
||||
args_repl.interactive = true;
|
||||
|
||||
rc = sched_sim_driver_run(&sim_repl, &args_repl);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
{
|
||||
pid_t pid = fork();
|
||||
int status;
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed");
|
||||
|
||||
if (pid == 0) {
|
||||
if (freopen(script_path, "r", stdin) == NULL) {
|
||||
_exit(120);
|
||||
}
|
||||
memset(&sim_repl, 0, sizeof(sim_repl));
|
||||
(void)sched_sim_driver_run(&sim_repl, &args_repl);
|
||||
_exit(111); /* unreachable -- quit must already have exited */
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/* ---- Decode every ReplaySession from both output files and compare
|
||||
* the decision sequence, session by session, decision by decision. ---- */
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
549
src/scheduler/testsuite/test_resval_ownership.c
Normal file
549
src/scheduler/testsuite/test_resval_ownership.c
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
/*
|
||||
* test_resval_ownership.c - regression test for bug-1585's true root cause:
|
||||
* a heap-use-after-free crash in the normal-mode driver's periodic resource-
|
||||
* value (resval) update path, live-reproduced via gdb/ASAN against a real
|
||||
* controller+scheduler+xcecd_virtual deployment (see .wolf/buglog.json
|
||||
* bug-1585 for the full incident/backtrace).
|
||||
*
|
||||
* Root cause: core_events.c's XCE_CORE_EVENT_RESVALS_FULL and
|
||||
* XCE_CORE_EVENT_RESVALS_DELTA handlers do `zarray_put(&(zarray), idx,
|
||||
* rv->val)` -- a SHALLOW store of the wire-decoded `xce_val_t*` pointer
|
||||
* (verified by reading zarray_put_ex(), src/lib/zarray.c: `array->data[idx]
|
||||
* = data[i]`, no copy). zarray now considers itself the sole owner of that
|
||||
* pointer, and will hand it to the zarray's killer (kill_sched_resval,
|
||||
* schedobj.c) the NEXT time this same index is replaced. But driver.c's
|
||||
* release_translated_payload() (XCE_SCHED_OP_UPDATE_RESVALS/_RESVALS_FULL
|
||||
* cases) UNCONDITIONALLY frees the entire decoded ResValList/ResourceVal
|
||||
* array immediately after xce_core_push_events() returns -- including the
|
||||
* SAME `rv->val` the zarray just adopted by reference. The result: the very
|
||||
* next periodic resval report for the same worker+resource (which happens
|
||||
* every resource_sync interval on any live deployment, i.e. inevitably)
|
||||
* triggers zarray_put's eviction of the now-dangling first value, which
|
||||
* calls kill_sched_resval() -> free_Value() -> a heap-use-after-free READ
|
||||
* on an already-fully-freed xce_val_t. This reliably crashes the live
|
||||
* scheduler process within one resval-report cycle of any real deployment,
|
||||
* which is why a `.replay` recording captured from a live session so often
|
||||
* turns out empty/truncated (bug-1585): the scheduler dies before or during
|
||||
* the interesting part of the session.
|
||||
*
|
||||
* This exact hazard was already NOTICED and explicitly documented as a
|
||||
* "PRE-EXISTING production ownership characteristic" in
|
||||
* test_driver_translate.c's test_drain_translates_and_pushes_all_ten_op_types
|
||||
* comment (task 5.1) -- but never fixed, because that file's own
|
||||
* kill_sched_resval is a pure no-op stub ("fixtures ... never torn down, so
|
||||
* these killers are never actually invoked" -- true for the OTHER kvmgr
|
||||
* datakillers, which fire only on core teardown, but NOT true for a
|
||||
* zarray's killer, which fires on every REPLACEMENT of an occupied slot,
|
||||
* not just teardown). That blind spot is exactly why no existing test
|
||||
* caught this: this file uses the REAL kill_sched_resval (mirroring
|
||||
* schedobj.c's implementation exactly) so the use-after-free is genuinely
|
||||
* exercised, under ASAN, the same way the live crash was.
|
||||
*
|
||||
* Fix (core_events.c): both call sites now `dup_val(rv->val)` before
|
||||
* zarray_put -- the same deep-copy-before-adopt convention this codebase
|
||||
* ALREADY establishes elsewhere for the identical hazard (snapshot_load.c:
|
||||
* `zarray_put(&(core->sharedInst), r->idx, dup_val(rv->val))` and
|
||||
* `zarray_put(&(w->resvals), r->idx, dup_val(nv->val))`). After the fix,
|
||||
* the zarray owns an independent copy; driver.c's unconditional free of the
|
||||
* original decoded payload no longer touches memory the core still holds.
|
||||
*
|
||||
* White-box #include of ../src/core.c, ../src/core_clock.c,
|
||||
* ../src/core_events.c, and ../src/driver.c -- same convention as
|
||||
* test_driver_translate.c (this file's sibling), including its full stub
|
||||
* set for driver.c/core_events.c's link-time externs this file's own tests
|
||||
* never exercise (session/plugin/recording/publish -- see each stub's own
|
||||
* comment, copied verbatim). The two stubs that matter for THIS file's
|
||||
* Observable, unlike test_driver_translate.c, are NOT no-ops:
|
||||
*
|
||||
* - kill_sched_resval: the REAL implementation (schedobj.c:260 verbatim
|
||||
* -- free_Value(val), no separate free(val); schedobj.c is not pulled
|
||||
* in directly for the same "avoid its large stub surface" reason
|
||||
* test_core_events.c/test_driver_translate.c already document for
|
||||
* sched_resolve_job_picker).
|
||||
* - dup_val: the REAL implementation (snapshot_wire.c:37 verbatim, same
|
||||
* "reimplement the one small function instead of pulling in the whole
|
||||
* file" rationale) -- core_events.c's fix (once applied) calls this by
|
||||
* `extern` declaration, same idiom snapshot_load.c/snapshot.c already
|
||||
* use for the identical symbol.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "controller.capnp.h"
|
||||
#include "kvmgr.h"
|
||||
#include "scheduler.h"
|
||||
#include "xstring.h"
|
||||
|
||||
/* Pull in the real xce_sched_core_init()/xce_core_push_events()/
|
||||
* xce_sched_driver_translate_pending_events() under test into this TU --
|
||||
* same white-box #include set as test_driver_translate.c. */
|
||||
#include "../src/core.c"
|
||||
#include "../src/core_clock.c"
|
||||
#include "../src/core_events.c"
|
||||
#include "../src/driver.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 plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* REAL implementation -- schedobj.c:260 verbatim (see file header comment:
|
||||
* this is the whole point of this test file). Deliberately reimplemented
|
||||
* rather than pulling in schedobj.c itself (large stub surface, same
|
||||
* rationale test_core_events.c/test_driver_translate.c already document for
|
||||
* sched_resolve_job_picker). */
|
||||
void kill_sched_resval(xce_val_t* val)
|
||||
{
|
||||
if (val == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
free_Value(val);
|
||||
}
|
||||
|
||||
/* REAL implementation -- snapshot_wire.c:37 verbatim. This is the fix under
|
||||
* test: core_events.c's RESVALS_FULL/RESVALS_DELTA handlers must dup_val()
|
||||
* before zarray_put() so the zarray owns an independent copy, not the same
|
||||
* pointer driver.c's release_translated_payload() is about to free. */
|
||||
xce_val_t* dup_val(const xce_val_t* src)
|
||||
{
|
||||
xce_val_t* v;
|
||||
|
||||
if (src == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v = (xce_val_t*)calloc(1, sizeof(*v));
|
||||
if (v == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v->which = src->which;
|
||||
if (src->which == XCE_VAL_STRING) {
|
||||
v->u.svalue
|
||||
= (src->u.svalue != NULL) ? makeString(src->u.svalue) : NULL;
|
||||
} else {
|
||||
v->u = src->u;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Link-satisfying stubs for scheduler.c's sched_publish_dispatch()/
|
||||
* sched_publish_pend_reason() -- copied verbatim from test_driver_translate.c
|
||||
* (see that file's header comment); never invoked by this file's tests. */
|
||||
int sched_publish_dispatch(xce_scheduler_t* sched, xce_job_dispatch_t* d)
|
||||
{
|
||||
(void)sched;
|
||||
(void)d;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_publish_pend_reason(xce_scheduler_t* sched, xce_preason_set_t* r)
|
||||
{
|
||||
(void)sched;
|
||||
(void)r;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Link-satisfying stub -- copied verbatim from test_driver_translate.c.
|
||||
* xce_sched_driver_translate_pending_events() calls this behind a NULL
|
||||
* `record_ctx` check; every call site below passes NULL, so never actually
|
||||
* invoked at runtime. */
|
||||
int sched_record_write_events(xce_scheduler_t* sched, xce_core_event_t* events,
|
||||
int n)
|
||||
{
|
||||
(void)sched;
|
||||
(void)events;
|
||||
(void)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_record_start(xce_scheduler_t* sched, const char* record_file)
|
||||
{
|
||||
(void)sched;
|
||||
(void)record_file;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_record_stop(xce_scheduler_t* sched)
|
||||
{
|
||||
(void)sched;
|
||||
return 0;
|
||||
}
|
||||
|
||||
sched_record_session_ctx_t* sched_record_session_begin(xce_scheduler_t* sched,
|
||||
uint64_t begin_round_ts)
|
||||
{
|
||||
(void)sched;
|
||||
(void)begin_round_ts;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void sched_record_session_add_decision(sched_record_session_ctx_t* ctx,
|
||||
uint64_t ts, xce_decision_t* decision)
|
||||
{
|
||||
(void)ctx;
|
||||
(void)ts;
|
||||
(void)decision;
|
||||
}
|
||||
|
||||
int sched_record_session_end(xce_scheduler_t* sched,
|
||||
sched_record_session_ctx_t* ctx, uint64_t end_ts)
|
||||
{
|
||||
(void)sched;
|
||||
(void)ctx;
|
||||
(void)end_ts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int xce_core_begin_session(xce_sched_core_t* core)
|
||||
{
|
||||
(void)core;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int xce_core_signal_go(xce_sched_core_t* core)
|
||||
{
|
||||
(void)core;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int xce_core_end_session(xce_sched_core_t* core)
|
||||
{
|
||||
(void)core;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int plugin_begin_session(void* arg)
|
||||
{
|
||||
(void)arg;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int plugin_end_session(void* arg)
|
||||
{
|
||||
(void)arg;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sched_account_jobs(xce_scheduler_t* sched)
|
||||
{
|
||||
(void)sched;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- copied
|
||||
* verbatim from test_driver_translate.c's own copy (same rationale: no
|
||||
* picker plugin is registered in this file's fixture, so this always
|
||||
* returns -EINVAL and xce_core_push_events() proceeds with picker=NULL --
|
||||
* every case in core_events.c already NULL-guards its picker hook call). */
|
||||
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 -- identical to test_driver_translate.c's own. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void fixture_init(xce_sched_core_t* core)
|
||||
{
|
||||
int rc;
|
||||
|
||||
rc = xce_sched_core_init(core);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
core->pend_updates[0] = (dataq_t*)calloc(1, sizeof(dataq_t));
|
||||
dataq_init(core->pend_updates[0], NULL);
|
||||
core->pend_updates[1] = (dataq_t*)calloc(1, sizeof(dataq_t));
|
||||
dataq_init(core->pend_updates[1], NULL);
|
||||
}
|
||||
|
||||
static void enqueue(xce_sched_core_t* core, xce_sched_update_request_t* req)
|
||||
{
|
||||
dataq_enq(core->pend_updates[0], &(req->link));
|
||||
}
|
||||
|
||||
static xce_sched_update_request_t* make_req(xce_sched_op_t op)
|
||||
{
|
||||
xce_sched_update_request_t* req
|
||||
= (xce_sched_update_request_t*)calloc(1, sizeof(*req));
|
||||
|
||||
req->op = op;
|
||||
return req;
|
||||
}
|
||||
|
||||
/* Builds one heap-allocated worker->name-tagged resval delta, exactly the
|
||||
* shape decode_ResourceVal_list() (controller.capnp.c) produces on the real
|
||||
* wire-decode path -- see test_driver_translate.c's own
|
||||
* make_wire_job()-adjacent comment for why heap allocation (not a stack
|
||||
* local) matters here: this is what release_translated_payload()'s real
|
||||
* free_ResValList_ptr() call expects to free. */
|
||||
static xce_resval_list_t* make_resval_delta(const char* host,
|
||||
const char* resname,
|
||||
double fvalue)
|
||||
{
|
||||
xce_val_t* val = (xce_val_t*)calloc(1, sizeof(*val));
|
||||
xce_resval_t* rv = (xce_resval_t*)calloc(1, sizeof(*rv));
|
||||
xce_resval_t** vals = (xce_resval_t**)calloc(1, sizeof(*vals));
|
||||
xce_resval_list_t* list = (xce_resval_list_t*)calloc(1, sizeof(*list));
|
||||
|
||||
val->which = XCE_VAL_NUMERIC;
|
||||
val->u.fvalue = fvalue;
|
||||
|
||||
rv->name = strdup(resname);
|
||||
rv->host = strdup(host);
|
||||
rv->val = val;
|
||||
|
||||
vals[0] = rv;
|
||||
list->count = 1;
|
||||
list->vals = vals;
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* RESVALS_DELTA: two successive drains targeting the SAME worker+resource */
|
||||
/* slot. RED (pre-fix): the second drain's zarray_put() eviction of the */
|
||||
/* first drain's already-freed value crashes under ASAN */
|
||||
/* (heap-use-after-free in free_Value, matching bug-1585's live backtrace).*/
|
||||
/* GREEN (post-fix): both drains succeed, and the worker's live resval */
|
||||
/* reads back the SECOND drain's value (8.0), proving no data corruption. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_resvals_delta_second_drain_does_not_use_after_free(void)
|
||||
{
|
||||
xce_sched_core_t core;
|
||||
xce_worker_t* wire_worker;
|
||||
xce_worker_t** worker_ptrs;
|
||||
xce_resource_t* wire_resource;
|
||||
xce_resource_t** resource_ptrs;
|
||||
xce_resource_t* registered_res = NULL;
|
||||
xce_sched_worker_t* w;
|
||||
xce_val_t* got;
|
||||
int rc;
|
||||
|
||||
fixture_init(&core);
|
||||
|
||||
/* Heap-allocated, heap-allocated-string wire payloads -- AND
|
||||
* heap-allocated wrapper ARRAYS (not a stack array of pointers) --
|
||||
* driver.c's REAL release_translated_payload() (this file links the
|
||||
* genuine driver.c, not a stub) calls the real capnp-generated
|
||||
* free_Worker_ptr()/free_Resource_ptr() on each element AND
|
||||
* free(req->u.workerupdate.workers) on the array itself, exactly like a
|
||||
* genuinely wire-decoded payload -- see test_driver_translate.c's
|
||||
* make_wire_job() comment for why a stack local or string literal here
|
||||
* would crash immediately (it does: this is precisely the pitfall this
|
||||
* comment warns about, for both the element and the array). */
|
||||
wire_worker = (xce_worker_t*)calloc(1, sizeof(*wire_worker));
|
||||
wire_worker->name = strdup("hostA");
|
||||
worker_ptrs = (xce_worker_t**)calloc(1, sizeof(*worker_ptrs));
|
||||
worker_ptrs[0] = wire_worker;
|
||||
|
||||
wire_resource = (xce_resource_t*)calloc(1, sizeof(*wire_resource));
|
||||
wire_resource->name = strdup("mem");
|
||||
resource_ptrs = (xce_resource_t**)calloc(1, sizeof(*resource_ptrs));
|
||||
resource_ptrs[0] = wire_resource;
|
||||
|
||||
{
|
||||
xce_sched_update_request_t* rw = make_req(XCE_SCHED_OP_UPDATE_WORKER);
|
||||
rw->u.workerupdate.nworkers = 1;
|
||||
rw->u.workerupdate.workers = worker_ptrs;
|
||||
enqueue(&core, rw);
|
||||
|
||||
xce_sched_update_request_t* rr
|
||||
= make_req(XCE_SCHED_OP_UPDATE_RESOURCES);
|
||||
rr->u.resourceupdate.nresources = 1;
|
||||
rr->u.resourceupdate.resources = resource_ptrs;
|
||||
enqueue(&core, rr);
|
||||
|
||||
/* xce_sched_driver_translate_pending_events() returns the number of
|
||||
* events translated on success (test_driver_translate.c's own
|
||||
* established convention -- e.g. its 10-op-type drain test asserts
|
||||
* `TEST_ASSERT_EQUAL_INT(10, rc)`), not a bare 0/-errno status. Two
|
||||
* requests (worker + resource) were enqueued above. */
|
||||
rc = xce_sched_driver_translate_pending_events(&core, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(2, rc);
|
||||
}
|
||||
|
||||
/* makeResource() (resobj.c) deep-copies into a FRESH xce_resource_t
|
||||
* instance and assigns ITS OWN idx from the resmgr's key_add() return
|
||||
* value -- wire_resource.idx (the source struct pushed above) is never
|
||||
* mutated in place, so the authoritative idx must be read back from the
|
||||
* resmgr, not assumed. */
|
||||
rc = key_find(core.resmgr, "mem", (void**)®istered_res);
|
||||
TEST_ASSERT_TRUE(rc > 0);
|
||||
TEST_ASSERT_NOT_NULL(registered_res);
|
||||
|
||||
/* Drain #1: first periodic resval report -- adopts value 4.0. */
|
||||
{
|
||||
xce_sched_update_request_t* r1 = make_req(XCE_SCHED_OP_UPDATE_RESVALS);
|
||||
r1->u.resvalupdate.resvals = make_resval_delta("hostA", "mem", 4.0);
|
||||
enqueue(&core, r1);
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&core, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
}
|
||||
|
||||
/* Drain #2: second periodic resval report for the SAME worker+resource
|
||||
* -- this is exactly what a live deployment's resource_sync thread does
|
||||
* every interval. Pre-fix, zarray_put()'s eviction of drain #1's value
|
||||
* (already freed by drain #1's own release_translated_payload()) is a
|
||||
* heap-use-after-free -- ASAN aborts the process right here. */
|
||||
{
|
||||
xce_sched_update_request_t* r2 = make_req(XCE_SCHED_OP_UPDATE_RESVALS);
|
||||
r2->u.resvalupdate.resvals = make_resval_delta("hostA", "mem", 8.0);
|
||||
enqueue(&core, r2);
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&core, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
}
|
||||
|
||||
w = sched_get_worker(&core, "hostA");
|
||||
TEST_ASSERT_NOT_NULL(w);
|
||||
|
||||
got = (xce_val_t*)zarray_get(&(w->resvals), registered_res->idx);
|
||||
TEST_ASSERT_NOT_NULL(got);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_VAL_NUMERIC, got->which);
|
||||
TEST_ASSERT_EQUAL_FLOAT(8.0f, (float)got->u.fvalue);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* RESVALS_FULL: same hazard, core->sharedInst instead of a per-worker */
|
||||
/* zarray (core_events.c's XCE_CORE_EVENT_RESVALS_FULL case, line ~750). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_resvals_full_second_drain_does_not_use_after_free(void)
|
||||
{
|
||||
xce_sched_core_t core;
|
||||
xce_resource_t* wire_resource;
|
||||
xce_resource_t** resource_ptrs;
|
||||
xce_resource_t* registered_res = NULL;
|
||||
xce_val_t* got;
|
||||
int rc;
|
||||
|
||||
fixture_init(&core);
|
||||
|
||||
wire_resource = (xce_resource_t*)calloc(1, sizeof(*wire_resource));
|
||||
wire_resource->name = strdup("slots");
|
||||
resource_ptrs = (xce_resource_t**)calloc(1, sizeof(*resource_ptrs));
|
||||
resource_ptrs[0] = wire_resource;
|
||||
|
||||
{
|
||||
xce_sched_update_request_t* rr
|
||||
= make_req(XCE_SCHED_OP_UPDATE_RESOURCES);
|
||||
rr->u.resourceupdate.nresources = 1;
|
||||
rr->u.resourceupdate.resources = resource_ptrs;
|
||||
enqueue(&core, rr);
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&core, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
}
|
||||
|
||||
/* See test_resvals_delta_second_drain_does_not_use_after_free()'s
|
||||
* identical comment: makeResource() assigns idx on its own fresh
|
||||
* instance, never on wire_resource itself. */
|
||||
rc = key_find(core.resmgr, "slots", (void**)®istered_res);
|
||||
TEST_ASSERT_TRUE(rc > 0);
|
||||
TEST_ASSERT_NOT_NULL(registered_res);
|
||||
|
||||
/* Drain #1: first full resval snapshot -- adopts value 16.0. */
|
||||
{
|
||||
xce_val_t* val1 = (xce_val_t*)calloc(1, sizeof(*val1));
|
||||
xce_resval_t* rv1 = (xce_resval_t*)calloc(1, sizeof(*rv1));
|
||||
xce_resval_t** vals1 = (xce_resval_t**)calloc(1, sizeof(*vals1));
|
||||
|
||||
val1->which = XCE_VAL_NUMERIC;
|
||||
val1->u.fvalue = 16.0;
|
||||
rv1->name = strdup("slots");
|
||||
rv1->val = val1;
|
||||
vals1[0] = rv1;
|
||||
|
||||
xce_sched_update_request_t* r1
|
||||
= make_req(XCE_SCHED_OP_UPDATE_RESVALS_FULL);
|
||||
r1->u.resvalfullupdate.nvals = 1;
|
||||
r1->u.resvalfullupdate.vals = vals1;
|
||||
enqueue(&core, r1);
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&core, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
}
|
||||
|
||||
/* Drain #2: second full resval snapshot for the SAME resource -- same
|
||||
* live hazard as the RESVALS_DELTA test above. */
|
||||
{
|
||||
xce_val_t* val2 = (xce_val_t*)calloc(1, sizeof(*val2));
|
||||
xce_resval_t* rv2 = (xce_resval_t*)calloc(1, sizeof(*rv2));
|
||||
xce_resval_t** vals2 = (xce_resval_t**)calloc(1, sizeof(*vals2));
|
||||
|
||||
val2->which = XCE_VAL_NUMERIC;
|
||||
val2->u.fvalue = 32.0;
|
||||
rv2->name = strdup("slots");
|
||||
rv2->val = val2;
|
||||
vals2[0] = rv2;
|
||||
|
||||
xce_sched_update_request_t* r2
|
||||
= make_req(XCE_SCHED_OP_UPDATE_RESVALS_FULL);
|
||||
r2->u.resvalfullupdate.nvals = 1;
|
||||
r2->u.resvalfullupdate.vals = vals2;
|
||||
enqueue(&core, r2);
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&core, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
}
|
||||
|
||||
got = (xce_val_t*)zarray_get(&(core.sharedInst), registered_res->idx);
|
||||
TEST_ASSERT_NOT_NULL(got);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_VAL_NUMERIC, got->which);
|
||||
TEST_ASSERT_EQUAL_FLOAT(32.0f, (float)got->u.fvalue);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_resvals_delta_second_drain_does_not_use_after_free);
|
||||
RUN_TEST(test_resvals_full_second_drain_does_not_use_after_free);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
@ -380,17 +380,23 @@ static char* run_sim_and_capture_stdout(const char* base_path,
|
|||
|
||||
static void test_baseline_run_uses_base_file_events(void)
|
||||
{
|
||||
char base_jobid_marker[32];
|
||||
char alt_jobid_marker[32];
|
||||
char base_jobid_marker[80];
|
||||
char alt_jobid_marker[80];
|
||||
char jobid_buf[64];
|
||||
char path[PATH_MAX];
|
||||
FILE* f;
|
||||
int rc;
|
||||
char* captured;
|
||||
|
||||
snprintf(base_jobid_marker, sizeof(base_jobid_marker), "jobid=%llu",
|
||||
(unsigned long long)BASE_JOBID);
|
||||
snprintf(alt_jobid_marker, sizeof(alt_jobid_marker), "jobid=%llu",
|
||||
(unsigned long long)ALT_JOBID);
|
||||
/* 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, computed the same way, not hardcoded. */
|
||||
fmt_jobid(jobid_buf, sizeof(jobid_buf), BASE_JOBID);
|
||||
snprintf(base_jobid_marker, sizeof(base_jobid_marker), "jobid=%s",
|
||||
jobid_buf);
|
||||
fmt_jobid(jobid_buf, sizeof(jobid_buf), ALT_JOBID);
|
||||
snprintf(alt_jobid_marker, sizeof(alt_jobid_marker), "jobid=%s",
|
||||
jobid_buf);
|
||||
|
||||
f = write_header_and_snapshot(path, sizeof(path), "_baseline");
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
|
|
@ -424,8 +430,9 @@ static void test_baseline_run_uses_base_file_events(void)
|
|||
|
||||
static void test_alt_events_run_uses_alt_file_events_not_base(void)
|
||||
{
|
||||
char base_jobid_marker[32];
|
||||
char alt_jobid_marker[32];
|
||||
char base_jobid_marker[80];
|
||||
char alt_jobid_marker[80];
|
||||
char jobid_buf[64];
|
||||
char base_path[PATH_MAX];
|
||||
char alt_path[PATH_MAX];
|
||||
FILE* base_f;
|
||||
|
|
@ -433,10 +440,14 @@ static void test_alt_events_run_uses_alt_file_events_not_base(void)
|
|||
int rc;
|
||||
char* captured;
|
||||
|
||||
snprintf(base_jobid_marker, sizeof(base_jobid_marker), "jobid=%llu",
|
||||
(unsigned long long)BASE_JOBID);
|
||||
snprintf(alt_jobid_marker, sizeof(alt_jobid_marker), "jobid=%llu",
|
||||
(unsigned long long)ALT_JOBID);
|
||||
/* bug-1591: see test_baseline_run_uses_base_file_events()'s identical
|
||||
* fmt_jobid() comment. */
|
||||
fmt_jobid(jobid_buf, sizeof(jobid_buf), BASE_JOBID);
|
||||
snprintf(base_jobid_marker, sizeof(base_jobid_marker), "jobid=%s",
|
||||
jobid_buf);
|
||||
fmt_jobid(jobid_buf, sizeof(jobid_buf), ALT_JOBID);
|
||||
snprintf(alt_jobid_marker, sizeof(alt_jobid_marker), "jobid=%s",
|
||||
jobid_buf);
|
||||
|
||||
/* Base file: header + empty snapshot, then its OWN (never-to-be-read,
|
||||
* once --sim-alt-events is set) events for BASE_JOBID -- present to
|
||||
|
|
|
|||
|
|
@ -695,6 +695,8 @@ static void
|
|||
test_job_present_only_in_initial_snapshot_still_reaches_the_picker(void)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
char pend_marker[80];
|
||||
char jobid_buf[64];
|
||||
FILE* f;
|
||||
int rc;
|
||||
char* captured;
|
||||
|
|
@ -714,9 +716,15 @@ test_job_present_only_in_initial_snapshot_still_reaches_the_picker(void)
|
|||
|
||||
captured = run_sim_and_capture_stdout(path, &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);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, "PEND jobid=4242"),
|
||||
strstr(captured, pend_marker),
|
||||
"a job present only in the replay file's INITIAL snapshot must "
|
||||
"still reach the real picker and receive a live PEND decision "
|
||||
"(bug-1567)");
|
||||
|
|
|
|||
|
|
@ -482,8 +482,16 @@ static void log_one_mismatch_for_capture(void)
|
|||
|
||||
static void test_log_mismatch_reports_logical_time_and_decision_detail(void)
|
||||
{
|
||||
char jobid_marker[80];
|
||||
char jobid_buf[64];
|
||||
char* buf = capture_stdout_around(log_one_mismatch_for_capture);
|
||||
|
||||
/* bug-1591: sim_verify_describe_{replay_,}decision() now render jobids
|
||||
* via fmt_jobid(), not raw %llu -- this marker must match that same
|
||||
* human-readable form. */
|
||||
fmt_jobid(jobid_buf, sizeof(jobid_buf), 9001ULL);
|
||||
snprintf(jobid_marker, sizeof(jobid_marker), "jobid=%s", jobid_buf);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(buf, "MISMATCH"),
|
||||
"log_mismatch must report a MISMATCH");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
|
|
@ -494,7 +502,7 @@ static void test_log_mismatch_reports_logical_time_and_decision_detail(void)
|
|||
"log_mismatch must report the decision's position within the "
|
||||
"session");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(buf, "jobid=9001"),
|
||||
strstr(buf, jobid_marker),
|
||||
"log_mismatch must report actionable decision detail (jobid)");
|
||||
|
||||
free(buf);
|
||||
|
|
@ -831,6 +839,8 @@ static void test_verify_against_self_reports_zero_mismatches(void)
|
|||
static void test_verify_against_altered_copy_reports_exact_mismatch(void)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
char jobid_marker[80];
|
||||
char jobid_buf[64];
|
||||
FILE* f;
|
||||
int rc;
|
||||
char* captured;
|
||||
|
|
@ -846,13 +856,19 @@ static void test_verify_against_altered_copy_reports_exact_mismatch(void)
|
|||
|
||||
captured = run_sim_and_capture_stdout(path, true, &rc);
|
||||
|
||||
/* bug-1591: sim_verify_describe_{replay_,}decision() now render
|
||||
* jobids via fmt_jobid(), not raw %llu -- this marker must match that
|
||||
* same human-readable form. */
|
||||
fmt_jobid(jobid_buf, sizeof(jobid_buf), 777ULL);
|
||||
snprintf(jobid_marker, sizeof(jobid_marker), "jobid=%s", jobid_buf);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, "MISMATCH"),
|
||||
"verifying against a deliberately-altered recorded decision must "
|
||||
"report a mismatch");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
strstr(captured, "jobid=777"),
|
||||
strstr(captured, jobid_marker),
|
||||
"the mismatch report must name the specific job");
|
||||
|
||||
free(captured);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue