xce_scheduler -i -s <replay>: WARN "failed to resolve job picker plugin (rc=-2)" on load, and every job already present in the replay file's INITIAL snapshot stayed PEND forever -- never even reached the picker, let alone got dispatched. Root cause: sched_sim_driver_start() (sim.c) loads the initial snapshot via xce_sched_core_load_snapshot() BEFORE any picker plugin is registered (sim.h's own documented "Known scope boundary" -- only the picker's NAME is threaded through from the ReplayHeader). sched_resolve_job_picker() therefore always returns -ENOENT there, and xce_sched_core_load_snapshot()'s own picker-rebuild walk (on_queue_update/on_job_push per queue/job) is silently skipped. The real builtin_fcfs picker only gets registered moments later in sched_sim_driver_run() -- but nothing then replayed that rebuild walk, so any job/queue from the INITIAL snapshot (as opposed to one arriving later via a ReplayEvent, which correctly reaches the picker through xce_core_push_events()'s own picker!=NULL check) was permanently invisible to next_job(). Fixed by a new file, sim_picker.c: sim_bootstrap_picker() (replacing sim_loop.c's sim_register_default_picker(), moved out because sim_loop.c was already at its own documented 800-line "zero line-budget headroom" cap) registers the picker AND then replays the missed rebuild walk against it. Wired into scheduler_srcs, sched_sim_isolation_lib, and all 9 white-box test TUs that #include sim_loop.c directly. New regression test (test_sim_event_loop.c): a job baked into a fixture's INITIAL snapshot (not a ReplayEvent) now gets a live PEND decision. RED confirmed by temporarily no-op'ing the rebuild walk (test failed as expected), GREEN with the real fix. Full scheduler suite green (41/41, excluding the pre-existing unrelated bug-1560). Note: sim mode still never DISPATCHes (only PENDs) -- that is a separate, intentionally out-of-scope gap (real resource-matching allocation belongs to the not-yet-designed scheduler-allocation spec), not addressed here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
753 lines
30 KiB
C
753 lines
30 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#ifndef XCE_SIM_H_PATH
|
|
#error "XCE_SIM_H_PATH must be defined by meson.build to sim.h's absolute path"
|
|
#endif
|
|
#ifndef XCE_SIM_C_PATH
|
|
#error "XCE_SIM_C_PATH must be defined by meson.build to sim.c's absolute path"
|
|
#endif
|
|
#ifndef XCE_SIM_LOOP_C_PATH
|
|
#error \
|
|
"XCE_SIM_LOOP_C_PATH must be defined by meson.build to sim_loop.c's absolute path"
|
|
#endif
|
|
#ifndef XCE_SIM_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
|
|
#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_picker.c+sim_record.c+repl.c+repl_break.c+repl_info.c+repl_params.c's isolated static_library() archive"
|
|
#endif
|
|
|
|
/* Forbidden headers (design.md "Allowed Dependencies", same list
|
|
* test_core_isolation.c uses for core.h/core.c, reused verbatim): the
|
|
* simulation driver must never directly include any of these. */
|
|
static const char* const FORBIDDEN_INCLUDES[] = {
|
|
"#include \"client.h\"", "#include \"netio.h\"",
|
|
"#include \"xceapi.h\"", "#include \"delay.h\"",
|
|
"#include \"idgen.h\"", "#include \"xce/cluster.h\"",
|
|
"#include <client.h>", "#include <netio.h>",
|
|
"#include <xceapi.h>", "#include <delay.h>",
|
|
"#include <idgen.h>", "#include <xce/cluster.h>",
|
|
};
|
|
#define NUM_FORBIDDEN_INCLUDES \
|
|
(sizeof(FORBIDDEN_INCLUDES) / sizeof(FORBIDDEN_INCLUDES[0]))
|
|
|
|
/* Forbidden call-site symbols -- the exact FORBIDDEN_SYMBOLS list
|
|
* test_core_isolation.c already established (verified against
|
|
* netio.c/xceapi.c/client.c/delay.c/idgen.c's own extern lists), reused
|
|
* verbatim here per this task's own instruction to reuse it, not
|
|
* reinvent it. */
|
|
static const char* const FORBIDDEN_SYMBOLS[] = {
|
|
"xcecli_",
|
|
"endpoint_",
|
|
"iomgr_",
|
|
"idgen_",
|
|
"delaymgr_",
|
|
"delay_task",
|
|
"xce_connect_cluster",
|
|
"xce_reconnect_",
|
|
"xce_parse_peers",
|
|
"xce_cluster_config_validate",
|
|
"socket_connect",
|
|
"create_ssl_context",
|
|
"initialize_openssl",
|
|
"cleanup_openssl",
|
|
"io_fsm_start",
|
|
};
|
|
#define NUM_FORBIDDEN_SYMBOLS \
|
|
(sizeof(FORBIDDEN_SYMBOLS) / sizeof(FORBIDDEN_SYMBOLS[0]))
|
|
|
|
/* Forbidden xce_sched_sim_driver_t field TYPES (Req 5 AC 17's own literal
|
|
* wording: "shall contain no xcecli_ctx_t, no xce_cluster_config_t, no
|
|
* capnp socket"). xcecli_ctx_t/xce_cluster_config_t are named directly by
|
|
* the requirement; "capnp socket" maps to netio.h's own connection-carrying
|
|
* types (endpoint_t/iomgr_t/io_fsm_t) -- confirmed by inspecting netio.h: no
|
|
* distinct "capnp socket" type exists in this codebase, these are the only
|
|
* live-socket types netio.h defines. Note xcecli_ctx_t/endpoint_t/iomgr_t
|
|
* would also be caught by FORBIDDEN_SYMBOLS' "xcecli_"/"endpoint_"/"iomgr_"
|
|
* prefixes above (layer 1) -- listed again here anyway so this check stands
|
|
* on its own as the AC-17-literal "struct definition itself" proof, not
|
|
* merely inheriting layer 1's coverage. xce_cluster_config_t is NOT caught
|
|
* by FORBIDDEN_SYMBOLS (that list has "xce_cluster_config_validate", a
|
|
* different string, not a substring of "xce_cluster_config_t" or vice
|
|
* versa) -- this is the one genuinely new check this layer adds. */
|
|
static const char* const STRUCT_FORBIDDEN_TYPES[] = {
|
|
"xcecli_ctx_t", "xce_cluster_config_t", "endpoint_t", "iomgr_t", "io_fsm_t",
|
|
};
|
|
#define NUM_STRUCT_FORBIDDEN_TYPES \
|
|
(sizeof(STRUCT_FORBIDDEN_TYPES) / sizeof(STRUCT_FORBIDDEN_TYPES[0]))
|
|
|
|
/* Markers bracketing xce_sched_sim_driver_t's own definition in sim.h (see
|
|
* extract_struct_body() below). sim.h defines a SECOND `typedef struct {`
|
|
* later in the file (sched_sim_args_t) -- extract_struct_body() searches for
|
|
* the LAST "typedef struct {" strictly before the end marker, which
|
|
* correctly resolves to THIS struct's own opening brace, not the later
|
|
* unrelated one. */
|
|
static const char* const STRUCT_START_MARKER = "typedef struct {";
|
|
static const char* const STRUCT_END_MARKER = "} xce_sched_sim_driver_t;";
|
|
#define STRUCT_BODY_BUF_SIZE 4096
|
|
|
|
static char* read_whole_file(const char* path)
|
|
{
|
|
FILE* f = fopen(path, "rb");
|
|
if (f == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
if (fseek(f, 0, SEEK_END) != 0) {
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
long len = ftell(f);
|
|
if (len < 0) {
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
rewind(f);
|
|
|
|
char* buf = (char*)malloc((size_t)len + 1);
|
|
if (buf == NULL) {
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
size_t nread = fread(buf, 1, (size_t)len, f);
|
|
fclose(f);
|
|
buf[nread] = '\0';
|
|
|
|
return buf;
|
|
}
|
|
|
|
/*
|
|
* strip_c_comments - returns a heap copy of `src` with every block comment
|
|
* (slash-star ... star-slash) and line comment (slash-slash to end of line)
|
|
* replaced by a single space, leaving string and character literal CONTENTS
|
|
* untouched (so a literal containing a comment-marker-shaped sequence is
|
|
* never misparsed as a comment start). Necessary because
|
|
* sim.h's own boundary doc comment legitimately names xcecli_ctx_t and
|
|
* xce_cluster_config_t in prose while documenting Req 5 AC 17 (see this
|
|
* file's own header comment, layer 2) -- a raw-text scan (fine for
|
|
* test_core_isolation.c, whose core.h/core.c comments never name a
|
|
* forbidden symbol) would false-positive on that legitimate documentation.
|
|
* Comments never produce compiled symbols either, so stripping them first
|
|
* keeps every source-text scan below consistent with what the nm(1)
|
|
* link-time proof (layer 3) actually measures: real code, not prose.
|
|
* Returns NULL on allocation failure.
|
|
*/
|
|
static char* strip_c_comments(const char* src)
|
|
{
|
|
size_t len = strlen(src);
|
|
char* out = (char*)malloc(len + 1);
|
|
size_t oi = 0;
|
|
size_t i = 0;
|
|
|
|
if (out == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
while (i < len) {
|
|
if ((src[i] == '/') && ((i + 1) < len) && (src[i + 1] == '*')) {
|
|
i += 2;
|
|
while (((i + 1) < len)
|
|
&& !((src[i] == '*') && (src[i + 1] == '/'))) {
|
|
i++;
|
|
}
|
|
i += 2;
|
|
out[oi++] = ' ';
|
|
continue;
|
|
}
|
|
|
|
if ((src[i] == '/') && ((i + 1) < len) && (src[i + 1] == '/')) {
|
|
i += 2;
|
|
while ((i < len) && (src[i] != '\n')) {
|
|
i++;
|
|
}
|
|
out[oi++] = ' ';
|
|
continue;
|
|
}
|
|
|
|
if (src[i] == '"') {
|
|
out[oi++] = src[i++];
|
|
while ((i < len) && (src[i] != '"')) {
|
|
if ((src[i] == '\\') && ((i + 1) < len)) {
|
|
out[oi++] = src[i++];
|
|
}
|
|
out[oi++] = src[i++];
|
|
}
|
|
if (i < len) {
|
|
out[oi++] = src[i++];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (src[i] == '\'') {
|
|
out[oi++] = src[i++];
|
|
while ((i < len) && (src[i] != '\'')) {
|
|
if ((src[i] == '\\') && ((i + 1) < len)) {
|
|
out[oi++] = src[i++];
|
|
}
|
|
out[oi++] = src[i++];
|
|
}
|
|
if (i < len) {
|
|
out[oi++] = src[i++];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
out[oi++] = src[i++];
|
|
}
|
|
out[oi] = '\0';
|
|
|
|
return out;
|
|
}
|
|
|
|
/*
|
|
* extract_struct_body - isolates xce_sched_sim_driver_t's own
|
|
* `{ ... }` field-listing text from `stripped_h` (already comment-stripped
|
|
* sim.h text) into out_buf. Fails the currently-running test (via Unity's
|
|
* TEST_ASSERT_* longjmp) if either bracketing marker cannot be found, or if
|
|
* the extracted text overflows out_sz -- the same "fail loud on a stale/
|
|
* broken assumption" discipline test_core_isolation.c's own
|
|
* XCE_CORE_*_PATH #error guards and any_undefined_seen check establish.
|
|
*/
|
|
static void extract_struct_body(const char* stripped_h, char* out_buf,
|
|
size_t out_sz)
|
|
{
|
|
const char* end_pos = strstr(stripped_h, STRUCT_END_MARKER);
|
|
const char* start_pos = NULL;
|
|
const char* scan;
|
|
size_t body_len;
|
|
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(
|
|
end_pos, "sim.h: could not locate '} xce_sched_sim_driver_t;' -- "
|
|
"has the struct been renamed or removed?");
|
|
|
|
for (scan = stripped_h; scan < end_pos;) {
|
|
const char* hit = strstr(scan, STRUCT_START_MARKER);
|
|
|
|
if ((hit == NULL) || (hit >= end_pos)) {
|
|
break;
|
|
}
|
|
start_pos = hit;
|
|
scan = hit + 1;
|
|
}
|
|
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(
|
|
start_pos, "sim.h: could not locate the 'typedef struct {' opening "
|
|
"xce_sched_sim_driver_t's own definition");
|
|
|
|
body_len = (size_t)(end_pos - start_pos);
|
|
TEST_ASSERT_TRUE_MESSAGE(
|
|
body_len < out_sz,
|
|
"sim.h: xce_sched_sim_driver_t's struct body text overflowed the "
|
|
"fixed scan buffer -- grow STRUCT_BODY_BUF_SIZE");
|
|
|
|
memcpy(out_buf, start_pos, body_len);
|
|
out_buf[body_len] = '\0';
|
|
}
|
|
|
|
static void assert_no_forbidden_includes(const char* stripped,
|
|
const char* label)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < NUM_FORBIDDEN_INCLUDES; i++) {
|
|
char msg[256];
|
|
|
|
snprintf(msg, sizeof(msg), "%s directly includes forbidden header '%s'",
|
|
label, FORBIDDEN_INCLUDES[i]);
|
|
TEST_ASSERT_NULL_MESSAGE(strstr(stripped, FORBIDDEN_INCLUDES[i]), msg);
|
|
}
|
|
}
|
|
|
|
static void assert_no_forbidden_symbols(const char* stripped, const char* label)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < NUM_FORBIDDEN_SYMBOLS; i++) {
|
|
char msg[256];
|
|
|
|
snprintf(msg, sizeof(msg),
|
|
"%s references forbidden networking/API/controller symbol "
|
|
"'%s'",
|
|
label, FORBIDDEN_SYMBOLS[i]);
|
|
TEST_ASSERT_NULL_MESSAGE(strstr(stripped, FORBIDDEN_SYMBOLS[i]), msg);
|
|
}
|
|
}
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
/* Sanity check: all three source paths baked in by meson.build must
|
|
* actually be readable -- without this, a stale path would make every scan
|
|
* below silently pass on zero bytes of text (the exact false-confidence
|
|
* failure mode this whole test exists to avoid). */
|
|
static void test_sim_source_paths_are_readable(void)
|
|
{
|
|
char* h = read_whole_file(XCE_SIM_H_PATH);
|
|
char* c = read_whole_file(XCE_SIM_C_PATH);
|
|
char* loop = read_whole_file(XCE_SIM_LOOP_C_PATH);
|
|
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* 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;
|
|
|
|
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);
|
|
|
|
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);
|
|
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);
|
|
|
|
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");
|
|
|
|
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(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_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* 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;
|
|
|
|
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);
|
|
|
|
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);
|
|
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);
|
|
|
|
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");
|
|
|
|
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(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);
|
|
}
|
|
|
|
/*
|
|
* 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 eight member objects,
|
|
* sim.c.o, sim_loop.c.o, sim_picker.c.o, sim_record.c.o, repl.c.o,
|
|
* repl_break.c.o, repl_info.c.o, and repl_params.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();
|
|
}
|