myxce/src/scheduler/testsuite/test_repl_limit_table.c
Rongsong Shen 5125536657
test(scheduler-sim-diagnostics): strengthen coincidentally-passing assertions (task 10.1)
Six pre-existing assertions in test_repl_limit_table.c (across 4 test
functions) used bare strstr() substring checks against whole-number
fixture values on "mem" -- e.g. strstr(captured, "accounted=4.00").
Task 9's fix changed the actual output to "accounted=4.000000 (4.00
B)", but the old substring is a literal prefix of the new one, so
these assertions kept passing by coincidence of formatting, providing
zero actual regression protection for the fix.

Strengthened each to assert the full corrected "<raw> (<human>)"
string. Proved via TDD: reverting task 9's fix now makes all 6
strengthened assertions (plus task 9's own 2 genuine tests) fail;
restoring the fix returns the suite to 85/85 green.

Confirmed via full-file scan that no other loose assertion of this
shape remains, and that info worker/info resource's own regression
suites were never vulnerable to this class of bug (they build expected
strings dynamically rather than hardcoding literal substrings).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
2026-09-20 07:35:11 +00:00

3327 lines
131 KiB
C

/*
* test_repl_limit_table.c - contract test for the interactive simulation
* REPL's `info limit <name|all>` 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 <errno.h>
#include <limits.h>
#include <sys/wait.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#include "controller.capnp.h"
#include "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=[<excluded>]).
* 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=<name>" 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 '<name>'" -- 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 \"<raw> (<human>)\" 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=<name>") 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 <name>`/`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 <name>' 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 <name>` 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 <name>' 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 <name>' 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");
}
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);
return UNITY_END();
}