myxce/src/scheduler/testsuite/test_sim_isolation.c
lei.chen a905baf013 test(scheduler): aggregate 28 white-box tests into one sched_unit_test
Collapse the 28 zero-#include white-box scheduler tests (whose meson
executable blocks each duplicated the full scheduler_srcs list) into a
single sched_unit_test aggregate, following the controller
controller_test.c precedent: one executable, one process, one runner.

- sched_unit_runner.c: SUITES registry (28 entries) with per-suite
  setUp/tearDown dispatch, -t <suite> filter, UnitySetTestFile per entry
- testsuite/meson.build: runner + 28 test TUs + every scheduler_srcs
  entry except src/main.c as separate TUs; foreach over scheduler_srcs
  so the aggregate can never drift from the production source list;
  all baked-path defines (XCE_SCHED_*_PATH, XCE_SIM_*) move to the
  aggregate's c_args; -Werror=incompatible-pointer-types carried
  suite-wide (verified clean across all 28 tests + 50 src TUs)
- transformed tests drop main()/setUp()/tearDown() and local stubs that
  now resolve against the aggregate's real src TUs
- test() registrations 126 -> 99 (126 - 28 + 1 aggregate); the three
  --wrap= interposition tests (decision publish retry, backpressure,
  decision redelivery) keep their standalone executables unchanged
- isolation libs (sched_core_isolation_lib, sched_sim_isolation_lib)
  stay build-only and are wired as test()-level depends; the nm(1)
  text-scan tests find them via XCE_CORE_LIB_PATH/XCE_SIM_LIB_PATH

Verified: meson test --suite scheduler 100/100 green x3 consecutive
runs (43s total, aggregate 3.3s); /tmp/xce_* sweep clean (the one
leftover is test_worker_launch_type_dispatch.c's by-design fixed-path
fake xrun launcher).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 10:39:55 +00:00

1168 lines
45 KiB
C

/*
* 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 <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.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_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 <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);
}
}
/*
* 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>", "\"tlog.h\"", "\"xcelog.h\"", "<xcelog.h>", "<syslog.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;
}
/* 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: "<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 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)");
}
/* ---- suite entry for the sched_unit_test aggregate ---------------------- */
void test_sched_sim_isolation(void)
{
UnitySetTestFile(__FILE__);
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);
}