/* * 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 + sim_record.c + repl.c/repl.h * together (task 11.1 + task 11.2's sibling file + task 11.5's sibling * file, the --sim-output writer + task 12.1's sibling files, the * interactive REPL) -- 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). sim_verify.c (task 11.4) * is likewise out of scope here -- it is never compiled into * sched_sim_isolation_lib below and carries no isolation-sensitive logic of * its own (pure struct comparison, no I/O); extending this guard to it was * not part of task 11.4's own text and is left as-is. repl.c/repl.h are * added here per task 12.1's own text (extending this exact 3-file * precedent to a 5-file one) -- the REPL reads/writes stdin/stdout and * touches xce_sched_sim_driver_t directly, the same category of * driver-level code this guard already covers. * * 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/sim_record.c/repl.c/ * repl.h: 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_RECORD_C_PATH #error \ "XCE_SIM_RECORD_C_PATH must be defined by meson.build to sim_record.c's absolute path" #endif #ifndef XCE_SIM_REPL_C_PATH #error \ "XCE_SIM_REPL_C_PATH must be defined by meson.build to repl.c's absolute path" #endif #ifndef XCE_SIM_REPL_H_PATH #error \ "XCE_SIM_REPL_H_PATH must be defined by meson.build to repl.h'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+sim_record.c+repl.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); char* record = read_whole_file(XCE_SIM_RECORD_C_PATH); char* repl_c = read_whole_file(XCE_SIM_REPL_C_PATH); char* repl_h = read_whole_file(XCE_SIM_REPL_H_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); TEST_ASSERT_NOT_NULL_MESSAGE(record, XCE_SIM_RECORD_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(repl_c, XCE_SIM_REPL_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(repl_h, XCE_SIM_REPL_H_PATH); free(h); free(c); free(loop); free(record); free(repl_c); free(repl_h); } 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* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); char* h; char* c; char* loop; char* record; char* repl_c; char* repl_h; TEST_ASSERT_NOT_NULL(h_raw); TEST_ASSERT_NOT_NULL(c_raw); TEST_ASSERT_NOT_NULL(loop_raw); TEST_ASSERT_NOT_NULL(record_raw); TEST_ASSERT_NOT_NULL(repl_c_raw); TEST_ASSERT_NOT_NULL(repl_h_raw); h = strip_c_comments(h_raw); c = strip_c_comments(c_raw); loop = strip_c_comments(loop_raw); record = strip_c_comments(record_raw); repl_c = strip_c_comments(repl_c_raw); repl_h = strip_c_comments(repl_h_raw); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(loop); TEST_ASSERT_NOT_NULL(record); TEST_ASSERT_NOT_NULL(repl_c); TEST_ASSERT_NOT_NULL(repl_h); assert_no_forbidden_includes(h, "sim.h"); assert_no_forbidden_includes(c, "sim.c"); assert_no_forbidden_includes(loop, "sim_loop.c"); assert_no_forbidden_includes(record, "sim_record.c"); assert_no_forbidden_includes(repl_c, "repl.c"); assert_no_forbidden_includes(repl_h, "repl.h"); free(h_raw); free(c_raw); free(loop_raw); free(record_raw); free(repl_c_raw); free(repl_h_raw); free(h); free(c); free(loop); free(record); free(repl_c); free(repl_h); } 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* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH); char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH); char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH); char* h; char* c; char* loop; char* record; char* repl_c; char* repl_h; TEST_ASSERT_NOT_NULL(h_raw); TEST_ASSERT_NOT_NULL(c_raw); TEST_ASSERT_NOT_NULL(loop_raw); TEST_ASSERT_NOT_NULL(record_raw); TEST_ASSERT_NOT_NULL(repl_c_raw); TEST_ASSERT_NOT_NULL(repl_h_raw); h = strip_c_comments(h_raw); c = strip_c_comments(c_raw); loop = strip_c_comments(loop_raw); record = strip_c_comments(record_raw); repl_c = strip_c_comments(repl_c_raw); repl_h = strip_c_comments(repl_h_raw); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(loop); TEST_ASSERT_NOT_NULL(record); TEST_ASSERT_NOT_NULL(repl_c); TEST_ASSERT_NOT_NULL(repl_h); assert_no_forbidden_symbols(h, "sim.h"); assert_no_forbidden_symbols(c, "sim.c"); assert_no_forbidden_symbols(loop, "sim_loop.c"); assert_no_forbidden_symbols(record, "sim_record.c"); assert_no_forbidden_symbols(repl_c, "repl.c"); assert_no_forbidden_symbols(repl_h, "repl.h"); free(h_raw); free(c_raw); free(loop_raw); free(record_raw); free(repl_c_raw); free(repl_h_raw); free(h); free(c); free(loop); free(record); free(repl_c); free(repl_h); } /* * 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 + sim_record.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 four member objects, * sim.c.o, sim_loop.c.o, sim_record.c.o, and repl.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(); }