Adds xce_core_go()/xce_core_end_session() (new core_session.c): xce_core_go() is non-blocking, performs begin-session state reset (select-eval-ctx, resval resets), signals the picker thread via wbuf, and returns -EINVAL on a reentrant call while a session is already active (proven a true no-op, not just an early return). Draining core->decisions and invoking the driver's on_decision callback is the driver's own responsibility between core_go() and core_end_session() (confirmed driver-owned, not core-owned, per the Driver <-> Core protocol). xce_core_set_decision_cb() registers the callback + driver context as a separate setter, keeping xce_sched_core_init()'s live sched_init() call site untouched. Adds XCE_DECISION_PREEMPT to decision.h (jobid/victims[]/reason) alongside the existing DISPATCH/ PEND types. Additive only — sched_go/sched_begin_session/ sched_end_session/sched_allocator/main.c remain untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
242 lines
8.9 KiB
C
242 lines
8.9 KiB
C
/*
|
|
* test_picker_pendreason_factory_audit.c - regression guard for Req 6 AC 2
|
|
* (scheduler-core spec, task 2.6): "All pend reason allocation shall go
|
|
* through make_pendreason() (the single factory in lib/preason.c). Callers
|
|
* never calloc(sizeof(xce_pendreason_t)) directly."
|
|
*
|
|
* Task 2.6 is primarily an audit, not new behavior. A one-off `grep -rn`
|
|
* during that audit confirmed the invariant currently holds (the only
|
|
* calloc() constructing an xce_pendreason_t anywhere in src/ is
|
|
* lib/preason.c:164, inside make_pendreason() itself, which lives outside
|
|
* src/scheduler entirely). This file turns that one-off finding into a
|
|
* durable, re-runnable check per the task's Observable ("a grep-based or
|
|
* code-review check confirms no new calloc(sizeof(...pendreason...)) call
|
|
* site was introduced") -- a future task that adds a picker, allocator, or
|
|
* handler path under src/scheduler and forgets make_pendreason() fails this
|
|
* test immediately instead of relying on someone re-running the grep by
|
|
* hand.
|
|
*
|
|
* Why a same-line check, not whole-file substring co-occurrence: verified
|
|
* during audit that src/decision.c and src/scheduler.c each contain BOTH
|
|
* "calloc(" and "pendreason_t" somewhere in the file, but never on the same
|
|
* line (decision.c:6 declares a `xce_pendreason_t* reason` parameter,
|
|
* decision.c:10/24 calloc a *different* struct, `xce_decision_t`;
|
|
* scheduler.c:350 reads an existing `job->schedobj->reason` pointer,
|
|
* scheduler.c's five calloc() calls all construct other struct types). A
|
|
* whole-file check would false-positive on both files. Flagging only a
|
|
* line that contains both substrings together is the same signal a human
|
|
* reviewer eyeballing `grep -n calloc` output would use, and matches every
|
|
* real violation shape this Observable cares about: `(xce_pendreason_t*)
|
|
* calloc(...)` or `calloc(1, sizeof(xce_pendreason_t))` collapse the type
|
|
* name and the allocation call onto one line by construction.
|
|
*
|
|
* Why a fixed file list, not a directory walk: mirrors
|
|
* test_picker_core_loop_boundary.c's own acknowledged limitation (a single
|
|
* baked-in function-signature anchor, not a fully generic scan) rather than
|
|
* adding a recursive-directory-walk dependency (nftw/opendir) for a
|
|
* 14-file, rarely-changing tree. SCANNED_RELATIVE_PATHS below is every
|
|
* non-testsuite .c/.h file under src/scheduler as of task 2.6 (verified via
|
|
* `find src/scheduler -name '*.c' -o -name '*.h' | grep -v testsuite`) --
|
|
* add new files here as they're created, the same maintenance burden
|
|
* test_picker_core_loop_boundary.c's FUNC_ANCHOR already carries.
|
|
*
|
|
* make_pendreason() itself is never allowlisted here because it lives in
|
|
* lib/preason.c, outside XCE_SCHEDULER_ROOT_PATH entirely -- the scanned
|
|
* tree structurally cannot see the one legitimate call site.
|
|
*/
|
|
#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
|
|
|
|
static const char* const SCANNED_RELATIVE_PATHS[] = {
|
|
"include/core.h",
|
|
"include/decision.h",
|
|
"include/plugin.h",
|
|
"include/scheduler.h",
|
|
"include/sched/types.h",
|
|
"src/core.c",
|
|
"src/core_session.c",
|
|
"src/decision.c",
|
|
"src/handler.c",
|
|
"src/main.c",
|
|
"src/plugin.c",
|
|
"src/plugins/builtin_fcfs.c",
|
|
"src/plugins/fcfs.c",
|
|
"src/plugins/limit.c",
|
|
"src/plugins/parjob_allocator.c",
|
|
"src/plugins/seqjob_allocator.c",
|
|
"src/resource.c",
|
|
"src/schedobj.c",
|
|
"src/scheduler.c",
|
|
};
|
|
#define NUM_SCANNED \
|
|
(sizeof(SCANNED_RELATIVE_PATHS) / sizeof(SCANNED_RELATIVE_PATHS[0]))
|
|
|
|
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;
|
|
}
|
|
|
|
/* The detector: a single source line that mentions both "calloc(" and
|
|
* "pendreason_t" is exactly the shape of a caller constructing an
|
|
* xce_pendreason_t (or a pointer to one) via direct calloc() rather than
|
|
* make_pendreason(). Operates on one already-NUL-terminated line (no
|
|
* embedded '\n'), never on whole-file text -- see file header for why. */
|
|
static int line_has_violation(const char* line)
|
|
{
|
|
return (strstr(line, "calloc(") != NULL)
|
|
&& (strstr(line, "pendreason_t") != NULL);
|
|
}
|
|
|
|
/* Scans `path` line by line. Returns 1 if some line violates (and copies
|
|
* that line into violating_line_out for the failure message), 0 if clean,
|
|
* -1 if the file could not be read at all (caller must treat -1 as a hard
|
|
* failure, not "skip" -- an unreadable path means this whole test is
|
|
* silently checking nothing). */
|
|
static int scan_file_for_violation(const char* path, char* violating_line_out,
|
|
size_t out_sz)
|
|
{
|
|
char* text = read_whole_file(path);
|
|
char* saveptr = NULL;
|
|
char* line;
|
|
int found = 0;
|
|
|
|
if (text == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
line = strtok_r(text, "\n", &saveptr);
|
|
while (line != NULL) {
|
|
if (line_has_violation(line)) {
|
|
found = 1;
|
|
if ((violating_line_out != NULL) && (out_sz > 0)) {
|
|
snprintf(violating_line_out, out_sz, "%s", line);
|
|
}
|
|
break;
|
|
}
|
|
line = strtok_r(NULL, "\n", &saveptr);
|
|
}
|
|
|
|
free(text);
|
|
return found;
|
|
}
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
/* Sanity check for the audit itself: every baked-in path must actually
|
|
* open. Without this, a stale/typo'd relative path would make
|
|
* scan_file_for_violation() silently treat a missing file as "clean" (0
|
|
* lines scanned, 0 violations found) -- the exact false-confidence failure
|
|
* mode this whole test exists to avoid for the real invariant. */
|
|
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);
|
|
}
|
|
}
|
|
|
|
/* Proves the detector can actually fire, on synthetic text -- not just that
|
|
* it stays quiet on today's real files. Without this, a typo in
|
|
* line_has_violation() (e.g. a misspelled substring) could make every real
|
|
* scan vacuously pass forever. Mirrors the spirit of
|
|
* test_picker_core_loop_boundary.c's manual RED demonstration, but as a
|
|
* committed, permanently-runnable test of the detector function itself
|
|
* rather than an uncommitted one-time manual edit. */
|
|
static void test_detector_fires_on_same_line_and_not_on_split_lines(void)
|
|
{
|
|
TEST_ASSERT_TRUE(line_has_violation(
|
|
" r = (xce_pendreason_t*)calloc(1, sizeof(*r));"));
|
|
TEST_ASSERT_TRUE(
|
|
line_has_violation(" calloc(1, sizeof(xce_pendreason_t));"));
|
|
|
|
/* same two substrings, but never on one line -- must NOT fire (the
|
|
* exact decision.c/scheduler.c shape found during the audit). */
|
|
TEST_ASSERT_FALSE(line_has_violation("xce_pendreason_t* reason;"));
|
|
TEST_ASSERT_FALSE(line_has_violation(
|
|
" thing = (xce_decision_t*)calloc(1, sizeof(*thing));"));
|
|
}
|
|
|
|
/* Req 6 AC 2's actual regression lock: no scanned file contains a line
|
|
* constructing an xce_pendreason_t via direct calloc(). */
|
|
static void test_no_direct_pendreason_calloc_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_for_violation(path, violating_line,
|
|
sizeof(violating_line));
|
|
|
|
TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, rc, path);
|
|
|
|
if (rc == 1) {
|
|
snprintf(msg, sizeof(msg),
|
|
"%s contains a direct xce_pendreason_t calloc() -- "
|
|
"Req 6 AC 2 requires make_pendreason() (lib/preason.c) "
|
|
"as the sole factory. Offending line: %s",
|
|
path, violating_line);
|
|
TEST_FAIL_MESSAGE(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_all_scanned_paths_are_readable);
|
|
RUN_TEST(test_detector_fires_on_same_line_and_not_on_split_lines);
|
|
RUN_TEST(test_no_direct_pendreason_calloc_in_scheduler_tree);
|
|
return UNITY_END();
|
|
}
|