/* * test_sim_startup.c - contract test for simulation-mode startup * (scheduler-core spec, task 11.1, Req 5 AC 13). * * Observable under test (task 11.1's own text): * - starting simulation against a valid fixture file reaches a * ready-to-run state with the virtual clock reporting the snapshot's * timestamp. * - starting against a file missing its initial snapshot entry fails with * a clear error before any event is processed. * * Fixture strategy: rather than hand-constructing capnp bytes, this file * builds REAL, valid `.replay` fixtures via the SAME production code path * normal-mode recording already uses (sched_record_start()/ * sched_record_stop(), task 9.2) against a small real xce_scheduler_t * fixture -- same technique test_record_lifecycle.c already establishes. * The "missing initial snapshot entry" fixture is then derived from a valid * one by truncating it to exactly its first message (the ReplayHeader), * discovered by decoding that one message and reading the resulting file * offset -- no hand-rolled capnp byte construction needed for that case * either. The "first message is not a valid ReplayHeader" and "first data * message is not a SnapshotEntry" fixtures DO hand-construct one wrong- * shaped message directly (same technique test_e2e_capnp.c/ * test_record_lifecycle.c already use for building capnp messages by hand), * since there is no production code path that ever writes a malformed * stream to reuse. * * White-box #include of ../src/core.c, ../src/core_clock.c (task 11.1's own * code under test, sim.c, needs both -- xce_sched_core_init/fini and * xce_core_now()) PLUS ../src/snapshot.c, ../src/snapshot_wire.c, * ../src/snapshot_load.c (task 8.1's real save/load, needed both to BUILD * the fixture via sched_record_start() and to exercise sim.c's own * xce_sched_core_load_snapshot() call) PLUS ../src/recording.c (real * sched_record_start()/sched_record_stop(), used ONLY to build fixtures -- * this file never asserts anything about recording.c's own behavior, that * is test_record_lifecycle.c's job) PLUS ../src/sim.c (the code under * test). No driver.c/plugin.c/core_events.c/core_session.c/decision.c: this * file never drives a real scheduling session (task 11.1 stops at * "ready-to-run", not the event loop), so none of those are needed. * * Stubs below mirror the exact same link-satisfying set * test_record_lifecycle.c/test_driver_session_ordering.c already establish * for this core.c/core_clock.c/snapshot.c/snapshot_wire.c/snapshot_load.c/ * recording.c combination (kill_sched_*, sched_resolve_job_picker's real * reimplementation -- schedobj.c is not linked). */ #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_record_lifecycle.c). */ #include "../src/core.c" #include "../src/core_clock.c" #include "../src/recording.c" #include "../src/sim.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; } /* plugin.c is not linked into this TU (no session is ever driven, so no * real plugin unregistration ever happens) -- same stub every other * core.c-including test file in this directory establishes for * xce_sched_core_init()'s kvmgr datakiller. */ void plugin_unregister(void* arg) { (void)arg; } /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * rationale/algorithm as test_record_lifecycle.c's own copy (schedobj.c is * not linked). Both the FIXTURE core (built via a real xce_scheduler_t, * needs a resolvable picker for sched_record_start()'s own snapshot to be * well-formed) and sim.c's own core (which never registers a picker, per * sim.h's documented scope boundary -- this will legitimately return * -EINVAL/-ENOENT for that core, which xce_sched_core_load_snapshot() * gracefully WARNs and skips) share this one implementation. */ 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 */ /* ---------------------------------------------------------------------- */ static void make_tmp_dir(char* out, size_t out_len, const char* suffix) { char tmpl[PATH_MAX]; snprintf(tmpl, sizeof(tmpl), "/tmp/sched_simstart%s_XXXXXX", suffix); TEST_ASSERT_NOT_NULL(mkdtemp(tmpl)); TEST_ASSERT_TRUE(strlen(tmpl) < out_len); strcpy(out, tmpl); } static int stub_picker_begin_session(void* picker) { (void)picker; return 0; } static xce_sched_job_t* stub_picker_next_job(void* picker) { (void)picker; return NULL; } static int stub_picker_end_session(void* picker) { (void)picker; return 0; } static int stub_picker_jobid_noop(void* picker, uint64_t jobid) { (void)picker; (void)jobid; return 0; } static int stub_picker_queue_update_noop(void* picker, xce_sched_queue_t* q) { (void)picker; (void)q; return 0; } static int stub_picker_job_noop(void* picker, xce_sched_job_t* job) { (void)picker; (void)job; return 0; } static int stub_picker_job_state_change_noop(void* picker, xce_sched_job_t* job, uint32_t state) { (void)picker; (void)job; (void)state; return 0; } static xce_job_picker_iface_t g_stub_picker_iface = { .init = NULL, .fini = NULL, .begin_session = stub_picker_begin_session, .next_job = stub_picker_next_job, .end_session = stub_picker_end_session, .on_dispatch = stub_picker_jobid_noop, .on_pend = stub_picker_jobid_noop, .on_queue_update = stub_picker_queue_update_noop, .on_job_push = stub_picker_job_noop, .on_job_state_change = stub_picker_job_state_change_noop, .on_job_update = stub_picker_job_noop, .on_config = NULL, .on_job_rusage = NULL }; static xce_sched_plugin_t g_stub_picker_plugin = { .name = "test_sim_startup_picker", .picker_iface = &g_stub_picker_iface }; /* Builds a small, real xce_scheduler_t fixture (same pattern * test_record_lifecycle.c's fixture_init() establishes) purely to drive * sched_record_start()/sched_record_stop() and produce a REAL, valid * `.replay` file this test's OWN code under test (sim.c) then consumes. */ 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); lock(sched->core.plugins, RTTI_LOCK_WRITE); rc = key_add(sched->core.plugins, (void*)g_stub_picker_plugin.name, (void*)&g_stub_picker_plugin); TEST_ASSERT_GREATER_OR_EQUAL(0, rc); unlock(sched->core.plugins); sched->core.job_pick_plugin = g_stub_picker_plugin.name; sched->record_dir = record_dir; } /* Produces a valid `.replay` fixture (ReplayHeader + initial SnapshotEntry) * at `out_path` via the real sched_record_start()/sched_record_stop(); the * chosen snapshot timestamp is returned via `out_ts`. */ static void write_valid_fixture(char* out_path, size_t out_path_len, uint64_t* out_ts, 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); *out_ts = sched.record_start_ts; rc = sched_record_stop(&sched); TEST_ASSERT_EQUAL_INT(0, rc); } /* Truncates `src_path` down to just its first capnp message (the * ReplayHeader) and writes the result to `dst_path` -- simulates a replay * file whose stream ends immediately after the header, i.e. "missing * initial snapshot entry". */ static void truncate_to_header_only(const char* src_path, const char* dst_path) { FILE* src; FILE* dst; struct capn c; long header_end; char* buf; size_t n; src = fopen(src_path, "r"); TEST_ASSERT_NOT_NULL(src); TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, src, 0)); capn_free(&c); header_end = ftell(src); TEST_ASSERT_TRUE(header_end > 0); rewind(src); buf = (char*)malloc((size_t)header_end); TEST_ASSERT_NOT_NULL(buf); n = fread(buf, 1, (size_t)header_end, src); TEST_ASSERT_EQUAL_UINT((size_t)header_end, n); fclose(src); dst = fopen(dst_path, "w"); TEST_ASSERT_NOT_NULL(dst); TEST_ASSERT_EQUAL_UINT((size_t)header_end, fwrite(buf, 1, (size_t)header_end, dst)); fclose(dst); free(buf); } /* Hand-builds a single capnp message shaped like `SnapshotEntry` * (word_count=1, pointer_count=1) but with all-default/empty field * contents, and writes it standalone to `path` -- used to simulate a * malformed "first message is not ReplayHeader" fixture (ReplayHeader is * shaped (1,5); a lone (1,1)-shaped message can never be mistaken for one * by sim.c's shape check). */ static void write_lone_snapshotentry_message(const char* path) { struct capn c; struct capn_segment* cs; SnapshotData_ptr sdata; SnapshotEntry_ptr snap; FILE* f; int rc; capn_init_malloc(&c); cs = capn_root(&c).seg; sdata = new_SnapshotData(cs); snap = new_SnapshotEntry(cs); SnapshotEntry_set_ts(snap, 42ULL); SnapshotEntry_set_data(snap, sdata); capn_setp(capn_root(&c), 0, snap.p); f = fopen(path, "w"); TEST_ASSERT_NOT_NULL(f); rc = write_capnp_message(f, &c); TEST_ASSERT_EQUAL_INT(0, rc); fclose(f); capn_free(&c); } /* ---------------------------------------------------------------------- */ /* Test 1: starting simulation against a valid fixture file reaches a */ /* ready-to-run state with the virtual clock reporting the snapshot's */ /* timestamp -- this task's own primary Observable. */ /* ---------------------------------------------------------------------- */ static void test_valid_fixture_reaches_ready_state_with_frozen_virtual_clock(void) { char path[PATH_MAX]; uint64_t snapshot_ts = 0; sched_sim_args_t args; xce_sched_sim_driver_t sim; int rc; write_valid_fixture(path, sizeof(path), &snapshot_ts, "_valid"); TEST_ASSERT_TRUE(snapshot_ts > 0); memset(&args, 0, sizeof(args)); args.replay_path = path; args.sim_start_ts = SCHED_SIM_START_TS_LATEST; args.sim_speed = 0; /* fast/frozen -- xce_core_now() stays pinned to * virtual_clock_base regardless of real elapsed * time, giving this test a deterministic, * non-flaky assertion. */ rc = sched_sim_driver_start(&sim, &args); TEST_ASSERT_EQUAL_INT(0, rc); /* Step 4's Observable: the virtual clock reports the snapshot's own * timestamp, exactly (frozen -- speed_num=0). */ TEST_ASSERT_EQUAL_UINT64(snapshot_ts, xce_core_now(&(sim.core))); /* Direct inspection of the anchors Step 4 documents setting. */ TEST_ASSERT_EQUAL_UINT64( snapshot_ts, atomic_load_explicit(&(sim.core.virtual_clock_base), memory_order_acquire)); TEST_ASSERT_EQUAL_UINT64(0ULL, atomic_load_explicit(&(sim.core.speed_num), memory_order_acquire)); TEST_ASSERT_EQUAL_UINT64(1ULL, atomic_load_explicit(&(sim.core.speed_den), memory_order_acquire)); TEST_ASSERT_EQUAL_UINT64(0ULL, sim.sim_speed_num); TEST_ASSERT_EQUAL_UINT64(1ULL, sim.sim_speed_den); /* "Ready-to-run" -- no session is active, load_snapshot()'s own * precondition would otherwise have been violated. */ TEST_ASSERT_EQUAL_INT(0, atomic_load(&(sim.core.session_active))); TEST_ASSERT_NOT_NULL(sim.replay_file); printf("test_valid_fixture_reaches_ready_state_with_frozen_virtual_clock:" " PASS\n"); } /* ---------------------------------------------------------------------- */ /* Test 2: starting against a file missing its initial snapshot entry */ /* fails with a clear error before any event is processed -- this task's */ /* own second Observable. */ /* ---------------------------------------------------------------------- */ static void test_missing_initial_snapshot_entry_fails_cleanly(void) { char valid_path[PATH_MAX]; char header_only_path[PATH_MAX + 32]; uint64_t snapshot_ts = 0; sched_sim_args_t args; xce_sched_sim_driver_t sim; int rc; write_valid_fixture(valid_path, sizeof(valid_path), &snapshot_ts, "_hdronly_src"); snprintf(header_only_path, sizeof(header_only_path), "%s.header_only.replay", valid_path); truncate_to_header_only(valid_path, header_only_path); memset(&args, 0, sizeof(args)); args.replay_path = header_only_path; args.sim_start_ts = SCHED_SIM_START_TS_LATEST; args.sim_speed = 1; rc = sched_sim_driver_start(&sim, &args); TEST_ASSERT_TRUE(rc < 0); printf("test_missing_initial_snapshot_entry_fails_cleanly: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Test 3: a replay file whose FIRST message is not a valid ReplayHeader */ /* is rejected (Req 5 AC 13 step 1) -- distinct code path from test 2. */ /* ---------------------------------------------------------------------- */ static void test_first_message_not_replayheader_fails_cleanly(void) { char dir[PATH_MAX]; char path[PATH_MAX + 32]; sched_sim_args_t args; xce_sched_sim_driver_t sim; int rc; make_tmp_dir(dir, sizeof(dir), "_badhdr"); snprintf(path, sizeof(path), "%s/bad_header.replay", dir); write_lone_snapshotentry_message(path); memset(&args, 0, sizeof(args)); args.replay_path = path; args.sim_start_ts = SCHED_SIM_START_TS_LATEST; args.sim_speed = 1; rc = sched_sim_driver_start(&sim, &args); TEST_ASSERT_TRUE(rc < 0); printf("test_first_message_not_replayheader_fails_cleanly: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Test 4: a missing replay file fails with a clear error (Req 5 AC 13 */ /* step 1, "the file is missing, unreadable"). */ /* ---------------------------------------------------------------------- */ static void test_missing_replay_file_fails_cleanly(void) { sched_sim_args_t args; xce_sched_sim_driver_t sim; int rc; memset(&args, 0, sizeof(args)); args.replay_path = "/nonexistent/path/does_not_exist.replay"; args.sim_start_ts = SCHED_SIM_START_TS_LATEST; args.sim_speed = 1; rc = sched_sim_driver_start(&sim, &args); TEST_ASSERT_TRUE(rc < 0); printf("test_missing_replay_file_fails_cleanly: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Test 5: NULL argument guards. */ /* ---------------------------------------------------------------------- */ static void test_null_arguments_return_einval(void) { xce_sched_sim_driver_t sim; sched_sim_args_t args; memset(&args, 0, sizeof(args)); args.replay_path = "/tmp/does-not-matter.replay"; TEST_ASSERT_EQUAL_INT(-EINVAL, sched_sim_driver_start(NULL, &args)); TEST_ASSERT_EQUAL_INT(-EINVAL, sched_sim_driver_start(&sim, NULL)); memset(&args, 0, sizeof(args)); args.replay_path = NULL; TEST_ASSERT_EQUAL_INT(-EINVAL, sched_sim_driver_start(&sim, &args)); printf("test_null_arguments_return_einval: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Test 6: sched_sim_driver_run() -- design.md's literal entry point -- */ /* calls start() then fails loudly with -ENOSYS (task 11.2's event loop */ /* does not exist yet), never silently returning success. */ /* ---------------------------------------------------------------------- */ static void test_run_starts_then_fails_enosys_pending_event_loop(void) { char path[PATH_MAX]; uint64_t snapshot_ts = 0; sched_sim_args_t args; xce_sched_sim_driver_t sim; int rc; write_valid_fixture(path, sizeof(path), &snapshot_ts, "_run"); memset(&args, 0, sizeof(args)); args.replay_path = path; args.sim_start_ts = SCHED_SIM_START_TS_LATEST; args.sim_speed = 0; rc = sched_sim_driver_run(&sim, &args); TEST_ASSERT_EQUAL_INT(-ENOSYS, rc); /* Startup itself still genuinely succeeded -- state is real and * inspectable, matching sched_sim_driver_start()'s own contract. */ TEST_ASSERT_EQUAL_UINT64(snapshot_ts, xce_core_now(&(sim.core))); printf("test_run_starts_then_fails_enosys_pending_event_loop: PASS\n"); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_valid_fixture_reaches_ready_state_with_frozen_virtual_clock); RUN_TEST(test_missing_initial_snapshot_entry_fails_cleanly); RUN_TEST(test_first_message_not_replayheader_fails_cleanly); RUN_TEST(test_missing_replay_file_fails_cleanly); RUN_TEST(test_null_arguments_return_einval); RUN_TEST(test_run_starts_then_fails_enosys_pending_event_loop); return UNITY_END(); }