feat(scheduler-core): implement scheduler-side admin request handler (task 9.4)
Implements the scheduler's first request-reply opcode (OPCode_schedAdmin). sched_handle_admin_request() (handler.c) decodes SchedAdminRequest and dispatches by action: recordStatus answers synchronously (pure read); recordStart/recordStop are deferred onto the existing pend_updates queue (XCE_SCHED_OP_ADMIN_RECORD) since sched_record_start()'s snapshot call requires the session_active==0 precondition only guaranteed on the driver thread, not the async request-handling thread. driver.c's xce_sched_driver_translate_pending_events() performs the deferred action and sends the reply via a new xcecli_reply_schedadmin() (client.c/client.h), correlated by seq copied before the original request is freed. Adversarial review found a genuine cross-thread race: sched_record_status() read driver-owned recording state (including a pointer the driver thread frees) from the vtask thread with no lock -- the codebase's first such race, missed by ASan (doesn't detect races). Fixed with a dedicated record_lock scoped strictly to in-memory field access, never spanning file I/O; sched_record_status() now strdup()s under the lock instead of returning a borrowed live pointer. Verified with a real two-thread lock-contention test (independently confirmed non-vacuous by removing the lock and observing the test fail) plus a deterministic use-after-free reproduction. 26/26 scheduler suite, clang-format clean.
This commit is contained in:
parent
7494542a60
commit
e819a50600
19 changed files with 1761 additions and 115 deletions
|
|
@ -434,6 +434,30 @@ sched_record_lifecycle_test = executable(
|
|||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Contract test for the scheduler-side OPCode_schedAdmin request handler
|
||||
# (scheduler-core spec, task 9.4, Req 5 AC 10): decodes a forwarded
|
||||
# SchedAdminRequest, dispatches by action to sched_record_start()/
|
||||
# sched_record_stop()/sched_record_status(), and sends back a
|
||||
# SchedAdminReply matching requirements.md's field-semantics table.
|
||||
# recordStatus is proven synchronous (answered directly on the handler
|
||||
# thread, both idle and while genuinely recording); recordStart/recordStop
|
||||
# are proven deferred (enqueued, not executed, until a subsequent
|
||||
# xce_sched_driver_translate_pending_events() drain actually performs the
|
||||
# real sched_record_start()/sched_record_stop() call and sends the reply).
|
||||
# Same white-box #include pattern as sched_driver_translate_test
|
||||
# (core.c/core_clock.c/core_events.c/driver.c) PLUS ../src/recording.c/
|
||||
# recording_events.c/snapshot.c/snapshot_wire.c (real record start/stop/
|
||||
# status/write, task 9.1/9.2/9.3) PLUS ../src/handler.c (the code under
|
||||
# test). xcecli_reply_schedadmin() is a SPY test double, same pattern
|
||||
# sched_driver_decision_cb_test already establishes for
|
||||
# sched_publish_dispatch()/sched_publish_pend_reason().
|
||||
sched_admin_request_test = executable(
|
||||
'sched_admin_request_test',
|
||||
['test_admin_request.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Register as meson tests so `meson test --suite scheduler` runs them all.
|
||||
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
|
||||
test('scheduler test', scheduler_test, suite: 'scheduler',
|
||||
|
|
@ -500,3 +524,6 @@ test('scheduler recording path validation test',
|
|||
test('scheduler record lifecycle test',
|
||||
sched_record_lifecycle_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler admin request test',
|
||||
sched_admin_request_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
903
src/scheduler/testsuite/test_admin_request.c
Normal file
903
src/scheduler/testsuite/test_admin_request.c
Normal file
|
|
@ -0,0 +1,903 @@
|
|||
/*
|
||||
* test_admin_request.c - contract test for the scheduler-side
|
||||
* OPCode_schedAdmin request handler (scheduler-core spec, task 9.4, Req 5
|
||||
* AC 10). This is the scheduler-side counterpart to task 10.1's
|
||||
* controller-side forwarding dispatch (out of THIS spec's boundary): the
|
||||
* controller relays a SchedAdminRequest verbatim to this endpoint, and this
|
||||
* file proves the scheduler decodes it, dispatches by action to
|
||||
* sched_record_start()/sched_record_stop()/sched_record_status() (tasks
|
||||
* 9.2/9.3/9.4), and sends back a SchedAdminReply whose fields match
|
||||
* requirements.md's "Reply field semantics by action" table.
|
||||
*
|
||||
* Observable under test:
|
||||
* - recordStatus is answered SYNCHRONOUSLY, on the calling (handler)
|
||||
* thread, directly from handler.c's sched_handle_admin_request() --
|
||||
* never enqueued -- both when not recording (all-zero/empty reply) and
|
||||
* while genuinely recording (live event count/file/started-at).
|
||||
* - recordStart/recordStop are DEFERRED: enqueued onto the same
|
||||
* pend_updates dataq every other controller-sourced update flows
|
||||
* through (XCE_SCHED_OP_ADMIN_RECORD), NOT executed synchronously --
|
||||
* `sched->recording` is provably unchanged immediately after
|
||||
* scheduler_req_handler() returns -- and only actually performed (real
|
||||
* sched_record_start()/sched_record_stop() call, real reply send) by a
|
||||
* subsequent xce_sched_driver_translate_pending_events() drain
|
||||
* (driver.c's handle_deferred_admin_record()).
|
||||
* - recordStart while already recording / recordStop while not recording
|
||||
* reply {ok=false, error="already recording"/"not recording"} with the
|
||||
* table's documented (unchanged) field values.
|
||||
* - An invalid recordFile (path separator) surfaces all the way through
|
||||
* the deferred path as {ok=false, error="invalid record file name"},
|
||||
* never just an internal log line, and never actually opens a file.
|
||||
* - An unrecognized SchedAdminAction returns -EINVAL, logs, and neither
|
||||
* enqueues anything nor sends any reply -- no crash.
|
||||
*
|
||||
* White-box #include of ../src/core.c, ../src/core_clock.c,
|
||||
* ../src/core_events.c, ../src/driver.c (same set test_driver_translate.c
|
||||
* uses to exercise xce_sched_driver_translate_pending_events() for real)
|
||||
* PLUS ../src/recording.c/recording_events.c (task 9.1/9.2/9.3's REAL
|
||||
* sched_record_start()/sched_record_stop()/sched_record_status()/
|
||||
* sched_record_write_events() -- this file's whole point is proving the
|
||||
* deferred path performs REAL file I/O, so these must be genuine, not
|
||||
* stubbed) PLUS ../src/snapshot.c/snapshot_wire.c (task 8.1's real
|
||||
* xce_sched_core_save_snapshot(), needed by sched_record_start()) PLUS
|
||||
* ../src/handler.c (task 9.4's sched_handle_admin_request()/
|
||||
* scheduler_req_handler(), the code under test).
|
||||
*
|
||||
* recording_session.c is NOT linked: its sched_record_session_begin()/
|
||||
* sched_record_session_add_decision()/sched_record_session_end() trio is
|
||||
* only reachable from xce_sched_driver_run_normal_mode_round(), which this
|
||||
* file never calls (it drives xce_sched_driver_translate_pending_events()
|
||||
* directly, same as test_driver_translate.c) -- stubbed below purely to
|
||||
* satisfy the linker, same rationale as that file's identical stubs for the
|
||||
* same reason. Likewise core_session.c/plugin.c are not linked (their
|
||||
* exported functions are stubbed below); no real scheduling session is ever
|
||||
* run by any test in this file.
|
||||
*
|
||||
* xcecli_reply_schedadmin() is a SPY test double (client.c's real
|
||||
* implementation needs a live capnp connection this file never sets up) --
|
||||
* same pattern test_driver_decision_cb.c already establishes for
|
||||
* sched_publish_dispatch()/sched_publish_pend_reason(). It deep-copies
|
||||
* every field of the passed sched_admin_reply_t (including string fields)
|
||||
* into fixed-size static storage rather than retaining the passed pointers:
|
||||
* both handle_deferred_admin_record() (recordStop's `file`) and
|
||||
* sched_record_status() (`file`, a BORROWED alias of
|
||||
* sched->record_file_path) document that their reply pointers are only
|
||||
* valid for the duration of the synchronous call -- recordStop's in
|
||||
* particular aliases a stack-local buffer that is gone the instant
|
||||
* handle_deferred_admin_record() returns, so retaining the raw pointer
|
||||
* would be a stack-use-after-return read the very next line.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "controller.capnp.h"
|
||||
#include "dataq.h"
|
||||
#include "kvmgr.h"
|
||||
#include "mylock.h"
|
||||
#include "scheduler.h"
|
||||
#include "xstring.h"
|
||||
|
||||
/* Pull in the real functions under test into this TU (white-box #include,
|
||||
* same convention as test_driver_translate.c/test_record_lifecycle.c). */
|
||||
#include "../src/core.c"
|
||||
#include "../src/core_clock.c"
|
||||
#include "../src/core_events.c"
|
||||
#include "../src/driver.c"
|
||||
#include "../src/recording.c"
|
||||
#include "../src/recording_events.c"
|
||||
#include "../src/snapshot.c"
|
||||
#include "../src/snapshot_wire.c"
|
||||
|
||||
void kill_sched_job(void* arg) { (void)arg; }
|
||||
void kill_sched_queue(void* arg) { (void)arg; }
|
||||
void kill_sched_worker(void* arg) { (void)arg; }
|
||||
void kill_sched_user(void* arg) { (void)arg; }
|
||||
void kill_sched_project(void* arg) { (void)arg; }
|
||||
void kill_sched_resval(xce_val_t* val) { (void)val; }
|
||||
void plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* Link-satisfying stubs for scheduler.c's sched_publish_dispatch()/
|
||||
* sched_publish_pend_reason() -- driver.c's (real, #included above)
|
||||
* xce_sched_driver_on_decision() references both; never actually invoked by
|
||||
* any test in this file (no session is ever run, so no decision is ever
|
||||
* drained). Same rationale as test_driver_translate.c's identical stubs. */
|
||||
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 stubs: xce_sched_driver_run_normal_mode_round() (driver.c,
|
||||
* real, #included above, but never CALLED by any test here -- this file
|
||||
* only drives xce_sched_driver_translate_pending_events() directly, same as
|
||||
* test_driver_translate.c) references xce_core_begin_session()/
|
||||
* xce_core_signal_go()/xce_core_end_session() (core_session.c) and
|
||||
* plugin_begin_session()/plugin_end_session() (plugin.c) /
|
||||
* sched_account_jobs() (scheduler.c). Never actually invoked at runtime. */
|
||||
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;
|
||||
}
|
||||
|
||||
/* Link-satisfying stubs for recording_session.c's real functions -- see
|
||||
* this file's header comment for why recording_session.c itself is not
|
||||
* linked. Never actually invoked (no session is ever run). */
|
||||
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;
|
||||
}
|
||||
|
||||
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
||||
* rationale as test_driver_translate.c's own copy (schedobj.c is not
|
||||
* linked). Never actually exercised (no test here pushes core events, so
|
||||
* xce_core_push_events() -- the only real caller -- is never reached: every
|
||||
* enqueued request in this file is XCE_SCHED_OP_ADMIN_RECORD, intercepted
|
||||
* by driver.c BEFORE the translated batch/push path). Present only to
|
||||
* satisfy the linker for core_events.c. */
|
||||
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
|
||||
{
|
||||
(void)core;
|
||||
(void)out;
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Link-satisfying stub for scheduler.c's sched_init_config() -- handler.c's
|
||||
* (real, #included below) OPCode_push/configpushmsg case references it;
|
||||
* this file never exercises that path (out of scope -- this file's whole
|
||||
* surface is OPCode_schedAdmin). Never actually invoked. */
|
||||
int sched_init_config(xce_scheduler_t* s, xce_conf4sched_t* c)
|
||||
{
|
||||
(void)s;
|
||||
(void)c;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Pull in the real sched_handle_admin_request()/scheduler_req_handler()
|
||||
* under test into this TU. */
|
||||
#include "../src/handler.c"
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Spy test double for client.c's xcecli_reply_schedadmin() -- see file */
|
||||
/* header comment for why every field is deep-copied rather than retaining */
|
||||
/* the passed sched_admin_reply_t's reply->file/reply->error pointers. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static int g_reply_calls;
|
||||
static uint64_t g_last_seq;
|
||||
static xcecli_ctx_t g_last_ctx;
|
||||
static int g_last_ok;
|
||||
static int g_last_error_null;
|
||||
static char g_last_error[256];
|
||||
static int g_last_recording;
|
||||
static int g_last_file_null;
|
||||
static char g_last_file[PATH_MAX];
|
||||
static uint64_t g_last_event_count;
|
||||
static uint64_t g_last_started_at;
|
||||
|
||||
static void reset_reply_spy(void)
|
||||
{
|
||||
g_reply_calls = 0;
|
||||
g_last_seq = 0;
|
||||
g_last_ctx = NULL;
|
||||
g_last_ok = 0;
|
||||
g_last_error_null = 1;
|
||||
g_last_error[0] = '\0';
|
||||
g_last_recording = 0;
|
||||
g_last_file_null = 1;
|
||||
g_last_file[0] = '\0';
|
||||
g_last_event_count = 0;
|
||||
g_last_started_at = 0;
|
||||
}
|
||||
|
||||
int xcecli_reply_schedadmin(xcecli_ctx_t c, uint64_t seq,
|
||||
sched_admin_reply_t* areply)
|
||||
{
|
||||
g_reply_calls++;
|
||||
g_last_ctx = c;
|
||||
g_last_seq = seq;
|
||||
|
||||
if (areply == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_last_ok = areply->ok;
|
||||
g_last_recording = areply->recording;
|
||||
g_last_event_count = areply->eventCount;
|
||||
g_last_started_at = areply->startedAt;
|
||||
|
||||
g_last_error_null = (areply->error == NULL);
|
||||
g_last_error[0] = '\0';
|
||||
if (areply->error != NULL) {
|
||||
strncpy(g_last_error, areply->error, sizeof(g_last_error) - 1);
|
||||
}
|
||||
|
||||
g_last_file_null = (areply->file == NULL);
|
||||
g_last_file[0] = '\0';
|
||||
if (areply->file != NULL) {
|
||||
strncpy(g_last_file, areply->file, sizeof(g_last_file) - 1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setUp(void) { reset_reply_spy(); }
|
||||
void tearDown(void) { return; }
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Fixture helpers -- same pattern as test_record_lifecycle.c's own. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void make_tmp_dir(char* out, size_t out_len, const char* suffix)
|
||||
{
|
||||
char tmpl[PATH_MAX];
|
||||
|
||||
snprintf(tmpl, sizeof(tmpl), "/tmp/sched_adminreq%s_XXXXXX", suffix);
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
|
||||
TEST_ASSERT_TRUE(strlen(tmpl) < out_len);
|
||||
strcpy(out, tmpl);
|
||||
}
|
||||
|
||||
/* `record_dir` must outlive `sched` (caller-owned storage), same convention
|
||||
* as test_record_lifecycle.c's fixture_init(). No picker plugin is
|
||||
* registered -- unlike test_record_lifecycle.c's fixture, no test in this
|
||||
* file ever runs a scheduling session, so core->job_pick_plugin staying
|
||||
* NULL is fine (sched_resolve_job_picker() above is never actually
|
||||
* reached). */
|
||||
static void fixture_init(xce_scheduler_t* sched, char* record_dir)
|
||||
{
|
||||
int rc;
|
||||
|
||||
memset(sched, 0, sizeof(*sched));
|
||||
|
||||
/* This fixture hand-builds xce_scheduler_t rather than calling the full
|
||||
* sched_init() (which starts real threads/network I/O this white-box
|
||||
* suite never wants) -- record_lock (scheduler.h field comments, task
|
||||
* 9.4 fix round) is normally initialized by sched_init(), so it must be
|
||||
* initialized here too, the same way core.lock is initialized by the
|
||||
* xce_sched_core_init() call just below. Every test in this file
|
||||
* exercises sched_record_status()/sched_record_start()/
|
||||
* sched_record_stop()/sched_record_write_events() for real, all of
|
||||
* which now take this lock. */
|
||||
mylock_init(&(sched->record_lock));
|
||||
|
||||
rc = xce_sched_core_init(&(sched->core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
sched->core.pend_updates[0] = (dataq_t*)calloc(1, sizeof(dataq_t));
|
||||
dataq_init(sched->core.pend_updates[0], NULL);
|
||||
sched->core.pend_updates[1] = (dataq_t*)calloc(1, sizeof(dataq_t));
|
||||
dataq_init(sched->core.pend_updates[1], NULL);
|
||||
|
||||
sched->record_dir = record_dir;
|
||||
/* Dummy non-NULL xcecli_ctx_t (an opaque void*) -- xcecli_reply_schedadmin
|
||||
* is spied above, so nothing ever dereferences this; a real production
|
||||
* ctx would come from xcecli_ctx_init(). */
|
||||
sched->apictx = (xcecli_ctx_t)sched;
|
||||
}
|
||||
|
||||
/* Builds a request/reply pair on the stack and drives it through the REAL
|
||||
* production entry point, scheduler_req_handler() (handler.c) -- exactly
|
||||
* the function main.c registers as the scheduler's request handler and the
|
||||
* function that dispatches OPCode_schedAdmin to sched_handle_admin_request()
|
||||
* (handler.c's own new `case` this task added). */
|
||||
static int send_admin_request(xce_scheduler_t* sched, int action,
|
||||
const char* record_file, uint64_t seq)
|
||||
{
|
||||
sched_admin_req_t areq;
|
||||
xce_request_t request;
|
||||
|
||||
memset(&areq, 0, sizeof(areq));
|
||||
areq.action = action;
|
||||
areq.recordFile = (char*)record_file;
|
||||
|
||||
memset(&request, 0, sizeof(request));
|
||||
request.opcode = OPCode_schedAdmin;
|
||||
request.seq = seq;
|
||||
request.body_which = Request_body_schedadminmsg;
|
||||
request.body.schedadminmsg = &areq;
|
||||
|
||||
return scheduler_req_handler(&request, sched);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* recordStatus -- synchronous, answered directly from the handler thread. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_recordstatus_not_recording_returns_empty_state(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_status_idle");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
rc = send_admin_request(&sched, SchedAdminAction_recordStatus, NULL, 111);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, g_reply_calls);
|
||||
TEST_ASSERT_EQUAL_UINT64(111, g_last_seq);
|
||||
TEST_ASSERT_EQUAL_PTR(sched.apictx, g_last_ctx);
|
||||
TEST_ASSERT_TRUE(g_last_ok);
|
||||
TEST_ASSERT_FALSE(g_last_recording);
|
||||
TEST_ASSERT_TRUE(g_last_file_null);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, g_last_event_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, g_last_started_at);
|
||||
|
||||
/* recordStatus never touches the deferred queue. */
|
||||
TEST_ASSERT_NULL(dataq_deq(sched.core.pend_updates[0], NULL));
|
||||
}
|
||||
|
||||
static void test_recordstatus_while_recording_reflects_live_state(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
xce_core_event_t ev;
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_status_live");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
rc = sched_record_start(&sched, "live-status.replay");
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_TRUE(sched.recording);
|
||||
|
||||
/* Real event writes via recording_events.c's genuine
|
||||
* sched_record_write_events() -- record_event_count genuinely advances,
|
||||
* not a fabricated field. */
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE;
|
||||
ev.u.job_state_change.jobid = 7ULL;
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_record_write_events(&sched, &ev, 1));
|
||||
ev.u.job_state_change.jobid = 8ULL;
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_record_write_events(&sched, &ev, 1));
|
||||
TEST_ASSERT_EQUAL_UINT64(2ULL, sched.record_event_count);
|
||||
|
||||
rc = send_admin_request(&sched, SchedAdminAction_recordStatus, NULL, 222);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, g_reply_calls);
|
||||
TEST_ASSERT_TRUE(g_last_ok);
|
||||
TEST_ASSERT_TRUE(g_last_recording);
|
||||
TEST_ASSERT_FALSE(g_last_file_null);
|
||||
TEST_ASSERT_TRUE(strstr(g_last_file, "live-status.replay") != NULL);
|
||||
TEST_ASSERT_EQUAL_UINT64(2ULL, g_last_event_count);
|
||||
TEST_ASSERT_TRUE(g_last_started_at > 0);
|
||||
TEST_ASSERT_EQUAL_UINT64(sched.record_start_ts, g_last_started_at);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* recordStatus concurrency -- record_lock (scheduler.h field comments, */
|
||||
/* task 9.4 fix round) genuinely protects the recording-state fields */
|
||||
/* against the vtask-thread sched_record_status() reader / driver-thread */
|
||||
/* writer race the code review flagged. Two DETERMINISTIC proofs rather */
|
||||
/* than a single flaky, purely timing-dependent race reproduction -- */
|
||||
/* task 9.2's TOCTOU test already established this codebase's precedent */
|
||||
/* of preferring a deterministic proof over a genuinely racy test: */
|
||||
/* */
|
||||
/* 1. record_lock is a REAL mutex a second, genuine pthread actually */
|
||||
/* blocks on -- not just "the code calls mylock_lock()" verified by */
|
||||
/* inspection. Proven by holding the lock on the main thread */
|
||||
/* (simulating "driver thread is mid-mutation"), launching a real */
|
||||
/* thread that calls sched_record_status(), and asserting it has NOT */
|
||||
/* completed after a generous wait -- then asserting it DOES */
|
||||
/* complete once the lock is released. The only timing dependency is */
|
||||
/* in the safe direction: a slow CI runner can only make the "still */
|
||||
/* blocked" assertion harder to observe, never produce a false */
|
||||
/* positive (record_lock is genuinely held for the whole wait). */
|
||||
/* 2. reply->file is a genuinely independent allocation, not an alias of */
|
||||
/* the live sched->record_file_path -- proven by taking a status */
|
||||
/* snapshot, then calling the real sched_record_stop() (which frees */
|
||||
/* sched->record_file_path for real) and confirming the snapshot's */
|
||||
/* reply->file is untouched. This reproduces the exact */
|
||||
/* use-after-free the finding identified, deterministically, with no */
|
||||
/* reliance on OS thread-scheduling timing at all. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
xce_scheduler_t* sched;
|
||||
sched_admin_reply_t reply;
|
||||
int rc;
|
||||
atomic_int done;
|
||||
} status_thread_arg_t;
|
||||
|
||||
static void* status_thread_fn(void* arg)
|
||||
{
|
||||
status_thread_arg_t* a = (status_thread_arg_t*)arg;
|
||||
|
||||
a->rc = sched_record_status(a->sched, &(a->reply));
|
||||
atomic_store(&(a->done), 1);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_recordstatus_genuinely_blocks_on_record_lock(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
status_thread_arg_t arg;
|
||||
pthread_t th;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_status_lockblock");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_record_start(&sched, "lockblock.replay"));
|
||||
|
||||
memset(&arg, 0, sizeof(arg));
|
||||
arg.sched = &sched;
|
||||
atomic_store(&(arg.done), 0);
|
||||
|
||||
/* Simulate "driver thread is mid-mutation": hold record_lock on THIS
|
||||
* (main test) thread, exactly the window sched_record_start()/
|
||||
* sched_record_stop()/sched_record_abort_on_error() hold it for in
|
||||
* production. */
|
||||
mylock_lock(&(sched.record_lock));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_create(&th, NULL, status_thread_fn, &arg));
|
||||
|
||||
/* Generous wait -- if record_lock were NOT actually taken by
|
||||
* sched_record_status() (i.e. the fix regressed to a no-op), the
|
||||
* spawned thread would complete almost instantly and `done` would
|
||||
* already be 1 here. */
|
||||
usleep(150000); /* 150ms */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, atomic_load(&(arg.done)));
|
||||
|
||||
mylock_unlock(&(sched.record_lock));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_join(th, NULL));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, atomic_load(&(arg.done)));
|
||||
TEST_ASSERT_EQUAL_INT(0, arg.rc);
|
||||
TEST_ASSERT_TRUE(arg.reply.recording);
|
||||
TEST_ASSERT_NOT_NULL(arg.reply.file);
|
||||
TEST_ASSERT_TRUE(strstr(arg.reply.file, "lockblock.replay") != NULL);
|
||||
|
||||
free(arg.reply.file);
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_record_stop(&sched));
|
||||
}
|
||||
|
||||
static void test_recordstatus_file_survives_concurrent_record_stop_free(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
sched_admin_reply_t snapshot;
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_status_uaf");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
rc = sched_record_start(&sched, "uaftest.replay");
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = sched_record_status(&sched, &snapshot);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_NOT_NULL(snapshot.file);
|
||||
/* Genuinely a DIFFERENT allocation from the live pointer -- not an
|
||||
* alias (the exact bug this fix closes: the pre-fix version set
|
||||
* reply->file = sched->record_file_path directly). */
|
||||
TEST_ASSERT_TRUE(sched.record_file_path != snapshot.file);
|
||||
TEST_ASSERT_EQUAL_STRING(sched.record_file_path, snapshot.file);
|
||||
|
||||
/* sched_record_stop() frees sched->record_file_path for real -- if
|
||||
* snapshot.file aliased it, this would be a genuine use-after-free
|
||||
* (ASan would abort) the moment the next assertion reads it. */
|
||||
rc = sched_record_stop(&sched);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_NULL(sched.record_file_path);
|
||||
|
||||
TEST_ASSERT_TRUE(strstr(snapshot.file, "uaftest.replay") != NULL);
|
||||
|
||||
free(snapshot.file);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* recordStart -- deferred, not synchronous. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_recordstart_is_deferred_then_performed_by_driver_round(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
char expect_path[PATH_MAX];
|
||||
int rc;
|
||||
struct stat st;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_start_deferred");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
rc = send_admin_request(&sched, SchedAdminAction_recordStart,
|
||||
"admin-start.replay", 333);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Deferred, not synchronous: no start yet, no reply yet. */
|
||||
TEST_ASSERT_FALSE(sched.recording);
|
||||
TEST_ASSERT_NULL(sched.record_file_path);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_reply_calls);
|
||||
|
||||
/* A subsequent driver round actually performs the deferred start. */
|
||||
rc = xce_sched_driver_translate_pending_events(&(sched.core), &sched);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc); /* n==0: the admin op never enters the
|
||||
* translated event batch. */
|
||||
|
||||
TEST_ASSERT_TRUE(sched.recording);
|
||||
TEST_ASSERT_NOT_NULL(sched.record_file_path);
|
||||
TEST_ASSERT_TRUE(strstr(sched.record_file_path, "admin-start.replay")
|
||||
!= NULL);
|
||||
|
||||
/* The file is genuinely created on disk. */
|
||||
snprintf(expect_path, sizeof(expect_path), "%s/admin-start.replay",
|
||||
record_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, stat(expect_path, &st));
|
||||
TEST_ASSERT_TRUE(S_ISREG(st.st_mode));
|
||||
|
||||
/* And the deferred reply was genuinely sent, matching the "recordStart
|
||||
* (ok)" column of requirements.md's field-semantics table. */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_reply_calls);
|
||||
TEST_ASSERT_EQUAL_UINT64(333, g_last_seq);
|
||||
TEST_ASSERT_TRUE(g_last_ok);
|
||||
TEST_ASSERT_TRUE(g_last_error_null);
|
||||
TEST_ASSERT_TRUE(g_last_recording);
|
||||
TEST_ASSERT_FALSE(g_last_file_null);
|
||||
TEST_ASSERT_TRUE(strstr(g_last_file, "admin-start.replay") != NULL);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, g_last_event_count);
|
||||
TEST_ASSERT_TRUE(g_last_started_at > 0);
|
||||
}
|
||||
|
||||
static void test_recordstart_empty_file_auto_generates_name(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_start_auto");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
/* NULL recordFile -- "empty = auto-generate" (requirements.md's
|
||||
* SchedAdminRequest.recordFile field comment). */
|
||||
rc = send_admin_request(&sched, SchedAdminAction_recordStart, NULL, 444);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&(sched.core), &sched);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
TEST_ASSERT_TRUE(sched.recording);
|
||||
TEST_ASSERT_NOT_NULL(sched.record_file_path);
|
||||
TEST_ASSERT_TRUE(strstr(sched.record_file_path, record_dir) != NULL);
|
||||
TEST_ASSERT_TRUE(strstr(sched.record_file_path, ".replay") != NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, g_reply_calls);
|
||||
TEST_ASSERT_TRUE(g_last_ok);
|
||||
TEST_ASSERT_FALSE(g_last_file_null);
|
||||
}
|
||||
|
||||
static void test_recordstart_while_already_recording_replies_error(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
char path_before[PATH_MAX];
|
||||
uint64_t started_before;
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_start_already");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
rc = sched_record_start(&sched, "first.replay");
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
strcpy(path_before, sched.record_file_path);
|
||||
started_before = sched.record_start_ts;
|
||||
|
||||
rc = send_admin_request(&sched, SchedAdminAction_recordStart,
|
||||
"second.replay", 555);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&(sched.core), &sched);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Original recording untouched by the rejected second start. */
|
||||
TEST_ASSERT_TRUE(sched.recording);
|
||||
TEST_ASSERT_EQUAL_STRING(path_before, sched.record_file_path);
|
||||
TEST_ASSERT_EQUAL_UINT64(0ULL, sched.record_event_count);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, g_reply_calls);
|
||||
TEST_ASSERT_EQUAL_UINT64(555, g_last_seq);
|
||||
TEST_ASSERT_FALSE(g_last_ok);
|
||||
TEST_ASSERT_FALSE(g_last_error_null);
|
||||
TEST_ASSERT_EQUAL_STRING("already recording", g_last_error);
|
||||
/* "(unchanged)" -- requirements.md's recordStart(err) column. */
|
||||
TEST_ASSERT_TRUE(g_last_recording);
|
||||
TEST_ASSERT_FALSE(g_last_file_null);
|
||||
TEST_ASSERT_EQUAL_STRING(path_before, g_last_file);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, g_last_event_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(started_before, g_last_started_at);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_record_stop(&sched));
|
||||
}
|
||||
|
||||
static void test_recordstart_invalid_filename_replies_error(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
char escape_path[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_start_invalid");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
/* Contains a path separator -- rejected before any filesystem access
|
||||
* (recording.c's sched_validate_record_filename(), task 9.1). */
|
||||
rc = send_admin_request(&sched, SchedAdminAction_recordStart,
|
||||
"sub/dir.replay", 666);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&(sched.core), &sched);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* No file opened, no directory created, recording never started. */
|
||||
TEST_ASSERT_FALSE(sched.recording);
|
||||
TEST_ASSERT_NULL(sched.record_file_path);
|
||||
TEST_ASSERT_NULL(sched.record_file);
|
||||
snprintf(escape_path, sizeof(escape_path), "%s/sub", record_dir);
|
||||
TEST_ASSERT_FALSE(
|
||||
(access(escape_path, F_OK) == 0)); /* "sub" was never created */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, g_reply_calls);
|
||||
TEST_ASSERT_FALSE(g_last_ok);
|
||||
TEST_ASSERT_FALSE(g_last_error_null);
|
||||
TEST_ASSERT_EQUAL_STRING("invalid record file name", g_last_error);
|
||||
TEST_ASSERT_FALSE(g_last_recording);
|
||||
TEST_ASSERT_TRUE(g_last_file_null);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, g_last_event_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, g_last_started_at);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* recordStop -- deferred, not synchronous. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_recordstop_is_deferred_then_performed_by_driver_round(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
char path_before[PATH_MAX];
|
||||
uint64_t started_before;
|
||||
xce_core_event_t ev;
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_stop_deferred");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
rc = sched_record_start(&sched, "stopme.replay");
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
strcpy(path_before, sched.record_file_path);
|
||||
started_before = sched.record_start_ts;
|
||||
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = XCE_CORE_EVENT_JOB_STATE_CHANGE;
|
||||
ev.u.job_state_change.jobid = 9ULL;
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_record_write_events(&sched, &ev, 1));
|
||||
TEST_ASSERT_EQUAL_UINT64(1ULL, sched.record_event_count);
|
||||
|
||||
rc = send_admin_request(&sched, SchedAdminAction_recordStop, NULL, 777);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Deferred, not synchronous: still recording, no reply yet. */
|
||||
TEST_ASSERT_TRUE(sched.recording);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_reply_calls);
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&(sched.core), &sched);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
TEST_ASSERT_FALSE(sched.recording);
|
||||
TEST_ASSERT_NULL(sched.record_file_path);
|
||||
TEST_ASSERT_NULL(sched.record_file);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, g_reply_calls);
|
||||
TEST_ASSERT_EQUAL_UINT64(777, g_last_seq);
|
||||
TEST_ASSERT_TRUE(g_last_ok);
|
||||
TEST_ASSERT_TRUE(g_last_error_null);
|
||||
TEST_ASSERT_FALSE(g_last_recording);
|
||||
/* "last path (informational)". */
|
||||
TEST_ASSERT_FALSE(g_last_file_null);
|
||||
TEST_ASSERT_EQUAL_STRING(path_before, g_last_file);
|
||||
/* "final count". */
|
||||
TEST_ASSERT_EQUAL_UINT64(1ULL, g_last_event_count);
|
||||
/* "original start (historical)". */
|
||||
TEST_ASSERT_EQUAL_UINT64(started_before, g_last_started_at);
|
||||
}
|
||||
|
||||
static void test_recordstop_while_not_recording_replies_error(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_stop_notrecording");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
rc = send_admin_request(&sched, SchedAdminAction_recordStop, NULL, 888);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_reply_calls); /* deferred */
|
||||
|
||||
rc = xce_sched_driver_translate_pending_events(&(sched.core), &sched);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
TEST_ASSERT_FALSE(sched.recording);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, g_reply_calls);
|
||||
TEST_ASSERT_EQUAL_UINT64(888, g_last_seq);
|
||||
TEST_ASSERT_FALSE(g_last_ok);
|
||||
TEST_ASSERT_FALSE(g_last_error_null);
|
||||
TEST_ASSERT_EQUAL_STRING("not recording", g_last_error);
|
||||
TEST_ASSERT_FALSE(g_last_recording);
|
||||
TEST_ASSERT_TRUE(g_last_file_null);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, g_last_event_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, g_last_started_at);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Unrecognized action / NULL argument guards. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_unrecognized_action_returns_einval_no_crash_no_reply(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_bad_action");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
rc = send_admin_request(&sched, 999 /* not a valid SchedAdminAction */,
|
||||
NULL, 1010);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_reply_calls); /* no synchronous reply */
|
||||
TEST_ASSERT_NULL(
|
||||
dataq_deq(sched.core.pend_updates[0], NULL)); /* nothing enqueued */
|
||||
|
||||
/* A subsequent driver round is a genuine no-op -- nothing to drain. */
|
||||
rc = xce_sched_driver_translate_pending_events(&(sched.core), &sched);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_reply_calls);
|
||||
}
|
||||
|
||||
static void test_null_sched_returns_einval(void)
|
||||
{
|
||||
sched_admin_req_t areq;
|
||||
xce_request_t request;
|
||||
|
||||
memset(&areq, 0, sizeof(areq));
|
||||
areq.action = SchedAdminAction_recordStatus;
|
||||
|
||||
memset(&request, 0, sizeof(request));
|
||||
request.opcode = OPCode_schedAdmin;
|
||||
request.body_which = Request_body_schedadminmsg;
|
||||
request.body.schedadminmsg = &areq;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_handle_admin_request(NULL, &request));
|
||||
TEST_ASSERT_EQUAL_INT(0, g_reply_calls);
|
||||
}
|
||||
|
||||
static void test_null_request_returns_einval(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_null_req");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_handle_admin_request(&sched, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(0, g_reply_calls);
|
||||
}
|
||||
|
||||
static void test_null_schedadminmsg_body_returns_einval(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
xce_request_t request;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_null_body");
|
||||
fixture_init(&sched, record_dir);
|
||||
|
||||
memset(&request, 0, sizeof(request));
|
||||
request.opcode = OPCode_schedAdmin;
|
||||
request.body_which = Request_body_schedadminmsg;
|
||||
request.body.schedadminmsg = NULL;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL,
|
||||
sched_handle_admin_request(&sched, &request));
|
||||
TEST_ASSERT_EQUAL_INT(0, g_reply_calls);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
|
||||
RUN_TEST(test_recordstatus_not_recording_returns_empty_state);
|
||||
RUN_TEST(test_recordstatus_while_recording_reflects_live_state);
|
||||
RUN_TEST(test_recordstatus_genuinely_blocks_on_record_lock);
|
||||
RUN_TEST(test_recordstatus_file_survives_concurrent_record_stop_free);
|
||||
|
||||
RUN_TEST(test_recordstart_is_deferred_then_performed_by_driver_round);
|
||||
RUN_TEST(test_recordstart_empty_file_auto_generates_name);
|
||||
RUN_TEST(test_recordstart_while_already_recording_replies_error);
|
||||
RUN_TEST(test_recordstart_invalid_filename_replies_error);
|
||||
|
||||
RUN_TEST(test_recordstop_is_deferred_then_performed_by_driver_round);
|
||||
RUN_TEST(test_recordstop_while_not_recording_replies_error);
|
||||
|
||||
RUN_TEST(test_unrecognized_action_returns_einval_no_crash_no_reply);
|
||||
RUN_TEST(test_null_sched_returns_einval);
|
||||
RUN_TEST(test_null_request_returns_einval);
|
||||
RUN_TEST(test_null_schedadminmsg_body_returns_einval);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
@ -134,6 +134,24 @@ int sched_record_write_events(xce_scheduler_t* sched, xce_core_event_t* events,
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* sched_record_start/sched_record_stop (scheduler-core spec, task 9.4) --
|
||||
* driver.c's handle_deferred_admin_record() (new this task) calls both;
|
||||
* this file never enqueues an XCE_SCHED_OP_ADMIN_RECORD request, so
|
||||
* neither is actually invoked at runtime, same rationale as
|
||||
* sched_record_write_events above. */
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -229,6 +229,24 @@ int sched_record_write_events(xce_scheduler_t* sched, xce_core_event_t* events,
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* sched_record_start/sched_record_stop (scheduler-core spec, task 9.4) --
|
||||
* driver.c's handle_deferred_admin_record() (new this task) calls both;
|
||||
* this file never enqueues an XCE_SCHED_OP_ADMIN_RECORD request, so
|
||||
* neither is actually invoked at runtime, same rationale as
|
||||
* sched_record_write_events above. */
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -112,6 +112,24 @@ int sched_record_write_events(xce_scheduler_t* sched, xce_core_event_t* events,
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* sched_record_start/sched_record_stop (scheduler-core spec, task 9.4) --
|
||||
* driver.c's handle_deferred_admin_record() (new this task) calls both;
|
||||
* this file never enqueues an XCE_SCHED_OP_ADMIN_RECORD request, so
|
||||
* neither is actually invoked at runtime, same rationale as
|
||||
* sched_record_write_events above. */
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,14 +27,30 @@
|
|||
* test_job_array_expand.c for the real kvmgr-backed sched_get_job). */
|
||||
int sched_init_config(xce_scheduler_t* s, xce_conf4sched_t* c)
|
||||
{
|
||||
(void)s; (void)c; return 0;
|
||||
(void)s;
|
||||
(void)c;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* sched_get_job relocated to core.h (scheduler-core spec, task 4.1) -- now
|
||||
* operates on xce_sched_core_t*, not xce_scheduler_t*. */
|
||||
xce_sched_job_t* sched_get_job(xce_sched_core_t* sched, uint64_t jobid)
|
||||
{
|
||||
(void)sched; (void)jobid; return NULL;
|
||||
(void)sched;
|
||||
(void)jobid;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* sched_record_status (recording.c, scheduler-core spec, task 9.4) --
|
||||
* handler.c's OPCode_schedAdmin/recordStatus path references it; this file
|
||||
* never exercises OPCode_schedAdmin (its own scope is notify/dataset
|
||||
* enqueue), so a link-satisfying stub is enough, same rationale as
|
||||
* sched_init_config above. */
|
||||
int sched_record_status(xce_scheduler_t* sched, sched_admin_reply_t* reply)
|
||||
{
|
||||
(void)sched;
|
||||
(void)reply;
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
#include "../src/handler.c"
|
||||
|
|
@ -118,8 +134,7 @@ static void test_handle_job_rusage_enqueues_rusage(void)
|
|||
xce_sched_op_t op = (xce_sched_op_t)-1;
|
||||
fixture_init(&sched);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL,
|
||||
sched_handle_sched_job_rusage(&sched, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_handle_sched_job_rusage(&sched, NULL));
|
||||
|
||||
xce_job_rusages_t* ru = (xce_job_rusages_t*)calloc(1, sizeof(*ru));
|
||||
ru->jobid = 7;
|
||||
|
|
@ -132,20 +147,18 @@ static void test_handle_job_rusage_enqueues_rusage(void)
|
|||
static void test_handle_update_resvals_skips_empty_enqueues_nonempty(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_op_t op = (xce_sched_op_t)-1;
|
||||
xce_sched_op_t op = (xce_sched_op_t)-1;
|
||||
xce_resval_list_t empty = { .count = 0, .vals = NULL };
|
||||
xce_resval_list_t full = { .count = 1, .vals = NULL };
|
||||
fixture_init(&sched);
|
||||
|
||||
/* NULL or count==0 -> skip (return 0, nothing enqueued) */
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_handle_sched_update_resvals(&sched, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
sched_handle_sched_update_resvals(&sched, &empty));
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_handle_sched_update_resvals(&sched, &empty));
|
||||
TEST_ASSERT_EQUAL_INT(0, pop_op(&sched, NULL));
|
||||
|
||||
/* count>0 -> enqueue UPDATE_RESVALS */
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
sched_handle_sched_update_resvals(&sched, &full));
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_handle_sched_update_resvals(&sched, &full));
|
||||
TEST_ASSERT_EQUAL_INT(1, pop_op(&sched, &op));
|
||||
TEST_ASSERT_EQUAL_INT(XCE_SCHED_OP_UPDATE_RESVALS, op);
|
||||
}
|
||||
|
|
@ -154,9 +167,9 @@ static void test_handle_update_resvals_skips_empty_enqueues_nonempty(void)
|
|||
|
||||
static void test_handle_resources_skips_empty_enqueues_nonempty(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_op_t op = (xce_sched_op_t)-1;
|
||||
xce_resource_t* one[1];
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_op_t op = (xce_sched_op_t)-1;
|
||||
xce_resource_t* one[1];
|
||||
fixture_init(&sched);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, sched_handle_resources(&sched, 0, NULL));
|
||||
|
|
@ -188,7 +201,8 @@ static void test_handle_resvals_full_skips_empty_enqueues_nonempty(void)
|
|||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
(void)argc; (void)argv;
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_handle_update_job_enqueues_job_fields, 0);
|
||||
RUN_TEST(test_handle_update_queue_enqueues_queue, 0);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,20 @@
|
|||
|
||||
int sched_init_config(xce_scheduler_t* s, xce_conf4sched_t* c)
|
||||
{
|
||||
(void)s; (void)c; return 0;
|
||||
(void)s;
|
||||
(void)c;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* sched_record_status (recording.c, scheduler-core spec, task 9.4) --
|
||||
* handler.c's OPCode_schedAdmin/recordStatus path references it; this file
|
||||
* never exercises OPCode_schedAdmin, so a link-satisfying stub is enough,
|
||||
* same rationale as sched_init_config above. */
|
||||
int sched_record_status(xce_scheduler_t* sched, sched_admin_reply_t* reply)
|
||||
{
|
||||
(void)sched;
|
||||
(void)reply;
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* REAL kvmgr-backed job add/get -- same rtti path production uses (mirrors
|
||||
|
|
@ -84,7 +97,7 @@ static void fixture_init(xce_scheduler_t* sched)
|
|||
static void seed_existing_job(xce_scheduler_t* sched, uint64_t jobid)
|
||||
{
|
||||
xce_sched_job_t* job = (xce_sched_job_t*)calloc(1, sizeof(*job));
|
||||
job->jobid = jobid;
|
||||
job->jobid = jobid;
|
||||
sched_add_job(&(sched->core), job);
|
||||
}
|
||||
|
||||
|
|
@ -99,9 +112,8 @@ static void seed_existing_job(xce_scheduler_t* sched, uint64_t jobid)
|
|||
static xce_job_submit_t* init_job_array(uint64_t base_jobid,
|
||||
uint64_t array_size)
|
||||
{
|
||||
job_submit_data_t* submit
|
||||
= (job_submit_data_t*)calloc(1, sizeof(*submit));
|
||||
xce_job_submit_t* ja = (xce_job_submit_t*)calloc(1, sizeof(*ja));
|
||||
job_submit_data_t* submit = (job_submit_data_t*)calloc(1, sizeof(*submit));
|
||||
xce_job_submit_t* ja = (xce_job_submit_t*)calloc(1, sizeof(*ja));
|
||||
|
||||
submit->queue = strdup("normal");
|
||||
submit->user = strdup("tester");
|
||||
|
|
@ -145,12 +157,12 @@ static int pop_job_update(xce_scheduler_t* sched, xce_job_t*** jobs_out)
|
|||
|
||||
static void test_job_array_push_expands_into_n_tasks(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_job_submit_t* ja;
|
||||
dataset_t dataset;
|
||||
xce_job_t** jobs = NULL;
|
||||
int njobs;
|
||||
int i;
|
||||
xce_scheduler_t sched;
|
||||
xce_job_submit_t* ja;
|
||||
dataset_t dataset;
|
||||
xce_job_t** jobs = NULL;
|
||||
int njobs;
|
||||
int i;
|
||||
|
||||
fixture_init(&sched);
|
||||
ja = init_job_array(5000, 3);
|
||||
|
|
@ -188,11 +200,11 @@ static void test_job_array_push_expands_into_n_tasks(void)
|
|||
|
||||
static void test_job_array_redelivery_does_not_duplicate(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
dataset_t dataset;
|
||||
xce_job_t** jobs = NULL;
|
||||
int njobs;
|
||||
int i;
|
||||
xce_scheduler_t sched;
|
||||
dataset_t dataset;
|
||||
xce_job_t** jobs = NULL;
|
||||
int njobs;
|
||||
int i;
|
||||
|
||||
fixture_init(&sched);
|
||||
|
||||
|
|
@ -289,9 +301,9 @@ static void test_wire_dispatch_chain_delivers_n_tasks(void)
|
|||
xce_data_t data;
|
||||
xce_request_t request;
|
||||
xce_request_t decoded;
|
||||
io_packet_t *pkt;
|
||||
io_packet_t* pkt;
|
||||
dataset_t dataset;
|
||||
xce_job_t **jobs = NULL;
|
||||
xce_job_t** jobs = NULL;
|
||||
int njobs;
|
||||
int i;
|
||||
|
||||
|
|
@ -319,12 +331,13 @@ static void test_wire_dispatch_chain_delivers_n_tasks(void)
|
|||
request.body_which = Request_body_pushmsg;
|
||||
request.body.pushmsg = &data;
|
||||
|
||||
pkt = (io_packet_t *)calloc(1, sizeof(*pkt));
|
||||
pkt = (io_packet_t*)calloc(1, sizeof(*pkt));
|
||||
TEST_ASSERT_NOT_NULL(pkt);
|
||||
TEST_ASSERT_EQUAL_INT(1, xce_encode(&request, pkt));
|
||||
|
||||
memset(&decoded, 0, sizeof(decoded));
|
||||
TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, &decoded)); /* consumes/frees pkt */
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
xce_decode(pkt, &decoded)); /* consumes/frees pkt */
|
||||
|
||||
/* Act 1: the REAL transport dispatcher (task 5.3b's fix) -- exactly what
|
||||
* vtask_entry() (xcectx.c) calls on every inbound push. */
|
||||
|
|
@ -364,7 +377,8 @@ static void test_wire_dispatch_chain_delivers_n_tasks(void)
|
|||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
(void)argc; (void)argv;
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_job_array_push_expands_into_n_tasks, 0);
|
||||
RUN_TEST(test_job_array_redelivery_does_not_duplicate, 0);
|
||||
|
|
|
|||
|
|
@ -79,6 +79,17 @@ int sched_init_config(xce_scheduler_t* s, xce_conf4sched_t* c)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* sched_record_status (recording.c, scheduler-core spec, task 9.4) --
|
||||
* handler.c's OPCode_schedAdmin/recordStatus path references it; this file
|
||||
* never exercises OPCode_schedAdmin, so a link-satisfying stub is enough,
|
||||
* same rationale as sched_init_config above. */
|
||||
int sched_record_status(xce_scheduler_t* sched, sched_admin_reply_t* reply)
|
||||
{
|
||||
(void)sched;
|
||||
(void)reply;
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* REAL kvmgr-backed job add/get -- same rtti path production uses (mirrors
|
||||
* test_job_array_expand.c's precedent), needed so sched_on_job_array()'s
|
||||
* per-element dedup check (sched_get_job) is exercised for real. Relocated
|
||||
|
|
|
|||
|
|
@ -342,6 +342,15 @@ static void fixture_init(xce_scheduler_t* sched, char* record_dir)
|
|||
|
||||
memset(sched, 0, sizeof(*sched));
|
||||
|
||||
/* record_lock (scheduler.h field comments, task 9.4 fix round) is
|
||||
* normally initialized by sched_init(), which this white-box fixture
|
||||
* deliberately bypasses (same rationale as this function's own header
|
||||
* comment) -- initialize it here too, same pairing as the
|
||||
* xce_sched_core_init() call just below initializing core.lock. Every
|
||||
* sched_record_start()/sched_record_stop() call in this file now takes
|
||||
* this lock. */
|
||||
mylock_init(&(sched->record_lock));
|
||||
|
||||
rc = xce_sched_core_init(&(sched->core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue