feat(scheduler-sim-decision-trace): trace record model foundation
Task 1.3: trace types and constants, per-record arena, job selection matching, no-decision record finishing; empty capture module; both sources scanned by the core isolation test with a no-stdio check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7f40ff27dc
commit
7d3c71fc34
8 changed files with 1234 additions and 1 deletions
|
|
@ -63,6 +63,17 @@
|
|||
* transitive include, or have to special-case it; the nm check
|
||||
* sidesteps that entirely by checking *usage*, not *visibility*).
|
||||
*
|
||||
* Decision trace sources (scheduler-sim-decision-trace spec, task 1.3,
|
||||
* design.md "Invariant Cross-Check" dimension 2): sched_trace.c (record
|
||||
* model) and sched_trace_capture.c (capture helpers) run on the allocator
|
||||
* thread inside the core's allocation chain, so they join layer 1's
|
||||
* forbidden-headers and forbidden-symbols scans, plus a check that applies
|
||||
* to these two files only: no stdio, no file I/O and no logging (a trace
|
||||
* record is pure data; only the simulator REPL renders or writes it). They
|
||||
* are NOT part of layer 2: sched_core_isolation_lib covers the four core
|
||||
* state-machine files, and the capture file references scheduler symbols
|
||||
* outside that archive's scope.
|
||||
*
|
||||
* Why nm on a static_library() rather than the full xce_scheduler
|
||||
* executable: the executable links the whole monolithic libxce_dep, so
|
||||
* every symbol from netio/xceapi/client/delay/idgen would appear in ITS
|
||||
|
|
@ -98,6 +109,14 @@
|
|||
#error \
|
||||
"XCE_CORE_CLOCK_C_PATH must be defined by meson.build to core_clock.c's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_SCHED_TRACE_C_PATH
|
||||
#error \
|
||||
"XCE_SCHED_TRACE_C_PATH must be defined by meson.build to sched_trace.c's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_SCHED_TRACE_CAPTURE_C_PATH
|
||||
#error \
|
||||
"XCE_SCHED_TRACE_CAPTURE_C_PATH must be defined by meson.build to sched_trace_capture.c's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_CORE_LIB_PATH
|
||||
#error \
|
||||
"XCE_CORE_LIB_PATH must be defined by meson.build to the absolute path of core.c's isolated static_library() archive"
|
||||
|
|
@ -196,18 +215,24 @@ static void test_core_source_paths_are_readable(void)
|
|||
char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH);
|
||||
char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH);
|
||||
char* cc = read_whole_file(XCE_CORE_CLOCK_C_PATH);
|
||||
char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH);
|
||||
char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_CORE_H_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_CORE_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(ce, XCE_CORE_EVENTS_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(cs, XCE_CORE_SESSION_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(cc, XCE_CORE_CLOCK_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(st, XCE_SCHED_TRACE_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(sc, XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
free(ce);
|
||||
free(cs);
|
||||
free(cc);
|
||||
free(st);
|
||||
free(sc);
|
||||
}
|
||||
|
||||
/* Scans one file's text for every FORBIDDEN_INCLUDES entry, tagging each
|
||||
|
|
@ -234,24 +259,32 @@ static void test_core_never_includes_forbidden_headers(void)
|
|||
char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH);
|
||||
char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH);
|
||||
char* cc = read_whole_file(XCE_CORE_CLOCK_C_PATH);
|
||||
char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH);
|
||||
char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
TEST_ASSERT_NOT_NULL(ce);
|
||||
TEST_ASSERT_NOT_NULL(cs);
|
||||
TEST_ASSERT_NOT_NULL(cc);
|
||||
TEST_ASSERT_NOT_NULL(st);
|
||||
TEST_ASSERT_NOT_NULL(sc);
|
||||
|
||||
assert_no_forbidden_includes(h, "core.h");
|
||||
assert_no_forbidden_includes(c, "core.c");
|
||||
assert_no_forbidden_includes(ce, "core_events.c");
|
||||
assert_no_forbidden_includes(cs, "core_session.c");
|
||||
assert_no_forbidden_includes(cc, "core_clock.c");
|
||||
assert_no_forbidden_includes(st, "sched_trace.c");
|
||||
assert_no_forbidden_includes(sc, "sched_trace_capture.c");
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
free(ce);
|
||||
free(cs);
|
||||
free(cc);
|
||||
free(st);
|
||||
free(sc);
|
||||
}
|
||||
|
||||
/* Shared by test_core_never_calls_forbidden_symbols() -- see
|
||||
|
|
@ -278,24 +311,32 @@ static void test_core_never_calls_forbidden_symbols(void)
|
|||
char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH);
|
||||
char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH);
|
||||
char* cc = read_whole_file(XCE_CORE_CLOCK_C_PATH);
|
||||
char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH);
|
||||
char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
TEST_ASSERT_NOT_NULL(ce);
|
||||
TEST_ASSERT_NOT_NULL(cs);
|
||||
TEST_ASSERT_NOT_NULL(cc);
|
||||
TEST_ASSERT_NOT_NULL(st);
|
||||
TEST_ASSERT_NOT_NULL(sc);
|
||||
|
||||
assert_no_forbidden_symbols(h, "core.h");
|
||||
assert_no_forbidden_symbols(c, "core.c");
|
||||
assert_no_forbidden_symbols(ce, "core_events.c");
|
||||
assert_no_forbidden_symbols(cs, "core_session.c");
|
||||
assert_no_forbidden_symbols(cc, "core_clock.c");
|
||||
assert_no_forbidden_symbols(st, "sched_trace.c");
|
||||
assert_no_forbidden_symbols(sc, "sched_trace_capture.c");
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
free(ce);
|
||||
free(cs);
|
||||
free(cc);
|
||||
free(st);
|
||||
free(sc);
|
||||
}
|
||||
|
||||
/* core_clock.c is deliberately NOT scanned here -- it is the one
|
||||
|
|
@ -342,6 +383,284 @@ static void test_core_never_calls_wallclock_functions(void)
|
|||
free(cs);
|
||||
}
|
||||
|
||||
/*
|
||||
* No stdio, no file I/O, no logging in the decision trace sources
|
||||
* (scheduler-sim-decision-trace design.md, TraceModel / TraceCapture: "No
|
||||
* I/O and no tlog"). Applies to sched_trace.c and sched_trace_capture.c
|
||||
* only.
|
||||
*
|
||||
* Comments, string literals and character literals are blanked before the
|
||||
* scan, so prose such as "never calls fopen" in a comment is not a hit.
|
||||
* Call names match on identifier boundaries followed by '(' (spaces
|
||||
* allowed), and not as a member call (`x.read(` / `x->read(`), so
|
||||
* `sched_trace_rec_free(` or `SCHED_TRACE(` never match `free(`/`TRACE(`.
|
||||
*/
|
||||
static const char* const TRACE_FORBIDDEN_INCLUDES[] = {
|
||||
"<stdio.h>", "<fcntl.h>", "<unistd.h>", "<sys/stat.h>",
|
||||
"<sys/file.h>", "<tlog.h>", "\"xcelog.h\"",
|
||||
};
|
||||
#define NUM_TRACE_FORBIDDEN_INCLUDES \
|
||||
(sizeof(TRACE_FORBIDDEN_INCLUDES) / sizeof(TRACE_FORBIDDEN_INCLUDES[0]))
|
||||
|
||||
/* Identifiers that must not appear at all (stdio stream objects). */
|
||||
static const char* const TRACE_FORBIDDEN_IDENTS[] = {
|
||||
"FILE",
|
||||
"stdin",
|
||||
"stdout",
|
||||
"stderr",
|
||||
};
|
||||
#define NUM_TRACE_FORBIDDEN_IDENTS \
|
||||
(sizeof(TRACE_FORBIDDEN_IDENTS) / sizeof(TRACE_FORBIDDEN_IDENTS[0]))
|
||||
|
||||
/* Function-like names that must not be called. */
|
||||
static const char* const TRACE_FORBIDDEN_CALLS[] = {
|
||||
/* stdio */
|
||||
"printf",
|
||||
"fprintf",
|
||||
"vprintf",
|
||||
"vfprintf",
|
||||
"dprintf",
|
||||
"vdprintf",
|
||||
"puts",
|
||||
"fputs",
|
||||
"putchar",
|
||||
"fputc",
|
||||
"putc",
|
||||
"getchar",
|
||||
"fgetc",
|
||||
"getc",
|
||||
"fgets",
|
||||
"scanf",
|
||||
"fscanf",
|
||||
"perror",
|
||||
"fopen",
|
||||
"fdopen",
|
||||
"freopen",
|
||||
"fmemopen",
|
||||
"open_memstream",
|
||||
"fclose",
|
||||
"fflush",
|
||||
"fwrite",
|
||||
"fread",
|
||||
"fseek",
|
||||
"fseeko",
|
||||
"ftell",
|
||||
"ftello",
|
||||
"rewind",
|
||||
"setvbuf",
|
||||
"popen",
|
||||
"pclose",
|
||||
"tmpfile",
|
||||
/* POSIX file I/O */
|
||||
"open",
|
||||
"openat",
|
||||
"creat",
|
||||
"close",
|
||||
"read",
|
||||
"write",
|
||||
"pread",
|
||||
"pwrite",
|
||||
"readv",
|
||||
"writev",
|
||||
"lseek",
|
||||
"fsync",
|
||||
"fdatasync",
|
||||
"ftruncate",
|
||||
"truncate",
|
||||
"unlink",
|
||||
"rename",
|
||||
"mkdir",
|
||||
"stat",
|
||||
"fstat",
|
||||
"lstat",
|
||||
"mmap",
|
||||
"syslog",
|
||||
/* project logging (tlog macros and SCHEDULER_LOG) */
|
||||
"TRACE",
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
"WARN",
|
||||
"ERROR",
|
||||
"FATAL",
|
||||
"SCHEDULER_LOG",
|
||||
};
|
||||
#define NUM_TRACE_FORBIDDEN_CALLS \
|
||||
(sizeof(TRACE_FORBIDDEN_CALLS) / sizeof(TRACE_FORBIDDEN_CALLS[0]))
|
||||
|
||||
static int is_ident_char(char c)
|
||||
{
|
||||
return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'))
|
||||
|| ((c >= '0') && (c <= '9')) || (c == '_');
|
||||
}
|
||||
|
||||
/* Blanks comments and string/char literals in place (newlines kept). A
|
||||
* line that is an #include directive keeps its header name, so the
|
||||
* include scan above can still see it. */
|
||||
static void blank_comments_and_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 + 1] == '*')) {
|
||||
while ((t[i] != '\0') && !((t[i] == '*') && (t[i + 1] == '/'))) {
|
||||
if (t[i] != '\n') {
|
||||
t[i] = ' ';
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (t[i] != '\0') {
|
||||
t[i] = ' ';
|
||||
t[i + 1] = ' ';
|
||||
i += 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ((t[i] == '/') && (t[i + 1] == '/')) {
|
||||
while ((t[i] != '\0') && (t[i] != '\n')) {
|
||||
t[i++] = ' ';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
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 identifier-bounded occurrence of `name` in `t`
|
||||
* (followed by '(' when `call`), or -1. */
|
||||
static long find_ident(const char* t, const char* name, int call)
|
||||
{
|
||||
size_t n = strlen(name);
|
||||
const char* p = t;
|
||||
|
||||
while ((p = strstr(p, name)) != NULL) {
|
||||
const char* e = p + n;
|
||||
int ok = 1;
|
||||
|
||||
if (p > t) {
|
||||
char b = p[-1];
|
||||
|
||||
if (is_ident_char(b) || (b == '.')
|
||||
|| ((b == '>') && (p - t >= 2) && (p[-2] == '-'))) {
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
if (is_ident_char(*e)) {
|
||||
ok = 0;
|
||||
}
|
||||
if (ok && call) {
|
||||
while ((*e == ' ') || (*e == '\t')) {
|
||||
e++;
|
||||
}
|
||||
ok = (*e == '(');
|
||||
}
|
||||
if (ok) {
|
||||
return (long)(p - t);
|
||||
}
|
||||
p += n;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void assert_trace_source_has_no_io(char* text, const char* label)
|
||||
{
|
||||
char msg[256];
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < NUM_TRACE_FORBIDDEN_INCLUDES; i++) {
|
||||
snprintf(msg, sizeof(msg), "%s includes stdio/file-I/O/log header %s",
|
||||
label, TRACE_FORBIDDEN_INCLUDES[i]);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(text, TRACE_FORBIDDEN_INCLUDES[i]),
|
||||
msg);
|
||||
}
|
||||
|
||||
blank_comments_and_literals(text);
|
||||
|
||||
for (i = 0; i < NUM_TRACE_FORBIDDEN_IDENTS; i++) {
|
||||
snprintf(msg, sizeof(msg), "%s references stdio stream '%s'", label,
|
||||
TRACE_FORBIDDEN_IDENTS[i]);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
-1, find_ident(text, TRACE_FORBIDDEN_IDENTS[i], 0), msg);
|
||||
}
|
||||
for (i = 0; i < NUM_TRACE_FORBIDDEN_CALLS; i++) {
|
||||
snprintf(msg, sizeof(msg),
|
||||
"%s calls stdio/file-I/O/log function '%s' -- trace model "
|
||||
"and capture code must do no I/O",
|
||||
label, TRACE_FORBIDDEN_CALLS[i]);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
-1, find_ident(text, TRACE_FORBIDDEN_CALLS[i], 1), msg);
|
||||
}
|
||||
}
|
||||
|
||||
/* The scan itself must be able to fail: a probe text with one hit of each
|
||||
* class is rejected, and the same names inside comments, literals and
|
||||
* longer identifiers are not. */
|
||||
static void test_trace_io_scan_detects_and_ignores(void)
|
||||
{
|
||||
char ok_text[] = "/* never fopen( or printf( here */\n"
|
||||
"// stdout FILE write(\n"
|
||||
"static const char* s = \"fprintf(stderr\";\n"
|
||||
"void f(void) { sched_trace_rec_free(0); "
|
||||
"SCHED_TRACE(core, x); p->read(1); q.write(2); "
|
||||
"readonly(3); my_FILE_t z; }\n";
|
||||
char hit_call[] = "void f(void) { fwrite (b, 1, 1, 0); }\n";
|
||||
char hit_ident[] = "static void* g = (void*)stderr;\n";
|
||||
char hit_log[] = "void f(void) { WARN(\"x\"); }\n";
|
||||
char hit_posix[] = "void f(void) { (void)write(1, 0, 0); }\n";
|
||||
|
||||
blank_comments_and_literals(ok_text);
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "fopen", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "printf", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "fprintf", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "stdout", 0));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "stderr", 0));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "FILE", 0));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "free", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "TRACE", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "read", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "write", 1));
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_call, "fwrite", 1));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_ident, "stderr", 0));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_log, "WARN", 1));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_posix, "write", 1));
|
||||
}
|
||||
|
||||
static void test_trace_sources_do_no_stdio_or_file_io(void)
|
||||
{
|
||||
char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH);
|
||||
char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(st);
|
||||
TEST_ASSERT_NOT_NULL(sc);
|
||||
|
||||
assert_trace_source_has_no_io(st, "sched_trace.c");
|
||||
assert_trace_source_has_no_io(sc, "sched_trace_capture.c");
|
||||
|
||||
free(st);
|
||||
free(sc);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_core_object_has_no_forbidden_undefined_symbols - the literal
|
||||
* build-time/link-time proof: core.c/core_events.c/core_session.c/
|
||||
|
|
@ -422,6 +741,8 @@ int main(void)
|
|||
RUN_TEST(test_core_never_includes_forbidden_headers);
|
||||
RUN_TEST(test_core_never_calls_forbidden_symbols);
|
||||
RUN_TEST(test_core_never_calls_wallclock_functions);
|
||||
RUN_TEST(test_trace_io_scan_detects_and_ignores);
|
||||
RUN_TEST(test_trace_sources_do_no_stdio_or_file_io);
|
||||
RUN_TEST(test_core_object_has_no_forbidden_undefined_symbols);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue