/* test_resreq_fingerprint.c — Req 7.1 (scheduler-allocation spec, task 1.2): * resreq fingerprinting for the unsatisfiable-resreq cache. * * Scope of THIS test file: xce_primary_resreq_t's new rusage_hash field * (set once by parse_resreq_primary(), src/lib/resreq.c) -- stability * across repeated parses of an identical resreq, and sensitivity to any * single differing rusage field. * * xce_resreq_fingerprint_t itself (min_tasks/max_tasks/select_node_id/ * rusage_hash) is defined in src/scheduler/include/sched/types.h, not * src/include -- it is a scheduler-side value object. src/lib/testsuite * builds against src/lib's own include path only (src/lib/meson.build: * `incdir = include_directories('../include')`) and cannot reach * src/scheduler/include without a layering violation (the scheduler * depends on libxce, never the reverse). To still prove "the struct * correctly reflects min_tasks/max_tasks [and rusage_hash] from a parsed * resreq" (this task's Observable), this file declares a local mirror * struct with the identical field layout and populates it exactly the way * a future job-ingest call site will. The real struct, and its population * at job ingest, is exercised by the scheduler test suite once that * wiring lands (out of this task's boundary -- see task 1.2's notes). */ #define UNIT_TEST 1 /* sel_optimize_test_reset() visibility, see test_select.c */ #include #include #include #include #include #include "resreq.h" #include "test_resmgr_fixture.h" #include "utils.h" /* Mirrors src/scheduler/include/sched/types.h's xce_resreq_fingerprint_t * layout exactly: int min_tasks; int max_tasks; uint32_t select_node_id; * uint64_t rusage_hash. See the file header comment for why this is a * local duplicate rather than an #include. */ typedef struct { int min_tasks; int max_tasks; uint32_t select_node_id; uint64_t rusage_hash; } xce_resreq_fingerprint_t; /* Builds the fingerprint the same way a real job-ingest call site * (core_build_primary_schedobj(), src/scheduler/src/core_events.c) will: * min/max tasks and the parse-time rusage_hash copied verbatim, and the * select root's CSE-interned node_id (0 if select is NULL). */ static xce_resreq_fingerprint_t fingerprint_from_resreq(const xce_primary_resreq_t* r) { xce_resreq_fingerprint_t fp; fp.min_tasks = r->minTasks; fp.max_tasks = r->maxTasks; fp.select_node_id = (r->select != NULL && r->select->root != NULL) ? r->select->root->node_id : 0; fp.rusage_hash = r->rusage_hash; return fp; } /* ── (a) stability across repeated computation for an identical resreq ── */ void test_resreq_fingerprint_stable_across_identical_parse(void) { char* resreqString = "rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]"; char* errmsg1 = NULL; char* errmsg2 = NULL; int rc1; int rc2; xce_primary_resreq_t r1; xce_primary_resreq_t r2; rc1 = parse_resreq_primary(&r1, resreqString, NULL, &errmsg1); rc2 = parse_resreq_primary(&r2, resreqString, NULL, &errmsg2); TEST_ASSERT_TRUE(rc1 >= 0); TEST_ASSERT_TRUE(rc2 >= 0); /* A hash over non-empty rusage content should not degenerate to 0. */ TEST_ASSERT_NOT_EQUAL_UINT64(0, r1.rusage_hash); /* Stability: identical resreq string parsed twice -> identical hash. */ TEST_ASSERT_EQUAL_UINT64(r1.rusage_hash, r2.rusage_hash); { xce_resreq_fingerprint_t fp1 = fingerprint_from_resreq(&r1); xce_resreq_fingerprint_t fp2 = fingerprint_from_resreq(&r2); TEST_ASSERT_EQUAL_INT(fp1.min_tasks, fp2.min_tasks); TEST_ASSERT_EQUAL_INT(fp1.max_tasks, fp2.max_tasks); TEST_ASSERT_EQUAL_UINT32(fp1.select_node_id, fp2.select_node_id); TEST_ASSERT_EQUAL_UINT64(fp1.rusage_hash, fp2.rusage_hash); } kill_primary_resreq(&r1); kill_primary_resreq(&r2); FREEUP(errmsg1); FREEUP(errmsg2); } /* Re-parsing the same resreq into a fresh xce_primary_resreq_t multiple * times in a loop must always land on the same hash -- guards against any * source of nondeterminism (e.g. hashing an uninitialized/pointer field) * that a two-shot test could miss by luck. */ void test_resreq_fingerprint_stable_across_repeated_reparse(void) { char* resreqString = "rusage[mem=42:duration=60:decay=0]"; uint64_t first_hash = 0; int i; for (i = 0; i < 5; i++) { char* errmsg = NULL; int rc; xce_primary_resreq_t r; rc = parse_resreq_primary(&r, resreqString, NULL, &errmsg); TEST_ASSERT_TRUE(rc >= 0); if (i == 0) { first_hash = r.rusage_hash; } else { TEST_ASSERT_EQUAL_UINT64(first_hash, r.rusage_hash); } kill_primary_resreq(&r); FREEUP(errmsg); } } /* ── (b)/(more): differs when any single contributing field changes ───── */ static void assert_rusage_hash_differs(char* base_str, char* variant_str) { char* errmsg1 = NULL; char* errmsg2 = NULL; int rc1; int rc2; xce_primary_resreq_t rbase; xce_primary_resreq_t rvariant; rc1 = parse_resreq_primary(&rbase, base_str, NULL, &errmsg1); rc2 = parse_resreq_primary(&rvariant, variant_str, NULL, &errmsg2); TEST_ASSERT_TRUE(rc1 >= 0); TEST_ASSERT_TRUE(rc2 >= 0); TEST_ASSERT_NOT_EQUAL_UINT64(rbase.rusage_hash, rvariant.rusage_hash); kill_primary_resreq(&rbase); kill_primary_resreq(&rvariant); FREEUP(errmsg1); FREEUP(errmsg2); } void test_resreq_fingerprint_differs_on_rusage_value(void) { assert_rusage_hash_differs( "rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]", "rusage[mem=21:duration=300:decay=1,lic=55:duration=20m:decay=0]"); } void test_resreq_fingerprint_differs_on_resource_name(void) { assert_rusage_hash_differs( "rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]", "rusage[cpu=20:duration=300:decay=1,lic=55:duration=20m:decay=0]"); } void test_resreq_fingerprint_differs_on_per_flag(void) { assert_rusage_hash_differs( "rusage[mem=20:duration=300:decay=1]", "rusage[mem=20/host:duration=300:decay=1]"); } void test_resreq_fingerprint_differs_on_decay_flag(void) { assert_rusage_hash_differs( "rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]", "rusage[mem=20:duration=300:decay=0,lic=55:duration=20m:decay=0]"); } void test_resreq_fingerprint_differs_on_duration(void) { assert_rusage_hash_differs( "rusage[mem=20:duration=300:decay=1]", "rusage[mem=20:duration=301:decay=1]"); } void test_resreq_fingerprint_differs_on_rusage_count(void) { assert_rusage_hash_differs( "rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]", "rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0,gpu=1]"); } /* No rusage clause at all is a legitimate resreq (test_resreq_primary2 in * test_resreq.c already covers the parse side); the hash of an empty * rusages list must still be stable and must differ from a non-empty one. */ void test_resreq_fingerprint_empty_rusages_stable_and_distinct(void) { char* errmsg1 = NULL; char* errmsg2 = NULL; char* errmsg3 = NULL; int rc1; int rc2; int rc3; xce_primary_resreq_t r1; xce_primary_resreq_t r2; xce_primary_resreq_t r3; rc1 = parse_resreq_primary(&r1, "select[type==any] ", NULL, &errmsg1); rc2 = parse_resreq_primary(&r2, "select[type==any] ", NULL, &errmsg2); rc3 = parse_resreq_primary(&r3, "rusage[mem=1]", NULL, &errmsg3); TEST_ASSERT_TRUE(rc1 >= 0); TEST_ASSERT_TRUE(rc2 >= 0); TEST_ASSERT_TRUE(rc3 >= 0); TEST_ASSERT_TRUE(r1.rusages.count == 0); TEST_ASSERT_EQUAL_UINT64(r1.rusage_hash, r2.rusage_hash); TEST_ASSERT_NOT_EQUAL_UINT64(r1.rusage_hash, r3.rusage_hash); kill_primary_resreq(&r1); kill_primary_resreq(&r2); kill_primary_resreq(&r3); FREEUP(errmsg1); FREEUP(errmsg2); FREEUP(errmsg3); } /* ── (c) xce_resreq_fingerprint_t correctly reflects min_tasks/max_tasks ─ */ void test_resreq_fingerprint_reflects_min_max_tasks(void) { char* errmsg = NULL; int rc; xce_primary_resreq_t r; xce_resreq_fingerprint_t fp; rc = parse_resreq_primary(&r, "rusage[mem=1]", NULL, &errmsg); TEST_ASSERT_TRUE(rc >= 0); /* parse_resreq_primary() currently hardcodes minTasks=maxTasks=1 (a * pre-existing FIXME unrelated to Req 7.1 -- real min/max task parsing * is a different requirement's scope). This test proves the * fingerprint construction copies whatever the parser produces * through faithfully, not that the parser itself computes them. */ fp = fingerprint_from_resreq(&r); TEST_ASSERT_EQUAL_INT(r.minTasks, fp.min_tasks); TEST_ASSERT_EQUAL_INT(r.maxTasks, fp.max_tasks); TEST_ASSERT_EQUAL_INT(1, fp.min_tasks); TEST_ASSERT_EQUAL_INT(1, fp.max_tasks); kill_primary_resreq(&r); FREEUP(errmsg); } /* ── select_node_id coverage (bonus: not required by this task's escape * hatch, but a clean reuse of existing CSE test fixtures/pattern from * test_select.c, so included rather than skipped). ────────────────────── */ void test_resreq_fingerprint_select_node_id_zero_when_no_select(void) { char* errmsg = NULL; int rc; xce_primary_resreq_t r; xce_resreq_fingerprint_t fp; rc = parse_resreq_primary(&r, "rusage[mem=20]", NULL, &errmsg); TEST_ASSERT_TRUE(rc >= 0); TEST_ASSERT_NULL(r.select); fp = fingerprint_from_resreq(&r); TEST_ASSERT_EQUAL_UINT32(0, fp.select_node_id); kill_primary_resreq(&r); FREEUP(errmsg); } void test_resreq_fingerprint_select_node_id_stable_for_identical_select(void) { var resmgr = make_test_resmgr(); char* resreqString = "select[type==any] rusage[mem=20]"; char* errmsg1 = NULL; char* errmsg2 = NULL; int rc1; int rc2; xce_primary_resreq_t r1; xce_primary_resreq_t r2; xce_resreq_fingerprint_t fp1; xce_resreq_fingerprint_t fp2; sel_node_table_init(); sel_optimize_set(1); rc1 = parse_resreq_primary(&r1, resreqString, resmgr, &errmsg1); rc2 = parse_resreq_primary(&r2, resreqString, resmgr, &errmsg2); TEST_ASSERT_TRUE(rc1 >= 0); TEST_ASSERT_TRUE(rc2 >= 0); TEST_ASSERT_NOT_NULL(r1.select); TEST_ASSERT_NOT_NULL(r2.select); TEST_ASSERT_NOT_NULL(r1.select->root); TEST_ASSERT_NOT_NULL(r2.select->root); fp1 = fingerprint_from_resreq(&r1); fp2 = fingerprint_from_resreq(&r2); /* CSE interns identical select ASTs to the same node -> same node_id. * This demonstrates select_node_id's contribution to fingerprint * stability without re-testing the CSE machinery itself (already * covered by test_select.c's own CSE tests). */ TEST_ASSERT_NOT_EQUAL_UINT32(0, fp1.select_node_id); TEST_ASSERT_EQUAL_UINT32(fp1.select_node_id, fp2.select_node_id); /* kill_primary_resreq() frees only the per-resreq select wrapper; in * CSE mode (optimize level > 0) it correctly leaves the interned node * itself alone (GC-owned) -- sel_optimize_test_reset() below tears * down the whole intern table. */ kill_primary_resreq(&r1); kill_primary_resreq(&r2); FREEUP(errmsg1); FREEUP(errmsg2); sel_optimize_test_reset(); free_test_resmgr(resmgr); } void test_resreq_fingerprint(void) { printf("\n>> resreq fingerprint test (Req 7.1)\n\n"); RUN_TEST(test_resreq_fingerprint_stable_across_identical_parse); RUN_TEST(test_resreq_fingerprint_stable_across_repeated_reparse); RUN_TEST(test_resreq_fingerprint_differs_on_rusage_value); RUN_TEST(test_resreq_fingerprint_differs_on_resource_name); RUN_TEST(test_resreq_fingerprint_differs_on_per_flag); RUN_TEST(test_resreq_fingerprint_differs_on_decay_flag); RUN_TEST(test_resreq_fingerprint_differs_on_duration); RUN_TEST(test_resreq_fingerprint_differs_on_rusage_count); RUN_TEST(test_resreq_fingerprint_empty_rusages_stable_and_distinct); RUN_TEST(test_resreq_fingerprint_reflects_min_max_tasks); RUN_TEST(test_resreq_fingerprint_select_node_id_zero_when_no_select); RUN_TEST(test_resreq_fingerprint_select_node_id_stable_for_identical_select); }