myxce/src/scheduler/testsuite/test_repl_trace_baseline.c
Rongsong Shen 4ae68998ce
test(scheduler): silence snprintf truncation warning in repl trace baseline cleanup
remove_workdir() now checks snprintf's result and skips a truncated path
instead of unlinking whatever it happens to name.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 08:28:22 +00:00

928 lines
32 KiB
C

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