test(scheduler-snapshot-job-allocation): backward-compat for allocation-less snapshots

Task 3.2: adds test_snapshot_load_leaves_running_job_without_allocation_at_zero_resinuse,
covering the specific Req 3.5 scenario the existing PENDING-job test didn't
-- a RUNNING job whose allocation was never captured (simulating a
pre-feature-saved snapshot). With a real worker/resource present (making
the assertion non-vacuous), confirms: load succeeds with no error,
job->schedobj is still valid (task 1.2, independent of allocation data),
job->allocs/accounts stay NULL, and resInUse correctly reads 0.0 via the
real accounting chain -- a clean degrade, no special-casing, matching
today's pre-feature behavior exactly.

Test-only task; no production code changed. Per this spec's own
"pre-release product" scope note, no archived legacy snapshot file is
needed as a fixture -- a freshly-built wire job with allocs unset
exercises the identical code path.

Requirements: 3.5
This commit is contained in:
Rongsong Shen 2026-08-27 15:42:37 +00:00
parent 7481cedb3a
commit f0fbebdeab
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
2 changed files with 164 additions and 1 deletions

View file

@ -45,7 +45,7 @@
- _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.3, 2.4_ - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.3, 2.4_
- _Depends: 1.2, 2.1, 2.2_ - _Depends: 1.2, 2.1, 2.2_
- [ ] 3.2 Verify a snapshot with no allocation data loads cleanly - [x] 3.2 Verify a snapshot with no allocation data loads cleanly
- Extend the round-trip coverage with a saved state containing no allocation data for its jobs (the natural case for a job that was never dispatched, or a save taken before this feature existed). - Extend the round-trip coverage with a saved state containing no allocation data for its jobs (the natural case for a job that was never dispatched, or a save taken before this feature existed).
- Assert the load succeeds with no error, and every such job's resource contribution reads as zero/unavailable, identical to today's behavior. - Assert the load succeeds with no error, and every such job's resource contribution reads as zero/unavailable, identical to today's behavior.
- Observable completion: the backward-compatibility test passes without any special-casing visible to the caller. - Observable completion: the backward-compatibility test passes without any special-casing visible to the caller.

View file

