feat(scheduler-core): enforce simulation isolation (task 11.3)

Three-layer regression guard (test_sim_isolation.c) proving sim.h/sim.c/
sim_loop.c hold no controller-facing client/cluster-config/capnp-socket
state: source-text scan, struct-body field scan, and nm(1) link-time
undefined-symbol audit of a new isolated sched_sim_isolation_lib target.
This commit is contained in:
Rongsong Shen 2026-08-07 00:19:45 +00:00
parent 7e1e861e4f
commit b8d2f5f48d
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
4 changed files with 604 additions and 1 deletions

View file

@ -323,11 +323,12 @@
- _Depends: 11.1_ - _Depends: 11.1_
- _Verified: 2026-08-06 — new `sim_loop.c` (562 lines) implements `sched_sim_driver_run()` (replacing task 11.1's `-ENOSYS` placeholder): decodes `.replay` messages in file order, jumps the virtual clock to each `ReplayEvent`'s exact timestamp as it's queued, and on each `ReplaySession` boundary runs a real `core_push_events→core_go→drain decisions→core_end_session` cycle before resetting the batch; mid-stream `SnapshotEntry` messages correctly skipped per AC 20. Discovered task 11.1 left no picker registered and no thread backing the core's cbufs, which would deadlock the decision-drain loop unconditionally the moment a real event loop existed — necessarily in-scope, not a convenience shortcut (independently confirmed by review). Fixed via new `plugin_register_core()` (`plugin.c`/`plugin.h`, registers a picker against a bare `xce_sched_core_t*`, no `xce_scheduler_t*` needed, precedent-matched to task 4.6's `test_core_full_cycle.c`) + `builtin_fcfs_new_plugin()` factory extracted from the existing `register_builtin_fcfs()` (`plugins/builtin_fcfs.c`) + a persistent picker-driving thread (`sim_picker_thread_fn`). Review independently verified the extraction is genuinely additive and behavior-preserving on the LIVE normal-mode path (32/32 whole-project tests including controller/libxce, not just the scheduler suite) — the only finding was a non-blocking doc-accuracy nit (`plugin_register_core()`'s doc comment overclaims parity with the legacy `job_elect_fn` adapter step, which it correctly omits since its only caller never uses that path). Since real resource-matching allocation is unreachable without a genuine `xce_scheduler_t*` (a pre-existing gap belonging to the not-yet-designed scheduler-allocation spec), `sim_picker_thread_fn()` conservatively PENDs every candidate rather than fabricating a DISPATCH — judged by review as honestly-scoped and sufficient for this task's narrow ordering/clock-timing Observable (AC 18's pseudocode drains "decisions" generically, not specifically dispatches), but flagged as a real gap once `--sim`'s decisions need to be meaningful (task 11.4/scheduler-allocation's problem, not this task's). RED phase independently re-reproduced by review (reverted to `-ENOSYS`, confirmed real test failure, restored). 28/28 scheduler suite, 32/32 whole-project, clang-format clean. Non-blocking follow-up noted: the PEND-candidate loop itself has no direct test yet (both new tests exercise the "no resolvable picker" branch only) — worth covering when task 11.4 needs that path exercised for real._ - _Verified: 2026-08-06 — new `sim_loop.c` (562 lines) implements `sched_sim_driver_run()` (replacing task 11.1's `-ENOSYS` placeholder): decodes `.replay` messages in file order, jumps the virtual clock to each `ReplayEvent`'s exact timestamp as it's queued, and on each `ReplaySession` boundary runs a real `core_push_events→core_go→drain decisions→core_end_session` cycle before resetting the batch; mid-stream `SnapshotEntry` messages correctly skipped per AC 20. Discovered task 11.1 left no picker registered and no thread backing the core's cbufs, which would deadlock the decision-drain loop unconditionally the moment a real event loop existed — necessarily in-scope, not a convenience shortcut (independently confirmed by review). Fixed via new `plugin_register_core()` (`plugin.c`/`plugin.h`, registers a picker against a bare `xce_sched_core_t*`, no `xce_scheduler_t*` needed, precedent-matched to task 4.6's `test_core_full_cycle.c`) + `builtin_fcfs_new_plugin()` factory extracted from the existing `register_builtin_fcfs()` (`plugins/builtin_fcfs.c`) + a persistent picker-driving thread (`sim_picker_thread_fn`). Review independently verified the extraction is genuinely additive and behavior-preserving on the LIVE normal-mode path (32/32 whole-project tests including controller/libxce, not just the scheduler suite) — the only finding was a non-blocking doc-accuracy nit (`plugin_register_core()`'s doc comment overclaims parity with the legacy `job_elect_fn` adapter step, which it correctly omits since its only caller never uses that path). Since real resource-matching allocation is unreachable without a genuine `xce_scheduler_t*` (a pre-existing gap belonging to the not-yet-designed scheduler-allocation spec), `sim_picker_thread_fn()` conservatively PENDs every candidate rather than fabricating a DISPATCH — judged by review as honestly-scoped and sufficient for this task's narrow ordering/clock-timing Observable (AC 18's pseudocode drains "decisions" generically, not specifically dispatches), but flagged as a real gap once `--sim`'s decisions need to be meaningful (task 11.4/scheduler-allocation's problem, not this task's). RED phase independently re-reproduced by review (reverted to `-ENOSYS`, confirmed real test failure, restored). 28/28 scheduler suite, 32/32 whole-project, clang-format clean. Non-blocking follow-up noted: the PEND-candidate loop itself has no direct test yet (both new tests exercise the "no resolvable picker" branch only) — worth covering when task 11.4 needs that path exercised for real._
- [ ] 11.3 Enforce simulation isolation - [x] 11.3 Enforce simulation isolation
- The simulation driver's state holds no live-controller client context, no cluster configuration, and no capnp socket of any kind — confirmed by the struct definition itself, not runtime checks alone - The simulation driver's state holds no live-controller client context, no cluster configuration, and no capnp socket of any kind — confirmed by the struct definition itself, not runtime checks alone
- Observable: a build-time or structural check confirms no controller-facing symbol is reachable from the simulation driver's compiled object - Observable: a build-time or structural check confirms no controller-facing symbol is reachable from the simulation driver's compiled object
- _Requirements: 5.17_ - _Requirements: 5.17_
- _Depends: 11.1_ - _Depends: 11.1_
- _Verified: 2026-08-06 — new `test_sim_isolation.c` (548 lines) enforces AC 17 across `sim.h`/`sim.c`/`sim_loop.c` in three layers, extending `test_core_isolation.c`'s two-layer pattern rather than duplicating it: (1) source-text scan for forbidden `#include`s and forbidden-symbol call sites, reusing `test_core_isolation.c`'s exact `FORBIDDEN_SYMBOLS` list; (2) a structural check `test_core_isolation.c` has no equivalent of — extracts only the `typedef struct {...} xce_sched_sim_driver_t;` body text (via a comment-stripping pass first, since `sim.h`'s own boundary doc comment legitimately names `xcecli_ctx_t`/`xce_cluster_config_t` in prose) and scans that narrower region for forbidden field types; (3) the literal build/link-time proof — `sim.c` and `sim_loop.c` compiled together into a new isolated `sched_sim_isolation_lib` static_library (`meson.build`), inspected via `nm(1)`'s undefined-symbol table to prove what the compiled object actually calls, independent of transitive header declarations. Deliberately excludes the wall-clock ban (`FORBIDDEN_WALLCLOCK`) — out of this task's Req 5 AC 17 scope, and `sim.c`/`sim_loop.c` legitimately call `clock_gettime()` to seed/advance the core's virtual clock from the driver side, already documented at those call sites. 5/5 new sub-tests pass; 29/29 full scheduler suite passes; whole-project build clean. Independently reviewed and approved.
- [ ] 11.4 Implement decision verification - [ ] 11.4 Implement decision verification
- Compare each core-emitted decision against the corresponding recorded decision at the same logical position when verification is enabled; report any mismatch with the logical time and decision detail - Compare each core-emitted decision against the corresponding recorded decision at the same logical position when verification is enabled; report any mismatch with the logical time and decision detail

View file

@ -61,4 +61,21 @@ sched_core_isolation_lib = static_library(
dependencies: scheduler_deps, dependencies: scheduler_deps,
install: false) install: false)
# Task 11.3 (scheduler-core spec, Req 5 AC 17): isolated compile target for
# the simulation driver's own files, consumed by
# testsuite/test_sim_isolation.c to prove at build time that sim.c/sim_loop.c
# reference zero controller-facing symbols (client/netio/xceapi/delay/idgen).
# Both files are compiled together into ONE archive -- task 11.2 added
# sim_loop.c as a sibling file to task 11.1's sim.c, and covering only one of
# them would repeat core.c's own siblings' (core_events.c/core_session.c/
# core_clock.c) never-mechanically-covered gap for the sim driver. Same
# "compiling to an object is enough, no real linking needed" rationale as
# sched_core_isolation_lib above.
sched_sim_isolation_lib = static_library(
'sched_sim_isolation',
['src/sim.c', 'src/sim_loop.c'],
include_directories: incdir,
dependencies: scheduler_deps,
install: false)
subdir('testsuite') subdir('testsuite')

View file

@ -501,6 +501,40 @@ sched_sim_event_loop_test = executable(
dependencies: [scheduler_test_deps], dependencies: [scheduler_test_deps],
install: false) install: false)
# Regression guard for task 11.3's Observable (scheduler-core spec, Req 5 AC
# 17, "Simulation isolation"): "a build-time or structural check confirms no
# controller-facing symbol is reachable from the simulation driver's compiled
# object", plus the AC's own "confirmed by the struct definition itself, not
# runtime checks alone" clause. Same source-text-scan + nm(1) link-time-proof
# genre as sched_core_isolation_test above, PLUS a third, narrower structural
# layer scanning ONLY xce_sched_sim_driver_t's own struct body text for
# forbidden field types -- see the test's own header comment for why a
# comment-stripping pass is required first (sim.h's own boundary doc comment
# legitimately names xcecli_ctx_t/xce_cluster_config_t in prose). link_with
# establishes the build-order dependency on sched_sim_isolation_lib (parent
# meson.build -- sim.c AND sim_loop.c compiled together, isolated from the
# rest of the scheduler); XCE_SIM_*_PATH bake in absolute paths the same way
# XCE_CORE_*_PATH does above.
sched_sim_isolation_test = executable(
'sched_sim_isolation_test',
['test_sim_isolation.c'],
include_directories:[incdir],
dependencies: [scheduler_test_deps],
link_with: [sched_sim_isolation_lib],
c_args: ['-DXCE_SIM_H_PATH="'
+ (meson.current_source_dir() / '..' / 'include' / 'sim.h')
+ '"',
'-DXCE_SIM_C_PATH="'
+ (meson.current_source_dir() / '..' / 'src' / 'sim.c')
+ '"',
'-DXCE_SIM_LOOP_C_PATH="'
+ (meson.current_source_dir() / '..' / 'src' / 'sim_loop.c')
+ '"',
'-DXCE_SIM_LIB_PATH="'
+ sched_sim_isolation_lib.full_path()
+ '"'],
install: false)
# Register as meson tests so `meson test --suite scheduler` runs them all. # Register as meson tests so `meson test --suite scheduler` runs them all.
# detect_leaks=0 matches the suite convention (fixtures are not torn down). # detect_leaks=0 matches the suite convention (fixtures are not torn down).
test('scheduler test', scheduler_test, suite: 'scheduler', test('scheduler test', scheduler_test, suite: 'scheduler',
@ -576,3 +610,6 @@ test('scheduler sim startup test',
test('scheduler sim event loop test', test('scheduler sim event loop test',
sched_sim_event_loop_test, suite: 'scheduler', sched_sim_event_loop_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0']) env: ['ASAN_OPTIONS=detect_leaks=0'])
test('scheduler sim isolation test',
sched_sim_isolation_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -0,0 +1,548 @@
/*
* test_sim_isolation.c - regression guard for task 11.3's Observable
* (scheduler-core spec, Req 5 AC 17, "Simulation isolation"): "a build-time
* or structural check confirms no controller-facing symbol is reachable
* from the simulation driver's compiled object" -- plus the AC's own
* "confirmed by the struct definition itself, not runtime checks alone"
* clause. Scope: sim.h + sim.c + sim_loop.c together (task 11.1 + task
* 11.2's sibling file) -- NOT a re-audit of core.c (task 4.1's own
* territory, test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c (shared
* normal-mode infra, out of this task's boundary).
*
* Three layers, extending test_core_isolation.c's two-layer pattern (same
* genre: source-text scans of the real, current on-disk files, not a frozen
* snapshot):
*
* 1. Source-text scan of sim.h/sim.c/sim_loop.c: no #include of
* client.h/netio.h/xceapi.h/delay.h/idgen.h/xce/cluster.h; no call-site
* substring matching a symbol exported only by those libraries (the
* exact FORBIDDEN_SYMBOLS list test_core_isolation.c already
* established, reused verbatim here, not reinvented).
*
* 2. A structural check test_core_isolation.c has no equivalent of: Req 5
* AC 17 explicitly requires "the xce_sched_sim_driver_t shall contain
* no xcecli_ctx_t, no xce_cluster_config_t, no capnp socket" --
* enforced by the STRUCT DEFINITION ITSELF, not merely by absence of
* the type names anywhere in the file (sim.h's own boundary comment,
* just above the struct, legitimately NAMES xcecli_ctx_t and
* xce_cluster_config_t in prose while documenting this exact
* requirement -- see strip_c_comments()'s doc comment below for why
* that makes a naive whole-file scan wrong). This layer extracts ONLY
* the `typedef struct { ... } xce_sched_sim_driver_t;` body text and
* scans that narrower region for forbidden field types.
*
* 3. The literal build-time/link-time proof the Observable asks for, not
* achievable by a text scan alone: sim.c AND sim_loop.c together (not
* just one -- task 11.2 added sim_loop.c as a sibling file to task
* 11.1's sim.c, and repeating core.c's siblings' own "never
* mechanically covered" gap for the sim driver's own second file would
* be exactly the mistake this task exists to avoid) are compiled in
* isolation into their own static library (meson.build's
* sched_sim_isolation_lib, this test's XCE_SIM_LIB_PATH), and this test
* inspects that archive's UNDEFINED SYMBOL TABLE via nm(1) -- proving
* what the compiled code ACTUALLY calls, independent of what any
* header happens to transitively declare. Same "compile alone, inspect
* undefined symbols" technique and same rationale
* test_core_isolation.c's own header comment gives for why nm on a
* static_library() (not the full xce_scheduler executable, which links
* the whole monolithic libxce_dep and would prove nothing about this
* one translation unit).
*
* Deliberately NOT covered here: the wall-clock ban (FORBIDDEN_WALLCLOCK in
* test_core_isolation.c) -- that constraint is scoped to core.c/core.h
* specifically (Req 5 AC 3), not to driver-level code. sim.c's
* sim_set_virtual_clock() and sim_loop.c's sim_advance_virtual_clock() both
* legitimately call clock_gettime() to seed/advance the core's virtual-clock
* anchors from the driver side -- their own doc comments already explain why
* this does not violate core.c's isolation boundary. Applying
* FORBIDDEN_WALLCLOCK here would incorrectly fail on that legitimate,
* documented behavior.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#ifndef XCE_SIM_H_PATH
#error "XCE_SIM_H_PATH must be defined by meson.build to sim.h's absolute path"
#endif
#ifndef XCE_SIM_C_PATH
#error "XCE_SIM_C_PATH must be defined by meson.build to sim.c's absolute path"
#endif
#ifndef XCE_SIM_LOOP_C_PATH
#error \
"XCE_SIM_LOOP_C_PATH must be defined by meson.build to sim_loop.c's absolute path"
#endif
#ifndef XCE_SIM_LIB_PATH
#error \
"XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c's isolated static_library() archive"
#endif
/* Forbidden headers (design.md "Allowed Dependencies", same list
* test_core_isolation.c uses for core.h/core.c, reused verbatim): the
* simulation driver 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 -- the exact FORBIDDEN_SYMBOLS list
* test_core_isolation.c already established (verified against
* netio.c/xceapi.c/client.c/delay.c/idgen.c's own extern lists), reused
* verbatim here per this task's own instruction to reuse it, not
* reinvent it. */
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]))
/* Forbidden xce_sched_sim_driver_t field TYPES (Req 5 AC 17's own literal
* wording: "shall contain no xcecli_ctx_t, no xce_cluster_config_t, no
* capnp socket"). xcecli_ctx_t/xce_cluster_config_t are named directly by
* the requirement; "capnp socket" maps to netio.h's own connection-carrying
* types (endpoint_t/iomgr_t/io_fsm_t) -- confirmed by inspecting netio.h: no
* distinct "capnp socket" type exists in this codebase, these are the only
* live-socket types netio.h defines. Note xcecli_ctx_t/endpoint_t/iomgr_t
* would also be caught by FORBIDDEN_SYMBOLS' "xcecli_"/"endpoint_"/"iomgr_"
* prefixes above (layer 1) -- listed again here anyway so this check stands
* on its own as the AC-17-literal "struct definition itself" proof, not
* merely inheriting layer 1's coverage. xce_cluster_config_t is NOT caught
* by FORBIDDEN_SYMBOLS (that list has "xce_cluster_config_validate", a
* different string, not a substring of "xce_cluster_config_t" or vice
* versa) -- this is the one genuinely new check this layer adds. */
static const char* const STRUCT_FORBIDDEN_TYPES[] = {
"xcecli_ctx_t", "xce_cluster_config_t", "endpoint_t", "iomgr_t", "io_fsm_t",
};
#define NUM_STRUCT_FORBIDDEN_TYPES \
(sizeof(STRUCT_FORBIDDEN_TYPES) / sizeof(STRUCT_FORBIDDEN_TYPES[0]))
/* Markers bracketing xce_sched_sim_driver_t's own definition in sim.h (see
* extract_struct_body() below). sim.h defines a SECOND `typedef struct {`
* later in the file (sched_sim_args_t) -- extract_struct_body() searches for
* the LAST "typedef struct {" strictly before the end marker, which
* correctly resolves to THIS struct's own opening brace, not the later
* unrelated one. */
static const char* const STRUCT_START_MARKER = "typedef struct {";
static const char* const STRUCT_END_MARKER = "} xce_sched_sim_driver_t;";
#define STRUCT_BODY_BUF_SIZE 4096
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;
}
/*
* strip_c_comments - returns a heap copy of `src` with every block comment
* (slash-star ... star-slash) and line comment (slash-slash to end of line)
* replaced by a single space, leaving string and character literal CONTENTS
* untouched (so a literal containing a comment-marker-shaped sequence is
* never misparsed as a comment start). Necessary because
* sim.h's own boundary doc comment legitimately names xcecli_ctx_t and
* xce_cluster_config_t in prose while documenting Req 5 AC 17 (see this
* file's own header comment, layer 2) -- a raw-text scan (fine for
* test_core_isolation.c, whose core.h/core.c comments never name a
* forbidden symbol) would false-positive on that legitimate documentation.
* Comments never produce compiled symbols either, so stripping them first
* keeps every source-text scan below consistent with what the nm(1)
* link-time proof (layer 3) actually measures: real code, not prose.
* Returns NULL on allocation failure.
*/
static char* strip_c_comments(const char* src)
{
size_t len = strlen(src);
char* out = (char*)malloc(len + 1);
size_t oi = 0;
size_t i = 0;
if (out == NULL) {
return NULL;
}
while (i < len) {
if ((src[i] == '/') && ((i + 1) < len) && (src[i + 1] == '*')) {
i += 2;
while (((i + 1) < len)
&& !((src[i] == '*') && (src[i + 1] == '/'))) {
i++;
}
i += 2;
out[oi++] = ' ';
continue;
}
if ((src[i] == '/') && ((i + 1) < len) && (src[i + 1] == '/')) {
i += 2;
while ((i < len) && (src[i] != '\n')) {
i++;
}
out[oi++] = ' ';
continue;
}
if (src[i] == '"') {
out[oi++] = src[i++];
while ((i < len) && (src[i] != '"')) {
if ((src[i] == '\\') && ((i + 1) < len)) {
out[oi++] = src[i++];
}
out[oi++] = src[i++];
}
if (i < len) {
out[oi++] = src[i++];
}
continue;
}
if (src[i] == '\'') {
out[oi++] = src[i++];
while ((i < len) && (src[i] != '\'')) {
if ((src[i] == '\\') && ((i + 1) < len)) {
out[oi++] = src[i++];
}
out[oi++] = src[i++];
}
if (i < len) {
out[oi++] = src[i++];
}
continue;
}
out[oi++] = src[i++];
}
out[oi] = '\0';
return out;
}
/*
* extract_struct_body - isolates xce_sched_sim_driver_t's own
* `{ ... }` field-listing text from `stripped_h` (already comment-stripped
* sim.h text) into out_buf. Fails the currently-running test (via Unity's
* TEST_ASSERT_* longjmp) if either bracketing marker cannot be found, or if
* the extracted text overflows out_sz -- the same "fail loud on a stale/
* broken assumption" discipline test_core_isolation.c's own
* XCE_CORE_*_PATH #error guards and any_undefined_seen check establish.
*/
static void extract_struct_body(const char* stripped_h, char* out_buf,
size_t out_sz)
{
const char* end_pos = strstr(stripped_h, STRUCT_END_MARKER);
const char* start_pos = NULL;
const char* scan;
size_t body_len;
TEST_ASSERT_NOT_NULL_MESSAGE(
end_pos, "sim.h: could not locate '} xce_sched_sim_driver_t;' -- "
"has the struct been renamed or removed?");
for (scan = stripped_h; scan < end_pos;) {
const char* hit = strstr(scan, STRUCT_START_MARKER);
if ((hit == NULL) || (hit >= end_pos)) {
break;
}
start_pos = hit;
scan = hit + 1;
}
TEST_ASSERT_NOT_NULL_MESSAGE(
start_pos, "sim.h: could not locate the 'typedef struct {' opening "
"xce_sched_sim_driver_t's own definition");
body_len = (size_t)(end_pos - start_pos);
TEST_ASSERT_TRUE_MESSAGE(
body_len < out_sz,
"sim.h: xce_sched_sim_driver_t's struct body text overflowed the "
"fixed scan buffer -- grow STRUCT_BODY_BUF_SIZE");
memcpy(out_buf, start_pos, body_len);
out_buf[body_len] = '\0';
}
static void assert_no_forbidden_includes(const char* stripped,
const char* label)
{
size_t i;
for (i = 0; i < NUM_FORBIDDEN_INCLUDES; i++) {
char msg[256];
snprintf(msg, sizeof(msg), "%s directly includes forbidden header '%s'",
label, FORBIDDEN_INCLUDES[i]);
TEST_ASSERT_NULL_MESSAGE(strstr(stripped, FORBIDDEN_INCLUDES[i]), msg);
}
}
static void assert_no_forbidden_symbols(const char* stripped, const char* label)
{
size_t i;
for (i = 0; i < NUM_FORBIDDEN_SYMBOLS; i++) {
char msg[256];
snprintf(msg, sizeof(msg),
"%s references forbidden networking/API/controller symbol "
"'%s'",
label, FORBIDDEN_SYMBOLS[i]);
TEST_ASSERT_NULL_MESSAGE(strstr(stripped, FORBIDDEN_SYMBOLS[i]), msg);
}
}
void setUp(void) { return; }
void tearDown(void) { return; }
/* Sanity check: all three 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_sim_source_paths_are_readable(void)
{
char* h = read_whole_file(XCE_SIM_H_PATH);
char* c = read_whole_file(XCE_SIM_C_PATH);
char* loop = read_whole_file(XCE_SIM_LOOP_C_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_SIM_H_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_SIM_C_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(loop, XCE_SIM_LOOP_C_PATH);
free(h);
free(c);
free(loop);
}
static void test_sim_never_includes_forbidden_headers(void)
{
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
char* h;
char* c;
char* loop;
TEST_ASSERT_NOT_NULL(h_raw);
TEST_ASSERT_NOT_NULL(c_raw);
TEST_ASSERT_NOT_NULL(loop_raw);
h = strip_c_comments(h_raw);
c = strip_c_comments(c_raw);
loop = strip_c_comments(loop_raw);
TEST_ASSERT_NOT_NULL(h);
TEST_ASSERT_NOT_NULL(c);
TEST_ASSERT_NOT_NULL(loop);
assert_no_forbidden_includes(h, "sim.h");
assert_no_forbidden_includes(c, "sim.c");
assert_no_forbidden_includes(loop, "sim_loop.c");
free(h_raw);
free(c_raw);
free(loop_raw);
free(h);
free(c);
free(loop);
}
static void test_sim_never_calls_forbidden_symbols(void)
{
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
char* h;
char* c;
char* loop;
TEST_ASSERT_NOT_NULL(h_raw);
TEST_ASSERT_NOT_NULL(c_raw);
TEST_ASSERT_NOT_NULL(loop_raw);
h = strip_c_comments(h_raw);
c = strip_c_comments(c_raw);
loop = strip_c_comments(loop_raw);
TEST_ASSERT_NOT_NULL(h);
TEST_ASSERT_NOT_NULL(c);
TEST_ASSERT_NOT_NULL(loop);
assert_no_forbidden_symbols(h, "sim.h");
assert_no_forbidden_symbols(c, "sim.c");
assert_no_forbidden_symbols(loop, "sim_loop.c");
free(h_raw);
free(c_raw);
free(loop_raw);
free(h);
free(c);
free(loop);
}
/*
* test_sim_driver_struct_has_no_forbidden_fields - Req 5 AC 17's
* "confirmed by the struct definition itself, not runtime checks alone"
* clause. Scans ONLY xce_sched_sim_driver_t's own `{ ... }` field-listing
* text (extract_struct_body(), on comment-stripped sim.h) for forbidden
* field types -- narrower and more literal than the whole-file scans above,
* and immune to sim.h's own boundary comment naming these same types in
* prose (comments are stripped before extraction).
*/
static void test_sim_driver_struct_has_no_forbidden_fields(void)
{
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
char* h;
char struct_body[STRUCT_BODY_BUF_SIZE];
size_t i;
TEST_ASSERT_NOT_NULL(h_raw);
h = strip_c_comments(h_raw);
TEST_ASSERT_NOT_NULL(h);
extract_struct_body(h, struct_body, sizeof(struct_body));
for (i = 0; i < NUM_STRUCT_FORBIDDEN_TYPES; i++) {
char msg[768];
snprintf(msg, sizeof(msg),
"xce_sched_sim_driver_t (sim.h) has a field referencing "
"forbidden controller-facing type '%s' -- Req 5 AC 17 "
"requires zero xcecli_ctx_t/xce_cluster_config_t/capnp-"
"socket fields, enforced by the struct definition itself",
STRUCT_FORBIDDEN_TYPES[i]);
TEST_ASSERT_NULL_MESSAGE(strstr(struct_body, STRUCT_FORBIDDEN_TYPES[i]),
msg);
}
free(h_raw);
free(h);
}
/*
* test_sim_object_has_no_forbidden_undefined_symbols - the literal
* build-time/link-time proof: sim.c + sim_loop.c, compiled together in
* isolation into sched_sim_isolation_lib (meson.build), must not reference
* (as an undefined external symbol in the compiled archive) anything
* matching the forbidden prefixes above. Independent of what any header
* transitively declares -- reflects only what this driver's compiled code
* actually calls. Same technique and same any_undefined_seen
* vacuous-pass guard as test_core_isolation.c's
* test_core_object_has_no_forbidden_undefined_symbols().
*/
static void test_sim_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_SIM_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 (this archive has two member objects,
* sim.c.o and sim_loop.c.o). */
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),
"sim.c/sim_loop.c's compiled object has an "
"undefined reference to '%s', matching forbidden "
"controller-facing prefix '%s' -- the simulation "
"driver must never link networking/API/client "
"library symbols (Req 5 AC 17)",
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_SIM_LIB_PATH -- the "
"archive path is likely wrong (this test would otherwise pass "
"vacuously)");
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_sim_source_paths_are_readable);
RUN_TEST(test_sim_never_includes_forbidden_headers);
RUN_TEST(test_sim_never_calls_forbidden_symbols);
RUN_TEST(test_sim_driver_struct_has_no_forbidden_fields);
RUN_TEST(test_sim_object_has_no_forbidden_undefined_symbols);
return UNITY_END();
}