/* * 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 #include #include #include #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: ` =` 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); }