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>
928 lines
32 KiB
C
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;
|
|
}
|