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
3327 lines
131 KiB
C
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();
|
|
}
|