fix(scheduler-sim-diagnostics): bootstrap limit plugin in sim mode (bug-1825)

info limit was correctly implemented and tested but inert against a real
replay: register_limit()'s only call site was normal-mode sched_register_
plugins(); sim mode's own plugin bootstrap (sim_bootstrap_picker()) never
registered the limit plugin. The snapshot format already carries the
original session's limit config (sim->core.config, a borrowed reference set
by xce_sched_core_load_snapshot()) -- nothing needed to change in the
recording/replay format.

New sim_bootstrap_limit_plugin() (sim_limit.c, mirroring sim_picker.c's
structure) registers the plugin via plugin_register_core() and seeds it via
a new limit_seed_config(plugin, xce_sched_core_t* core, config) entry point,
wired into sim_loop.c alongside the existing sim_bootstrap_picker() call.
limit_new_plugin() extracted from register_limit() as a factory, mirroring
builtin_fcfs_new_plugin()'s established pattern; register_limit() itself is
behavior-preserving.

First attempt set plugin->sched to a disguised xce_sched_sim_driver_t*,
relying on it sharing xce_scheduler_t's first-member layout to reuse the
existing config_hook path -- rejected on review: this repeats the exact
cross-cast pattern that already corrupted memory once in this codebase (see
core.h's xce_core_cache_clear_cb_t history). Fixed by making
limit_seed_config() take xce_sched_core_t* as an honest parameter instead,
with limit_update_config() (normal mode) and the new sim path both
delegating to a shared limit_seed_config_impl() -- no cast, no aliasing risk,
independently verified by two review rounds reading the actual struct
layouts and call chains.

Also added sim_limit.c to sched_sim_isolation_lib so Req 5 AC 17's
controller-I/O-isolation check covers it (an initial gap, closed in the same
remediation round). Two ripple waves of the same established link-stub
pattern applied to the ~24 white-box test files that #include sim_loop.c.

info limit now shows real configured-limit data against a genuine replay.
Live usage will still correctly show "no active usage instance yet" until a
separate, larger fix (logged as bug-1826) wires limit *matching* into the
sim decision path -- deliberately out of this task's scope.

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-18 04:46:49 +00:00
parent 2f1ffd1e4a
commit 487e7b9419
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
32 changed files with 1377 additions and 28 deletions

View file

@ -90,6 +90,7 @@
#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"
@ -2395,6 +2396,180 @@ test_repl_step_continue_with_info_limit_interleaved_matches_baseline_decision_se
"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");
}
int main(void)
{
UNITY_BEGIN();
@ -2443,5 +2618,11 @@ int main(void)
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);
return UNITY_END();
}