myxce/src/lib/testsuite/test_resreq_fingerprint.c
Rongsong Shen 5a6706761d
feat(scheduler-allocation): add resreq fingerprinting for the cache
Adds rusage_hash to xce_primary_resreq_t (computed once by
parse_resreq_primary() via a new hash_rusages() helper, hashing
rusage content rather than raw struct bytes since resname is a
non-interned strdup'd pointer), the xce_resreq_fingerprint_t value
type, and resreq_fp on xce_primary_schedobj_t, populated once at real
job-ingest time in core_build_primary_schedobj().

Task 1.2 (scheduler-allocation spec). Requirement 7.1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
2026-09-04 15:00:12 +00:00

348 lines
13 KiB
C

/* 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#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);
}