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

@ -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 <id>` 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 <stdio.h> (core isolation test); capture copies raw strings/numbers and leaves formatting to the renderer.

View file

@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/* 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

View file

@ -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',

View file

@ -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 <stdlib.h>
#include <string.h>
#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;
}

View file

@ -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"

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