feat(scheduler-sim-diagnostics): implement info limit name/all resolution

repl_info_limit() (repl_info.c, not yet wired into repl_cmd_info() -- that's
task 4) resolves "all" vs a specific name against limit_get_configured_limits():
a NULL/empty handle on the all path reports "no limits configured" (Req 1.7);
a NULL handle or key_find() miss on the named path reports "no such limit
'<name>'" (Req 1.3) and never collapses into the all-only message. New
white-box test_repl_limit_table.c exercises both paths directly, including a
dedicated regression guard for the NULL-scoping rule.

Making repl_info.c genuinely call limit_get_configured_limits() broke the link
for ~24 other white-box test files that #include repl_info.c without also
linking plugins/limit.c -- fixed with the same link-satisfying-stub idiom this
file family already uses elsewhere (all return NULL, verified behavior-
preserving since none of these fixtures register the limit plugin).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
Rongsong Shen 2026-09-17 09:53:59 +00:00
parent b79a547a1e
commit d3df8fb29f
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
28 changed files with 1098 additions and 1 deletions

View file

@ -0,0 +1,621 @@
/*
* 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_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;
}
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);
}
/* ---------------------------------------------------------------------- */
/* 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]);
}
/* ---------------------------------------------------------------------- */
/* 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");
}
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);
return UNITY_END();
}