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:
Rongsong Shen 2026-10-08 07:49:09 +00:00
parent 7f40ff27dc
commit 7d3c71fc34
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
8 changed files with 1234 additions and 1 deletions

View file

@ -234,6 +234,13 @@ sched_core_isolation_test = executable(
'-DXCE_CORE_CLOCK_C_PATH="'
+ (meson.current_source_dir() / '..' / 'src' / 'core_clock.c')
+ '"',
'-DXCE_SCHED_TRACE_C_PATH="'
+ (meson.current_source_dir() / '..' / 'src' / 'sched_trace.c')
+ '"',
'-DXCE_SCHED_TRACE_CAPTURE_C_PATH="'
+ (meson.current_source_dir() / '..' / 'src'
/ 'sched_trace_capture.c')
+ '"',
'-DXCE_CORE_LIB_PATH="'
+ sched_core_isolation_lib.full_path()
+ '"'],
@ -2509,3 +2516,18 @@ test('scheduler repl trace baseline test',
sched_repl_trace_baseline_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'],
depends: scheduler)
# scheduler-sim-decision-trace task 1.3: decision trace record model
# (src/sched_trace.c) unit tests -- selection matching, record header and
# per-record arena copying and size, record finishing. Links only the model
# source; libxce supplies the jobid helpers.
sched_trace_model_test = executable(
'sched_trace_model_test',
['test_trace_model.c', '../src/sched_trace.c'],
include_directories:[incdir],
dependencies: [scheduler_test_deps],
install: false)
test('scheduler trace model test',
sched_trace_model_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -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();
}

View file