@ -2124,6 +2124,167 @@ test_snapshot_round_trip_mixed_jobs_shared_pool_resinuse_and_teardown_independen
TEST_ASSERT_EQUAL_FLOAT(1.0f, restored_shared_lic); TEST_ASSERT_EQUAL_FLOAT(1.0f, restored_shared_lic);
} }
/* ---------------------------------------------------------------------- */
/* scheduler-snapshot-job-allocation task 3.2 (Req 3.5): the backward- */
/* compatibility case made explicit for the scenario Req 3.5's own text and */
/* the original bug report actually describe -- a job that was ALREADY */
/* RUNNING as of a PRE-feature build's snapshot save point, so its */
/* dispatch-time allocation was never captured onto the wire at all. This */
/* is distinct from task 2.2's own load-side companion test above */
/* (test_snapshot_load_leaves_allocs_and_accounts_null_for_job_without_ */
/* allocation), which only covers a PENDING job -- the "never dispatched" */
/* case, not the "dispatched before a pre-feature build could capture it" */
/* case Req 3.5 is actually about. */
/* */
/* Since the wire job's `allocs` field is a plain optional pointer, not a */
/* schema-versioned/gated blob (brief.md's "Backward compatibility note */
/* (pre-release product)"), a pre-feature build's snapshot and "this */
/* RUNNING job's allocs happens to be NULL today" are the literal same wire */
/* shape -- so this test builds the wire job directly with `allocs == NULL` */
/* and `state == JOB_STAT_RUN`, exactly what a pre-feature build would have */
/* produced for a job that was running when it saved. */
/* */
/* Asserts, all against a fresh core with NO special-casing visible to the */
/* caller: */
/* (a) load succeeds (rc == 0); */
/* (b) job->state correctly restores as RUNNING -- state round-trips */
/* independently of allocs, this feature does not touch it; */
/* (c) job->schedobj is non-NULL (task 1.2's fix -- independent of */
/* allocation data, so a RUNNING job from a legacy snapshot is just */
/* as safe to hand to the allocator/picker as any other restored */
/* job); */
/* (d) job->allocs and job->accounts are both NULL -- clean degrade, not */
/* a fabricated allocation or a crash; */
/* (e) with a REAL worker present, the REAL sched_account_jobs() chain */
/* (test_sched_account_jobs() above, task 3.1's own real */
/* reimplementation) correctly contributes ZERO from this job to the */
/* worker's resInUse -- the concrete "used=0, identical to today's */
/* behavior" claim Req 3.5 and the originally-reported "info worker" */
/* symptom are both actually about, not merely that job->accounts is */
/* NULL in isolation. */
/* ---------------------------------------------------------------------- */
static void
test_snapshot_load_leaves_running_job_without_allocation_at_zero_resinuse(void)
{
xce_sched_core_t core;
sched_snapshot_data_t in;
xce_jobs_data_t jobs_data;
xce_resource_data_t resources_data;
xce_resource_t res_slots;
xce_resource_t* resources_arr[1];
xce_workers_data_t workers_data;
xce_worker_t wire_worker;
xce_worker_t* workers_arr[1];
xce_val_t worker_slots_val;
xce_name_val_t worker_slots_nv;
xce_name_val_t* worker_resvals[1];
xce_queue_t wire_queue;
xce_queue_t* queues[1];
xce_job_t* wire_job;
xce_job_t* jobs[1];
xce_sched_job_t* restored;
xce_sched_worker_t* worker;
xce_resource_t* slots_res;
float resinuse_slots = -1.0f;
int rc;
memset(&g_stub_state_a, 0, sizeof(g_stub_state_a));
fixture_init(&core, &g_stub_plugin_a);
/* one real resource ("slots") and one real worker advertising it -- so
* assertion (e) below is a genuine "worker present, zero charged" proof,
* not vacuously true because no worker/resource exists at all. */
memset(&res_slots, 0, sizeof(res_slots));
res_slots.name = "slots";
resources_arr[0] = &res_slots;
memset(&resources_data, 0, sizeof(resources_data));
resources_data.count = 1;
resources_data.resources = resources_arr;
memset(&worker_slots_val, 0, sizeof(worker_slots_val));
worker_slots_val.which = XCE_VAL_NUMERIC;
worker_slots_val.u.ivalue32 = 16;
memset(&worker_slots_nv, 0, sizeof(worker_slots_nv));
worker_slots_nv.name = "slots";
worker_slots_nv.val = &worker_slots_val;
worker_resvals[0] = &worker_slots_nv;
memset(&wire_worker, 0, sizeof(wire_worker));
wire_worker.name = "hostA";
wire_worker.status = 0; /* available */
wire_worker.nresvals_to_sched = 1;
wire_worker.resvals_to_sched = worker_resvals;
workers_arr[0] = &wire_worker;
memset(&workers_data, 0, sizeof(workers_data));
workers_data.count = 1;
workers_data.workers = workers_arr;
memset(&wire_queue, 0, sizeof(wire_queue));
wire_queue.queue = "normal";
wire_queue.priority = 1;
wire_queue.status = QUEUE_STAT_OPEN;
queues[0] = &wire_queue;
/* the pre-feature wire shape: a job already RUNNING as of the snapshot's
* save point, whose allocs field was never populated -- make_job()
* never sets ->allocs (calloc()-zeroed NULL default, same helper task
* 2.2's own PENDING-job companion test uses above), but here with state
* forced to JOB_STAT_RUN to simulate "already running when a
* pre-feature build took the snapshot" (Req 3.5's actual described
* scenario), not "pending / never dispatched". */
wire_job = make_job(801, JOB_STAT_RUN, "normal", "alice", "proj1");
jobs[0] = wire_job;
memset(&jobs_data, 0, sizeof(jobs_data));
jobs_data.count = 1;
jobs_data.jobs = jobs;
memset(&in, 0, sizeof(in));
in.jobs = &jobs_data;
in.queues = queues;
in.n_queues = 1;
in.resources = &resources_data;
in.workers = &workers_data;
rc = xce_sched_core_load_snapshot(&core, &in);
TEST_ASSERT_EQUAL_INT(0, rc);
/* (a)+(b): load succeeded, state correctly restored as RUNNING. */
restored = sched_get_job(&core, 801);
TEST_ASSERT_NOT_NULL(restored);
TEST_ASSERT_EQUAL_UINT32(JOB_STAT_RUN, restored->state);
/* (c): task 1.2's fix -- schedobj construction is independent of
* allocation data, so a RUNNING job with no allocs still gets a valid,
* non-NULL scheduling representation. */
TEST_ASSERT_NOT_NULL(restored->schedobj);
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, restored->schedobj->tag);
TEST_ASSERT_NOT_NULL(restored->schedobj->u.primary);
/* (d): clean degrade, not a fabricated allocation. */
TEST_ASSERT_NULL(restored->allocs);
TEST_ASSERT_NULL(restored->accounts);
/* (e): the REAL accounting chain, run against the restored core, charges
* NOTHING for this job (job->accounts == NULL skips it, same guard live
* dispatch's own sched_account_jobs() uses) -- the worker's resInUse for
* "slots" reads back as exactly 0, matching today's pre-feature
* behavior, not an uninitialized/undefined value. */
worker = sched_get_worker(&core, "hostA");
TEST_ASSERT_NOT_NULL(worker);
rc = key_find(core.resmgr, (void*)"slots", (void**)&slots_res);
TEST_ASSERT_GREATER_THAN_INT(0, rc);
TEST_ASSERT_NOT_NULL(slots_res);
rc = test_sched_account_jobs(&core);
TEST_ASSERT_EQUAL_INT(0, rc);
get_resvals(&(worker->resInUse), 1, &(slots_res->idx), &resinuse_slots);
TEST_ASSERT_EQUAL_FLOAT(0.0f, resinuse_slots);
}
int main(void) int main(void)
{ {
UNITY_BEGIN(); UNITY_BEGIN();
@ -2146,5 +2307,7 @@ int main(void)
test_snapshot_load_leaves_allocs_and_accounts_null_for_job_without_allocation); test_snapshot_load_leaves_allocs_and_accounts_null_for_job_without_allocation);
RUN_TEST( RUN_TEST(
test_snapshot_round_trip_mixed_jobs_shared_pool_resinuse_and_teardown_independence); test_snapshot_round_trip_mixed_jobs_shared_pool_resinuse_and_teardown_independence);
RUN_TEST(
test_snapshot_load_leaves_running_job_without_allocation_at_zero_resinuse);
return UNITY_END(); return UNITY_END();
} }