diff --git a/.kiro/specs/scheduler-core/tasks.md b/.kiro/specs/scheduler-core/tasks.md index dcead18e..41f294c2 100644 --- a/.kiro/specs/scheduler-core/tasks.md +++ b/.kiro/specs/scheduler-core/tasks.md @@ -323,11 +323,12 @@ - _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._ -- [ ] 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 - Observable: a build-time or structural check confirms no controller-facing symbol is reachable from the simulation driver's compiled object - _Requirements: 5.17_ - _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 - 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 diff --git a/src/scheduler/meson.build b/src/scheduler/meson.build index b61cdb40..cd946a57 100644 --- a/src/scheduler/meson.build +++ b/src/scheduler/meson.build @@ -61,4 +61,21 @@ sched_core_isolation_lib = static_library( dependencies: scheduler_deps, 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') diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 1ef213f4..c36545cd 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -501,6 +501,40 @@ sched_sim_event_loop_test = executable( dependencies: [scheduler_test_deps], 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. # detect_leaks=0 matches the suite convention (fixtures are not torn down). test('scheduler test', scheduler_test, suite: 'scheduler', @@ -576,3 +610,6 @@ test('scheduler sim startup test', test('scheduler sim event loop test', sched_sim_event_loop_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) +test('scheduler sim isolation test', + sched_sim_isolation_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_sim_isolation.c b/src/scheduler/testsuite/test_sim_isolation.c new file mode 100644 index 00000000..30e88176 --- /dev/null +++ b/src/scheduler/testsuite/test_sim_isolation.c @@ -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 +#include +#include + +#include + +#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 ", "#include ", + "#include ", "#include ", + "#include ", "#include ", +}; +#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: " 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(); +}