myxce/src/scheduler/testsuite/test_sim_discrete_clock.c
2026-09-29 05:06:06 +00:00

1272 lines
44 KiB
C

/*
* 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 <errno.h>
#include <limits.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <unity/unity.h>
#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();
}