feat(scheduler-core): implement simulation-mode startup (task 11.1)

Add sim.h/sim.c implementing AC 13 steps 1-4: open+validate ReplayHeader,
locate the most recent SnapshotEntry at/before --sim-start-ts, load it via
xce_sched_core_load_snapshot(), and set the virtual clock anchors. Wire
all seven simulation CLI flags into main.c's task-5.2 mode-selection
branch. Event loop itself (step 5) deferred to task 11.2. Fixes two real
bugs found during TDD: a SnapshotEntry_get_ts() use-after-free and a
pend_updates NULL-deref in xce_sched_core_fini() for bare-core callers.
27/27 scheduler suite green, full project build clean.
This commit is contained in:
Rongsong Shen 2026-08-06 09:22:04 +00:00
parent 16d756aca3
commit 5d587ecf32
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
7 changed files with 1366 additions and 21 deletions

228
src/scheduler/include/sim.h Normal file
View file

@ -0,0 +1,228 @@
#if !defined(_SIM_H_)
#define _SIM_H_ 1
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "core.h"
/*
* sim.h - simulation-mode driver (scheduler-core spec, task 11.1, Req 5 AC
* 13/14/17). NEW file per design.md's File Structure Plan ("sim_driver.h --
* NEW (Phase 3): xce_sched_sim_driver_t"; named `sim.h`/`sim.c` here,
* matching this codebase's established one-focused-file-per-subsystem
* pattern -- driver.h/driver.c for normal mode, recording.h/recording.c for
* recording, snapshot.c for snapshot serialization).
*
* Boundary (Req 5 AC 17, "Simulation isolation"): `xce_sched_sim_driver_t`
* below contains no `xcecli_ctx_t`, no `xce_cluster_config_t`, no capnp
* socket, and no controller RPC of any kind -- confirmed by the struct
* definition itself (every field is either the pure-core `xce_sched_core_t`,
* a `FILE*`, or a scalar). Task 11.3 (not this task) builds the formal
* build-time isolation audit; this task's own obligation is simply to never
* introduce such a dependency here.
*
* This header only depends on core.h (+ stdbool/stdint/stdio) -- no
* scheduler.h, no client.h -- matching driver.h's own documented boundary
* comment for the same reason.
*/
/*
* xce_sched_sim_driver_t - design.md's SimDriver Service Interface struct,
* verbatim field-for-field:
*
* xce_sched_core_t core; -- the restored scheduling state
* FILE *replay_file; -- input .replay stream (task 11.1)
* FILE *alt_events_file; -- optional --sim-alt-events input
* (task 11.5 consumes its content;
* this task only opens it)
* FILE *record_file; -- optional --sim-output stream
* (task 11.5 writes to it; this
* task only opens it)
* uint64_t sim_speed_num/sim_speed_den -- the SAME two numbers this task
* also stamps onto
* `core.speed_num`/`core.speed_den`
* (kept here too per design.md's
* literal struct, e.g. for the
* REPL/diagnostics to read without
* reaching into `core`)
* bool verify; -- --sim-verify (task 11.4 acts on
* it; this task only stores it)
* bool interactive; -- --interactive/-i (task 12 acts on
* it; this task only stores it)
*/
typedef struct {
xce_sched_core_t core;
FILE* replay_file;
FILE* alt_events_file;
FILE* record_file;
uint64_t sim_speed_num;
uint64_t sim_speed_den;
bool verify;
bool interactive;
} xce_sched_sim_driver_t;
/*
* SCHED_SIM_START_TS_LATEST - default value for `sched_sim_args_t.sim_start_ts`
* when the caller does not pass --sim-start-ts (main.c). AC 13's own
* pseudocode ("find the most recent SnapshotEntry with ts <= sim_start_ts")
* has no documented default -- 0 would perversely mean "the earliest
* possible snapshot" and reject nearly every real (non-zero, wall-clock-
* derived) recording unless the user always passes an explicit value.
* UINT64_MAX instead means "no upper bound" -- the most recent SnapshotEntry
* in the whole file, i.e. resume simulation from wherever the recording (or
* a prior simulation's own --sim-output) left off. A deliberate, documented
* engineering decision (not specified verbatim in requirements.md/design.md),
* consistent with the AC's own filtering formula.
*/
#define SCHED_SIM_START_TS_LATEST UINT64_MAX
/*
* sched_sim_args_t - CLI arguments for simulation mode (task 11.1, Req 5 AC
* 13's "The scheduler shall support simulation mode activated by --sim FILE
* and --sim-start-ts TIMESTAMP", plus this task's own "Add the simulation-
* mode CLI flags (replay file path, start timestamp, speed, verify,
* alt-events file, output file, interactive)" bullet -- all seven listed
* there are fields below). Populated by main.c's argp parsing, passed by
* const pointer into sched_sim_driver_start()/sched_sim_driver_run().
*
* Deliberately a plain, self-contained struct -- only primitive/string
* fields, zero dependency on client.h/xce/cluster.h/scheduler.h -- so this
* header stays inside core.h's own dependency boundary (Req 5 AC 17).
* design.md's Service Interface literally writes `sched_sim_driver_run(...,
* const void* args)`, but a concrete, named struct type costs nothing extra
* here (no circular-include risk the way driver.h's `void* driver_ctx`
* idiom is solving for) and is safer -- both for this task's own test suite
* and for main.c's call site -- than an opaque void* that every caller must
* blindly cast.
*/
typedef struct {
const char* replay_path; /* --sim FILE (required) */
uint64_t sim_start_ts; /* --sim-start-ts TIMESTAMP (optional;
* default SCHED_SIM_START_TS_LATEST) */
uint64_t sim_speed; /* --sim-speed N: 0=as-fast-as-possible,
* 1=real-time, N>1=N x real-time
* (optional; default 1) */
bool verify; /* --sim-verify (task 11.4) */
const char*
alt_events_path; /* --sim-alt-events FILE (optional, task 11.5) */
const char* output_path; /* --sim-output FILE (optional, task 11.5) */
bool interactive; /* --interactive / -i (task 12) */
} sched_sim_args_t;
/*
* sched_sim_driver_start - Req 5 AC 13's five-step startup sequence, steps
* 1-4 ONLY (step 5, "enter event loop", is task 11.2's job -- see
* sched_sim_driver_run() below for the full entry point design.md
* documents). Independently exported (unlike design.md's single
* `sched_sim_driver_run()`) so this task's own Observable -- "starting
* simulation against a valid fixture file reaches a ready-to-run state" --
* is directly, narrowly unit-testable without needing task 11.2's event
* loop to exist first. Matches this codebase's established pattern of
* splitting a driver's lifecycle into independently-testable pieces (e.g.
* driver.h's xce_sched_driver_translate_pending_events()/
* xce_sched_driver_on_decision()/xce_sched_driver_run_normal_mode_round()).
*
* Step 1 (Open and validate replay file): opens `args->replay_path`, reads
* the first capnp message, and validates it is a ReplayHeader BEFORE
* touching anything else. Also threads `ReplayHeader.schedPolicy` (if
* present) onto the freshly-initialized `sim->core.job_pick_plugin` -- the
* ONLY place in the whole `.replay` stream that carries the picker-policy
* name (SnapshotData has no such field); this is simply completing the
* decode this step already performs, not separate new work. Registering a
* picker PLUGIN under that name is explicitly NOT this function's job (see
* "Known scope boundary" below).
*
* Step 2 (Locate starting snapshot): the file's SECOND message (the first
* data message after ReplayHeader) MUST itself be a SnapshotEntry -- Req 5
* AC 20's stream-ordering rule and this task's own "Reject a replay file
* whose first data message isn't a snapshot entry" requirement. If it is
* not (including the case where the file simply ends right after the
* header), this function fails immediately, before scanning any further and
* before any ReplayEvent is ever decoded. Given that check passes, this
* function then scans every remaining message to EOF, decoding every
* SnapshotEntry's timestamp (cheaply, via SnapshotEntry_get_ts(), no
* allocation) and keeping the most recent one with `ts <=
* args->sim_start_ts` as the running best candidate (materialized fully
* only when it becomes the new best). If no SnapshotEntry anywhere in the
* file qualifies, the file is rejected (AC 13: "Without initial state,
* simulation has no starting point"). `sim->replay_file`'s read position is
* left immediately after the CHOSEN SnapshotEntry's message -- ready for
* task 11.2's event loop to resume sequential reads from exactly that point
* (every message before it, if any, is already reflected in the loaded
* snapshot's state and must never be replayed a second time).
*
* Step 3 (Load snapshot and rebuild picker): xce_sched_core_load_snapshot()
* (core.h, task 8.1) is called with the chosen candidate's decoded
* SnapshotData. That function ALREADY performs the picker-state rebuild
* walk (on_queue_update per queue, then on_job_push per pending job)
* internally as documented on its own core.h doc comment -- this function
* does not duplicate that walk.
*
* Step 4 (Set virtual clock): stamps `sim->core.virtual_clock_base` to the
* chosen snapshot's own timestamp, `sim->core.real_clock_base` to a fresh
* `clock_gettime(CLOCK_MONOTONIC)` wall-clock reading taken right here (Req
* 5 AC 13's own pseudocode explicitly performs this read from the DRIVER,
* not from core.c -- core.c's "zero wall-clock calls" isolation boundary,
* test_core_isolation.c, is scoped to core.c specifically, not to driver-
* level code that legitimately seeds the core's clock anchors), and
* `sim->core.speed_num`/`speed_den` to `args->sim_speed`/1. From this point,
* `xce_core_now(&sim->core)` returns virtual time computed from the
* snapshot's own logical timestamp -- this function's own Observable.
*
* Known scope boundary: this function does NOT register any picker plugin
* into `sim->core.plugins` (builtin_fcfs and friends are only registrable
* via plugin_register(), which requires a genuine `xce_scheduler_t*` --
* `xce_sched_sim_driver_t` deliberately has no such field, Req 5 AC 17 --
* and this codebase has no core.h-scoped plugin registration entry point).
* xce_sched_core_load_snapshot()'s own documented graceful degradation
* (WARN + skip the picker-rebuild hooks when no picker resolves) keeps this
* a non-fatal gap for THIS task's Observable, which only requires reaching
* a ready-to-run state with the correct virtual clock -- not a working
* picker (task 11.2, which actually drives scheduling rounds, is where a
* resolvable picker becomes load-bearing).
*
* Returns 0 on success (`*sim` is fully populated and ready). Returns a
* negative errno on failure; `*sim` is left in an indeterminate state, and
* IN THAT CASE ONLY, all resources this function itself opened (files,
* `sim->core`) have already been released before it returns -- the caller
* must not call anything else on `*sim` and must not fclose()/finalize it
* again itself. -EINVAL for NULL `sim`/`args`/`args->replay_path`; -ENOENT/
* -EACCES/etc. (errno passthrough) for an unopenable replay/alt-events/
* output file; -EIO for an empty/unreadable/malformed first capnp message;
* -EINVAL for a first message that is not ReplayHeader-shaped, or a first
* data message that is not SnapshotEntry-shaped, or a file with no
* SnapshotEntry anywhere satisfying `ts <= args->sim_start_ts`.
*/
extern int sched_sim_driver_start(xce_sched_sim_driver_t* sim,
const sched_sim_args_t* args);
/*
* sched_sim_driver_run - design.md's literal SimDriver Service Interface
* entry point (`int sched_sim_driver_run(xce_sched_sim_driver_t*, const
* void* args)`, adapted to take the concrete sched_sim_args_t* this header
* defines rather than an opaque void* -- see sched_sim_args_t's own doc
* comment for why). This is what main.c's mode-selection branch calls when
* `--sim` is present (design.md's own Integration note).
*
* Calls sched_sim_driver_start() above (task 11.1's own work, this task's
* Observable) then -- since task 11.2's event loop (Req 5 AC 13 step 5 /
* AC 14/18) does not exist yet -- fails loudly with -ENOSYS rather than
* silently returning success having processed zero events. This is the
* same "honest, temporary placeholder" precedent main.c's own pre-task-11.1
* `--sim` stub already established (task 5.2): startup is now genuinely
* real, working behavior; only the event loop remains a documented gap for
* task 11.2 to fill in, without requiring another main.c edit when it does
* (task 11.2 only needs to extend THIS function's body, after the
* sched_sim_driver_start() call, with the real event loop).
*
* Returns 0 (once task 11.2 lands) or a negative errno: whatever
* sched_sim_driver_start() returned on a startup failure, or -ENOSYS if
* startup succeeded but the event loop is not yet implemented.
*/
extern int sched_sim_driver_run(xce_sched_sim_driver_t* sim,
const sched_sim_args_t* args);
#endif

