diff --git a/.kiro/specs/scheduler-sim-diagnostics/tasks.md b/.kiro/specs/scheduler-sim-diagnostics/tasks.md index 7fdb11d6..d66d16e4 100644 --- a/.kiro/specs/scheduler-sim-diagnostics/tasks.md +++ b/.kiro/specs/scheduler-sim-diagnostics/tasks.md @@ -1,6 +1,6 @@ # Implementation Plan: scheduler-sim-diagnostics (Requirement Set 1 — Limit Inspection) -- [ ] 1. Add read-only accessors exposing the limit plugin's configured-limits and live-usage-instances tables +- [x] 1. Add read-only accessors exposing the limit plugin's configured-limits and live-usage-instances tables - Two new functions (`limit_get_configured_limits()`, `limit_get_usage_instances()`) returning raw kvmgr handles (or `NULL` when the plugin is absent), declared in `plugin.h` alongside `register_limit()`, defined in `plugins/limit.c` - Never allocate, mutate, or trigger plugin hooks — a pure read of already-existing pointers - New/extended unit tests in `test_limit_plugin.c` cover: plugin registered with configured limits (handle returned, iterable), plugin registered with zero limits, plugin not registered (`NULL`) diff --git a/src/scheduler/include/plugin.h b/src/scheduler/include/plugin.h index cfcb7055..dcd9c615 100644 --- a/src/scheduler/include/plugin.h +++ b/src/scheduler/include/plugin.h @@ -114,6 +114,20 @@ extern int plugin_job_rusage_update(void*, xce_sched_job_t*, extern int register_fcfs(void*); extern int register_builtin_fcfs(void*); extern int register_limit(void*); + +/* limit_get_configured_limits/limit_get_usage_instances - scheduler-sim- + * diagnostics spec, Requirement Set 1 (Limit Plugin Accessor), task 1. + * Read-only crossing of the plugin-private-state boundary: each looks up + * the limit plugin ("xce_sched_limit") in core->plugins and returns the + * requested field of its private limit_plugin_data_t (plugins/limit.c) -- + * a raw kvmgr handle, iterable via iter_make()/iter_next() -- or NULL if + * the plugin is not registered or not yet initialized (plugin->data == + * NULL). Never allocates, mutates, or triggers plugin hooks; performs no + * locking beyond what key_find() already does internally. NULL is a + * semantic signal only ("nothing is configured yet"), not an error -- the + * caller decides what to display for it. */ +extern var limit_get_configured_limits(xce_sched_core_t* core); +extern var limit_get_usage_instances(xce_sched_core_t* core); extern int register_seqjob_allocator(void*); extern int register_parjob_allocator(void*); diff --git a/src/scheduler/src/plugins/limit.c b/src/scheduler/src/plugins/limit.c index ea45fcc6..0c48b97a 100644 --- a/src/scheduler/src/plugins/limit.c +++ b/src/scheduler/src/plugins/limit.c @@ -551,6 +551,61 @@ static void limit_fini(xce_sched_plugin_t* plugin) FREEUP(plugin); } +/* limit_get_configured_limits/limit_get_usage_instances -- scheduler-sim- + * diagnostics spec, Requirement Set 1 (Limit Plugin Accessor), task 1. Pure + * reads of already-existing pointers: look up the limit plugin by its + * fixed registration name (owned here, hardcoded nowhere else) and return + * the requested privdata field. NULL whenever the plugin is not registered + * or not yet initialized (plugin->data == NULL) -- never a partially-valid + * handle. No locking beyond what key_find() already does internally + * (matches plugin_job_update()'s (plugin.c) equally lock-free + * core->plugins reads); no allocation, mutation, or hook invocation. */ +var limit_get_configured_limits(xce_sched_core_t* core) +{ + xce_sched_plugin_t* plugin = NULL; + limit_plugin_data_t* privdata; + int rc; + + if (core == NULL) { + return NULL; + } + + rc = key_find(core->plugins, "xce_sched_limit", (void**)&plugin); + if ((rc <= 0) || (plugin == NULL)) { + return NULL; + } + + privdata = (limit_plugin_data_t*)plugin->data; + if (privdata == NULL) { + return NULL; + } + + return privdata->limits; +} + +var limit_get_usage_instances(xce_sched_core_t* core) +{ + xce_sched_plugin_t* plugin = NULL; + limit_plugin_data_t* privdata; + int rc; + + if (core == NULL) { + return NULL; + } + + rc = key_find(core->plugins, "xce_sched_limit", (void**)&plugin); + if ((rc <= 0) || (plugin == NULL)) { + return NULL; + } + + privdata = (limit_plugin_data_t*)plugin->data; + if (privdata == NULL) { + return NULL; + } + + return privdata->instances; +} + int register_limit(void* sched) { static xce_sched_plugin_t limit_plugin = { diff --git a/src/scheduler/testsuite/test_limit_plugin.c b/src/scheduler/testsuite/test_limit_plugin.c index 9b62ee8f..52ecd8fc 100644 --- a/src/scheduler/testsuite/test_limit_plugin.c +++ b/src/scheduler/testsuite/test_limit_plugin.c @@ -238,10 +238,195 @@ static void test_distinct_limit_names_both_succeed(void) free_limit_config(limits[1]); } +/* ---------------------------------------------------------------------- */ +/* scheduler-sim-diagnostics spec, Requirement Set 1 (Limit Plugin + * Accessor), task 1: limit_get_configured_limits()/limit_get_usage_ + * instances() (plugin.h, this file). Pure read accessors crossing the + * plugin-private-state boundary. These tests prove (a) a registered + * plugin's real limits/instances kvmgr handles come back non-NULL and + * genuinely iterable via iter_make()/iter_next() -- including yielding + * back the exact same limit that was configured, (b) a registered-but- + * empty plugin (limit_init() ran, but no limit/instance was ever added) + * still returns a non-NULL, empty-iterating handle rather than NULL, and + * (c) an unregistered plugin returns NULL from both accessors. + * + * fixture_init() (above) builds a real limit plugin via limit_init() + * directly (plugin_register() is stubbed to a no-op in this TU), so + * plugin->plugin_id/core->plugins are never populated by it -- these + * accessors' key_find(core->plugins, "xce_sched_limit", &plugin) lookup + * needs a real core->plugins kvmgr with the plugin actually key_add()'d + * into it under that exact name. The real plugin_register() (plugin.c) + * is not linked into this TU (scheduler_test_deps carries no scheduler- + * library dependency -- see this file's own header comment), so + * xce_sched_core_init() (core.c) can't be called either. The two helpers + * below replicate just the one piece of it these tests need: the same + * core->plugins kvmgr core.c's xce_sched_core_init() creates (same key + * type/sizing as its plugins_params), minus its .datakiller + * (plugin_unregister -- also unlinked here; NULL is safe since this + * suite never tears fixtures down, matching every other fixture's + * "ASAN_OPTIONS=detect_leaks=0" convention, meson.build). + */ + +/* Registered case: core->plugins is a real kvmgr with the plugin key_add()'d + * into it under its real registration name, exactly as plugin_register() + * (plugin.c) itself would do -- so the accessors' key_find() success path + * is genuinely exercised, not just limit_init()'s privdata setup. */ +static void fixture_register_plugin_in_core(xce_sched_core_t* core, + xce_sched_plugin_t* plugin) +{ + kvmgr_params_t plugins_params = { .key_type = KVMGR_KEY_STRING, + .nslots = 16, + .delta = 8, + .datakiller = NULL }; + int rc; + + core->plugins = makeInstance(&xkvmgr_type, &plugins_params); + TEST_ASSERT_NOT_NULL(core->plugins); + + plugin->name = "xce_sched_limit"; + + rc = key_add(core->plugins, (void*)plugin->name, (void*)plugin); + TEST_ASSERT_TRUE(rc >= 0); +} + +/* Not-registered case: core->plugins is still a real, initialized + * (non-NULL) kvmgr -- matching the accessors' documented precondition + * that core is xce_sched_core_init()-initialized -- but the + * "xce_sched_limit" key is never added to it. This is the realistic + * not-registered scenario: core->plugins itself always exists once a + * core is initialized; only the specific plugin entry is ever missing. */ +static void fixture_init_empty_plugin_registry(xce_sched_core_t* core) +{ + kvmgr_params_t plugins_params = { .key_type = KVMGR_KEY_STRING, + .nslots = 16, + .delta = 8, + .datakiller = NULL }; + + core->plugins = makeInstance(&xkvmgr_type, &plugins_params); + TEST_ASSERT_NOT_NULL(core->plugins); +} + +/* (c) plugin not registered -> NULL from both accessors. */ +static void test_accessors_null_when_plugin_not_registered(void) +{ + xce_scheduler_t sched; + xce_sched_plugin_t plugin; + var limits_handle; + var instances_handle; + + fixture_init(&sched, &plugin); + fixture_init_empty_plugin_registry(&(sched.core)); + + limits_handle = limit_get_configured_limits(&(sched.core)); + instances_handle = limit_get_usage_instances(&(sched.core)); + + TEST_ASSERT_NULL(limits_handle); + TEST_ASSERT_NULL(instances_handle); +} + +/* (a) The observable case: registered plugin with a known configured + * limit -- limit_get_configured_limits()'s returned handle, iterated via + * iter_make()/iter_next(), yields that same limit back. Also confirms + * limit_get_usage_instances() reads a genuinely different field (not an + * alias of the limits handle) and, since no job has matched against this + * limit yet, is itself non-NULL but empty (Requirement 2.2's "no active + * usage instance yet"). */ +static void test_get_configured_limits_handle_iterates_registered_limit(void) +{ + xce_scheduler_t sched; + xce_sched_plugin_t plugin; + xce_limit_config_t* limits[1]; + xce_conf4sched_t config; + var limits_handle; + var instances_handle; + iterator_t it; + xce_limit_t* found = NULL; + int rc; + + fixture_init(&sched, &plugin); + fixture_register_plugin_in_core(&(sched.core), &plugin); + + limits[0] = make_limit_config("observable_limit"); + + memset(&config, 0, sizeof(config)); + config.nlimits = 1; + config.limits = limits; + + rc = limit_update_config(&plugin, &config); + TEST_ASSERT_EQUAL_INT(0, rc); + + limits_handle = limit_get_configured_limits(&(sched.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("observable_limit", found->name); + + rc = iter_next(&it, (void**)&found); + TEST_ASSERT_EQUAL_INT(RTTI_ITER_NO_DATA, rc); + + iter_destroy(&it); + + instances_handle = limit_get_usage_instances(&(sched.core)); + TEST_ASSERT_NOT_NULL(instances_handle); + TEST_ASSERT_TRUE(instances_handle != limits_handle); + + { + void* item = NULL; + + rc = iter_make(&it, instances_handle); + TEST_ASSERT_TRUE(rc >= 0); + + rc = iter_next(&it, &item); + TEST_ASSERT_EQUAL_INT(RTTI_ITER_NO_DATA, rc); + + iter_destroy(&it); + } + + free_limit_config(limits[0]); +} + +/* (b) plugin registered with zero limits: privdata->limits is a real, + * freshly created (limit_init()) kvmgr with nothing added to it yet -- + * the accessor must still return a non-NULL handle (distinguishing + * "plugin present, zero limits" from "plugin absent" per Requirement + * 1.7), and iterating it must yield no elements. */ +static void test_get_configured_limits_registered_with_zero_limits(void) +{ + xce_scheduler_t sched; + xce_sched_plugin_t plugin; + var limits_handle; + iterator_t it; + void* item = NULL; + int rc; + + fixture_init(&sched, &plugin); + fixture_register_plugin_in_core(&(sched.core), &plugin); + + limits_handle = limit_get_configured_limits(&(sched.core)); + TEST_ASSERT_NOT_NULL(limits_handle); + + 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); +} + int main(void) { UNITY_BEGIN(); RUN_TEST(test_duplicate_limit_name_no_double_free); RUN_TEST(test_distinct_limit_names_both_succeed); + RUN_TEST(test_accessors_null_when_plugin_not_registered); + RUN_TEST(test_get_configured_limits_handle_iterates_registered_limit); + RUN_TEST(test_get_configured_limits_registered_with_zero_limits); return UNITY_END(); }