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>
1168 lines
45 KiB
C
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);
|
|
}
|