@ -0,0 +1,432 @@
/*
* test_trace_model.c - unit tests for the decision trace record model
* (scheduler-sim-decision-trace spec, task 1.3; design.md "TraceModel").
*
* Covers the model foundation:
* - job selection matching: all jobs, a plain job, an array element, and
* a whole array including elements that did not exist when the
* selector was made (Requirements 1.2, 1.3);
* - the record header copied from the settings snapshot and the
* per-record arena: every string and number is copied, so a record
* never points into scheduler (or REPL) memory, and the record reports
* its own byte size (Requirement 9.1, design "Invariants");
* - finishing a record as a no-decision kind (Requirement 2.4);
* - freeing a NULL record is a no-op.
*
* Links only src/sched_trace.c (plus libxce for the jobid helpers).
*/
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "sched/trace.h"
#include "utils.h"
/* A plain job, and an array block whose elements are tagged TAG_JOB_ARRAY
* and numbered from 1 within the block (job-array-one-based-index). */
#define PLAIN_ID(seq) MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)(seq))
#define ARRAY_BASE(blk) \
MAKE_JOBID(1, 2, 1000, 0x0, ((uint64_t)(blk)) << N_ARRAY_IDX_BITS)
#define ARRAY_ELEM(blk, k) \
MAKE_JOBID(1, 2, 1000, TAG_JOB_ARRAY, \
(((uint64_t)(blk)) << N_ARRAY_IDX_BITS) | (uint64_t)(k))
void setUp(void) { return; }
void tearDown(void) { return; }
static xce_sched_trace_ctl_t make_ctl(xce_sched_trace_sel_t* sel, int nsel)
{
xce_sched_trace_ctl_t ctl;
memset(&ctl, 0, sizeof(ctl));
ctl.all = false;
ctl.nsel = nsel;
ctl.sel = sel;
ctl.level = XCE_TRACE_LEVEL_SUMMARY;
ctl.line_bound = XCE_TRACE_LINE_BOUND_DEFAULT;
ctl.session_no = 7;
ctl.policy_name = "fcfs";
ctl.position = 3;
return ctl;
}
/* ---- constants ------------------------------------------------------- */
static void test_constants_match_requirements(void)
{
TEST_ASSERT_EQUAL_INT(1000, XCE_TRACE_LINE_BOUND_DEFAULT);
TEST_ASSERT_EQUAL_INT(50, XCE_TRACE_LINE_BOUND_MIN);
TEST_ASSERT_EQUAL_INT(100000, XCE_TRACE_LINE_BOUND_MAX);
TEST_ASSERT_EQUAL_INT(50, XCE_TRACE_OUTCOME_HOSTS);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_SUMMARY, 0);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_DECISION, 0);
/* verdict kinds are distinct single bits, one per counter slot */
TEST_ASSERT_EQUAL_HEX(0x3f, XCE_TRACE_V_ACCEPTED | XCE_TRACE_V_SELECT
| XCE_TRACE_V_SHORTFALL | XCE_TRACE_V_LIMIT
| XCE_TRACE_V_CLAIMED
| XCE_TRACE_V_INTERNAL);
TEST_ASSERT_EQUAL_INT(6, XCE_TRACE_V_NKINDS);
}
/* ---- selection matching (1.2, 1.3) ----------------------------------- */
static void test_wants_null_ctl_is_false(void)
{
TEST_ASSERT_FALSE(sched_trace_ctl_wants(NULL, PLAIN_ID(5)));
}
static void test_wants_all_matches_every_job(void)
{
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
ctl.all = true;
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, PLAIN_ID(5)));
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 1)));
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 400)));
}
static void test_wants_empty_selection_matches_nothing(void)
{
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, PLAIN_ID(5)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 1)));
}
static void test_wants_plain_job_matches_only_that_job(void)
{
xce_sched_trace_sel_t sel[] = {
{ PLAIN_ID(5), false }
};
xce_sched_trace_ctl_t ctl = make_ctl(sel, 1);
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, PLAIN_ID(5)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, PLAIN_ID(6)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 5)));
}
static void test_wants_element_matches_only_that_element(void)
{
xce_sched_trace_sel_t sel[] = {
{ ARRAY_ELEM(3, 2), false }
};
xce_sched_trace_ctl_t ctl = make_ctl(sel, 1);
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 2)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 1)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 3)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(4, 2)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_BASE(3)));
}
static void test_wants_whole_array_matches_every_element(void)
{
xce_sched_trace_sel_t sel[] = {
{ ARRAY_BASE(3), true }
};
xce_sched_trace_ctl_t ctl = make_ctl(sel, 1);
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 1)));
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 2)));
/* elements created later (any index in the block) also match (1.3) */
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, MAX_ARRAY_IDX)));
/* another array, and a plain job that happens to share the base id's
* bit pattern, do not */
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(4, 1)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_BASE(3)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, PLAIN_ID(7)));
}
static void test_wants_unknown_yet_future_ids(void)
{
/* Selectors naming jobs that do not exist yet are kept as plain keys:
* the predicate is a pure function of the id, so the job matches once
* it appears (1.3). */
xce_sched_trace_sel_t sel[] = {
{ PLAIN_ID(900), false },
{ ARRAY_ELEM(8, 4), false },
{ ARRAY_BASE(9), true }
};
xce_sched_trace_ctl_t ctl = make_ctl(sel, 3);
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, PLAIN_ID(900)));
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(8, 4)));
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(9, 1)));
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(9, 77)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(8, 5)));
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, PLAIN_ID(901)));
}
/* ---- record header and arena ----------------------------------------- */
static void test_rec_new_null_ctl_returns_null(void)
{
TEST_ASSERT_NULL(
sched_trace_rec_new(NULL, PLAIN_ID(5), XCE_TRACE_REC_DECISION));
}
static void test_rec_new_copies_header_from_ctl(void)
{
char policy[] = "priority-fcfs";
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
xce_sched_trace_rec_t* rec;
ctl.level = XCE_TRACE_LEVEL_FULL;
ctl.line_bound = 75;
ctl.policy_name = policy;
rec = sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
/* change every source the record was built from */
ctl.level = XCE_TRACE_LEVEL_SUMMARY;
ctl.line_bound = 9999;
ctl.session_no = 99;
ctl.position = 42;
memset(policy, 'x', sizeof(policy) - 1);
TEST_ASSERT_EQUAL_UINT64(PLAIN_ID(5), rec->jobid);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_DECISION, rec->kind);
TEST_ASSERT_EQUAL_INT(0, rec->err);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_FULL, rec->level);
TEST_ASSERT_EQUAL_INT(75, rec->line_bound);
TEST_ASSERT_EQUAL_UINT64(7, rec->session_no);
TEST_ASSERT_EQUAL_INT(3, rec->position);
TEST_ASSERT_FALSE(rec->incomplete);
TEST_ASSERT_NOT_NULL(rec->policy_name);
TEST_ASSERT_TRUE(rec->policy_name != policy);
TEST_ASSERT_EQUAL_STRING("priority-fcfs", rec->policy_name);
TEST_ASSERT_EQUAL_INT(0, rec->path.depth);
TEST_ASSERT_EQUAL_INT(0, rec->counters.plans_tried);
TEST_ASSERT_EQUAL_INT(0, rec->counters.omitted_lines);
sched_trace_rec_free(rec);
}
static void test_rec_new_null_policy_stays_null(void)
{
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
xce_sched_trace_rec_t* rec;
ctl.policy_name = NULL;
rec = sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_RECONSTRUCTED);
TEST_ASSERT_NOT_NULL(rec);
TEST_ASSERT_NULL(rec->policy_name);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_RECONSTRUCTED, rec->kind);
TEST_ASSERT_FALSE(rec->incomplete);
sched_trace_rec_free(rec);
}
static void test_rec_new_does_not_keep_ctl_selection(void)
{
/* The record keeps nothing that points into the ctl: freeing the
* ctl's selector array and policy string must leave it intact. */
xce_sched_trace_sel_t* sel = calloc(1, sizeof(*sel));
char* policy = strdup("fcfs");
xce_sched_trace_ctl_t ctl;
xce_sched_trace_rec_t* rec;
TEST_ASSERT_NOT_NULL(sel);
TEST_ASSERT_NOT_NULL(policy);
sel[0].key = PLAIN_ID(5);
ctl = make_ctl(sel, 1);
ctl.policy_name = policy;
rec = sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
free(sel);
free(policy);
/* ASAN would flag a read of either freed block here */
TEST_ASSERT_EQUAL_STRING("fcfs", rec->policy_name);
sched_trace_rec_free(rec);
}
static void test_arena_strdup_copies_string(void)
{
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
xce_sched_trace_rec_t* rec
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
char* src = strdup("host-n01");
char* cp;
TEST_ASSERT_NOT_NULL(rec);
TEST_ASSERT_NOT_NULL(src);
cp = sched_trace_arena_strdup(rec, src);
TEST_ASSERT_NOT_NULL(cp);
TEST_ASSERT_TRUE(cp != src);
free(src);
TEST_ASSERT_EQUAL_STRING("host-n01", cp);
/* empty string is a real copy, NULL is NULL without marking the
* record incomplete */
cp = sched_trace_arena_strdup(rec, "");
TEST_ASSERT_NOT_NULL(cp);
TEST_ASSERT_EQUAL_STRING("", cp);
TEST_ASSERT_NULL(sched_trace_arena_strdup(rec, NULL));
TEST_ASSERT_FALSE(rec->incomplete);
TEST_ASSERT_NULL(sched_trace_arena_strdup(NULL, "x"));
sched_trace_rec_free(rec);
}
static void test_arena_memdup_copies_numbers(void)
{
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
xce_sched_trace_rec_t* rec
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
float* src = malloc(4 * sizeof(float));
float* cp;
TEST_ASSERT_NOT_NULL(rec);
TEST_ASSERT_NOT_NULL(src);
src[0] = 1.5f;
src[1] = 2.0f;
src[2] = -3.25f;
src[3] = 1e9f;
cp = sched_trace_arena_memdup(rec, src, 4 * sizeof(float));
TEST_ASSERT_NOT_NULL(cp);
TEST_ASSERT_TRUE(cp != src);
src[0] = 0.0f;
free(src);
TEST_ASSERT_EQUAL_FLOAT(1.5f, cp[0]);
TEST_ASSERT_EQUAL_FLOAT(2.0f, cp[1]);
TEST_ASSERT_EQUAL_FLOAT(-3.25f, cp[2]);
TEST_ASSERT_EQUAL_FLOAT(1e9f, cp[3]);
/* storage is suitably aligned for any scalar */
TEST_ASSERT_EQUAL_UINT(0, ((uintptr_t)cp) % _Alignof(max_align_t));
TEST_ASSERT_NULL(sched_trace_arena_memdup(rec, NULL, 8));
TEST_ASSERT_FALSE(rec->incomplete);
sched_trace_rec_free(rec);
}
static void test_arena_alloc_is_zeroed_and_aligned(void)
{
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
xce_sched_trace_rec_t* rec
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
unsigned char* p;
size_t i;
int k;
TEST_ASSERT_NOT_NULL(rec);
for (k = 0; k < 50; k++) {
size_t n = (size_t)(k * 37 + 1);
p = sched_trace_arena_alloc(rec, n);
TEST_ASSERT_NOT_NULL(p);
TEST_ASSERT_EQUAL_UINT(0, ((uintptr_t)p) % _Alignof(max_align_t));
for (i = 0; i < n; i++) {
TEST_ASSERT_EQUAL_UINT8(0, p[i]);
}
memset(p, 0xa5, n); /* ASAN flags any overlap with a neighbour */
}
sched_trace_rec_free(rec);
}
static void test_rec_bytes_reports_record_and_arena(void)
{
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
xce_sched_trace_rec_t* rec;
size_t b0, b1, b2;
int k;
TEST_ASSERT_EQUAL_UINT(0, sched_trace_rec_bytes(NULL));
rec = sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
b0 = sched_trace_rec_bytes(rec);
TEST_ASSERT_TRUE(b0 >= sizeof(*rec) + strlen("fcfs") + 1);
TEST_ASSERT_EQUAL_UINT(sizeof(*rec) + rec->arena_bytes, b0);
/* a large copy grows the arena by at least its own size */
TEST_ASSERT_NOT_NULL(sched_trace_arena_alloc(rec, 1 << 20));
b1 = sched_trace_rec_bytes(rec);
TEST_ASSERT_TRUE(b1 >= b0 + (1 << 20));
/* many small copies: size never shrinks and covers everything copied */
for (k = 0; k < 1000; k++) {
TEST_ASSERT_NOT_NULL(sched_trace_arena_strdup(rec, "0123456789"));
}
b2 = sched_trace_rec_bytes(rec);
TEST_ASSERT_TRUE(b2 >= b1 + 1000 * 11);
TEST_ASSERT_EQUAL_UINT(sizeof(*rec) + rec->arena_bytes, b2);
sched_trace_rec_free(rec);
}
static void test_rec_free_null_is_noop(void)
{
sched_trace_rec_free(NULL);
TEST_PASS();
}
/* ---- record finishing (2.4) ------------------------------------------ */
static void finish_and_check(xce_sched_trace_rec_kind_t kind, int err)
{
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
xce_sched_trace_rec_t* rec
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
sched_trace_rec_finish(rec, kind, err);
TEST_ASSERT_EQUAL_INT(kind, rec->kind);
TEST_ASSERT_EQUAL_INT(err, rec->err);
/* the header is otherwise untouched */
TEST_ASSERT_EQUAL_UINT64(PLAIN_ID(5), rec->jobid);
TEST_ASSERT_EQUAL_INT(3, rec->position);
TEST_ASSERT_EQUAL_UINT64(7, rec->session_no);
sched_trace_rec_free(rec);
}
static void test_finish_records_kind_and_error(void)
{
finish_and_check(XCE_TRACE_REC_NOT_FOUND, -2 /* -ENOENT */);
finish_and_check(XCE_TRACE_REC_NO_SCHEDOBJ, 0);
finish_and_check(XCE_TRACE_REC_INTERNAL_ERROR, -5 /* -EIO */);
finish_and_check(XCE_TRACE_REC_INTERNAL_ERROR, -12 /* -ENOMEM */);
finish_and_check(XCE_TRACE_REC_PEND_NO_REASON, 0);
}
static void test_finish_rejects_non_no_decision_kinds(void)
{
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
xce_sched_trace_rec_t* rec
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
sched_trace_rec_finish(rec, XCE_TRACE_REC_RECONSTRUCTED, -5);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_DECISION, rec->kind);
TEST_ASSERT_EQUAL_INT(0, rec->err);
sched_trace_rec_finish(rec, XCE_TRACE_REC_DECISION, -5);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_DECISION, rec->kind);
TEST_ASSERT_EQUAL_INT(0, rec->err);
sched_trace_rec_finish(NULL, XCE_TRACE_REC_NOT_FOUND, -2); /* no crash */
sched_trace_rec_free(rec);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_constants_match_requirements);
RUN_TEST(test_wants_null_ctl_is_false);
RUN_TEST(test_wants_all_matches_every_job);
RUN_TEST(test_wants_empty_selection_matches_nothing);
RUN_TEST(test_wants_plain_job_matches_only_that_job);
RUN_TEST(test_wants_element_matches_only_that_element);
RUN_TEST(test_wants_whole_array_matches_every_element);
RUN_TEST(test_wants_unknown_yet_future_ids);
RUN_TEST(test_rec_new_null_ctl_returns_null);
RUN_TEST(test_rec_new_copies_header_from_ctl);
RUN_TEST(test_rec_new_null_policy_stays_null);
RUN_TEST(test_rec_new_does_not_keep_ctl_selection);
RUN_TEST(test_arena_strdup_copies_string);
RUN_TEST(test_arena_memdup_copies_numbers);
RUN_TEST(test_arena_alloc_is_zeroed_and_aligned);
RUN_TEST(test_rec_bytes_reports_record_and_arena);
RUN_TEST(test_rec_free_null_is_noop);
RUN_TEST(test_finish_records_kind_and_error);
RUN_TEST(test_finish_rejects_non_no_decision_kinds);
return UNITY_END();
}