/* * test_repl_trace_baseline.c - scheduler-sim-decision-trace spec, task 1.1 * (Requirements 10.4, 10.5; design.md "Testing Strategy" -> "Equivalence * Tests (Requirement 10)" item 2): the trace-off REPL transcript baseline. * * A scripted REPL session runs the real, built xce_scheduler binary * (XCE_SCHEDULER_BIN, from testsuite/meson.build) in interactive simulation * mode over a committed resource-contention replay. Its console transcript, * its --sim-output recording, the recording a `set output` command opens * midway, and its --sim-verify result are compared with committed fixtures * captured before any decision-trace code existed. Later tasks of the spec * use this test to prove that, with tracing off, the REPL output is * unchanged (task 5.2 updates exactly the help and info-usage lines). * * Fixtures (testsuite/fixtures/repl_trace_baseline/, XCE_BASELINE_DIR): * contention.replay input replay (written by write_contention_replay()) * script.txt the REPL command script fed on stdin * transcript.txt expected stdout, byte for byte * sim_output.replay expected --sim-output recording * set_output.replay expected recording opened by `set output` * * sim_output.replay holds only the header, the snapshot and the input * events, with no session decisions. This is existing behaviour: the * script's `set output` runs in xce_repl_post_session_pause(), before * sched_record_session_finalize() writes session 1 (sim_loop.c), so session * 1's decisions land in set_output.replay, as do all later sessions. * Decision coverage therefore comes from set_output.replay and * sim_verify.txt. The file written by the script's `snapshot snap.replay` * is not compared; only its transcript line is. * sim_verify.txt expected --sim-verify result: the mismatch lines of * the log, each cut to start at its message * ("sim_log_mismatch decision verification MISMATCH") * so the logger's time/thread-id prefix is dropped * * The only metadata the comparison ignores: ReplayHeader. * recordingStartWall in both recordings, and the log-line prefix (time and * thread id) of the verify lines. Everything else is compared byte for * byte. * * The contention replay: two workers (4 slots, 16384 mem each), a 2-unit * cluster license, a cluster-wide limit of 6 slots and a host-scoped limit * of 75% of each host's slots. Jobs compete for the license, both limits * and the hosts: license jobs, a parallel job (2..4 tasks), an alternative- * requirement job whose first alternative cannot fit, and slot-heavy jobs. * Later rounds finish jobs (JOB_STATE_CHANGE) and push a late job, with * idle rounds in between so `continue` collapses ranges. Every session's * recorded decision list is empty, so --sim-verify reports each live * decision as unexpected and no recorded-decision reconciliation runs. * * The child runs in a fresh temporary directory holding copies of the * replay and script, with relative --sim-output/--logfile/`set output` * paths, so no temporary path reaches the transcript. * * Regenerating (only when a spec task deliberately changes the expected * output, e.g. task 5.2's help lines): run this binary with * XCE_REPL_BASELINE_REGEN=1. It rewrites contention.replay and every * expected-output fixture from the current tree; script.txt is hand * written and never regenerated. Review the fixture diff before * committing. * * bug-1923 regenerated set_output.replay only: the snapshot `set output` * takes mid-run now carries the shared resvals (license, * limit=cluster_slots) and each worker's limit=host_slots value, which the * old snapshot writer dropped. Every other fixture is unchanged (a regen * also rewrites recordingStartWall, which the comparison ignores). * * Link style: the real scheduler_srcs list (minus main.c), same as * test_sim_e2e_limit_visibility.c, which this file's replay writer * follows (resources, license value and limit config baked into the * snapshot; workers, queue and jobs as wire events). */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "controller.capnp.h" #include "recording.h" #include "resobj.h" #include "scheduler.h" #include "sim.h" #include "xcelimit.h" #include "xstring.h" #if !defined(XCE_SCHEDULER_BIN) || !defined(XCE_BASELINE_DIR) #error "XCE_SCHEDULER_BIN and XCE_BASELINE_DIR must be defined by meson" #endif extern int write_capnp_message(FILE* f, struct capn* c); #define REPLAY_NAME "contention.replay" #define SCRIPT_NAME "script.txt" #define TRANSCRIPT_NAME "transcript.txt" #define SIM_OUT_NAME "sim_output.replay" #define SET_OUT_NAME "set_output.replay" #define VERIFY_NAME "sim_verify.txt" #define LOG_NAME "sched.log" #define STDERR_NAME "stderr.txt" #define SNAP_NAME "snap.replay" #define QUEUE_NAME "normal" #define WORKER_A "w01" #define WORKER_B "w02" #define LICENSE_RES "license" #define SNAPSHOT_TS 1000ULL #define GLIMIT_NAME "cluster_slots" #define HLIMIT_NAME "host_slots" #define MISMATCH_TAG "decision verification MISMATCH" #define VERIFY_MARK "sim_log_mismatch " MISMATCH_TAG void setUp(void) { return; } void tearDown(void) { return; } /* ---------------------------------------------------------------------- */ /* File helpers */ /* ---------------------------------------------------------------------- */ /* Reads a whole file. Returns NULL (and *len = 0) when it cannot be read, * so a missing fixture fails the caller's assertion, not this helper. */ static char* slurp(const char* path, long* len) { FILE* f = fopen(path, "rb"); char* buf; long n; *len = 0; if (f == NULL) { return NULL; } if ((fseek(f, 0, SEEK_END) != 0) || ((n = ftell(f)) < 0)) { fclose(f); return NULL; } rewind(f); buf = (char*)malloc((size_t)n + 1); if (buf == NULL) { fclose(f); return NULL; } if ((n > 0) && (fread(buf, 1, (size_t)n, f) != (size_t)n)) { free(buf); fclose(f); return NULL; } buf[n] = '\0'; fclose(f); *len = n; return buf; } static void spit(const char* path, const char* buf, long len) { FILE* f = fopen(path, "wb"); TEST_ASSERT_NOT_NULL_MESSAGE(f, path); if (len > 0) { TEST_ASSERT_EQUAL_UINT((size_t)len, fwrite(buf, 1, (size_t)len, f)); } TEST_ASSERT_EQUAL_INT(0, fclose(f)); } static void copy_file(const char* from, const char* to) { long len; char* buf = slurp(from, &len); TEST_ASSERT_NOT_NULL_MESSAGE(buf, from); spit(to, buf, len); free(buf); } static void join_path(char* out, size_t outsz, const char* dir, const char* name) { int n = snprintf(out, outsz, "%s/%s", dir, name); TEST_ASSERT_TRUE((n > 0) && ((size_t)n < outsz)); } /* ---------------------------------------------------------------------- */ /* Contention replay writer */ /* ---------------------------------------------------------------------- */ static xce_resource_t* bake_resource(var resmgr, const char* name, int flags) { xce_resource_t res; xce_resource_t* r; memset(&res, 0, sizeof(res)); res.name = strdup(name); res.valtype = XCE_VAL_NUMERIC; res.flags = flags; r = makeResource(resmgr, &res); TEST_ASSERT_NOT_NULL(r); return r; } static xce_name_val_t* make_nv(const char* name, float v) { xce_name_val_t* nv = (xce_name_val_t*)calloc(1, sizeof(*nv)); xce_val_t* val = (xce_val_t*)calloc(1, sizeof(*val)); TEST_ASSERT_NOT_NULL(nv); TEST_ASSERT_NOT_NULL(val); val->which = Value_u_fvalue; val->u.fvalue = v; nv->name = strdup(name); nv->val = val; return nv; } static xce_worker_t* make_wire_worker(const char* name, float slots, float mem) { xce_worker_t* w = (xce_worker_t*)calloc(1, sizeof(*w)); xce_name_val_t** nv = (xce_name_val_t**)calloc(2, sizeof(*nv)); TEST_ASSERT_NOT_NULL(w); TEST_ASSERT_NOT_NULL(nv); nv[0] = make_nv("slots", slots); nv[1] = make_nv("mem", mem); w->name = strdup(name); w->status = 0; w->nresvals_to_sched = 2; w->resvals_to_sched = nv; return w; } static xce_job_t* make_push_job(uint64_t jobid, uint64_t sub_time, const char* resreq, const char* user, uint32_t mintasks, uint32_t maxtasks) { xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j)); TEST_ASSERT_NOT_NULL(j); j->jobid = jobid; j->state = JOB_STAT_PEND; j->sub_time = sub_time; j->queue_time = sub_time; j->submit = (job_submit_data_t*)calloc(1, sizeof(*(j->submit))); TEST_ASSERT_NOT_NULL(j->submit); j->submit->queue = (char*)QUEUE_NAME; j->submit->user = (char*)user; j->submit->project_name = (char*)"proj1"; j->submit->resreq = (char*)resreq; j->submit->mintasks = mintasks; j->submit->maxtasks = maxtasks; return j; } static void write_event(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_workers(FILE* f, uint64_t ts, xce_worker_t** w, int n) { xce_workers_data_t wrap; replay_event_t src; memset(&wrap, 0, sizeof(wrap)); wrap.n_total = n; wrap.count = n; wrap.workers = w; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_workersPush; src.body.workers_push = &wrap; write_event(f, &src); } static void append_queue(FILE* f, uint64_t ts, const char* name) { xce_queue_t q; replay_event_t src; memset(&q, 0, sizeof(q)); q.queue = (char*)name; q.status = QUEUE_STAT_OPEN; q.priority = 1; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_queueUpdate; src.body.queue_update = &q; write_event(f, &src); } /* The license's cluster-wide value, through the same RESVALS_FULL ingest * the live driver records. bug-1923: shared resvals baked into the * snapshot are dropped on load, so the snapshot cannot carry it. */ static void append_license_value(FILE* f, uint64_t ts, float total) { xce_val_t val; xce_resval_t rv; xce_resval_t* rvs[1]; xce_resval_list_t list; replay_event_t src; memset(&val, 0, sizeof(val)); val.which = Value_u_fvalue; val.u.fvalue = total; memset(&rv, 0, sizeof(rv)); rv.name = (char*)LICENSE_RES; rv.val = &val; rvs[0] = &rv; memset(&list, 0, sizeof(list)); list.count = 1; list.vals = rvs; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_resvalsFull; src.body.resvals_full = &list; write_event(f, &src); } static void append_jobs(FILE* f, uint64_t ts, xce_job_t** jobs, int n) { xce_jobs_data_t wrap; replay_event_t src; memset(&wrap, 0, sizeof(wrap)); wrap.n_total = n; wrap.count = n; wrap.jobs = jobs; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_jobsPush; src.body.jobs_push = &wrap; write_event(f, &src); } static void append_state_change(FILE* f, uint64_t ts, uint64_t jobid, uint32_t old_state, uint32_t new_state) { job_state_change_event_t change; replay_event_t src; memset(&change, 0, sizeof(change)); change.jobid = jobid; change.old_state = old_state; change.new_state = new_state; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_jobStateChange; src.body.job_state_change = &change; write_event(f, &src); } /* An empty decisions list: see this file's header comment. */ static void append_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(FILE* f) { replay_event_t src; memset(&src, 0, sizeof(src)); src.body_which = ReplayEvent_body_endSession; write_event(f, &src); } /* Header + snapshot via the real sched_record_start(), with the resources * and both limits baked into the snapshot (the limit plugin is seeded from * the snapshot before any replay event runs; see * test_sim_e2e_limit_visibility.c's header comment). Records into a * scratch directory; the caller moves the file to its final place. */ static FILE* write_header_and_snapshot(char* out_path, size_t out_len, const char* record_dir) { xce_scheduler_t sched; xce_sched_plugin_t* fcfs; xce_limit_val_t gval = { .resname = (char*)"slots", .percent = 0, .val = 6.0 }; xce_limit_config_t gcfg = { .name = (char*)GLIMIT_NAME, .val = &gval }; xce_limit_val_t hval = { .resname = (char*)"slots", .percent = 1, .val = 0.75 }; xce_limit_config_t hcfg = { .name = (char*)HLIMIT_NAME, .val = &hval }; xce_limit_config_t* limits[2] = { &gcfg, &hcfg }; xce_conf4sched_t config; memset(&sched, 0, sizeof(sched)); mylock_init(&(sched.record_lock)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); fcfs = builtin_fcfs_new_plugin(); TEST_ASSERT_NOT_NULL(fcfs); TEST_ASSERT_TRUE(plugin_register_core(&(sched.core), fcfs) >= 0); (void)bake_resource(sched.core.resmgr, "slots", XCE_RESF_CONSUMABLE); (void)bake_resource(sched.core.resmgr, "mem", XCE_RESF_CONSUMABLE); (void)bake_resource(sched.core.resmgr, LICENSE_RES, XCE_RESF_GLOBAL | XCE_RESF_SHARED | XCE_RESF_CONSUMABLE); memset(&config, 0, sizeof(config)); config.nlimits = 2; config.limits = limits; sched.core.config = &config; /* A frozen logical clock (speed 0, the simulator's own discrete-clock * setting) pins the header's recordingStartTs and the snapshot ts to * SNAPSHOT_TS instead of a CLOCK_MONOTONIC reading. */ atomic_store(&(sched.core.virtual_clock_base), SNAPSHOT_TS); atomic_store(&(sched.core.speed_num), 0ULL); atomic_store(&(sched.core.speed_den), 1ULL); sched.core.job_pick_plugin = (char*)"builtin_fcfs"; sched.record_dir = (char*)record_dir; TEST_ASSERT_EQUAL_INT(0, sched_record_start(&sched, REPLAY_NAME)); TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_len); strcpy(out_path, sched.record_file_path); return sched.record_file; } /* * write_contention_replay - the input fixture (regeneration only). The * license's value (2) arrives as a RESVALS_FULL event after the workers. * Rounds (beginRound = ts) and what the baseline shows for them: * 2000 jobs 101..107 pending, in FCFS order: 101 is a 2..4-task parallel * job (dispatches 4 tasks over both hosts), 102/103/104 want one * license each (104 pends: license exhausted), 105 wants an * alternative whose first branch (4 tasks x 12288 mem) cannot fit, * 106/107 want 3 slots each; the cluster-wide limit (6) and the * host limit (3 per host) are then full, so 105..107 pend * 3000, 4000, 5000 identical repeats (collapsed by `continue`) * 6000 after 101 finishes and 102 exits: 104, 105 and 106 dispatch * 7000, 8000 identical repeats * 9000 after a late license job 108 is pushed and 103 finishes * 10000 final round */ static void write_contention_replay(const char* dest) { char record_dir[PATH_MAX]; char path[PATH_MAX]; FILE* f; xce_worker_t* workers[2]; xce_job_t* jobs[7]; xce_job_t* late[1]; snprintf(record_dir, sizeof(record_dir), "/tmp/sched_trbase_gen_XXXXXX"); TEST_ASSERT_NOT_NULL(mkdtemp(record_dir)); f = write_header_and_snapshot(path, sizeof(path), record_dir); workers[0] = make_wire_worker(WORKER_A, 4.0f, 16384.0f); workers[1] = make_wire_worker(WORKER_B, 4.0f, 16384.0f); append_workers(f, 1001ULL, workers, 2); append_queue(f, 1002ULL, QUEUE_NAME); append_license_value(f, 1002ULL, 2.0f); jobs[0] = make_push_job(101, 1101, "rusage[mem=1024]", "alice", 2, 4); jobs[1] = make_push_job(102, 1102, "rusage[license=1]", "bob", 1, 1); jobs[2] = make_push_job(103, 1103, "rusage[license=1]", "alice", 1, 1); jobs[3] = make_push_job(104, 1104, "rusage[license=1]", "bob", 1, 1); jobs[4] = make_push_job(105, 1105, "{4*{rusage[mem=12288]}} || " "{1*{rusage[mem=2048]}}", "alice", 1, 1); jobs[5] = make_push_job(106, 1106, "rusage[mem=512]", "bob", 3, 3); jobs[6] = make_push_job(107, 1107, "rusage[mem=512]", "alice", 3, 3); append_jobs(f, 1003ULL, jobs, 7); append_session(f, 2000ULL); append_session(f, 3000ULL); append_session(f, 4000ULL); append_session(f, 5000ULL); append_state_change(f, 5500ULL, 101, JOB_STAT_RUN, JOB_STAT_DONE); append_state_change(f, 5600ULL, 102, JOB_STAT_RUN, JOB_STAT_EXIT); append_session(f, 6000ULL); append_session(f, 7000ULL); append_session(f, 8000ULL); late[0] = make_push_job(108, 8500, "rusage[license=1]", "bob", 1, 1); append_jobs(f, 8500ULL, late, 1); append_state_change(f, 8600ULL, 103, JOB_STAT_RUN, JOB_STAT_DONE); append_session(f, 9000ULL); append_session(f, 10000ULL); append_end_of_stream(f); TEST_ASSERT_EQUAL_INT(0, fclose(f)); copy_file(path, dest); unlink(path); rmdir(record_dir); } /* ---------------------------------------------------------------------- */ /* Running the scripted REPL session */ /* ---------------------------------------------------------------------- */ static char g_workdir[PATH_MAX]; static int g_ran; /* Runs the built scheduler in a fresh work directory: stdin = script, * stdout = transcript, stderr kept apart (sanitizer and startup noise is * not part of the transcript). Returns the child's exit status. */ static int run_session(const char* replay_src) { char path[PATH_MAX]; pid_t pid; int status; snprintf(g_workdir, sizeof(g_workdir), "/tmp/sched_trbase_XXXXXX"); TEST_ASSERT_NOT_NULL(mkdtemp(g_workdir)); join_path(path, sizeof(path), g_workdir, REPLAY_NAME); copy_file(replay_src, path); { char src[PATH_MAX]; join_path(src, sizeof(src), XCE_BASELINE_DIR, SCRIPT_NAME); join_path(path, sizeof(path), g_workdir, SCRIPT_NAME); copy_file(src, path); } fflush(stdout); fflush(stderr); pid = fork(); TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork must succeed"); if (pid == 0) { if ((chdir(g_workdir) != 0) || (freopen(SCRIPT_NAME, "r", stdin) == NULL) || (freopen(TRANSCRIPT_NAME, "w", stdout) == NULL) || (freopen(STDERR_NAME, "w", stderr) == NULL)) { _exit(120); } setenv("TERM", "dumb", 1); execl(XCE_SCHEDULER_BIN, XCE_SCHEDULER_BIN, "--sim", REPLAY_NAME, "--interactive", "--sim-verify", "--sim-output", SIM_OUT_NAME, "--logfile", LOG_NAME, (char*)NULL); _exit(121); } TEST_ASSERT_EQUAL_INT(pid, waitpid(pid, &status, 0)); TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status), "the scheduler must exit normally"); return WEXITSTATUS(status); } /* The --sim-verify result: every mismatch line of the log, cut to start at * the message itself, which begins with the reporting function's name. * This drops the logger's prefix: the time, the thread id, the level and * the padded function-name column. */ static char* extract_verify_result(const char* log_path, long* out_len) { FILE* f = fopen(log_path, "r"); char* out = NULL; size_t cap = 0; size_t len = 0; char line[8192]; TEST_ASSERT_NOT_NULL_MESSAGE(f, "the scheduler log must exist"); out = (char*)calloc(1, 1); TEST_ASSERT_NOT_NULL(out); while (fgets(line, sizeof(line), f) != NULL) { char* start; size_t n; if (strstr(line, MISMATCH_TAG) == NULL) { continue; } start = strstr(line, VERIFY_MARK); TEST_ASSERT_NOT_NULL_MESSAGE(start, "mismatch line must name " VERIFY_MARK); n = strlen(start); if (len + n + 1 > cap) { cap = (len + n + 1) * 2; out = (char*)realloc(out, cap); TEST_ASSERT_NOT_NULL(out); } memcpy(out + len, start, n + 1); len += n; } fclose(f); *out_len = (long)len; return out; } /* Runs the session once per process; in regeneration mode first rewrites * the input replay, then copies every produced output into the fixtures. */ static void ensure_session_ran(void) { char replay[PATH_MAX]; int regen = (getenv("XCE_REPL_BASELINE_REGEN") != NULL); int rc; if (g_ran) { return; } g_ran = 1; join_path(replay, sizeof(replay), XCE_BASELINE_DIR, REPLAY_NAME); if (regen) { write_contention_replay(replay); } TEST_ASSERT_EQUAL_INT_MESSAGE(0, access(replay, R_OK), "committed contention.replay fixture " "must exist"); rc = run_session(replay); TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "the scripted session must end " "with quit's exit(0)"); if (regen) { static const char* const outs[] = { TRANSCRIPT_NAME, SIM_OUT_NAME, SET_OUT_NAME }; char from[PATH_MAX]; char to[PATH_MAX]; char* verify; long vlen; int i; for (i = 0; i < 3; i++) { join_path(from, sizeof(from), g_workdir, outs[i]); join_path(to, sizeof(to), XCE_BASELINE_DIR, outs[i]); copy_file(from, to); } join_path(from, sizeof(from), g_workdir, LOG_NAME); verify = extract_verify_result(from, &vlen); join_path(to, sizeof(to), XCE_BASELINE_DIR, VERIFY_NAME); spit(to, verify, vlen); free(verify); } } /* ---------------------------------------------------------------------- */ /* Comparisons */ /* ---------------------------------------------------------------------- */ /* Reports the first differing line of two texts, for a readable failure. */ static void assert_text_equal(const char* what, const char* exp, long elen, const char* got, long glen) { long i = 0; long line = 1; long bol = 0; char msg[1024]; while ((i < elen) && (i < glen) && (exp[i] == got[i])) { if (exp[i] == '\n') { line++; bol = i + 1; } i++; } if ((i == elen) && (i == glen)) { return; } { const char* ee = memchr(exp + bol, '\n', (size_t)(elen - bol)); const char* ge = memchr(got + bol, '\n', (size_t)(glen - bol)); int el = ee ? (int)(ee - (exp + bol)) : (int)(elen - bol); int gl = ge ? (int)(ge - (got + bol)) : (int)(glen - bol); snprintf(msg, sizeof(msg), "%s differs from the baseline at line %ld:\n" " expected: %.*s\n actual: %.*s", what, line, (el > 400) ? 400 : el, exp + bol, (gl > 400) ? 400 : gl, got + bol); } TEST_FAIL_MESSAGE(msg); } /* A recording's header fields other than recordingStartWall, plus the * offset just past the header message. */ static void read_header(const char* path, replay_header_t* hdr, long* hdr_end) { FILE* f = fopen(path, "rb"); struct capn c; ReplayHeader_ptr p; struct ReplayHeader wire; TEST_ASSERT_NOT_NULL_MESSAGE(f, path); 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); fclose(f); } static void assert_recording_equal(const char* name) { char exp_path[PATH_MAX]; char got_path[PATH_MAX]; replay_header_t eh; replay_header_t gh; long e_end; long g_end; long elen; long glen; char* eb; char* gb; join_path(exp_path, sizeof(exp_path), XCE_BASELINE_DIR, name); join_path(got_path, sizeof(got_path), g_workdir, name); eb = slurp(exp_path, &elen); TEST_ASSERT_NOT_NULL_MESSAGE(eb, "baseline recording fixture missing"); TEST_ASSERT_TRUE_MESSAGE(elen > 0, "baseline recording fixture empty"); gb = slurp(got_path, &glen); TEST_ASSERT_NOT_NULL_MESSAGE(gb, "the session must write the recording"); read_header(exp_path, &eh, &e_end); read_header(got_path, &gh, &g_end); TEST_ASSERT_EQUAL_STRING(eh.recorder_version, gh.recorder_version); TEST_ASSERT_EQUAL_UINT64(eh.recording_start_ts, gh.recording_start_ts); TEST_ASSERT_EQUAL_STRING(eh.sched_policy, gh.sched_policy); TEST_ASSERT_EQUAL_PTR(NULL, eh.cluster); TEST_ASSERT_EQUAL_PTR(NULL, gh.cluster); TEST_ASSERT_EQUAL_INT64_MESSAGE(elen - e_end, glen - g_end, "recording length after the header " "must match the baseline"); TEST_ASSERT_EQUAL_MEMORY_MESSAGE(eb + e_end, gb + g_end, (size_t)(elen - e_end), "recording bytes after the header " "must match the baseline"); free_ReplayHeader(&eh); free_ReplayHeader(&gh); free(eb); free(gb); } /* ---------------------------------------------------------------------- */ /* Tests */ /* ---------------------------------------------------------------------- */ /* The console transcript is byte-identical to the baseline (10.4, 10.5). */ static void test_transcript_matches_baseline(void) { char path[PATH_MAX]; char* exp; char* got; long elen; long glen; ensure_session_ran(); join_path(path, sizeof(path), XCE_BASELINE_DIR, TRANSCRIPT_NAME); exp = slurp(path, &elen); TEST_ASSERT_NOT_NULL_MESSAGE(exp, "baseline transcript fixture missing"); TEST_ASSERT_TRUE_MESSAGE(elen > 0, "baseline transcript fixture empty"); join_path(path, sizeof(path), g_workdir, TRANSCRIPT_NAME); got = slurp(path, &glen); TEST_ASSERT_NOT_NULL(got); assert_text_equal("transcript", exp, elen, got, glen); free(exp); free(got); } /* The baseline really covers what task 1.1 asks the session to exercise * and the contention it asks the fixture to have, so a regenerated fixture * cannot silently degrade to a trivial one. */ static void test_baseline_covers_required_commands_and_contention(void) { static const char* const needles[] = { "[sim] available commands:", /* help */ "[sim] info: usage: info <", /* info usage */ "[sim] summary: ", /* info subcommands */ "[sim] jobs:", "[sim] job 104 (0-0-0-104): state=PEND", "[sim] worker w01: status=online", "[sim] resource license:", "[sim] limit: cluster_slots", "[sim] limit: host_slots", "[sim] units set to raw", /* set units */ "[sim] units set to human", "[sim] units set to both", "resource mem: value=16.00 KB used=2.00 KB", /* human */ "resource mem: value=16384.000000 (16.00 KB)", /* both */ "[sim] queue normal: priority=1", /* info queue all */ "[sim] verify set to off", "[sim] verify set to on", "[sim] snapshot written: snap.replay", "[sim] output started: set_output.replay", /* set output */ "[sim] output stopped: set_output.replay", "last session: round=2000", /* step */ "[sim] round 2-4 (3 round(s)", /* collapsed range */ "[sim] hit breakpoint 1 @ decision: DISPATCH jobid=0-0-0-104", "[sim] hit breakpoint 2 @ BEGIN_ROUND round=7000", "[sim] replay complete.", /* contention: parallel job over both hosts, license exhausted, * cluster-wide and host-scoped limits full */ "assigned job: jobid=0-0-0-101*2", "[sim] PEND jobid=0-0-0-104", " value=2.00 used=2.00", "usage limit=cluster_slots: accounted=6.00 capacity=6.00", "usage limit=host_slots host=w01: accounted=3.00 capacity=3.00", }; char path[PATH_MAX]; char* t; long len; size_t i; ensure_session_ran(); join_path(path, sizeof(path), XCE_BASELINE_DIR, TRANSCRIPT_NAME); t = slurp(path, &len); TEST_ASSERT_NOT_NULL_MESSAGE(t, "baseline transcript fixture missing"); for (i = 0; i < sizeof(needles) / sizeof(needles[0]); i++) { TEST_ASSERT_NOT_NULL_MESSAGE(strstr(t, needles[i]), needles[i]); } free(t); } /* The --sim-output and `set output` recordings match apart from * ReplayHeader.recordingStartWall (10.4, 10.5). */ static void test_recordings_match_baseline(void) { ensure_session_ran(); assert_recording_equal(SIM_OUT_NAME); assert_recording_equal(SET_OUT_NAME); } /* The --sim-verify result matches the baseline (10.4, 10.5). */ static void test_sim_verify_result_matches_baseline(void) { char path[PATH_MAX]; char* exp; char* got; long elen; long glen; ensure_session_ran(); join_path(path, sizeof(path), XCE_BASELINE_DIR, VERIFY_NAME); exp = slurp(path, &elen); TEST_ASSERT_NOT_NULL_MESSAGE(exp, "baseline sim_verify fixture missing"); TEST_ASSERT_TRUE_MESSAGE(elen > 0, "baseline sim_verify fixture empty"); join_path(path, sizeof(path), g_workdir, LOG_NAME); got = extract_verify_result(path, &glen); assert_text_equal("--sim-verify result", exp, elen, got, glen); free(exp); free(got); } /* Removes the work directory and the files the session leaves in it. */ static void remove_workdir(void) { static const char* const names[] = { REPLAY_NAME, SCRIPT_NAME, TRANSCRIPT_NAME, SIM_OUT_NAME, SET_OUT_NAME, LOG_NAME, STDERR_NAME, SNAP_NAME }; char path[PATH_MAX]; size_t i; if (g_workdir[0] == '\0') { return; } for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) { int n = snprintf(path, sizeof(path), "%s/%s", g_workdir, names[i]); /* A truncated path names some other file: leave it alone. */ if ((n < 0) || ((size_t)n >= sizeof(path))) { continue; } unlink(path); } rmdir(g_workdir); } int main(void) { int rc; UNITY_BEGIN(); RUN_TEST(test_transcript_matches_baseline); RUN_TEST(test_baseline_covers_required_commands_and_contention); RUN_TEST(test_recordings_match_baseline); RUN_TEST(test_sim_verify_result_matches_baseline); rc = UNITY_END(); remove_workdir(); return rc; }