diff --git a/.kiro/specs/scheduler-sim-diagnostics/tasks.md b/.kiro/specs/scheduler-sim-diagnostics/tasks.md index 99755f29..d9a0ccad 100644 --- a/.kiro/specs/scheduler-sim-diagnostics/tasks.md +++ b/.kiro/specs/scheduler-sim-diagnostics/tasks.md @@ -17,7 +17,7 @@ - _Requirements: 1.3, 1.7, 4.1, 4.2_ - _Depends: 1_ -- [ ] 2.2 Display configured setting including scope membership +- [x] 2.2 Display configured setting including scope membership - Matched limit's name, constrained resource, value/percent, and per-dimension scope print via the existing `fmt_limit()` library formatter into a stack buffer, line-prefixed with the existing `[sim] ` convention; all output via `fprintf(stdout, ...)`, never bare `printf()` (`test_no_bare_printf.c` regression-gates this) - Inclusion and exclusion group membership both render (`fmt_group()` already handles this, adopted as-is) - Observable: a limit configured with an exclusion group ("all except X") shows both the inclusion and the exclusion in its output; `info limit all` against a fixture with multiple configured limits displays every one's setting diff --git a/src/scheduler/src/repl_info.c b/src/scheduler/src/repl_info.c index 0695e32a..3d76821b 100644 --- a/src/scheduler/src/repl_info.c +++ b/src/scheduler/src/repl_info.c @@ -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 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 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. */ } } diff --git a/src/scheduler/testsuite/test_repl_limit_table.c b/src/scheduler/testsuite/test_repl_limit_table.c index c4b46a7d..eb692f93 100644 --- a/src/scheduler/testsuite/test_repl_limit_table.c +++ b/src/scheduler/testsuite/test_repl_limit_table.c @@ -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=[]). + * 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(); }