/* * 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 #include #include #include #include #include #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 Astate, "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(); }