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
This commit is contained in:
parent
604a564904
commit
7c229284e2
26 changed files with 1233 additions and 661 deletions
315
src/scheduler/testsuite/test_core_isolation.c
Normal file
315
src/scheduler/testsuite/test_core_isolation.c
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* test_core_isolation.c - regression guard for task 4.1's Observable
|
||||
* (scheduler-core spec, Req 5.1/5.2): "the core structure compiles and
|
||||
* links with zero symbols from the networking/API libraries; a build-time
|
||||
* or link-time check confirms this" -- and the accompanying constraint
|
||||
* "no wall-clock time call appears anywhere in the extracted core's
|
||||
* source."
|
||||
*
|
||||
* Two layers, same genre as test_picker_core_loop_boundary.c /
|
||||
* test_picker_pendreason_factory_audit.c (source-text scans of the real,
|
||||
* current on-disk files, not a frozen snapshot):
|
||||
*
|
||||
* 1. Source-text scan of core.h/core.c: no #include of client.h,
|
||||
* netio.h, xceapi.h, delay.h, idgen.h, or xce/cluster.h (the
|
||||
* "Allowed Dependencies" list in design.md names exactly these as
|
||||
* forbidden); no call-site substring matching a symbol exported only
|
||||
* by those libraries; no wall-clock call (time()/clock_gettime()/
|
||||
* gettimeofday()) anywhere in core.c.
|
||||
*
|
||||
* 2. The literal build-time/link-time proof the Observable asks for,
|
||||
* not achievable by a text scan alone: core.c is compiled in
|
||||
* isolation into its own static library (meson.build's
|
||||
* sched_core_isolation_lib, this test's XCE_CORE_LIB_PATH), and this
|
||||
* test inspects that archive's UNDEFINED SYMBOL TABLE via nm(1) --
|
||||
* proving what core.c's *compiled code actually calls*, independent
|
||||
* of what any header happens to transitively declare (xce/types.h,
|
||||
* used by literally every file in this directory, itself includes
|
||||
* netio.h for unrelated reasons -- so a "no forbidden #include"
|
||||
* check alone would either false-positive on that pre-existing,
|
||||
* harmless transitive include, or have to special-case it; the nm
|
||||
* check sidesteps that entirely by checking *usage*, not
|
||||
* *visibility*).
|
||||
*
|
||||
* Why nm on a static_library() rather than the full xce_scheduler
|
||||
* executable: the executable links the whole monolithic libxce_dep, so
|
||||
* every symbol from netio/xceapi/client/delay/idgen would appear in ITS
|
||||
* undefined-symbol table regardless of whether core.c itself ever calls
|
||||
* one -- that would prove nothing. Compiling core.c alone into its own
|
||||
* archive (sched_core_isolation_lib, meson.build) and inspecting THAT
|
||||
* object's undefined symbols isolates exactly what this one translation
|
||||
* unit references.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#ifndef XCE_CORE_H_PATH
|
||||
#error \
|
||||
"XCE_CORE_H_PATH must be defined by meson.build to core.h's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_CORE_C_PATH
|
||||
#error \
|
||||
"XCE_CORE_C_PATH must be defined by meson.build to core.c's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_CORE_LIB_PATH
|
||||
#error \
|
||||
"XCE_CORE_LIB_PATH must be defined by meson.build to the absolute path of core.c's isolated static_library() archive"
|
||||
#endif
|
||||
|
||||
/* Forbidden headers (design.md "Allowed Dependencies", Req 5 AC 3): the
|
||||
* core must never directly include any of these. */
|
||||
static const char* const FORBIDDEN_INCLUDES[] = {
|
||||
"#include \"client.h\"", "#include \"netio.h\"",
|
||||
"#include \"xceapi.h\"", "#include \"delay.h\"",
|
||||
"#include \"idgen.h\"", "#include \"xce/cluster.h\"",
|
||||
"#include <client.h>", "#include <netio.h>",
|
||||
"#include <xceapi.h>", "#include <delay.h>",
|
||||
"#include <idgen.h>", "#include <xce/cluster.h>",
|
||||
};
|
||||
#define NUM_FORBIDDEN_INCLUDES \
|
||||
(sizeof(FORBIDDEN_INCLUDES) / sizeof(FORBIDDEN_INCLUDES[0]))
|
||||
|
||||
/* Forbidden call-site symbols: exported only by netio.c/xceapi.c/client.c/
|
||||
* delay.c/idgen.c (verified against those headers' own extern lists) --
|
||||
* none of these are legitimately callable from a core with zero controller
|
||||
* I/O, network, or timer dependency. */
|
||||
static const char* const FORBIDDEN_SYMBOLS[] = {
|
||||
"xcecli_",
|
||||
"endpoint_",
|
||||
"iomgr_",
|
||||
"idgen_",
|
||||
"delaymgr_",
|
||||
"delay_task",
|
||||
"xce_connect_cluster",
|
||||
"xce_reconnect_",
|
||||
"xce_parse_peers",
|
||||
"xce_cluster_config_validate",
|
||||
"socket_connect",
|
||||
"create_ssl_context",
|
||||
"initialize_openssl",
|
||||
"cleanup_openssl",
|
||||
"io_fsm_start",
|
||||
};
|
||||
#define NUM_FORBIDDEN_SYMBOLS \
|
||||
(sizeof(FORBIDDEN_SYMBOLS) / sizeof(FORBIDDEN_SYMBOLS[0]))
|
||||
|
||||
/* Wall-clock calls (Req 5 AC "no wall-clock time call appears anywhere in
|
||||
* the extracted core's source"). */
|
||||
static const char* const FORBIDDEN_WALLCLOCK[] = {
|
||||
"time(",
|
||||
"clock_gettime(",
|
||||
"gettimeofday(",
|
||||
};
|
||||
#define NUM_FORBIDDEN_WALLCLOCK \
|
||||
(sizeof(FORBIDDEN_WALLCLOCK) / sizeof(FORBIDDEN_WALLCLOCK[0]))
|
||||
|
||||
static char* read_whole_file(const char* path)
|
||||
{
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (f == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
long len = ftell(f);
|
||||
if (len < 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
rewind(f);
|
||||
|
||||
char* buf = (char*)malloc((size_t)len + 1);
|
||||
if (buf == NULL) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t nread = fread(buf, 1, (size_t)len, f);
|
||||
fclose(f);
|
||||
buf[nread] = '\0';
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
void setUp(void) { return; }
|
||||
void tearDown(void) { return; }
|
||||
|
||||
/* Sanity check: both source paths baked in by meson.build must actually be
|
||||
* readable -- without this, a stale path would make every scan below
|
||||
* silently pass on zero bytes of text (the exact false-confidence failure
|
||||
* mode this whole test exists to avoid). */
|
||||
static void test_core_source_paths_are_readable(void)
|
||||
{
|
||||
char* h = read_whole_file(XCE_CORE_H_PATH);
|
||||
char* c = read_whole_file(XCE_CORE_C_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_CORE_H_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_CORE_C_PATH);
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
}
|
||||
|
||||
static void test_core_never_includes_forbidden_headers(void)
|
||||
{
|
||||
char* h = read_whole_file(XCE_CORE_H_PATH);
|
||||
char* c = read_whole_file(XCE_CORE_C_PATH);
|
||||
size_t i;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
|
||||
for (i = 0; i < NUM_FORBIDDEN_INCLUDES; i++) {
|
||||
char msg[256];
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"core.h directly includes forbidden header '%s'",
|
||||
FORBIDDEN_INCLUDES[i]);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(h, FORBIDDEN_INCLUDES[i]), msg);
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"core.c directly includes forbidden header '%s'",
|
||||
FORBIDDEN_INCLUDES[i]);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(c, FORBIDDEN_INCLUDES[i]), msg);
|
||||
}
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
}
|
||||
|
||||
static void test_core_never_calls_forbidden_symbols(void)
|
||||
{
|
||||
char* h = read_whole_file(XCE_CORE_H_PATH);
|
||||
char* c = read_whole_file(XCE_CORE_C_PATH);
|
||||
size_t i;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
|
||||
for (i = 0; i < NUM_FORBIDDEN_SYMBOLS; i++) {
|
||||
char msg[256];
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"core.h references forbidden networking/API symbol '%s'",
|
||||
FORBIDDEN_SYMBOLS[i]);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(h, FORBIDDEN_SYMBOLS[i]), msg);
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"core.c references forbidden networking/API symbol '%s'",
|
||||
FORBIDDEN_SYMBOLS[i]);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(c, FORBIDDEN_SYMBOLS[i]), msg);
|
||||
}
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
}
|
||||
|
||||
static void test_core_never_calls_wallclock_functions(void)
|
||||
{
|
||||
char* c = read_whole_file(XCE_CORE_C_PATH);
|
||||
size_t i;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
|
||||
for (i = 0; i < NUM_FORBIDDEN_WALLCLOCK; i++) {
|
||||
char msg[256];
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"core.c calls wall-clock function '%s' -- the core must "
|
||||
"access time exclusively through a future xce_core_now() "
|
||||
"(scheduler-core spec, task 4.5)",
|
||||
FORBIDDEN_WALLCLOCK[i]);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(c, FORBIDDEN_WALLCLOCK[i]), msg);
|
||||
}
|
||||
|
||||
free(c);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_core_object_has_no_forbidden_undefined_symbols - the literal
|
||||
* build-time/link-time proof: core.c, compiled in isolation into
|
||||
* sched_core_isolation_lib (meson.build), must not reference (as an
|
||||
* undefined external symbol in the compiled object) anything matching the
|
||||
* forbidden prefixes above. This is independent of what any header
|
||||
* transitively declares -- it reflects only what core.c's compiled code
|
||||
* actually calls.
|
||||
*/
|
||||
static void test_core_object_has_no_forbidden_undefined_symbols(void)
|
||||
{
|
||||
char cmd[4096];
|
||||
char line[1024];
|
||||
FILE* p;
|
||||
int any_undefined_seen = 0;
|
||||
|
||||
snprintf(cmd, sizeof(cmd), "nm --format=posix -u \"%s\" 2>&1",
|
||||
XCE_CORE_LIB_PATH);
|
||||
|
||||
p = popen(cmd, "r");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(p, "failed to popen() nm(1)");
|
||||
|
||||
while (fgets(line, sizeof(line), p) != NULL) {
|
||||
char symbol[512];
|
||||
char type;
|
||||
size_t i;
|
||||
|
||||
/* posix format for an undefined symbol: "<name> U" (no
|
||||
* value/size columns). Skip anything that doesn't parse as
|
||||
* exactly that -- e.g. a blank line or an archive member header
|
||||
* some nm versions print. */
|
||||
if (sscanf(line, "%511s %c", symbol, &type) != 2) {
|
||||
continue;
|
||||
}
|
||||
if (type != 'U') {
|
||||
continue;
|
||||
}
|
||||
|
||||
any_undefined_seen = 1;
|
||||
|
||||
for (i = 0; i < NUM_FORBIDDEN_SYMBOLS; i++) {
|
||||
if (strncmp(symbol, FORBIDDEN_SYMBOLS[i],
|
||||
strlen(FORBIDDEN_SYMBOLS[i]))
|
||||
== 0) {
|
||||
char msg[768];
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"core.c's compiled object has an undefined "
|
||||
"reference to '%s', matching forbidden "
|
||||
"networking/API prefix '%s' -- core.c must never "
|
||||
"link networking/API library symbols (Req 5.1/"
|
||||
"5.2)",
|
||||
symbol, FORBIDDEN_SYMBOLS[i]);
|
||||
pclose(p);
|
||||
TEST_FAIL_MESSAGE(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pclose(p);
|
||||
|
||||
/* If nm produced no undefined symbols at all, either the archive path
|
||||
* is wrong or nm itself failed silently -- either way this test would
|
||||
* be vacuously passing without having checked anything real. */
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
any_undefined_seen,
|
||||
"nm reported zero undefined symbols for XCE_CORE_LIB_PATH -- "
|
||||
"the archive path is likely wrong (this test would otherwise pass "
|
||||
"vacuously)");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_core_source_paths_are_readable);
|
||||
RUN_TEST(test_core_never_includes_forbidden_headers);
|
||||
RUN_TEST(test_core_never_calls_forbidden_symbols);
|
||||
RUN_TEST(test_core_never_calls_wallclock_functions);
|
||||
RUN_TEST(test_core_object_has_no_forbidden_undefined_symbols);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue