feat(scheduler-sim-diagnostics): display configured limit setting via fmt_limit()

repl_info_limit() now prints a matched limit's configured setting on both the
all and named-lookup success paths: fmt_limit() (existing libxce formatter,
unmodified) into a stack buffer, re-split and printed through a new
repl_print_limit_dump() helper matching this file's existing [sim]-prefix-
per-line convention. Inclusion and exclusion group membership both render
since fmt_group() already handles both, adopted as-is per design.md's
explicit reuse decision.

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 10:32:21 +00:00
parent d3df8fb29f
commit 117d01c77e
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
3 changed files with 199 additions and 15 deletions

View file

@ -7,6 +7,7 @@
#include "repl_internal.h"
#include "resreq.h"
#include "utils.h"
#include "xcelimit.h"
/*
* repl_info.c - state inspection (scheduler-core spec, task 12.2, Req 5 AC
@ -1155,14 +1156,52 @@ static void repl_info_resource(xce_sched_sim_driver_t* sim, const char* arg)
}
/*
* repl_info_limit - `info limit <name|all>` name/all resolution
* (scheduler-sim-diagnostics spec, Requirement Set 1, task 2.1, Req 1.3,
* 1.7, 4.1, 4.2). THIS task implements only the name/all resolution and
* the two absence messages -- printing a matched limit's configured
* setting (fmt_limit(), Req 1.1/1.2/1.5) is task 2.2's job, and printing
* its live usage instances (Req 2.1-2.3, 3.1-3.4) is task 3's job. Not yet
* wired into repl_cmd_info() (task 4 adds that branch) -- static and
* currently uncalled from production code; exercised directly by
* repl_print_limit_dump - print fmt_limit()'s (src/lib/xcelimit.c) fully
* verbatim, library-owned buffer for a matched limit's configured setting
* (task 2.2, Req 1.1/1.2/1.5) using this file's own existing "[sim] "
* per-line prefix convention for a multi-line info dump -- the same
* convention repl_info_break() and repl_info_resource_one() (both above)
* already use. fmt_limit() embeds '\n' between its logical lines (limit
* name, one line per configured consumer dimension via fmt_consumer()/
* fmt_group(), then the constrained resource/value line) rather than
* returning an array of strings, so each line is re-split here and printed
* individually with its own "[sim] " prefix rather than emitting the whole
* buffer behind a single leading prefix (design.md's REPL Limit Info
* Command Responsibilities: "fmt_limit() into a stack buffer, printed
* line-by-line with the existing [sim] prefix convention").
*
* Per design.md's/research.md's explicit Boundary Commitments, this
* function only re-prefixes fmt_limit()'s own output -- it does not
* reimplement, reformat, or second-guess anything fmt_limit()/
* fmt_consumer()/fmt_group() decided, including their exclusion-group "~"
* rendering (Req 1.5).
*/
static void repl_print_limit_dump(const char* buf)
{
const char* line = buf;
while (*line != '\0') {
const char* nl = strchr(line, '\n');
int len = (nl != NULL) ? (int)(nl - line) : (int)strlen(line);
fprintf(stdout, "[sim] %.*s\n", len, line);
if (nl == NULL) {
break;
}
line = nl + 1;
}
}
/*
* repl_info_limit - `info limit <name|all>` name/all resolution (task 2.1)
* plus, once a genuine match is found, printing that limit's configured
* setting via the library's own fmt_limit()/fmt_group() formatters (task
* 2.2, Req 1.1/1.2/1.5) -- scheduler-sim-diagnostics spec, Requirement Set
* 1. Printing the Req 1.6 effective-host-scope note is task 2.3's job, and
* printing live usage instances (Req 2.1-2.3, 3.1-3.4) is task 3's job.
* Not yet wired into repl_cmd_info() (task 4 adds that branch) -- static
* and currently uncalled from production code; exercised directly by
* test_repl_limit_table.c per this task's own scope.
*
* Requirement 1.7's "no limits configured" message is reserved for the
@ -1202,9 +1241,16 @@ static void repl_info_limit(xce_sched_sim_driver_t* sim, const char* arg)
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA) || (limit == NULL)) {
break;
}
/* task 2.2 prints this limit's configured setting here
* (fmt_limit()); task 3 adds its live usage. This task's own
* scope stops at proving resolution reaches a genuine match. */
{
char limit_buf[1024];
int fmt_rc = fmt_limit(limit_buf, sizeof(limit_buf), limit);
if (fmt_rc >= 0) {
repl_print_limit_dump(limit_buf);
}
}
/* task 2.3 adds the Req 1.6 effective-host-scope note here;
* task 3 adds this limit's live usage. */
n_shown++;
}
iter_destroy(&it);
@ -1226,9 +1272,16 @@ static void repl_info_limit(xce_sched_sim_driver_t* sim, const char* arg)
return;
}
/* task 2.2 prints the matched limit's configured setting here
* (fmt_limit()); task 3 adds its live usage. This task's own scope
* stops at proving resolution reaches a genuine match. */
{
char limit_buf[1024];
int fmt_rc = fmt_limit(limit_buf, sizeof(limit_buf), match);
if (fmt_rc >= 0) {
repl_print_limit_dump(limit_buf);
}
}
/* task 2.3 adds the Req 1.6 effective-host-scope note here; task 3
* adds this limit's live usage. */
}
}

View file

@ -378,6 +378,51 @@ static void add_configured_limit(xce_sched_plugin_t* plugin, const char* name)
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);
}
/* ---------------------------------------------------------------------- */
/* Tests */
/* ---------------------------------------------------------------------- */
@ -602,6 +647,89 @@ test_info_limit_named_does_not_report_no_such_limit_for_configured_name(void)
"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");
}
int main(void)
{
UNITY_BEGIN();
@ -617,5 +745,8 @@ int main(void)
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);
return UNITY_END();
}