View file

@ -14,6 +14,7 @@ scheduler_srcs = [
'src/recording.c',
'src/recording_events.c',
'src/recording_session.c',
'src/sim.c',
'src/schedobj.c',
'src/plugin.c',
'src/resource.c',

View file

@ -1,4 +1,5 @@
#include <argp.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
@ -12,6 +13,7 @@
#include "driver.h"
#include "scheduler.h"
#include "sim.h"
#include "resreq.h"
#include "utils.h"
@ -28,27 +30,59 @@ const char* argp_program_bug_address = BUG_EMAIL;
* short-option letters above -- same convention as the controller's
* main.c, e.g. its OPT_PEERS). */
#define OPT_RECORD_DIR 256
/* Req 5 AC 13 (scheduler-core spec, task 11.1): the remaining simulation-
* mode CLI flags task 5.2's own --sim option left unparsed ("Phase 3 -- not
* yet implemented"). --sim-verify and --interactive are boolean flags (no
* argument); --interactive also gets the short letter -i (unused until
* now). */
#define OPT_SIM_START_TS 257
#define OPT_SIM_SPEED 258
#define OPT_SIM_VERIFY 259
#define OPT_SIM_ALT_EVENTS 260
#define OPT_SIM_OUTPUT 261
struct arguments {
char* logfile;
int verbose;
char* peers; /* --peers value (optional) */
char* sim_file; /* --sim value (optional; Phase 3, not yet implemented) */
char* sim_file; /* --sim value (optional; selects simulation mode) */
char* record_dir; /* --record-dir value (optional; Req 5 AC 10, task 9.1) */
/* Req 5 AC 13 (scheduler-core spec, task 11.1): the rest of simulation
* mode's CLI surface, all optional except --sim itself. Raw strings
* here, same as sim_file/record_dir above -- numeric/bool
* interpretation happens in main() after argp_parse() returns (see
* sched_sim_args_t's own field comments, sim.h, for each flag's
* meaning/default). */
char* sim_start_ts; /* --sim-start-ts TIMESTAMP */
char* sim_speed; /* --sim-speed N */
int sim_verify; /* --sim-verify */
char* sim_alt_events; /* --sim-alt-events FILE */
char* sim_output; /* --sim-output FILE */
int interactive; /* --interactive / -i */
};
static struct argp_option options[] = {
{ "logfile", 'l', "FILE", 0, "The full path of log file" },
{ "verbose", 'v', 0, 0, "Produce verbose log" },
{ "peers", 'P', "LIST", 0, "HA candidate list: [name=]host:port,..." },
/* Req 5 AC 9 "Mode selection" / "CLI flags" (scheduler-core spec, task
* 5.2): --sim FILE selects the simulation driver. Parsing only, as of
* this task -- SimDriver itself is Phase 3 (design.md's File Structure
* Plan marks sim_driver.c/sim_driver.h "NEW (Phase 3)"); see main()'s
* mode-selection branch below for the loud not-yet-implemented failure. */
{ "sim", 's', "FILE", 0,
"Replay FILE for simulation mode "
"(Phase 3 -- not yet implemented)" },
/* Req 5 AC 9 "Mode selection" / Req 5 AC 13 "Simulation mode startup"
* (scheduler-core spec, tasks 5.2/11.1): --sim FILE selects the
* simulation driver; the flags below configure it. */
{ "sim", 's', "FILE", 0, "Replay FILE for simulation mode" },
{ "sim-start-ts", OPT_SIM_START_TS, "TIMESTAMP", 0,
"Simulation start timestamp (ns since epoch); default: the replay "
"file's most recent snapshot" },
{ "sim-speed", OPT_SIM_SPEED, "N", 0,
"Simulation speed: 0=as-fast-as-possible, 1=real-time (default), "
"N>1=N x real-time" },
{ "sim-verify", OPT_SIM_VERIFY, 0, 0,
"Compare simulated decisions against the replay file's recorded ones" },
{ "sim-alt-events", OPT_SIM_ALT_EVENTS, "FILE", 0,
"Replay events from FILE instead of the base --sim file (what-if "
"branching)" },
{ "sim-output", OPT_SIM_OUTPUT, "FILE", 0,
"Write simulated decisions to FILE as a .replay stream" },
{ "interactive", 'i', 0, 0, "Open the simulation REPL" },
/* Req 5 AC 10 "Record file path validation" (scheduler-core spec, task
* 9.1): the ONLY way record_dir is ever provisioned -- never settable
* via any request-path field (SchedAdminRequest included). Parsed
@ -76,6 +110,24 @@ static error_t parse_opt(int key, char* arg, struct argp_state* state)
case 's':
arguments->sim_file = arg;
break;
case OPT_SIM_START_TS:
arguments->sim_start_ts = arg;
break;
case OPT_SIM_SPEED:
arguments->sim_speed = arg;
break;
case OPT_SIM_VERIFY:
arguments->sim_verify = 1;
break;
case OPT_SIM_ALT_EVENTS:
arguments->sim_alt_events = arg;
break;
case OPT_SIM_OUTPUT:
arguments->sim_output = arg;
break;
case 'i':
arguments->interactive = 1;
break;
case OPT_RECORD_DIR:
arguments->record_dir = arg;
break;
@ -158,18 +210,79 @@ int main(int argc, char* argv[])
mylog_init(logfile, loglevel, logsize, logcount);
/* Req 5 AC 9 "Mode selection" (scheduler-core spec, task 5.2): --sim
* selects the simulation driver (SimDriver). Not yet implemented as of
* this task (Phase 3 -- design.md's File Structure Plan explicitly
* marks sim_driver.c/sim_driver.h "NEW (Phase 3)"; sched_sim_driver_run()
* does not exist). Fail LOUDLY and immediately rather than silently
* no-op'ing or crashing -- an honest, temporary placeholder for Phase 3
* to fill in. Checked before any network/config setup below: there is
* no reason to pay for a controller connection attempt on a path that
* is about to fail. */
/* Req 5 AC 9 "Mode selection" / Req 5 AC 13 "Simulation mode startup"
* (scheduler-core spec, task 11.1): --sim selects the simulation driver
* (SimDriver) instead of the normal, controller-attached one. Checked
* before any network/config setup below -- Req 5 AC 17 "Simulation
* isolation" requires the scheduler process establish NO connection to
* the controller in simulation mode, so this branch must both come
* before, and always `return` out ahead of, every sched_init()/
* xce_client_config_t call further down (never falls through). */
if (arguments.sim_file != NULL) {
fprintf(stderr, "xce_scheduler: --sim mode not yet implemented\n");
return 1;
sched_sim_args_t sim_args;
xce_sched_sim_driver_t sim;
uint64_t sim_start_ts = SCHED_SIM_START_TS_LATEST;
uint64_t sim_speed = 1; /* default: real-time */
int rc;
if (arguments.sim_start_ts != NULL) {
char* endp = NULL;
sim_start_ts = strtoull(arguments.sim_start_ts, &endp, 10);
if ((endp == NULL) || (*endp != '\0')
|| (endp == arguments.sim_start_ts)) {
fprintf(stderr,
"xce_scheduler: --sim-start-ts expects a numeric "
"timestamp, got \"%s\"\n",
arguments.sim_start_ts);
return 1;
}
}
if (arguments.sim_speed != NULL) {
char* endp = NULL;
sim_speed = strtoull(arguments.sim_speed, &endp, 10);
if ((endp == NULL) || (*endp != '\0')
|| (endp == arguments.sim_speed)) {
fprintf(stderr,
"xce_scheduler: --sim-speed expects a numeric "
"value, got \"%s\"\n",
arguments.sim_speed);
return 1;
}
}
memset(&sim_args, 0, sizeof(sim_args));
sim_args.replay_path = arguments.sim_file;
sim_args.sim_start_ts = sim_start_ts;
sim_args.sim_speed = sim_speed;
sim_args.verify = (arguments.sim_verify != 0);
sim_args.alt_events_path = arguments.sim_alt_events;
sim_args.output_path = arguments.sim_output;
sim_args.interactive = (arguments.interactive != 0);
rc = sched_sim_driver_run(&sim, &sim_args);
if (rc == -ENOSYS) {
/* Task 11.2 (scheduler-core spec, Req 5 AC 14/18) owns the
* event loop -- not yet implemented. Startup itself (this
* task's own Observable) genuinely succeeded: sim.core holds
* the fully restored, ready-to-run state and virtual clock.
* Same "honest, temporary placeholder" precedent this stub
* previously used for the whole --sim flag. */
fprintf(stderr,
"xce_scheduler: --sim startup complete (virtual clock "
"at ts=%llu); event loop not yet implemented\n",
(unsigned long long)xce_core_now(&(sim.core)));
return 1;
}
if (rc < 0) {
fprintf(stderr, "xce_scheduler: --sim startup failed: %s\n",
strerror(-rc));
return 1;
}
return 0;
}
/* P2.5-6: populate and validate xce_cluster_config_t when --peers given */

450
src/scheduler/src/sim.c Normal file
View file

@ -0,0 +1,450 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "controller.capnp.h"
#include "sim.h"
#include "utils.h"
#include "xstring.h"
/*
* sim.c - simulation-mode driver startup (scheduler-core spec, task 11.1,
* Req 5 AC 13/17/20). See sim.h for the full per-function contract. This
* file implements Req 5 AC 13's five-step startup sequence, steps 1-4 only
* (step 5, the event loop, is task 11.2's job -- sched_sim_driver_run()
* below fails loudly with -ENOSYS after a successful startup, rather than
* silently no-op'ing).
*
* Message-type discrimination (Req 5 AC 20): a `.replay` stream has no
* self-describing type tag for any message -- position alone pins down the
* first two messages (ReplayHeader, then SnapshotEntry), and c-capnproto's
* decode functions do not themselves distinguish "this struct is the wrong
* type" (decoding a SnapshotEntry's bytes as a ReplayHeader would silently
* "succeed" with garbage field values). What DOES reliably work -- verified
* empirically by the xceadmin `scheduler dump` command (task 10.4,
* xceadmin/scheduler.py's own module-section docstring) against the SAME
* schema -- is that each of the four top-level `.replay` message types has
* a distinct (dataWordCount, pointerCount) struct shape, encoded directly
* in the message's own root struct pointer, independent of whatever schema
* later interprets its fields: ReplayHeader=(1,5), SnapshotEntry=(1,1),
* ReplayEvent=(2,1), ReplaySession=(1,2) (controller.capnp.h's own
* generated `<Type>_word_count`/`<Type>_pointer_count` constants). This
* file uses the exact same shape-check technique, at the c-capnproto C API
* level (capn_ptr.datasz/.ptrs after capn_getp(capn_root(&c), 0, 1) --
* every message in this codebase's convention stores its real payload at
* pointer slot 0 of the implicit root struct, matching recording.c's
* write_capnp_message() callers, which all `capn_setp(capn_root(&c), 0,
* ptr.p)`) rather than raw byte-peeking (unnecessary in C: capn_init_fp()
* already parses the stream framing for us).
*/
/*
* msg_root_has_shape - true iff `c`'s decoded root payload (pointer slot 0
* of the implicit root struct) is a CAPN_STRUCT with exactly `want_words`
* data words and `want_ptrs` pointers. On a match, `*out` receives the
* resolved capn_ptr (ready to wrap in the caller's own `<Type>_ptr`). Never
* allocates; safe to call speculatively before deciding whether to fully
* decode a message.
*/
static int msg_root_has_shape(struct capn* c, capn_ptr* out, size_t want_words,
size_t want_ptrs)
{
capn_ptr p = capn_getp(capn_root(c), 0, 1);
if (p.type != CAPN_STRUCT) {
return 0;
}
if (((size_t)(p.datasz / 8) != want_words)
|| ((size_t)p.ptrs != want_ptrs)) {
return 0;
}
if (out != NULL) {
*out = p;
}
return 1;
}
/*
* sim_open_and_validate_header - Req 5 AC 13 Step 1. Opens `path`, decodes
* the first capnp message, and validates it is ReplayHeader-shaped. On
* success, `sim->replay_file` is set (left open, positioned right after the
* header, ready for the Step-2 scan below) and `*out_sched_policy` receives
* a fresh heap copy of `ReplayHeader.schedPolicy` (NULL if the field is
* empty/absent) -- see sim.h's own doc comment on sched_sim_driver_start()
* for why this is threaded through here rather than left for a later step.
*/
static int sim_open_and_validate_header(xce_sched_sim_driver_t* sim,
const char* path,
char** out_sched_policy)
{
FILE* f;
struct capn c;
capn_ptr hdr_root;
ReplayHeader_ptr hdr;
struct ReplayHeader wire;
replay_header_t decoded;
*out_sched_policy = NULL;
f = fopen(path, "r");
if (f == NULL) {
int rc = (errno > 0) ? -errno : -EIO;
ERROR("%s cannot open replay file '%s': %s", __func__, path,
strerror(errno));
return rc;
}
if (capn_init_fp(&c, f, 0) != 0) {
ERROR("%s replay file '%s' is empty, unreadable, or its first "
"message is malformed",
__func__, path);
fclose(f);
return -EIO;
}
if (!msg_root_has_shape(&c, &hdr_root, ReplayHeader_word_count,
ReplayHeader_pointer_count)) {
ERROR("%s replay file '%s': first message is not a valid "
"ReplayHeader (wrong wire shape)",
__func__, path);
capn_free(&c);
fclose(f);
return -EINVAL;
}
hdr.p = hdr_root;
read_ReplayHeader(&wire, hdr);
memset(&decoded, 0, sizeof(decoded));
decode_ReplayHeader(&decoded, &wire);
if ((decoded.sched_policy != NULL) && (decoded.sched_policy[0] != '\0')) {
*out_sched_policy = makeString(decoded.sched_policy);
}
free_ReplayHeader(&decoded);
capn_free(&c);
sim->replay_file = f;
return 0;
}
/*
* sim_locate_and_load_snapshot - Req 5 AC 13 Step 2 + Step 3. Requires
* `sim->replay_file` to already be open and positioned right after the
* ReplayHeader (sim_open_and_validate_header() above). See sim.h's own doc
* comment on sched_sim_driver_start() for the full algorithm and the
* ownership reasoning behind never freeing the CHOSEN candidate.
*/
static int sim_locate_and_load_snapshot(xce_sched_sim_driver_t* sim,
uint64_t sim_start_ts,
uint64_t* out_snapshot_ts)
{
FILE* f = sim->replay_file;
struct capn c;
capn_ptr snap_root;
SnapshotEntry_ptr sp;
replay_snapshot_entry_t best;
int have_best = 0;
uint64_t best_ts = 0;
long resume_at = -1;
int n_seen = 0;
int first = 1;
int rc;
memset(&best, 0, sizeof(best));
for (;;) {
uint64_t ts;
rc = capn_init_fp(&c, f, 0);
if (rc != 0) {
/* EOF (or malformed trailing data, treated identically -- same
* "nonzero means no more messages" convention
* test_record_lifecycle.c's own capn_init_fp() end-of-stream
* assertions already establish). */
if (first) {
ERROR("%s replay file has no data messages after "
"ReplayHeader (missing initial SnapshotEntry, Req 5 "
"AC 20 violation)",
__func__);
return -EINVAL;
}
break;
}
if (!msg_root_has_shape(&c, &snap_root, SnapshotEntry_word_count,
SnapshotEntry_pointer_count)) {
if (first) {
/* The file's SECOND message (the first data message after
* ReplayHeader) MUST itself be a SnapshotEntry -- Req 5 AC
* 20's stream-ordering rule and this task's own "reject a
* replay file whose first data message isn't a snapshot
* entry" requirement. Fail immediately here, before ANY
* ReplayEvent is ever decoded -- do not fall through to the
* general scan below. */
ERROR("%s first data message after ReplayHeader is not a "
"SnapshotEntry (Req 5 AC 20 violation) -- rejecting "
"replay file",
__func__);
capn_free(&c);
return -EINVAL;
}
/* Not the first message, and not SnapshotEntry-shaped --
* ReplayEvent/ReplaySession/unrecognized. Task 11.1's own scan
* only cares about locating a starting snapshot; ignore and
* keep scanning. */
capn_free(&c);
first = 0;
continue;
}
first = 0;
n_seen++;
sp.p = snap_root;
ts = SnapshotEntry_get_ts(sp);
if (ts <= sim_start_ts) {
struct SnapshotEntry entry_wire;
replay_snapshot_entry_t decoded;
read_SnapshotEntry(&entry_wire, sp);
memset(&decoded, 0, sizeof(decoded));
decode_SnapshotEntry(&decoded, &entry_wire);
capn_free(&c);
/* SnapshotEntry timestamps are monotonically non-decreasing
* across a well-formed `.replay` stream (both normal-mode
* recording and simulation --sim-output derive ts from
* xce_core_now(), which never runs backwards) -- so any
* qualifying candidate found later in the scan is always at
* least as recent as the current best. Freeing the OLD best
* here is always safe: xce_sched_core_load_snapshot() is only
* ever called once, below, on whichever candidate survives to
* the end of this loop -- an intermediate candidate that gets
* replaced was never passed to it, so nothing aliases its
* memory yet. */
if (have_best) {
free_SnapshotEntry(&best);
}
best = decoded;
have_best = 1;
best_ts = ts;
resume_at = ftell(f);
} else {
capn_free(&c);
}
}
if (!have_best) {
ERROR("%s no SnapshotEntry with ts <= %llu found (%d snapshot "
"entries seen in file) -- replay file has no usable starting "
"point",
__func__, (unsigned long long)sim_start_ts, n_seen);
return -EINVAL;
}
if (fseek(f, resume_at, SEEK_SET) != 0) {
rc = (errno > 0) ? -errno : -EIO;
/* best.config is about to be handed to xce_sched_core_load_snapshot()
* in the success path -- but we are failing before that call ever
* happens, so best's contents are still wholly ours to free here
* (nothing aliases them yet). */
free_SnapshotEntry(&best);
return rc;
}
/* Step 3: xce_sched_core_load_snapshot() (core.h, task 8.1) already
* performs the picker-rebuild walk (on_queue_update per queue, then
* on_job_push per pending job) internally -- see sim.h's own doc
* comment on sched_sim_driver_start() Step 3. Not duplicated here. */
rc = xce_sched_core_load_snapshot(&(sim->core), best.data);
if (rc < 0) {
ERROR("%s xce_sched_core_load_snapshot() failed: rc=%d", __func__, rc);
free_SnapshotEntry(&best);
return rc;
}
/* `best` (specifically best.data->config) is NOT freed on success:
* xce_sched_core_load_snapshot() aliases `core->config = in->config`
* (a borrowed-reference assignment, not a deep copy -- core.h/
* snapshot_load.c's own documented "config -- borrowed reference"
* choice) -- core->config now points at the SAME memory as
* best.data->config. Freeing `best` here would immediately dangle
* `sim->core.config`. Same "no free-snapshot function, deliberate
* accepted leak of the non-aliased remainder" precedent
* recording.c's write_snapshot_entry() already documents for the
* identical save-side ownership shape (detect_leaks=0 across this
* whole test suite, by design). */
/* Read from `best_ts` (captured while the message's own capn arena was
* still live), never by re-reading `sp` here -- the capn_free(&c) call
* above (for whichever scan iteration produced `best`) already freed
* the arena `sp` points into; re-reading through it here would be a
* use-after-free. */
*out_snapshot_ts = best_ts;
return 0;
}
/*
* sim_set_virtual_clock - Req 5 AC 13 Step 4. See sim.h's own doc comment
* on sched_sim_driver_start() for why this driver-level clock_gettime()
* call does not violate core.c's own "zero wall-clock calls" isolation
* boundary (test_core_isolation.c) -- that boundary is scoped to core.c
* itself, not to driver-level code legitimately seeding the core's clock
* anchors, exactly as AC 13's own pseudocode specifies.
*/
static void sim_set_virtual_clock(xce_sched_sim_driver_t* sim,
uint64_t snapshot_ts, uint64_t sim_speed)
{
struct timespec ts;
uint64_t real_clock_base_ns;
clock_gettime(CLOCK_MONOTONIC, &ts);
real_clock_base_ns
= ((uint64_t)ts.tv_sec * 1000000000ULL) + (uint64_t)ts.tv_nsec;
atomic_store_explicit(&(sim->core.virtual_clock_base), snapshot_ts,
memory_order_release);
atomic_store_explicit(&(sim->core.real_clock_base), real_clock_base_ns,
memory_order_release);
atomic_store_explicit(&(sim->core.speed_num), sim_speed,
memory_order_release);
atomic_store_explicit(&(sim->core.speed_den), 1, memory_order_release);
sim->sim_speed_num = sim_speed;
sim->sim_speed_den = 1;
}
int sched_sim_driver_start(xce_sched_sim_driver_t* sim,
const sched_sim_args_t* args)
{
char* sched_policy = NULL;
uint64_t snapshot_ts = 0;
int rc;
if ((sim == NULL) || (args == NULL) || (args->replay_path == NULL)) {
return -EINVAL;
}
memset(sim, 0, sizeof(*sim));
rc = xce_sched_core_init(&(sim->core));
if (rc < 0) {
return rc;
}
/* xce_sched_core_init() deliberately leaves core->pend_updates[0]/[1]
* NULL (core.c's own file header comment: their dataq_init() killer
* callback stays driver-owned) -- but xce_sched_core_fini()
* unconditionally dataq_fini()s both, so ANY caller of
* xce_sched_core_init() must allocate+dataq_init() them before the core
* can safely be torn down, matching the exact pattern scheduler.c's
* sched_init() (normal mode) and every core.c-including test fixture in
* this directory already establish. Simulation mode never actually
* enqueues anything into these queues (task 11.2's event loop
* translates .replay ReplayEvent messages directly into
* xce_core_event_t batches and calls xce_core_push_events() itself, Req
* 5 AC 18 -- it bypasses pend_updates entirely, unlike the normal-mode
* driver's xce_sched_driver_translate_pending_events()) -- a NULL
* killer is therefore correct here, same choice the test fixtures make
* for the same reason. */
sim->core.pend_updates[0] = (dataq_t*)calloc(1, sizeof(dataq_t));
dataq_init(sim->core.pend_updates[0], NULL);
sim->core.pend_updates[1] = (dataq_t*)calloc(1, sizeof(dataq_t));
dataq_init(sim->core.pend_updates[1], NULL);
rc = sim_open_and_validate_header(sim, args->replay_path, &sched_policy);
if (rc < 0) {
xce_sched_core_fini(&(sim->core));
return rc;
}
/* See sim.h's doc comment on sched_sim_driver_start() Step 1 / "Known
* scope boundary": this only threads the picker NAME through -- no
* plugin is registered under it, so
* xce_sched_core_load_snapshot()'s own internal
* sched_resolve_job_picker() call may still legitimately WARN and skip
* the picker rebuild below. Ownership transfers to sim->core (freed by
* xce_sched_core_fini(), same as job_pick_plugin's normal-mode
* lifecycle). */
if (sched_policy != NULL) {
sim->core.job_pick_plugin = sched_policy;
}
rc = sim_locate_and_load_snapshot(sim, args->sim_start_ts, &snapshot_ts);
if (rc < 0) {
fclose(sim->replay_file);
sim->replay_file = NULL;
xce_sched_core_fini(&(sim->core));
return rc;
}
sim_set_virtual_clock(sim, snapshot_ts, args->sim_speed);
sim->verify = args->verify;
sim->interactive = args->interactive;
if (args->alt_events_path != NULL) {
sim->alt_events_file = fopen(args->alt_events_path, "r");
if (sim->alt_events_file == NULL) {
rc = (errno > 0) ? -errno : -EIO;
ERROR("%s cannot open --sim-alt-events file '%s': %s", __func__,
args->alt_events_path, strerror(errno));
fclose(sim->replay_file);
sim->replay_file = NULL;
xce_sched_core_fini(&(sim->core));
return rc;
}
}
if (args->output_path != NULL) {
sim->record_file = fopen(args->output_path, "w");
if (sim->record_file == NULL) {
rc = (errno > 0) ? -errno : -EIO;
ERROR("%s cannot open --sim-output file '%s': %s", __func__,
args->output_path, strerror(errno));
if (sim->alt_events_file != NULL) {
fclose(sim->alt_events_file);
sim->alt_events_file = NULL;
}
fclose(sim->replay_file);
sim->replay_file = NULL;
xce_sched_core_fini(&(sim->core));
return rc;
}
}
return 0;
}
int sched_sim_driver_run(xce_sched_sim_driver_t* sim,
const sched_sim_args_t* args)
{
int rc = sched_sim_driver_start(sim, args);
if (rc < 0) {
return rc;
}
/* Task 11.2 (scheduler-core spec, Req 5 AC 14/18) owns the event loop --
* not yet implemented. Startup (task 11.1, this file's own work) is
* genuinely complete and correct up to this point: sim->core holds the
* fully restored, ready-to-run state and virtual clock. Fail loudly
* rather than silently returning success having processed zero events
* -- same "honest, temporary placeholder" precedent main.c's own
* pre-task-11.1 --sim stub already established (task 5.2). */
ERROR("%s simulation startup complete but the event loop is not yet "
"implemented (scheduler-core spec task 11.2)",
__func__);
return -ENOSYS;
}

View file

@ -458,6 +458,27 @@ sched_admin_request_test = executable(
dependencies: [scheduler_test_deps],
install: false)
# Contract test for simulation-mode startup (scheduler-core spec, task 11.1,
# Req 5 AC 13): opening and validating the replay file (first message must
# be ReplayHeader, first data message must be SnapshotEntry), locating the
# most recent SnapshotEntry at or before --sim-start-ts, loading it via the
# real xce_sched_core_load_snapshot(), and setting the virtual clock's
# anchors from the loaded snapshot's timestamp and requested speed. Same
# white-box #include pattern as sched_snapshot_save_load_test (../src/core.c,
# ../src/core_clock.c, ../src/snapshot.c, ../src/snapshot_wire.c,
# ../src/snapshot_load.c) PLUS ../src/recording.c (used only to build real
# `.replay` fixtures via sched_record_start()/sched_record_stop(), not under
# test here) PLUS the new ../src/sim.c under test. No driver.c/plugin.c/
# core_events.c/core_session.c/decision.c: this task stops at "ready-to-run"
# (task 11.2 owns the event loop), so no real scheduling session is ever
# driven.
sched_sim_startup_test = executable(
'sched_sim_startup_test',
['test_sim_startup.c'],
include_directories:[incdir],
dependencies: [scheduler_test_deps],
install: false)
# Register as meson tests so `meson test --suite scheduler` runs them all.
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
test('scheduler test', scheduler_test, suite: 'scheduler',
@ -527,3 +548,6 @@ test('scheduler record lifecycle test',
test('scheduler admin request test',
sched_admin_request_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])
test('scheduler sim startup test',
sched_sim_startup_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -0,0 +1,528 @@
/*
* test_sim_startup.c - contract test for simulation-mode startup
* (scheduler-core spec, task 11.1, Req 5 AC 13).
*
* Observable under test (task 11.1's own text):
* - starting simulation against a valid fixture file reaches a
* ready-to-run state with the virtual clock reporting the snapshot's
* timestamp.
* - starting against a file missing its initial snapshot entry fails with
* a clear error before any event is processed.
*
* Fixture strategy: rather than hand-constructing capnp bytes, this file
* builds REAL, valid `.replay` fixtures via the SAME production code path
* normal-mode recording already uses (sched_record_start()/
* sched_record_stop(), task 9.2) against a small real xce_scheduler_t
* fixture -- same technique test_record_lifecycle.c already establishes.
* The "missing initial snapshot entry" fixture is then derived from a valid
* one by truncating it to exactly its first message (the ReplayHeader),
* discovered by decoding that one message and reading the resulting file
* offset -- no hand-rolled capnp byte construction needed for that case
* either. The "first message is not a valid ReplayHeader" and "first data
* message is not a SnapshotEntry" fixtures DO hand-construct one wrong-
* shaped message directly (same technique test_e2e_capnp.c/
* test_record_lifecycle.c already use for building capnp messages by hand),
* since there is no production code path that ever writes a malformed
* stream to reuse.
*
* White-box #include of ../src/core.c, ../src/core_clock.c (task 11.1's own
* code under test, sim.c, needs both -- xce_sched_core_init/fini and
* xce_core_now()) PLUS ../src/snapshot.c, ../src/snapshot_wire.c,
* ../src/snapshot_load.c (task 8.1's real save/load, needed both to BUILD
* the fixture via sched_record_start() and to exercise sim.c's own
* xce_sched_core_load_snapshot() call) PLUS ../src/recording.c (real
* sched_record_start()/sched_record_stop(), used ONLY to build fixtures --
* this file never asserts anything about recording.c's own behavior, that
* is test_record_lifecycle.c's job) PLUS ../src/sim.c (the code under
* test). No driver.c/plugin.c/core_events.c/core_session.c/decision.c: this
* file never drives a real scheduling session (task 11.1 stops at
* "ready-to-run", not the event loop), so none of those are needed.
*
* Stubs below mirror the exact same link-satisfying set
* test_record_lifecycle.c/test_driver_session_ordering.c already establish
* for this core.c/core_clock.c/snapshot.c/snapshot_wire.c/snapshot_load.c/
* recording.c combination (kill_sched_*, sched_resolve_job_picker's real
* reimplementation -- schedobj.c is not linked).
*/
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "controller.capnp.h"
#include "kvmgr.h"
#include "scheduler.h"
#include "xstring.h"
/* Pull in the real functions under test into this TU (white-box #include,
* same convention as test_record_lifecycle.c). */
#include "../src/core.c"
#include "../src/core_clock.c"
#include "../src/recording.c"
#include "../src/sim.c"
#include "../src/snapshot.c"
#include "../src/snapshot_load.c"
#include "../src/snapshot_wire.c"
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; }
/* plugin.c is not linked into this TU (no session is ever driven, so no
* real plugin unregistration ever happens) -- same stub every other
* core.c-including test file in this directory establishes for
* xce_sched_core_init()'s kvmgr datakiller. */
void plugin_unregister(void* arg) { (void)arg; }
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* rationale/algorithm as test_record_lifecycle.c's own copy (schedobj.c is
* not linked). Both the FIXTURE core (built via a real xce_scheduler_t,
* needs a resolvable picker for sched_record_start()'s own snapshot to be
* well-formed) and sim.c's own core (which never registers a picker, per
* sim.h's documented scope boundary -- this will legitimately return
* -EINVAL/-ENOENT for that core, which xce_sched_core_load_snapshot()
* gracefully WARNs and skips) share this one implementation. */
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 setUp(void) { return; }
void tearDown(void) { return; }
/* ---------------------------------------------------------------------- */
/* Fixture helpers */
/* ---------------------------------------------------------------------- */
static void make_tmp_dir(char* out, size_t out_len, const char* suffix)
{
char tmpl[PATH_MAX];
snprintf(tmpl, sizeof(tmpl), "/tmp/sched_simstart%s_XXXXXX", suffix);
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
TEST_ASSERT_TRUE(strlen(tmpl) < out_len);
strcpy(out, tmpl);
}
static int stub_picker_begin_session(void* picker)
{
(void)picker;
return 0;
}
static xce_sched_job_t* stub_picker_next_job(void* picker)
{
(void)picker;
return NULL;
}
static int stub_picker_end_session(void* picker)
{
(void)picker;
return 0;
}
static int stub_picker_jobid_noop(void* picker, uint64_t jobid)
{
(void)picker;
(void)jobid;
return 0;
}
static int stub_picker_queue_update_noop(void* picker, xce_sched_queue_t* q)
{
(void)picker;
(void)q;
return 0;
}
static int stub_picker_job_noop(void* picker, xce_sched_job_t* job)
{
(void)picker;
(void)job;
return 0;
}
static int stub_picker_job_state_change_noop(void* picker, xce_sched_job_t* job,
uint32_t state)
{
(void)picker;
(void)job;
(void)state;
return 0;
}
static xce_job_picker_iface_t g_stub_picker_iface
= { .init = NULL,
.fini = NULL,
.begin_session = stub_picker_begin_session,
.next_job = stub_picker_next_job,
.end_session = stub_picker_end_session,
.on_dispatch = stub_picker_jobid_noop,
.on_pend = stub_picker_jobid_noop,
.on_queue_update = stub_picker_queue_update_noop,
.on_job_push = stub_picker_job_noop,
.on_job_state_change = stub_picker_job_state_change_noop,
.on_job_update = stub_picker_job_noop,
.on_config = NULL,
.on_job_rusage = NULL };
static xce_sched_plugin_t g_stub_picker_plugin
= { .name = "test_sim_startup_picker",
.picker_iface = &g_stub_picker_iface };
/* Builds a small, real xce_scheduler_t fixture (same pattern
* test_record_lifecycle.c's fixture_init() establishes) purely to drive
* sched_record_start()/sched_record_stop() and produce a REAL, valid
* `.replay` file this test's OWN code under test (sim.c) then consumes. */
static void build_fixture_scheduler(xce_scheduler_t* sched, char* record_dir)
{
int rc;
memset(sched, 0, sizeof(*sched));
mylock_init(&(sched->record_lock));
rc = xce_sched_core_init(&(sched->core));
TEST_ASSERT_EQUAL_INT(0, rc);
lock(sched->core.plugins, RTTI_LOCK_WRITE);
rc = key_add(sched->core.plugins, (void*)g_stub_picker_plugin.name,
(void*)&g_stub_picker_plugin);
TEST_ASSERT_GREATER_OR_EQUAL(0, rc);
unlock(sched->core.plugins);
sched->core.job_pick_plugin = g_stub_picker_plugin.name;
sched->record_dir = record_dir;
}
/* Produces a valid `.replay` fixture (ReplayHeader + initial SnapshotEntry)
* at `out_path` via the real sched_record_start()/sched_record_stop(); the
* chosen snapshot timestamp is returned via `out_ts`. */
static void write_valid_fixture(char* out_path, size_t out_path_len,
uint64_t* out_ts, const char* suffix)
{
char record_dir[PATH_MAX];
xce_scheduler_t sched;
int rc;
make_tmp_dir(record_dir, sizeof(record_dir), suffix);
build_fixture_scheduler(&sched, record_dir);
rc = sched_record_start(&sched, "fixture.replay");
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_path_len);
strcpy(out_path, sched.record_file_path);
*out_ts = sched.record_start_ts;
rc = sched_record_stop(&sched);
TEST_ASSERT_EQUAL_INT(0, rc);
}
/* Truncates `src_path` down to just its first capnp message (the
* ReplayHeader) and writes the result to `dst_path` -- simulates a replay
* file whose stream ends immediately after the header, i.e. "missing
* initial snapshot entry". */
static void truncate_to_header_only(const char* src_path, const char* dst_path)
{
FILE* src;
FILE* dst;
struct capn c;
long header_end;
char* buf;
size_t n;
src = fopen(src_path, "r");
TEST_ASSERT_NOT_NULL(src);
TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, src, 0));
capn_free(&c);
header_end = ftell(src);
TEST_ASSERT_TRUE(header_end > 0);
rewind(src);
buf = (char*)malloc((size_t)header_end);
TEST_ASSERT_NOT_NULL(buf);
n = fread(buf, 1, (size_t)header_end, src);
TEST_ASSERT_EQUAL_UINT((size_t)header_end, n);
fclose(src);
dst = fopen(dst_path, "w");
TEST_ASSERT_NOT_NULL(dst);
TEST_ASSERT_EQUAL_UINT((size_t)header_end,
fwrite(buf, 1, (size_t)header_end, dst));
fclose(dst);
free(buf);
}
/* Hand-builds a single capnp message shaped like `SnapshotEntry`
* (word_count=1, pointer_count=1) but with all-default/empty field
* contents, and writes it standalone to `path` -- used to simulate a
* malformed "first message is not ReplayHeader" fixture (ReplayHeader is
* shaped (1,5); a lone (1,1)-shaped message can never be mistaken for one
* by sim.c's shape check). */
static void write_lone_snapshotentry_message(const char* path)
{
struct capn c;
struct capn_segment* cs;
SnapshotData_ptr sdata;
SnapshotEntry_ptr snap;
FILE* f;
int rc;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
sdata = new_SnapshotData(cs);
snap = new_SnapshotEntry(cs);
SnapshotEntry_set_ts(snap, 42ULL);
SnapshotEntry_set_data(snap, sdata);
capn_setp(capn_root(&c), 0, snap.p);
f = fopen(path, "w");
TEST_ASSERT_NOT_NULL(f);
rc = write_capnp_message(f, &c);
TEST_ASSERT_EQUAL_INT(0, rc);
fclose(f);
capn_free(&c);
}
/* ---------------------------------------------------------------------- */
/* Test 1: starting simulation against a valid fixture file reaches a */
/* ready-to-run state with the virtual clock reporting the snapshot's */
/* timestamp -- this task's own primary Observable. */
/* ---------------------------------------------------------------------- */
static void
test_valid_fixture_reaches_ready_state_with_frozen_virtual_clock(void)
{
char path[PATH_MAX];
uint64_t snapshot_ts = 0;
sched_sim_args_t args;
xce_sched_sim_driver_t sim;
int rc;
write_valid_fixture(path, sizeof(path), &snapshot_ts, "_valid");
TEST_ASSERT_TRUE(snapshot_ts > 0);
memset(&args, 0, sizeof(args));
args.replay_path = path;
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
args.sim_speed = 0; /* fast/frozen -- xce_core_now() stays pinned to
* virtual_clock_base regardless of real elapsed
* time, giving this test a deterministic,
* non-flaky assertion. */
rc = sched_sim_driver_start(&sim, &args);
TEST_ASSERT_EQUAL_INT(0, rc);
/* Step 4's Observable: the virtual clock reports the snapshot's own
* timestamp, exactly (frozen -- speed_num=0). */
TEST_ASSERT_EQUAL_UINT64(snapshot_ts, xce_core_now(&(sim.core)));
/* Direct inspection of the anchors Step 4 documents setting. */
TEST_ASSERT_EQUAL_UINT64(
snapshot_ts, atomic_load_explicit(&(sim.core.virtual_clock_base),
memory_order_acquire));
TEST_ASSERT_EQUAL_UINT64(0ULL, atomic_load_explicit(&(sim.core.speed_num),
memory_order_acquire));
TEST_ASSERT_EQUAL_UINT64(1ULL, atomic_load_explicit(&(sim.core.speed_den),
memory_order_acquire));
TEST_ASSERT_EQUAL_UINT64(0ULL, sim.sim_speed_num);
TEST_ASSERT_EQUAL_UINT64(1ULL, sim.sim_speed_den);
/* "Ready-to-run" -- no session is active, load_snapshot()'s own
* precondition would otherwise have been violated. */
TEST_ASSERT_EQUAL_INT(0, atomic_load(&(sim.core.session_active)));
TEST_ASSERT_NOT_NULL(sim.replay_file);
printf("test_valid_fixture_reaches_ready_state_with_frozen_virtual_clock:"
" PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Test 2: starting against a file missing its initial snapshot entry */
/* fails with a clear error before any event is processed -- this task's */
/* own second Observable. */
/* ---------------------------------------------------------------------- */
static void test_missing_initial_snapshot_entry_fails_cleanly(void)
{
char valid_path[PATH_MAX];
char header_only_path[PATH_MAX + 32];
uint64_t snapshot_ts = 0;
sched_sim_args_t args;
xce_sched_sim_driver_t sim;
int rc;
write_valid_fixture(valid_path, sizeof(valid_path), &snapshot_ts,
"_hdronly_src");
snprintf(header_only_path, sizeof(header_only_path),
"%s.header_only.replay", valid_path);
truncate_to_header_only(valid_path, header_only_path);
memset(&args, 0, sizeof(args));
args.replay_path = header_only_path;
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
args.sim_speed = 1;
rc = sched_sim_driver_start(&sim, &args);
TEST_ASSERT_TRUE(rc < 0);
printf("test_missing_initial_snapshot_entry_fails_cleanly: PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Test 3: a replay file whose FIRST message is not a valid ReplayHeader */
/* is rejected (Req 5 AC 13 step 1) -- distinct code path from test 2. */
/* ---------------------------------------------------------------------- */
static void test_first_message_not_replayheader_fails_cleanly(void)
{
char dir[PATH_MAX];
char path[PATH_MAX + 32];
sched_sim_args_t args;
xce_sched_sim_driver_t sim;
int rc;
make_tmp_dir(dir, sizeof(dir), "_badhdr");
snprintf(path, sizeof(path), "%s/bad_header.replay", dir);
write_lone_snapshotentry_message(path);
memset(&args, 0, sizeof(args));
args.replay_path = path;
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
args.sim_speed = 1;
rc = sched_sim_driver_start(&sim, &args);
TEST_ASSERT_TRUE(rc < 0);
printf("test_first_message_not_replayheader_fails_cleanly: PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Test 4: a missing replay file fails with a clear error (Req 5 AC 13 */
/* step 1, "the file is missing, unreadable"). */
/* ---------------------------------------------------------------------- */
static void test_missing_replay_file_fails_cleanly(void)
{
sched_sim_args_t args;
xce_sched_sim_driver_t sim;
int rc;
memset(&args, 0, sizeof(args));
args.replay_path = "/nonexistent/path/does_not_exist.replay";
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
args.sim_speed = 1;
rc = sched_sim_driver_start(&sim, &args);
TEST_ASSERT_TRUE(rc < 0);
printf("test_missing_replay_file_fails_cleanly: PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Test 5: NULL argument guards. */
/* ---------------------------------------------------------------------- */
static void test_null_arguments_return_einval(void)
{
xce_sched_sim_driver_t sim;
sched_sim_args_t args;
memset(&args, 0, sizeof(args));
args.replay_path = "/tmp/does-not-matter.replay";
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_sim_driver_start(NULL, &args));
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_sim_driver_start(&sim, NULL));
memset(&args, 0, sizeof(args));
args.replay_path = NULL;
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_sim_driver_start(&sim, &args));
printf("test_null_arguments_return_einval: PASS\n");
}
/* ---------------------------------------------------------------------- */
/* Test 6: sched_sim_driver_run() -- design.md's literal entry point -- */
/* calls start() then fails loudly with -ENOSYS (task 11.2's event loop */
/* does not exist yet), never silently returning success. */
/* ---------------------------------------------------------------------- */
static void test_run_starts_then_fails_enosys_pending_event_loop(void)
{
char path[PATH_MAX];
uint64_t snapshot_ts = 0;
sched_sim_args_t args;
xce_sched_sim_driver_t sim;
int rc;
write_valid_fixture(path, sizeof(path), &snapshot_ts, "_run");
memset(&args, 0, sizeof(args));
args.replay_path = path;
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
args.sim_speed = 0;
rc = sched_sim_driver_run(&sim, &args);
TEST_ASSERT_EQUAL_INT(-ENOSYS, rc);
/* Startup itself still genuinely succeeded -- state is real and
* inspectable, matching sched_sim_driver_start()'s own contract. */
TEST_ASSERT_EQUAL_UINT64(snapshot_ts, xce_core_now(&(sim.core)));
printf("test_run_starts_then_fails_enosys_pending_event_loop: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_valid_fixture_reaches_ready_state_with_frozen_virtual_clock);
RUN_TEST(test_missing_initial_snapshot_entry_fails_cleanly);
RUN_TEST(test_first_message_not_replayheader_fails_cleanly);
RUN_TEST(test_missing_replay_file_fails_cleanly);
RUN_TEST(test_null_arguments_return_einval);
RUN_TEST(test_run_starts_then_fails_enosys_pending_event_loop);
return UNITY_END();
}