/* * test_sim_alt_events.c - contract test for what-if event-source branching * (scheduler-core spec, task 11.5, Req 5 AC 16). * * Observable under test (task 11.5's own text, task text's Observable (a)): * - running with an alternate-events file produces decisions driven by * the alternate events, not the base file's original events. * * Two tests: * - a baseline run against the base .replay file ALONE (no * --sim-alt-events) confirms its own event stream drives a PEND * decision for the BASE job, and NOT the alt job. * - a what-if run against the SAME base file's snapshot, but with * --sim-alt-events pointed at a separate events-only stream, confirms * the opposite: a PEND decision for the ALT job, and NOT the base job. * Together these prove the switch is real (not merely "some decision * appears"): the DRIVING SOURCE genuinely changed. * * Fixture strategy: the SAME real QUEUE_UPDATE+JOBS_PUSH-driving-the-real- * builtin_fcfs-picker technique test_sim_verify.c's own integration-level * fixture helpers establish (build_fixture_scheduler() records * schedPolicy="builtin_fcfs" so sim_register_default_picker() resolves the * real plugin) -- since core.c's own sched_job_is_ready()/ * sched_queue_is_ready() are unconditional `return 1` stubs (task 2.4's own * documented, still-live scope note), any queue/job pair immediately * becomes a next_job() candidate, so each run deterministically produces * exactly one live PEND decision via make_pendreason() for whichever jobid * that run's own event stream pushed. Decisions are observed by * redirecting STDOUT_FILENO around sched_sim_driver_run(), the same * dup()/dup2() capture technique test_sim_verify.c's own * run_sim_and_capture_stdout() establishes (sim_print_decision()'s * "[sim] PEND jobid=%llu" line, sim_loop.c). * * The alt-events file itself is a BARE sequence of ReplayEvent/ReplaySession * messages (no ReplayHeader, no SnapshotEntry) -- Req 5 AC 16's own text: * "the driver loads the snapshot from the base file, then replays events * from the alt-events file (same .replay capnp stream format)". Once * --sim-alt-events is set, sched_sim_driver_run()'s main loop reads * EXCLUSIVELY from it from message 1 onward -- the same shape-based * message dispatch (msg_root_has_shape(), never position-based beyond the * base file's own first two messages) that already governs every message * after a SnapshotEntry in a full `.replay` stream, so an alt-events file * that is simply "the tail of a .replay stream" needs no header/snapshot * prefix of its own. * * White-box #include of the same TU set test_sim_verify.c uses (see that * file's own header comment) -- core.c/core_clock.c/core_events.c/ * core_session.c/decision.c/plugin.c/plugins/builtin_fcfs.c/recording.c/ * recording_session.c/repl.c/sim.c/sim_loop.c/sim_record.c/sim_verify.c/ * snapshot.c/snapshot_load.c/snapshot_wire.c. sim_verify.c AND * recording_session.c/sim_record.c/repl.c are all required at link time * (sched_sim_driver_run() externs sim_decision_eq()/sim_log_mismatch() and * sched_record_session_ctx_new()/_finalize()/sim_record_write_header_and_ * snapshot()/sim_record_write_event() unconditionally, and repl.h's task * 12.1 inline wrappers -- pulled in transitively by sim_loop.c's own * `#include "repl.h"` -- reference xce_repl_start()/ * xce_repl_post_session_check() unconditionally too) even though this file * never sets --sim-verify/--sim-output/--interactive, same reason * test_sim_event_loop.c's own header comment documents. */ #include #include #include #include #include #include #include #include #include #include #include "controller.capnp.h" #include "kvmgr.h" #include "scheduler.h" #include "xstring.h" /* Pull in the real functions under test into this TU (white-box #include, * same convention as test_sim_verify.c). */ #include "../src/core.c" #include "../src/core_clock.c" #include "../src/core_events.c" #include "../src/core_session.c" #include "../src/decision.c" #include "../src/plugin.c" #include "../src/plugins/builtin_fcfs.c" #include "../src/recording.c" #include "../src/recording_session.c" #include "../src/repl.c" #include "../src/repl_break.c" #include "../src/repl_info.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_record.c" #include "../src/sim_verify.c" #include "../src/snapshot.c" #include "../src/snapshot_load.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 sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; } int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec, xlist_t* limits) { (void)sched; (void)vec; (void)limits; return 0; } /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * copy test_sim_verify.c/test_sim_event_loop.c/test_sim_startup.c each * carry (schedobj.c is not linked here: it would need plugin_register(), * which requires a genuine xce_scheduler_t* neither this file's fixtures * nor sim_loop.c's own core-scoped registration path have, Req 5 AC 17). */ 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 helpers -- same techniques test_sim_verify.c already */ /* establishes for its own integration-level tests. */ /* ---------------------------------------------------------------------- */ static void make_tmp_dir(char* out, size_t out_len, const char* suffix) { char tmpl[PATH_MAX]; snprintf(tmpl, sizeof(tmpl), "/tmp/sched_altevents%s_XXXXXX", suffix); TEST_ASSERT_NOT_NULL(mkdtemp(tmpl)); TEST_ASSERT_TRUE(strlen(tmpl) < out_len); strcpy(out, tmpl); } static void build_fixture_scheduler(xce_scheduler_t* sched, char* record_dir) { int rc; memset(sched, 0, sizeof(*sched)); mylock_init(&(sched->record_lock)); rc = xce_sched_core_init(&(sched->core)); TEST_ASSERT_EQUAL_INT(0, rc); sched->core.job_pick_plugin = (char*)"builtin_fcfs"; sched->record_dir = record_dir; } static FILE* write_header_and_snapshot(char* out_path, size_t out_path_len, const char* suffix) { char record_dir[PATH_MAX]; xce_scheduler_t sched; int rc; make_tmp_dir(record_dir, sizeof(record_dir), suffix); build_fixture_scheduler(&sched, record_dir); rc = sched_record_start(&sched, "fixture.replay"); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_path_len); strcpy(out_path, sched.record_file_path); return sched.record_file; } static void append_queue_update_event(FILE* f, uint64_t ts, const char* queue_name) { xce_queue_t queue; replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&queue, 0, sizeof(queue)); queue.queue = (char*)queue_name; queue.status = QUEUE_STAT_OPEN; queue.priority = 1; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_queueUpdate; src.body.queue_update = &queue; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void append_jobs_push_event(FILE* f, uint64_t ts, uint64_t jobid, const char* queue_name) { job_submit_data_t submit; xce_job_t job; xce_job_t* jobs[1]; xce_jobs_data_t jobs_data; replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&submit, 0, sizeof(submit)); submit.queue = (char*)queue_name; submit.user = (char*)"alice"; submit.mintasks = 1; submit.maxtasks = 1; memset(&job, 0, sizeof(job)); job.jobid = jobid; job.state = JOB_STAT_PEND; job.submit = &submit; jobs[0] = &job; memset(&jobs_data, 0, sizeof(jobs_data)); jobs_data.n_total = 1; jobs_data.count = 1; jobs_data.jobs = jobs; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_jobsPush; src.body.jobs_push = &jobs_data; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void append_replay_session(FILE* f, uint64_t begin_round) { struct capn c; struct capn_segment* cs; ReplaySession_ptr sp; capn_init_malloc(&c); cs = capn_root(&c).seg; sp = new_ReplaySession(cs); ReplaySession_set_beginRound(sp, begin_round); capn_setp(capn_root(&c), 0, sp.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void append_end_of_stream_marker(FILE* f) { replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&src, 0, sizeof(src)); src.body_which = ReplayEvent_body_endSession; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } /* Runs sched_sim_driver_run() with optional --sim-alt-events, capturing * everything written to stdout during the call -- same dup()/dup2() * technique test_sim_verify.c's own run_sim_and_capture_stdout() * establishes, extended with an `alt_path` parameter (NULL to omit * --sim-alt-events entirely). */ static char* run_sim_and_capture_stdout(const char* base_path, const char* alt_path, int* out_rc) { sched_sim_args_t args; xce_sched_sim_driver_t sim; char tmp[] = "/tmp/xce_altevents_run_XXXXXX"; int fd = mkstemp(tmp); int saved_stdout; off_t len; char* buf; TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "mkstemp for stdout capture must " "succeed"); fflush(stdout); saved_stdout = dup(STDOUT_FILENO); TEST_ASSERT_TRUE_MESSAGE(saved_stdout >= 0, "dup(STDOUT_FILENO) must " "succeed"); TEST_ASSERT_TRUE_MESSAGE(dup2(fd, STDOUT_FILENO) >= 0, "dup2 redirect of STDOUT_FILENO must succeed"); memset(&args, 0, sizeof(args)); args.replay_path = base_path; args.sim_start_ts = SCHED_SIM_START_TS_LATEST; args.sim_speed = 0; /* frozen -- deterministic, no wall-clock wait */ args.alt_events_path = alt_path; *out_rc = sched_sim_driver_run(&sim, &args); fflush(stdout); dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); len = lseek(fd, 0, SEEK_CUR); TEST_ASSERT_TRUE(len >= 0); buf = (char*)calloc(1, (size_t)len + 1); TEST_ASSERT_NOT_NULL(buf); TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0); if (len > 0) { ssize_t nread = read(fd, buf, (size_t)len); TEST_ASSERT_TRUE(nread == len); } close(fd); unlink(tmp); return buf; } #define BASE_JOBID 777ULL #define ALT_JOBID 888ULL /* ---------------------------------------------------------------------- */ /* Test 1: baseline -- no --sim-alt-events, the base file's OWN events */ /* drive scheduling. */ /* ---------------------------------------------------------------------- */ static void test_baseline_run_uses_base_file_events(void) { char base_jobid_marker[32]; char alt_jobid_marker[32]; 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); f = write_header_and_snapshot(path, sizeof(path), "_baseline"); TEST_ASSERT_NOT_NULL(f); append_queue_update_event(f, 1000ULL, "normal"); append_jobs_push_event(f, 1500ULL, BASE_JOBID, "normal"); append_replay_session(f, 2000ULL); append_end_of_stream_marker(f); fclose(f); captured = run_sim_and_capture_stdout(path, NULL, &rc); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, base_jobid_marker), "baseline run (no --sim-alt-events) must produce a decision for " "the base file's own jobid"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, alt_jobid_marker), "baseline run must never produce a decision for a jobid it never " "saw"); free(captured); printf("test_baseline_run_uses_base_file_events: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Test 2: what-if -- --sim-alt-events set, the ALTERNATE file's events */ /* drive scheduling instead (Req 5 AC 16's own Observable). */ /* ---------------------------------------------------------------------- */ 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_path[PATH_MAX]; char alt_path[PATH_MAX]; FILE* base_f; FILE* alt_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); /* Base file: header + empty snapshot, then its OWN (never-to-be-read, * once --sim-alt-events is set) events for BASE_JOBID -- present to * prove the switch is real: if alt-events were silently ignored, this * run would fall back to seeing BASE_JOBID here too. */ base_f = write_header_and_snapshot(base_path, sizeof(base_path), "_whatif_base"); TEST_ASSERT_NOT_NULL(base_f); append_queue_update_event(base_f, 1000ULL, "normal"); append_jobs_push_event(base_f, 1500ULL, BASE_JOBID, "normal"); append_replay_session(base_f, 2000ULL); append_end_of_stream_marker(base_f); fclose(base_f); /* Alt-events file: a BARE ReplayEvent/ReplaySession stream (no * ReplayHeader, no SnapshotEntry -- this file's own header comment) for * a DIFFERENT job, ALT_JOBID, on a queue the alt stream creates for * itself (the loaded snapshot has zero queues). */ TEST_ASSERT_TRUE(snprintf(alt_path, sizeof(alt_path), "%s.alt", base_path) < (int)sizeof(alt_path)); alt_f = fopen(alt_path, "w"); TEST_ASSERT_NOT_NULL(alt_f); append_queue_update_event(alt_f, 3000ULL, "altqueue"); append_jobs_push_event(alt_f, 3500ULL, ALT_JOBID, "altqueue"); append_replay_session(alt_f, 4000ULL); append_end_of_stream_marker(alt_f); fclose(alt_f); captured = run_sim_and_capture_stdout(base_path, alt_path, &rc); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, alt_jobid_marker), "--sim-alt-events run must produce a decision for the ALTERNATE " "file's own jobid"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, base_jobid_marker), "--sim-alt-events run must NOT produce a decision for the base " "file's original jobid -- the base file's own events must never " "be read once --sim-alt-events is set"); free(captured); printf("test_alt_events_run_uses_alt_file_events_not_base: PASS\n"); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_baseline_run_uses_base_file_events); RUN_TEST(test_alt_events_run_uses_alt_file_events_not_base); return UNITY_END(); }