/* * test_alloc_cache.c - unit test for the session-level unsatisfiable- * resreq cache (scheduler-allocation spec, task 2.2, Req 7.1, 7.3, 7.4, * 7.5). * * White-box test TU: #includes ../src/alloc_cache.c directly (same * convention test_host_iter.c/test_job_update.c/test_handlers.c already * establish for this test suite), so it exercises the REAL * cache_key_hash_/cache_key_cmp_/cache_key_assign_/cache_key_destroy_ and * cache_lookup()/cache_insert()/cache_clear() implementations, not a stub. * * alloc_cache.c only depends on kv.h (libxce, already linked) and * scheduler.h/sched/types.h (header-only struct definitions) -- it never * touches xce_scheduler_t.core, so this fixture never calls * xce_sched_core_init(); a bare memset()'d xce_scheduler_t with its own * unsatisfiable_cache field constructed directly (mirroring sched_init()'s * own construction call, scheduler.c) is sufficient. * * Covers, per this task's Observable line: * - same-key-twice HIT (not just insert-then-miss-on-a-different-key, * which the cache_key_cmp_() inverted-polarity bug already found and * fixed during design review -- requirements.md's "New types (Req 7)" * section -- would still have passed). This is deliberately the FIRST * test below. * - a lookup differing in any single field (each of the four resreq * fingerprint fields, and each of the three identity fields) is a * miss. * - reason-category round-trip: insert with category X, hit later * returns X -- tested with two distinct category values to rule out a * trivial always-returns-0 implementation. * - clear() empties all entries. */ #include #include #include #include #include #include "sched/alloc_cache.h" #include "scheduler.h" /* Pull in the real cache_key_ops/cache_lookup/cache_insert/cache_clear * implementation under test. */ #include "../src/alloc_cache.c" void setUp(void) { return; } void tearDown(void) { return; } /* ---------------------------------------------------------------------- */ /* Fixture helpers */ /* ---------------------------------------------------------------------- */ /* * init_sched - a bare xce_scheduler_t with only unsatisfiable_cache * constructed (this cache never touches sched->core). Mirrors sched_init()'s * own construction call (scheduler.c) exactly, so the fixture and * production code can never silently drift. */ static void init_sched(xce_scheduler_t* sched) { kv_params_t params = { .nslots = XCE_UNSAT_CACHE_NSLOTS, .keyops = &cache_key_ops, .datakiller = NULL }; memset(sched, 0, sizeof(*sched)); sched->unsatisfiable_cache = makeInstance(&kvtab_type, ¶ms); TEST_ASSERT_NOT_NULL(sched->unsatisfiable_cache); } static void fini_sched(xce_scheduler_t* sched) { if (sched->unsatisfiable_cache != NULL) { destroy(sched->unsatisfiable_cache); sched->unsatisfiable_cache = NULL; } } /* * init_primary_obj - a minimal, valid xce_sched_obj_t/xce_primary_schedobj_t * pair: tag == XCE_RESREQ_PRIMARY, u.primary pointing at `primary`, with * `primary`'s resreq_fp set to the given fingerprint fields and the * object's own user/queue/project identity pointers set. Every other field * (owner/resreq/reason on primary; glimits/hlimits/alloc/reason on obj) is * left zeroed -- cache_lookup()/cache_insert() never touch them. */ static void init_primary_obj(xce_sched_obj_t* obj, xce_primary_schedobj_t* primary, int min_tasks, int max_tasks, uint32_t select_node_id, uint64_t rusage_hash, const char* user, const char* queue, const char* project) { memset(obj, 0, sizeof(*obj)); memset(primary, 0, sizeof(*primary)); primary->resreq_fp.min_tasks = min_tasks; primary->resreq_fp.max_tasks = max_tasks; primary->resreq_fp.select_node_id = select_node_id; primary->resreq_fp.rusage_hash = rusage_hash; obj->tag = XCE_RESREQ_PRIMARY; obj->u.primary = primary; obj->user = (char*)user; obj->queue = (char*)queue; obj->project = (char*)project; } /* Baseline fixture fingerprint + identity, reused (with one field varied * at a time) across the miss tests below. Distinct string literals (not * merely distinct content) are used for the identity fields so pointer * inequality is unambiguous regardless of any compiler string-literal * pooling -- this cache's cmp is pointer-based, exactly mirroring the real * xstring-interned-pointer contract identity fields carry in production. */ #define BASE_MIN_TASKS 1 #define BASE_MAX_TASKS 4 #define BASE_SELECT_NODE_ID 100u #define BASE_RUSAGE_HASH ((uint64_t)0xdeadbeefULL) static const char BASE_USER[] = "alice"; static const char BASE_QUEUE[] = "normal"; static const char BASE_PROJECT[] = "proj1"; /* ---------------------------------------------------------------------- */ /* Tests */ /* ---------------------------------------------------------------------- */ /* * THE critical test (design.md's own words: "a functionally dead cache * from day one would not fail any test that doesn't specifically assert a * hit"): insert once, then look up an INDEPENDENTLY constructed key with * every field identical (including pointer-identical identity fields) -- * must be a hit, returning the exact category given at insert time. A * cache_key_cmp_() with the earlier, inverted boolean polarity would fail * this test (every lookup a false miss) while still passing a weaker * insert-then-miss-on-a-different-key round-trip. */ static void test_same_key_twice_is_a_hit(void) { xce_scheduler_t sched; xce_sched_obj_t obj1, obj2; xce_primary_schedobj_t primary1, primary2; int category = 42; int out = -1; int rc; init_sched(&sched); init_primary_obj(&obj1, &primary1, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); init_primary_obj(&obj2, &primary2, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); rc = cache_insert(&sched, &obj1, category); TEST_ASSERT_EQUAL_INT(0, rc); rc = cache_lookup(&sched, &obj2, &out); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_EQUAL_INT(category, out); fini_sched(&sched); } /* * A lookup before any insert is a plain miss. */ static void test_lookup_before_any_insert_is_a_miss(void) { xce_scheduler_t sched; xce_sched_obj_t obj; xce_primary_schedobj_t primary; int out = -1; int rc; init_sched(&sched); init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); rc = cache_lookup(&sched, &obj, &out); TEST_ASSERT_EQUAL_INT(0, rc); fini_sched(&sched); } /* * differing_field_misses - shared body for "a lookup differing in exactly * one field is a miss": insert the base key with `category`, then look up * a second key built from the given (possibly-varied) fields -- must miss. */ static void assert_variant_misses(int min_tasks, int max_tasks, uint32_t select_node_id, uint64_t rusage_hash, const char* user, const char* queue, const char* project) { xce_scheduler_t sched; xce_sched_obj_t base_obj, variant_obj; xce_primary_schedobj_t base_primary, variant_primary; int out = -1; int rc; init_sched(&sched); init_primary_obj(&base_obj, &base_primary, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); init_primary_obj(&variant_obj, &variant_primary, min_tasks, max_tasks, select_node_id, rusage_hash, user, queue, project); rc = cache_insert(&sched, &base_obj, 7); TEST_ASSERT_EQUAL_INT(0, rc); rc = cache_lookup(&sched, &variant_obj, &out); TEST_ASSERT_EQUAL_INT(0, rc); fini_sched(&sched); } static void test_miss_on_differing_min_tasks(void) { assert_variant_misses(BASE_MIN_TASKS + 1, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); } static void test_miss_on_differing_max_tasks(void) { assert_variant_misses(BASE_MIN_TASKS, BASE_MAX_TASKS + 1, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); } static void test_miss_on_differing_select_node_id(void) { assert_variant_misses(BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID + 1, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); } static void test_miss_on_differing_rusage_hash(void) { assert_variant_misses(BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH ^ 0x1ULL, BASE_USER, BASE_QUEUE, BASE_PROJECT); } static void test_miss_on_differing_user(void) { static const char other_user[] = "bob"; assert_variant_misses(BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, other_user, BASE_QUEUE, BASE_PROJECT); } static void test_miss_on_differing_queue(void) { static const char other_queue[] = "priority"; assert_variant_misses(BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, other_queue, BASE_PROJECT); } static void test_miss_on_differing_project(void) { static const char other_project[] = "proj2"; assert_variant_misses(BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, other_project); } /* * Req 7.5: a cache hit reports the SAME reason category the original * miss's exhausted attempt stored -- tested with two distinct category * values (on two distinct keys, since re-inserting the identical key * would hit kvtab_add()'s own -EEXIST contract) to rule out a trivial * always-returns-0 (or always-returns-the-first-inserted-value) * implementation. */ static void test_reason_category_round_trip_two_distinct_categories(void) { xce_scheduler_t sched; xce_sched_obj_t obj_a, obj_b; xce_primary_schedobj_t primary_a, primary_b; static const char user_b[] = "carol"; int out_a = -1, out_b = -1; int rc; init_sched(&sched); init_primary_obj(&obj_a, &primary_a, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); /* Distinct key (differs only in identity) so this doesn't collide with * obj_a's own insert. */ init_primary_obj(&obj_b, &primary_b, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, user_b, BASE_QUEUE, BASE_PROJECT); rc = cache_insert(&sched, &obj_a, 11); TEST_ASSERT_EQUAL_INT(0, rc); rc = cache_insert(&sched, &obj_b, 99); TEST_ASSERT_EQUAL_INT(0, rc); rc = cache_lookup(&sched, &obj_a, &out_a); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_EQUAL_INT(11, out_a); rc = cache_lookup(&sched, &obj_b, &out_b); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_EQUAL_INT(99, out_b); fini_sched(&sched); } /* * cache_clear() empties every entry: insert two distinct keys, clear, then * confirm BOTH now miss (not just the one inserted last). */ static void test_clear_empties_all_entries(void) { xce_scheduler_t sched; xce_sched_obj_t obj_a, obj_b; xce_primary_schedobj_t primary_a, primary_b; static const char user_b[] = "dave"; int out = -1; int rc; init_sched(&sched); init_primary_obj(&obj_a, &primary_a, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); init_primary_obj(&obj_b, &primary_b, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, user_b, BASE_QUEUE, BASE_PROJECT); TEST_ASSERT_EQUAL_INT(0, cache_insert(&sched, &obj_a, 1)); TEST_ASSERT_EQUAL_INT(0, cache_insert(&sched, &obj_b, 2)); /* Sanity: both are hits before clear. */ TEST_ASSERT_EQUAL_INT(1, cache_lookup(&sched, &obj_a, &out)); TEST_ASSERT_EQUAL_INT(1, cache_lookup(&sched, &obj_b, &out)); cache_clear(&sched); TEST_ASSERT_NOT_NULL(sched.unsatisfiable_cache); rc = cache_lookup(&sched, &obj_a, &out); TEST_ASSERT_EQUAL_INT(0, rc); rc = cache_lookup(&sched, &obj_b, &out); TEST_ASSERT_EQUAL_INT(0, rc); fini_sched(&sched); } /* * Re-inserting the identical key without an intervening clear propagates * kvtab_add()'s own -EEXIST contract -- exercising cache_insert()'s error * path. */ static void test_insert_duplicate_key_returns_eexist(void) { xce_scheduler_t sched; xce_sched_obj_t obj1, obj2; xce_primary_schedobj_t primary1, primary2; int rc; init_sched(&sched); init_primary_obj(&obj1, &primary1, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); init_primary_obj(&obj2, &primary2, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); TEST_ASSERT_EQUAL_INT(0, cache_insert(&sched, &obj1, 5)); rc = cache_insert(&sched, &obj2, 6); TEST_ASSERT_EQUAL_INT(-EEXIST, rc); fini_sched(&sched); } /* * Defensive argument checking: NULL sched/obj/out_reason_category, and a * non-primary tag object, are all rejected rather than crashing. */ static void test_argument_validation(void) { xce_scheduler_t sched; xce_sched_obj_t obj; xce_primary_schedobj_t primary; xce_sched_obj_t non_primary_obj; int out = -1; init_sched(&sched); init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); memset(&non_primary_obj, 0, sizeof(non_primary_obj)); non_primary_obj.tag = XCE_RESREQ_PRIMARY + 1; /* anything != PRIMARY */ TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(NULL, &obj, &out)); TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(&sched, NULL, &out)); TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(&sched, &obj, NULL)); TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(&sched, &non_primary_obj, &out)); TEST_ASSERT_EQUAL_INT(-EINVAL, cache_insert(NULL, &obj, 1)); TEST_ASSERT_EQUAL_INT(-EINVAL, cache_insert(&sched, NULL, 1)); TEST_ASSERT_EQUAL_INT(-EINVAL, cache_insert(&sched, &non_primary_obj, 1)); /* cache_clear(NULL) must not crash. */ cache_clear(NULL); fini_sched(&sched); } /* ---------------------------------------------------------------------- */ /* Task 1.1 (scheduler-sim-live-allocation spec, Req 5.2): the */ /* unsatisfiable-cache callback seam. */ /* ---------------------------------------------------------------------- */ /* * A freshly-zeroed xce_sched_core_t (the same memset() default * xce_sched_core_init() applies -- core.c is not linked into this * white-box TU, so this test zeroes a bare instance directly rather than * calling that function) leaves BOTH new callback fields NULL. This is * the exact state every simulation-mode xce_sched_core_t is in forever -- * sim_loop.c never registers either callback (Req 5 AC 17 "Simulation * isolation"). */ static void test_seam_fields_default_null_on_zeroed_core(void) { xce_sched_core_t core; memset(&core, 0, sizeof(core)); TEST_ASSERT_NULL(core.unsatisfiable_lookup_cb); TEST_ASSERT_NULL(core.unsatisfiable_populate_cb); } /* * THE Observable this task's own TDD note names: "with both callbacks * left NULL, the allocation dispatch chokepoint always computes fresh * with no crash and no incorrect skip." The real chokepoint * (sched_make_primary_alloc_(), scheduler.c) is migrated to actually * consult this seam by a LATER task (tasks.md 2.2, boundary "Core-Safe * Allocation Chain, Unsatisfiable-Cache Callback Seam") -- this task adds * only the seam itself, not that call site. This test proves the seam's * own NULL-safety contract in isolation, using the identical NULL-check * shape design.md's own invariant specifies for that future call site * ("the allocation chain ... only ever NULL-checks and calls these"): * with both fields NULL, a NULL-checked consult never dereferences either * function pointer (no crash), is never mistaken for a cache HIT (no * incorrect skip -- `hit` stays 0, meaning "compute fresh"), and a * NULL-checked populate attempt is silently skipped, not a fatal error. */ static void test_null_callbacks_mean_always_compute_fresh(void) { xce_sched_core_t core; xce_sched_obj_t obj; xce_primary_schedobj_t primary; int category = -1; int hit = -1; /* poison: neither 0 nor 1 yet */ memset(&core, 0, sizeof(core)); init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); /* Prove the guard's PRECONDITION explicitly, immediately before relying * on it below -- not just that the end result happens to be "compute * fresh", but that the NULL-ness this test's two guarded calls depend * on genuinely holds at the exact point each guard is evaluated. A * design that accidentally called through a non-NULL stale/garbage * pointer here (e.g. an uninitialized field instead of a true NULL) * could still coincidentally return 0/not crash on some platforms; * asserting NULL directly, right where each guard fires, rules that * out. */ TEST_ASSERT_NULL(core.unsatisfiable_lookup_cb); if (core.unsatisfiable_lookup_cb != NULL) { hit = core.unsatisfiable_lookup_cb(&core, &obj, &category); } else { hit = 0; /* no cache registered => always a miss => compute fresh */ } TEST_ASSERT_EQUAL_INT(0, hit); /* A NULL-checked populate attempt must be a true no-op, not a crash -- * same "assert the guard's own precondition, right at the guard" * discipline as the lookup check just above. */ TEST_ASSERT_NULL(core.unsatisfiable_populate_cb); if (core.unsatisfiable_populate_cb != NULL) { core.unsatisfiable_populate_cb(&core, &obj, 7); } } /* * Normal-mode observable (this task's own note): "a normal-mode test * confirming the cache is still consulted/populated exactly as before * this change." xce_alloc_cache_lookup_cb()/xce_alloc_cache_populate_cb() * (alloc_cache.c) are the two adapters main.c registers on the real * xce_scheduler_t's core (xce_sched_core_t core is xce_scheduler_t's * first member, scheduler.h -- the same pointer-to-first-member idiom * xce_alloc_cache_clear_cb() already uses). This test calls them through * core.unsatisfiable_lookup_cb/unsatisfiable_populate_cb exactly as a * normal-mode driver would, and confirms the result is identical to * calling cache_lookup()/cache_insert() directly -- proving the adapters * are a transparent passthrough, not a behavior change. */ static void test_lookup_and_populate_cb_adapters_match_direct_cache_calls(void) { xce_scheduler_t sched; xce_sched_obj_t obj; xce_primary_schedobj_t primary; int out_direct = -1; int out_via_cb = -1; int rc; init_sched(&sched); sched.core.unsatisfiable_lookup_cb = xce_alloc_cache_lookup_cb; sched.core.unsatisfiable_populate_cb = xce_alloc_cache_populate_cb; init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS, BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER, BASE_QUEUE, BASE_PROJECT); /* Miss before any insert, via the callback -- same as a direct * cache_lookup() call (test_lookup_before_any_insert_is_a_miss * above). */ TEST_ASSERT_NOT_NULL(sched.core.unsatisfiable_lookup_cb); rc = sched.core.unsatisfiable_lookup_cb(&(sched.core), &obj, &out_via_cb); TEST_ASSERT_EQUAL_INT(0, rc); /* Populate via the callback... */ TEST_ASSERT_NOT_NULL(sched.core.unsatisfiable_populate_cb); sched.core.unsatisfiable_populate_cb(&(sched.core), &obj, 55); /* ...then confirm a DIRECT cache_lookup() call sees the SAME entry -- * the adapter genuinely populated the real cache, not a shadow copy. */ rc = cache_lookup(&sched, &obj, &out_direct); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_EQUAL_INT(55, out_direct); /* And the callback-based lookup now also reports the same hit. */ rc = sched.core.unsatisfiable_lookup_cb(&(sched.core), &obj, &out_via_cb); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_EQUAL_INT(55, out_via_cb); fini_sched(&sched); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_same_key_twice_is_a_hit); RUN_TEST(test_lookup_before_any_insert_is_a_miss); RUN_TEST(test_miss_on_differing_min_tasks); RUN_TEST(test_miss_on_differing_max_tasks); RUN_TEST(test_miss_on_differing_select_node_id); RUN_TEST(test_miss_on_differing_rusage_hash); RUN_TEST(test_miss_on_differing_user); RUN_TEST(test_miss_on_differing_queue); RUN_TEST(test_miss_on_differing_project); RUN_TEST(test_reason_category_round_trip_two_distinct_categories); RUN_TEST(test_clear_empties_all_entries); RUN_TEST(test_insert_duplicate_key_returns_eexist); RUN_TEST(test_argument_validation); RUN_TEST(test_seam_fields_default_null_on_zeroed_core); RUN_TEST(test_null_callbacks_mean_always_compute_fresh); RUN_TEST(test_lookup_and_populate_cb_adapters_match_direct_cache_calls); return UNITY_END(); }