From 7d3c71fc34a50d727b162e863a2386e7c6e02acc Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 8 Oct 2026 07:49:09 +0000 Subject: [PATCH] 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) --- .../scheduler-sim-decision-trace/tasks.md | 5 +- src/scheduler/include/sched/trace.h | 206 +++++++++ src/scheduler/meson.build | 2 + src/scheduler/src/sched_trace.c | 231 ++++++++++ src/scheduler/src/sched_trace_capture.c | 16 + src/scheduler/testsuite/meson.build | 22 + src/scheduler/testsuite/test_core_isolation.c | 321 +++++++++++++ src/scheduler/testsuite/test_trace_model.c | 432 ++++++++++++++++++ 8 files changed, 1234 insertions(+), 1 deletion(-) create mode 100644 src/scheduler/include/sched/trace.h create mode 100644 src/scheduler/src/sched_trace.c create mode 100644 src/scheduler/src/sched_trace_capture.c create mode 100644 src/scheduler/testsuite/test_trace_model.c diff --git a/.kiro/specs/scheduler-sim-decision-trace/tasks.md b/.kiro/specs/scheduler-sim-decision-trace/tasks.md index 5e6d0a93..2a3c0edb 100644 --- a/.kiro/specs/scheduler-sim-decision-trace/tasks.md +++ b/.kiro/specs/scheduler-sim-decision-trace/tasks.md @@ -22,7 +22,7 @@ - Done when every existing REPL test and the 1.1 baseline test pass unchanged under ASAN, and the helpers can be called from other REPL files. - _Requirements: 7.7_ -- [ ] 1.3 Build the trace record model foundation, test first +- [x] 1.3 Build the trace record model foundation, test first - Define these types: record, settings snapshot, verdict kinds, record kinds, path, no-plan and position-end. Define these constants: line bound (default 1000, range 50 to 100000) and outcome hosts (50). - The per-record arena copies every string and number, so a record never points into scheduler memory. The record reports its own byte size, and freeing a NULL record is a no-op. - Job selection matching covers all jobs, a plain job, an array element, and every element of an array (including elements created later). @@ -337,3 +337,6 @@ ## Implementation Notes - 1.1: build dir is src/xce.build. Baseline test sched_repl_trace_baseline_test runs the built xce_scheduler (depends: scheduler); regenerate fixtures with XCE_REPL_BASELINE_REGEN=1 (script.txt is hand-written). Fixture sends the license value as a RESVALS_FULL event because shared resvals in the snapshot are dropped on load (bug-1923). sim_output.replay has no session decisions (set output switches files before session 1 is finalized); decisions are covered by set_output.replay and sim_verify.txt. +- 1.3: `sched_trace_ctl_wants` whole-array selectors match elements only, not the bare key. A bare id in `trace on job ` can't be told apart from an array base for a job that doesn't exist yet, so 4.3's parser must emit both `{key,false}` and `{key,true}`, or the matcher must also accept `jobid == key`. +- 1.3: design's record header lists a "file tag", but `xce_sched_trace_ctl_t` has no such field; the 4.2/4.6 store/emit tasks must set it or drop it explicitly. +- 1.3: trace sources (sched_trace.c, sched_trace_capture.c) may not include (core isolation test); capture copies raw strings/numbers and leaves formatting to the renderer. diff --git a/src/scheduler/include/sched/trace.h b/src/scheduler/include/sched/trace.h new file mode 100644 index 00000000..3d4cd349 --- /dev/null +++ b/src/scheduler/include/sched/trace.h @@ -0,0 +1,206 @@ +#if !defined(_SCHED_TRACE_H_) + +#define _SCHED_TRACE_H_ 1 + +/* + * trace.h - scheduling decision trace model (scheduler-sim-decision-trace + * spec, design.md "TraceModel"). + * + * A trace record explains one allocator decision for one traced job. It is + * built on the allocator thread while the job is allocated, handed to the + * simulator REPL on the decision, and rendered there. The record is + * self-contained: every string and number it holds is copied into its own + * per-record arena, so it never points into scheduler memory and stays + * valid after the job, its schedobj or the core state it was captured from + * change or go away. + * + * Only the simulator REPL ever publishes a control block (`trace_ctl`), so + * the live scheduler never builds a record (Requirement 11.2). + * + * The model (src/sched_trace.c) and the capture helpers + * (src/sched_trace_capture.c) do no I/O and no logging; test_core_isolation.c + * scans both files for stdio and file I/O. + */ + +#include +#include +#include + +/* Requirement 1.8: two detail levels; summary is the default. */ +typedef enum { + XCE_TRACE_LEVEL_SUMMARY = 0, + XCE_TRACE_LEVEL_FULL +} xce_sched_trace_level_t; + +/* Requirement 5.10 verdict kinds, bit flags (a host may carry several). */ +typedef enum { + XCE_TRACE_V_ACCEPTED = 1u << 0, + XCE_TRACE_V_SELECT = 1u << 1, /* selection mismatch (pre-filter) */ + XCE_TRACE_V_SHORTFALL = 1u << 2, /* resource shortfall (per-host check) */ + XCE_TRACE_V_LIMIT = 1u << 3, /* limit exceeded (per-host check) */ + XCE_TRACE_V_CLAIMED = 1u << 4, /* already claimed by earlier position */ + XCE_TRACE_V_INTERNAL = 1u << 5 /* not evaluated: internal error */ +} xce_sched_trace_verdict_t; + +/* Number of verdict kinds above: the size of every per-kind counter array, + * indexed by the kind's bit position. */ +#define XCE_TRACE_V_NKINDS 6 + +/* Requirement 2.4 / 2.5 record kinds. */ +typedef enum { + XCE_TRACE_REC_DECISION = 0, + XCE_TRACE_REC_NOT_FOUND, + XCE_TRACE_REC_NO_SCHEDOBJ, + XCE_TRACE_REC_INTERNAL_ERROR, /* alloc_rc < 0 or decision build failure */ + XCE_TRACE_REC_PEND_NO_REASON, + XCE_TRACE_REC_RECONSTRUCTED +} xce_sched_trace_rec_kind_t; + +/* One selector from `trace on job ...` (Requirements 1.2, 1.3). */ +typedef struct xce_sched_trace_sel_s { + uint64_t key; /* plain jobid, array base id, or element id */ + bool whole_array; /* true: key is an array base, matches every element */ +} xce_sched_trace_sel_t; + +/* + * Settings snapshot published to the core for one session. Immutable during + * the session except `position`, which only the allocator thread writes. + * REPL-owned; a record copies what it needs and never keeps a pointer here. + */ +typedef struct xce_sched_trace_ctl_s { + bool all; + int nsel; + xce_sched_trace_sel_t* sel; + xce_sched_trace_level_t level; + int line_bound; /* 50..100000 */ + uint64_t session_no; /* rs->session_count of this session */ + const char* policy_name; /* picker name, owned by the ctl */ + int position; /* allocator-owned, reset at session begin */ +} xce_sched_trace_ctl_t; + +typedef struct xce_sched_trace_rec_s xce_sched_trace_rec_t; + +#define XCE_TRACE_LINE_BOUND_DEFAULT 1000 /* Requirement 9.1 */ +#define XCE_TRACE_LINE_BOUND_MIN 50 +#define XCE_TRACE_LINE_BOUND_MAX 100000 +#define XCE_TRACE_OUTCOME_HOSTS 50 /* Requirement 9.4 */ + +typedef enum { + XCE_TRACE_PATH_SINGLE, /* PRIMARY job: the single requirement */ + XCE_TRACE_PATH_COMPOUND, /* top-level COMPOUND container */ + XCE_TRACE_PATH_ALT, /* alternative i/n of an ALTER (a compound) */ + XCE_TRACE_PATH_MEMBER /* member j/m of the enclosing compound */ +} xce_sched_trace_path_kind_t; + +/* Kept inside the record, not passed by hooks. */ +typedef struct { + int depth; /* 0..2: container (COMPOUND|ALT) > MEMBER, or SINGLE */ + struct { + xce_sched_trace_path_kind_t kind; + int i; + int n; + } lvl[2]; +} sched_trace_path_t; + +/* Requirement 4.7 reasons. */ +typedef enum { + XCE_TRACE_NOPLAN_DECLINED, /* allocator accepts() declined */ + XCE_TRACE_NOPLAN_GENERATOR_FAILED, /* plan_init failed */ + XCE_TRACE_NOPLAN_NONE_GENERATED /* generator produced no plan */ +} xce_sched_trace_noplan_t; + +typedef enum { + XCE_TRACE_POS_FILLED, + XCE_TRACE_POS_UNFILLED, + XCE_TRACE_POS_SCAN_ERROR, /* host scan could not start or stopped (4.4) */ + XCE_TRACE_POS_INTERNAL /* host accepted but claim snapshot failed */ +} xce_sched_trace_pos_end_t; + +/* + * Decision-wide counters (Requirement 9.3). They stay exact when the line + * budget truncates the stored entries. + */ +typedef struct { + int plans_tried; + int plans_discarded; + int reqs_none_generated; + int reqs_all_discarded; + int reqs_none_placed; + int host_evals[XCE_TRACE_V_NKINDS]; /* indexed by verdict bit position */ + int omitted_lines; +} sched_trace_counters_t; + +/* One arena chunk; the record frees the whole chain at once. */ +typedef struct sched_trace_chunk_s { + struct sched_trace_chunk_s* next; + size_t cap; + size_t used; + /* followed by `cap` bytes of storage */ +} sched_trace_chunk_t; + +/* + * Record layout. Read only by sched_trace*.c and the REPL renderer; every + * other file treats the record as opaque. All pointers point into the + * record's own arena. + */ +struct xce_sched_trace_rec_s { + /* header */ + uint64_t jobid; + xce_sched_trace_rec_kind_t kind; + int err; /* no-decision kinds: the error, else 0 */ + xce_sched_trace_level_t level; + uint64_t session_no; + int position; + int line_bound; + bool incomplete; /* arena growth failed: appending stopped */ + + /* selection block */ + const char* policy_name; /* arena copy of ctl->policy_name, or NULL */ + + /* current requirement path (pushed and popped by the allocation chain) */ + sched_trace_path_t path; + + sched_trace_counters_t counters; + + /* per-record arena */ + sched_trace_chunk_t* arena; + size_t arena_bytes; /* sum of chunk sizes, headers included */ +}; + +/* True iff `jobid` is selected by `ctl`: `all`, an exact plain-job or + * element selector, or a whole-array selector whose array the element + * belongs to (also for elements created after the command). NULL ctl: + * false. */ +extern bool sched_trace_ctl_wants(const xce_sched_trace_ctl_t* ctl, + uint64_t jobid); + +/* New record for `jobid` with the header copied from `ctl`. NULL when `ctl` + * is NULL or allocation fails (the job is then not traced). */ +extern xce_sched_trace_rec_t* +sched_trace_rec_new(const xce_sched_trace_ctl_t* ctl, uint64_t jobid, + xce_sched_trace_rec_kind_t kind); + +/* Frees the record and its arena. NULL-safe. */ +extern void sched_trace_rec_free(xce_sched_trace_rec_t* rec); + +/* Finish a record as a no-decision kind (NOT_FOUND, NO_SCHEDOBJ, + * INTERNAL_ERROR, PEND_NO_REASON) before it is pushed via + * make_trace_decision (2.4). Any other kind leaves the record unchanged. */ +extern void sched_trace_rec_finish(xce_sched_trace_rec_t* rec, + xce_sched_trace_rec_kind_t kind, int err); + +/* Bytes owned by the record: the record block plus its arena. 0 for NULL. */ +extern size_t sched_trace_rec_bytes(const xce_sched_trace_rec_t* rec); + +/* + * Arena copies. Each returns storage owned by the record, freed with it. + * On allocation failure they set rec->incomplete and return NULL. A NULL + * source (or rec) returns NULL without marking the record incomplete. + */ +extern void* sched_trace_arena_alloc(xce_sched_trace_rec_t* rec, size_t n); +extern void* sched_trace_arena_memdup(xce_sched_trace_rec_t* rec, + const void* src, size_t n); +extern char* sched_trace_arena_strdup(xce_sched_trace_rec_t* rec, + const char* s); + +#endif diff --git a/src/scheduler/meson.build b/src/scheduler/meson.build index 9895ff9b..68b87af2 100644 --- a/src/scheduler/meson.build +++ b/src/scheduler/meson.build @@ -35,6 +35,8 @@ scheduler_srcs = [ 'src/alloc_cache.c', 'src/host_iter.c', 'src/alloc_runner.c', + 'src/sched_trace.c', + 'src/sched_trace_capture.c', 'src/plugins/fcfs.c', 'src/plugins/builtin_fcfs.c', 'src/plugins/seqjob_allocator.c', diff --git a/src/scheduler/src/sched_trace.c b/src/scheduler/src/sched_trace.c new file mode 100644 index 00000000..39aad53e --- /dev/null +++ b/src/scheduler/src/sched_trace.c @@ -0,0 +1,231 @@ +/* + * sched_trace.c - decision trace record model (scheduler-sim-decision-trace + * spec, design.md "TraceModel"). + * + * Owns the trace record: its header (copied from the session's settings + * snapshot), the job selection predicate and the per-record arena that + * holds every string and number the record keeps, so a record never + * points into scheduler memory (design "Invariants"). + * + * A record is built by exactly one thread at a time (the allocator, or the + * sim thread for a reconstructed record) and is immutable once attached to + * a decision, so nothing here locks. + * + * No I/O and no logging: test_core_isolation.c scans this file for stdio + * and file I/O. + */ +#include +#include + +#include "sched/trace.h" +#include "utils.h" + +/* Smallest chunk the arena allocates; a larger request gets a chunk of its + * own size. Small enough that an untruncated short record stays cheap. */ +#define SCHED_TRACE_CHUNK_MIN 4096 + +#define SCHED_TRACE_ALIGN _Alignof(max_align_t) + +static size_t align_up(size_t n) +{ + return (n + (SCHED_TRACE_ALIGN - 1)) & ~(SCHED_TRACE_ALIGN - 1); +} + +/* Header size rounded so the storage that follows is max-aligned. */ +static size_t chunk_hdr_size(void) +{ + return align_up(sizeof(sched_trace_chunk_t)); +} + +static unsigned char* chunk_data(sched_trace_chunk_t* c) +{ + return ((unsigned char*)c) + chunk_hdr_size(); +} + +bool sched_trace_ctl_wants(const xce_sched_trace_ctl_t* ctl, uint64_t jobid) +{ + int i; + + if (ctl == NULL) { + return false; + } + if (ctl->all) { + return true; + } + if (ctl->sel == NULL) { + return false; + } + + for (i = 0; i < ctl->nsel; i++) { + const xce_sched_trace_sel_t* s = &ctl->sel[i]; + + if (s->whole_array) { + /* every element of the array, including elements created + * after the selector was made (1.2, 1.3) */ + if (is_array_tag(jobid) + && (array_jobid(jobid) == array_jobid(s->key))) { + return true; + } + } else if (jobid == s->key) { + return true; + } + } + + return false; +} + +void* sched_trace_arena_alloc(xce_sched_trace_rec_t* rec, size_t n) +{ + sched_trace_chunk_t* c; + size_t need; + unsigned char* p; + + if (rec == NULL) { + return NULL; + } + + need = align_up((n == 0) ? 1 : n); + if (need < n) { /* overflow */ + rec->incomplete = true; + return NULL; + } + + c = rec->arena; + if ((c == NULL) || ((c->cap - c->used) < need)) { + size_t cap + = (need > SCHED_TRACE_CHUNK_MIN) ? need : SCHED_TRACE_CHUNK_MIN; + size_t total = chunk_hdr_size() + cap; + + if (total < cap) { /* overflow */ + rec->incomplete = true; + return NULL; + } + c = (sched_trace_chunk_t*)malloc(total); + if (c == NULL) { + rec->incomplete = true; + return NULL; + } + c->cap = cap; + c->used = 0; + c->next = rec->arena; + rec->arena = c; + rec->arena_bytes += total; + } + + p = chunk_data(c) + c->used; + c->used += need; + memset(p, 0, need); + return p; +} + +void* sched_trace_arena_memdup(xce_sched_trace_rec_t* rec, const void* src, + size_t n) +{ + void* p; + + if ((rec == NULL) || (src == NULL)) { + return NULL; + } + + p = sched_trace_arena_alloc(rec, n); + if (p == NULL) { + return NULL; + } + memcpy(p, src, n); + return p; +} + +char* sched_trace_arena_strdup(xce_sched_trace_rec_t* rec, const char* s) +{ + if ((rec == NULL) || (s == NULL)) { + return NULL; + } + + return (char*)sched_trace_arena_memdup(rec, s, strlen(s) + 1); +} + +xce_sched_trace_rec_t* sched_trace_rec_new(const xce_sched_trace_ctl_t* ctl, + uint64_t jobid, + xce_sched_trace_rec_kind_t kind) +{ + xce_sched_trace_rec_t* rec; + + if (ctl == NULL) { + return NULL; + } + + rec = (xce_sched_trace_rec_t*)calloc(1, sizeof(*rec)); + if (rec == NULL) { + return NULL; + } + + rec->jobid = jobid; + rec->kind = kind; + rec->err = 0; + rec->level = ctl->level; + rec->session_no = ctl->session_no; + rec->position = ctl->position; + rec->line_bound = ctl->line_bound; + + if (ctl->policy_name != NULL) { + rec->policy_name = sched_trace_arena_strdup(rec, ctl->policy_name); + if (rec->policy_name == NULL) { + /* the record itself could not be built: the job is not traced + * (design "Error Strategy") */ + sched_trace_rec_free(rec); + return NULL; + } + } + + return rec; +} + +void sched_trace_rec_free(xce_sched_trace_rec_t* rec) +{ + sched_trace_chunk_t* c; + + if (rec == NULL) { + return; + } + + c = rec->arena; + while (c != NULL) { + sched_trace_chunk_t* next = c->next; + + free(c); + c = next; + } + free(rec); +} + +void sched_trace_rec_finish(xce_sched_trace_rec_t* rec, + xce_sched_trace_rec_kind_t kind, int err) +{ + if (rec == NULL) { + return; + } + + switch (kind) { + case XCE_TRACE_REC_NOT_FOUND: + case XCE_TRACE_REC_NO_SCHEDOBJ: + case XCE_TRACE_REC_INTERNAL_ERROR: + case XCE_TRACE_REC_PEND_NO_REASON: + rec->kind = kind; + rec->err = err; + break; + case XCE_TRACE_REC_DECISION: + case XCE_TRACE_REC_RECONSTRUCTED: + default: + /* not a no-decision kind (2.4): the record is left unchanged */ + break; + } +} + +size_t sched_trace_rec_bytes(const xce_sched_trace_rec_t* rec) +{ + if (rec == NULL) { + return 0; + } + + return sizeof(*rec) + rec->arena_bytes; +} diff --git a/src/scheduler/src/sched_trace_capture.c b/src/scheduler/src/sched_trace_capture.c new file mode 100644 index 00000000..cfd6aed9 --- /dev/null +++ b/src/scheduler/src/sched_trace_capture.c @@ -0,0 +1,16 @@ +/* + * sched_trace_capture.c - allocator-side trace capture helpers + * (scheduler-sim-decision-trace spec, design.md "TraceCapture"). + * + * Will read scheduler state (job and queue keys, cluster-wide and host + * limits, resource flags, allocation and pend outcomes) on the thread that + * owns it and write it into a trace record through the model + * (sched_trace.c). Inputs are read-only: nothing here may call a scheduler + * API that mutates state (Requirement 10). + * + * The helpers are added by the capture tasks; this module is registered in + * the scheduler build and in test_core_isolation.c's source scan from the + * start, so the no-I/O rule (no stdio, no file I/O, no logging) applies to + * every line that lands here. + */ +#include "sched/trace.h" diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 0e760a75..0ab0937d 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -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']) diff --git a/src/scheduler/testsuite/test_core_isolation.c b/src/scheduler/testsuite/test_core_isolation.c index d01ca4f3..73069f8c 100644 --- a/src/scheduler/testsuite/test_core_isolation.c +++ b/src/scheduler/testsuite/test_core_isolation.c @@ -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[] = { + "", "", "", "", + "", "", "\"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(); } diff --git a/src/scheduler/testsuite/test_trace_model.c b/src/scheduler/testsuite/test_trace_model.c new file mode 100644 index 00000000..dd145b3b --- /dev/null +++ b/src/scheduler/testsuite/test_trace_model.c @@ -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 +#include + +#include + +#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(); +}