Splits xce_scheduler_t along the controller-I/O seam: xce_sched_core_t
(new core.h/core.c) now holds all scheduling state (jobs/queues/workers/
users/projects/resource managers/plugins/three-thread-cbuf pipeline),
while xce_scheduler_t shrinks to {core; apictx; cluster_cfg}. Proven via
an nm-based link-time isolation test (zero netio/xcecli/idgen/delaymgr
symbols reachable from core.c) plus a reproducible wall-clock-call guard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
249 lines
8.4 KiB
C
249 lines
8.4 KiB
C
/*
|
|
* test_plugin_picker_registration.c - white-box tests for the picker
|
|
* plugin registration category and job_elect_fn compatibility wrapper
|
|
* added to plugin.c (scheduler-core spec, task 1.2).
|
|
*
|
|
* RED: before task 1.2's plugin.h/plugin.c changes, xce_sched_plugin_t had
|
|
* no picker_iface field and legacy_picker_wrapper_iface did not exist --
|
|
* this file failed to compile.
|
|
* GREEN: plugin_register() adapts an old-style, job_elect_fn-only plugin
|
|
* to xce_job_picker_iface_t via a compatibility wrapper and logs a
|
|
* deprecation warning exactly once, at registration time (Req 5
|
|
* "Migration from old job_elect_fn"); a plugin that already provides
|
|
* picker_iface registers as-is, with no warning; a plugin that provides
|
|
* neither (today's allocator/limit categories) is unaffected.
|
|
*
|
|
* #include's the real plugin.c (white-box, same convention as
|
|
* test_handlers.c / test_job_array_expand.c / test_job_array_wire_e2e.c)
|
|
* so this test exercises the real plugin_register() and the real
|
|
* legacy_picker_wrapper_iface -- not a re-implementation of either.
|
|
*/
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <tlog.h>
|
|
#include <unity/unity.h>
|
|
|
|
#include "kvmgr.h"
|
|
#include "scheduler.h"
|
|
|
|
/* plugin.c's plugin_prepare_allocation() (unrelated to the picker
|
|
* registration category under test here) calls sched_fill_limit_requests,
|
|
* defined in scheduler.c -- not pulled into this white-box TU. Stubbed the
|
|
* same way test_handlers.c stubs sched_init_config/sched_get_job: this
|
|
* test never exercises plugin_prepare_allocation, so a no-op body is
|
|
* sufficient to satisfy the linker. */
|
|
int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* req,
|
|
xlist_t* limits)
|
|
{
|
|
(void)sched;
|
|
(void)req;
|
|
(void)limits;
|
|
return 0;
|
|
}
|
|
|
|
#include "../src/plugin.c"
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Early-print log capture: same pattern as test_job_array_wire_e2e.c's
|
|
* wire_e2e_early_print_cb. No test binary in this suite calls tlog_init(),
|
|
* so WARN() (tlog_warn -> tlog(TLOG_WARN, ...)) routes through tlog's
|
|
* pre-init "early print" fallback, which tlog_reg_early_printf_callback()
|
|
* lets a test intercept synchronously on the calling thread -- no
|
|
* tlog_init()/tlog_exit() lifecycle, so it cannot disturb any other test
|
|
* sharing this process.
|
|
* ---------------------------------------------------------------------- */
|
|
|
|
static int g_warn_count = 0;
|
|
|
|
static void picker_reg_early_print_cb(struct tlog_loginfo* loginfo,
|
|
const char* format, va_list ap)
|
|
{
|
|
char buf[512];
|
|
va_list ap_copy;
|
|
|
|
if (loginfo == NULL || loginfo->level != TLOG_WARN) {
|
|
return;
|
|
}
|
|
|
|
va_copy(ap_copy, ap);
|
|
vsnprintf(buf, sizeof(buf), format, ap_copy);
|
|
va_end(ap_copy);
|
|
|
|
if (strstr(buf, "job_elect_fn") != NULL) {
|
|
g_warn_count++;
|
|
}
|
|
}
|
|
|
|
static void start_log_capture(void)
|
|
{
|
|
g_warn_count = 0;
|
|
tlog_set_early_printf(1, 1, 0);
|
|
tlog_reg_early_printf_callback(picker_reg_early_print_cb);
|
|
}
|
|
|
|
static void stop_log_capture(void) { tlog_reg_early_printf_callback(NULL); }
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Fixture: a real sched->core.plugins kvmgr -- same setup as scheduler.c's
|
|
* (static, so not reusable) sched_plugin_init(). Each test gets its own
|
|
* fresh instance; nothing is torn down (suite convention, see meson.build:
|
|
* ASAN_OPTIONS=detect_leaks=0). */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static void fixture_init(xce_scheduler_t* sched)
|
|
{
|
|
kvmgr_params_t params = { .key_type = KVMGR_KEY_STRING,
|
|
.nslots = 16,
|
|
.delta = 8,
|
|
.datakiller = plugin_unregister };
|
|
|
|
memset(sched, 0, sizeof(*sched));
|
|
sched->core.plugins = makeInstance(&xkvmgr_type, ¶ms);
|
|
TEST_ASSERT_NOT_NULL(sched->core.plugins);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Old-style plugin fixture: only job_elect_fn is set, no native
|
|
* picker_iface -- the same shape as today's plugins/fcfs.c (out of scope
|
|
* for this task; this is a minimal standalone stand-in so the test does
|
|
* not depend on fcfs.c's real threading behavior). */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static xce_sched_job_t g_legacy_elected_job;
|
|
static int g_legacy_elect_calls = 0;
|
|
|
|
static void* legacy_job_elect_fn(void* arg)
|
|
{
|
|
TEST_ASSERT_NOT_NULL(arg);
|
|
g_legacy_elect_calls++;
|
|
|
|
return (void*)&g_legacy_elected_job;
|
|
}
|
|
|
|
static void legacy_plugin_init(xce_sched_plugin_t* plugin, char* name)
|
|
{
|
|
memset(plugin, 0, sizeof(*plugin));
|
|
plugin->name = name;
|
|
plugin->job_elect_fn = (plugin_job_election_fn_t)legacy_job_elect_fn;
|
|
}
|
|
|
|
/* Req 5 "Migration from old job_elect_fn": an old-style, job_elect_fn-only
|
|
* plugin registers successfully via the compatibility wrapper and logs a
|
|
* deprecation warning exactly once, at registration time. */
|
|
static void
|
|
test_register_legacy_job_elect_fn_only_plugin_uses_wrapper_and_warns(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
int rc;
|
|
|
|
fixture_init(&sched);
|
|
legacy_plugin_init(&plugin, "test_legacy_picker");
|
|
|
|
start_log_capture();
|
|
rc = plugin_register(&sched, &plugin);
|
|
stop_log_capture();
|
|
|
|
TEST_ASSERT_GREATER_OR_EQUAL(0, rc);
|
|
TEST_ASSERT_EQUAL_INT(1, g_warn_count);
|
|
TEST_ASSERT_NOT_NULL(plugin.picker_iface);
|
|
TEST_ASSERT_EQUAL_PTR(&legacy_picker_wrapper_iface, plugin.picker_iface);
|
|
}
|
|
|
|
/* The wrapper must actually work: next_job() genuinely forwards to the old
|
|
* job_elect_fn, it does not merely register without crashing. */
|
|
static void test_legacy_wrapper_next_job_calls_job_elect_fn(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
xce_sched_job_t* got;
|
|
|
|
fixture_init(&sched);
|
|
legacy_plugin_init(&plugin, "test_legacy_picker_2");
|
|
g_legacy_elect_calls = 0;
|
|
|
|
TEST_ASSERT_TRUE(plugin_register(&sched, &plugin) >= 0);
|
|
TEST_ASSERT_NOT_NULL(plugin.picker_iface);
|
|
TEST_ASSERT_NOT_NULL(plugin.picker_iface->next_job);
|
|
|
|
got = plugin.picker_iface->next_job(&plugin);
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, g_legacy_elect_calls);
|
|
TEST_ASSERT_EQUAL_PTR(&g_legacy_elected_job, got);
|
|
}
|
|
|
|
/* Req 1 AC 2 / Req 5: a plugin that already implements
|
|
* xce_job_picker_iface_t natively registers as-is -- no wrapper
|
|
* substitution, no deprecation warning. */
|
|
static xce_sched_job_t* new_style_next_job_stub(void* picker)
|
|
{
|
|
(void)picker;
|
|
return NULL;
|
|
}
|
|
|
|
static xce_job_picker_iface_t g_new_style_iface = {
|
|
.next_job = new_style_next_job_stub,
|
|
};
|
|
|
|
static void
|
|
test_register_new_style_picker_plugin_skips_wrapper_and_does_not_warn(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
int rc;
|
|
|
|
fixture_init(&sched);
|
|
memset(&plugin, 0, sizeof(plugin));
|
|
plugin.name = "test_new_style_picker";
|
|
plugin.picker_iface = &g_new_style_iface;
|
|
|
|
start_log_capture();
|
|
rc = plugin_register(&sched, &plugin);
|
|
stop_log_capture();
|
|
|
|
TEST_ASSERT_GREATER_OR_EQUAL(0, rc);
|
|
TEST_ASSERT_EQUAL_INT(0, g_warn_count);
|
|
TEST_ASSERT_EQUAL_PTR(&g_new_style_iface, plugin.picker_iface);
|
|
}
|
|
|
|
/* Regression guard: a plugin that is neither category (no job_elect_fn, no
|
|
* picker_iface -- today's allocator/limit plugins) registers unaffected --
|
|
* no wrapper substitution, no warning, picker_iface stays NULL. */
|
|
static void test_register_non_picker_plugin_is_unaffected(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t plugin;
|
|
int rc;
|
|
|
|
fixture_init(&sched);
|
|
memset(&plugin, 0, sizeof(plugin));
|
|
plugin.name = "test_non_picker_plugin";
|
|
|
|
start_log_capture();
|
|
rc = plugin_register(&sched, &plugin);
|
|
stop_log_capture();
|
|
|
|
TEST_ASSERT_GREATER_OR_EQUAL(0, rc);
|
|
TEST_ASSERT_EQUAL_INT(0, g_warn_count);
|
|
TEST_ASSERT_NULL(plugin.picker_iface);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(
|
|
test_register_legacy_job_elect_fn_only_plugin_uses_wrapper_and_warns);
|
|
RUN_TEST(test_legacy_wrapper_next_job_calls_job_elect_fn);
|
|
RUN_TEST(
|
|
test_register_new_style_picker_plugin_skips_wrapper_and_does_not_warn);
|
|
RUN_TEST(test_register_non_picker_plugin_is_unaffected);
|
|
return UNITY_END();
|
|
}
|