sim_process_replay_event_message() now hands the decoded replay event to the caller, which keeps it beside the translated core event and frees it after xce_core_push_events() with driver.c's release_translated_payload() rules: a pushed config is kept by the core, and a rusages report is kept by the job it found. An untranslatable event is freed at once, and events after the last session are freed at the end. The payloads were an "accepted leak" per event; exit-time leak on the reproduction replay 75,495 -> 25,807 bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
711 lines
30 KiB
C
711 lines
30 KiB
C
/*
|
|
* test_maxtasks_wiring_sim_driver_parity.c - resreq-maxtasks-wiring spec,
|
|
* task 3.2 remediation round (Req 1.4, 5.4, and design.md's "Project
|
|
* Invariant Cross-Check" driver/simulation parity commitment).
|
|
*
|
|
* ── Why this is a SEPARATE file, not an addition to
|
|
* test_maxtasks_wiring_ingest.c ─────────────────────────────────────────
|
|
* This task's driver/simulation-parity test previously lived in
|
|
* test_maxtasks_wiring_ingest.c as
|
|
* test_driver_and_simulation_style_jobs_push_apply_identical_task_count_
|
|
* wiring(), an independent-review REJECTED design: it never executed
|
|
* sim_loop.c's/driver.c's own `static` translators
|
|
* (replay_event_to_core_event()/sim_process_replay_event_message()/
|
|
* translate_one_pending_update()) -- it hand-built an xce_core_event_t by
|
|
* MIRRORING the test author's own belief about those translators' field-
|
|
* copy bodies, then fed that hand-built event into the already-covered
|
|
* xce_core_push_events() path tests 1-3 of that file exercise. A real
|
|
* future regression in either translator's own body (e.g. an incorrect
|
|
* field name, a dropped assignment) would NOT have been caught -- the
|
|
* mirror would just as happily reproduce the bug, or never notice a
|
|
* divergence, since it never calls the translator at all.
|
|
*
|
|
* The reviewer identified that reaching the REAL, `static`
|
|
* sim_process_replay_event_message() (and, through it, the REAL, `static`
|
|
* replay_event_to_core_event()) is NOT impractical: test_sim_event_loop.c
|
|
* ALREADY #include's ../src/sim_loop.c directly (a white-box #include,
|
|
* not a link-only technique) and calls sim_process_replay_event_message()
|
|
* with a hand-built, in-memory capnp ReplayEvent message -- no real
|
|
* `.replay` file or full sched_sim_driver_run() boot required. This file
|
|
* copies that exact technique (see test_sim_event_loop.c's own file header
|
|
* comment for the fully-justified #include/stub set this file mirrors
|
|
* verbatim).
|
|
*
|
|
* The reviewer also identified the genuine obstacle to folding this
|
|
* directly into test_maxtasks_wiring_ingest.c: that file links the whole
|
|
* real `scheduler_srcs` object list (needed for ITS OWN test 3, which
|
|
* drives a real sched_init()'d allocator dispatch) -- white-box
|
|
* #include-ing core_events.c/sim_loop.c into that SAME translation unit
|
|
* would collide with meson's separate compilation of those same files into
|
|
* the linked scheduler_srcs archive (duplicate-symbol link errors). This
|
|
* file therefore lives on its own, using test_maxtasks_wiring_ingest.c's
|
|
* task-3.1 fixture-helper NAMES (make_wire_queue()/make_push_job()) but
|
|
* its own small, independent copies of them (this directory's own
|
|
* established norm -- every white-box-#include test file in this
|
|
* directory keeps its own self-contained fixture helpers rather than
|
|
* sharing a header, e.g. test_sim_event_loop.c's own
|
|
* build_fixture_scheduler() is not shared with test_sim_startup.c's
|
|
* near-identical one either).
|
|
*
|
|
* Per design.md's own explicit language ("exercising this path through
|
|
* sim_loop.c's own entry point") and this codebase's own cited history
|
|
* (the scheduler-allocation task 5.2 incident: a fix applied only to the
|
|
* real-driver path once caused a real driver/simulation divergence), the
|
|
* SIMULATION side is this file's target -- driver.c's own real-driver-side
|
|
* translate_one_pending_update() is already exercised for a JOBS_PUSH
|
|
* payload by test_driver_translate.c's own test_translate_jobs_push()
|
|
* (a real, non-mirrored proof on that side already exists); duplicating
|
|
* that here would add risk (a second white-box #include-of-driver.c
|
|
* translation unit alongside this one, its own separate link-surface to
|
|
* verify) for no new coverage, so this file focuses on the one side that
|
|
* genuinely lacked a real-code proof.
|
|
*
|
|
* ── What this test proves ───────────────────────────────────────────────
|
|
* A real multi-task job (mintasks=4/maxtasks=6, distinct from every other
|
|
* fixture value elsewhere in this feature's test suite) is carried inside
|
|
* a hand-built, in-memory ReplayEvent{jobsPush} capnp message (built via
|
|
* the SAME plain-C-struct + encode_ReplayEvent_ptr() technique
|
|
* recording_events.c's own write_one_replay_event() uses for this exact
|
|
* XCE_CORE_EVENT_JOBS_PUSH case in production -- not a hand-rolled capnp
|
|
* builder walk). That message is written to a real temp file, reopened,
|
|
* and fed through the REAL, `static` sim_process_replay_event_message()
|
|
* (sim_loop.c) -- the same function sched_sim_driver_run()'s own read loop
|
|
* calls for every ReplayEvent message in a genuine `.replay` file. The
|
|
* xce_core_event_t THAT REAL FUNCTION populates (via its own internal,
|
|
* equally-real call to replay_event_to_core_event()) is then pushed
|
|
* through the identical xce_core_push_events()/core_build_job() ingest
|
|
* chain this feature's fix lives in, and the resulting job's dispatched
|
|
* standalone primary resreq is asserted to carry the job's correct real
|
|
* task count (4/6), not parse_resreq_primary()'s own always-1 default.
|
|
*
|
|
* This directly catches a REAL regression in sim_loop.c's own translator
|
|
* body: confirmed live (see "RED-phase verification" below) by temporarily
|
|
* mutating replay_event_to_core_event()'s ReplayEvent_body_jobsPush case
|
|
* and observing this test fail, then reverting.
|
|
*
|
|
* ── RED-phase verification performed while writing this file (not
|
|
* committed here) ─────────────────────────────────────────────────────
|
|
* sim_loop.c's replay_event_to_core_event(), ReplayEvent_body_jobsPush
|
|
* case, was temporarily mutated from:
|
|
* ev->u.jobs_push.njobs = src->body.jobs_push->count;
|
|
* to:
|
|
* ev->u.jobs_push.njobs = 0;
|
|
* This file's binary was rebuilt and run in isolation: the test FAILED at
|
|
* the `TEST_ASSERT_EQUAL_INT(1, translated_ev.u.jobs_push.njobs)`
|
|
* assertion (0 != 1) -- proving this test genuinely calls the REAL,
|
|
* live sim_loop.c translator body, not a mirror of it (a mirror would be
|
|
* unaffected by a mutation to the real function it never calls). The
|
|
* mutation was then reverted and `git diff` against sim_loop.c confirmed
|
|
* clean before this file was finalized.
|
|
*/
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "controller.capnp.h"
|
|
#include "kvmgr.h"
|
|
#include "resreq.h"
|
|
#include "scheduler.h"
|
|
#include "xstring.h"
|
|
|
|
/* Pull in the real functions under test into this TU (white-box #include),
|
|
* the EXACT same set test_sim_event_loop.c's own file header comment
|
|
* justifies file-by-file (see that file for the full per-file rationale
|
|
* this comment does not repeat): the core/session/decision machinery
|
|
* sim_loop.c's event loop drives, the core.h-scoped picker registration
|
|
* path (plugin.c/builtin_fcfs.c), recording.c/recording_session.c/
|
|
* sim_record.c (sim_loop.c unconditionally references their symbols),
|
|
* repl.c and its siblings (sim_loop.c #includes repl.h, whose task 12.1
|
|
* inline wrappers unconditionally reference xce_repl_start()/
|
|
* xce_repl_post_session_check()), sim.c/sim_loop.c/sim_picker.c (the code
|
|
* under test plus its bug-1567 sim_bootstrap_picker() sibling),
|
|
* sim_verify.c (sched_sim_driver_run() references sim_decision_eq()/
|
|
* sim_log_mismatch() as externs even though this file never drives that
|
|
* function), and snapshot.c/snapshot_wire.c/snapshot_load.c (needed by
|
|
* sim.c's own snapshot load). */
|
|
#include "../src/core.c"
|
|
#include "../src/core_clock.c"
|
|
#include "../src/core_events.c"
|
|
#include "../src/core_session.c"
|
|
#include "../src/decision.c"
|
|
#include "../src/plugin.c"
|
|
#include "../src/plugins/builtin_fcfs.c"
|
|
#include "../src/recording.c"
|
|
#include "../src/recording_session.c"
|
|
#include "../src/repl.c"
|
|
#include "../src/repl_fmt.c"
|
|
#include "../src/repl_rollup.c"
|
|
#include "../src/repl_break.c"
|
|
#include "../src/repl_info.c"
|
|
#include "../src/repl_params.c"
|
|
#include "../src/sim.c"
|
|
#include "../src/sim_loop.c"
|
|
#include "../src/sim_picker.c"
|
|
#include "../src/sim_limit.c" /* bug-1825: sched_sim_driver_run() now also calls sim_bootstrap_limit_plugin() (sim_loop.c-cap sibling, sim_picker.c's own bug-1567 precedent) as an extern -- undefined reference at link time otherwise */
|
|
#include "../src/sim_record.c"
|
|
#include "../src/sim_verify.c"
|
|
#include "../src/snapshot.c"
|
|
#include "../src/snapshot_load.c"
|
|
#include "../src/snapshot_wire.c"
|
|
|
|
/* Link-satisfying stubs -- identical rationale/bodies to
|
|
* test_sim_event_loop.c's own copies (schedobj.c/resource.c are not linked
|
|
* here, same as that file). */
|
|
|
|
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; }
|
|
|
|
/* repl_info.c (scheduler-sim-diagnostics spec, task 2.1) now calls
|
|
* limit_get_configured_limits() (plugins/limit.c, not part of this TU) to
|
|
* resolve `info limit`'s name/all argument -- link-satisfying stub only,
|
|
* same rationale as kill_sched_job()/kill_sched_resval() above; this
|
|
* file's own fixtures never register the limit plugin, so NULL
|
|
* (limit_get_configured_limits()'s own documented "plugin absent" return)
|
|
* is also the behaviorally correct answer here, not just a link
|
|
* placeholder. repl_info_limit() itself is not yet wired into
|
|
* repl_cmd_info() (task 4), so no test in this file can reach it anyway. */
|
|
var limit_get_configured_limits(xce_sched_core_t* core)
|
|
{
|
|
(void)core;
|
|
return NULL;
|
|
}
|
|
|
|
/* scheduler-sim-diagnostics spec, task 3.1: repl_info.c now also calls
|
|
* limit_get_usage_instances() (plugins/limit.c, not part of this TU) to
|
|
* read a limit's live usage instances -- identical link-satisfying stub,
|
|
* same rationale as limit_get_configured_limits() immediately above; this
|
|
* file's own fixtures never register the limit plugin, so NULL
|
|
* (limit_get_usage_instances()'s own documented "plugin absent" return) is
|
|
* also the behaviorally correct answer here, not just a link placeholder. */
|
|
var limit_get_usage_instances(xce_sched_core_t* core)
|
|
{
|
|
(void)core;
|
|
return NULL;
|
|
}
|
|
|
|
/* scheduler-sim-diagnostics spec, task 6 (bug-1825): sim_loop.c's
|
|
* sched_sim_driver_run() now also calls sim_bootstrap_limit_plugin()
|
|
* (sim_limit.c, pulled into this TU via the #include above), which in
|
|
* turn calls limit_new_plugin() (plugins/limit.c, not part of this TU) --
|
|
* identical link-satisfying stub rationale as limit_get_configured_
|
|
* limits()/limit_get_usage_instances() immediately above. Returning NULL
|
|
* here is also the behaviorally correct answer, not just a link
|
|
* placeholder: sim_bootstrap_limit_plugin() already handles a NULL
|
|
* limit_new_plugin() return gracefully (logs an ERROR, registers nothing,
|
|
* returns) -- the exact same "out of memory" path sim_bootstrap_picker()
|
|
* (sim_picker.c) takes for a NULL builtin_fcfs_new_plugin() -- so this
|
|
* file's fixtures simply end up with no limit plugin registered, same
|
|
* observable state as before this task. */
|
|
xce_sched_plugin_t* limit_new_plugin(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* limit_seed_config() (plugins/limit.c, not part of this TU) -- bug-1825
|
|
* remediation round 2's new honestly-typed config-seed entry point.
|
|
* sim_bootstrap_limit_plugin() (sim_limit.c) only calls it after
|
|
* plugin_register_core() has already succeeded against a real, non-NULL
|
|
* limit_new_plugin() plugin -- unreachable in this file since the
|
|
* limit_new_plugin() stub immediately above always returns NULL, but the
|
|
* symbol must still resolve at link time. Same link-satisfying-stub
|
|
* rationale as limit_new_plugin() immediately above. */
|
|
int limit_seed_config(xce_sched_plugin_t* plugin, xce_sched_core_t* core,
|
|
xce_conf4sched_t* config)
|
|
{
|
|
(void)plugin;
|
|
(void)core;
|
|
(void)config;
|
|
return 0;
|
|
}
|
|
|
|
int sched_reconcile_job_resource_usage(xce_sched_core_t* c,
|
|
xce_job_account_t* a,
|
|
xce_job_rusages_t* r)
|
|
{
|
|
(void)c; (void)a; (void)r; return 0;
|
|
}
|
|
|
|
void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; }
|
|
|
|
int sched_fill_limit_requests(xce_sched_alloc_req_t* alloc_req,
|
|
xlist_t* limits, xlist_t* rusages, int nhosts,
|
|
int nslots)
|
|
{
|
|
(void)alloc_req;
|
|
(void)limits;
|
|
(void)rusages;
|
|
(void)nhosts;
|
|
(void)nslots;
|
|
return 0;
|
|
}
|
|
|
|
int sched_account_jobs(xce_sched_core_t* core)
|
|
{
|
|
(void)core;
|
|
return 0;
|
|
}
|
|
|
|
/* bug-1836 (scheduler-driver-equivalence fix, Recorded-Decision
|
|
* Reconciliation capacity gate): link-satisfying stubs, mirroring this
|
|
* file's existing sched_account_jobs() no-op precedent immediately above --
|
|
* sim_loop.c's sim_reconcile_unaccounted_jobs() now also calls
|
|
* sched_check_host_satisfy()/sched_check_shared_satisfy() (resource.c) to
|
|
* re-validate a recorded decision against live capacity, and
|
|
* sched_charge_account() (resource.c) to keep that check's own view of
|
|
* capacity up to date across multiple candidates in the same round. This
|
|
* file never exercises real resource contention (same rationale as the
|
|
* existing sched_account_jobs()/sched_ctx_get_uncomit_host() no-ops), so
|
|
* the satisfy checks trivially always succeed and the charge call is a
|
|
* no-op. */
|
|
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
|
|
xvec_t* uncomitted, xvec_idval_t* wanted,
|
|
xvec_idval_t* unmeet)
|
|
{
|
|
(void)core;
|
|
(void)w;
|
|
(void)uncomitted;
|
|
(void)wanted;
|
|
(void)unmeet;
|
|
return 1;
|
|
}
|
|
|
|
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
|
|
xvec_idval_t* wanted, xvec_idval_t* unmeet)
|
|
{
|
|
(void)core;
|
|
(void)uncomitted;
|
|
(void)wanted;
|
|
(void)unmeet;
|
|
return 1;
|
|
}
|
|
|
|
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
|
{
|
|
(void)core;
|
|
(void)acct;
|
|
return 0;
|
|
}
|
|
|
|
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
|
|
{
|
|
(void)core;
|
|
(void)acct;
|
|
return 0;
|
|
}
|
|
|
|
|
|
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
|
|
{
|
|
(void)core;
|
|
|
|
if (out == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* No picker plugin is ever registered in this file's fixture -- this
|
|
* test's Observable is the ingest-time resreq wiring, not picker
|
|
* hook-firing (task 4.3's own test suite covers that exhaustively) --
|
|
* so -EINVAL (core_events.c's own documented behavior on an
|
|
* unresolved picker, "WARNs and proceeds with picker=NULL") is always
|
|
* the correct, deliberate return here. */
|
|
*out = NULL;
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* scheduler-sim-live-allocation spec, task 4.1: sim_loop.c's real
|
|
* sched_sim_driver_run() (white-box #include'd into this TU above) now
|
|
* spawns sched_job_picker()/sched_allocator() (sched_election.c) directly
|
|
* instead of its own, now-removed sim_picker_thread_fn() -- an undefined
|
|
* reference at link time in this white-box TU otherwise, since
|
|
* sched_election.c's own sched_allocator() calls the REAL
|
|
* sched_make_allocation()/sched_commit_decision() (scheduler.c), neither
|
|
* part of this file's own hand-picked white-box #include set (this file's
|
|
* own header comment lists that set). This file's own test(s) do not
|
|
* exercise genuine resource-matching allocation, so this link-satisfying
|
|
* pair faithfully reproduces the exact PEND-only contract the REMOVED
|
|
* sim_picker_thread_fn() itself used to provide -- split across the SAME
|
|
* two-thread jbuf hand-off the real production pair now uses (one
|
|
* producer, one consumer, matching the project's jbuf capacity-1
|
|
* rendezvous invariant) instead of the old single fused thread -- so this
|
|
* file's own existing assertions are unaffected by this task's production
|
|
* change. Genuine resource-based dispatch through the real, unmodified
|
|
* sched_job_picker()/sched_allocator() pair is proved separately by
|
|
* test_sim_live_allocation.c (task 4.1's own dedicated fixture, which
|
|
* links the real scheduler_srcs instead of white-boxing individual files,
|
|
* precisely so it can reach the real allocation chain this stub
|
|
* deliberately does not). */
|
|
void* sched_job_picker(void* arg)
|
|
{
|
|
xce_sched_core_t* core = (xce_sched_core_t*)arg;
|
|
xce_sched_plugin_t* picker = NULL;
|
|
|
|
for (;;) {
|
|
int go = 0;
|
|
uint64_t jobid = 0;
|
|
|
|
cbuf_get(&(core->wbuf), &go);
|
|
|
|
if (sched_resolve_job_picker(core, &picker) < 0) {
|
|
jobid = 0;
|
|
cbuf_put(&(core->jbuf), &jobid);
|
|
continue;
|
|
}
|
|
|
|
picker->picker_iface->begin_session(picker);
|
|
|
|
for (;;) {
|
|
xce_sched_job_t* job = picker->picker_iface->next_job(picker);
|
|
|
|
if (job == NULL) {
|
|
break;
|
|
}
|
|
|
|
jobid = job->jobid;
|
|
cbuf_put(&(core->jbuf), &jobid);
|
|
}
|
|
|
|
jobid = 0;
|
|
cbuf_put(&(core->jbuf), &jobid);
|
|
|
|
picker->picker_iface->end_session(picker);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void* sched_allocator(void* arg)
|
|
{
|
|
xce_sched_core_t* core = (xce_sched_core_t*)arg;
|
|
xce_sched_plugin_t* picker = NULL;
|
|
|
|
for (;;) {
|
|
uint64_t jobid = 0;
|
|
xce_decision_t* decision = NULL;
|
|
xce_sched_job_t* job;
|
|
|
|
cbuf_get(&(core->jbuf), &jobid);
|
|
if (jobid == 0) {
|
|
decision = NULL;
|
|
cbuf_put(&(core->decisions), &decision);
|
|
continue;
|
|
}
|
|
|
|
job = sched_get_job(core, jobid);
|
|
if (job == NULL) {
|
|
continue;
|
|
}
|
|
|
|
(void)sched_resolve_job_picker(core, &picker);
|
|
if (picker != NULL) {
|
|
picker->picker_iface->on_pend(picker, job->jobid);
|
|
}
|
|
|
|
decision = make_pend_decision(make_pendreason(job->jobid));
|
|
cbuf_put(&(core->decisions), &decision);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* scheduler-sim-live-allocation spec, task 4.1 (bug-1829 fix): sim_loop.c's
|
|
* real sched_sim_driver_run() (white-box #include'd into this TU above) now
|
|
* also calls sim_bootstrap_seqjob_allocator()/sim_bootstrap_parjob_
|
|
* allocator() (sim_alloc.c) -- undefined references at link time in this
|
|
* white-box TU otherwise, since neither symbol is part of this file's own
|
|
* hand-picked white-box #include set. This file's own test(s) exercise the
|
|
* link-satisfying sched_job_picker()/sched_allocator() stub pair above
|
|
* (which never reaches real primary allocation), so a true no-op is the
|
|
* correct, behavior-preserving stand-in here -- the real bootstrap logic is
|
|
* exercised separately by test_sim_live_allocation.c (task 4.1's own
|
|
* dedicated fixture, which links the real scheduler_srcs). */
|
|
void sim_bootstrap_seqjob_allocator(xce_sched_sim_driver_t* sim)
|
|
{
|
|
(void)sim;
|
|
}
|
|
|
|
void sim_bootstrap_parjob_allocator(xce_sched_sim_driver_t* sim)
|
|
{
|
|
(void)sim;
|
|
}
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
#define JOBID_SIM_DRIVER_PARITY ((uint64_t)910701ULL)
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Fixture helpers */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/* make_tmp_dir -- identical technique/rationale to
|
|
* test_sim_event_loop.c's own helper of the same name. */
|
|
static void make_tmp_dir(char* out, size_t out_len, const char* suffix)
|
|
{
|
|
char tmpl[PATH_MAX];
|
|
|
|
snprintf(tmpl, sizeof(tmpl), "/tmp/sched_maxtasks_simparity%s_XXXXXX",
|
|
suffix);
|
|
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
|
|
TEST_ASSERT_TRUE(strlen(tmpl) < out_len);
|
|
strcpy(out, tmpl);
|
|
}
|
|
|
|
/* make_wire_queue / make_push_job -- same shape and rationale as
|
|
* test_maxtasks_wiring_ingest.c's own helpers of the same names (this
|
|
* file's own small, independent copies -- see this file's header comment
|
|
* for why this directory's convention is per-file fixtures, not a shared
|
|
* header, for white-box-#include test TUs). Never torn down, same
|
|
* directory-wide convention. */
|
|
static xce_queue_t* make_wire_queue(const char* name, int32_t priority)
|
|
{
|
|
xce_queue_t* q = (xce_queue_t*)calloc(1, sizeof(*q));
|
|
|
|
TEST_ASSERT_NOT_NULL(q);
|
|
|
|
q->queue = strdup(name);
|
|
q->status = QUEUE_STAT_OPEN;
|
|
q->priority = priority;
|
|
|
|
return q;
|
|
}
|
|
|
|
static xce_job_t* make_push_job(uint64_t jobid, char* resreq_str, char* user,
|
|
char* queue, char* project, uint32_t mintasks,
|
|
uint32_t maxtasks)
|
|
{
|
|
xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j));
|
|
|
|
TEST_ASSERT_NOT_NULL(j);
|
|
|
|
j->jobid = jobid;
|
|
j->state = JOB_STAT_PEND;
|
|
j->sub_time = 1000;
|
|
j->queue_time = 1000;
|
|
|
|
j->submit = (job_submit_data_t*)calloc(1, sizeof(*(j->submit)));
|
|
TEST_ASSERT_NOT_NULL(j->submit);
|
|
|
|
j->submit->queue = queue;
|
|
j->submit->user = user;
|
|
j->submit->project_name = project;
|
|
j->submit->resreq = resreq_str;
|
|
j->submit->mintasks = mintasks;
|
|
j->submit->maxtasks = maxtasks;
|
|
|
|
return j;
|
|
}
|
|
|
|
/* append_jobspush_event -- appends one ReplayEvent{jobsPush} message to the
|
|
* already-open stream `f`, using the SAME plain-C-struct
|
|
* (xce_jobs_data_t wrapper) + encode_ReplayEvent_ptr()/write_capnp_message()
|
|
* technique BOTH recording_events.c's own write_one_replay_event()
|
|
* (production write-side, XCE_CORE_EVENT_JOBS_PUSH case) and
|
|
* test_sim_event_loop.c's own append_jobstatechange_event()/
|
|
* append_replay_session() (test-side precedent) already establish --
|
|
* NOT a hand-rolled capnp builder walk. */
|
|
static void append_jobspush_event(FILE* f, uint64_t ts, xce_job_t** jobs,
|
|
int njobs)
|
|
{
|
|
xce_jobs_data_t jobs_wrap;
|
|
replay_event_t src;
|
|
struct capn c;
|
|
struct capn_segment* cs;
|
|
ReplayEvent_ptr p;
|
|
|
|
memset(&jobs_wrap, 0, sizeof(jobs_wrap));
|
|
jobs_wrap.n_total = njobs;
|
|
jobs_wrap.count = njobs;
|
|
jobs_wrap.jobs = jobs;
|
|
|
|
memset(&src, 0, sizeof(src));
|
|
src.ts = ts;
|
|
src.body_which = ReplayEvent_body_jobsPush;
|
|
src.body.jobs_push = &jobs_wrap;
|
|
|
|
capn_init_malloc(&c);
|
|
cs = capn_root(&c).seg;
|
|
encode_ReplayEvent_ptr(cs, &p, &src);
|
|
capn_setp(capn_root(&c), 0, p.p);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
|
capn_free(&c);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* The test */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* See this file's own header comment for the full narrative. Summary: a
|
|
* real multi-task job's ReplayEvent{jobsPush} wire message is decoded and
|
|
* translated by the REAL, `static` sim_process_replay_event_message()
|
|
* (which internally calls the REAL, `static` replay_event_to_core_event())
|
|
* -- not a hand-built mirror of either -- and the resulting real
|
|
* xce_core_event_t is pushed through the identical
|
|
* xce_core_push_events()/core_build_job() ingest chain this feature's fix
|
|
* lives in, proving sim_loop.c's OWN translator code applies this
|
|
* feature's task-count wiring correctly.
|
|
*/
|
|
static void
|
|
test_sim_process_replay_event_jobs_push_wires_real_task_count_into_resreq_primary(
|
|
void)
|
|
{
|
|
char path[PATH_MAX];
|
|
FILE* f;
|
|
xce_sched_sim_driver_t sim;
|
|
xce_queue_t* queue;
|
|
xce_core_event_t queue_event;
|
|
xce_job_t* job;
|
|
xce_job_t* job_ptrs[1];
|
|
struct capn c;
|
|
capn_ptr root;
|
|
xce_core_event_t translated_ev;
|
|
replay_event_t translated_decoded;
|
|
int record_rc = 0;
|
|
int translated;
|
|
xce_sched_job_t* sjob;
|
|
xce_primary_resreq_t* primary;
|
|
int rc;
|
|
|
|
/* A minimal sim driver -- same rationale as
|
|
* test_sim_event_loop.c's own Test 1: only sim->core is exercised by
|
|
* sim_process_replay_event_message() (via sim_advance_virtual_clock()),
|
|
* so xce_sched_core_init() plus a frozen (speed=0) clock is a
|
|
* sufficient, self-contained fixture. */
|
|
memset(&sim, 0, sizeof(sim));
|
|
rc = xce_sched_core_init(&(sim.core));
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
atomic_store_explicit(&(sim.core.speed_num), 0, memory_order_release);
|
|
atomic_store_explicit(&(sim.core.speed_den), 1, memory_order_release);
|
|
|
|
/* Register a real queue the job references -- hand-built ingest, not
|
|
* part of this test's own Observable (the jobsPush translator below);
|
|
* same technique test_maxtasks_wiring_ingest.c's own fixtures use for
|
|
* an identical, incidental purpose. */
|
|
queue = make_wire_queue("normal", 1);
|
|
memset(&queue_event, 0, sizeof(queue_event));
|
|
queue_event.type = XCE_CORE_EVENT_QUEUE_UPDATE;
|
|
queue_event.u.queue_update.queue = queue;
|
|
rc = xce_core_push_events(&(sim.core), &queue_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real QUEUE_UPDATE ingest "
|
|
"must succeed");
|
|
|
|
/* A real multi-task job (mintasks=4/maxtasks=6, deliberately distinct
|
|
* from every other fixture value elsewhere in this feature's test
|
|
* suite, so this is a genuinely independent data point). */
|
|
job = make_push_job(JOBID_SIM_DRIVER_PARITY, "type == any", "alice",
|
|
"normal", "proj1", 4, 6);
|
|
job_ptrs[0] = job;
|
|
|
|
/* ── Build a REAL ReplayEvent{jobsPush} capnp wire message on disk,
|
|
* then reopen and decode it -- the SAME on-disk round trip
|
|
* sched_sim_driver_run()'s own read loop performs for every message in
|
|
* a genuine `.replay` file. ─────────────────────────────────────── */
|
|
make_tmp_dir(path, sizeof(path), "_jobspush");
|
|
strcat(path, "/event.replay");
|
|
|
|
f = fopen(path, "w");
|
|
TEST_ASSERT_NOT_NULL(f);
|
|
append_jobspush_event(f, 42000ULL, job_ptrs, 1);
|
|
fclose(f);
|
|
|
|
f = fopen(path, "r");
|
|
TEST_ASSERT_NOT_NULL(f);
|
|
TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, f, 0));
|
|
TEST_ASSERT_TRUE(msg_root_has_shape(&c, &root, ReplayEvent_word_count,
|
|
ReplayEvent_pointer_count));
|
|
|
|
/* ── The proof-of-real-code step: drive this message through the
|
|
* REAL, `static` sim_process_replay_event_message() (sim_loop.c) --
|
|
* not a mirror of it. record_rc is always 0 here: sim.record_file is
|
|
* NULL for this whole fixture (no --sim-output involved). ────────── */
|
|
translated = sim_process_replay_event_message(&sim, &c, root,
|
|
&translated_ev,
|
|
&translated_decoded,
|
|
&record_rc);
|
|
fclose(f);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, record_rc);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, translated, "sim_loop.c's real sim_process_replay_event_message() "
|
|
"must translate a jobsPush ReplayEvent into a "
|
|
"usable xce_core_event_t");
|
|
TEST_ASSERT_EQUAL_INT(XCE_CORE_EVENT_JOBS_PUSH, translated_ev.type);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, translated_ev.u.jobs_push.njobs,
|
|
"the real translator must carry through the real njobs count -- "
|
|
"this exact field is what a regression in "
|
|
"replay_event_to_core_event()'s own body would corrupt");
|
|
TEST_ASSERT_NOT_NULL(translated_ev.u.jobs_push.jobs[0]);
|
|
|
|
/* NOT a pointer-identity check against the original `job` local: this
|
|
* message went through a REAL on-disk capnp encode/decode round trip,
|
|
* so decode_ReplayEvent() (per sim_process_replay_event_message()'s
|
|
* own doc comment, sim_loop.c) deep-copies a brand-new xce_job_t/
|
|
* job_submit_data_t pair out of the arena -- `job` itself was only
|
|
* ever the SOURCE this test serialized from, never aliased back. The
|
|
* real proof is that the decoded copy's own real fields (jobid, and
|
|
* -- the point of this whole test -- mintasks/maxtasks below) match
|
|
* what was serialized. */
|
|
TEST_ASSERT_EQUAL_UINT64(JOBID_SIM_DRIVER_PARITY,
|
|
translated_ev.u.jobs_push.jobs[0]->jobid);
|
|
|
|
/* ── Push the REAL translator's own output through the identical
|
|
* xce_core_push_events()/core_build_job() ingest chain this feature's
|
|
* fix lives in, and assert the resulting job's dispatched standalone
|
|
* primary resreq carries the job's correct real task count -- proving
|
|
* sim_loop.c's OWN, actual translator code (not a hand-written mirror
|
|
* of it) applies this feature's wiring correctly. ─────────────────── */
|
|
rc = xce_core_push_events(&(sim.core), &translated_ev, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(
|
|
rc >= 0, "pushing the real translator's own output through "
|
|
"xce_core_push_events() must succeed");
|
|
|
|
sjob = sched_get_job(&(sim.core), JOBID_SIM_DRIVER_PARITY);
|
|
TEST_ASSERT_NOT_NULL(sjob);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(
|
|
sjob->schedobj, "core_build_job() must have built job->schedobj "
|
|
"for the job carried by the real translator's "
|
|
"output");
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, sjob->schedobj->tag);
|
|
primary = sjob->schedobj->u.primary->resreq;
|
|
TEST_ASSERT_NOT_NULL(primary);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
4, primary->minTasks,
|
|
"the REAL simulation-driver translator path (sim_process_replay_"
|
|
"event_message() -> replay_event_to_core_event(), sim_loop.c), not "
|
|
"a hand-built mirror of it, must have carried the job's real "
|
|
"minTasks through to the dispatched standalone primary");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
6, primary->maxTasks,
|
|
"the REAL simulation-driver translator path (sim_process_replay_"
|
|
"event_message() -> replay_event_to_core_event(), sim_loop.c), not "
|
|
"a hand-built mirror of it, must have carried the job's real "
|
|
"maxTasks through to the dispatched standalone primary");
|
|
|
|
free_ReplayEvent(&translated_decoded);
|
|
|
|
printf("test_sim_process_replay_event_jobs_push_wires_real_task_count_"
|
|
"into_resreq_primary: PASS\n");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(
|
|
test_sim_process_replay_event_jobs_push_wires_real_task_count_into_resreq_primary);
|
|
return UNITY_END();
|
|
}
|