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:
parent
16d756aca3
commit
5d587ecf32
7 changed files with 1366 additions and 21 deletions
228
src/scheduler/include/sim.h
Normal file
228
src/scheduler/include/sim.h
Normal 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
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
450
src/scheduler/src/sim.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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'])
|
||||
|
|
|
|||
528
src/scheduler/testsuite/test_sim_startup.c
Normal file
528
src/scheduler/testsuite/test_sim_startup.c
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue