myxce/src/scheduler/testsuite/test_reason_ownership_audit.c
lei.chen 1fd6dc1c13 test(scheduler): lock reason-ownership threading contract (Phase 1)
Phase 1 of scheduler-hardening-plan.md: mechanically locks the bug-1521
contract that 91c3fb7f established but which lived only in code comments:
the allocator thread is the ONLY writer of a schedobj's reason fields;
the picker thread never touches reasons.

test_reason_ownership_audit.c (in the sched_unit_test aggregate) enforces
this in three layers:
- a whole-tree scan of every src/scheduler source (fixed 51-file list) for
  schedobj-shaped reason ownership lines (obj->reason / schedobj->reason /
  primary->reason / cobj->reason / compound->reason combined with
  MOVE_PTR( / free_PendReason_ptr( / calloc( / a bare = store), checked
  against an OWNERSHIP_SITES allowlist whose fragments must still exist in
  their files (so the list cannot rot silently);
- a dedicated scan of sched_job_picker()'s brace-matched function body
  rejecting ANY reason ownership inside the picker;
- detector self-tests proving it fires on the historical violation shapes
  and stays quiet on sanctioned reads, ctx->reason (the sched_ctx_t local
  shuttle), construction =NULL, and comment lines (skipped by the scans --
  the contract's own documentation names the write shape in prose at
  scheduler.c:496 / fcfs.c:243).

Red proof: a temporary MOVE_PTR(job->schedobj->reason, ...) injected into
the picker loop trips BOTH the file scan and the picker-body scan; the
injection is reverted and the suite is green.

.kiro/steering/scheduler-reason-ownership.md formalizes the discipline
(who allocates, who frees, which thread, MOVE_PTR semantics, the
ctx->reason exemption) for review use.

Full scheduler suite: 102/102 OK.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-30 04:56:34 +00:00

684 lines
28 KiB
C

/*
* test_reason_ownership_audit.c - Phase 1 of the scheduler rejoin-hardening
* plan (scheduler-hardening-plan.md): mechanically locks the bug-1521
* reason-ownership threading contract that commit 91c3fb7f established but
* which until now lived only in code comments and commit messages.
*
* THE CONTRACT (91c3fb7f, bug-1521): the picker thread (sched_job_picker(),
* sched_election.c) and the allocator thread (sched_allocator(), same file)
* run concurrently over the same xce_sched_job_t. The allocator thread is
* the ONLY writer of a schedobj's reason chain
* (obj->reason / obj->u.primary->reason / obj->u.compound->reason /
* job->schedobj->reason): the MOVE_PTR(obj->reason, ctx.reason) sites in
* scheduler.c's sched_make_primary_alloc_()/sched_make_compound_alloc_()
* and plugins/limit.c's limit_job_election() all execute on the allocator
* thread, while the picker only pushes jobids into the capacity-1 jbuf
* rendezvous. bug-1521's live heap corruption was the picker thread
* writing job->schedobj->reason racing the allocator thread's MOVE_PTR of
* the same field.
*
* WHAT THIS TEST LOCKS, in three layers (same genre as
* test_picker_pendreason_factory_audit.c's fixed-file-list same-line scan,
* which this file's scanner is modeled on):
*
* 1. OWNERSHIP-SITE CLOSURE: every line in the scanned scheduler tree that
* performs a reason OWNERSHIP operation on a schedobj field -- a
* MOVE_PTR into a `->reason`, a free_PendReason_ptr() of a `->reason`,
* or a calloc() constructing an xce_pendreason_t -- must be a line that
* ALSO names the site's thread ownership per the OWNERSHIP_SITES
* allowlist below. A new write site added anywhere else fails the scan
* with instructions: move it into the allocator thread's call path or,
* if genuinely new, update the allowlist WITH a thread-ownership note.
* (Same-line discipline, not whole-file co-occurrence, keeps this
* precise: scheduler.c/decision.c/etc. contain "calloc(" and
* "pendreason" elsewhere, never together on the one line that matters --
* the same finding test_picker_pendreason_factory_audit.c documented.)
*
* 2. PICKER-THREAD BAN (the bug-1521 shape): sched_job_picker()'s function
* body (extracted from sched_election.c by brace matching) must contain
* NO reason-ownership operation at all -- no `->reason` reference
* combined with MOVE_PTR/free_PendReason_ptr/calloc, and no assignment
* into any `->reason` field. The picker's sanctioned surface is:
* resolve the picker, pull a jobid from wbuf, run the session protocol,
* push jobids into jbuf. Everything reason-shaped belongs to the
* allocator thread.
*
* 3. DETECTOR SELF-TESTS: the scan predicates are proven to fire on the
* historical violation shapes (a picker-thread
* MOVE_PTR(job->schedobj->reason, ...), a direct xce_pendreason_t
* calloc, a bare `obj->reason = ...` assignment) and to stay quiet on
* sanctioned lines (a READ like sched_election.c:373's
* `= job->schedobj->reason` with no ownership op, a comment line, an
* unrelated struct's calloc) -- so a typo cannot make the real scans
* vacuously pass forever.
*
* KNOWN LIMITATIONS (inherited deliberately from the factory-audit model):
* a fixed file list that must be extended when files are added under
* src/scheduler (the readability test catches a stale path); a same-line
* textual detector, which misses a multi-line ownership expression but
* matches this codebase's clang-format style where a MOVE_PTR/free call
* and its `->reason` argument share a line; and comments are scanned too
* (a comment merely MENTIONING "MOVE_PTR" plus "->reason" on one line
* would false-positive -- today's comments are wrapped so this does not
* happen, and the failure message makes the cause obvious).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#ifndef XCE_SCHEDULER_ROOT_PATH
#error \
"XCE_SCHEDULER_ROOT_PATH must be defined by meson.build to the absolute path of src/scheduler"
#endif
#ifndef XCE_SCHED_ELECTION_C_PATH
#error \
"XCE_SCHED_ELECTION_C_PATH must be defined by meson.build to sched_election.c's absolute path"
#endif
/* Every non-testsuite .c file under src/scheduler (src/ and src/plugins/)
* that the ownership scan covers -- the same fixed-list discipline as
* test_picker_pendreason_factory_audit.c's SCANNED_RELATIVE_PATHS (that
* list's own header comment records why: no directory-walk dependency for
* a rarely-changing tree; add files here as they're created). This list is
* the FULL scheduler_srcs set minus main.c, derived from src/scheduler/
* meson.build's scheduler_srcs at the time of writing -- main.c is a
* startup/round-loop file that never touches schedobj reasons, but is
* listed anyway so a future edit there is still covered by the scan (and
* the readability test proves the path stays valid). */
static const char* const SCANNED_RELATIVE_PATHS[] = {
"src/cli_compat.c",
"src/handler.c",
"src/scheduler.c",
"src/sched_election.c",
"src/core.c",
"src/core_events.c",
"src/core_session.c",
"src/core_clock.c",
"src/driver.c",
"src/snapshot.c",
"src/snapshot_wire.c",
"src/snapshot_load.c",
"src/decision.c",
"src/recording.c",
"src/recording_events.c",
"src/recording_session.c",
"src/sim.c",
"src/sim_loop.c",
"src/sim_picker.c",
"src/sim_limit.c",
"src/sim_alloc.c",
"src/sim_verify.c",
"src/sim_record.c",
"src/repl.c",
"src/repl_fmt.c",
"src/repl_rollup.c",
"src/repl_break.c",
"src/repl_info.c",
"src/repl_params.c",
"src/repl_trace_render.c",
"src/repl_trace_render_outcome.c",
"src/repl_trace_store.c",
"src/repl_trace.c",
"src/repl_trace_emit.c",
"src/schedobj.c",
"src/plugin.c",
"src/resource.c",
"src/alloc_cache.c",
"src/host_iter.c",
"src/alloc_runner.c",
"src/sched_trace.c",
"src/sched_trace_scan.c",
"src/sched_trace_capture.c",
"src/sched_trace_outcome.c",
"src/plugins/fcfs.c",
"src/plugins/builtin_fcfs.c",
"src/plugins/seqjob_allocator.c",
"src/plugins/parjob_allocator.c",
"src/plugins/limit.c",
"src/plugins/limit_config.c",
};
#define NUM_SCANNED \
(sizeof(SCANNED_RELATIVE_PATHS) / sizeof(SCANNED_RELATIVE_PATHS[0]))
/* --------------------------------------------------------------------------
* Scanner primitives (shared shape with test_picker_pendreason_factory_
* audit.c: whole-file read, per-line predicate, hard-fail on unreadable)
* -------------------------------------------------------------------------- */
static char* read_whole_file(const char* path)
{
FILE* f = fopen(path, "rb");
if (f == NULL) {
return NULL;
}
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return NULL;
}
long len = ftell(f);
if (len < 0) {
fclose(f);
return NULL;
}
rewind(f);
char* buf = (char*)malloc((size_t)len + 1);
if (buf == NULL) {
fclose(f);
return NULL;
}
size_t nread = fread(buf, 1, (size_t)len, f);
fclose(f);
buf[nread] = '\0';
return buf;
}
/* Extract the body of the C function whose definition starts with `sig`
* (a prefix of the definition line, e.g. "static void* sched_job_picker").
* Returns a malloc'd copy of the text from the function's opening '{' to
* its matching closing '}' (inclusive), or NULL if the signature or a
* balanced brace pair cannot be found. Brace matching ignores braces
* inside double-quoted strings and char literals and after '//' to the end
* of a line -- enough for this codebase's formatting; comment-block
* braces ({/* ... *}/) are handled by also skipping /*...*\/ spans. */
static char* extract_function_body(const char* src, const char* sig)
{
const char* defn = strstr(src, sig);
if (defn == NULL) {
return NULL;
}
const char* brace = strchr(defn, '{');
if (brace == NULL) {
return NULL;
}
int depth = 0;
const char* p = brace;
while (*p != '\0') {
if (*p == '"') { /* skip string literal */
p++;
while ((*p != '"') && (*p != '\0')) {
if ((*p == '\\') && (*(p + 1) != '\0')) {
p++;
}
p++;
}
} else if ((*p == '\'') && (*(p + 1) != '\0')) { /* char literal */
p += 2;
while ((*p != '\'') && (*p != '\0')) {
p++;
}
} else if ((*p == '/') && (*(p + 1) == '/')) { /* line comment */
while ((*p != '\n') && (*p != '\0')) {
p++;
}
} else if ((*p == '/') && (*(p + 1) == '*')) { /* block comment */
p += 2;
while ((*p != '\0')
&& !((*p == '*') && (*(p + 1) == '/'))) {
p++;
}
if (*p != '\0') {
p++;
}
} else if (*p == '{') {
depth++;
} else if (*p == '}') {
depth--;
if (depth == 0) {
size_t len = (size_t)(p - brace) + 1;
char* body = (char*)malloc(len + 1);
if (body == NULL) {
return NULL;
}
memcpy(body, brace, len);
body[len] = '\0';
return body;
}
}
if (*p == '\0') {
break;
}
p++;
}
return NULL;
}
/* --------------------------------------------------------------------------
* The detector
* -------------------------------------------------------------------------- */
/* line_has_reason_ownership - a single source line performs a reason
* OWNERSHIP operation on a SCHEDOBJ reason field if it contains a
* schedobj-shaped reason lvalue together with one of the ownership verbs,
* in any order on the line:
* MOVE_PTR( -- ownership transfer INTO the field (the bug-1521
* racing write shape)
* free_PendReason_ptr( -- ownership release OF the field
* calloc( -- construction of a new xce_pendreason_t being aimed
* at the field (factory bypass; the factory-audit
* test separately catches calloc+pendreason_t lines)
* A bare `obj->reason = ...` pointer-store is ALSO ownership (the third
* historical shape) and is caught by assignment detection below.
*
* SCHEDOBJ-SHAPED means the lvalue's object is a schedobj (or its
* primary/compound/alter sub-structs): `obj->reason`,
* `job->schedobj->reason`, `primary->reason`, `cobj->reason`,
* `compound->reason`. The scanner approximates this by REQUIRING one of
* those exact bases on the line -- `ctx->reason`/`ctx.reason` (the
* sched_ctx_t LOCAL shuttle field that sched_ctx_init() owns and
* sched_ctx_fini() releases; the alloc chain builds into it, then the
* sanctioned MOVE_PTR hands it to the schedobj) is a deliberate
* NON-target: its assignments (alloc_runner.c:48, scheduler.c:186,
* resource.c:570/587) are ctx construction, thread-confined by the alloc
* chain's own single-thread shape, and never shared with the picker.
* The sanctioned MOVE_PTR/free lines themselves carry `obj->reason`, so
* they match the schedobj-shape requirement. If a future site moves a
* reason with a new base name, the scan going red HERE is the contract
* working: the base must be added to the detector or the site justified.
*
* Deliberately NOT an ownership op: a plain READ of ->reason (the
* allocator thread reads job->schedobj->reason at sched_election.c:373 to
* wrap it in a pend decision -- that is the sanctioned consumer shape).
* Line-level discipline (both substrings on ONE line) keeps comments that
* merely discuss the contract from firing as long as they wrap, the same
* trade the factory audit documented and accepted. */
static int line_has_reason_ownership(const char* line)
{
/* schedobj-shaped reason lvalues only (see block comment): */
const char* bases[] = {"obj->reason",
"schedobj->reason",
"primary->reason",
"cobj->reason",
"compound->reason"};
const int n_bases = (int)(sizeof(bases) / sizeof(bases[0]));
const char* r = NULL;
int i;
for (i = 0; i < n_bases; i++) {
r = strstr(line, bases[i]);
if (r != NULL) {
break;
}
}
if (r == NULL) {
return 0;
}
if ((strstr(line, "MOVE_PTR(") != NULL)
|| (strstr(line, "free_PendReason_ptr(") != NULL)
|| (strstr(line, "calloc(") != NULL)) {
return 1;
}
/* bare pointer-store detection: `<base> =` where the '=' is a real
* assignment (not ==, !=, <=, >=, +=, -=). */
{
const char* p = r + strlen(bases[i]);
while ((*p == ' ') || (*p == '\t')) {
p++;
}
if (*p == '=' && (*(p + 1) != '=')) {
return 1;
}
}
return 0;
}
/* A line that MENTIONS an ownership verb but has no schedobj reason
* lvalue is fine (e.g. MOVE_PTR(job->allocs, ...) at sched_election.c:388
* -- allocs, not reason). The helper below is only for the self-test's
* readability. */
static int line_mentions_verb_but_not_reason(const char* line)
{
return ((strstr(line, "MOVE_PTR(") != NULL)
|| (strstr(line, "free_PendReason_ptr(") != NULL))
&& (strstr(line, "obj->reason") == NULL)
&& (strstr(line, "schedobj->reason") == NULL);
}
/* The allowlist: lines performing reason ownership that the contract
* SANCTIONS, matched by (file-relative-path, normalized-content-fragment).
* Each entry carries its thread ownership in the comment -- that note is
* the point of the list: a new site must either join an existing thread
* context or justify a new one HERE, in review, before the scan goes
* green again.
*
* Today's complete inventory (verified 2026-10-30 by scanning the whole
* scheduler tree for ->reason / .reason):
* scheduler.c:338-339 free+MOVE_PTR -- ALLOCATOR thread
* (sched_make_primary_alloc_ <- sched_make_primary_allocation <-
* sched_make_allocation <- sched_allocator, sched_election.c)
* scheduler.c:429-430 free+MOVE_PTR -- ALLOCATOR thread
* (sched_make_compound_alloc_, same call chain)
* plugins/limit.c:296 MOVE_PTR -- ALLOCATOR thread
* (limit_job_election <- plugin_job_election <- sched_allocator /
* sim_loop.c's reconstruction loop, both allocator-thread shapes;
* dead-by-construction today -- match_job_global_limit_() never
* attaches a reason -- kept as the hook's historical carry-shape)
* core.c:236/242/252/263 free_PendReason_ptr -- DESTRUCTION path
* (xce_sched_core_free_schedobj / kill chain; no concurrent writer
* can race it because the object is being torn down)
* core_events.c:140 / schedobj.c:57 `= NULL` -- CONSTRUCTION
* (calloc-initialized then explicitly NULLed; no reader yet exists)
* sched_election.c:373 READ only -- allocator thread, sanctioned
* consumer (fetched to build the pend decision)
* plugins/fcfs.c:242-248 comment only -- the picker-thread ban note
*/
typedef struct {
const char* path; /* relative to XCE_SCHEDULER_ROOT_PATH */
const char* fragment; /* a substring that must appear on the line */
} sanctioned_site_t;
static const sanctioned_site_t OWNERSHIP_SITES[] = {
/* scheduler.c -- both alloc-chain sites, ALLOCATOR thread */
{"src/scheduler.c", "MOVE_PTR(obj->reason, ctx.reason)"},
{"src/scheduler.c", "free_PendReason_ptr(&(obj->reason))"},
/* plugins/limit.c -- election hook carry-shape, ALLOCATOR thread */
{"src/plugins/limit.c", "MOVE_PTR(obj->reason, ctx.reason)"},
/* core.c -- destruction path */
{"src/core.c", "free_PendReason_ptr(&(obj->u.primary->reason))"},
{"src/core.c", "free_PendReason_ptr(&(obj->u.compound->reason))"},
{"src/core.c", "free_PendReason_ptr(&(cobj->reason))"},
{"src/core.c", "free_PendReason_ptr(&(obj->reason))"},
/* construction: calloc'd then explicitly NULLed; no reader exists
* yet, so no concurrent-writer hazard (core_events.c:140,
* schedobj.c:57) */
{"src/core_events.c", "primary->reason = NULL"},
{"src/schedobj.c", "primary->reason = NULL"},
};
#define NUM_SITES (sizeof(OWNERSHIP_SITES) / sizeof(OWNERSHIP_SITES[0]))
/* Is this (file, line) sanctioned? A line is sanctioned if its file is in
* the allowlist AND the line contains one of that file's fragments. */
static int line_is_sanctioned(const char* rel_path, const char* line)
{
size_t i;
for (i = 0; i < NUM_SITES; i++) {
if (strcmp(OWNERSHIP_SITES[i].path, rel_path) == 0) {
/* Allowing any sanctioned fragment on any line of the file is
* deliberately file-grained, not line-grained: line numbers
* drift with every edit and would rot the allowlist into a
* maintenance tax. The fragments are specific enough (the
* exact lvalue expression) that a DIFFERENT reason write in
* the same file -- the thing the scan exists to catch -- does
* not match any of them. */
if (strstr(line, OWNERSHIP_SITES[i].fragment) != NULL) {
return 1;
}
}
}
return 0;
}
/* is_comment_line - a line whose first non-whitespace character opens or
* continues a comment (`*`, `/*`, `//`). Comment lines are SKIPPED by the
* scans: this codebase's contract documentation itself lives in comments
* that name the write shape ("cannot race MOVE_PTR(obj->reason, ...)" at
* scheduler.c:496, fcfs.c's picker-ban note, sched_election.c's
* bug-1521 paragraph), and clang-format keeps every comment line's
* leading marker as the first non-blank character, so a leading-character
* test is exact for this tree. (The scanner does not attempt full
* block-comment state tracking -- trailing inline comments after real
* code are NOT skipped, which only risks a false POSITIVE on a code line
* whose trailing comment repeats the shape, not a false negative, and no
* such line exists today.) */
static int is_comment_line(const char* line)
{
while ((*line == ' ') || (*line == '\t')) {
line++;
}
return (*line == '*' || (line[0] == '/' && line[1] == '*')
|| (line[0] == '/' && line[1] == '/'));
}
/* scan_file -- returns 1 if an unsanctioned reason-ownership line was
* found (copied to out), 0 if clean, -1 if unreadable (hard failure). */
static int scan_file(const char* root, const char* rel, char* out,
size_t out_sz)
{
char path[4096];
char* text;
char* saveptr = NULL;
char* line;
int found = 0;
snprintf(path, sizeof(path), "%s/%s", root, rel);
text = read_whole_file(path);
if (text == NULL) {
return -1;
}
line = strtok_r(text, "\n", &saveptr);
while (line != NULL) {
if (!is_comment_line(line) && line_has_reason_ownership(line)
&& !line_is_sanctioned(rel, line)) {
found = 1;
if ((out != NULL) && (out_sz > 0)) {
snprintf(out, out_sz, "%s", line);
}
break;
}
line = strtok_r(NULL, "\n", &saveptr);
}
free(text);
return found;
}
/* --------------------------------------------------------------------------
* Self-tests: the detector must fire on the violation shapes and stay
* quiet on the sanctioned shapes.
* -------------------------------------------------------------------------- */
static void test_detector_fires_on_violation_shapes(void)
{
/* bug-1521's exact shape: a picker-thread write racing the allocator */
TEST_ASSERT_TRUE(line_has_reason_ownership(
" MOVE_PTR(job->schedobj->reason, ctx.reason);"));
/* ownership release */
TEST_ASSERT_TRUE(line_has_reason_ownership(
" free_PendReason_ptr(&(obj->u.primary->reason));"));
/* factory bypass aimed at the field */
TEST_ASSERT_TRUE(line_has_reason_ownership(
" obj->reason = (xce_pendreason_t*)calloc(1, sizeof(*r0));"));
/* bare pointer-store */
TEST_ASSERT_TRUE(line_has_reason_ownership(
" obj->reason = other;"));
/* calloc-first line that also carries a schedobj reason lvalue */
TEST_ASSERT_TRUE(line_has_reason_ownership(
" xce_pendreason_t* r1 = calloc(1, sizeof(xce_pendreason_t));"
" obj->reason = r1;"));
/* the compound/alter sub-struct bases are also targets */
TEST_ASSERT_TRUE(line_has_reason_ownership(
" free_PendReason_ptr(&(cobj->reason));"));
}
static void test_detector_quiet_on_sanctioned_shapes(void)
{
/* the sanctioned allocator-thread READ (sched_election.c:373) --
* pointer READ with no ownership verb and no bare store */
TEST_ASSERT_FALSE(line_has_reason_ownership(
" xce_pendreason_t* reason = job->schedobj->reason;"));
/* ctx->reason assignments: the sched_ctx_t LOCAL shuttle field, not a
* schedobj field (see line_has_reason_ownership's block comment) */
TEST_ASSERT_FALSE(line_has_reason_ownership(
" ctx->reason = make_pendreason(0);"));
TEST_ASSERT_FALSE(line_has_reason_ownership(
" if (ctx->reason != NULL) {"));
/* a MOVE_PTR of a DIFFERENT field (allocs) -- no schedobj reason
* lvalue on the line */
TEST_ASSERT_TRUE(line_mentions_verb_but_not_reason(
" MOVE_PTR(job->allocs, job->schedobj->alloc);"));
TEST_ASSERT_FALSE(line_has_reason_ownership(
" MOVE_PTR(job->allocs, job->schedobj->alloc);"));
/* comments/discussion lines: comment lines are skipped by the SCANS
* (is_comment_line), and lines whose substrings never share a line
* are quiet for the detector itself */
TEST_ASSERT_FALSE(line_has_reason_ownership(
" * writing job->schedobj->reason races this thread's"));
/* a comment line that DOES carry both substrings on one line (the
* exact scheduler.c:496 / fcfs.c:243 shape) fires the raw detector --
* which is precisely why the scans skip comment lines */
TEST_ASSERT_TRUE(line_has_reason_ownership(
" * MOVE_PTR(obj->reason, ...) (bug-1521 heap corruption)."));
TEST_ASSERT_TRUE(is_comment_line(
" * MOVE_PTR(obj->reason, ...) (bug-1521 heap corruption)."));
TEST_ASSERT_TRUE(is_comment_line(" /* trailing open"));
TEST_ASSERT_TRUE(is_comment_line(" // line comment"));
TEST_ASSERT_FALSE(is_comment_line(
" MOVE_PTR(job->allocs, job->schedobj->alloc);"));
/* pend decision construction on a fresh reason (no schedobj reason
* lvalue; decision->u.reason is the decision's own field, written by
* make_pend_decision's single factory in decision.c) */
TEST_ASSERT_FALSE(line_has_reason_ownership(
" decision->u.reason = reason;"));
}
static void test_sanctioned_table_matches_live_sites(void)
{
/* Every allowlisted fragment must actually be found in its file --
* this catches the allowlist itself rotting (a renamed lvalue, a
* moved site) and, symmetrically, guarantees each inventory entry
* still exists in the shape the table claims. */
size_t i;
for (i = 0; i < NUM_SITES; i++) {
char path[4096];
char* text;
snprintf(path, sizeof(path), "%s/%s", XCE_SCHEDULER_ROOT_PATH,
OWNERSHIP_SITES[i].path);
text = read_whole_file(path);
TEST_ASSERT_NOT_NULL_MESSAGE(text, path);
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(text, OWNERSHIP_SITES[i].fragment),
OWNERSHIP_SITES[i].fragment);
free(text);
}
}
/* --------------------------------------------------------------------------
* The real scans
* -------------------------------------------------------------------------- */
static void test_all_scanned_paths_are_readable(void)
{
size_t i;
for (i = 0; i < NUM_SCANNED; i++) {
char path[4096];
char* text;
snprintf(path, sizeof(path), "%s/%s", XCE_SCHEDULER_ROOT_PATH,
SCANNED_RELATIVE_PATHS[i]);
text = read_whole_file(path);
TEST_ASSERT_NOT_NULL_MESSAGE(text, path);
free(text);
}
}
static void test_no_unsanctioned_reason_ownership_in_scheduler_tree(void)
{
size_t i;
for (i = 0; i < NUM_SCANNED; i++) {
char path[4096];
char violating_line[512];
char msg[4096 + 512 + 256];
int rc;
snprintf(path, sizeof(path), "%s/%s", XCE_SCHEDULER_ROOT_PATH,
SCANNED_RELATIVE_PATHS[i]);
rc = scan_file(XCE_SCHEDULER_ROOT_PATH, SCANNED_RELATIVE_PATHS[i],
violating_line, sizeof(violating_line));
TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, rc, path);
if (rc == 1) {
snprintf(msg, sizeof(msg),
"%s contains an unsanctioned reason-ownership write "
"(bug-1521 contract): %s. Reason ownership belongs to "
"the ALLOCATOR thread only (scheduler.c's "
"sched_make_{primary,compound}_alloc_ and "
"plugins/limit.c's limit_job_election carry today's "
"sanctioned sites; core.c frees on destruction; "
"constructors NULL the field). Either move this write "
"into the allocator thread's call path, or -- if it "
"is genuinely a new sanctioned site -- add it to "
"OWNERSHIP_SITES in this test WITH a thread-ownership "
"note. Offending line: %s",
path, violating_line, violating_line);
TEST_FAIL_MESSAGE(msg);
}
}
}
/* The bug-1521 shape itself: the picker's function body must contain no
* reason-ownership operation. Extracted by brace matching from
* sched_election.c (XCE_SCHED_ELECTION_C_PATH), so a future edit that
* adds one fails here regardless of which line it landed on. Comment
* lines inside the body are skipped for the same reason the file scan
* skips them (sched_job_picker's body carries the bug-1521 discussion in
* comments? No -- the picker body itself is clean prose-free code today;
* the skip is future-proofing consistent with the file scan). */
static void test_picker_body_has_no_reason_ownership(void)
{
char* src = read_whole_file(XCE_SCHED_ELECTION_C_PATH);
char* body = NULL;
char* saveptr = NULL;
char* line;
int violation = 0;
char violating_line[512];
TEST_ASSERT_NOT_NULL_MESSAGE(src,
"could not read sched_election.c for scan");
body = extract_function_body(src, "void* sched_job_picker(");
TEST_ASSERT_NOT_NULL_MESSAGE(
body, "sched_job_picker() definition not found in sched_election.c");
line = strtok_r(body, "\n", &saveptr);
while (line != NULL) {
if (!is_comment_line(line) && line_has_reason_ownership(line)) {
violation = 1;
snprintf(violating_line, sizeof(violating_line), "%s", line);
break;
}
line = strtok_r(NULL, "\n", &saveptr);
}
free(body);
free(src);
TEST_ASSERT_FALSE_MESSAGE(
violation, violating_line[0] != '\0' ? violating_line
: "(line unavailable)");
}
/* --------------------------------------------------------------------------
* Suite entry for the sched_unit_test aggregate
* -------------------------------------------------------------------------- */
void test_sched_reason_ownership_audit(void)
{
UnitySetTestFile(__FILE__);
RUN_TEST(test_detector_fires_on_violation_shapes);
RUN_TEST(test_detector_quiet_on_sanctioned_shapes);
RUN_TEST(test_sanctioned_table_matches_live_sites);
RUN_TEST(test_all_scanned_paths_are_readable);
RUN_TEST(test_no_unsanctioned_reason_ownership_in_scheduler_tree);
RUN_TEST(test_picker_body_has_no_reason_ownership);
}