myxce/src/scheduler/testsuite/test_plugin_picker_registration.c
Rongsong Shen 7c229284e2
feat(scheduler-core): extract xce_sched_core_t from monolithic scheduler struct (task 4.1)
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
2026-08-03 10:55:58 +00:00

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, &params);
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();
}