/* * test_sim_discrete_clock.c - tests for the scheduler-sim-discrete-clock * spec. * * Task 1.1 (Requirements 2.3, 2.4, 2.5; design.md, Components: CliCompat, * Testing Strategy: Unit (CliCompat)): sched_cli_warn_deprecated_options() * logs exactly one warning naming the quoted --sim-speed value, stating it * is deprecated, has no effect and will be removed, for ANY value (numeric, * non-numeric, negative, empty), and logs nothing when the option was not * given (NULL). * * Task 2.1 (Requirements 1.1-1.8, 4.2; design.md, Components: SimClock, * Testing Strategy: Unit (SimClock) and Integration (replay loop)): in * simulation mode the logical clock moves only at the start snapshot, at * each recorded event and at each recorded session, is set exactly (never * clamped, even backwards), and never drifts with real time. Every clock * test passes a non-zero speed through the still-present * sched_sim_args_t.sim_speed (DISCRETE_CLOCK_RED_SPEED) to prove the * simulator no longer reads it; task 3.1 removes that field and these * assignments while keeping the pinned-clock assertions. * * White-box #include of the same simulator TU set test_sim_record_output.c * uses, with its link-satisfying stubs and stand-in picker; the stand-in * allocator here additionally records core->session_start on every * cbuf_get() return (including the jobid==0 terminator) and can sleep * before deciding, so real time visibly passes inside a session. * * This binary deliberately never calls mylog_init(): without it, tlog's * early-print path writes WARN lines to stdout, which the test-local * capture_stdout_around() below captures. * * Task 2.3 (Requirements 1.4, 1.7, 1.8, 2.1-2.6; design.md, Testing * Strategy: CLI help and CLI end-to-end (a)-(e)): XCE_SCHEDULER_BIN * (compile-time, from the scheduler executable's full_path() in * testsuite/meson.build) runs as a child process on a fixture this file * writes. Without --sim-speed the default command line produces exact * session starts, zero durations and decision timestamps equal to the * session start, with no warning; with any value (valid or not) the output * is unchanged except recordingStartWall and the log holds exactly one * warning naming the (last) value; a trailing --sim-speed is still an argp * error; the REPL exit path keeps the warning; --help calls the option * deprecated. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "cli_compat.h" #include "controller.capnp.h" #include "kvmgr.h" #include "scheduler.h" #include "xstring.h" /* White-box #include of the simulator under test (same set and order as * test_sim_record_output.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_fmt.c" #include "../src/repl_rollup.c" #include "../src/repl_break.c" #include "../src/repl_info.c" #include "../src/repl_params.c" #include "../src/sim.c" #include "../src/sim_loop.c" #include "../src/sim_picker.c" #include "../src/sim_limit.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" #if !defined(XCE_SCHEDULER_BIN) #error "XCE_SCHEDULER_BIN must be defined by testsuite/meson.build" #endif /* Link-satisfying stubs and the stand-in picker below are copied from * test_sim_record_output.c, whose comments give the rationale for each. */ 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; } var limit_get_configured_limits(xce_sched_core_t* core) { (void)core; return NULL; } var limit_get_usage_instances(xce_sched_core_t* core) { (void)core; return NULL; } xce_sched_plugin_t* limit_new_plugin(void) { return NULL; } int limit_seed_config(xce_sched_plugin_t* plugin, xce_sched_core_t* core, xce_conf4sched_t* config) { (void)plugin; (void)core; (void)config; return 0; } int sched_reconcile_job_resource_usage(xce_sched_core_t* c, xce_job_account_t* a, xce_job_rusages_t* r) { (void)c; (void)a; (void)r; return 0; } void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; } int sched_fill_limit_requests(xce_sched_alloc_req_t* alloc_req, xlist_t* limits, xlist_t* rusages, int nhosts, int nslots) { (void)alloc_req; (void)limits; (void)rusages; (void)nhosts; (void)nslots; return 0; } int sched_account_jobs(xce_sched_core_t* core) { (void)core; return 0; } int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w, xvec_t* uncomitted, xvec_idval_t* wanted, xvec_idval_t* unmeet) { (void)core; (void)w; (void)uncomitted; (void)wanted; (void)unmeet; return 1; } int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted, xvec_idval_t* wanted, xvec_idval_t* unmeet) { (void)core; (void)uncomitted; (void)wanted; (void)unmeet; return 1; } int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct) { (void)core; (void)acct; return 0; } int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct) { (void)core; (void)acct; return 0; } 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* sched_job_picker(void* arg) { xce_sched_core_t* core = (xce_sched_core_t*)arg; xce_sched_plugin_t* picker = NULL; for (;;) { int go = 0; uint64_t jobid = 0; cbuf_get(&(core->wbuf), &go); if (sched_resolve_job_picker(core, &picker) < 0) { jobid = 0; cbuf_put(&(core->jbuf), &jobid); continue; } picker->picker_iface->begin_session(picker); for (;;) { xce_sched_job_t* job = picker->picker_iface->next_job(picker); if (job == NULL) { break; } jobid = job->jobid; cbuf_put(&(core->jbuf), &jobid); } jobid = 0; cbuf_put(&(core->jbuf), &jobid); picker->picker_iface->end_session(picker); } return NULL; } /* Stand-in allocator (design.md, Testing Strategy: Integration): same * PEND-only contract as test_sim_record_output.c's, plus two observation * hooks for the core under observation (g_obs_core): every cbuf_get() * return -- including the jobid==0 end-of-session terminator, so a session * with no pending job is still observed -- records core->session_start * and the jobid, and a real job decision is preceded by a real sleep * (g_alloc_sleep_us) so wall-clock time visibly passes inside a session. * Other cores (threads left parked by earlier runs, never joined) are * served but not recorded. The main thread reads the records only after * sched_sim_driver_run() returns, which it cannot do before this thread's * last cbuf_put(&core->decisions) -- the cbuf's mutex orders the writes. */ #define MAX_OBS 64 static xce_sched_core_t* _Atomic g_obs_core; static _Atomic unsigned g_alloc_sleep_us; static int g_obs_n; static uint64_t g_obs_session_start[MAX_OBS]; static uint64_t g_obs_jobid[MAX_OBS]; void* sched_allocator(void* arg) { xce_sched_core_t* core = (xce_sched_core_t*)arg; xce_sched_plugin_t* picker = NULL; for (;;) { uint64_t jobid = 0; xce_decision_t* decision = NULL; xce_sched_job_t* job; bool observed; cbuf_get(&(core->jbuf), &jobid); observed = (atomic_load(&g_obs_core) == core); if (observed && (g_obs_n < MAX_OBS)) { g_obs_session_start[g_obs_n] = core->session_start; g_obs_jobid[g_obs_n] = jobid; g_obs_n++; } if (jobid == 0) { decision = NULL; cbuf_put(&(core->decisions), &decision); continue; } job = sched_get_job(core, jobid); if (job == NULL) { continue; } if (observed && (atomic_load(&g_alloc_sleep_us) > 0)) { usleep(atomic_load(&g_alloc_sleep_us)); } (void)sched_resolve_job_picker(core, &picker); if (picker != NULL) { picker->picker_iface->on_pend(picker, job->jobid); } decision = make_pend_decision(make_pendreason(job->jobid)); cbuf_put(&(core->decisions), &decision); } return NULL; } void sim_bootstrap_seqjob_allocator(xce_sched_sim_driver_t* sim) { (void)sim; } void sim_bootstrap_parjob_allocator(xce_sched_sim_driver_t* sim) { (void)sim; } void setUp(void) { return; } void tearDown(void) { return; } /* ---------------------------------------------------------------------- */ /* Fixture helpers -- test-local copy of the dup()/dup2() stdout capture */ /* used by test_repl_help.c (each test file carries its own copy). */ /* ---------------------------------------------------------------------- */ static const char* g_warn_value; static void call_warn_helper(void) { sched_cli_warn_deprecated_options(g_warn_value); } static char* capture_stdout_around(void (*fn)(void)) { char tmp[] = "/tmp/xce_simclock_stdoutcap_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"); fn(); 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); if (len > 0) { TEST_ASSERT_EQUAL_INT64((int64_t)len, (int64_t)pread(fd, buf, (size_t)len, 0)); } close(fd); unlink(tmp); return buf; } static int count_lines(const char* s) { int n = 0; for (; *s != '\0'; s++) { if (*s == '\n') { n++; } } return n; } /* Asserts the captured output is exactly one line naming the quoted value * and carrying every required phrase. */ static void assert_one_warning_for(const char* value) { char* out; char quoted[64]; g_warn_value = value; out = capture_stdout_around(call_warn_helper); snprintf(quoted, sizeof(quoted), "\"%s\"", value); TEST_ASSERT_EQUAL_INT_MESSAGE(1, count_lines(out), "exactly one warning line expected"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, quoted), "warning must name the quoted value"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "--sim-speed"), "warning must name --sim-speed"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "deprecated"), "warning must say deprecated"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "no effect"), "warning must say no effect"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "will be removed"), "warning must say will be removed"); free(out); } /* ---------------------------------------------------------------------- */ /* CliCompat (Requirements 2.3, 2.4, 2.5) */ /* ---------------------------------------------------------------------- */ static void test_warn_numeric_zero_value(void) { assert_one_warning_for("0"); } static void test_warn_non_numeric_value(void) { assert_one_warning_for("abc"); } static void test_warn_negative_value(void) { assert_one_warning_for("-1"); } static void test_warn_empty_value(void) { assert_one_warning_for(""); } static void test_no_warning_when_option_absent(void) { char* out; g_warn_value = NULL; out = capture_stdout_around(call_warn_helper); TEST_ASSERT_EQUAL_STRING_MESSAGE("", out, "NULL (option not given) must log " "nothing"); free(out); } /* ---------------------------------------------------------------------- */ /* SimClock fixtures (Requirements 1.1-1.8, 4.2). Fixture writers copied */ /* from test_sim_record_output.c; the fixture core's clock is frozen at a */ /* chosen value first, so the recorded start snapshot's ts is exact. */ /* ---------------------------------------------------------------------- */ /* Task 2.1 RED: a non-zero speed through the still-present simulator * argument. The simulator must ignore it; removed with the field in 3.1. */ #define DISCRETE_CLOCK_RED_SPEED 7 #define SNAP_TS 1000000000000ULL /* start snapshot, 1000 s */ #define JOB_A 4101ULL #define JOB_B 4102ULL #define SLEEP_US 12000U /* stand-in allocator sleep per decision */ #define WAIT_US 25000U /* real time allowed to pass between reads */ static FILE* write_header_and_snapshot(char* out_path, size_t out_path_len, uint64_t snap_ts) { char tmpl[] = "/tmp/sched_discrete_clock_XXXXXX"; xce_scheduler_t sched; TEST_ASSERT_NOT_NULL(mkdtemp(tmpl)); memset(&sched, 0, sizeof(sched)); mylock_init(&(sched.record_lock)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); sched.core.job_pick_plugin = (char*)"builtin_fcfs"; sched.record_dir = tmpl; atomic_store(&(sched.core.virtual_clock_base), snap_ts); atomic_store(&(sched.core.speed_num), 0); atomic_store(&(sched.core.speed_den), 1); TEST_ASSERT_EQUAL_INT(0, sched_record_start(&sched, "fixture.replay")); TEST_ASSERT_EQUAL_UINT64(snap_ts, sched.record_start_ts); TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_path_len); strcpy(out_path, sched.record_file_path); return sched.record_file; } static void write_event_msg(FILE* f, replay_event_t* src) { struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; 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_queue_update_event(FILE* f, uint64_t ts, const char* queue_name) { xce_queue_t queue; replay_event_t src; 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; write_event_msg(f, &src); } 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; 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; write_event_msg(f, &src); } 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); } /* endSession marker with ts 0, as test_sim_record_output.c writes it. */ static void append_end_of_stream_marker(FILE* f) { replay_event_t src; memset(&src, 0, sizeof(src)); src.body_which = ReplayEvent_body_endSession; write_event_msg(f, &src); } /* Hand-built mid-replay SnapshotEntry (test_sim_startup.c's technique). */ static void append_snapshot_entry(FILE* f, uint64_t ts) { struct capn c; struct capn_segment* cs; SnapshotData_ptr sdata; SnapshotEntry_ptr snap; capn_init_malloc(&c); cs = capn_root(&c).seg; sdata = new_SnapshotData(cs); snap = new_SnapshotEntry(cs); SnapshotEntry_set_ts(snap, ts); SnapshotEntry_set_data(snap, sdata); capn_setp(capn_root(&c), 0, snap.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void make_tmp_file(char* out, size_t out_len, const char* tag) { int fd; snprintf(out, out_len, "/tmp/xce_discrete_clock_%s_XXXXXX", tag); fd = mkstemp(out); TEST_ASSERT_TRUE(fd >= 0); close(fd); } static void sim_args_init(sched_sim_args_t* args, const char* replay_path) { memset(args, 0, sizeof(*args)); args->replay_path = replay_path; args->sim_start_ts = SCHED_SIM_START_TS_LATEST; args->sim_speed = DISCRETE_CLOCK_RED_SPEED; } /* Runs a batch replay with `sim` as the observed core. */ static int run_observed(xce_sched_sim_driver_t* sim, const sched_sim_args_t* args, unsigned sleep_us) { g_obs_n = 0; atomic_store(&g_alloc_sleep_us, sleep_us); atomic_store(&g_obs_core, &(sim->core)); return sched_sim_driver_run(sim, args); } /* Asserts the stand-in allocator's records split (at each jobid==0 * terminator) into exactly n sessions, the k-th seeing session_start == * begin[k]. */ static void assert_observed_session_starts(const uint64_t* begin, int n) { int k = 0; int i; for (i = 0; i < g_obs_n; i++) { TEST_ASSERT_TRUE_MESSAGE(k < n, "more sessions observed than " "recorded"); TEST_ASSERT_EQUAL_UINT64_MESSAGE(begin[k], g_obs_session_start[i], "session_start must equal the " "recorded beginRound exactly"); if (g_obs_jobid[i] == 0) { k++; } } TEST_ASSERT_EQUAL_INT_MESSAGE(n, k, "every recorded session must be " "observed"); } /* Fixture shared by the output and determinism tests: two sessions whose * beginRound is not a whole second, each with a pending job before it. */ #define OUT_B1 (SNAP_TS + 1234567891ULL) #define OUT_B2 (OUT_B1 + 2000000333ULL) static void write_two_session_fixture(char* path, size_t path_len) { FILE* f = write_header_and_snapshot(path, path_len, SNAP_TS); append_queue_update_event(f, SNAP_TS + 100ULL, "normal"); append_jobs_push_event(f, SNAP_TS + 200ULL, JOB_A, "normal"); append_replay_session(f, OUT_B1); append_jobs_push_event(f, OUT_B1 + 1000ULL, JOB_B, "normal"); append_replay_session(f, OUT_B2); append_end_of_stream_marker(f); fclose(f); } /* ---------------------------------------------------------------------- */ /* SimClock unit tests (Requirements 1.1, 1.3, 1.5) */ /* ---------------------------------------------------------------------- */ /* 1.1, 1.5: after start the clock is the start snapshot's ts, and stays * there however much real time passes, with the core at speed 0/1. */ static void test_start_pins_clock_to_snapshot_ts(void) { static xce_sched_sim_driver_t sim; char path[PATH_MAX]; sched_sim_args_t args; FILE* f; f = write_header_and_snapshot(path, sizeof(path), SNAP_TS); append_end_of_stream_marker(f); fclose(f); sim_args_init(&args, path); TEST_ASSERT_EQUAL_INT(0, sched_sim_driver_start(&sim, &args)); TEST_ASSERT_EQUAL_UINT64(SNAP_TS, xce_core_now(&(sim.core))); usleep(WAIT_US); TEST_ASSERT_EQUAL_UINT64_MESSAGE(SNAP_TS, xce_core_now(&(sim.core)), "clock must not drift with real time"); TEST_ASSERT_EQUAL_UINT64(0ULL, atomic_load(&(sim.core.speed_num))); TEST_ASSERT_EQUAL_UINT64(1ULL, atomic_load(&(sim.core.speed_den))); } /* 1.3 (session case): a session whose beginRound is earlier than the * preceding event's ts starts at exactly that beginRound; the replay * completes. */ static void test_session_earlier_than_preceding_event_sets_exact(void) { static xce_sched_sim_driver_t sim; char path[PATH_MAX]; sched_sim_args_t args; FILE* f; const uint64_t b1 = SNAP_TS + 1500000001ULL; const uint64_t e = b1 + 900000000ULL; const uint64_t b2 = b1 + 500000007ULL; /* < e */ const uint64_t begin[2] = { b1, b2 }; f = write_header_and_snapshot(path, sizeof(path), SNAP_TS); append_queue_update_event(f, SNAP_TS + 100ULL, "normal"); append_jobs_push_event(f, SNAP_TS + 200ULL, JOB_A, "normal"); append_replay_session(f, b1); append_queue_update_event(f, e, "padq"); /* inert padding event */ append_replay_session(f, b2); append_end_of_stream_marker(f); fclose(f); sim_args_init(&args, path); TEST_ASSERT_EQUAL_INT(0, run_observed(&sim, &args, SLEEP_US)); assert_observed_session_starts(begin, 2); TEST_ASSERT_EQUAL_UINT64(b2, xce_core_now(&(sim.core))); } /* 1.3 (event case, as the normal-mode recorder produces): events whose ts * is earlier than the start snapshot set the clock to exactly that ts -- * observed through the static sim_process_replay_event_message() -- and a * full replay of the same file completes. */ static void test_events_earlier_than_start_snapshot_set_exact(void) { static xce_sched_sim_driver_t start_sim; static xce_sched_sim_driver_t run_sim; char path[PATH_MAX]; sched_sim_args_t args; FILE* f; const uint64_t ev_ts[2] = { SNAP_TS - 5000ULL, SNAP_TS - 3000ULL }; const uint64_t begin[1] = { SNAP_TS + 1000000017ULL }; int i; f = write_header_and_snapshot(path, sizeof(path), SNAP_TS); append_queue_update_event(f, ev_ts[0], "normal"); append_jobs_push_event(f, ev_ts[1], JOB_A, "normal"); append_replay_session(f, begin[0]); append_end_of_stream_marker(f); fclose(f); sim_args_init(&args, path); TEST_ASSERT_EQUAL_INT(0, sched_sim_driver_start(&start_sim, &args)); TEST_ASSERT_EQUAL_UINT64(SNAP_TS, xce_core_now(&(start_sim.core))); for (i = 0; i < 2; i++) { struct capn c; capn_ptr root; xce_core_event_t ev; int record_rc = 0; TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, start_sim.replay_file, 0)); TEST_ASSERT_TRUE(msg_root_has_shape(&c, &root, ReplayEvent_word_count, ReplayEvent_pointer_count)); TEST_ASSERT_TRUE(sim_process_replay_event_message(&start_sim, &c, root, &ev, &record_rc) >= 0); TEST_ASSERT_EQUAL_INT(0, record_rc); usleep(WAIT_US / 5); TEST_ASSERT_EQUAL_UINT64_MESSAGE(ev_ts[i], xce_core_now(&(start_sim.core)), "an event earlier than the clock " "must set it exactly, unclamped"); } TEST_ASSERT_EQUAL_INT(0, run_observed(&run_sim, &args, 0)); assert_observed_session_starts(begin, 1); } /* 1.5: a mid-replay SnapshotEntry (ts S) followed by the endSession marker * (ts 0) leaves the clock at the last event's ts T, after the run and * after a real sleep. sim_start_ts is pinned to the initial snapshot so S * is not chosen as the start snapshot and the real skip branch runs. */ static void test_mid_replay_snapshot_and_end_leave_clock_at_last_event(void) { static xce_sched_sim_driver_t sim; char path[PATH_MAX]; sched_sim_args_t args; FILE* f; const uint64_t t = SNAP_TS + 700000011ULL; const uint64_t s = SNAP_TS + 900000000ULL; f = write_header_and_snapshot(path, sizeof(path), SNAP_TS); append_queue_update_event(f, t, "padq"); append_snapshot_entry(f, s); append_end_of_stream_marker(f); fclose(f); sim_args_init(&args, path); args.sim_start_ts = SNAP_TS; TEST_ASSERT_EQUAL_INT(0, run_observed(&sim, &args, 0)); TEST_ASSERT_EQUAL_UINT64(SNAP_TS, sim.snapshot_ts); TEST_ASSERT_EQUAL_UINT64(t, xce_core_now(&(sim.core))); usleep(WAIT_US); TEST_ASSERT_EQUAL_UINT64_MESSAGE(t, xce_core_now(&(sim.core)), "clock must stay at the last event " "after the end of the recording"); } /* ---------------------------------------------------------------------- */ /* Replay-loop integration tests (Requirements 1.2, 1.4, 1.6, 1.7, 1.8) */ /* ---------------------------------------------------------------------- */ typedef struct { uint64_t begin_round; uint64_t duration_ns; int n_decisions; uint64_t decision_ts[8]; } out_session_t; /* Decodes every ReplaySession in a simulation output file. */ static int read_output_sessions(const char* path, out_session_t* out, int max) { FILE* f = fopen(path, "r"); struct capn c; int n = 0; TEST_ASSERT_NOT_NULL(f); while (capn_init_fp(&c, f, 0) == 0) { capn_ptr root; ReplaySession_ptr sp; replay_session_t* dst = NULL; int i; if (!msg_root_has_shape(&c, &root, ReplaySession_word_count, ReplaySession_pointer_count)) { capn_free(&c); continue; } TEST_ASSERT_TRUE(n < max); sp.p = root; decode_ReplaySession_ptr(&dst, sp); capn_free(&c); TEST_ASSERT_NOT_NULL(dst); TEST_ASSERT_NOT_NULL(dst->summary); TEST_ASSERT_TRUE(dst->n_decisions <= 8); out[n].begin_round = dst->begin_round; out[n].duration_ns = dst->summary->duration_ns; out[n].n_decisions = dst->n_decisions; for (i = 0; i < dst->n_decisions; i++) { out[n].decision_ts[i] = dst->decisions[i]->ts; } free_ReplaySession_ptr(&dst); n++; } fclose(f); return n; } /* 1.2, 1.4, 1.7, 1.8: with an allocator that sleeps before deciding and * beginRounds that are not whole seconds, session_start seen inside each * session equals the recorded beginRound exactly, and the output has that * beginRound, durationNs 0, and every decision ts equal to it. */ static void test_session_start_and_output_times_equal_begin_round(void) { static xce_sched_sim_driver_t sim; char path[PATH_MAX]; char out_path[PATH_MAX]; sched_sim_args_t args; const uint64_t begin[2] = { OUT_B1, OUT_B2 }; out_session_t sessions[4]; int n; int k; int i; write_two_session_fixture(path, sizeof(path)); make_tmp_file(out_path, sizeof(out_path), "out"); sim_args_init(&args, path); args.output_path = out_path; TEST_ASSERT_EQUAL_INT(0, run_observed(&sim, &args, SLEEP_US)); assert_observed_session_starts(begin, 2); n = read_output_sessions(out_path, sessions, 4); TEST_ASSERT_EQUAL_INT(2, n); for (k = 0; k < n; k++) { TEST_ASSERT_EQUAL_UINT64(begin[k], sessions[k].begin_round); TEST_ASSERT_EQUAL_UINT64_MESSAGE(0ULL, sessions[k].duration_ns, "no logical time passes inside " "a replayed session"); TEST_ASSERT_TRUE_MESSAGE(sessions[k].n_decisions >= 1, "each session must decide at least once"); for (i = 0; i < sessions[k].n_decisions; i++) { TEST_ASSERT_EQUAL_UINT64(begin[k], sessions[k].decision_ts[i]); } } unlink(out_path); } /* Reads a whole file; *hdr_end is the offset just past its first message * (the ReplayHeader), whose fields other than recordingStartWall are * returned in *hdr. */ static char* slurp_output(const char* path, long* len, long* hdr_end, replay_header_t* hdr) { FILE* f = fopen(path, "r"); struct capn c; ReplayHeader_ptr p; struct ReplayHeader wire; char* buf; TEST_ASSERT_NOT_NULL(f); TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, f, 0)); p.p = capn_getp(capn_root(&c), 0, 1); read_ReplayHeader(&wire, p); memset(hdr, 0, sizeof(*hdr)); decode_ReplayHeader(hdr, &wire); capn_free(&c); *hdr_end = ftell(f); TEST_ASSERT_EQUAL_INT(0, fseek(f, 0, SEEK_END)); *len = ftell(f); buf = (char*)malloc((size_t)*len); TEST_ASSERT_NOT_NULL(buf); rewind(f); TEST_ASSERT_EQUAL_UINT((size_t)*len, fread(buf, 1, (size_t)*len, f)); fclose(f); return buf; } /* 1.6: the same recording replayed twice gives identical output (every * logical timestamp and every decision), except recordingStartWall. */ static void test_two_replays_produce_identical_output(void) { static xce_sched_sim_driver_t sim1; static xce_sched_sim_driver_t sim2; char path[PATH_MAX]; char out[2][PATH_MAX]; sched_sim_args_t args; replay_header_t hdr[2]; long len[2]; long hdr_end[2]; char* buf[2]; int r; write_two_session_fixture(path, sizeof(path)); for (r = 0; r < 2; r++) { make_tmp_file(out[r], sizeof(out[r]), "det"); sim_args_init(&args, path); args.output_path = out[r]; TEST_ASSERT_EQUAL_INT(0, run_observed((r == 0) ? &sim1 : &sim2, &args, SLEEP_US)); buf[r] = slurp_output(out[r], &len[r], &hdr_end[r], &hdr[r]); } TEST_ASSERT_EQUAL_UINT64(hdr[0].recording_start_ts, hdr[1].recording_start_ts); TEST_ASSERT_EQUAL_UINT64(SNAP_TS, hdr[0].recording_start_ts); TEST_ASSERT_EQUAL_STRING(hdr[0].recorder_version, hdr[1].recorder_version); TEST_ASSERT_EQUAL_STRING(hdr[0].sched_policy, hdr[1].sched_policy); TEST_ASSERT_EQUAL_PTR(NULL, hdr[0].cluster); TEST_ASSERT_EQUAL_PTR(NULL, hdr[1].cluster); TEST_ASSERT_EQUAL_INT64(len[0] - hdr_end[0], len[1] - hdr_end[1]); TEST_ASSERT_EQUAL_MEMORY_MESSAGE(buf[0] + hdr_end[0], buf[1] + hdr_end[1], (size_t)(len[0] - hdr_end[0]), "everything after the header must be " "byte-identical across replays"); for (r = 0; r < 2; r++) { free_ReplayHeader(&hdr[r]); free(buf[r]); unlink(out[r]); } } /* ---------------------------------------------------------------------- */ /* CLI end to end and --help (task 2.3; Requirements 1.4, 1.7, 1.8, */ /* 2.1-2.6). The built scheduler binary runs as a child process on the */ /* two-session fixture; its stdout (decision lines) goes to /dev/null and */ /* the exit status comes from WEXITSTATUS(pclose()). Every run gets a */ /* fresh --sim-output and --logfile. */ /* ---------------------------------------------------------------------- */ /* Runs `cmd` through the shell, drains its stdout to EOF and returns its * exit status (-1 if it did not exit normally). */ static int run_child(const char* cmd) { FILE* p = popen(cmd, "r"); char sink[512]; int st; TEST_ASSERT_NOT_NULL_MESSAGE(p, "popen of the scheduler must succeed"); while (fread(sink, 1, sizeof(sink), p) > 0) { } st = pclose(p); TEST_ASSERT_TRUE_MESSAGE(st != -1, "pclose must succeed"); return WIFEXITED(st) ? WEXITSTATUS(st) : -1; } /* Runs the scheduler in batch simulation mode on `fixture` with `extra` * appended to the command line; writes to fresh output and log files whose * paths are returned. */ static int run_sim_cli(const char* fixture, const char* extra, char* out, char* log) { char cmd[4 * PATH_MAX]; make_tmp_file(out, PATH_MAX, "cliout"); make_tmp_file(log, PATH_MAX, "clilog"); snprintf(cmd, sizeof(cmd), XCE_SCHEDULER_BIN " --sim %s --sim-output %s --logfile %s %s " "> /dev/null", fixture, out, log, extra); return run_child(cmd); } /* Counts the log lines naming --sim-speed; *named is set to how many of * them also contain `quoted`. An absent log counts as zero lines. */ static int count_speed_log_lines(const char* log, const char* quoted, int* named) { FILE* f = fopen(log, "r"); char line[4096]; int n = 0; *named = 0; if (f == NULL) { return 0; } while (fgets(line, sizeof(line), f) != NULL) { if (strstr(line, "--sim-speed") != NULL) { n++; if ((quoted != NULL) && (strstr(line, quoted) != NULL)) { (*named)++; } } } fclose(f); return n; } static void assert_one_speed_warning(const char* log, const char* quoted) { int named = 0; TEST_ASSERT_EQUAL_INT_MESSAGE(1, count_speed_log_lines(log, quoted, &named), "the log must hold exactly one " "--sim-speed warning"); TEST_ASSERT_EQUAL_INT_MESSAGE(1, named, "the warning must name the " "quoted value"); } /* Output identical to the reference except ReplayHeader.recordingStartWall: * header decoded and compared field by field, the rest byte for byte. */ static void assert_same_output(const char* ref, const char* got) { replay_header_t hdr[2]; long len[2]; long hdr_end[2]; char* buf[2]; int r; buf[0] = slurp_output(ref, &len[0], &hdr_end[0], &hdr[0]); buf[1] = slurp_output(got, &len[1], &hdr_end[1], &hdr[1]); TEST_ASSERT_EQUAL_UINT64(hdr[0].recording_start_ts, hdr[1].recording_start_ts); TEST_ASSERT_EQUAL_STRING(hdr[0].recorder_version, hdr[1].recorder_version); TEST_ASSERT_EQUAL_STRING(hdr[0].sched_policy, hdr[1].sched_policy); TEST_ASSERT_EQUAL_PTR(NULL, hdr[0].cluster); TEST_ASSERT_EQUAL_PTR(NULL, hdr[1].cluster); TEST_ASSERT_EQUAL_INT64(len[0] - hdr_end[0], len[1] - hdr_end[1]); TEST_ASSERT_EQUAL_MEMORY_MESSAGE(buf[0] + hdr_end[0], buf[1] + hdr_end[1], (size_t)(len[0] - hdr_end[0]), "--sim-speed must not change the " "output after the header"); for (r = 0; r < 2; r++) { free_ReplayHeader(&hdr[r]); free(buf[r]); } } /* (a) no --sim-speed: exit 0, exact session starts, zero durations, * decision ts == beginRound (at least one decision per session), and no * warning in the log. Returns the output path in `out` for (b). */ static void run_default_cli(const char* fixture, char* out) { const uint64_t begin[2] = { OUT_B1, OUT_B2 }; char log[PATH_MAX]; out_session_t sessions[4]; int named = 0; int n; int k; int i; TEST_ASSERT_EQUAL_INT_MESSAGE(0, run_sim_cli(fixture, "", out, log), "the default command line must exit 0"); n = read_output_sessions(out, sessions, 4); TEST_ASSERT_EQUAL_INT(2, n); for (k = 0; k < n; k++) { TEST_ASSERT_EQUAL_UINT64(begin[k], sessions[k].begin_round); TEST_ASSERT_EQUAL_UINT64(0ULL, sessions[k].duration_ns); TEST_ASSERT_TRUE_MESSAGE(sessions[k].n_decisions >= 1, "each session must decide at least once"); for (i = 0; i < sessions[k].n_decisions; i++) { TEST_ASSERT_EQUAL_UINT64(begin[k], sessions[k].decision_ts[i]); } } TEST_ASSERT_EQUAL_INT_MESSAGE(0, count_speed_log_lines(log, NULL, &named), "no --sim-speed warning without the " "option"); unlink(log); } /* (a) 1.4, 1.7, 1.8, 2.5 on the default command line. */ static void test_cli_default_exact_times_no_warning(void) { char fixture[PATH_MAX]; char out[PATH_MAX]; write_two_session_fixture(fixture, sizeof(fixture)); run_default_cli(fixture, out); unlink(out); } /* (b) 2.1-2.4: any value, valid or not, exits 0 with output identical to * the default run and exactly one warning naming the value. */ static void test_cli_sim_speed_any_value_is_noop_with_one_warning(void) { static const char* const extra[] = { "--sim-speed 7", "--sim-speed abc", "--sim-speed -1", "--sim-speed=" }; static const char* const quoted[] = { "\"7\"", "\"abc\"", "\"-1\"", "\"\"" }; char fixture[PATH_MAX]; char ref[PATH_MAX]; char out[PATH_MAX]; char log[PATH_MAX]; size_t i; write_two_session_fixture(fixture, sizeof(fixture)); run_default_cli(fixture, ref); for (i = 0; i < sizeof(extra) / sizeof(extra[0]); i++) { TEST_ASSERT_EQUAL_INT_MESSAGE(0, run_sim_cli(fixture, extra[i], out, log), extra[i]); assert_same_output(ref, out); assert_one_speed_warning(log, quoted[i]); unlink(out); unlink(log); } unlink(ref); } /* (c) 2.3: a repeated option logs one warning, naming the last value. */ static void test_cli_repeated_sim_speed_names_last_value(void) { char fixture[PATH_MAX]; char out[PATH_MAX]; char log[PATH_MAX]; write_two_session_fixture(fixture, sizeof(fixture)); TEST_ASSERT_EQUAL_INT(0, run_sim_cli(fixture, "--sim-speed 1 --sim-speed 9", out, log)); assert_one_speed_warning(log, "\"9\""); unlink(out); unlink(log); } /* (d) 2.4: a trailing --sim-speed with no value is still an argp error. */ static void test_cli_trailing_sim_speed_without_value_fails(void) { char fixture[PATH_MAX]; char cmd[2 * PATH_MAX]; write_two_session_fixture(fixture, sizeof(fixture)); snprintf(cmd, sizeof(cmd), XCE_SCHEDULER_BIN " --sim %s --sim-speed > /dev/null 2>&1", fixture); TEST_ASSERT_NOT_EQUAL_INT_MESSAGE(0, run_child(cmd), "--sim-speed without a value must be " "a command-line error"); } /* (e) 2.3 on the REPL exit path: end of input -> quit -> exit(0), and the * log holds exactly one warning. */ static void test_cli_interactive_sim_speed_logs_one_warning(void) { char fixture[PATH_MAX]; char log[PATH_MAX]; char cmd[3 * PATH_MAX]; write_two_session_fixture(fixture, sizeof(fixture)); make_tmp_file(log, sizeof(log), "clilogi"); snprintf(cmd, sizeof(cmd), XCE_SCHEDULER_BIN " --sim %s -i --sim-speed 3 --logfile %s " "< /dev/null > /dev/null", fixture, log); TEST_ASSERT_EQUAL_INT(0, run_child(cmd)); assert_one_speed_warning(log, "\"3\""); unlink(log); } /* 2.6: --help describes --sim-speed as deprecated and offers no speed or * real-time mode. */ static void test_cli_help_describes_sim_speed_as_deprecated(void) { FILE* p = popen(XCE_SCHEDULER_BIN " --help", "r"); char* text; size_t cap = 65536; size_t len = 0; size_t got; char* entry; char* eol; TEST_ASSERT_NOT_NULL(p); text = (char*)calloc(1, cap); TEST_ASSERT_NOT_NULL(text); while ((got = fread(text + len, 1, cap - 1 - len, p)) > 0) { len += got; } TEST_ASSERT_EQUAL_INT(0, WEXITSTATUS(pclose(p))); entry = strstr(text, "--sim-speed"); TEST_ASSERT_NOT_NULL_MESSAGE(entry, "--help must list --sim-speed"); eol = strchr(entry, '\n'); if (eol != NULL) { *eol = '\0'; } TEST_ASSERT_NOT_NULL_MESSAGE(strstr(entry, "Deprecated"), "the --sim-speed entry must say " "Deprecated"); if (eol != NULL) { *eol = '\n'; } TEST_ASSERT_NULL_MESSAGE(strstr(text, "real-time"), "--help must not mention real-time"); TEST_ASSERT_NULL_MESSAGE(strstr(text, "as-fast-as-possible"), "--help must not mention as-fast-as-possible"); free(text); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_warn_numeric_zero_value); RUN_TEST(test_warn_non_numeric_value); RUN_TEST(test_warn_negative_value); RUN_TEST(test_warn_empty_value); RUN_TEST(test_no_warning_when_option_absent); RUN_TEST(test_start_pins_clock_to_snapshot_ts); RUN_TEST(test_session_earlier_than_preceding_event_sets_exact); RUN_TEST(test_events_earlier_than_start_snapshot_set_exact); RUN_TEST(test_mid_replay_snapshot_and_end_leave_clock_at_last_event); RUN_TEST(test_session_start_and_output_times_equal_begin_round); RUN_TEST(test_two_replays_produce_identical_output); RUN_TEST(test_cli_default_exact_times_no_warning); RUN_TEST(test_cli_sim_speed_any_value_is_noop_with_one_warning); RUN_TEST(test_cli_repeated_sim_speed_names_last_value); RUN_TEST(test_cli_trailing_sim_speed_without_value_fails); RUN_TEST(test_cli_interactive_sim_speed_logs_one_warning); RUN_TEST(test_cli_help_describes_sim_speed_as_deprecated); return UNITY_END(); }