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

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