test(scheduler-sim-decision-trace): close validation coverage gaps; sync design.md
Feature validation round 1: tests for trace off keeping recall (1.5), no logging from REPL trace sources (8.7), reconstructed records in the trace file (8.1) and first in continue mode (7.3), output after trace off matching an untraced run (10.4), and tracing enabled only from the REPL (11.1). design.md now reflects the REPL-side file split, the added API, the dropped file tag and the test files; duplicate trace declarations removed from repl_internal.h. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7f1c7953a1
commit
5ca48206b2
4 changed files with 609 additions and 39 deletions
|
|
@ -76,9 +76,11 @@
|
|||
* 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>
|
||||
|
||||
|
|
@ -398,6 +400,209 @@ static void assert_no_forbidden_symbols(const char* stripped, const char* label)
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
void setUp(void) { return; }
|
||||
void tearDown(void) { return; }
|
||||
|
||||
|
|
@ -698,12 +903,144 @@ static void test_sim_trace_repl_sources_are_isolated(void)
|
|||
|
||||
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"
|
||||
|
|
@ -826,6 +1163,8 @@ int main(void)
|
|||
RUN_TEST(test_sim_never_includes_forbidden_headers);
|
||||
RUN_TEST(test_sim_never_calls_forbidden_symbols);
|
||||
RUN_TEST(test_sim_trace_repl_sources_are_isolated);
|
||||
RUN_TEST(test_repl_trace_log_scan_detects_and_ignores);
|
||||
RUN_TEST(test_trace_enable_is_repl_only);
|
||||
RUN_TEST(test_sim_driver_struct_has_no_forbidden_fields);
|
||||
RUN_TEST(test_sim_object_has_no_forbidden_undefined_symbols);
|
||||
return UNITY_END();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue