test(scheduler-core): guard core scheduling loop against ordering-logic regressions (task 2.5)
Task 2.4 already made sched_job_picker() delegate entirely to the picker interface with zero direct queue/job traversal, satisfying Req 1 AC 1/AC 7 as a byproduct. This adds a source-text regression test locking that property in place: asserts sched_job_picker()'s body contains none of the forbidden direct-traversal symbols and does contain the expected picker-interface call sites.
This commit is contained in:
parent
7ee9a16f53
commit
9d91d470b4
3 changed files with 265 additions and 1 deletions
|
|
@ -60,7 +60,7 @@
|
||||||
- _Requirements: 1.3, 1.4, 1.6, 2.5, 3.1, 3.5_
|
- _Requirements: 1.3, 1.4, 1.6, 2.5, 3.1, 3.5_
|
||||||
- _Depends: 2.3, 1.3_
|
- _Depends: 2.3, 1.3_
|
||||||
|
|
||||||
- [ ] 2.5 Remove ordering logic from the core scheduling loop
|
- [x] 2.5 Remove ordering logic from the core scheduling loop
|
||||||
- The core loop calls only `picker->next_job()` to obtain the next schedulable job — no project/queue/job iteration or ordering logic remains in the loop itself
|
- The core loop calls only `picker->next_job()` to obtain the next schedulable job — no project/queue/job iteration or ordering logic remains in the loop itself
|
||||||
- Observable: a code-level check confirms the scheduling loop function contains no direct queue/job traversal — all selection reads happen through the picker interface
|
- Observable: a code-level check confirms the scheduling loop function contains no direct queue/job traversal — all selection reads happen through the picker interface
|
||||||
- _Requirements: 1.1, 1.7_
|
- _Requirements: 1.1, 1.7_
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,27 @@ sched_picker_wiring_test = executable('sched_picker_wiring_test',
|
||||||
dependencies: [scheduler_test_deps],
|
dependencies: [scheduler_test_deps],
|
||||||
install: false)
|
install: false)
|
||||||
|
|
||||||
|
# Regression guard for the core scheduling loop boundary (scheduler-core
|
||||||
|
# spec, task 2.5, Req 1 AC 1 / AC 7): "the core loop calls only
|
||||||
|
# picker->next_job() ... no project/queue/job iteration or ordering logic
|
||||||
|
# remains in the loop itself." Does NOT #include scheduler.c (that file's
|
||||||
|
# external dependency surface is why test_picker_wiring.c's own header
|
||||||
|
# comment already ruled out driving sched_job_picker() directly) -- instead
|
||||||
|
# it reads scheduler.c's real on-disk text at test run time and mechanically
|
||||||
|
# checks sched_job_picker()'s body for required picker-interface call sites
|
||||||
|
# and absent direct-traversal symbols. XCE_SCHEDULER_C_PATH bakes in the
|
||||||
|
# absolute path via meson.current_source_dir() so the check has no
|
||||||
|
# dependency on the test's cwd at run time.
|
||||||
|
sched_picker_core_loop_boundary_test = executable(
|
||||||
|
'sched_picker_core_loop_boundary_test',
|
||||||
|
['test_picker_core_loop_boundary.c'],
|
||||||
|
include_directories:[incdir],
|
||||||
|
dependencies: [scheduler_test_deps],
|
||||||
|
c_args: ['-DXCE_SCHEDULER_C_PATH="'
|
||||||
|
+ (meson.current_source_dir() / '..' / 'src' / 'scheduler.c')
|
||||||
|
+ '"'],
|
||||||
|
install: false)
|
||||||
|
|
||||||
# Register as meson tests so `meson test --suite scheduler` runs them all.
|
# Register as meson tests so `meson test --suite scheduler` runs them all.
|
||||||
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
|
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
|
||||||
test('scheduler test', scheduler_test, suite: 'scheduler',
|
test('scheduler test', scheduler_test, suite: 'scheduler',
|
||||||
|
|
@ -158,3 +179,6 @@ test('scheduler picker fcfs session test', sched_picker_fcfs_session_test, suite
|
||||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||||
test('scheduler picker wiring test', sched_picker_wiring_test, suite: 'scheduler',
|
test('scheduler picker wiring test', sched_picker_wiring_test, suite: 'scheduler',
|
||||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||||
|
test('scheduler picker core loop boundary test',
|
||||||
|
sched_picker_core_loop_boundary_test, suite: 'scheduler',
|
||||||
|
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||||
|
|
|
||||||
240
src/scheduler/testsuite/test_picker_core_loop_boundary.c
Normal file
240
src/scheduler/testsuite/test_picker_core_loop_boundary.c
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
/*
|
||||||
|
* test_picker_core_loop_boundary.c - regression guard for the core
|
||||||
|
* scheduling loop boundary (scheduler-core spec, task 2.5, Req 1 AC 1 /
|
||||||
|
* AC 7): "The Scheduler core loop shall call picker->next_job(picker) ...
|
||||||
|
* and shall not contain any logic about project iteration, queue
|
||||||
|
* iteration, or job ordering -- those concerns are entirely encapsulated
|
||||||
|
* inside the picker plugin."
|
||||||
|
*
|
||||||
|
* Task 2.5's own verification pass (independently re-reading scheduler.c
|
||||||
|
* after task 2.4's rewrite) found sched_job_picker() already satisfies
|
||||||
|
* this property as a byproduct of task 2.4's work: it resolves the
|
||||||
|
* configured picker via sched_resolve_job_picker(), then either delegates
|
||||||
|
* wholesale to a legacy job_elect_fn plugin (self-contained, unchanged) or
|
||||||
|
* runs a generic loop that reads job order exclusively through
|
||||||
|
* picker_iface->begin_session()/next_job()/end_session() -- zero direct
|
||||||
|
* sched->queues/sched->jobs traversal. This task's Observable is therefore
|
||||||
|
* the *regression guard* on an already-true property, not a behavior
|
||||||
|
* change: a code-level check that keeps this true if a future change
|
||||||
|
* reintroduces ordering logic into the core loop.
|
||||||
|
*
|
||||||
|
* Why a source-text scan and not a linked white-box test: test_picker_wiring.c
|
||||||
|
* (task 2.4)'s own header comment already established that sched_job_picker()
|
||||||
|
* cannot be tractably driven directly -- it is `static` in scheduler.c, whose
|
||||||
|
* external dependency surface (xcecli_ctx_*, sel_register_alloc_thread,
|
||||||
|
* resource.c's allocation path, decision.c's decision constructors, ...) is
|
||||||
|
* far larger than any other white-box test in this directory stubs, and
|
||||||
|
* spinning the real picker/allocator threads to exercise it live is exactly
|
||||||
|
* what that file's task chose not to do. This file instead reads the
|
||||||
|
* actual, current on-disk source of sched_job_picker() at test run time (an
|
||||||
|
* absolute path baked in by meson.build via XCE_SCHEDULER_C_PATH, so no cwd
|
||||||
|
* assumption is needed -- see meson.build's comment on this test target)
|
||||||
|
* and mechanically checks the function body: it must contain the
|
||||||
|
* picker-interface call sites the Observable requires, and must NOT
|
||||||
|
* contain any of the direct-traversal symbols that would indicate
|
||||||
|
* project/queue/job ordering logic crept back into the loop. This is a
|
||||||
|
* grep-based assertion, same genre as test_upgrade_snapshot_apply.c's
|
||||||
|
* popen+grep structural check (controller testsuite) -- here done via
|
||||||
|
* fopen+strstr instead of popen since no shell/glob is needed.
|
||||||
|
*
|
||||||
|
* RED demonstration performed manually during development (not committed):
|
||||||
|
* temporarily inserting a `for (job = sched->jobs.head; ...)`-style
|
||||||
|
* fragment into sched_job_picker()'s real body made
|
||||||
|
* test_core_loop_has_no_forbidden_traversal_symbols fail immediately with
|
||||||
|
* the exact forbidden-symbol message, confirming the check exercises live
|
||||||
|
* source text rather than a frozen snapshot -- then reverted.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <unity/unity.h>
|
||||||
|
|
||||||
|
#ifndef XCE_SCHEDULER_C_PATH
|
||||||
|
#error \
|
||||||
|
"XCE_SCHEDULER_C_PATH must be defined by meson.build to the absolute path of scheduler.c"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Anchor is the literal function signature text, unique to the real
|
||||||
|
* definition -- every comment reference to sched_job_picker elsewhere in
|
||||||
|
* this codebase omits the "(void* arg)" argument (verified via `grep -rn
|
||||||
|
* sched_job_picker src/scheduler`: only the definition itself carries this
|
||||||
|
* exact substring). If this signature is ever reformatted, this test fails
|
||||||
|
* loudly (extraction returns NULL) rather than silently passing on stale
|
||||||
|
* text. */
|
||||||
|
static const char* const FUNC_ANCHOR
|
||||||
|
= "static void* sched_job_picker(void* arg)";
|
||||||
|
|
||||||
|
/* Symbols whose presence in sched_job_picker()'s body would mean project/
|
||||||
|
* queue/job iteration or ordering logic leaked back into the core loop
|
||||||
|
* (Req 1 AC 1). None of these are legitimately needed by a function whose
|
||||||
|
* only jobs are: resolve the picker, delegate to job_elect_fn OR drive
|
||||||
|
* picker_iface's session hooks, and shuttle jobids across cbuf. */
|
||||||
|
static const char* const FORBIDDEN_SYMBOLS[] = {
|
||||||
|
"sched->queues", "sched->jobs", "iter_make(", "iter_next(",
|
||||||
|
"ordq_get(", "ordq_put(", "ordq_peek(", "key_find(",
|
||||||
|
};
|
||||||
|
#define NUM_FORBIDDEN (sizeof(FORBIDDEN_SYMBOLS) / sizeof(FORBIDDEN_SYMBOLS[0]))
|
||||||
|
|
||||||
|
/* Picker-interface call sites the Observable requires: "all selection
|
||||||
|
* reads happen through the picker interface." Both the legacy-delegation
|
||||||
|
* branch and the native session-loop branch must be present -- task 2.5
|
||||||
|
* did not collapse them (that would be a behavior change, out of scope;
|
||||||
|
* see this task's own "likely non-behavioral" framing). */
|
||||||
|
static const char* const REQUIRED_SYMBOLS[] = {
|
||||||
|
"job_elect_fn(job_picker)",
|
||||||
|
"picker_iface->begin_session(job_picker)",
|
||||||
|
"picker_iface->next_job(job_picker)",
|
||||||
|
"picker_iface->end_session(job_picker)",
|
||||||
|
};
|
||||||
|
#define NUM_REQUIRED (sizeof(REQUIRED_SYMBOLS) / sizeof(REQUIRED_SYMBOLS[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* extract_function_body - returns a newly malloc'd, NUL-terminated copy of
|
||||||
|
* the {...} body of the function whose signature text is `anchor` within
|
||||||
|
* `text` (brace-depth counting from the first '{' following the anchor).
|
||||||
|
* Returns NULL if the anchor isn't found or braces never balance before
|
||||||
|
* EOF -- the caller must treat NULL as a hard test failure, not "skip".
|
||||||
|
*/
|
||||||
|
static char* extract_function_body(const char* text, const char* anchor)
|
||||||
|
{
|
||||||
|
const char* sig = strstr(text, anchor);
|
||||||
|
if (sig == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* brace = strchr(sig, '{');
|
||||||
|
if (brace == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int depth = 0;
|
||||||
|
const char* p = brace;
|
||||||
|
do {
|
||||||
|
if (*p == '{') {
|
||||||
|
depth++;
|
||||||
|
} else if (*p == '}') {
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
} while ((depth > 0) && (*p != '\0'));
|
||||||
|
|
||||||
|
if (depth != 0) {
|
||||||
|
return NULL; /* unbalanced -- EOF reached mid-function */
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t len = (size_t)(p - brace);
|
||||||
|
char* body = (char*)malloc(len + 1);
|
||||||
|
if (body == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
memcpy(body, brace, len);
|
||||||
|
body[len] = '\0';
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loads scheduler.c and extracts sched_job_picker()'s body fresh for each
|
||||||
|
* test (no shared/torn-down fixture state -- matches this directory's
|
||||||
|
* "fixtures aren't torn down" convention, but here each test cleans up its
|
||||||
|
* own two allocations at the end anyway since there's no ASAN leak check
|
||||||
|
* reason not to). */
|
||||||
|
static char* load_core_loop_body(void)
|
||||||
|
{
|
||||||
|
char* file_text = read_whole_file(XCE_SCHEDULER_C_PATH);
|
||||||
|
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||||
|
file_text, "could not read scheduler.c at XCE_SCHEDULER_C_PATH");
|
||||||
|
|
||||||
|
char* body = extract_function_body(file_text, FUNC_ANCHOR);
|
||||||
|
free(file_text);
|
||||||
|
|
||||||
|
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||||
|
body, "could not locate/extract sched_job_picker()'s body -- has its "
|
||||||
|
"signature changed? update FUNC_ANCHOR in this test if so");
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Req 1 AC 1: "shall not contain any logic about project iteration, queue
|
||||||
|
* iteration, or job ordering". */
|
||||||
|
static void test_core_loop_has_no_forbidden_traversal_symbols(void)
|
||||||
|
{
|
||||||
|
char* body = load_core_loop_body();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < NUM_FORBIDDEN; i++) {
|
||||||
|
char msg[256];
|
||||||
|
snprintf(msg, sizeof(msg),
|
||||||
|
"sched_job_picker() body contains forbidden direct-"
|
||||||
|
"traversal symbol '%s' -- ordering logic has leaked back "
|
||||||
|
"into the core scheduling loop (Req 1 AC 1)",
|
||||||
|
FORBIDDEN_SYMBOLS[i]);
|
||||||
|
TEST_ASSERT_NULL_MESSAGE(strstr(body, FORBIDDEN_SYMBOLS[i]), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Req 1 AC 1 / Req 1 AC 4: "the core scheduling loop shall call
|
||||||
|
* picker->next_job(picker) ... all selection reads happen through the
|
||||||
|
* picker interface" -- the positive-side check, so a change that silently
|
||||||
|
* dropped a picker-interface call site (e.g. hardcoding a job again, or
|
||||||
|
* skipping begin_session/end_session) fails loud even though it wouldn't
|
||||||
|
* trip the forbidden-symbol list above. */
|
||||||
|
static void test_core_loop_calls_only_through_picker_interface(void)
|
||||||
|
{
|
||||||
|
char* body = load_core_loop_body();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < NUM_REQUIRED; i++) {
|
||||||
|
char msg[256];
|
||||||
|
snprintf(msg, sizeof(msg),
|
||||||
|
"sched_job_picker() body is missing required picker-"
|
||||||
|
"interface call site '%s'",
|
||||||
|
REQUIRED_SYMBOLS[i]);
|
||||||
|
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(body, REQUIRED_SYMBOLS[i]), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUp(void) { return; }
|
||||||
|
void tearDown(void) { return; }
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_core_loop_has_no_forbidden_traversal_symbols);
|
||||||
|
RUN_TEST(test_core_loop_calls_only_through_picker_interface);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue