feat(scheduler-core): enforce simulation isolation (task 11.3)
Three-layer regression guard (test_sim_isolation.c) proving sim.h/sim.c/ sim_loop.c hold no controller-facing client/cluster-config/capnp-socket state: source-text scan, struct-body field scan, and nm(1) link-time undefined-symbol audit of a new isolated sched_sim_isolation_lib target.
This commit is contained in:
parent
7e1e861e4f
commit
b8d2f5f48d
4 changed files with 604 additions and 1 deletions
|
|
@ -61,4 +61,21 @@ sched_core_isolation_lib = static_library(
|
|||
dependencies: scheduler_deps,
|
||||
install: false)
|
||||
|
||||
# Task 11.3 (scheduler-core spec, Req 5 AC 17): isolated compile target for
|
||||
# the simulation driver's own files, consumed by
|
||||
# testsuite/test_sim_isolation.c to prove at build time that sim.c/sim_loop.c
|
||||
# reference zero controller-facing symbols (client/netio/xceapi/delay/idgen).
|
||||
# Both files are compiled together into ONE archive -- task 11.2 added
|
||||
# sim_loop.c as a sibling file to task 11.1's sim.c, and covering only one of
|
||||
# them would repeat core.c's own siblings' (core_events.c/core_session.c/
|
||||
# core_clock.c) never-mechanically-covered gap for the sim driver. Same
|
||||
# "compiling to an object is enough, no real linking needed" rationale as
|
||||
# sched_core_isolation_lib above.
|
||||
sched_sim_isolation_lib = static_library(
|
||||
'sched_sim_isolation',
|
||||
['src/sim.c', 'src/sim_loop.c'],
|
||||
include_directories: incdir,
|
||||
dependencies: scheduler_deps,
|
||||
install: false)
|
||||
|
||||
subdir('testsuite')
|
||||
|
|
|
|||
|
|
@ -501,6 +501,40 @@ sched_sim_event_loop_test = executable(
|
|||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Regression guard for task 11.3's Observable (scheduler-core spec, Req 5 AC
|
||||
# 17, "Simulation isolation"): "a build-time or structural check confirms no
|
||||
# controller-facing symbol is reachable from the simulation driver's compiled
|
||||
# object", plus the AC's own "confirmed by the struct definition itself, not
|
||||
# runtime checks alone" clause. Same source-text-scan + nm(1) link-time-proof
|
||||
# genre as sched_core_isolation_test above, PLUS a third, narrower structural
|
||||
# layer scanning ONLY xce_sched_sim_driver_t's own struct body text for
|
||||
# forbidden field types -- see the test's own header comment for why a
|
||||
# comment-stripping pass is required first (sim.h's own boundary doc comment
|
||||
# legitimately names xcecli_ctx_t/xce_cluster_config_t in prose). link_with
|
||||
# establishes the build-order dependency on sched_sim_isolation_lib (parent
|
||||
# meson.build -- sim.c AND sim_loop.c compiled together, isolated from the
|
||||
# rest of the scheduler); XCE_SIM_*_PATH bake in absolute paths the same way
|
||||
# XCE_CORE_*_PATH does above.
|
||||
sched_sim_isolation_test = executable(
|
||||
'sched_sim_isolation_test',
|
||||
['test_sim_isolation.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
link_with: [sched_sim_isolation_lib],
|
||||
c_args: ['-DXCE_SIM_H_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'include' / 'sim.h')
|
||||
+ '"',
|
||||
'-DXCE_SIM_C_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'src' / 'sim.c')
|
||||
+ '"',
|
||||
'-DXCE_SIM_LOOP_C_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'src' / 'sim_loop.c')
|
||||
+ '"',
|
||||
'-DXCE_SIM_LIB_PATH="'
|
||||
+ sched_sim_isolation_lib.full_path()
|
||||
+ '"'],
|
||||
install: false)
|
||||
|
||||
# Register as meson tests so `meson test --suite scheduler` runs them all.
|
||||
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
|
||||
test('scheduler test', scheduler_test, suite: 'scheduler',
|
||||
|
|
@ -576,3 +610,6 @@ test('scheduler sim startup test',
|
|||
test('scheduler sim event loop test',
|
||||
sched_sim_event_loop_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler sim isolation test',
|
||||
sched_sim_isolation_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
548
src/scheduler/testsuite/test_sim_isolation.c
Normal file
548
src/scheduler/testsuite/test_sim_isolation.c
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
/*
|
||||
* test_sim_isolation.c - regression guard for task 11.3's Observable
|
||||
* (scheduler-core spec, Req 5 AC 17, "Simulation isolation"): "a build-time
|
||||
* or structural check confirms no controller-facing symbol is reachable
|
||||
* from the simulation driver's compiled object" -- plus the AC's own
|
||||
* "confirmed by the struct definition itself, not runtime checks alone"
|
||||
* clause. Scope: sim.h + sim.c + sim_loop.c together (task 11.1 + task
|
||||
* 11.2's sibling file) -- NOT a re-audit of core.c (task 4.1's own
|
||||
* territory, test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c (shared
|
||||
* normal-mode infra, out of this task's boundary).
|
||||
*
|
||||
* Three layers, extending test_core_isolation.c's two-layer pattern (same
|
||||
* genre: source-text scans of the real, current on-disk files, not a frozen
|
||||
* snapshot):
|
||||
*
|
||||
* 1. Source-text scan of sim.h/sim.c/sim_loop.c: no #include of
|
||||
* client.h/netio.h/xceapi.h/delay.h/idgen.h/xce/cluster.h; no call-site
|
||||
* substring matching a symbol exported only by those libraries (the
|
||||
* exact FORBIDDEN_SYMBOLS list test_core_isolation.c already
|
||||
* established, reused verbatim here, not reinvented).
|
||||
*
|
||||
* 2. A structural check test_core_isolation.c has no equivalent of: Req 5
|
||||
* AC 17 explicitly requires "the xce_sched_sim_driver_t shall contain
|
||||
* no xcecli_ctx_t, no xce_cluster_config_t, no capnp socket" --
|
||||
* enforced by the STRUCT DEFINITION ITSELF, not merely by absence of
|
||||
* the type names anywhere in the file (sim.h's own boundary comment,
|
||||
* just above the struct, legitimately NAMES xcecli_ctx_t and
|
||||
* xce_cluster_config_t in prose while documenting this exact
|
||||
* requirement -- see strip_c_comments()'s doc comment below for why
|
||||
* that makes a naive whole-file scan wrong). This layer extracts ONLY
|
||||
* the `typedef struct { ... } xce_sched_sim_driver_t;` body text and
|
||||
* scans that narrower region for forbidden field types.
|
||||
*
|
||||
* 3. The literal build-time/link-time proof the Observable asks for, not
|
||||
* achievable by a text scan alone: sim.c AND sim_loop.c together (not
|
||||
* just one -- task 11.2 added sim_loop.c as a sibling file to task
|
||||
* 11.1's sim.c, and repeating core.c's siblings' own "never
|
||||
* mechanically covered" gap for the sim driver's own second file would
|
||||
* be exactly the mistake this task exists to avoid) are compiled in
|
||||
* isolation into their own static library (meson.build's
|
||||
* sched_sim_isolation_lib, this test's XCE_SIM_LIB_PATH), and this test
|
||||
* inspects that archive's UNDEFINED SYMBOL TABLE via nm(1) -- proving
|
||||
* what the compiled code ACTUALLY calls, independent of what any
|
||||
* header happens to transitively declare. Same "compile alone, inspect
|
||||
* undefined symbols" technique and same rationale
|
||||
* test_core_isolation.c's own header comment gives for why nm on a
|
||||
* static_library() (not the full xce_scheduler executable, which links
|
||||
* the whole monolithic libxce_dep and would prove nothing about this
|
||||
* one translation unit).
|
||||
*
|
||||
* Deliberately NOT covered here: the wall-clock ban (FORBIDDEN_WALLCLOCK in
|
||||
* test_core_isolation.c) -- that constraint is scoped to core.c/core.h
|
||||
* specifically (Req 5 AC 3), not to driver-level code. sim.c's
|
||||
* sim_set_virtual_clock() and sim_loop.c's sim_advance_virtual_clock() both
|
||||
* legitimately call clock_gettime() to seed/advance the core's virtual-clock
|
||||
* anchors from the driver side -- their own doc comments already explain why
|
||||
* this does not violate core.c's isolation boundary. Applying
|
||||
* FORBIDDEN_WALLCLOCK here would incorrectly fail on that legitimate,
|
||||
* documented behavior.
|
||||
*/
|
||||
#include <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_LIB_PATH
|
||||
#error \
|
||||
"XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c's isolated static_library() archive"
|
||||
#endif
|
||||
|
||||
/* Forbidden headers (design.md "Allowed Dependencies", same list
|
||||
* test_core_isolation.c uses for core.h/core.c, reused verbatim): the
|
||||
* simulation driver must never directly include any of these. */
|
||||
static const char* const FORBIDDEN_INCLUDES[] = {
|
||||
"#include \"client.h\"", "#include \"netio.h\"",
|
||||
"#include \"xceapi.h\"", "#include \"delay.h\"",
|
||||
"#include \"idgen.h\"", "#include \"xce/cluster.h\"",
|
||||
"#include <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);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_SIM_H_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_SIM_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(loop, XCE_SIM_LOOP_C_PATH);
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
free(loop);
|
||||
}
|
||||
|
||||
static void test_sim_never_includes_forbidden_headers(void)
|
||||
{
|
||||
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
|
||||
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
|
||||
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
|
||||
char* h;
|
||||
char* c;
|
||||
char* loop;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h_raw);
|
||||
TEST_ASSERT_NOT_NULL(c_raw);
|
||||
TEST_ASSERT_NOT_NULL(loop_raw);
|
||||
|
||||
h = strip_c_comments(h_raw);
|
||||
c = strip_c_comments(c_raw);
|
||||
loop = strip_c_comments(loop_raw);
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
TEST_ASSERT_NOT_NULL(loop);
|
||||
|
||||
assert_no_forbidden_includes(h, "sim.h");
|
||||
assert_no_forbidden_includes(c, "sim.c");
|
||||
assert_no_forbidden_includes(loop, "sim_loop.c");
|
||||
|
||||
free(h_raw);
|
||||
free(c_raw);
|
||||
free(loop_raw);
|
||||
free(h);
|
||||
free(c);
|
||||
free(loop);
|
||||
}
|
||||
|
||||
static void test_sim_never_calls_forbidden_symbols(void)
|
||||
{
|
||||
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
|
||||
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
|
||||
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
|
||||
char* h;
|
||||
char* c;
|
||||
char* loop;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h_raw);
|
||||
TEST_ASSERT_NOT_NULL(c_raw);
|
||||
TEST_ASSERT_NOT_NULL(loop_raw);
|
||||
|
||||
h = strip_c_comments(h_raw);
|
||||
c = strip_c_comments(c_raw);
|
||||
loop = strip_c_comments(loop_raw);
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
TEST_ASSERT_NOT_NULL(loop);
|
||||
|
||||
assert_no_forbidden_symbols(h, "sim.h");
|
||||
assert_no_forbidden_symbols(c, "sim.c");
|
||||
assert_no_forbidden_symbols(loop, "sim_loop.c");
|
||||
|
||||
free(h_raw);
|
||||
free(c_raw);
|
||||
free(loop_raw);
|
||||
free(h);
|
||||
free(c);
|
||||
free(loop);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_sim_driver_struct_has_no_forbidden_fields - Req 5 AC 17's
|
||||
* "confirmed by the struct definition itself, not runtime checks alone"
|
||||
* clause. Scans ONLY xce_sched_sim_driver_t's own `{ ... }` field-listing
|
||||
* text (extract_struct_body(), on comment-stripped sim.h) for forbidden
|
||||
* field types -- narrower and more literal than the whole-file scans above,
|
||||
* and immune to sim.h's own boundary comment naming these same types in
|
||||
* prose (comments are stripped before extraction).
|
||||
*/
|
||||
static void test_sim_driver_struct_has_no_forbidden_fields(void)
|
||||
{
|
||||
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
|
||||
char* h;
|
||||
char struct_body[STRUCT_BODY_BUF_SIZE];
|
||||
size_t i;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h_raw);
|
||||
h = strip_c_comments(h_raw);
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
|
||||
extract_struct_body(h, struct_body, sizeof(struct_body));
|
||||
|
||||
for (i = 0; i < NUM_STRUCT_FORBIDDEN_TYPES; i++) {
|
||||
char msg[768];
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"xce_sched_sim_driver_t (sim.h) has a field referencing "
|
||||
"forbidden controller-facing type '%s' -- Req 5 AC 17 "
|
||||
"requires zero xcecli_ctx_t/xce_cluster_config_t/capnp-"
|
||||
"socket fields, enforced by the struct definition itself",
|
||||
STRUCT_FORBIDDEN_TYPES[i]);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(struct_body, STRUCT_FORBIDDEN_TYPES[i]),
|
||||
msg);
|
||||
}
|
||||
|
||||
free(h_raw);
|
||||
free(h);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_sim_object_has_no_forbidden_undefined_symbols - the literal
|
||||
* build-time/link-time proof: sim.c + sim_loop.c, compiled together in
|
||||
* isolation into sched_sim_isolation_lib (meson.build), must not reference
|
||||
* (as an undefined external symbol in the compiled archive) anything
|
||||
* matching the forbidden prefixes above. Independent of what any header
|
||||
* transitively declares -- reflects only what this driver's compiled code
|
||||
* actually calls. Same technique and same any_undefined_seen
|
||||
* vacuous-pass guard as test_core_isolation.c's
|
||||
* test_core_object_has_no_forbidden_undefined_symbols().
|
||||
*/
|
||||
static void test_sim_object_has_no_forbidden_undefined_symbols(void)
|
||||
{
|
||||
char cmd[4096];
|
||||
char line[1024];
|
||||
FILE* p;
|
||||
int any_undefined_seen = 0;
|
||||
|
||||
snprintf(cmd, sizeof(cmd), "nm --format=posix -u \"%s\" 2>&1",
|
||||
XCE_SIM_LIB_PATH);
|
||||
|
||||
p = popen(cmd, "r");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(p, "failed to popen() nm(1)");
|
||||
|
||||
while (fgets(line, sizeof(line), p) != NULL) {
|
||||
char symbol[512];
|
||||
char type;
|
||||
size_t i;
|
||||
|
||||
/* posix format for an undefined symbol: "<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 two member objects,
|
||||
* sim.c.o and sim_loop.c.o). */
|
||||
if (sscanf(line, "%511s %c", symbol, &type) != 2) {
|
||||
continue;
|
||||
}
|
||||
if (type != 'U') {
|
||||
continue;
|
||||
}
|
||||
|
||||
any_undefined_seen = 1;
|
||||
|
||||
for (i = 0; i < NUM_FORBIDDEN_SYMBOLS; i++) {
|
||||
if (strncmp(symbol, FORBIDDEN_SYMBOLS[i],
|
||||
strlen(FORBIDDEN_SYMBOLS[i]))
|
||||
== 0) {
|
||||
char msg[768];
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"sim.c/sim_loop.c's compiled object has an "
|
||||
"undefined reference to '%s', matching forbidden "
|
||||
"controller-facing prefix '%s' -- the simulation "
|
||||
"driver must never link networking/API/client "
|
||||
"library symbols (Req 5 AC 17)",
|
||||
symbol, FORBIDDEN_SYMBOLS[i]);
|
||||
pclose(p);
|
||||
TEST_FAIL_MESSAGE(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pclose(p);
|
||||
|
||||
/* If nm produced no undefined symbols at all, either the archive path
|
||||
* is wrong or nm itself failed silently -- either way this test would
|
||||
* be vacuously passing without having checked anything real. */
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
any_undefined_seen,
|
||||
"nm reported zero undefined symbols for XCE_SIM_LIB_PATH -- the "
|
||||
"archive path is likely wrong (this test would otherwise pass "
|
||||
"vacuously)");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_sim_source_paths_are_readable);
|
||||
RUN_TEST(test_sim_never_includes_forbidden_headers);
|
||||
RUN_TEST(test_sim_never_calls_forbidden_symbols);
|
||||
RUN_TEST(test_sim_driver_struct_has_no_forbidden_fields);
|
||||
RUN_TEST(test_sim_object_has_no_forbidden_undefined_symbols);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue