test(scheduler-sim-live-allocation): prove contention parity across drivers (task 5.1)

Adds two new fixtures spawning the real sched_job_picker/sched_allocator
thread pair directly against bare normal-mode and sim-mode core state
under genuine resource contention (2.0 capacity, 3 jobs x 1.0 demand),
asserting identical admission decisions and identical accounting state
(job->accounts, w->resInUse), with and without a matching host-scoped
limit. The pre-existing test_sim_contention_parity.c could not be reused
since its white-box stubs hard-code the old always-PEND behavior and
never exercise live election/allocation.

Confirms build_limit_vres_requests()/get_vres_value() (plugins/limit.c)
are #if 0'd dead code with zero live callers -- no limit in this codebase
currently gates/denies admission in either mode, only tracks/displays
usage -- so this proves usage-tracking parity, documented explicitly as
an honest scope boundary rather than a silent narrowing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
Rongsong Shen 2026-09-19 08:24:35 +00:00
parent f6a2862110
commit ab88848f91
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 1230 additions and 1 deletions

View file

@ -95,7 +95,7 @@
- _Boundary: Unified Picker/Allocator Threads, Limit Plugin Core-Safe Matching_
- [ ] 5. Validation: prove equivalence and the concrete acceptance test
- [ ] 5.1 Prove identical decisions and accounting state under resource contention across both drivers
- [x] 5.1 Prove identical decisions and accounting state under resource contention across both drivers
- Build a resource-contention fixture (multiple jobs, capacity insufficient for all of them, including capacity constrained by a matching limit) and drive it through both drivers
- Observable: both drivers produce the same admission/pend outcome and the same final resource-accounting state for the identical input
- _Depends: 4.1_
@ -129,3 +129,4 @@
- **Task 2.3**: while migrating a plugin's matching functions, writing the mandated TDD test for a code path that had genuinely never been exercised before (zero prior test call sites for `match_job_host_limit_()`/`limit_prepare_allocation()`) surfaced a real pre-existing bug (bug-1828) blocking that path entirely — the same defect class as bug-1786, fixed the same way, never applied to this sibling function. Fixing a blocking, in-boundary, minimally-scoped pre-existing bug discovered this way is acceptable within a task rather than escalating, but its buglog entry must state the REAL impact (here: host-scoped limits were never enforced in any mode, not merely a display gap) rather than the narrower symptom that surfaced it — the first-written impact description undersold this until review caught it.
- **Task 1.3**: grep for ALL call sites of a function before migrating it, not just the ones named in the task brief — `host_iter.c`'s `sched_host_iter_next()` and `parjob_allocator.c`/`seqjob_allocator.c`'s `my_alloc()` all call the capacity-check functions directly, not only via `sched_try_alloc_on_host_()`. A private `static` helper with no other callers besides the functions being migrated should be migrated alongside them (leaving it stale forces an ugly cast, which this codebase's own design explicitly forbids in shared code).
- **Task 1.2**: retyping a widely-called function's signature (e.g. `plugin_job_election`, `plugin_prepare_allocation`, `plugin_commit_decision`) breaks ALL of its call sites as hard compile errors in one step (this toolchain treats `-Wincompatible-pointer-types` as an error, not a warning) — there is no way to migrate a signature and keep the build green without touching every call site in the same commit, even when those call sites live in files nominally owned by a later task. The correct scope is the *minimal* fix (wrap the existing argument, e.g. `sched` → `&(sched->core)`; touch nothing else in that function), not a rewrite of the caller. Also: design.md's Implementation Notes said `plugin_prepare_allocation()` has "one normal-mode call site" — it actually has two (`resource.c`'s `sched_try_alloc_on_host_()` and `schedobj.c`'s `sched_ctx_set_primary()`). Grep for ALL call sites before assuming a design doc's count is exhaustive.
- **Task 5.1**: the pre-existing `test_sim_contention_parity.c` fixture could not be reused because it drives reconciliation only (its own white-box stubs hard-code the old always-PEND behavior), so proving live-path contention parity required two new fixtures spawning the real `sched_job_picker()`/`sched_allocator()` pair directly against bare normal-mode and sim-mode core state. Confirmed as a genuine, requirement-consistent scope boundary (not a silent narrowing): `build_limit_vres_requests()`/`get_vres_value()` (`plugins/limit.c`) are `#if 0`'d dead code with zero live callers — no limit, in either mode, currently gates/denies admission anywhere in this codebase, only tracks/displays usage. Any future task attempting real limit-based admission denial must first un-stub these two functions; that is out of boundary for this feature.

View file

@ -2135,3 +2135,59 @@ sched_sim_alloc_limit_fanout_test = executable(
test('scheduler sim alloc limit fanout test',
sched_sim_alloc_limit_fanout_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])
# scheduler-sim-live-allocation spec, task 5.1 (Req 3.1, 3.2): drives the
# real, unmodified sched_job_picker()/sched_allocator() pair against a
# multi-job, capacity-insufficient contention fixture on BOTH a normal-mode-
# shaped core (xce_scheduler_t) and a simulation-mode-shaped core
# (xce_sched_sim_driver_t) side by side, confirming identical admission
# outcomes and identical final resource-accounting state. Same real-
# scheduler_srcs link technique as sched_election_sim_fixture_test/
# sched_sim_live_allocation_test/sched_sim_alloc_limit_fanout_test above.
sched_sim_contention_resource_driver_parity_test_srcs = [
'test_sim_contention_resource_driver_parity.c'
]
foreach f : scheduler_srcs
if f != 'src/main.c'
sched_sim_contention_resource_driver_parity_test_srcs += ['../' + f]
endif
endforeach
sched_sim_contention_resource_driver_parity_test = executable(
'sched_sim_contention_resource_driver_parity_test',
sched_sim_contention_resource_driver_parity_test_srcs,
include_directories:[incdir],
dependencies: [scheduler_test_deps],
install: false)
test('scheduler sim contention resource driver parity test',
sched_sim_contention_resource_driver_parity_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])
# scheduler-sim-live-allocation spec, task 5.1 (Req 3.1, 3.2): sibling of
# sched_sim_contention_resource_driver_parity_test above, adding a host-
# scoped limit that actively matches every contending job on both drivers
# (Requirement 3.2's "capacity constrained by a matching limit" /
# scheduler-driver-equivalence.md's testing discipline), confirming both
# the admission outcome AND the limit-usage-instance accounting state match
# between drivers. Same real-scheduler_srcs link technique as the test
# immediately above.
sched_sim_contention_limit_driver_parity_test_srcs = [
'test_sim_contention_limit_driver_parity.c'
]
foreach f : scheduler_srcs
if f != 'src/main.c'
sched_sim_contention_limit_driver_parity_test_srcs += ['../' + f]
endif
endforeach
sched_sim_contention_limit_driver_parity_test = executable(
'sched_sim_contention_limit_driver_parity_test',
sched_sim_contention_limit_driver_parity_test_srcs,
include_directories:[incdir],
dependencies: [scheduler_test_deps],
install: false)
test('scheduler sim contention limit driver parity test',
sched_sim_contention_limit_driver_parity_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -0,0 +1,586 @@
/*
* test_sim_contention_limit_driver_parity.c - scheduler-sim-live-allocation
* spec, task 5.1 (Req 3.1, 3.2, boundary: none new -- pure validation of
* the Core-Safe Allocation Chain / Limit Plugin Core-Safe Matching / Unified
* Picker/Allocator Threads components tasks 1.1-4.3 already built).
*
* Sibling of test_sim_contention_resource_driver_parity.c (read that file's
* own header comment first for the full rationale on why this is a NEW
* file rather than an extension of test_sim_contention_parity.c, and for
* the shared fixture technique both files use). This file adds the one
* ingredient that one deliberately scopes out: Requirement 3.2's own text
* explicitly calls out "capacity constrained by a matching limit" as part
* of what a contention fixture must cover, and `scheduler-driver-
* equivalence.md`'s testing discipline names limit-constrained capacity by
* name. This file is that coverage.
*
* ── What "constrained by a matching limit" means in THIS codebase today
* (read before assuming this test denies a job via the limit) ─────────
* Reading plugins/limit.c in full before writing this fixture: a limit
* match (match_job_global_limit_()/match_job_host_limit_()) computes and
* records a CAP value (update_limit_res_value(), percent-of-underlying-
* resource or flat) into core->sharedInst (global) or worker->resvals
* (host-scoped) under a synthetic vres, and stores it in the limit
* plugin's own instances registry (limit_get_usage_instances(), the
* `info limit` accessor) -- but the code path that would actually ADD a
* corresponding "wanted" entry for that vres into a job's own capacity
* check (build_limit_vres_requests(), plugins/limit.c) is `#if 0`'d out
* dead code with zero live callers, and get_vres_value() (its own would-be
* per-job charge amount) is `static` and, once that dead code is excluded,
* has zero live callers either. In other words: in this codebase's CURRENT
* implementation, a configured limit's cap is tracked and displayed
* (Requirement 2's entire scope -- `info limit` visibility) but never
* itself gates admission -- that is a genuinely separate, not-yet-built
* capability, not a gap this validation-only task is scoped to fix (doing
* so would be undisclosed production-code scope expansion, forbidden by
* this task's own Critical Constraints).
*
* This fixture therefore proves the honest, in-scope claim: under the SAME
* genuine resource-capacity contention as this file's sibling (raw "slots"
* capacity is what actually admits/denies each job), a host-scoped limit
* that ACTIVELY MATCHES every contending job (via the real, core-safe
* plugin_prepare_allocation()->limit_prepare_allocation()->match_job_host_
* limit_() chain, task 2.3/4.3) produces the IDENTICAL admission outcome
* AND the IDENTICAL limit-usage-instance state (same vres registered, same
* count) on both drivers -- i.e. limit-matching correctness under
* contention is driver-equivalent, exactly as Requirement 2.1/2.2 (matched
* the same way normal mode would) and Requirement 3.1 (identical
* accounting state, limit usage explicitly included in that list) require,
* even though the limit itself does not additionally constrain WHICH jobs
* admit in this codebase's current implementation. If a future spec wires
* build_limit_vres_requests() in for real, this fixture's own "A and B
* admit, C is denied by raw capacity" outcome is unaffected either way,
* since the limit's own generous cap (percent=1, val=50.0 against a
* WORKER_HOSTRES of 1000.0 -- the exact values test_sim_alloc_limit_
* fanout.c's own proven-safe fixture already uses) is far larger than
* anything this fixture's jobs could ever request.
*
* ── Link style / fixture-safety ───────────────────────────────────────────
* Identical to test_sim_contention_resource_driver_parity.c: real,
* unmodified scheduler_srcs (minus src/main.c); two `static` (not stack-
* local) fixtures, exactly one RUN_TEST(), per this directory's own bug-
* 1598/1632/1763 fixture-safety convention.
*/
#include <errno.h>
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "controller.capnp.h"
#include "decision.h"
#include "plugin.h"
#include "preason.h"
#include "resobj.h"
#include "sched/types.h"
#include "scheduler.h"
#include "sim.h"
#include "xcelimit.h"
#include "xstring.h"
/* Consumed the same "extern declared where consumed" idiom sim_loop.c
* itself and its own sibling real-scheduler_srcs tests already use (no
* shared header declares these). */
extern void sim_bootstrap_picker(xce_sched_sim_driver_t* sim);
extern void sim_bootstrap_limit_plugin(xce_sched_sim_driver_t* sim);
extern void sim_bootstrap_seqjob_allocator(xce_sched_sim_driver_t* sim);
extern void sim_bootstrap_parjob_allocator(xce_sched_sim_driver_t* sim);
#define WORKER_NAME "contdp_limit_worker"
#define QUEUE_NAME "normal"
#define TEST_RESREQ ((char*)"select[type==any] rusage[mem=1]")
#define WORKER_CAPACITY 2.0f /* same insufficient-for-all-3 shape as the
* sibling resource-only fixture */
#define LIMIT_RESNAME "contdp_host_res"
#define LIMIT_NAME "contdp_host_limit"
#define WORKER_HOSTRES 1000.0f /* generous -- the limit itself must not be
* the thing that denies any job here, see
* this file's own header comment */
#define JOBA_ID ((uint64_t)510201ULL)
#define JOBB_ID ((uint64_t)510202ULL)
#define JOBC_ID ((uint64_t)510203ULL)
void setUp(void) { return; }
void tearDown(void) { return; }
/* ---------------------------------------------------------------------- */
/* Fixture helpers -- identical techniques to this file's sibling */
/* (test_sim_contention_resource_driver_parity.c) plus test_sim_alloc_ */
/* limit_fanout.c's own limit-config shape, duplicated per this directory's */
/* established "duplicate, don't share" testsuite convention. */
/* ---------------------------------------------------------------------- */
static void register_slots_resource(var resmgr)
{
xce_resource_t res;
memset(&res, 0, sizeof(res));
res.name = strdup("slots");
res.valtype = XCE_VAL_NUMERIC;
res.ordtype = XCE_ORD_DECR;
res.flags = XCE_RESF_LIMIT | XCE_RESF_CONSUMABLE | XCE_RESF_RELEASE
| XCE_RESF_DEFAULT_HOST;
TEST_ASSERT_NOT_NULL(makeResource(resmgr, &res));
}
/* register_plain_resource - same shape test_sim_alloc_limit_fanout.c's own
* identically-named helper establishes: a bare resource with no LIMIT/
* CONSUMABLE/host flags of its own -- is_host_based_limit() (xcelimit.c)
* resolves LIMIT_NAME as host-based purely from limit->val.percent!=0, not
* from any flag on this resource (confirmed by reading is_host_based_
* limit()'s body before writing this fixture). */
static xce_resource_t* register_plain_resource(var resmgr, const char* name)
{
xce_resource_t res;
memset(&res, 0, sizeof(res));
res.name = strdup(name);
return makeResource(resmgr, &res);
}
static void add_worker_resval(xce_sched_worker_t* worker,
xce_sched_core_t* core,
const char* resname, double value)
{
xce_resource_t* r = NULL;
xce_val_t* val = NULL;
int rc;
rc = key_find(core->resmgr, (void*)resname, (void**)&r);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(r);
val = (xce_val_t*)calloc(1, sizeof(*val));
TEST_ASSERT_NOT_NULL(val);
val->which = Value_u_fvalue;
val->u.fvalue = value;
zarray_put(&(worker->resvals), r->idx, val);
}
static xce_sched_worker_t* make_worker(xce_sched_core_t* core, const char* name,
double slots_capacity, double hostres)
{
xce_sched_worker_t* w = (xce_sched_worker_t*)calloc(1, sizeof(*w));
TEST_ASSERT_NOT_NULL(w);
w->name = strdup(name);
w->status = 0;
w->idx = -1;
zarray_init(&(w->resvals), 128, (zarray_item_killer_t)kill_sched_resval);
zarray_init(&(w->resInUse), 128, (zarray_item_killer_t)kill_sched_resval);
add_worker_resval(w, core, "slots", slots_capacity);
add_worker_resval(w, core, LIMIT_RESNAME, hostres);
return w;
}
static xce_sched_job_t* push_contention_job(xce_sched_core_t* core,
xce_scheduler_t* job_ctor_wrapper,
xce_sched_plugin_t* picker,
uint64_t jobid, uint64_t queue_time)
{
job_submit_data_t submit;
xce_job_t j;
xce_sched_job_t* job;
int rc;
memset(&submit, 0, sizeof(submit));
submit.queue = (char*)QUEUE_NAME;
submit.user = (char*)"contdp_limit_user";
submit.project_name = (char*)"default";
submit.resreq = TEST_RESREQ;
submit.mintasks = 1;
submit.maxtasks = 1;
memset(&j, 0, sizeof(j));
j.jobid = jobid;
j.state = JOB_STAT_PEND;
j.sub_time = queue_time;
j.queue_time = queue_time;
j.submit = &submit;
job = make_sched_job(job_ctor_wrapper, &j);
TEST_ASSERT_NOT_NULL(job);
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, job->schedobj->tag);
job->schedobj->u.primary->resreq->minTasks = 1;
job->schedobj->u.primary->resreq->maxTasks = 1;
rc = sched_add_job(core, job);
TEST_ASSERT_TRUE(rc >= 0);
picker->picker_iface->on_job_push(picker, job);
return job;
}
static void run_contention_round(xce_sched_core_t* core, int njobs)
{
pthread_t picker_tid;
pthread_t allocator_tid;
xce_decision_t* d;
xce_decision_t* end_marker = (xce_decision_t*)0x1;
int rc;
int i;
rc = pthread_create(&picker_tid, NULL, sched_job_picker, core);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = pthread_create(&allocator_tid, NULL, sched_allocator, core);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xce_core_go(core);
TEST_ASSERT_EQUAL_INT(0, rc);
for (i = 0; i < njobs; i++) {
d = NULL;
cbuf_get(&(core->decisions), &d);
TEST_ASSERT_NOT_NULL_MESSAGE(
d, "the real thread pair must carry every contending job all "
"the way to a committed decision");
}
end_marker = (xce_decision_t*)0x1;
cbuf_get(&(core->decisions), &end_marker);
TEST_ASSERT_NULL_MESSAGE(end_marker,
"decisions-cbuf entry after all real jobs must "
"be the end-of-session NULL marker");
rc = xce_core_end_session(core);
TEST_ASSERT_EQUAL_INT(0, rc);
}
/* count_usage_instances - iterates limit_get_usage_instances(core)
* (plugins/limit.c, unchanged scheduler-sim-diagnostics accessor) and
* returns how many usage-instance entries exist -- 0 if the accessor
* itself returns NULL (no limit plugin registered / no instance yet). */
static int count_usage_instances(xce_sched_core_t* core)
{
var handle = limit_get_usage_instances(core);
iterator_t it;
void* item = NULL;
int rc;
int count = 0;
if (handle == NULL) {
return 0;
}
rc = iter_make(&it, handle);
if (rc < 0) {
return 0;
}
for (;;) {
rc = iter_next(&it, &item);
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
break;
}
count++;
}
iter_destroy(&it);
return count;
}
/* ---------------------------------------------------------------------- */
/* The two fixtures under test -- `static`, see the sibling file's own */
/* header comment ("bug-1598/1632/1763 fixture-safety requirement"). */
/* ---------------------------------------------------------------------- */
static xce_scheduler_t g_normal;
static xce_sched_sim_driver_t g_sim;
static xce_scheduler_t g_sim_job_wrapper;
/* ---------------------------------------------------------------------- */
/* The test */
/* ---------------------------------------------------------------------- */
static void
test_limit_constrained_contention_matches_between_normal_and_sim_drivers(
void)
{
xce_limit_val_t limit_val = { .resname = (char*)LIMIT_RESNAME,
.percent = 1,
.val = 50.0 };
xce_limit_config_t limit_cfg = { .name = (char*)LIMIT_NAME,
.val = &limit_val };
xce_limit_config_t* limits[1] = { &limit_cfg };
xce_conf4sched_t config;
xce_sched_plugin_t* fcfs_normal = NULL;
xce_sched_plugin_t* fcfs_sim = NULL;
xce_sched_plugin_t* limit_normal = NULL;
xce_sched_plugin_t* limit_sim = NULL;
xce_sched_queue_t* queue_normal;
xce_sched_queue_t* queue_sim;
xce_sched_job_t* jobA_n;
xce_sched_job_t* jobB_n;
xce_sched_job_t* jobC_n;
xce_sched_job_t* jobA_s;
xce_sched_job_t* jobB_s;
xce_sched_job_t* jobC_s;
xce_sched_worker_t* w_normal;
xce_sched_worker_t* w_sim;
xce_resource_t* r_normal = NULL;
xce_resource_t* r_sim = NULL;
int residx_normal;
int residx_sim;
float resinuse_normal = -1.0f;
float resinuse_sim = -1.0f;
int instances_normal;
int instances_sim;
int rc;
memset(&config, 0, sizeof(config));
config.nlimits = 1;
config.limits = limits;
/* ================================================================
* Normal-mode-shaped fixture
* ================================================================ */
memset(&g_normal, 0, sizeof(g_normal));
rc = xce_sched_core_init(&(g_normal.core));
TEST_ASSERT_EQUAL_INT(0, rc);
register_slots_resource(g_normal.core.resmgr);
TEST_ASSERT_NOT_NULL(
register_plain_resource(g_normal.core.resmgr, LIMIT_RESNAME));
fcfs_normal = builtin_fcfs_new_plugin();
TEST_ASSERT_NOT_NULL(fcfs_normal);
rc = plugin_register(&g_normal, fcfs_normal);
TEST_ASSERT_TRUE(rc >= 0);
g_normal.core.job_pick_plugin = (char*)"builtin_fcfs";
/* Real production limit registration: register_limit()'s own
* sequence (plugin_register(sched, limit_new_plugin())), inlined so
* this fixture retains the plugin pointer to seed its config directly
* via limit_seed_config() -- the same driver-agnostic, core-safe entry
* point (task 2.3/bug-1825) sim mode's own sim_bootstrap_limit_
* plugin() uses below, applied here to normal mode's own registration
* path instead of routing through a live CONFIG_PUSH event (out of
* this fixture's own boundary). */
limit_normal = limit_new_plugin();
TEST_ASSERT_NOT_NULL(limit_normal);
rc = plugin_register(&g_normal, limit_normal);
TEST_ASSERT_TRUE(rc >= 0);
rc = limit_seed_config(limit_normal, &(g_normal.core), &config);
TEST_ASSERT_TRUE(rc >= 0);
rc = register_seqjob_allocator(&g_normal);
TEST_ASSERT_TRUE(rc >= 0);
rc = register_parjob_allocator(&g_normal);
TEST_ASSERT_TRUE(rc >= 0);
queue_normal = (xce_sched_queue_t*)calloc(1, sizeof(*queue_normal));
TEST_ASSERT_NOT_NULL(queue_normal);
queue_normal->name = makeString(QUEUE_NAME);
rc = sched_add_queue(&(g_normal.core), queue_normal);
TEST_ASSERT_TRUE(rc >= 0);
fcfs_normal->picker_iface->on_queue_update(fcfs_normal, queue_normal);
w_normal = make_worker(&(g_normal.core), WORKER_NAME, WORKER_CAPACITY,
WORKER_HOSTRES);
rc = sched_add_worker(&(g_normal.core), w_normal);
TEST_ASSERT_TRUE(rc >= 0);
push_contention_job(&(g_normal.core), &g_normal, fcfs_normal, JOBA_ID,
1000ULL);
push_contention_job(&(g_normal.core), &g_normal, fcfs_normal, JOBB_ID,
1001ULL);
push_contention_job(&(g_normal.core), &g_normal, fcfs_normal, JOBC_ID,
1002ULL);
run_contention_round(&(g_normal.core), 3);
/* ================================================================
* Sim-mode-shaped fixture
* ================================================================ */
memset(&g_sim, 0, sizeof(g_sim));
rc = xce_sched_core_init(&(g_sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
register_slots_resource(g_sim.core.resmgr);
TEST_ASSERT_NOT_NULL(
register_plain_resource(g_sim.core.resmgr, LIMIT_RESNAME));
sim_bootstrap_picker(&g_sim);
rc = key_find(g_sim.core.plugins, "builtin_fcfs", (void**)&fcfs_sim);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(fcfs_sim);
g_sim.core.config = &config;
sim_bootstrap_limit_plugin(&g_sim);
rc = key_find(g_sim.core.plugins, "xce_sched_limit", (void**)&limit_sim);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(limit_sim);
TEST_ASSERT_EQUAL_PTR_MESSAGE(
&(g_sim.core), limit_sim->core,
"sanity: the limit plugin must be registered directly against "
"sim.core -- the same registry the seqjob/parjob allocator "
"plugin's own prepare_allocation_hook fan-out must reach for a "
"host-scoped match to ever be found (task 4.3, bug-1829's own "
"follow-up fix)");
sim_bootstrap_seqjob_allocator(&g_sim);
sim_bootstrap_parjob_allocator(&g_sim);
TEST_ASSERT_NOT_NULL_MESSAGE(
g_sim.core.seqjob_allocator,
"sim_bootstrap_seqjob_allocator() must populate core->seqjob_"
"allocator (task 4.1)");
queue_sim = (xce_sched_queue_t*)calloc(1, sizeof(*queue_sim));
TEST_ASSERT_NOT_NULL(queue_sim);
queue_sim->name = makeString(QUEUE_NAME);
rc = sched_add_queue(&(g_sim.core), queue_sim);
TEST_ASSERT_TRUE(rc >= 0);
fcfs_sim->picker_iface->on_queue_update(fcfs_sim, queue_sim);
w_sim = make_worker(&(g_sim.core), WORKER_NAME, WORKER_CAPACITY,
WORKER_HOSTRES);
rc = sched_add_worker(&(g_sim.core), w_sim);
TEST_ASSERT_TRUE(rc >= 0);
memset(&g_sim_job_wrapper, 0, sizeof(g_sim_job_wrapper));
rc = xce_sched_core_init(&(g_sim_job_wrapper.core));
TEST_ASSERT_EQUAL_INT(0, rc);
g_sim_job_wrapper.core.resmgr = g_sim.core.resmgr;
g_sim_job_wrapper.core.workers = g_sim.core.workers;
g_sim_job_wrapper.core.queues = g_sim.core.queues;
push_contention_job(&(g_sim.core), &g_sim_job_wrapper, fcfs_sim, JOBA_ID,
1000ULL);
push_contention_job(&(g_sim.core), &g_sim_job_wrapper, fcfs_sim, JOBB_ID,
1001ULL);
push_contention_job(&(g_sim.core), &g_sim_job_wrapper, fcfs_sim, JOBC_ID,
1002ULL);
run_contention_round(&(g_sim.core), 3);
/* ================================================================
* Assertions
* ================================================================ */
jobA_n = sched_get_job(&(g_normal.core), JOBA_ID);
jobB_n = sched_get_job(&(g_normal.core), JOBB_ID);
jobC_n = sched_get_job(&(g_normal.core), JOBC_ID);
jobA_s = sched_get_job(&(g_sim.core), JOBA_ID);
jobB_s = sched_get_job(&(g_sim.core), JOBB_ID);
jobC_s = sched_get_job(&(g_sim.core), JOBC_ID);
TEST_ASSERT_NOT_NULL(jobA_n);
TEST_ASSERT_NOT_NULL(jobB_n);
TEST_ASSERT_NOT_NULL(jobC_n);
TEST_ASSERT_NOT_NULL(jobA_s);
TEST_ASSERT_NOT_NULL(jobB_s);
TEST_ASSERT_NOT_NULL(jobC_s);
/* Same raw-capacity-driven outcome as the sibling fixture -- the
* matching limit (generous cap, see file header) must not itself
* change which jobs admit. */
TEST_ASSERT_EQUAL_UINT_MESSAGE(
JOB_STAT_RUN, jobA_n->state,
"normal mode: job A must dispatch (limit must not spuriously deny "
"a job it has no admission-gating effect on in this codebase)");
TEST_ASSERT_EQUAL_UINT_MESSAGE(JOB_STAT_RUN, jobB_n->state,
"normal mode: job B must dispatch");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
JOB_STAT_PEND, jobC_n->state,
"normal mode: job C must still be denied by genuine raw-capacity "
"contention (2.0 slots, 3x1.0 demand)");
TEST_ASSERT_EQUAL_UINT_MESSAGE(JOB_STAT_RUN, jobA_s->state,
"sim mode: job A must dispatch");
TEST_ASSERT_EQUAL_UINT_MESSAGE(JOB_STAT_RUN, jobB_s->state,
"sim mode: job B must dispatch");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
JOB_STAT_PEND, jobC_s->state,
"sim mode: job C must still be denied by genuine raw-capacity "
"contention");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
jobA_n->state, jobA_s->state,
"job A: normal and sim mode must agree under limit-matched "
"contention");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
jobB_n->state, jobB_s->state,
"job B: normal and sim mode must agree under limit-matched "
"contention");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
jobC_n->state, jobC_s->state,
"job C: normal and sim mode must agree under limit-matched "
"contention");
/* Final resource-accounting-state parity for the RAW resource, same
* as the sibling fixture. */
rc = key_find(g_normal.core.resmgr, (void*)"slots", (void**)&r_normal);
TEST_ASSERT_TRUE(rc >= 0);
residx_normal = r_normal->idx;
rc = key_find(g_sim.core.resmgr, (void*)"slots", (void**)&r_sim);
TEST_ASSERT_TRUE(rc >= 0);
residx_sim = r_sim->idx;
rc = get_resvals(&(w_normal->resInUse), 1, &residx_normal,
&resinuse_normal);
TEST_ASSERT_EQUAL_INT(1, rc);
rc = get_resvals(&(w_sim->resInUse), 1, &residx_sim, &resinuse_sim);
TEST_ASSERT_EQUAL_INT(1, rc);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
WORKER_CAPACITY, resinuse_normal,
"normal mode: worker resInUse must reach exactly full saturation");
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
WORKER_CAPACITY, resinuse_sim,
"sim mode: worker resInUse must reach exactly full saturation");
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
resinuse_normal, resinuse_sim,
"driver parity: worker resInUse must match byte-for-byte under "
"limit-matched contention too");
/* Limit-usage-instance parity (Requirement 2.1/2.2/3.1's own explicit
* inclusion of limit usage in the accounting state that must match).
* Every one of the 3 jobs' allocation attempts reaches plugin_prepare_
* allocation() (resource.c's sched_try_alloc_on_host_() calls it
* BEFORE sched_check_host_satisfy(), confirmed by reading that
* function's body before writing this fixture -- i.e. a job that is
* ultimately DENIED by raw capacity still genuinely matches the limit
* on its way there), and none of this fixture's consumer scoping
* (user/queue/project all left unconfigured) differentiates jobs from
* one another, so exactly ONE shared vres/usage-instance is expected
* on each side, not three. */
instances_normal = count_usage_instances(&(g_normal.core));
instances_sim = count_usage_instances(&(g_sim.core));
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, instances_normal,
"normal mode: the host-scoped limit must genuinely match and "
"register exactly one usage instance under this contention");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, instances_sim,
"sim mode: the host-scoped limit must genuinely match and "
"register exactly one usage instance under this contention -- "
"before task 2.3/4.3's fixes this stayed 0 (plugin->sched NULL / "
"wrong plugin registry), which is exactly what this assertion "
"would have caught");
TEST_ASSERT_EQUAL_INT_MESSAGE(
instances_normal, instances_sim,
"driver parity: limit-usage-instance count must match between "
"normal and sim mode under identical limit-matched contention");
printf("test_limit_constrained_contention_matches_between_normal_and_"
"sim_drivers: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(
test_limit_constrained_contention_matches_between_normal_and_sim_drivers);
return UNITY_END();
}

View file

@ -0,0 +1,586 @@
/*
* test_sim_contention_resource_driver_parity.c - scheduler-sim-live-
* allocation spec, task 5.1 (Req 3.1, 3.2, boundary: none new -- pure
* validation of the Core-Safe Allocation Chain / Unified Picker/Allocator
* Threads components tasks 1.1-4.3 already built).
*
* ── Why this is a NEW file, not an extension of test_sim_contention_
* parity.c ─────────────────────────────────────────────────────────────
* test_sim_contention_parity.c (scheduler-sim-decision-parity spec, task
* 4.1, plus scheduler-sim-eager-reconciliation's own sibling test in the
* same file) is a white-box #include of individual sim_loop.c/resource.c/
* etc. source files that deliberately does NOT link sched_election.c --
* instead it defines its OWN LOCAL stub sched_job_picker()/sched_allocator()
* (see that file's own comment above those two functions) that
* unconditionally reproduces the OLD, now-removed sim_picker_thread_fn()'s
* always-PEND behavior, specifically so its own reconciliation-focused
* assertions keep working unchanged. That file's entire two-test suite is
* therefore validating Recorded-Decision Reconciliation
* (sim_reconcile_unaccounted_jobs()) against a hand-crafted `.replay` whose
* jobs are ALREADY resolved (dispatched) before the replay's own live
* picker ever reaches them -- never the genuinely-live
* sched_job_picker()/sched_allocator() pair this feature (scheduler-sim-
* live-allocation) wires into simulation mode for the first time (task
* 4.1). Extending that file to ALSO prove live-allocation parity would mean
* either breaking its own carefully-scoped stub (defeating the tests
* already there) or duplicating this file's entire fixture inside a file
* whose own header comment explicitly documents a narrower purpose. A new
* file is the correct scope, matching this task's own brief ("write an
* entirely new file... may be" is exactly this situation, confirmed by
* reading that file first per this task's Execution Protocol).
*
* ── What this file actually proves (the gap task 5.1 exists to close) ────
* Requirement 3.2 / `scheduler-driver-equivalence.md`'s testing discipline:
* "a resource-contention fixture -- multiple jobs competing for capacity
* insufficient for all of them -- ... driven through both drivers,
* confirming identical admission/pend outcomes AND identical final
* resource-accounting state." No existing test in this codebase drives the
* SAME real, unmodified sched_job_picker()/sched_allocator() pair
* (sched_election.c) against a MULTI-JOB, capacity-insufficient job set on
* BOTH a normal-mode-shaped core (embedded in a real xce_scheduler_t) and a
* simulation-mode-shaped core (embedded in a real xce_sched_sim_driver_t)
* side by side and diffs the outcome -- test_sched_election_sim_fixture.c
* (task 3.1) and test_sim_live_allocation.c/test_sim_alloc_limit_fanout.c
* (tasks 4.1/4.3) each drive exactly ONE job through exactly ONE driver at
* a time. This file is the first genuine two-driver, multi-job, real-
* contention comparison of the shared implementation itself.
*
* ── Link style: real scheduler_srcs, not a white-box #include ────────────
* Same technique test_sched_election_sim_fixture.c/test_sim_live_
* allocation.c/test_sim_alloc_limit_fanout.c already establish (see those
* files' own header comments): links the REAL, unmodified `scheduler_srcs`
* object list (minus src/main.c), so sched_allocator()'s real
* sched_make_allocation()/sched_check_host_satisfy()/sched_commit_
* decision() chain is genuinely exercised on BOTH sides, not stubbed.
* Because of this, this file needs none of test_sim_contention_parity.c's
* own link-satisfying stub soup (kill_sched_job/kill_sched_resval/
* sched_resolve_job_picker/etc.) -- those symbols are real, linked
* production code here (schedobj.c).
*
* ── Fixture shape ──────────────────────────────────────────────────────
* Two bare-core fixtures, built identically except for which top-level
* struct embeds the core (xce_scheduler_t for "normal mode", xce_sched_
* sim_driver_t for "sim mode") -- mirroring test_sched_election_sim_
* fixture.c's own bare-core technique, generalized to THREE contending jobs
* instead of one. Each fixture registers the real "slots" resource (same
* flags test_sim_contention_parity.c's own register_slots_resource()
* establishes: LIMIT|CONSUMABLE|RELEASE|DEFAULT_HOST -- needed for genuine
* host-capacity-check semantics, not merely a name), a worker with
* WORKER_CAPACITY=2.0 "slots", a queue, and three single-task jobs (A, B,
* C; ascending queue_time so builtin_fcfs's own priority-desc/queue_time-
* asc ordq comparator (builtin_fcfs.c) evaluates them in that fixed order
* on BOTH sides) -- each auto-injecting exactly 1.0 "slots" want per task
* (test_sim_live_allocation.c's own documented convention). 2.0 capacity
* against 3x1.0 demand is genuine insufficient-for-all-of-them contention
* (Requirement 3.2's own wording): A and B must exactly saturate capacity,
* C must be denied.
*
* sched_commit_decision()'s DISPATCH branch (scheduler.c, confirmed by
* reading its body before writing this fixture) sets job->state =
* JOB_STAT_RUN AND calls sched_commit_job_alloc() -> sched_charge_account()
* SYNCHRONOUSLY at commit time -- no second round, no separate JOB_STATE_
* CHANGE event, no sched_account_jobs() call is needed for this fixture's
* own resInUse assertions to be meaningful (unlike test_sim_contention_
* parity.c's own reconciliation-based fixture, which predates this
* feature and therefore needed that extra machinery). One round is
* sufficient and sufficient to prove Requirement 3.1's "at every point in
* the replay" claim here, since sched_allocator() is a single thread that
* fully commits one job (including its resInUse charge) before pulling the
* next off jbuf -- job C's own allocation attempt therefore genuinely sees
* A+B's already-charged 2.0 resInUse, on both sides.
*
* ── Scope decision: core->sharedInUse not exercised here ─────────────────
* This fixture's contention is entirely host-scoped ("slots" on one
* worker); no shared/global resource is requested by any job. Req 3.1/3.2
* mention core->sharedInUse as one of several accounting fields that must
* match between drivers where applicable -- test_sim_contention_parity.c
* (the pre-existing driver-parity precedent this file extends the coverage
* of) also never exercises it. A limit-constrained scenario that DOES
* touch shared/host limit-vres accounting is covered separately by this
* task's sibling file, test_sim_contention_limit_driver_parity.c.
*
* ── bug-1598/1632/1763 fixture-safety requirement ─────────────────────────
* Both g_normal and g_sim below are `static` (file/process lifetime), NOT
* stack-local -- the two never-joined threads spawned against each read
* `&(g_normal.core)`/`&(g_sim.core)` for the remainder of the process's
* short life. Exactly one RUN_TEST() in this binary (this directory's own
* established convention for this fixture style), so the process exits
* immediately after, before either stack frame concern could ever apply
* anyway -- `static` storage is nevertheless used, not assumed safe by
* analogy, per this task's own explicit requirement to positively confirm
* this.
*/
#include <errno.h>
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "controller.capnp.h"
#include "decision.h"
#include "plugin.h"
#include "preason.h"
#include "resobj.h"
#include "sched/types.h"
#include "scheduler.h"
#include "sim.h"
#include "xstring.h"
/* Consumed the same "extern declared where consumed" idiom sim_loop.c
* itself and its own sibling real-scheduler_srcs tests already use (no
* shared header declares these). */
extern void sim_bootstrap_picker(xce_sched_sim_driver_t* sim);
extern void sim_bootstrap_seqjob_allocator(xce_sched_sim_driver_t* sim);
extern void sim_bootstrap_parjob_allocator(xce_sched_sim_driver_t* sim);
#define WORKER_NAME "contdp_worker"
#define QUEUE_NAME "normal"
#define TEST_RESREQ ((char*)"select[type==any] rusage[mem=1]")
#define WORKER_CAPACITY 2.0f /* 3 jobs x 1.0 "slots" each > 2.0 -- genuine,
* insufficient-for-all-of-them contention */
#define JOBA_ID ((uint64_t)510101ULL)
#define JOBB_ID ((uint64_t)510102ULL)
#define JOBC_ID ((uint64_t)510103ULL)
void setUp(void) { return; }
void tearDown(void) { return; }
/* ---------------------------------------------------------------------- */
/* Fixture helpers -- same techniques test_sched_election_sim_fixture.c/ */
/* test_sim_alloc_limit_fanout.c/test_sim_contention_parity.c each already */
/* establish, generalized to a core-agnostic, N-job shape. */
/* ---------------------------------------------------------------------- */
/* Same flags test_sim_contention_parity.c's own register_slots_resource()
* establishes -- LIMIT|CONSUMABLE|RELEASE|DEFAULT_HOST, needed for genuine
* host-capacity-check semantics (not merely a registered name) so that a
* real sched_check_host_satisfy() call genuinely denies an over-capacity
* request rather than trivially granting everything. */
static void register_slots_resource(var resmgr)
{
xce_resource_t res;
memset(&res, 0, sizeof(res));
res.name = strdup("slots");
res.valtype = XCE_VAL_NUMERIC;
res.ordtype = XCE_ORD_DECR;
res.flags = XCE_RESF_LIMIT | XCE_RESF_CONSUMABLE | XCE_RESF_RELEASE
| XCE_RESF_DEFAULT_HOST;
TEST_ASSERT_NOT_NULL(makeResource(resmgr, &res));
}
/* Same field-population step test_sim_alloc_limit_fanout.c's own
* add_test_worker_resval() establishes -- applied by hand since neither
* core_build_worker() nor make_sched_worker() is reachable without a real
* wire xce_worker_t payload. */
static void add_worker_resval(xce_sched_worker_t* worker,
xce_sched_core_t* core,
const char* resname, double value)
{
xce_resource_t* r = NULL;
xce_val_t* val = NULL;
int rc;
rc = key_find(core->resmgr, (void*)resname, (void**)&r);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(r);
val = (xce_val_t*)calloc(1, sizeof(*val));
TEST_ASSERT_NOT_NULL(val);
val->which = Value_u_fvalue;
val->u.fvalue = value;
zarray_put(&(worker->resvals), r->idx, val);
}
static xce_sched_worker_t* make_worker(xce_sched_core_t* core,
const char* name, double slots_capacity)
{
xce_sched_worker_t* w = (xce_sched_worker_t*)calloc(1, sizeof(*w));
TEST_ASSERT_NOT_NULL(w);
w->name = strdup(name);
w->status = 0;
w->idx = -1;
zarray_init(&(w->resvals), 128, (zarray_item_killer_t)kill_sched_resval);
zarray_init(&(w->resInUse), 128, (zarray_item_killer_t)kill_sched_resval);
add_worker_resval(w, core, "slots", slots_capacity);
return w;
}
/* push_contention_job - builds one single-task (mintasks=maxtasks=1) real
* job via make_sched_job() (the same function a live JOBS_PUSH/config-push
* uses) with the given jobid/queue_time, adds it to `core`, and notifies
* `picker` -- the same on_job_push sequence every real-scheduler_srcs
* fixture in this directory already establishes. `job_ctor_wrapper` is the
* xce_scheduler_t make_sched_job() itself still requires (void* arg cast,
* an unrelated out-of-boundary gap task 3.1's own fixture already routes
* around the same way) -- for the normal-mode fixture this is the real
* `&g_normal` itself; for the sim-mode fixture it is a separate, alias-only
* wrapper (see g_sim_job_wrapper below). */
static xce_sched_job_t* push_contention_job(xce_sched_core_t* core,
xce_scheduler_t* job_ctor_wrapper,
xce_sched_plugin_t* picker,
uint64_t jobid, uint64_t queue_time)
{
job_submit_data_t submit;
xce_job_t j;
xce_sched_job_t* job;
int rc;
memset(&submit, 0, sizeof(submit));
submit.queue = (char*)QUEUE_NAME;
submit.user = (char*)"contdp_user";
submit.project_name = (char*)"default";
submit.resreq = TEST_RESREQ;
submit.mintasks = 1;
submit.maxtasks = 1;
memset(&j, 0, sizeof(j));
j.jobid = jobid;
j.state = JOB_STAT_PEND;
j.sub_time = queue_time;
j.queue_time = queue_time;
j.submit = &submit;
job = make_sched_job(job_ctor_wrapper, &j);
TEST_ASSERT_NOT_NULL(job);
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, job->schedobj->tag);
/* make_sched_job()'s own parse_resreq() call does not thread
* submit.mintasks/maxtasks into the parsed primary's own minTasks/
* maxTasks (task 3.1's own documented gap, tasks.md Implementation
* Notes) -- set directly so this fixture deterministically exercises
* the seqjob path (maxTasks<=1) on both sides. */
job->schedobj->u.primary->resreq->minTasks = 1;
job->schedobj->u.primary->resreq->maxTasks = 1;
rc = sched_add_job(core, job);
TEST_ASSERT_TRUE(rc >= 0);
picker->picker_iface->on_job_push(picker, job);
return job;
}
/* run_contention_round - spawns the REAL, unmodified sched_job_picker()/
* sched_allocator() thread pair (sched_election.c) against `core`, runs one
* real session (xce_core_go() -> real drain -> xce_core_end_session()), and
* drains exactly `njobs` decisions plus the end-of-session NULL marker --
* same blocking-drain discipline every real-scheduler_srcs bare-core
* fixture in this directory already establishes. This is the ONE piece of
* driver-facing code this whole test relies on being identical for both
* "drivers" -- and it IS identical, since both calls below hand it nothing
* but a bare xce_sched_core_t*. */
static void run_contention_round(xce_sched_core_t* core, int njobs)
{
pthread_t picker_tid;
pthread_t allocator_tid;
xce_decision_t* d;
xce_decision_t* end_marker = (xce_decision_t*)0x1;
int rc;
int i;
rc = pthread_create(&picker_tid, NULL, sched_job_picker, core);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = pthread_create(&allocator_tid, NULL, sched_allocator, core);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = xce_core_go(core);
TEST_ASSERT_EQUAL_INT(0, rc);
for (i = 0; i < njobs; i++) {
d = NULL;
cbuf_get(&(core->decisions), &d);
TEST_ASSERT_NOT_NULL_MESSAGE(
d, "the real thread pair must carry every contending job all "
"the way to a committed decision");
}
end_marker = (xce_decision_t*)0x1;
cbuf_get(&(core->decisions), &end_marker);
TEST_ASSERT_NULL_MESSAGE(end_marker,
"decisions-cbuf entry after all real jobs must "
"be the end-of-session NULL marker");
rc = xce_core_end_session(core);
TEST_ASSERT_EQUAL_INT(0, rc);
}
/* ---------------------------------------------------------------------- */
/* The two fixtures under test -- see this file's own header comment */
/* ("bug-1598/1632/1763 fixture-safety requirement") for why `static`. */
/* ---------------------------------------------------------------------- */
static xce_scheduler_t g_normal;
static xce_sched_sim_driver_t g_sim;
static xce_scheduler_t g_sim_job_wrapper; /* setup-only, see
* push_contention_job()'s
* own doc comment */
/* ---------------------------------------------------------------------- */
/* The test */
/* ---------------------------------------------------------------------- */
static void
test_resource_contention_matches_between_normal_and_sim_drivers(void)
{
xce_sched_plugin_t* fcfs_normal = NULL;
xce_sched_plugin_t* fcfs_sim = NULL;
xce_sched_queue_t* queue_normal;
xce_sched_queue_t* queue_sim;
xce_sched_job_t* jobA_n;
xce_sched_job_t* jobB_n;
xce_sched_job_t* jobC_n;
xce_sched_job_t* jobA_s;
xce_sched_job_t* jobB_s;
xce_sched_job_t* jobC_s;
xce_sched_worker_t* w_normal;
xce_sched_worker_t* w_sim;
xce_resource_t* r_normal = NULL;
xce_resource_t* r_sim = NULL;
int residx_normal;
int residx_sim;
float resinuse_normal = -1.0f;
float resinuse_sim = -1.0f;
int rc;
/* ================================================================
* Normal-mode-shaped fixture
* ================================================================ */
memset(&g_normal, 0, sizeof(g_normal));
rc = xce_sched_core_init(&(g_normal.core));
TEST_ASSERT_EQUAL_INT(0, rc);
register_slots_resource(g_normal.core.resmgr);
fcfs_normal = builtin_fcfs_new_plugin();
TEST_ASSERT_NOT_NULL(fcfs_normal);
rc = plugin_register(&g_normal, fcfs_normal);
TEST_ASSERT_TRUE(rc >= 0);
g_normal.core.job_pick_plugin = (char*)"builtin_fcfs";
rc = register_seqjob_allocator(&g_normal);
TEST_ASSERT_TRUE(rc >= 0);
rc = register_parjob_allocator(&g_normal);
TEST_ASSERT_TRUE(rc >= 0);
queue_normal = (xce_sched_queue_t*)calloc(1, sizeof(*queue_normal));
TEST_ASSERT_NOT_NULL(queue_normal);
queue_normal->name = makeString(QUEUE_NAME);
rc = sched_add_queue(&(g_normal.core), queue_normal);
TEST_ASSERT_TRUE(rc >= 0);
fcfs_normal->picker_iface->on_queue_update(fcfs_normal, queue_normal);
w_normal = make_worker(&(g_normal.core), WORKER_NAME, WORKER_CAPACITY);
rc = sched_add_worker(&(g_normal.core), w_normal);
TEST_ASSERT_TRUE(rc >= 0);
push_contention_job(&(g_normal.core), &g_normal, fcfs_normal, JOBA_ID,
1000ULL);
push_contention_job(&(g_normal.core), &g_normal, fcfs_normal, JOBB_ID,
1001ULL);
push_contention_job(&(g_normal.core), &g_normal, fcfs_normal, JOBC_ID,
1002ULL);
run_contention_round(&(g_normal.core), 3);
/* ================================================================
* Sim-mode-shaped fixture -- identical population, only the top-level
* struct embedding `core` differs.
* ================================================================ */
memset(&g_sim, 0, sizeof(g_sim));
rc = xce_sched_core_init(&(g_sim.core));
TEST_ASSERT_EQUAL_INT(0, rc);
register_slots_resource(g_sim.core.resmgr);
sim_bootstrap_picker(&g_sim);
rc = key_find(g_sim.core.plugins, "builtin_fcfs", (void**)&fcfs_sim);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(fcfs_sim);
sim_bootstrap_seqjob_allocator(&g_sim);
sim_bootstrap_parjob_allocator(&g_sim);
TEST_ASSERT_NOT_NULL_MESSAGE(
g_sim.core.seqjob_allocator,
"sim_bootstrap_seqjob_allocator() must populate core->seqjob_"
"allocator (task 4.1) -- otherwise this fixture cannot reach a real "
"allocation attempt at all");
queue_sim = (xce_sched_queue_t*)calloc(1, sizeof(*queue_sim));
TEST_ASSERT_NOT_NULL(queue_sim);
queue_sim->name = makeString(QUEUE_NAME);
rc = sched_add_queue(&(g_sim.core), queue_sim);
TEST_ASSERT_TRUE(rc >= 0);
fcfs_sim->picker_iface->on_queue_update(fcfs_sim, queue_sim);
w_sim = make_worker(&(g_sim.core), WORKER_NAME, WORKER_CAPACITY);
rc = sched_add_worker(&(g_sim.core), w_sim);
TEST_ASSERT_TRUE(rc >= 0);
/* g_sim_job_wrapper: see push_contention_job()'s own doc comment --
* make_sched_job() still takes `void* arg` cast to xce_scheduler_t*
* (an unrelated, out-of-boundary gap task 3.1's own fixture already
* routes around identically). Its core.resmgr/workers/queues are
* ALIASED (not copied) to g_sim.core's own, the same technique
* test_sim_alloc_limit_fanout.c's own job_wrapper establishes, so
* parse_resreq()/sched_get_queue() resolve against the exact same
* registries this fixture's real dispatch attempt uses. Never passed
* to anything else -- never pthread_create()'d, never dereferenced by
* sched_job_picker()/sched_allocator(). */
memset(&g_sim_job_wrapper, 0, sizeof(g_sim_job_wrapper));
rc = xce_sched_core_init(&(g_sim_job_wrapper.core));
TEST_ASSERT_EQUAL_INT(0, rc);
g_sim_job_wrapper.core.resmgr = g_sim.core.resmgr;
g_sim_job_wrapper.core.workers = g_sim.core.workers;
g_sim_job_wrapper.core.queues = g_sim.core.queues;
push_contention_job(&(g_sim.core), &g_sim_job_wrapper, fcfs_sim, JOBA_ID,
1000ULL);
push_contention_job(&(g_sim.core), &g_sim_job_wrapper, fcfs_sim, JOBB_ID,
1001ULL);
push_contention_job(&(g_sim.core), &g_sim_job_wrapper, fcfs_sim, JOBC_ID,
1002ULL);
run_contention_round(&(g_sim.core), 3);
/* ================================================================
* Assertions: each side's own outcome, THEN cross-driver parity.
* ================================================================ */
jobA_n = sched_get_job(&(g_normal.core), JOBA_ID);
jobB_n = sched_get_job(&(g_normal.core), JOBB_ID);
jobC_n = sched_get_job(&(g_normal.core), JOBC_ID);
jobA_s = sched_get_job(&(g_sim.core), JOBA_ID);
jobB_s = sched_get_job(&(g_sim.core), JOBB_ID);
jobC_s = sched_get_job(&(g_sim.core), JOBC_ID);
TEST_ASSERT_NOT_NULL(jobA_n);
TEST_ASSERT_NOT_NULL(jobB_n);
TEST_ASSERT_NOT_NULL(jobC_n);
TEST_ASSERT_NOT_NULL(jobA_s);
TEST_ASSERT_NOT_NULL(jobB_s);
TEST_ASSERT_NOT_NULL(jobC_s);
/* Concrete, driver-independent expectations: worker capacity 2.0,
* three single-task jobs each auto-injecting 1.0 "slots", queue_time
* ascending A<B<C (builtin_fcfs's own priority-desc/queue_time-asc
* ordq comparator) -- A and B must exactly saturate capacity, C must
* be denied. This is genuine contention (Req 3.2's own wording:
* "capacity insufficient to satisfy all of them concurrently"), not a
* fixture where every job happens to fit. */
TEST_ASSERT_EQUAL_UINT_MESSAGE(JOB_STAT_RUN, jobA_n->state,
"normal mode: job A must genuinely dispatch");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
JOB_STAT_RUN, jobB_n->state,
"normal mode: job B must genuinely dispatch (A+B=2.0 exactly "
"saturates the worker's capacity)");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
JOB_STAT_PEND, jobC_n->state,
"normal mode: job C must be genuinely denied -- zero headroom "
"remains once A and B are charged");
TEST_ASSERT_NOT_NULL(jobA_n->accounts);
TEST_ASSERT_NOT_NULL(jobB_n->accounts);
TEST_ASSERT_NULL(jobC_n->accounts);
TEST_ASSERT_EQUAL_UINT_MESSAGE(JOB_STAT_RUN, jobA_s->state,
"sim mode: job A must genuinely dispatch");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
JOB_STAT_RUN, jobB_s->state,
"sim mode: job B must genuinely dispatch (A+B=2.0 exactly "
"saturates the worker's capacity)");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
JOB_STAT_PEND, jobC_s->state,
"sim mode: job C must be genuinely denied -- zero headroom remains "
"once A and B are charged");
TEST_ASSERT_NOT_NULL(jobA_s->accounts);
TEST_ASSERT_NOT_NULL(jobB_s->accounts);
TEST_ASSERT_NULL(jobC_s->accounts);
/* Driver PARITY -- the actual point of this task (Req 3.1/3.2): each
* job's outcome must match byte-for-byte between drivers, not merely
* each independently look plausible in isolation. */
TEST_ASSERT_EQUAL_UINT_MESSAGE(
jobA_n->state, jobA_s->state,
"job A: normal and sim mode must reach the identical admission "
"outcome under identical contention");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
jobB_n->state, jobB_s->state,
"job B: normal and sim mode must reach the identical admission "
"outcome under identical contention");
TEST_ASSERT_EQUAL_UINT_MESSAGE(
jobC_n->state, jobC_s->state,
"job C: normal and sim mode must reach the identical (denied) "
"outcome under identical contention");
/* Per-job accounting-state parity (job->accounts, per the task brief's
* own list). make_job_account()'s field mapping (account->reserved =
* alloc->value, xcealloc.c) is what a real committed DISPATCH populates
* -- not .used, which stays 0.0 on the per-job account structure itself
* (test_sim_contention_parity.c's own identical observation). */
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
1.0f, jobA_n->accounts->accounts[0]->accounts[0]->reserved,
"normal mode: job A's charge must be exactly its 1-task auto-"
"injected want (1.0)");
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
1.0f, jobB_n->accounts->accounts[0]->accounts[0]->reserved,
"normal mode: job B's charge must be exactly its 1-task auto-"
"injected want (1.0)");
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
1.0f, jobA_s->accounts->accounts[0]->accounts[0]->reserved,
"sim mode: job A's charge must be exactly its 1-task auto-"
"injected want (1.0)");
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
1.0f, jobB_s->accounts->accounts[0]->accounts[0]->reserved,
"sim mode: job B's charge must be exactly its 1-task auto-"
"injected want (1.0)");
/* Final resource-accounting state parity (w->resInUse, per the task
* brief's own list and Requirement 3.1's "identical resource-
* accounting state ... at every point"). */
rc = key_find(g_normal.core.resmgr, (void*)"slots", (void**)&r_normal);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(r_normal);
residx_normal = r_normal->idx;
rc = key_find(g_sim.core.resmgr, (void*)"slots", (void**)&r_sim);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(r_sim);
residx_sim = r_sim->idx;
rc = get_resvals(&(w_normal->resInUse), 1, &residx_normal,
&resinuse_normal);
TEST_ASSERT_EQUAL_INT(1, rc); /* get_resvals() returns nres on success */
rc = get_resvals(&(w_sim->resInUse), 1, &residx_sim, &resinuse_sim);
TEST_ASSERT_EQUAL_INT(1, rc);
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
WORKER_CAPACITY, resinuse_normal,
"normal mode: worker resInUse must reach exactly full saturation "
"(A+B=2.0 of 2.0 total capacity)");
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
WORKER_CAPACITY, resinuse_sim,
"sim mode: worker resInUse must reach exactly full saturation "
"(A+B=2.0 of 2.0 total capacity)");
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
resinuse_normal, resinuse_sim,
"driver parity: worker resInUse must match byte-for-byte between "
"normal and sim mode under identical contention -- this is the "
"load-bearing assertion scheduler-driver-equivalence.md's state-"
"equivalence invariant exists to protect");
printf("test_resource_contention_matches_between_normal_and_sim_"
"drivers: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_resource_contention_matches_between_normal_and_sim_drivers);
return UNITY_END();
}