/* * 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_picker.c + sim_record.c + * repl.c/repl.h/repl_break.c/repl_info.c/repl_internal.h/repl_params.c * together (task 11.1 + task 11.2's sibling file + task 11.5's sibling * file, the --sim-output writer + task 12.1's REPL files + task 12.2's own * repl_break.c/repl_info.c/repl_internal.h + task 12.3's own repl_params.c, * each an 800-line-cap split of repl.c -- see repl_internal.h's own "Why a * split at all" section -- + bug-1567's own sim_picker.c, the identical * split applied to sim_loop.c once IT hit the same cap) -- 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 were added per task 12.1's own text (extending * this exact 3-file precedent to a 5-file one); repl_break.c/repl_info.c/ * repl_internal.h extended it again (task 12.2, to an 8-file one); * repl_params.c extends it once more (task 12.3, to a 9-file one); * sim_picker.c extends it a final time (bug-1567, to a 10-file one) -- all * touch xce_sched_sim_driver_t directly or read/write stdin/stdout, 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/repl_break.c/repl_info.c/repl_internal.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 #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_PICKER_C_PATH #error \ "XCE_SIM_PICKER_C_PATH must be defined by meson.build to sim_picker.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_REPL_BREAK_C_PATH #error \ "XCE_SIM_REPL_BREAK_C_PATH must be defined by meson.build to repl_break.c's absolute path" #endif #ifndef XCE_SIM_REPL_INFO_C_PATH #error \ "XCE_SIM_REPL_INFO_C_PATH must be defined by meson.build to repl_info.c's absolute path" #endif #ifndef XCE_SIM_REPL_INTERNAL_H_PATH #error \ "XCE_SIM_REPL_INTERNAL_H_PATH must be defined by meson.build to repl_internal.h's absolute path" #endif #ifndef XCE_SIM_REPL_PARAMS_C_PATH #error \ "XCE_SIM_REPL_PARAMS_C_PATH must be defined by meson.build to repl_params.c's absolute path" #endif /* Task 5.1 (scheduler-sim-decision-trace, Req 11.3): the REPL trace * sources and the renderer's private header. */ #if !defined(XCE_SIM_REPL_TRACE_C_PATH) \ || !defined(XCE_SIM_REPL_TRACE_STORE_C_PATH) \ || !defined(XCE_SIM_REPL_TRACE_RENDER_C_PATH) \ || !defined(XCE_SIM_REPL_TRACE_RENDER_OUTCOME_C_PATH) \ || !defined(XCE_SIM_REPL_TRACE_RENDER_H_PATH) \ || !defined(XCE_SIM_REPL_TRACE_EMIT_C_PATH) #error \ "XCE_SIM_REPL_TRACE_*_PATH must be defined by meson.build to the absolute paths of the repl_trace*.c sources and repl_trace_render.h" #endif #ifndef XCE_SIM_LIB_PATH #error \ "XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of the sched_sim_isolation_lib static_library() archive (its sources are listed in src/scheduler/meson.build)" #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); } } /* * Requirement 8.7 (scheduler-sim-decision-trace): the REPL trace sources * must never write trace content to the scheduler's regular log. They may * use stdio (the REPL prints), so this is narrower than * test_core_isolation.c's trace no-I/O scan: no logging header and no * logging call. utils.h (which the REPL needs) pulls in tlog.h and defines * the TRACE/DBG/INFO/WARN/ERROR macros, so the calls are what is checked. * Names match as a call on identifier boundaries, not as a member call, * with string literals blanked first. */ static const char* const REPL_TRACE_LOG_INCLUDES[] = { "", "\"tlog.h\"", "\"xcelog.h\"", "", "", }; static const char* const REPL_TRACE_LOG_CALLS[] = { "TRACE", "DEBUG", "DBG", "INFO", "WARN", "ERROR", "FATAL", "SCHEDULER_LOG", "CONTROLLER_LOG", "XCECD_LOG", "XCECD_WORKER_LOG", "syslog", "vsyslog", "openlog", }; /* identifier prefixes: any call starting with one is a log call */ static const char* const REPL_TRACE_LOG_PREFIXES[] = { "tlog", "xce_log", "xce_init_xcecd", }; static int is_ident_char(char c) { return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= '0') && (c <= '9')) || (c == '_'); } /* Blanks the contents of string and char literals in place, except on * #include lines. */ static void blank_string_literals(char* t) { size_t i = 0; int in_include = 0; while (t[i] != '\0') { if ((t[i] == '#') && (strncmp(t + i, "#include", 8) == 0)) { in_include = 1; } else if (t[i] == '\n') { in_include = 0; } if (((t[i] == '"') || (t[i] == '\'')) && !in_include) { char q = t[i++]; while ((t[i] != '\0') && (t[i] != q) && (t[i] != '\n')) { if ((t[i] == '\\') && (t[i + 1] != '\0')) { t[i++] = ' '; } t[i++] = ' '; } if (t[i] == q) { i++; } continue; } i++; } } /* Offset of the first call to `name` (or, when `prefix`, to any * identifier starting with `name`) in `t`, or -1. A member call * (`x.name(` / `x->name(`) and a longer identifier do not count. */ static long find_call(const char* t, const char* name, int prefix) { size_t n = strlen(name); const char* p = t; for (; (p = strstr(p, name)) != NULL; p += n) { const char* e = p + n; if ((p > t) && (is_ident_char(p[-1]) || (p[-1] == '.') || ((p[-1] == '>') && (p - t >= 2) && (p[-2] == '-')))) { continue; } if (prefix) { while (is_ident_char(*e)) { e++; } } else if (is_ident_char(*e)) { continue; } while ((*e == ' ') || (*e == '\t')) { e++; } if (*e == '(') { return (long)(p - t); } } return -1; } static void assert_repl_trace_source_never_logs(const char* stripped, const char* label) { char* t = strdup(stripped); char msg[512]; size_t i; TEST_ASSERT_NOT_NULL(t); for (i = 0; i < sizeof(REPL_TRACE_LOG_INCLUDES) / sizeof(char*); i++) { snprintf(msg, sizeof(msg), "%s includes logging header %s (Req 8.7)", label, REPL_TRACE_LOG_INCLUDES[i]); TEST_ASSERT_NULL_MESSAGE(strstr(t, REPL_TRACE_LOG_INCLUDES[i]), msg); } blank_string_literals(t); for (i = 0; i < sizeof(REPL_TRACE_LOG_CALLS) / sizeof(char*); i++) { snprintf(msg, sizeof(msg), "%s calls logging macro/function '%s' -- trace content " "must never reach the scheduler log (Req 8.7)", label, REPL_TRACE_LOG_CALLS[i]); TEST_ASSERT_EQUAL_INT_MESSAGE( -1, find_call(t, REPL_TRACE_LOG_CALLS[i], 0), msg); } for (i = 0; i < sizeof(REPL_TRACE_LOG_PREFIXES) / sizeof(char*); i++) { snprintf(msg, sizeof(msg), "%s calls a '%s*' logging function -- trace content must " "never reach the scheduler log (Req 8.7)", label, REPL_TRACE_LOG_PREFIXES[i]); TEST_ASSERT_EQUAL_INT_MESSAGE( -1, find_call(t, REPL_TRACE_LOG_PREFIXES[i], 1), msg); } free(t); } /* Offset (>= from) of the next plain assignment `name = ...` (not `==`, * not a longer identifier) in comment-stripped `t`, or -1. Member * assignments (`x->name =`, `x.name =`) count. */ static long find_assignment(const char* t, const char* name, long from) { size_t n = strlen(name); const char* p = t + from; for (; (p = strstr(p, name)) != NULL; p += n) { const char* e = p + n; if (((p > t) && is_ident_char(p[-1])) || is_ident_char(*e)) { continue; } while ((*e == ' ') || (*e == '\t') || (*e == '\n')) { e++; } if ((e[0] == '=') && (e[1] != '=')) { return (long)(p - t); } } return -1; } /* True when a string literal in comment-stripped `t` contains `word` * (case-insensitive). */ static int literal_mentions(const char* t, const char* word) { size_t n = strlen(word); size_t i = 0; while (t[i] != '\0') { if (t[i] == '\'') { /* skip a char literal */ i++; while ((t[i] != '\0') && (t[i] != '\'') && (t[i] != '\n')) { i += ((t[i] == '\\') && (t[i + 1] != '\0')) ? 2 : 1; } i += (t[i] == '\''); continue; } if (t[i] == '"') { size_t j = i + 1; while ((t[j] != '\0') && (t[j] != '"') && (t[j] != '\n')) { if (strncasecmp(t + j, word, n) == 0) { return 1; } j += ((t[j] == '\\') && (t[j + 1] != '\0')) ? 2 : 1; } i = j + (t[j] == '"'); continue; } i++; } return 0; } static int dirname_of(const char* path, char* out, size_t out_sz) { const char* slash = strrchr(path, '/'); size_t n; if (slash == NULL) { return 0; } n = (size_t)(slash - path); if (n + 1 > out_sz) { return 0; } memcpy(out, path, n); out[n] = '\0'; return 1; } 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* picker = read_whole_file(XCE_SIM_PICKER_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); char* repl_break_c = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); char* repl_info_c = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); char* repl_int_h = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); char* repl_params_c = read_whole_file(XCE_SIM_REPL_PARAMS_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); TEST_ASSERT_NOT_NULL_MESSAGE(picker, XCE_SIM_PICKER_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); TEST_ASSERT_NOT_NULL_MESSAGE(repl_break_c, XCE_SIM_REPL_BREAK_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(repl_info_c, XCE_SIM_REPL_INFO_C_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(repl_int_h, XCE_SIM_REPL_INTERNAL_H_PATH); TEST_ASSERT_NOT_NULL_MESSAGE(repl_params_c, XCE_SIM_REPL_PARAMS_C_PATH); free(h); free(c); free(loop); free(picker); free(record); free(repl_c); free(repl_h); free(repl_break_c); free(repl_info_c); free(repl_int_h); free(repl_params_c); } 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* picker_raw = read_whole_file(XCE_SIM_PICKER_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* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); char* repl_params_c_raw = read_whole_file(XCE_SIM_REPL_PARAMS_C_PATH); char* repl_rollup_c_raw = read_whole_file(XCE_SIM_REPL_ROLLUP_C_PATH); char* repl_fmt_c_raw = read_whole_file(XCE_SIM_REPL_FMT_C_PATH); char* h; char* c; char* loop; char* picker; char* record; char* repl_c; char* repl_h; char* repl_break_c; char* repl_info_c; char* repl_int_h; char* repl_params_c; char* repl_rollup_c; char* repl_fmt_c; TEST_ASSERT_NOT_NULL(h_raw); TEST_ASSERT_NOT_NULL(c_raw); TEST_ASSERT_NOT_NULL(loop_raw); TEST_ASSERT_NOT_NULL(picker_raw); TEST_ASSERT_NOT_NULL(record_raw); TEST_ASSERT_NOT_NULL(repl_c_raw); TEST_ASSERT_NOT_NULL(repl_h_raw); TEST_ASSERT_NOT_NULL(repl_break_c_raw); TEST_ASSERT_NOT_NULL(repl_info_c_raw); TEST_ASSERT_NOT_NULL(repl_int_h_raw); TEST_ASSERT_NOT_NULL(repl_params_c_raw); TEST_ASSERT_NOT_NULL(repl_rollup_c_raw); TEST_ASSERT_NOT_NULL(repl_fmt_c_raw); h = strip_c_comments(h_raw); c = strip_c_comments(c_raw); loop = strip_c_comments(loop_raw); picker = strip_c_comments(picker_raw); record = strip_c_comments(record_raw); repl_c = strip_c_comments(repl_c_raw); repl_h = strip_c_comments(repl_h_raw); repl_break_c = strip_c_comments(repl_break_c_raw); repl_info_c = strip_c_comments(repl_info_c_raw); repl_int_h = strip_c_comments(repl_int_h_raw); repl_params_c = strip_c_comments(repl_params_c_raw); repl_rollup_c = strip_c_comments(repl_rollup_c_raw); repl_fmt_c = strip_c_comments(repl_fmt_c_raw); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(loop); TEST_ASSERT_NOT_NULL(picker); TEST_ASSERT_NOT_NULL(record); TEST_ASSERT_NOT_NULL(repl_c); TEST_ASSERT_NOT_NULL(repl_h); TEST_ASSERT_NOT_NULL(repl_break_c); TEST_ASSERT_NOT_NULL(repl_info_c); TEST_ASSERT_NOT_NULL(repl_int_h); TEST_ASSERT_NOT_NULL(repl_params_c); TEST_ASSERT_NOT_NULL(repl_rollup_c); TEST_ASSERT_NOT_NULL(repl_fmt_c); 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(picker, "sim_picker.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"); assert_no_forbidden_includes(repl_break_c, "repl_break.c"); assert_no_forbidden_includes(repl_info_c, "repl_info.c"); assert_no_forbidden_includes(repl_int_h, "repl_internal.h"); assert_no_forbidden_includes(repl_params_c, "repl_params.c"); assert_no_forbidden_includes(repl_rollup_c, "repl_rollup.c"); assert_no_forbidden_includes(repl_fmt_c, "repl_fmt.c"); free(h_raw); free(c_raw); free(loop_raw); free(picker_raw); free(record_raw); free(repl_c_raw); free(repl_h_raw); free(repl_break_c_raw); free(repl_info_c_raw); free(repl_int_h_raw); free(repl_params_c_raw); free(repl_rollup_c_raw); free(repl_fmt_c_raw); free(h); free(c); free(loop); free(picker); free(record); free(repl_c); free(repl_h); free(repl_break_c); free(repl_info_c); free(repl_int_h); free(repl_params_c); free(repl_rollup_c); free(repl_fmt_c); } 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* picker_raw = read_whole_file(XCE_SIM_PICKER_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* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH); char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH); char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH); char* repl_params_c_raw = read_whole_file(XCE_SIM_REPL_PARAMS_C_PATH); char* repl_rollup_c_raw = read_whole_file(XCE_SIM_REPL_ROLLUP_C_PATH); char* repl_fmt_c_raw = read_whole_file(XCE_SIM_REPL_FMT_C_PATH); char* h; char* c; char* loop; char* picker; char* record; char* repl_c; char* repl_h; char* repl_break_c; char* repl_info_c; char* repl_int_h; char* repl_params_c; char* repl_rollup_c; char* repl_fmt_c; TEST_ASSERT_NOT_NULL(h_raw); TEST_ASSERT_NOT_NULL(c_raw); TEST_ASSERT_NOT_NULL(loop_raw); TEST_ASSERT_NOT_NULL(picker_raw); TEST_ASSERT_NOT_NULL(record_raw); TEST_ASSERT_NOT_NULL(repl_c_raw); TEST_ASSERT_NOT_NULL(repl_h_raw); TEST_ASSERT_NOT_NULL(repl_break_c_raw); TEST_ASSERT_NOT_NULL(repl_info_c_raw); TEST_ASSERT_NOT_NULL(repl_int_h_raw); TEST_ASSERT_NOT_NULL(repl_params_c_raw); TEST_ASSERT_NOT_NULL(repl_rollup_c_raw); TEST_ASSERT_NOT_NULL(repl_fmt_c_raw); h = strip_c_comments(h_raw); c = strip_c_comments(c_raw); loop = strip_c_comments(loop_raw); picker = strip_c_comments(picker_raw); record = strip_c_comments(record_raw); repl_c = strip_c_comments(repl_c_raw); repl_h = strip_c_comments(repl_h_raw); repl_break_c = strip_c_comments(repl_break_c_raw); repl_info_c = strip_c_comments(repl_info_c_raw); repl_int_h = strip_c_comments(repl_int_h_raw); repl_params_c = strip_c_comments(repl_params_c_raw); repl_rollup_c = strip_c_comments(repl_rollup_c_raw); repl_fmt_c = strip_c_comments(repl_fmt_c_raw); TEST_ASSERT_NOT_NULL(h); TEST_ASSERT_NOT_NULL(c); TEST_ASSERT_NOT_NULL(loop); TEST_ASSERT_NOT_NULL(picker); TEST_ASSERT_NOT_NULL(record); TEST_ASSERT_NOT_NULL(repl_c); TEST_ASSERT_NOT_NULL(repl_h); TEST_ASSERT_NOT_NULL(repl_break_c); TEST_ASSERT_NOT_NULL(repl_info_c); TEST_ASSERT_NOT_NULL(repl_int_h); TEST_ASSERT_NOT_NULL(repl_params_c); TEST_ASSERT_NOT_NULL(repl_rollup_c); TEST_ASSERT_NOT_NULL(repl_fmt_c); 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(picker, "sim_picker.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"); assert_no_forbidden_symbols(repl_break_c, "repl_break.c"); assert_no_forbidden_symbols(repl_info_c, "repl_info.c"); assert_no_forbidden_symbols(repl_int_h, "repl_internal.h"); assert_no_forbidden_symbols(repl_params_c, "repl_params.c"); assert_no_forbidden_symbols(repl_rollup_c, "repl_rollup.c"); assert_no_forbidden_symbols(repl_fmt_c, "repl_fmt.c"); free(h_raw); free(c_raw); free(loop_raw); free(picker_raw); free(record_raw); free(repl_c_raw); free(repl_h_raw); free(repl_break_c_raw); free(repl_info_c_raw); free(repl_int_h_raw); free(repl_params_c_raw); free(repl_rollup_c_raw); free(repl_fmt_c_raw); free(h); free(c); free(loop); free(picker); free(record); free(repl_c); free(repl_h); free(repl_break_c); free(repl_info_c); free(repl_int_h); free(repl_params_c); free(repl_rollup_c); free(repl_fmt_c); } /* * test_sim_trace_repl_sources_are_isolated - the REPL trace sources * (control, store, renderer, emitter) are part of the simulation driver: * each one must be readable, include no forbidden header and name no * controller-facing symbol. They may use stdio (the REPL prints), unlike * the core trace sources test_core_isolation.c scans. */ static void test_sim_trace_repl_sources_are_isolated(void) { static const char* const paths[] = { XCE_SIM_REPL_TRACE_C_PATH, XCE_SIM_REPL_TRACE_STORE_C_PATH, XCE_SIM_REPL_TRACE_RENDER_C_PATH, XCE_SIM_REPL_TRACE_RENDER_OUTCOME_C_PATH, XCE_SIM_REPL_TRACE_RENDER_H_PATH, XCE_SIM_REPL_TRACE_EMIT_C_PATH, }; size_t i; for (i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) { char* raw = read_whole_file(paths[i]); char* stripped; TEST_ASSERT_NOT_NULL_MESSAGE(raw, paths[i]); stripped = strip_c_comments(raw); TEST_ASSERT_NOT_NULL_MESSAGE(stripped, paths[i]); TEST_ASSERT_TRUE_MESSAGE(stripped[0] != '\0', paths[i]); assert_no_forbidden_includes(stripped, paths[i]); assert_no_forbidden_symbols(stripped, paths[i]); assert_repl_trace_source_never_logs(stripped, paths[i]); free(raw); free(stripped); } } /* The log scan itself must be able to fail: each class of planted log * call or include is a hit, and the same names inside literals or longer * identifiers are not. */ static void test_repl_trace_log_scan_detects_and_ignores(void) { char ok_text[] = "static const char* s = \"WARN(x) tlog_info(\";\n" "void f(void) { MY_INFO(1); p->ERROR(2); " "sched_trace_rec_free(0); fprintf(stdout, \"x\"); }\n"; char hit_mac[] = "void f(void) { WARN (\"x\"); }\n"; char hit_tlog[] = "void f(void) { tlog_debug3(\"x\"); }\n"; char hit_sl[] = "void f(void) { SCHEDULER_LOG(INFO, \"x\"); }\n"; blank_string_literals(ok_text); blank_string_literals(hit_mac); blank_string_literals(hit_tlog); blank_string_literals(hit_sl); TEST_ASSERT_EQUAL_INT(-1, find_call(ok_text, "WARN", 0)); TEST_ASSERT_EQUAL_INT(-1, find_call(ok_text, "tlog", 1)); TEST_ASSERT_EQUAL_INT(-1, find_call(ok_text, "INFO", 0)); TEST_ASSERT_EQUAL_INT(-1, find_call(ok_text, "ERROR", 0)); TEST_ASSERT_NOT_EQUAL(-1, find_call(hit_mac, "WARN", 0)); TEST_ASSERT_NOT_EQUAL(-1, find_call(hit_tlog, "tlog", 1)); TEST_ASSERT_NOT_EQUAL(-1, find_call(hit_sl, "SCHEDULER_LOG", 0)); } /* * test_trace_enable_is_repl_only - Requirement 11.1: no command-line * option, configuration setting or administrative request enables * tracing. Structural proof over every scheduler source and header: * - the only assignments to `trace_ctl` (the core's published trace * settings, the one switch the trace hooks test) are in the REPL trace * sources repl_trace.c / repl_trace_emit.c, plus core.c's reset, which * must store NULL; * - no non-REPL source carries a string literal naming "trace" (an * option, config key or request name would be one), other than an * #include of the trace headers. The REPL files (repl*.c/h) are the * interactive command surface and are exempt from this half. */ static void scan_trace_enable_dir(const char* dir, int* nfiles) { DIR* d = opendir(dir); struct dirent* e; TEST_ASSERT_NOT_NULL_MESSAGE(d, dir); while ((e = readdir(d)) != NULL) { size_t n = strlen(e->d_name); char path[1024]; char msg[1200]; char* raw; char* t; long at; int is_repl, writer_ok, i; if ((n < 3) || ((strcmp(e->d_name + n - 2, ".c") != 0) && (strcmp(e->d_name + n - 2, ".h") != 0))) { continue; } snprintf(path, sizeof(path), "%s/%s", dir, e->d_name); raw = read_whole_file(path); TEST_ASSERT_NOT_NULL_MESSAGE(raw, path); t = strip_c_comments(raw); TEST_ASSERT_NOT_NULL_MESSAGE(t, path); (*nfiles)++; is_repl = (strncmp(e->d_name, "repl", 4) == 0); writer_ok = (strcmp(e->d_name, "repl_trace.c") == 0) || (strcmp(e->d_name, "repl_trace_emit.c") == 0); for (at = find_assignment(t, "trace_ctl", 0); at >= 0; at = find_assignment(t, "trace_ctl", at + 1)) { const char* rhs = strchr(t + at, '=') + 1; while ((*rhs == ' ') || (*rhs == '\t')) { rhs++; } snprintf(msg, sizeof(msg), "%s assigns trace_ctl -- only the REPL trace sources may " "enable tracing (Req 11.1)", path); TEST_ASSERT_TRUE_MESSAGE( writer_ok || ((strcmp(e->d_name, "core.c") == 0) && (strncmp(rhs, "NULL", 4) == 0)), msg); } if (!is_repl) { /* drop #include lines, then look for "trace" in a literal */ for (i = 0; t[i] != '\0'; i++) { if ((t[i] == '#') && (strncmp(t + i, "#include", 8) == 0)) { while ((t[i] != '\0') && (t[i] != '\n')) { t[i++] = ' '; } if (t[i] == '\0') { break; } } } snprintf(msg, sizeof(msg), "%s has a string literal naming \"trace\" -- tracing " "must not be reachable from an option, config key or " "request (Req 11.1)", path); TEST_ASSERT_FALSE_MESSAGE(literal_mentions(t, "trace"), msg); } free(raw); free(t); } closedir(d); } static void test_trace_enable_is_repl_only(void) { char src_dir[1024]; char inc_dir[1024]; char sub[1100]; int nfiles = 0; TEST_ASSERT_TRUE(dirname_of(XCE_SIM_C_PATH, src_dir, sizeof(src_dir))); TEST_ASSERT_TRUE(dirname_of(XCE_SIM_H_PATH, inc_dir, sizeof(inc_dir))); scan_trace_enable_dir(src_dir, &nfiles); snprintf(sub, sizeof(sub), "%s/plugins", src_dir); scan_trace_enable_dir(sub, &nfiles); scan_trace_enable_dir(inc_dir, &nfiles); snprintf(sub, sizeof(sub), "%s/sched", inc_dir); scan_trace_enable_dir(sub, &nfiles); /* a guard against scanning an empty or wrong directory */ TEST_ASSERT_GREATER_THAN_INT(50, nfiles); } /* * 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_picker.c + * sim_record.c + repl.c + repl_break.c + repl_info.c + repl_params.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 one member object per * source in sched_sim_isolation_lib, src/scheduler/meson.build). */ 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_trace_repl_sources_are_isolated); RUN_TEST(test_repl_trace_log_scan_detects_and_ignores); RUN_TEST(test_trace_enable_is_repl_only); RUN_TEST(test_sim_driver_struct_has_no_forbidden_fields); RUN_TEST(test_sim_object_has_no_forbidden_undefined_symbols); return UNITY_END(); }