/* * test_repl_limit_table.c - contract test for the interactive simulation * REPL's `info limit ` name/all resolution and absence messaging * (scheduler-sim-diagnostics spec, Requirement Set 1, task 2.1, Req 1.3, * 1.7, 4.1, 4.2). * * New white-box test file, sibling of test_repl_resource_table.c -- kept * separate rather than added to that file, per design.md's own explicit * build-wiring risk note (Implementation Notes, "REPL Limit Info Command"): * test_repl_resource_table.c white-box #includes repl_info.c but * deliberately does NOT link plugins/limit.c or resource.c ("resource.c, * not linked here"). This file needs the REAL plugins/limit.c (for the * real limit_get_configured_limits()/limit_init()/limit_update_config() * repl_info_limit() and this file's own fixtures call) -- not stubbed, * same bug-1762 precedent design.md cites for a different scheduler-sim- * repl-verbosity command that hit the identical class of undefined- * reference link failure. * * `repl_info_limit()` is NOT yet wired into repl_cmd_info() (that's task * 4) -- every test below calls it directly, not through repl_dispatch(). * * White-box #include set: identical to test_repl_resource_table.c's own * (core.c, core_clock.c, core_events.c, core_session.c, decision.c, * plugin.c, plugins/builtin_fcfs.c, recording.c, recording_session.c, * repl.c, repl_fmt.c, repl_rollup.c, repl_break.c, repl_info.c, * repl_params.c, sim.c, sim_loop.c, sim_picker.c, sim_record.c, * sim_verify.c, snapshot.c, snapshot_load.c, snapshot_wire.c) PLUS the new * ../src/plugins/limit.c under test. task 2.1's own scope never reads live * usage/resource accounting (that's task 3), so resource.c is NOT linked * here either -- the same minimal link-satisfying stubs * test_limit_plugin.c already establishes for limit.c's unused * limit_job_election()/limit_prepare_allocation() hook bodies * (reset_sched_obj()/get_resvals()/put_resvals(), none of which task 2.1's * own code path ever calls) are reused verbatim here, confirmed sufficient * by actually attempting the build and reading the resulting undefined- * reference errors rather than guessing blindly, per design.md's own risk * note. * * Fixture strategy: unlike test_limit_plugin.c's own TU (which links * neither core.c nor plugin.c, so its fixture_init()/ * fixture_register_plugin_in_core() hand-build a standalone core->plugins * kvmgr), this file links the REAL ../src/core.c, so xce_sched_core_init() * already creates a real, usable sim->core.plugins kvmgr on its own -- * register_limit_plugin() (below) key_add()s a real, limit_init()'d * xce_sched_plugin_t directly into THAT kvmgr, and add_configured_limit() * drives the real limit_update_config() (plugins/limit.c) against it, * exactly the machinery Implementation Notes' "task 1" note predicted this * file would have available and confirms here. */ #include #include #include #include #include #include #include #include #include #include #include #include "controller.capnp.h" #include "group.h" #include "kvmgr.h" #include "scheduler.h" #include "xce/types.h" #include "xcelimit.h" #include "xstring.h" /* Pull in the real functions under test into this TU (white-box #include, * same convention as test_repl_resource_table.c/test_repl_round_rollup.c/ * test_repl_event_echo.c). */ #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/plugins/limit.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" /* bug-1567: sched_sim_driver_run() now calls sim_bootstrap_picker() (sim_loop.c-cap sibling) as an extern -- undefined reference at link time otherwise */ #include "../src/sim_limit.c" /* bug-1825: sched_sim_driver_run() now calls sim_bootstrap_limit_plugin() (sim_loop.c-cap sibling) 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" 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; } /* resource-accounting.md's reconciliation (resource.c, not linked here -- * this file only #includes core_events.c, same as test_repl_resource_table.c). * core_events.c's XCE_CORE_EVENT_JOB_RUSAGE handler calls this whenever * job->accounts != NULL; link-satisfying stub only, same rationale as every * other stub in this block. */ 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; } /* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same * copy test_repl_resource_table.c/test_repl_execution_control.c/ * test_sim_alt_events.c/etc. each carry (schedobj.c is not linked here). */ 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; } /* plugins/limit.c's limit_job_election()/limit_prepare_allocation() (never * exercised by any test in this file -- task 2.1's own scope never touches * usage/accounting) reference reset_sched_obj() (../src/schedobj.c) and * get_resvals()/put_resvals() (../src/resource.c), neither of which is part * of this white-box TU. Stubbed as no-ops purely to satisfy the linker -- * same pattern test_limit_plugin.c already establishes for the identical * symbols. */ void reset_sched_obj(xce_sched_obj_t* obj) { (void)obj; } int get_resvals(zarray_t* array, int nres, int* idxs, float* vals) { (void)array; (void)nres; (void)idxs; (void)vals; return 0; } int put_resvals(zarray_t* array, int nres, int* idxs, float* vals) { (void)array; (void)nres; (void)idxs; (void)vals; return 0; } /* 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; } /* ---------------------------------------------------------------------- */ /* stdout-capture helper -- duplicated from test_repl_resource_table.c */ /* (same dup()/dup2()-around-STDOUT_FILENO technique that file's own */ /* header comment establishes), per this file family's own "duplicate, */ /* don't share" convention. */ /* ---------------------------------------------------------------------- */ static char* capture_stdout_around(void (*fn)(void)) { char tmp[] = "/tmp/xce_repllimtbl_stdoutcap_XXXXXX"; int fd = mkstemp(tmp); int saved_stdout; off_t len; char* buf; TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "mkstemp for stdout capture must " "succeed"); fflush(stdout); saved_stdout = dup(STDOUT_FILENO); TEST_ASSERT_TRUE_MESSAGE(saved_stdout >= 0, "dup(STDOUT_FILENO) must " "succeed"); TEST_ASSERT_TRUE_MESSAGE(dup2(fd, STDOUT_FILENO) >= 0, "dup2 redirect of STDOUT_FILENO must succeed"); fn(); fflush(stdout); dup2(saved_stdout, STDOUT_FILENO); close(saved_stdout); len = lseek(fd, 0, SEEK_CUR); TEST_ASSERT_TRUE(len >= 0); buf = (char*)calloc(1, (size_t)len + 1); TEST_ASSERT_NOT_NULL(buf); TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0); if (len > 0) { ssize_t nread = read(fd, buf, (size_t)len); TEST_ASSERT_TRUE(nread == len); } close(fd); unlink(tmp); return buf; } /* Captures the output of calling repl_info_limit(sim, arg) DIRECTLY -- * task 2.1's own scope calls it directly, not through repl_dispatch() * (that wiring is task 4's job). */ static xce_sched_sim_driver_t* g_direct_sim; static const char* g_direct_arg; static void run_info_limit_for_capture(void) { repl_info_limit(g_direct_sim, g_direct_arg); } static char* capture_info_limit(xce_sched_sim_driver_t* sim, const char* arg) { g_direct_sim = sim; g_direct_arg = arg; return capture_stdout_around(run_info_limit_for_capture); } /* * Captures the output of dispatching one REPL command line THROUGH THE REAL * repl_dispatch() entry point -- task 4's own dispatch-round-trip * requirement (unlike capture_info_limit() above, which calls * repl_info_limit() directly, bypassing repl_cmd_info()'s tokenization and * dispatch entirely). Same g_sim/g_line/run_for_capture()/capture() shape * as capture_info_limit() above, and the same pattern * test_repl_resource_table.c's/test_repl_breakpoints_inspection.c's own * capture_dispatch() already establish for this file family. Never a * "step"/"continue"/"quit" line (this helper does not expect * repl_dispatch() to return true). */ static xce_sched_sim_driver_t* g_dispatch_sim; static const char* g_dispatch_line; static void run_dispatch_for_capture(void) { (void)repl_dispatch(g_dispatch_sim, g_dispatch_line); } static char* capture_dispatch(xce_sched_sim_driver_t* sim, const char* line) { g_dispatch_sim = sim; g_dispatch_line = line; return capture_stdout_around(run_dispatch_for_capture); } /* ---------------------------------------------------------------------- */ /* Limit configuration fixture helpers */ /* ---------------------------------------------------------------------- */ /* * register_limit_plugin - registers a real, limit_init()'d xce_sched_plugin_t * directly into `sim`'s own core->plugins kvmgr (xce_sched_core_init() -- * already called on `sim` by every test below -- creates that kvmgr for * real, since this TU links ../src/core.c), so * limit_get_configured_limits(&(sim->core))'s key_find(core->plugins, * "xce_sched_limit", ...) genuinely finds it -- the exact real lookup path * repl_info_limit() drives. * * `plugin`/`group_sched` are caller-owned (stack) storage, not static * globals, so each test gets its own independent fixture. `group_sched` is * a throwaway xce_scheduler_t existing purely so limit_update_config() * (called separately by add_configured_limit() below) has real group * managers to resolve each consumer's "all"-group fallback from * (build_limit()/build_consumer(), xcelimit.c) -- the built xce_limit_t/ * xce_group_t values it produces are owned by plugin->data's own * privdata->limits kvmgr, not by group_sched, so group_sched need not be * (and is not) sim's own core. Same minimal group-manager technique * test_limit_plugin.c's own fixture_init() already establishes * (groupmgr_init() x4), reused here rather than re-derived, per * Implementation Notes' own "task 1" note. */ static void register_limit_plugin(xce_sched_sim_driver_t* sim, xce_sched_plugin_t* plugin, xce_scheduler_t* group_sched) { int rc; memset(group_sched, 0, sizeof(*group_sched)); groupmgr_init(&(group_sched->core.ugmgr), 4, 4); groupmgr_init(&(group_sched->core.qgmgr), 4, 4); groupmgr_init(&(group_sched->core.pgmgr), 4, 4); groupmgr_init(&(group_sched->core.hgmgr), 4, 4); TEST_ASSERT_NOT_NULL(group_sched->core.ugmgr); TEST_ASSERT_NOT_NULL(group_sched->core.qgmgr); TEST_ASSERT_NOT_NULL(group_sched->core.pgmgr); TEST_ASSERT_NOT_NULL(group_sched->core.hgmgr); memset(plugin, 0, sizeof(*plugin)); plugin->name = "xce_sched_limit"; plugin->sched = group_sched; rc = limit_init(plugin); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_NOT_NULL(plugin->data); rc = key_add(sim->core.plugins, plugin->name, (void*)plugin); TEST_ASSERT_TRUE(rc >= 0); } /* A minimal, heap-allocated xce_limit_config_t -- identical shape to * test_limit_plugin.c's own make_limit_config()/free_limit_config() * (duplicated here per this file family's own "duplicate, don't share" * convention): no per-consumer groups, so build_limit()'s build_consumer() * falls back to get_group(mgr, "all") for every consumer. `name` is * expected to be a string literal (build_limit() strdup()s it). */ static xce_limit_config_t* make_limit_config(const char* name) { xce_limit_config_t* cfg = (xce_limit_config_t*)calloc(1, sizeof(*cfg)); xce_limit_val_t* val = (xce_limit_val_t*)calloc(1, sizeof(*val)); TEST_ASSERT_NOT_NULL(cfg); TEST_ASSERT_NOT_NULL(val); val->resname = "mem"; val->percent = 0; val->val = 10.0; cfg->name = (char*)name; cfg->val = val; return cfg; } static void free_limit_config(xce_limit_config_t* cfg) { FREEUP(cfg->val); FREEUP(cfg); } /* Drives the real limit_update_config() (plugins/limit.c) to configure ONE * limit named `name` on `plugin` -- the exact production entry point a * scheduler config update reaches this plugin through, not a hand-rolled * xce_limit_t. */ static void add_configured_limit(xce_sched_plugin_t* plugin, const char* name) { xce_limit_config_t* limits[1]; xce_conf4sched_t config; int rc; limits[0] = make_limit_config(name); memset(&config, 0, sizeof(config)); config.nlimits = 1; config.limits = limits; rc = limit_update_config(plugin, &config); TEST_ASSERT_EQUAL_INT(0, rc); free_limit_config(limits[0]); } /* * add_configured_limit_with_host_exclusion - task 2.2's own Requirement 1.5 * fixture: an "all except X" exclusion group on the HOST dimension, the * same shape test_limit.c's own test_build_consumer2() already establishes * for the USER dimension (.members=["all"], .ex_members=[]). * Unlike add_configured_limit()/make_limit_config() (which leaves every * consumer's xce_group_config_t* NULL, so build_consumer() falls back to * the builtin "all" group for all four dimensions), this drives the real * limit_update_config() with an explicit `.hosts` group config. * * All storage here (val, hosts_cfg, limit_cfg, the `limits` array) lives on * this function's own stack frame; limit_update_config() -> build_limit() * -> build_consumer() -> makeGroup() synchronously copies every member * name into the real, persisted xce_group_t (and build_limit() itself * strdup()s the limit/resource names) before this function returns, so * nothing here needs to outlive the call -- the identical lifetime * assumption test_limit.c's own test_build_limit_ok1()-style compound * literals already rely on. */ static void add_configured_limit_with_host_exclusion( xce_sched_plugin_t* plugin, const char* name, const char* included, const char* excluded) { xce_limit_val_t val = { .resname = "mem", .percent = 0, .val = 10.0 }; xce_group_config_t hosts_cfg = { .members = { .count = 1, .data = (char*[]) { (char*)included } }, .ex_members = { .count = 1, .data = (char*[]) { (char*)excluded } } }; xce_limit_config_t limit_cfg = { .name = (char*)name, .hosts = &hosts_cfg, .val = &val }; xce_limit_config_t* limits[1] = { &limit_cfg }; xce_conf4sched_t config; int rc; memset(&config, 0, sizeof(config)); config.nlimits = 1; config.limits = limits; rc = limit_update_config(plugin, &config); TEST_ASSERT_EQUAL_INT(0, rc); } /* * register_per_host_resource - task 2.3's own fixture: registers a plain, * non-GLOBAL/non-SHARED resource named `resname` directly into `sim`'s real * core.resmgr, the same makeResource() pattern test_repl_resource_table.c's * own build_worker_with_one_numeric_resource() already establishes. * is_host_based_limit() (src/lib/xcelimit.c) key_find()s a limit's * val.resname directly against core->resmgr and inspects the resulting * xce_resource_t's flags -- a bare resource-name string with no backing * xce_resource_t makes that key_find() miss (-ENOENT), which is NOT the * same as "genuinely a per-host, not-shared-cluster-wide resource" for this * check's purposes, so this fixture must exist for is_host_based_limit()'s * implicit-host-scope rule to be exercised at all. */ static xce_resource_t* register_per_host_resource(xce_sched_sim_driver_t* sim, const char* resname) { xce_resource_t wire_resource; xce_resource_t* r; memset(&wire_resource, 0, sizeof(wire_resource)); wire_resource.name = (char*)resname; wire_resource.valtype = XCE_VAL_NUMERIC; wire_resource.flags = XCE_RESF_CONSUMABLE; r = makeResource(sim->core.resmgr, &wire_resource); TEST_ASSERT_NOT_NULL(r); return r; } /* * add_configured_percent_limit - task 2.3's own Requirement 1.6 POSITIVE * fixture: a percent-based limit (val.percent=1) on `resname`, with NO * explicit per_host/hosts config (per_host left at its zero default, same * "no explicit scoping" shape make_limit_config()/add_configured_limit() * already establish for the other three dimensions) -- the exact shape * is_host_based_limit()'s own implicit-host-scope rule targets (xcelimit.c: * resource flags lack XCE_RESF_GLOBAL/XCE_RESF_SHARED AND * limit->val.percent != 0 -> host-based regardless of configured * per_host/hosts). */ static void add_configured_percent_limit(xce_sched_plugin_t* plugin, const char* name, const char* resname) { xce_limit_val_t val = { .resname = (char*)resname, .percent = 1, .val = 50.0 }; xce_limit_config_t limit_cfg = { .name = (char*)name, .val = &val }; xce_limit_config_t* limits[1] = { &limit_cfg }; xce_conf4sched_t config; int rc; memset(&config, 0, sizeof(config)); config.nlimits = 1; config.limits = limits; rc = limit_update_config(plugin, &config); TEST_ASSERT_EQUAL_INT(0, rc); } /* * add_configured_percent_limit_explicit_per_host - task 2.3's own * self-review fixture (design.md: "if CONSUMER_HOST IS explicitly * configured, fmt_limit()'s own output already shows PER_HOST = [...], so * no extra note is needed there"): identical to * add_configured_percent_limit() above except `.per_host = 1` is set * explicitly, so build_consumer() (xcelimit.c) marks * consumers[CONSUMER_HOST]->per_consumer = 1 -- the genuinely-explicit case * the Requirement 1.6 note must NOT duplicate. */ static void add_configured_percent_limit_explicit_per_host( xce_sched_plugin_t* plugin, const char* name, const char* resname) { xce_limit_val_t val = { .resname = (char*)resname, .percent = 1, .val = 50.0 }; xce_limit_config_t limit_cfg = { .name = (char*)name, .per_host = 1, .val = &val }; xce_limit_config_t* limits[1] = { &limit_cfg }; xce_conf4sched_t config; int rc; memset(&config, 0, sizeof(config)); config.nlimits = 1; config.limits = limits; rc = limit_update_config(plugin, &config); TEST_ASSERT_EQUAL_INT(0, rc); } /* * add_configured_limit_with_value - task 8.2's own fixture: an * absolute-value (percent=0) limit on `resname` with a caller-supplied * `val_amount`, so a test can pick a byte amount large enough to actually * exercise xce_format_bytes_human()'s KB/MB/GB scaling (make_limit_config()'s * own fixed 10.0 is too small to distinguish "raw" from "human" rendering in * a reader-legible way -- both render as single-digit text). Otherwise * identical in shape to add_configured_percent_limit() above (same * `xce_limit_config_t`-on-the-stack, single-limit `xce_conf4sched_t`, real * `limit_update_config()` drive). */ static void add_configured_limit_with_value(xce_sched_plugin_t* plugin, const char* name, const char* resname, double val_amount) { xce_limit_val_t val = { .resname = (char*)resname, .percent = 0, .val = val_amount }; xce_limit_config_t limit_cfg = { .name = (char*)name, .val = &val }; xce_limit_config_t* limits[1] = { &limit_cfg }; xce_conf4sched_t config; int rc; memset(&config, 0, sizeof(config)); config.nlimits = 1; config.limits = limits; rc = limit_update_config(plugin, &config); TEST_ASSERT_EQUAL_INT(0, rc); } /* ---------------------------------------------------------------------- */ /* Live-usage fixture helpers (task 3.1, Requirement 2.1/2.2/2.3/3.3/1.4) */ /* ---------------------------------------------------------------------- */ /* * instantiate_and_register_global_vres - drives the real, already-linked * instantiate_limit() (xcelimit.c, part of libxce -- NOT plugins/limit.c, * so no new link dependency) directly against `matched_limit` to create a * genuine xce_vres_t exactly the way match_job_global_limit_() * (plugins/limit.c) would on a real job match, then key_add()s it into * `plugin`'s privdata->instances -- this TU has direct access to * limit_plugin_data_t since it white-box #includes ../src/plugins/limit.c * itself, so this is the same table entry limit_get_usage_instances() * exposes, populated the same way production code populates it. * * `matched_limit` must be configured the way add_configured_limit() shapes * it (absolute value, i.e. percent=0, on "mem") so is_host_based_limit() * returns 0 and instantiate_limit()'s internal `shared` local stays 1 -- * add_vres() then flags the resulting vres->vres XCE_RESF_SHARED| * XCE_RESF_GLOBAL, the exact case task 3.1's own scope covers (a per-host * vres is task 3.2's job, deliberately not exercised here). * * user/queue/project are passed as empty strings, never NULL -- * instantiate_limit()/build_vres_name() both reject NULL outright (see * xcelimit.c). Since this fixture's limit has no per-user/queue/project * scoping configured (per_consumer=0 for all three, add_configured_limit()'s * own shape), build_vres_name() never actually reads these values into the * vresname it builds anyway -- only "limit=" is ever produced for * this fixture family. */ static xce_vres_t* instantiate_and_register_global_vres( xce_sched_sim_driver_t* sim, xce_sched_plugin_t* plugin, xce_limit_t* matched_limit) { limit_plugin_data_t* privdata = (limit_plugin_data_t*)plugin->data; xce_vres_t* vres = NULL; int rc; rc = instantiate_limit(&vres, sim->core.resmgr, matched_limit, "", "", ""); /* instantiate_limit() returns its own final key_find()'s positive * result on success (matching match_job_global_limit_()'s own * `if (rc < 0) return rc;` success convention, plugins/limit.c), not * literally 0 -- do not assert rc == 0 here. */ TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_NULL(vres); TEST_ASSERT_NOT_NULL(vres->vres); TEST_ASSERT_TRUE(vres->vres->idx >= 0); TEST_ASSERT_TRUE_MESSAGE( (vres->vres->flags & (XCE_RESF_GLOBAL | XCE_RESF_SHARED)) != 0, "fixture sanity: this helper only ever produces a GLOBAL/SHARED " "vres -- a per-host vres would mean the fixture's own limit shape " "drifted from add_configured_limit()'s absolute-value contract"); rc = key_add(privdata->instances, (void*)vres->vres->name, (void*)vres); TEST_ASSERT_TRUE(rc >= 0); return vres; } /* * put_shared_fvalue - manually populates sim->core.sharedInst or * core.sharedInUse at `idx` with a Value_u_fvalue xce_val_t -- the exact * shape put_resvals() (resource.c, deliberately NOT linked into this TU, * see this file's own header comment) would build. Constructed by hand * here rather than driving the real put_resvals()/update_limit_res_value() * so this fixture does not need to link resource.c for real -- a * deliberate, simpler-and-equally-valid choice this task's own brief calls * out ("directly construct an xce_vres_t ... and manually populate * core.sharedInst/core.sharedInUse via zarray_put() ... matching how * update_limit_res_value()/put_resvals() do it"). */ static void put_shared_fvalue(zarray_t* array, int idx, double val) { xce_val_t* v = (xce_val_t*)calloc(1, sizeof(*v)); int rc; TEST_ASSERT_NOT_NULL(v); v->which = Value_u_fvalue; v->u.fvalue = val; rc = zarray_put(array, idx, (void*)v); TEST_ASSERT_TRUE(rc >= 0); } /* * instantiate_and_register_host_vres - task 3.2's own fixture, mirroring * instantiate_and_register_global_vres() above but for a HOST-SCOPED * xce_vres_t: drives the real, already-linked instantiate_limit() * (xcelimit.c) against `matched_limit` the same way, then key_add()s it * into `plugin`'s privdata->instances. * * `matched_limit` must be configured the way add_configured_percent_limit() * shapes it (percent-based, on a resource register_per_host_resource() * registered as non-GLOBAL/non-SHARED) so is_host_based_limit() returns > 0 * and instantiate_limit()'s internal `shared` local stays 0 -- add_vres() * then flags the resulting vres->vres XCE_RESF_DEFAULT_HOST (never GLOBAL/ * SHARED), the exact case task 3.2's own scope covers. */ static xce_vres_t* instantiate_and_register_host_vres( xce_sched_sim_driver_t* sim, xce_sched_plugin_t* plugin, xce_limit_t* matched_limit) { limit_plugin_data_t* privdata = (limit_plugin_data_t*)plugin->data; xce_vres_t* vres = NULL; int rc; rc = instantiate_limit(&vres, sim->core.resmgr, matched_limit, "", "", ""); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_NULL(vres); TEST_ASSERT_NOT_NULL(vres->vres); TEST_ASSERT_TRUE(vres->vres->idx >= 0); TEST_ASSERT_TRUE_MESSAGE( (vres->vres->flags & (XCE_RESF_GLOBAL | XCE_RESF_SHARED)) == 0, "fixture sanity: this helper only ever produces a host-scoped " "(non-GLOBAL/non-SHARED) vres -- a GLOBAL/SHARED vres would mean " "the fixture's own limit shape drifted from " "add_configured_percent_limit()'s implicit-host-scope contract"); rc = key_add(privdata->instances, (void*)vres->vres->name, (void*)vres); TEST_ASSERT_TRUE(rc >= 0); return vres; } /* * build_bare_worker - registers a real xce_sched_worker_t named `hostname` * into `sim`'s core via the real XCE_CORE_EVENT_WORKERS_PUSH path * (core_events.c's core_upsert_worker()/core_build_worker(), the exact * production path test_repl_resource_table.c's own * build_worker_with_one_numeric_resource() already drives) -- this * zarray_init()s a real resvals/resInUse pair on the new worker, each with * a real killer (kill_sched_resval, stubbed above), so this file's own * put_shared_fvalue() can safely zarray_put() directly into a returned * worker's OWN resvals/resInUse afterward (task 3.2's fan-out reads * exactly those two zarrays per worker) without this fixture needing to * drive the real put_resvals()/update_limit_res_value() (resource.c, * deliberately not linked into this TU, see this file's own header * comment). No wire resvals are carried on the push itself * (nresvals_to_sched=0) -- this fixture only needs the worker to exist with * its own zarrays ready for a direct put_shared_fvalue() write at the * limit's own shadow-resource idx. */ static xce_sched_worker_t* build_bare_worker(xce_sched_sim_driver_t* sim, const char* hostname) { xce_worker_t* wire_worker; xce_worker_t* worker_ptrs[1]; xce_core_event_t events[1]; int rc; wire_worker = (xce_worker_t*)calloc(1, sizeof(*wire_worker)); TEST_ASSERT_NOT_NULL(wire_worker); wire_worker->name = (char*)hostname; wire_worker->status = 0; wire_worker->nresvals_to_sched = 0; wire_worker->resvals_to_sched = NULL; worker_ptrs[0] = wire_worker; memset(events, 0, sizeof(events)); events[0].type = XCE_CORE_EVENT_WORKERS_PUSH; events[0].u.workers_push.nworkers = 1; events[0].u.workers_push.workers = worker_ptrs; rc = xce_core_push_events(&(sim->core), events, 1); TEST_ASSERT_EQUAL_INT(0, rc); { xce_sched_worker_t* w = sched_get_worker(&(sim->core), (char*)hostname); TEST_ASSERT_NOT_NULL(w); return w; } } /* ---------------------------------------------------------------------- */ /* Tests */ /* ---------------------------------------------------------------------- */ /* * Requirement 1.7: `info limit all` against a fixture with zero configured * limits (the limit plugin was never registered at all -- the realistic * "nothing configured" scenario per design.md's Error Handling) must * report that no limits are configured. */ static void test_info_limit_all_reports_no_limits_configured_when_plugin_never_registered( void) { xce_sched_sim_driver_t sim; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); captured = capture_info_limit(&sim, "all"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "no limits configured"), "Requirement 1.7: 'info limit all' against a fixture with zero " "configured limits must report that no limits are configured"); free(captured); printf("test_info_limit_all_reports_no_limits_configured_when_plugin_" "never_registered: PASS\n"); } /* * Requirement 1.7 / design.md's Error Handling section: a REGISTERED * plugin with zero configured limits (privdata->limits is a real, non-NULL, * empty kvmgr) must ALSO collapse to "no limits configured" on the `all` * path -- not an empty listing with no explanation. */ static void test_info_limit_all_reports_no_limits_configured_when_plugin_registered_with_zero_limits( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); /* Deliberately no add_configured_limit() call -- plugin registered, * privdata->limits real but empty. */ captured = capture_info_limit(&sim, "all"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "no limits configured"), "a registered plugin with zero configured limits must also report " "'no limits configured' on the 'all' path"); free(captured); printf("test_info_limit_all_reports_no_limits_configured_when_plugin_" "registered_with_zero_limits: PASS\n"); } /* * Positive-path proof for the `all` resolution: once at least one limit is * genuinely configured, the Requirement 1.7 message must NOT appear -- * proves the all-path iteration reaches the real, non-empty handle rather * than misfiring the empty-case message unconditionally. */ static void test_info_limit_all_does_not_report_no_limits_configured_when_limits_are_configured( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "ug1_slots"); captured = capture_info_limit(&sim, "all"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "no limits configured"), "'info limit all' must NOT report 'no limits configured' once at " "least one limit is genuinely configured"); free(captured); printf("test_info_limit_all_does_not_report_no_limits_configured_when_" "limits_are_configured: PASS\n"); } /* * Requirement 1.3 (this task's own Observable): a named lookup for a limit * name that is NOT configured, against a fixture with OTHER limits * configured, must report "no such limit" -- and must never report * Requirement 1.7's all-only message. */ static void test_info_limit_named_reports_no_such_limit_for_unconfigured_name_against_fixture_with_other_limits( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "ug1_slots"); captured = capture_info_limit(&sim, "does_not_exist"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "no such limit 'does_not_exist'"), "Requirement 1.3: a named lookup for an unconfigured name, against " "a fixture with other limits configured, must report 'no such " "limit'"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "no limits configured"), "Requirement 1.7's message is reserved for the 'all' path -- a " "named-lookup miss must never say 'no limits configured'"); free(captured); printf("test_info_limit_named_reports_no_such_limit_for_unconfigured_" "name_against_fixture_with_other_limits: PASS\n"); } /* * THE central scoping-regression guard (design.md's own explicit * correction, called out by this task's brief): a NULL * limit_get_configured_limits() handle on the NAMED path (the limit plugin * was never registered at all) must report Requirement 1.3's "no such * limit ''" -- and must NEVER fall through to Requirement 1.7's * all-only "no limits configured" message, even though both conditions * stem from the identical underlying NULL handle. */ static void test_info_limit_named_reports_no_such_limit_when_plugin_never_registered_not_no_limits_configured( void) { xce_sched_sim_driver_t sim; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); captured = capture_info_limit(&sim, "foo"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "no such limit 'foo'"), "a NULL limit_get_configured_limits() handle on the NAMED path " "(plugin never registered) must report 'no such limit', never " "Requirement 1.7's all-only message"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "no limits configured"), "a NULL handle on the named path must never be treated as 'no " "limits configured' -- that message is exclusively the 'all' " "path's, per its own acceptance criterion's scoping"); free(captured); printf("test_info_limit_named_reports_no_such_limit_when_plugin_never_" "registered_not_no_limits_configured: PASS\n"); } /* * Positive-path proof for the named-lookup resolution: a name that IS * genuinely configured must never trigger the "no such limit" message -- * proves key_find() resolves a real hit, not a false miss (would fail if * the resolution logic were broken or reversed). */ static void test_info_limit_named_does_not_report_no_such_limit_for_configured_name(void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "ug1_slots"); captured = capture_info_limit(&sim, "ug1_slots"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "no such limit"), "a named lookup for a limit that genuinely IS configured must " "never report 'no such limit'"); free(captured); printf("test_info_limit_named_does_not_report_no_such_limit_for_" "configured_name: PASS\n"); } /* * Task 2.2's own Observable #1 (Requirement 1.1, 1.5): a matched limit's * configured setting is actually printed -- for the first time, * repl_info_limit() finding a real match must emit something beyond * silence. A limit configured with an exclusion group ("all except X" on * the HOST dimension) must show BOTH the inclusion member and the * exclusion member (fmt_group()'s "~" prefix) in the same displayed scope * line, not just the presence of some output. */ static void test_info_limit_named_shows_both_inclusion_and_exclusion_membership(void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit_with_host_exclusion(&plugin, "host_limit1", "all", "excluded_host"); captured = capture_info_limit(&sim, "host_limit1"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "limit: host_limit1"), "the matched limit's fmt_limit() dump must include its name line " "(proves real fmt_limit() output, not a stub)"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "HOST = [all ~excluded_host]"), "Requirement 1.5: the HOST scope line must show BOTH the inclusion " "member ('all') and the exclusion member ('~excluded_host', " "fmt_group()'s own exclusion prefix) together, distinguishing " "included members from explicitly excluded ones"); free(captured); printf("test_info_limit_named_shows_both_inclusion_and_exclusion_" "membership: PASS\n"); } /* * Task 2.2's own Observable #2 (Requirement 1.2): `info limit all` against * a fixture with MULTIPLE configured limits must display every one's * configured setting, not just the first match or a truncated listing. */ static void test_info_limit_all_displays_every_configured_limits_setting(void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "limitA"); add_configured_limit(&plugin, "limitB"); captured = capture_info_limit(&sim, "all"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "limit: limitA"), "Requirement 1.2: 'info limit all' must display limitA's " "configured setting"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "limit: limitB"), "Requirement 1.2: 'info limit all' must display limitB's " "configured setting too -- every configured limit, not just one"); free(captured); printf("test_info_limit_all_displays_every_configured_limits_setting: " "PASS\n"); } /* * Task 2.3's own Observable #1 (Requirement 1.6, positive case): a * percent-based limit on a resource that is registered but not * GLOBAL/SHARED (an ordinary per-host resource, "mem"), with NO explicit * per-host scoping configured, must display the Requirement 1.6 effective- * host-scope note after fmt_limit()'s own dump -- is_host_based_limit()'s * implicit rule (xcelimit.c) treats this limit as host-based purely because * of its percentage value on a non-shared resource, regardless of the * limit's own consumers[CONSUMER_HOST] configuration, and Requirement 1.6 * requires the REPL to surface that to the operator. */ static void test_info_limit_named_shows_host_scope_note_for_implicit_percent_based_limit( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_percent_limit(&plugin, "pct_limit", "mem"); captured = capture_info_limit(&sim, "pct_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "limit: pct_limit"), "the matched limit's fmt_limit() dump must still print first " "(proves this note is appended after, not instead of, task 2.2's " "own output)"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "tracked separately per host"), "Requirement 1.6: a percent-based limit on a resource that is not " "shared cluster-wide, with no explicit host scoping configured, " "must display the effective host-scope note"); free(captured); printf("test_info_limit_named_shows_host_scope_note_for_implicit_" "percent_based_limit: PASS\n"); } /* * Task 2.3's own Observable #2 (Requirement 1.6, negative contrast): an * absolute-valued limit (percent=0) on the SAME per-host resource must NOT * display the note -- is_host_based_limit()'s implicit rule only fires for * a PERCENTAGE value, so an absolute-value limit is never implicitly * host-based and no host-scope note applies. Reuses add_configured_limit()'s * existing absolute-value ("mem", percent=0, val=10.0) fixture shape, * proving the contrast is driven purely by percent vs. absolute, not by * some other difference between fixtures. */ static void test_info_limit_named_does_not_show_host_scope_note_for_absolute_value_limit( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "abs_limit"); captured = capture_info_limit(&sim, "abs_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "limit: abs_limit"), "sanity: the matched limit's fmt_limit() dump must still print " "(proves this really matched a real, configured limit)"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "tracked separately per host"), "Requirement 1.6: an absolute-valued limit is not implicitly " "host-based, so the effective host-scope note must NOT appear"); free(captured); printf("test_info_limit_named_does_not_show_host_scope_note_for_" "absolute_value_limit: PASS\n"); } /* * Task 2.3's own self-review guard (design.md: "if CONSUMER_HOST IS * explicitly configured, fmt_limit()'s own output already shows * PER_HOST = [...], so no extra note is needed there"): a percent-based * limit on the same non-shared resource, but with per_host EXPLICITLY * configured, must show fmt_limit()'s own "PER_HOST = [...]" line but must * NOT ALSO print the Requirement 1.6 note -- proves the condition does not * accidentally fire for the genuinely-explicit case too (it would be * redundant, since the operator already sees the host dimension called out * by task 2.2's own output). */ static void test_info_limit_named_does_not_duplicate_host_scope_note_when_per_host_explicitly_configured( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_percent_limit_explicit_per_host(&plugin, "explicit_pct_limit", "mem"); captured = capture_info_limit(&sim, "explicit_pct_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "PER_HOST ="), "sanity: fmt_limit() must show a PER_HOST scope line for an " "explicitly per-host-scoped limit (task 2.2's own output)"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "tracked separately per host"), "Requirement 1.6's note would be redundant once fmt_limit() " "already shows PER_HOST -- it must not duplicate"); free(captured); printf("test_info_limit_named_does_not_duplicate_host_scope_note_when_" "per_host_explicitly_configured: PASS\n"); } /* * Task 3.1's own central Observable #1 (Requirement 2.1, 1.4, 3.3): a * GLOBAL/SHARED limit instance that has genuinely been matched AND charged * shows an accounted/capacity pair reflecting that real state -- not a * placeholder or the no-instance message. */ static void test_info_limit_named_shows_accounted_and_capacity_for_matched_and_charged_global_instance( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "charged_limit"); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"charged_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_global_vres(&sim, &plugin, matched); put_shared_fvalue(&(sim.core.sharedInst), vres->vres->idx, 10.0); put_shared_fvalue(&(sim.core.sharedInUse), vres->vres->idx, 4.0); captured = capture_info_limit(&sim, "charged_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "accounted=4.000000 (4.00 B)"), "Requirement 2.1/6.1: a global limit matched and charged must show " "its accounted usage, classified by the limit's OWN constrained " "resource ('mem') and rendered human-readable -- a bare " "\"accounted=4.00\" substring check would also match the pre-fix " "vres->vres->name-misclassified plain \"accounted=4.00\" output, " "so this asserts the full corrected \" ()\" string " "instead"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "capacity=10.000000 (10.00 B)"), "Requirement 2.1/6.1: ... alongside its configured capacity, same " "full-string rationale as the accounted check above"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "no active usage instance yet"), "a genuinely matched+charged instance must not report 'no active " "usage instance yet'"); free(captured); printf("test_info_limit_named_shows_accounted_and_capacity_for_matched_" "and_charged_global_instance: PASS\n"); } /* * Task 3.1's own central Observable #2 (Requirement 2.2): a configured * limit that has NEVER been evaluated against any job (no xce_vres_t at * all in privdata->instances for it) must report the explicit no-instance * message, never a fabricated zero-usage line. */ static void test_info_limit_named_shows_no_active_usage_instance_for_never_matched_limit( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "never_matched_limit"); /* Deliberately no instantiate_and_register_global_vres() call -- this * limit has never been evaluated against any job, so * privdata->instances has no xce_vres_t at all for it. */ captured = capture_info_limit(&sim, "never_matched_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "no active usage instance yet"), "Requirement 2.2: a configured-but-never-evaluated limit must " "report no active usage instance, not a fabricated zero"); free(captured); printf("test_info_limit_named_shows_no_active_usage_instance_for_never_" "matched_limit: PASS\n"); } /* * Task 3.1's own central Observable #3 -- THE existence-vs-zero-usage * distinction (Requirement 2.2's own most important correctness property, * per design.md's explicit callout): a vres that genuinely exists AND has * a real capacity entry, but has never been charged (no core.sharedInUse * entry), must display a genuine accounted=0 -- NOT the no-instance * message, since the instance itself (and its capacity) both genuinely * exist. */ static void test_info_limit_named_shows_accounted_zero_for_matched_but_never_charged_instance( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "uncharged_limit"); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"uncharged_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_global_vres(&sim, &plugin, matched); put_shared_fvalue(&(sim.core.sharedInst), vres->vres->idx, 10.0); /* Deliberately no put_shared_fvalue() into core.sharedInUse -- a * genuinely matched vres with a real capacity entry, but nothing * charged against it yet. */ captured = capture_info_limit(&sim, "uncharged_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "accounted=0.000000 (0.00 B)"), "the existence-vs-zero-usage distinction: a matched vres with a " "real capacity entry but an absent in-use entry must display a " "genuine accounted=0, not the no-instance message -- full " "human-readable string (Requirement 6.1), not a bare \"accounted=" "0.00\" substring that would equally match the pre-fix " "misclassified output"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "no active usage instance yet"), "must NOT report 'no active usage instance yet' -- the vres and " "its capacity both genuinely exist"); free(captured); printf("test_info_limit_named_shows_accounted_zero_for_matched_but_" "never_charged_instance: PASS\n"); } /* * Task 3.2's own central Observable (Requirement 3.1, 3.2, 3.4): a * host-scoped limit matched against jobs on TWO distinct hosts shows TWO * SEPARATE usage lines, each labeled with its own host -- and a THIRD * configured-but-untouched host (registered as a real worker, but never * given a populated capacity entry at this limit's shadow-resource idx) * shows no line at all, proving existence is gated on a per-worker * capacity read, not on the worker merely existing. */ static void test_info_limit_named_shows_separate_per_host_usage_lines_for_touched_hosts_and_none_for_untouched_host( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; xce_sched_worker_t* wA; xce_sched_worker_t* wB; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_percent_limit(&plugin, "host_usage_limit", "mem"); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"host_usage_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_host_vres(&sim, &plugin, matched); wA = build_bare_worker(&sim, "hostA"); wB = build_bare_worker(&sim, "hostB"); /* hostC is registered as a real worker but deliberately never given a * populated capacity entry below -- configured, but untouched by this * limit. */ (void)build_bare_worker(&sim, "hostC"); put_shared_fvalue(&(wA->resvals), vres->vres->idx, 5.0); put_shared_fvalue(&(wA->resInUse), vres->vres->idx, 2.0); put_shared_fvalue(&(wB->resvals), vres->vres->idx, 5.0); put_shared_fvalue(&(wB->resInUse), vres->vres->idx, 3.0); captured = capture_info_limit(&sim, "host_usage_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "host=hostA"), "Requirement 3.2/3.4: a host-scoped limit matched on hostA must " "show a usage line labeled with hostA"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "host=hostB"), "Requirement 3.2/3.4: a host-scoped limit matched on hostB must " "show a SEPARATE usage line labeled with hostB"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "host=hostC"), "Observable: a third configured-but-untouched host must show no " "line at all, not a fabricated zero"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "accounted=2.000000 (2.00 B)"), "Requirement 2.1/6.1/6.4: hostA's own accounted usage must be " "shown, classified by the limit's OWN constrained resource ('mem') " "and rendered human-readable -- a bare \"accounted=2.00\" " "substring check would also match the pre-fix vres->vres->name-" "misclassified plain output"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "accounted=3.000000 (3.00 B)"), "Requirement 2.1/6.1/6.4: hostB's own accounted usage must be " "shown, distinct from hostA's, same full-string rationale as the " "hostA check above"); free(captured); printf("test_info_limit_named_shows_separate_per_host_usage_lines_for_" "touched_hosts_and_none_for_untouched_host: PASS\n"); } /* * Task 3.2's own existence-vs-zero-usage distinction, applied per host * (mirrors task 3.1's central Observable #3 for the GLOBAL/SHARED case): * a worker with a genuinely populated CAPACITY entry at this limit's * shadow-resource idx, but no in-use entry (never charged), must display a * genuine accounted=0 line for that host -- not omit the host entirely. */ static void test_info_limit_named_shows_accounted_zero_for_host_with_capacity_but_never_charged( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; xce_sched_worker_t* wA; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_percent_limit(&plugin, "host_uncharged_limit", "mem"); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"host_uncharged_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_host_vres(&sim, &plugin, matched); wA = build_bare_worker(&sim, "hostA"); put_shared_fvalue(&(wA->resvals), vres->vres->idx, 5.0); /* Deliberately no put_shared_fvalue() into wA->resInUse -- a genuinely * populated capacity entry, but nothing charged against it yet. */ captured = capture_info_limit(&sim, "host_uncharged_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "host=hostA"), "hostA has a genuinely populated capacity entry and must appear as " "a line, even though nothing has been charged against it yet"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "accounted=0.000000 (0.00 B)"), "the existence-vs-zero-usage distinction applies per host too: a " "populated capacity entry with an absent in-use entry must display " "a genuine accounted=0, not omit the host -- full human-readable " "string (Requirement 6.1/6.4), not a bare \"accounted=0.00\" " "substring that would equally match the pre-fix misclassified " "output"); free(captured); printf("test_info_limit_named_shows_accounted_zero_for_host_with_" "capacity_but_never_charged: PASS\n"); } /* * Requirement 2.2 applied to the host-scoped case as a whole: a host-scoped * xce_vres_t that genuinely exists (instantiated, real capacity idx * allocated) but for which NO worker has ever been given a populated * capacity entry must report the same "no active usage instance yet" * message the GLOBAL/SHARED case reports for an unmatched limit -- proving * repl_print_limit_usage_per_host()'s own zero-lines-printed return value * correctly threads through to repl_print_limit_usage()'s outer n_shown * fallback rather than silently printing nothing with no explanation. */ static void test_info_limit_named_shows_no_active_usage_instance_when_host_scoped_vres_exists_but_no_worker_populated( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_percent_limit(&plugin, "host_untouched_limit", "mem"); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"host_untouched_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); (void)instantiate_and_register_host_vres(&sim, &plugin, matched); /* A worker exists, but its capacity entry at this vres's idx is * deliberately never populated -- the limit's shadow resource has * never actually been written for any host. */ (void)build_bare_worker(&sim, "hostA"); captured = capture_info_limit(&sim, "host_untouched_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "no active usage instance yet"), "Requirement 2.2: a host-scoped vres that exists but has no worker " "with a populated capacity entry must report no active usage " "instance, not a fabricated zero and not silence with no " "explanation"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "host=hostA"), "hostA has no populated capacity entry and must not appear as a " "line"); free(captured); printf("test_info_limit_named_shows_no_active_usage_instance_when_host_" "scoped_vres_exists_but_no_worker_populated: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Task 9 (Requirement 6.1, 6.3, 6.4, 6.5): repl_print_limit_usage()'s */ /* GLOBAL/SHARED path and repl_print_limit_usage_per_host()'s per-host */ /* path both classify a live usage instance's accounted/capacity amounts */ /* by the instance's own synthesized vres name (vres->vres->name, e.g. */ /* "limit=") instead of the limit's own constrained resource */ /* (limit->val.resname / vres->limit->val.resname, "mem") -- */ /* xce_is_bytes_valued_resource() is therefore always asked about the */ /* wrong string and always returns false for a limit's usage instance, so */ /* a byte-valued limit's live accounted/capacity amounts never actually */ /* render human-readable even though the exact same limit's CONFIGURED- */ /* setting line (task 8.2) already does. These two tests prove the fix. */ /* ---------------------------------------------------------------------- */ /* * Cluster-wide (GLOBAL/SHARED) path fix -- mirrors * test_info_limit_named_shows_accounted_and_capacity_for_matched_and_ * charged_global_instance's exact fixture shape, substituting byte-scale * capacity/used amounts (the same 5368709120.0/5000000000.0 pair * test_info_limit_named_shows_byte_valued_amount_human_readable_under_ * default_units_mode and test_utils.c's own fractional-scaling test already * establish as "5.00 GB"/"4.66 GB") for the pre-existing fixture's small * 10.0/4.0 pair (too small to distinguish raw from human rendering in a * reader-legible way). RED against today's code: vres->vres->name (e.g. * "limit=byte_usage_limit") is never "mem", so repl_is_bytes_valued_ * resource() returns false and both amounts fall back to plain "%.2f" -- * neither "GB" string below appears. */ static void test_info_limit_named_shows_global_usage_amounts_human_readable_for_byte_valued_limit( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "byte_usage_limit"); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"byte_usage_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_global_vres(&sim, &plugin, matched); put_shared_fvalue(&(sim.core.sharedInst), vres->vres->idx, 5368709120.0); put_shared_fvalue(&(sim.core.sharedInUse), vres->vres->idx, 5000000000.0); captured = capture_info_limit(&sim, "byte_usage_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "capacity=5368709120.000000 (5.00 GB)"), "Requirement 6.1/6.5: a byte-valued limit's live CAPACITY amount " "must render human-readable, classified by the limit's OWN " "constrained resource (limit->val.resname, 'mem') -- this fails " "against today's buggy vres->vres->name classification, which " "always prints the plain 'capacity=5368709120.00' fallback " "instead"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "accounted=5000000000.000000 (4.66 GB)"), "Requirement 6.1/6.5: ... and its live ACCOUNTED amount too, using " "the exact same classification fix"); free(captured); printf( "test_info_limit_named_shows_global_usage_amounts_human_readable_" "for_byte_valued_limit: PASS\n"); } /* * Per-host path fix, doubling as Requirement 6.4's own proof: this * fixture's limit is itself PERCENTAGE-based (add_configured_percent_ * limit(), val.percent=1 -- the exact shape task 3.2's own host-scoped * fixtures already use), so a passing test here demonstrates that a limit * expressed as a percentage of a byte-valued resource shows its RESOLVED * ABSOLUTE per-host usage amounts (not the percentage itself) in * human-readable form too. Mirrors test_info_limit_named_shows_separate_ * per_host_usage_lines_for_touched_hosts_and_none_for_untouched_host's * exact fixture shape, substituting byte-scale capacity/used amounts for * the pre-existing fixture's small 5.0/2.0 pair. RED against today's code: * same vres->vres->name misclassification as the GLOBAL/SHARED test above. */ static void test_info_limit_named_shows_per_host_usage_amounts_human_readable_for_percent_based_byte_valued_limit( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; xce_sched_worker_t* wA; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_percent_limit(&plugin, "byte_host_usage_limit", "mem"); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"byte_host_usage_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_host_vres(&sim, &plugin, matched); wA = build_bare_worker(&sim, "hostA"); put_shared_fvalue(&(wA->resvals), vres->vres->idx, 5368709120.0); put_shared_fvalue(&(wA->resInUse), vres->vres->idx, 5000000000.0); captured = capture_info_limit(&sim, "byte_host_usage_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "host=hostA"), "sanity: hostA's populated capacity entry must still produce a " "usage line"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "capacity=5368709120.000000 (5.00 GB)"), "Requirement 6.1/6.4/6.5: a percentage-based limit's per-host " "RESOLVED ABSOLUTE capacity amount must render human-readable, " "classified by the limit's OWN constrained resource " "(vres->limit->val.resname, 'mem') -- this fails against today's " "buggy vres->vres->name classification, which always prints the " "plain 'capacity=5368709120.00' fallback instead"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "accounted=5000000000.000000 (4.66 GB)"), "Requirement 6.1/6.4/6.5: ... and its resolved absolute ACCOUNTED " "amount too, not the percentage itself"); free(captured); printf( "test_info_limit_named_shows_per_host_usage_amounts_human_readable_" "for_percent_based_byte_valued_limit: PASS\n"); } /* * Task 4 (Requirement Set 1, Req 4.1/4.2): the first tests in this file to * drive `info limit `/`info limit all` through the REAL repl_dispatch() * entry point (capture_dispatch(), above) instead of calling * repl_info_limit() directly (capture_info_limit(), used by every test * above this point) -- proving the `else if (strcmp(tokens[1], "limit") == * 0)` dispatch branch this task adds to repl_cmd_info() (repl_info.c) * actually routes real typed-in REPL input to repl_info_limit(), not just * that repl_info_limit() itself works in isolation (already proven by every * test above). Before this task's GREEN phase, repl_cmd_info() has no * "limit" branch at all, so `tokens[1] == "limit"` falls into its own * unrecognized-subcommand else branch ("[sim] info: unrecognized subcommand * 'limit'") -- both assertions below are expected to fail at RED. */ static void test_info_limit_dispatch_named_reaches_repl_info_limit_via_real_dispatch( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "ug1_slots"); captured = capture_dispatch(&sim, "info limit ug1_slots"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "unrecognized subcommand"), "Req 4.1: 'info limit ' typed as real REPL input must be " "recognized -- repl_cmd_info() must route it to repl_info_limit(), " "not fall through to the unrecognized-subcommand branch"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "limit: ug1_slots"), "Req 4.1: dispatching 'info limit ug1_slots' through repl_dispatch() " "must produce the same fmt_limit() dump a direct repl_info_limit() " "call already produces for this fixture"); free(captured); printf("test_info_limit_dispatch_named_reaches_repl_info_limit_via_real_" "dispatch: PASS\n"); } static void test_info_limit_dispatch_all_reaches_repl_info_limit_via_real_dispatch(void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "limitA"); add_configured_limit(&plugin, "limitB"); captured = capture_dispatch(&sim, "info limit all"); TEST_ASSERT_NULL_MESSAGE( strstr(captured, "unrecognized subcommand"), "Req 4.2: 'info limit all' typed as real REPL input must be " "recognized -- repl_cmd_info() must route it to repl_info_limit(), " "not fall through to the unrecognized-subcommand branch"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "limit: limitA"), "Req 4.2: dispatching 'info limit all' through repl_dispatch() must " "display every configured limit's setting, starting with limitA"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "limit: limitB"), "Req 4.2: ... and limitB too -- every configured limit, not just " "one"); free(captured); printf("test_info_limit_dispatch_all_reaches_repl_info_limit_via_real_" "dispatch: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Task 8.2 (Requirement 6.1/6.2/6.5): the configured-setting display must */ /* render a byte-valued limit's amount using the operator's own selected */ /* units mode, via fmt_limit_units()/translate_units_mode()/ */ /* repl_current_units_mode() (tasks 7/8.1), not the RAW-only fmt_limit(). */ /* ---------------------------------------------------------------------- */ /* * Direct-call path (no repl_state ever allocated -- sim.repl_state stays * NULL, so repl_current_units_mode() falls back to its own documented * default, XCE_REPL_UNITS_BOTH -- the same default repl_state_ensure() * itself assigns). Requirement 6.1/6.2: under that default mode, a * byte-valued ("mem") limit's configured amount must show human-readable * scaling, not a bare byte count -- proving the REPL's own already-selected * display mode (defaulted or explicit) reaches fmt_limit_units(), not * fmt_limit()'s hardcoded XCE_UNITS_RAW. */ static void test_info_limit_named_shows_byte_valued_amount_human_readable_under_default_units_mode( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_limit_plugin(&sim, &plugin, &group_sched); /* 5 GiB (5 * 1024^3 bytes) -- large enough that xce_format_bytes_human() * scales it all the way to "5.00 GB", unmistakably distinct from the raw * "5368709120.000000" fmt_limit()'s hardcoded RAW mode would print. */ add_configured_limit_with_value(&plugin, "mem_limit", "mem", 5368709120.0); captured = capture_info_limit(&sim, "mem_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "limit: mem_limit"), "sanity: the matched limit's fmt_limit_units() dump must still " "print the limit's own name line"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured, "5.00 GB"), "Requirement 6.1/6.2: under the REPL's default units mode (BOTH), " "a byte-valued limit's configured amount must render in " "human-readable form -- this fails against today's fmt_limit()-" "calling code, which is hardcoded to XCE_UNITS_RAW regardless of " "the operator's selected/default display mode and would only ever " "print the raw byte count"); free(captured); printf( "test_info_limit_named_shows_byte_valued_amount_human_readable_" "under_default_units_mode: PASS\n"); } /* * Dispatch path, through the real `set units raw|human|both` command * (repl_params.c) followed by the real `info limit ` dispatch -- * Requirement 6.2/6.5: switching the operator's selected display mode must * consistently change a byte-valued limit's configured-amount rendering, * the same units convention every other byte-valued resource display in the * REPL already honors. */ static void test_info_limit_named_configured_amount_changes_consistently_with_units_mode( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured_raw; char* captured_human; char* captured_both; bool handled; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); sim.repl_state = repl_state_ensure(&sim); TEST_ASSERT_NOT_NULL(sim.repl_state); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit_with_value(&plugin, "mem_limit2", "mem", 5368709120.0); handled = repl_dispatch(&sim, "set units raw"); TEST_ASSERT_FALSE(handled); captured_raw = capture_dispatch(&sim, "info limit mem_limit2"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_raw, "mem = 5368709120.000000"), "`set units raw` must render the configured amount as a bare byte " "count, matching fmt_limit()'s pre-existing RAW-mode contract"); TEST_ASSERT_NULL_MESSAGE( strstr(captured_raw, "GB"), "RAW mode must not include any human-readable unit suffix"); free(captured_raw); handled = repl_dispatch(&sim, "set units human"); TEST_ASSERT_FALSE(handled); captured_human = capture_dispatch(&sim, "info limit mem_limit2"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_human, "mem = 5.00 GB"), "Requirement 6.1/6.2: `set units human` must render the configured " "amount in human-readable form alone"); TEST_ASSERT_NULL_MESSAGE( strstr(captured_human, "5368709120"), "HUMAN mode must not include the raw byte count"); free(captured_human); handled = repl_dispatch(&sim, "set units both"); TEST_ASSERT_FALSE(handled); captured_both = capture_dispatch(&sim, "info limit mem_limit2"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_both, "mem = 5368709120.000000 (5.00 GB)"), "Requirement 6.5: `set units both` must render raw and " "human-readable together, the same combined shape every other " "byte-valued resource display in the REPL already uses"); free(captured_both); printf( "test_info_limit_named_configured_amount_changes_consistently_with_" "units_mode: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Task 5 (Requirement Set 1's own Validation task, Requirement 5): proves */ /* -- rather than merely asserting by construction, as every prior task's */ /* own tests above already do -- that displaying limit information never */ /* mutates configuration or accounting state (5.1), that issuing it mid- */ /* replay never perturbs scheduling decisions (5.2), and (via the full */ /* regression sweep reported in this task's own status report, not a new */ /* test here) that every pre-existing `info` subcommand's behavior is */ /* unchanged (5.3). */ /* ---------------------------------------------------------------------- */ /* * Requirement 5.1 (GLOBAL/SHARED path): snapshots the matched limit's own * configured fields (val, all four consumers) BY VALUE, and the matched * vres's capacity/in-use zarray slots BY POINTER IDENTITY (a `put_resvals`- * style accidental write swaps in a NEW xce_val_t* at that index even if * the new value happened to coincide numerically, so pointer identity is a * strictly stronger check than value equality alone) -- then calls * `info limit` repeatedly, in every arg form this fixture can produce a * match or non-match for, through BOTH the direct entry point * (repl_info_limit(), task 2's own call shape) and the real repl_dispatch() * entry point (task 4's own call shape), and re-checks every snapshot * afterward. */ static void test_info_limit_never_mutates_global_limit_config_or_accounting_state(void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; xce_val_t* cap_before; xce_val_t* used_before; xce_limit_val_t val_before; char* name_before; xce_consumer_t consumer_before[CONSUMER_MAX]; char* captured1; char* captured2; char* captured3; char* captured4; char* captured5; int rc; int i; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit(&plugin, "guarded_limit"); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"guarded_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_global_vres(&sim, &plugin, matched); put_shared_fvalue(&(sim.core.sharedInst), vres->vres->idx, 10.0); put_shared_fvalue(&(sim.core.sharedInUse), vres->vres->idx, 4.0); /* ---- Snapshot BEFORE. ---- */ name_before = matched->name; val_before = matched->val; for (i = 0; i < CONSUMER_MAX; i++) { if (matched->consumers[i] != NULL) { consumer_before[i] = *(matched->consumers[i]); } else { memset(&consumer_before[i], 0, sizeof(consumer_before[i])); } } cap_before = (xce_val_t*)zarray_get(&(sim.core.sharedInst), vres->vres->idx); used_before = (xce_val_t*)zarray_get(&(sim.core.sharedInUse), vres->vres->idx); TEST_ASSERT_NOT_NULL(cap_before); TEST_ASSERT_NOT_NULL(used_before); /* ---- Exercise `info limit` repeatedly, in every arg form this * fixture supports (named hit, "all", named miss), both via the direct * call and via the real repl_dispatch() entry point -- Requirement 5.1 * must hold for every one of these call shapes. ---- */ captured1 = capture_info_limit(&sim, "guarded_limit"); captured2 = capture_info_limit(&sim, "guarded_limit"); captured3 = capture_info_limit(&sim, "all"); captured4 = capture_dispatch(&sim, "info limit guarded_limit"); captured5 = capture_info_limit(&sim, "does_not_exist"); TEST_ASSERT_EQUAL_STRING_MESSAGE( captured1, captured2, "Requirement 5.2 (no intervening step): 'info limit ' issued " "twice in a row must produce byte-identical output"); /* ---- Snapshot AFTER -- must be byte-for-byte/pointer-for-pointer * identical to BEFORE. ---- */ TEST_ASSERT_TRUE_MESSAGE( matched->name == name_before, "Requirement 5.1: the matched limit's own name pointer must not be " "reallocated/reassigned by displaying it"); TEST_ASSERT_EQUAL_MEMORY_MESSAGE( &val_before, &(matched->val), sizeof(val_before), "Requirement 5.1: the matched limit's configured value (resname/" "percent/val) must be bit-for-bit unchanged after displaying it"); for (i = 0; i < CONSUMER_MAX; i++) { if (matched->consumers[i] != NULL) { TEST_ASSERT_EQUAL_MEMORY_MESSAGE( &consumer_before[i], matched->consumers[i], sizeof(consumer_before[i]), "Requirement 5.1: each consumer's per_consumer flag/group " "pointer must be unchanged after displaying the limit"); } } TEST_ASSERT_TRUE_MESSAGE( cap_before == (xce_val_t*)zarray_get(&(sim.core.sharedInst), vres->vres->idx), "Requirement 5.1: the capacity zarray slot must still hold the " "SAME xce_val_t* -- no accidental write/reallocation"); TEST_ASSERT_TRUE_MESSAGE( used_before == (xce_val_t*)zarray_get(&(sim.core.sharedInUse), vres->vres->idx), "Requirement 5.1: the in-use zarray slot must still hold the SAME " "xce_val_t* -- no accidental charge/write"); TEST_ASSERT_EQUAL_FLOAT(10.0f, (float)cap_before->u.fvalue); TEST_ASSERT_EQUAL_FLOAT(4.0f, (float)used_before->u.fvalue); free(captured1); free(captured2); free(captured3); free(captured4); free(captured5); printf("test_info_limit_never_mutates_global_limit_config_or_" "accounting_state: PASS\n"); } /* * Requirement 5.1, host-scoped path: mirrors the GLOBAL/SHARED no-mutation * test above, but for the per-worker fan-out path * (repl_print_limit_usage_per_host()) -- a host-scoped limit's capacity/ * in-use zarray entries live on EACH worker's OWN resvals/resInUse, not on * core.sharedInst/sharedInUse, so this is a genuinely distinct read path * needing its own no-mutation proof. */ static void test_info_limit_never_mutates_host_scoped_limit_accounting_state(void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; xce_sched_worker_t* wA; xce_sched_worker_t* wB; xce_val_t* capA_before; xce_val_t* usedA_before; xce_val_t* capB_before; xce_val_t* usedB_before; char* captured1; char* captured2; char* captured3; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_percent_limit(&plugin, "guarded_host_limit", "mem"); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"guarded_host_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_host_vres(&sim, &plugin, matched); wA = build_bare_worker(&sim, "hostA"); wB = build_bare_worker(&sim, "hostB"); put_shared_fvalue(&(wA->resvals), vres->vres->idx, 5.0); put_shared_fvalue(&(wA->resInUse), vres->vres->idx, 2.0); put_shared_fvalue(&(wB->resvals), vres->vres->idx, 5.0); put_shared_fvalue(&(wB->resInUse), vres->vres->idx, 3.0); capA_before = (xce_val_t*)zarray_get(&(wA->resvals), vres->vres->idx); usedA_before = (xce_val_t*)zarray_get(&(wA->resInUse), vres->vres->idx); capB_before = (xce_val_t*)zarray_get(&(wB->resvals), vres->vres->idx); usedB_before = (xce_val_t*)zarray_get(&(wB->resInUse), vres->vres->idx); TEST_ASSERT_NOT_NULL(capA_before); TEST_ASSERT_NOT_NULL(usedA_before); TEST_ASSERT_NOT_NULL(capB_before); TEST_ASSERT_NOT_NULL(usedB_before); captured1 = capture_info_limit(&sim, "guarded_host_limit"); captured2 = capture_info_limit(&sim, "guarded_host_limit"); captured3 = capture_dispatch(&sim, "info limit guarded_host_limit"); TEST_ASSERT_EQUAL_STRING_MESSAGE( captured1, captured2, "Requirement 5.2: 'info limit ' issued twice in a row with " "no intervening step must produce byte-identical output for a " "host-scoped limit too"); TEST_ASSERT_TRUE_MESSAGE( capA_before == (xce_val_t*)zarray_get(&(wA->resvals), vres->vres->idx), "Requirement 5.1: hostA's capacity zarray slot must still hold the " "SAME xce_val_t*"); TEST_ASSERT_TRUE_MESSAGE( usedA_before == (xce_val_t*)zarray_get(&(wA->resInUse), vres->vres->idx), "Requirement 5.1: hostA's in-use zarray slot must still hold the " "SAME xce_val_t*"); TEST_ASSERT_TRUE_MESSAGE( capB_before == (xce_val_t*)zarray_get(&(wB->resvals), vres->vres->idx), "Requirement 5.1: hostB's capacity zarray slot must still hold the " "SAME xce_val_t*"); TEST_ASSERT_TRUE_MESSAGE( usedB_before == (xce_val_t*)zarray_get(&(wB->resInUse), vres->vres->idx), "Requirement 5.1: hostB's in-use zarray slot must still hold the " "SAME xce_val_t*"); TEST_ASSERT_EQUAL_FLOAT(5.0f, (float)capA_before->u.fvalue); TEST_ASSERT_EQUAL_FLOAT(2.0f, (float)usedA_before->u.fvalue); TEST_ASSERT_EQUAL_FLOAT(5.0f, (float)capB_before->u.fvalue); TEST_ASSERT_EQUAL_FLOAT(3.0f, (float)usedB_before->u.fvalue); free(captured1); free(captured2); free(captured3); printf("test_info_limit_never_mutates_host_scoped_limit_accounting_" "state: PASS\n"); } /* * Requirement 5.1/5.2, the degenerate "nothing configured at all" case: * calling `info limit` (named and "all") against a sim whose limit plugin * was never registered must be safely repeatable and must not, e.g., * spuriously register/create the plugin as a side effect of merely being * asked about it -- key_find(core->plugins, "xce_sched_limit", ...) must * keep missing after these calls, exactly as it did before. */ static void test_info_limit_on_empty_registry_is_repeatable_and_never_registers_plugin( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t* found_before = NULL; xce_sched_plugin_t* found_after = NULL; char* captured1; char* captured2; char* captured3; char* captured4; int rc_before; int rc_after; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); rc_before = key_find(sim.core.plugins, "xce_sched_limit", (void**)&found_before); captured1 = capture_info_limit(&sim, "all"); captured2 = capture_info_limit(&sim, "all"); captured3 = capture_info_limit(&sim, "anything"); captured4 = capture_dispatch(&sim, "info limit all"); TEST_ASSERT_EQUAL_STRING_MESSAGE( captured1, captured2, "Requirement 5.2: repeated 'info limit all' calls against a never-" "registered plugin must produce byte-identical output"); rc_after = key_find(sim.core.plugins, "xce_sched_limit", (void**)&found_after); TEST_ASSERT_TRUE_MESSAGE( rc_before <= 0, "sanity: the limit plugin must genuinely be absent before any " "'info limit' call"); TEST_ASSERT_EQUAL_INT_MESSAGE( rc_before, rc_after, "Requirement 5.1: 'info limit' must never spuriously register the " "limit plugin as a side effect of being asked about it"); TEST_ASSERT_NULL_MESSAGE( found_after, "Requirement 5.1: the limit plugin must still be absent after " "every form of 'info limit' has been called against it"); free(captured1); free(captured2); free(captured3); free(captured4); printf("test_info_limit_on_empty_registry_is_repeatable_and_never_" "registers_plugin: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Requirement 5.2 (determinism): `info limit` issued mid-replay must not */ /* perturb the replay's own scheduling decisions -- extends the */ /* established REPL-vs-batch equivalence discipline */ /* (test_repl_equivalence.c) to this new read path specifically, per */ /* design.md's own Integration Tests bullet and */ /* scheduler-driver-equivalence.md's testing discipline. Every helper */ /* below is duplicated (not shared) from test_repl_equivalence.c, per this */ /* file family's own established "duplicate, don't share" convention (no */ /* shared test-utility header exists across this directory) -- prefixed */ /* det_ to avoid any symbol collision within this TU. */ /* ---------------------------------------------------------------------- */ static void det_make_tmp_dir(char* out, size_t out_len, const char* suffix) { char tmpl[PATH_MAX]; snprintf(tmpl, sizeof(tmpl), "/tmp/sched_repllimitdet%s_XXXXXX", suffix); TEST_ASSERT_NOT_NULL(mkdtemp(tmpl)); TEST_ASSERT_TRUE(strlen(tmpl) < out_len); strcpy(out, tmpl); } static void det_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); sched->core.job_pick_plugin = (char*)"builtin_fcfs"; sched->record_dir = record_dir; } static FILE* det_write_header_and_snapshot(char* out_path, size_t out_path_len, const char* suffix) { char record_dir[PATH_MAX]; xce_scheduler_t sched; int rc; det_make_tmp_dir(record_dir, sizeof(record_dir), suffix); det_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); return sched.record_file; } static void det_append_queue_update_event(FILE* f, uint64_t ts, const char* queue_name, int32_t priority) { xce_queue_t queue; replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&queue, 0, sizeof(queue)); queue.queue = (char*)queue_name; queue.status = QUEUE_STAT_OPEN; queue.priority = priority; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_queueUpdate; src.body.queue_update = &queue; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void det_append_jobs_push_event(FILE* f, uint64_t ts, uint64_t jobid, const char* queue_name) { job_submit_data_t submit; xce_job_t job; xce_job_t* jobs[1]; xce_jobs_data_t jobs_data; replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&submit, 0, sizeof(submit)); submit.queue = (char*)queue_name; submit.user = (char*)"alice"; submit.mintasks = 1; submit.maxtasks = 1; memset(&job, 0, sizeof(job)); job.jobid = jobid; job.state = JOB_STAT_PEND; job.submit = &submit; jobs[0] = &job; memset(&jobs_data, 0, sizeof(jobs_data)); jobs_data.n_total = 1; jobs_data.count = 1; jobs_data.jobs = jobs; memset(&src, 0, sizeof(src)); src.ts = ts; src.body_which = ReplayEvent_body_jobsPush; src.body.jobs_push = &jobs_data; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void det_append_replay_session(FILE* f, uint64_t begin_round) { struct capn c; struct capn_segment* cs; ReplaySession_ptr sp; capn_init_malloc(&c); cs = capn_root(&c).seg; sp = new_ReplaySession(cs); ReplaySession_set_beginRound(sp, begin_round); capn_setp(capn_root(&c), 0, sp.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void det_append_end_of_stream_marker(FILE* f) { replay_event_t src; struct capn c; struct capn_segment* cs; ReplayEvent_ptr p; memset(&src, 0, sizeof(src)); src.body_which = ReplayEvent_body_endSession; capn_init_malloc(&c); cs = capn_root(&c).seg; encode_ReplayEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); capn_free(&c); } static void det_write_text_file(const char* path, const char* content) { FILE* f = fopen(path, "w"); TEST_ASSERT_NOT_NULL(f); TEST_ASSERT_TRUE(fputs(content, f) >= 0); fclose(f); } static void det_decode_all_sessions(const char* path, replay_session_t*** out, int* out_count) { FILE* f = fopen(path, "r"); replay_session_t** sessions = NULL; int count = 0; TEST_ASSERT_NOT_NULL(f); for (;;) { struct capn c; capn_ptr root; if (capn_init_fp(&c, f, 0) != 0) { break; /* EOF */ } if (msg_root_has_shape(&c, &root, ReplaySession_word_count, ReplaySession_pointer_count)) { ReplaySession_ptr sp; replay_session_t* decoded = NULL; replay_session_t** grown; sp.p = root; decode_ReplaySession_ptr(&decoded, sp); capn_free(&c); TEST_ASSERT_NOT_NULL(decoded); grown = (replay_session_t**)realloc( sessions, (size_t)(count + 1) * sizeof(*sessions)); TEST_ASSERT_NOT_NULL(grown); sessions = grown; sessions[count] = decoded; count++; continue; } capn_free(&c); } fclose(f); *out = sessions; *out_count = count; } static void det_free_all_sessions(replay_session_t** sessions, int count) { int i; for (i = 0; i < count; i++) { free_ReplaySession_ptr(&(sessions[i])); } free(sessions); } static int det_replay_decision_content_eq(const replay_decision_t* a, const replay_decision_t* b) { if ((a == NULL) || (b == NULL)) { return 0; } if (a->body_which != b->body_which) { return 0; } switch (a->body_which) { case ReplayDecision_body_dispatch: if ((a->body.dispatch == NULL) || (b->body.dispatch == NULL)) { return 0; } return sim_verify_job_alloc_eq(a->body.dispatch->alloc, b->body.dispatch->alloc); case ReplayDecision_body_pend: if ((a->body.pend == NULL) || (b->body.pend == NULL)) { return 0; } return (a->body.pend->jobid == b->body.pend->jobid) && sim_verify_job_preason_eq(a->body.pend->reason, b->body.pend->reason); default: return 0; } } /* * det_run_scripted_repl_session - runs sched_sim_driver_run() against * `fixture_path` in interactive mode, driven by `script_content` fed as * stdin, writing its own decision stream to a fresh temp file whose path is * returned in `out_path`. Forked, matching test_repl_equivalence.c's own * rationale verbatim: `continue` re-enters the REPL at replay completion * (bug-1594) rather than returning, so the script's trailing `quit` always * exit(0)s -- this must happen in a child, or it would tear down this * whole test binary. */ static void det_run_scripted_repl_session(const char* fixture_path, const char* script_content, char* out_path, size_t out_path_len) { char script_dir[PATH_MAX]; char script_path[PATH_MAX]; int out_fd; sched_sim_args_t args; pid_t pid; int status; snprintf(out_path, out_path_len, "/tmp/xce_repllimitdet_out_XXXXXX"); out_fd = mkstemp(out_path); TEST_ASSERT_TRUE(out_fd >= 0); close(out_fd); det_make_tmp_dir(script_dir, sizeof(script_dir), "_script"); snprintf(script_path, sizeof(script_path), "%s/script.txt", script_dir); det_write_text_file(script_path, script_content); memset(&args, 0, sizeof(args)); args.replay_path = (char*)fixture_path; args.sim_start_ts = SCHED_SIM_START_TS_LATEST; args.sim_speed = 0; args.output_path = out_path; args.interactive = true; pid = fork(); TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork() must succeed"); if (pid == 0) { xce_sched_sim_driver_t sim; if (freopen(script_path, "r", stdin) == NULL) { _exit(120); } memset(&sim, 0, sizeof(sim)); (void)sched_sim_driver_run(&sim, &args); _exit(111); /* unreachable -- quit must already have exited */ } TEST_ASSERT_EQUAL_INT_MESSAGE(pid, waitpid(pid, &status, 0), "waitpid must reap the REPL child"); TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status), "the REPL child must exit normally via quit's " "exit(0), not crash/signal"); TEST_ASSERT_EQUAL_INT_MESSAGE(0, WEXITSTATUS(status), "quit's exit(0) is the only correct exit " "path"); } /* * THE central determinism assertion (Requirement 5.2, design.md's own * Integration Tests bullet): the SAME `.replay` fixture, driven through the * SAME `step 1`/`continue` script, must produce byte-identical scheduling * decisions whether or not `info limit` is issued at several points along * the way. No limit plugin is registered for this fixture (this task's own * scope is proving that ISSUING the command has zero effect on the replay, * not re-proving the config/usage display correctness the no-mutation * tests above and tasks 2-3's own tests already cover) -- every `info * limit` call below still reaches the real repl_info_limit()/ * limit_get_configured_limits() code path and resolves through its genuine * "no limits configured"/"no such limit" short-circuits, so this proves * THOSE paths are exactly as non-perturbing as the matched-limit path. */ static void test_repl_step_continue_with_info_limit_interleaved_matches_baseline_decision_sequence( void) { char fixture_path[PATH_MAX]; char baseline_out_path[PATH_MAX]; char infolimit_out_path[PATH_MAX]; FILE* f; replay_session_t** baseline_sessions = NULL; replay_session_t** infolimit_sessions = NULL; int n_baseline = 0; int n_infolimit = 0; int total_baseline_decisions; int saw_jobid_501; int i; int j; /* Same fixture shape test_repl_equivalence.c's own decision-parity test * establishes (two queues at different priorities, four jobs split * across three ReplaySession boundaries) -- reused verbatim rather than * re-derived, since this task validates an ADDITIONAL invariant (info * limit's own non-interference) against an already-proven decision- * producing fixture shape, not a new one. */ f = det_write_header_and_snapshot(fixture_path, sizeof(fixture_path), "_fixture"); TEST_ASSERT_NOT_NULL(f); det_append_queue_update_event(f, 1000ULL, "high", 5); det_append_queue_update_event(f, 1001ULL, "normal", 1); det_append_jobs_push_event(f, 1500ULL, 501ULL, "high"); det_append_jobs_push_event(f, 1501ULL, 502ULL, "normal"); det_append_replay_session(f, 2000ULL); /* session 1 */ det_append_jobs_push_event(f, 2500ULL, 503ULL, "high"); det_append_replay_session(f, 3000ULL); /* session 2 */ det_append_jobs_push_event(f, 3500ULL, 504ULL, "normal"); det_append_replay_session(f, 4000ULL); /* session 3 */ det_append_end_of_stream_marker(f); fclose(f); /* ---- Run 1: baseline -- step/continue to completion, info limit * never issued. ---- */ det_run_scripted_repl_session(fixture_path, "step 1\ncontinue\nquit\n", baseline_out_path, sizeof(baseline_out_path)); /* ---- Run 2: the SAME fixture and SAME step/continue script, but with * `info limit` issued before the pre-replay pause, between step and * continue, and after continue completes every remaining session -- * every position an operator could type it at, in both arg forms this * feature accepts (a name and "all"). ---- */ det_run_scripted_repl_session( fixture_path, "info limit all\nstep 1\ninfo limit all\ncontinue\ninfo limit " "ug1_slots\nquit\n", infolimit_out_path, sizeof(infolimit_out_path)); det_decode_all_sessions(baseline_out_path, &baseline_sessions, &n_baseline); det_decode_all_sessions(infolimit_out_path, &infolimit_sessions, &n_infolimit); TEST_ASSERT_EQUAL_INT_MESSAGE(3, n_baseline, "the fixture pushes exactly 3 " "ReplaySession boundaries"); TEST_ASSERT_EQUAL_INT_MESSAGE( n_baseline, n_infolimit, "Requirement 5.2: issuing 'info limit' at several points in the " "REPL session must not change how many ReplaySession boundaries " "the replay produces"); /* Non-tautology guard #1 (mirrors test_repl_equivalence.c's own): an * unambiguous floor independent of any 'info limit' interference -- * session 1 always has exactly 2 pending candidates. */ TEST_ASSERT_EQUAL_INT(2, baseline_sessions[0]->n_decisions); /* Non-tautology guard #2: pin real decision CONTENT on the baseline * side, not merely "both sides agree" (a shared bug could satisfy that * too). */ saw_jobid_501 = 0; for (j = 0; j < baseline_sessions[0]->n_decisions; j++) { replay_decision_t* d = baseline_sessions[0]->decisions[j]; TEST_ASSERT_EQUAL_INT((int)ReplayDecision_body_pend, (int)d->body_which); if ((d->body.pend != NULL) && (d->body.pend->jobid == 501ULL)) { saw_jobid_501 = 1; } } TEST_ASSERT_TRUE_MESSAGE(saw_jobid_501, "session 1's baseline decisions must " "genuinely include job 501"); /* Non-tautology guard #3: a genuinely non-trivial total decision * count. */ total_baseline_decisions = 0; for (i = 0; i < n_baseline; i++) { total_baseline_decisions += baseline_sessions[i]->n_decisions; } TEST_ASSERT_TRUE_MESSAGE(total_baseline_decisions >= 4, "the fixture must produce a genuinely " "non-trivial decision sequence"); /* ---- The central assertion: per session, per decision, the * info-limit-interleaved run must match the baseline exactly. ---- */ for (i = 0; i < n_baseline; i++) { TEST_ASSERT_EQUAL_INT_MESSAGE( baseline_sessions[i]->n_decisions, infolimit_sessions[i]->n_decisions, "Requirement 5.2: session decision COUNT must match whether or " "not 'info limit' was issued during the replay"); for (j = 0; j < baseline_sessions[i]->n_decisions; j++) { replay_decision_t* bd = baseline_sessions[i]->decisions[j]; replay_decision_t* id = infolimit_sessions[i]->decisions[j]; TEST_ASSERT_EQUAL_INT((int)ReplayDecision_body_pend, (int)bd->body_which); TEST_ASSERT_EQUAL_INT((int)ReplayDecision_body_pend, (int)id->body_which); TEST_ASSERT_TRUE_MESSAGE( det_replay_decision_content_eq(bd, id), "Requirement 5.2: 'info limit' issued mid-session must " "produce a byte-identical decision to the baseline at this " "session and position"); } } det_free_all_sessions(baseline_sessions, n_baseline); det_free_all_sessions(infolimit_sessions, n_infolimit); unlink(baseline_out_path); unlink(infolimit_out_path); printf( "test_repl_step_continue_with_info_limit_interleaved_matches_" "baseline_decision_sequence: PASS\n"); } /* ---------------------------------------------------------------------- */ /* scheduler-sim-diagnostics spec, task 6 (bug-1825 fix): sim mode never */ /* registers the limit plugin at all, so `info limit`'s otherwise-correct */ /* display logic (tested above via the hand-rolled register_limit_plugin() */ /* fixture) is permanently inert against a REAL replay. These tests drive */ /* the actual production bootstrap function, sim_bootstrap_limit_plugin() */ /* (new src/sim_limit.c, sim_loop.c's sim_bootstrap_picker()-call-site */ /* sibling), against a real xce_sched_core_init()-initialized core -- not */ /* the register_limit_plugin()/fixture_register_plugin_in_core() shortcut */ /* the rest of this file's tests use, since the whole point here is */ /* proving the real bootstrap path (registration + config-seed) works, */ /* not re-proving repl_info_limit()'s own display logic again. */ /* ---------------------------------------------------------------------- */ /* * (1) Registration: before sim_bootstrap_limit_plugin() runs, the limit * plugin is not present in sim->core.plugins at all (limit_get_configured_ * limits()'s own documented "plugin absent" NULL return). After it runs, * the accessor resolves a real, iterable handle -- proving the function * actually registered "xce_sched_limit" against the simulation core via * plugin_register_core(), mirroring sim_bootstrap_picker()'s own * registration call exactly. */ static void test_sim_bootstrap_limit_plugin_registers_plugin_against_real_core(void) { xce_sched_sim_driver_t sim; var limits_handle; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); limits_handle = limit_get_configured_limits(&(sim.core)); TEST_ASSERT_NULL_MESSAGE( limits_handle, "the limit plugin must not be registered before " "sim_bootstrap_limit_plugin() runs"); sim_bootstrap_limit_plugin(&sim); limits_handle = limit_get_configured_limits(&(sim.core)); TEST_ASSERT_NOT_NULL_MESSAGE( limits_handle, "sim_bootstrap_limit_plugin() must register the limit plugin " "against sim->core, matching sim_bootstrap_picker()'s own " "plugin_register_core() pattern"); printf( "test_sim_bootstrap_limit_plugin_registers_plugin_against_real_" "core: PASS\n"); } /* * (2) THE observable criterion (task 6's own stated Observable): a real * xce_sched_sim_driver_t whose core.config already carries a real * xce_limit_config_t (as sim_locate_and_load_snapshot()/ * xce_sched_core_load_snapshot() would have set it up from a replay's own * snapshot, before sim_bootstrap_limit_plugin() ever runs -- reproduced * here via direct construction per this task's own stated test-level * guidance, no capnp/.replay file needed) ends up with * limit_get_configured_limits() iterating to that SAME real limit by name * once sim_bootstrap_limit_plugin() runs. This is the test that actually * proves the config-seed step (limit_seed_config(), plugins/limit.c, * called directly with a real xce_sched_core_t* -- bug-1825 remediation * round 2 replaced the original plugin->sched-type-punned * plugin->hooks.config_hook() call with this honestly-typed entry point; * see sim_limit.c's own header comment for why), not just registration. */ static void test_sim_bootstrap_limit_plugin_seeds_real_config_limit_when_config_present( void) { xce_sched_sim_driver_t sim; xce_limit_config_t* limits[1]; xce_conf4sched_t config; var limits_handle; iterator_t it; xce_limit_t* found = NULL; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); limits[0] = make_limit_config("sim_replay_limit"); memset(&config, 0, sizeof(config)); config.nlimits = 1; config.limits = limits; /* Mirrors sim_locate_and_load_snapshot()'s (sim.c) borrowed-reference * assignment of sim->core.config from the replay's own snapshot -- * this test only needs the END STATE that call produces, not the * capnp/file machinery that gets there. */ sim.core.config = &config; sim_bootstrap_limit_plugin(&sim); limits_handle = limit_get_configured_limits(&(sim.core)); TEST_ASSERT_NOT_NULL(limits_handle); rc = iter_make(&it, limits_handle); TEST_ASSERT_TRUE(rc >= 0); rc = iter_next(&it, (void**)&found); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_EQUAL(RTTI_ITER_NO_DATA, rc); TEST_ASSERT_NOT_NULL(found); TEST_ASSERT_EQUAL_STRING_MESSAGE( "sim_replay_limit", found->name, "sim_bootstrap_limit_plugin() must seed the plugin with the REAL " "limit already sitting in sim->core.config, not merely register " "an empty plugin"); rc = iter_next(&it, (void**)&found); TEST_ASSERT_EQUAL_INT(RTTI_ITER_NO_DATA, rc); iter_destroy(&it); free_limit_config(limits[0]); printf( "test_sim_bootstrap_limit_plugin_seeds_real_config_limit_when_" "config_present: PASS\n"); } /* * (3) Graceful no-config case (task 6's explicit design constraint): a * replay whose snapshot carries no config -- or a test fixture that never * sets one up -- leaves sim->core.config NULL. sim_bootstrap_limit_plugin() * must still register the plugin successfully (non-NULL, empty-iterating * accessor handle) and must NOT crash attempting to seed a config that * does not exist; this is the normal case, not an error. */ static void test_sim_bootstrap_limit_plugin_registers_without_crash_when_config_null( void) { xce_sched_sim_driver_t sim; var limits_handle; iterator_t it; void* item = NULL; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_NULL(sim.core.config); sim_bootstrap_limit_plugin(&sim); limits_handle = limit_get_configured_limits(&(sim.core)); TEST_ASSERT_NOT_NULL_MESSAGE( limits_handle, "the plugin must still register successfully when " "sim->core.config is NULL -- the config-seed step must be " "skipped silently, not treated as a registration failure"); rc = iter_make(&it, limits_handle); TEST_ASSERT_TRUE(rc >= 0); rc = iter_next(&it, &item); TEST_ASSERT_EQUAL_INT(RTTI_ITER_NO_DATA, rc); iter_destroy(&it); printf( "test_sim_bootstrap_limit_plugin_registers_without_crash_when_" "config_null: PASS\n"); } /* ---------------------------------------------------------------------- */ /* scheduler-sim-live-allocation spec, task 2.3 (Limit Plugin Core-Safe */ /* Matching, Req 2.1/2.2): the sim_bootstrap_limit_plugin() tests above */ /* prove REGISTRATION and CONFIG-SEEDING work against a plugin_register_ */ /* core()-registered plugin (plugin->sched left NULL by design). Neither */ /* proves MATCHING works -- limit_job_election()/match_job_global_limit_()*/ /* and limit_prepare_allocation()/match_job_host_limit_() have always */ /* derived their own `xce_scheduler_t* sched` from `plugin->sched`, so a */ /* sim-registered plugin (sched == NULL) either NULL-derefs (global path, */ /* limit_job_election()'s own `sched->core.configver` read, no prior NULL */ /* guard) or returns -EINVAL without matching anything (host-scoped path, */ /* match_job_host_limit_()'s own `sched == NULL` guard) -- both are the */ /* "genuinely never invoked/no-op in sim mode" gap bug-1826/this task's own*/ /* brief describe. These two tests drive the real hook-registered entry */ /* points directly (the same (void* arg, ...) shape plugin_job_election()/ */ /* plugin_prepare_allocation() -- both already core-safe, task 1.2/2.2 -- */ /* dispatch through in production) against a plugin registered ONLY via */ /* plugin_register_core(), proving matching now reads plugin->core (task */ /* 1.2's honest core-state reference) instead of plugin->sched. */ /* ---------------------------------------------------------------------- */ /* * (1) Global limit match (Req 2.1): a limit configured on a plugin * registered via plugin_register_core() must still match a real job and * register its usage instance, verifiable through limit_get_usage_ * instances() -- the exact "Observable" this task's brief specifies. * `obj.user`/`.queue`/`.project` are non-NULL empty strings (never NULL -- * match_limit()/is_in_group() both reject NULL outright), matching against * the limit's default "all"-group fallback (add_configured_limit()'s own * config shape, seeded here via limit_seed_config() through sim_bootstrap_ * limit_plugin() instead, since plugin->sched is NULL for this plugin). */ static void test_limit_job_election_matches_global_limit_via_sim_registered_plugin(void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t* plugin = NULL; xce_limit_config_t* limits[1]; xce_conf4sched_t config; xce_sched_obj_t obj; xce_sched_job_t job; var instances_handle; iterator_t it; void* item = NULL; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); /* instantiate_limit() (xcelimit.c) requires the limit's underlying * resource (val.resname, "mem" -- make_limit_config()'s own hardcoded * shape) to genuinely exist in core->resmgr (matching production, where * sched_init_resources() registers it at config load) -- every other * test in this file that drives real matching (not just registration/ * config-seeding) does the same via register_per_host_resource(). Its * non-GLOBAL/non-SHARED flags are harmless here: this limit's own * percent=0/per_host=0 shape (make_limit_config()) still resolves as a * GLOBAL limit via is_host_based_limit()'s own fallback rule, matching * instantiate_and_register_global_vres()'s identical comment above. */ register_per_host_resource(&sim, "mem"); limits[0] = make_limit_config("sim_global_election_limit"); memset(&config, 0, sizeof(config)); config.nlimits = 1; config.limits = limits; sim.core.config = &config; /* Real production bootstrap: plugin_register_core() + limit_seed_ * config(), exactly as sched_sim_driver_run() calls it (sim_limit.c). */ sim_bootstrap_limit_plugin(&sim); rc = key_find(sim.core.plugins, "xce_sched_limit", (void**)&plugin); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_NULL(plugin); TEST_ASSERT_NULL_MESSAGE( plugin->sched, "sanity: plugin_register_core() must leave plugin->sched NULL -- " "this test proves matching works WITHOUT it, via plugin->core"); TEST_ASSERT_EQUAL_PTR_MESSAGE( &(sim.core), plugin->core, "sanity: plugin->core (task 1.2) must reference this real sim " "core"); /* limit_job_election() skips any schedobj whose own configver is * already >= core->configver (the "already evaluated against this * config" guard, core_events.c's own config-push bump being the * production trigger) -- both default to 0 (xce_sched_core_init() / * this memset()), so bump core->configver past the fresh obj's 0 here, * exactly as a real CONFIG_PUSH event would before this job is ever * offered to the picker. */ sim.core.configver = 1; memset(&obj, 0, sizeof(obj)); obj.user = (char*)""; obj.queue = (char*)""; obj.project = (char*)""; xlist_init(&(obj.glimits), 8); zarray_init(&(obj.hlimits), 8, NULL); memset(&job, 0, sizeof(job)); job.jobid = 1; job.schedobj = &obj; /* The real job_election_hook-registered entry point, called exactly * the way the now-core-safe plugin_job_election() dispatcher (plugin.c, * task 1.2) calls it in production -- `arg` is the plugin instance. */ rc = limit_job_election((void*)plugin, &job); TEST_ASSERT_TRUE_MESSAGE( rc >= 0, "limit_job_election() must succeed for a plugin registered via " "plugin_register_core(), reading plugin->core instead of the " "always-NULL-here plugin->sched"); instances_handle = limit_get_usage_instances(&(sim.core)); TEST_ASSERT_NOT_NULL(instances_handle); rc = iter_make(&it, instances_handle); TEST_ASSERT_TRUE(rc >= 0); rc = iter_next(&it, &item); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_EQUAL(RTTI_ITER_NO_DATA, rc); TEST_ASSERT_NOT_NULL_MESSAGE( item, "a global limit match via the simulation-mode-registered plugin " "must create a real usage instance, where today it silently never " "matches at all (plugin->sched is NULL for a plugin_register_" "core()-registered plugin)"); iter_destroy(&it); free_limit_config(limits[0]); printf( "test_limit_job_election_matches_global_limit_via_sim_registered_" "plugin: PASS\n"); } /* * (2) Host-scoped limit match (Req 2.2): same proof as (1), but for the * prepare_allocation_hook path -- a percent-based limit on a real, * non-GLOBAL/non-SHARED per-host resource (register_per_host_resource(), * the exact shape add_configured_percent_limit() uses for the normal-mode * equivalent of this scenario), matched via limit_prepare_allocation() * against a real xce_sched_ctx_t/xce_sched_worker_t, on a plugin registered * ONLY via plugin_register_core() (plugin->sched NULL). */ static void test_limit_prepare_allocation_matches_host_limit_via_sim_registered_plugin( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t* plugin = NULL; xce_limit_val_t val = { .resname = (char*)"sim_host_res", .percent = 1, .val = 50.0 }; xce_limit_config_t limit_cfg = { .name = (char*)"sim_host_limit", .val = &val }; xce_limit_config_t* limits[1] = { &limit_cfg }; xce_conf4sched_t config; xce_sched_obj_t obj; xce_sched_worker_t worker; xce_sched_ctx_t ctx; var instances_handle; iterator_t it; void* item = NULL; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); register_per_host_resource(&sim, "sim_host_res"); memset(&config, 0, sizeof(config)); config.nlimits = 1; config.limits = limits; sim.core.config = &config; sim_bootstrap_limit_plugin(&sim); rc = key_find(sim.core.plugins, "xce_sched_limit", (void**)&plugin); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_NULL(plugin); TEST_ASSERT_NULL_MESSAGE( plugin->sched, "sanity: plugin_register_core() must leave plugin->sched NULL"); memset(&obj, 0, sizeof(obj)); obj.user = (char*)""; obj.queue = (char*)""; obj.project = (char*)""; xlist_init(&(obj.glimits), 8); zarray_init(&(obj.hlimits), 8, NULL); memset(&worker, 0, sizeof(worker)); worker.name = (char*)"sim_host1"; worker.idx = 0; memset(&ctx, 0, sizeof(ctx)); ctx.worker = &worker; ctx.schedobj = &obj; /* The real prepare_allocation_hook-registered entry point, called * exactly the way the now-core-safe plugin_prepare_allocation() * dispatcher (plugin.c, task 1.2) and sched_ctx_set_primary() * (schedobj.c, task 2.1) call it in production. */ rc = limit_prepare_allocation((void*)plugin, &ctx); TEST_ASSERT_TRUE_MESSAGE( rc >= 0, "limit_prepare_allocation() must succeed for a plugin registered " "via plugin_register_core(), reading plugin->core instead of the " "always-NULL-here plugin->sched"); instances_handle = limit_get_usage_instances(&(sim.core)); TEST_ASSERT_NOT_NULL(instances_handle); rc = iter_make(&it, instances_handle); TEST_ASSERT_TRUE(rc >= 0); rc = iter_next(&it, &item); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NOT_EQUAL(RTTI_ITER_NO_DATA, rc); TEST_ASSERT_NOT_NULL_MESSAGE( item, "a host-scoped limit match via the simulation-mode-registered " "plugin must create a real usage instance, where today it either " "no-ops (-EINVAL from match_job_host_limit_()'s own sched==NULL " "guard) or never matches at all"); iter_destroy(&it); printf( "test_limit_prepare_allocation_matches_host_limit_via_sim_" "registered_plugin: PASS\n"); } /* ---------------------------------------------------------------------- */ /* Task 10.2 (Requirement 6.1-6.5, 5.1-5.3): the cross-cutting validation */ /* task closing out this entire increment. Every task above (7/8.1/8.2/9/ */ /* 10.1) individually proved its own half of the units-consistency story: */ /* task 8.2's own tests only ever set up a configured-setting line (no */ /* vres/usage instance in play at all), and task 9's own tests only ever */ /* exercise the usage line under the REPL's single default (BOTH) mode */ /* (sim.repl_state is never assigned in either of those two tests, so */ /* repl_current_units_mode() falls back to XCE_REPL_UNITS_BOTH and is */ /* never switched). Neither proves, in one request, that BOTH surfaces */ /* agree on units AND that the agreement holds across more than one mode */ /* -- which is the literal content of Requirement 6.5 ("...consistently... */ /* throughout a single request for limit information"). The test below */ /* closes that gap directly. */ /* ---------------------------------------------------------------------- */ /* * THE central Requirement 6.5 proof: a single byte-valued ("mem") limit, * configured with a real GLOBAL/SHARED usage instance so ONE `info limit * ` dispatch prints both the configured-setting line ("mem = ...") * and a live usage line ("capacity=... accounted=..."). Captures that one * combined dump under `set units raw` and again under `set units human` * (the real repl_params.c dispatch path, not a direct units_mode * parameter), and asserts BOTH lines agree on the SAME units convention * within each single capture: under RAW, neither line shows any * human-readable unit suffix; under HUMAN, neither line shows the raw byte * count. This is strictly stronger than task 8.2's or task 9's own tests, * neither of which combines both surfaces in one request, and neither of * which switches mode against a fixture that has both a configured setting * AND a live usage instance at the same time. */ static void test_info_limit_configured_and_usage_amounts_agree_on_units_within_one_request( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; char* captured_raw; char* captured_human; bool handled; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); sim.repl_state = repl_state_ensure(&sim); TEST_ASSERT_NOT_NULL(sim.repl_state); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit_with_value(&plugin, "cross_surface_limit", "mem", 5368709120.0); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"cross_surface_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_global_vres(&sim, &plugin, matched); put_shared_fvalue(&(sim.core.sharedInst), vres->vres->idx, 5368709120.0); put_shared_fvalue(&(sim.core.sharedInUse), vres->vres->idx, 5000000000.0); /* --- RAW mode: both the configured-setting line and the usage line(s) * must show the bare byte count, with no human-readable suffix * anywhere in the combined dump. */ handled = repl_dispatch(&sim, "set units raw"); TEST_ASSERT_FALSE(handled); captured_raw = capture_dispatch(&sim, "info limit cross_surface_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_raw, "mem = 5368709120.000000"), "Requirement 6.5 (RAW): the configured-setting line must render the " "bare byte count"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_raw, "capacity=5368709120.000000"), "Requirement 6.5 (RAW): the usage line's capacity must ALSO render " "the bare byte count -- same request, same mode, same convention " "as the configured-setting line above"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_raw, "accounted=5000000000.000000"), "Requirement 6.5 (RAW): ... and the usage line's accounted amount " "too"); TEST_ASSERT_NULL_MESSAGE( strstr(captured_raw, "GB"), "Requirement 6.5 (RAW): NEITHER surface may show a human-readable " "unit suffix under RAW mode -- a 'GB' anywhere in this combined " "dump would mean the two surfaces disagree on units within the " "same request"); free(captured_raw); /* --- HUMAN mode: both the configured-setting line and the usage * line(s) must switch to human-readable form together, with no raw * byte count surviving anywhere in the combined dump. */ handled = repl_dispatch(&sim, "set units human"); TEST_ASSERT_FALSE(handled); captured_human = capture_dispatch(&sim, "info limit cross_surface_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_human, "mem = 5.00 GB"), "Requirement 6.1/6.2/6.5 (HUMAN): the configured-setting line must " "switch to human-readable form"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_human, "capacity=5.00 GB"), "Requirement 6.1/6.5 (HUMAN): the usage line's capacity must ALSO " "switch to human-readable form -- same request, same mode, same " "convention as the configured-setting line above"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_human, "accounted=4.66 GB"), "Requirement 6.1/6.5 (HUMAN): ... and the usage line's accounted " "amount too"); TEST_ASSERT_NULL_MESSAGE( strstr(captured_human, "5368709120"), "Requirement 6.5 (HUMAN): NEITHER surface may show the raw byte " "count under HUMAN mode -- a bare '5368709120' anywhere in this " "combined dump would mean the two surfaces disagree on units " "within the same request"); TEST_ASSERT_NULL_MESSAGE( strstr(captured_human, "5000000000"), "Requirement 6.5 (HUMAN): ... the usage line's raw accounted " "figure must not survive either"); free(captured_human); printf( "test_info_limit_configured_and_usage_amounts_agree_on_units_" "within_one_request: PASS\n"); } /* * Requirement 6.3's non-byte-valued half, proven end-to-end through the * REPL (not just fmt_limit_units()'s own direct unit tests in test_limit.c, * which already cover this at the library level for the configured-setting * formatter alone). No existing fixture in this file configures a limit on * a genuinely non-byte-valued resource -- every add_configured_limit()/ * add_configured_percent_limit() fixture above hardcodes resname "mem". * This fixture uses "slots" instead, with both a configured setting AND a * live GLOBAL usage instance, and asserts the ENTIRE combined dump (both * lines together) is byte-for-byte IDENTICAL across all three units modes * -- the strongest possible "completely unaffected" proof. */ static void test_info_limit_named_non_byte_valued_limit_display_completely_unaffected_by_units_mode( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; xce_limit_t* matched = NULL; var limits_handle; xce_vres_t* vres; char* captured_raw; char* captured_human; char* captured_both; bool handled; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); sim.repl_state = repl_state_ensure(&sim); TEST_ASSERT_NOT_NULL(sim.repl_state); /* "slots" -- a real, registered, genuinely non-byte-valued resource * (not one of xce_is_bytes_valued_resource()'s five recognized names), * required so instantiate_limit()'s own vres->res lookup succeeds (see * xcelimit.c: a miss there is a hard -ENOENT, independent of any * units-mode concern). */ register_per_host_resource(&sim, "slots"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_limit_with_value(&plugin, "slots_limit", "slots", 3.0); limits_handle = limit_get_configured_limits(&(sim.core)); rc = key_find(limits_handle, (void*)"slots_limit", (void**)&matched); TEST_ASSERT_TRUE(rc > 0); TEST_ASSERT_NOT_NULL(matched); vres = instantiate_and_register_global_vres(&sim, &plugin, matched); put_shared_fvalue(&(sim.core.sharedInst), vres->vres->idx, 3.0); put_shared_fvalue(&(sim.core.sharedInUse), vres->vres->idx, 1.0); handled = repl_dispatch(&sim, "set units raw"); TEST_ASSERT_FALSE(handled); captured_raw = capture_dispatch(&sim, "info limit slots_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_raw, "slots = 3.000000"), "sanity: the configured-setting line must show the real value"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_raw, "accounted=1.00 capacity=3.00"), "sanity: the usage line must show the real accounted/capacity " "amounts"); handled = repl_dispatch(&sim, "set units human"); TEST_ASSERT_FALSE(handled); captured_human = capture_dispatch(&sim, "info limit slots_limit"); TEST_ASSERT_EQUAL_STRING_MESSAGE( captured_raw, captured_human, "Requirement 6.3: a non-byte-valued limit's ENTIRE display (both " "the configured-setting line and the usage line together) must be " "byte-for-byte unaffected by `set units human` -- any difference " "here would mean byte-unit scaling leaked onto a resource that " "isn't byte-valued"); handled = repl_dispatch(&sim, "set units both"); TEST_ASSERT_FALSE(handled); captured_both = capture_dispatch(&sim, "info limit slots_limit"); TEST_ASSERT_EQUAL_STRING_MESSAGE( captured_raw, captured_both, "Requirement 6.3: ... and unaffected by `set units both` too -- " "identical to the RAW-mode capture in every byte"); free(captured_raw); free(captured_human); free(captured_both); printf( "test_info_limit_named_non_byte_valued_limit_display_completely_" "unaffected_by_units_mode: PASS\n"); } /* * Requirement 6.3's percentage half, proven end-to-end through the REPL * (fmt_limit_units()'s own test_fmt_limit_units_percent_limit_ignores_ * units_mode() in test_limit.c already covers this at the library level * directly; this closes the same gap at the REPL dispatch level, through * the real `set units` command and repl_info_limit()'s real * translate_units_mode() bridge -- neither of which the library-level test * exercises). The limit is percent-based (val.percent=1) on the * byte-valued resource "mem" -- deliberately so, since Requirement 6.3's * own wording singles out "a limit expressed as a percentage of a resource * whose own canonical unit is not bytes" as the case that must never be * scaled REGARDLESS of whether the underlying resource is itself * byte-valued. No usage instance is instantiated here -- this test's whole * point is the raw percentage literal on the configured-setting line, which * task 9's own per-host byte-valued-percent-limit test already covers for * the DIFFERENT, RESOLVED-ABSOLUTE usage amount (Requirement 6.4) -- not * re-litigated here. * * Percent-display scale (bug fix, added after Requirement 6 shipped): * `add_configured_percent_limit()`'s shared fixture stores `val.val = 50.0` * as an arbitrary test constant -- but `xce_limit_val_t.val` is genuinely a * 0-1 FRACTION when `percent != 0` (confirmed against the real capacity * computation, `update_limit_res_value()`, plugins/limit.c, which multiplies * this value DIRECTLY against the resource's current value with no /100 * anywhere in the pipeline -- traced end to end from * src/configs/limits.yaml through capnp to this function). `fmt_limit_units()` * now correctly renders `val.val * 100` for the percent branch, so this * fixture's raw 50.0 displays as the (visually odd but mathematically * correct) "5000.000000%" -- not because 50.0 means 5000%, but because 50.0 * was never a realistic percent-scale value to begin with (a real 50% limit * needs `val.val = 0.5`). The shared fixture is deliberately left unchanged * here rather than "corrected" to 0.5, since many OTHER tests in this file * reuse it for their own accounted/capacity math (which depends on the * capacity computation directly, not on this display fix, and remains * correct either way) -- changing it would ripple into those tests for no * benefit to this one, which only cares about verifying units_mode has zero * effect on the percent line, not about the fixture's real-world realism. */ static void test_info_limit_named_percent_limit_own_percentage_value_never_scaled_by_units_mode( void) { xce_sched_sim_driver_t sim; xce_sched_plugin_t plugin; xce_scheduler_t group_sched; char* captured_raw; char* captured_human; char* captured_both; bool handled; int rc; memset(&sim, 0, sizeof(sim)); rc = xce_sched_core_init(&(sim.core)); TEST_ASSERT_EQUAL_INT(0, rc); sim.repl_state = repl_state_ensure(&sim); TEST_ASSERT_NOT_NULL(sim.repl_state); register_per_host_resource(&sim, "mem"); register_limit_plugin(&sim, &plugin, &group_sched); add_configured_percent_limit(&plugin, "pct_never_scaled_limit", "mem"); handled = repl_dispatch(&sim, "set units raw"); TEST_ASSERT_FALSE(handled); captured_raw = capture_dispatch(&sim, "info limit pct_never_scaled_limit"); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(captured_raw, "mem = 5000.000000%"), "sanity: the configured-setting line must show the real percent " "value under RAW mode (val.val=50.0 * 100, see this test's own " "header comment on the percent-display scale fix)"); handled = repl_dispatch(&sim, "set units human"); TEST_ASSERT_FALSE(handled); captured_human = capture_dispatch(&sim, "info limit pct_never_scaled_limit"); TEST_ASSERT_EQUAL_STRING_MESSAGE( captured_raw, captured_human, "Requirement 6.3: a percentage-based limit's OWN percentage value " "must be byte-for-byte unaffected by `set units human` -- the " "percent literal must never gain a human-readable byte-unit " "suffix, even though its constrained resource ('mem') is itself " "byte-valued"); handled = repl_dispatch(&sim, "set units both"); TEST_ASSERT_FALSE(handled); captured_both = capture_dispatch(&sim, "info limit pct_never_scaled_limit"); TEST_ASSERT_EQUAL_STRING_MESSAGE( captured_raw, captured_both, "Requirement 6.3: ... and unaffected by `set units both` too -- " "identical to the RAW-mode capture in every byte"); free(captured_raw); free(captured_human); free(captured_both); printf( "test_info_limit_named_percent_limit_own_percentage_value_never_" "scaled_by_units_mode: PASS\n"); } int main(void) { UNITY_BEGIN(); RUN_TEST( test_info_limit_all_reports_no_limits_configured_when_plugin_never_registered); RUN_TEST( test_info_limit_all_reports_no_limits_configured_when_plugin_registered_with_zero_limits); RUN_TEST( test_info_limit_all_does_not_report_no_limits_configured_when_limits_are_configured); RUN_TEST( test_info_limit_named_reports_no_such_limit_for_unconfigured_name_against_fixture_with_other_limits); RUN_TEST( test_info_limit_named_reports_no_such_limit_when_plugin_never_registered_not_no_limits_configured); RUN_TEST( test_info_limit_named_does_not_report_no_such_limit_for_configured_name); RUN_TEST( test_info_limit_named_shows_both_inclusion_and_exclusion_membership); RUN_TEST(test_info_limit_all_displays_every_configured_limits_setting); RUN_TEST( test_info_limit_named_shows_host_scope_note_for_implicit_percent_based_limit); RUN_TEST( test_info_limit_named_does_not_show_host_scope_note_for_absolute_value_limit); RUN_TEST( test_info_limit_named_does_not_duplicate_host_scope_note_when_per_host_explicitly_configured); RUN_TEST( test_info_limit_named_shows_accounted_and_capacity_for_matched_and_charged_global_instance); RUN_TEST( test_info_limit_named_shows_no_active_usage_instance_for_never_matched_limit); RUN_TEST( test_info_limit_named_shows_accounted_zero_for_matched_but_never_charged_instance); RUN_TEST( test_info_limit_named_shows_separate_per_host_usage_lines_for_touched_hosts_and_none_for_untouched_host); RUN_TEST( test_info_limit_named_shows_accounted_zero_for_host_with_capacity_but_never_charged); RUN_TEST( test_info_limit_named_shows_no_active_usage_instance_when_host_scoped_vres_exists_but_no_worker_populated); RUN_TEST( test_info_limit_named_shows_global_usage_amounts_human_readable_for_byte_valued_limit); RUN_TEST( test_info_limit_named_shows_per_host_usage_amounts_human_readable_for_percent_based_byte_valued_limit); RUN_TEST( test_info_limit_dispatch_named_reaches_repl_info_limit_via_real_dispatch); RUN_TEST( test_info_limit_dispatch_all_reaches_repl_info_limit_via_real_dispatch); RUN_TEST( test_info_limit_named_shows_byte_valued_amount_human_readable_under_default_units_mode); RUN_TEST( test_info_limit_named_configured_amount_changes_consistently_with_units_mode); RUN_TEST( test_info_limit_never_mutates_global_limit_config_or_accounting_state); RUN_TEST( test_info_limit_never_mutates_host_scoped_limit_accounting_state); RUN_TEST( test_info_limit_on_empty_registry_is_repeatable_and_never_registers_plugin); RUN_TEST( test_repl_step_continue_with_info_limit_interleaved_matches_baseline_decision_sequence); RUN_TEST( test_sim_bootstrap_limit_plugin_registers_plugin_against_real_core); RUN_TEST( test_sim_bootstrap_limit_plugin_seeds_real_config_limit_when_config_present); RUN_TEST( test_sim_bootstrap_limit_plugin_registers_without_crash_when_config_null); RUN_TEST( test_limit_job_election_matches_global_limit_via_sim_registered_plugin); RUN_TEST( test_limit_prepare_allocation_matches_host_limit_via_sim_registered_plugin); RUN_TEST( test_info_limit_configured_and_usage_amounts_agree_on_units_within_one_request); RUN_TEST( test_info_limit_named_non_byte_valued_limit_display_completely_unaffected_by_units_mode); RUN_TEST( test_info_limit_named_percent_limit_own_percentage_value_never_scaled_by_units_mode); return UNITY_END(); }