Adds apply_job_task_count() to overwrite a standalone primary resreq's minTasks/maxTasks with a job's real requested task count, no-op for COMPOUND/ALTER nested primaries, with a defensive min>max clamp. Closes the gap that lets scheduler-allocation's multi-host allocator ever be reached (bug-1770). Wiring at live ingest call sites follows in subsequent tasks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
262 lines
8.9 KiB
C
262 lines
8.9 KiB
C
/* test_resreq_task_count.c — resreq-maxtasks-wiring spec, task 1
|
|
* (TaskCountWiring component).
|
|
*
|
|
* Unit tests for apply_job_task_count() (src/lib/resreq.c), the pure
|
|
* helper that overwrites a standalone primary resreq's minTasks/maxTasks
|
|
* with the job's real requested task count, per design.md's
|
|
* TaskCountWiring component. Covers the three cases the task requires:
|
|
* - a standalone primary resreq receives the applied task count
|
|
* (Requirements 1.1, 1.2)
|
|
* - a COMPOUND/ALTER resreq's nested primaries are left untouched
|
|
* (Requirement 3.3)
|
|
* - an invalid (minTasks > maxTasks) pair is defensively clamped by
|
|
* raising maxTasks to minTasks, mirroring convert.go's clamp
|
|
* direction (Requirement 1.5)
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "resreq.h"
|
|
#include "utils.h"
|
|
|
|
/* ── standalone primary: overwritten with the requested task count ─────── */
|
|
|
|
void test_apply_job_task_count_standalone_primary_override(void)
|
|
{
|
|
char* resreqString = "rusage[mem=20]";
|
|
char* errmsg = NULL;
|
|
int rc = 0;
|
|
xce_resreq_t resreq;
|
|
|
|
memset(&resreq, 0x0, sizeof(resreq));
|
|
|
|
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_PRIMARY);
|
|
|
|
/* Sanity: parser's own hardcoded default before the helper ever runs. */
|
|
TEST_ASSERT_EQUAL_INT(1, resreq.u.primary->minTasks);
|
|
TEST_ASSERT_EQUAL_INT(1, resreq.u.primary->maxTasks);
|
|
|
|
apply_job_task_count(&resreq, 4, 6);
|
|
|
|
TEST_ASSERT_EQUAL_INT(4, resreq.u.primary->minTasks);
|
|
TEST_ASSERT_EQUAL_INT(6, resreq.u.primary->maxTasks);
|
|
|
|
/* Idempotency: calling again with identical inputs must not change
|
|
* the result (design.md's TaskCountWiring invariant). */
|
|
apply_job_task_count(&resreq, 4, 6);
|
|
TEST_ASSERT_EQUAL_INT(4, resreq.u.primary->minTasks);
|
|
TEST_ASSERT_EQUAL_INT(6, resreq.u.primary->maxTasks);
|
|
|
|
kill_resreq(&resreq);
|
|
FREEUP(errmsg);
|
|
}
|
|
|
|
/* A default/single-task request (Requirement 1.2) must still land on 1/1 --
|
|
* exercised explicitly rather than merely relying on the parser's own
|
|
* pre-helper default, since the helper is expected to be applied
|
|
* unconditionally at every live ingest call site regardless of the
|
|
* requested count. */
|
|
void test_apply_job_task_count_default_single_task(void)
|
|
{
|
|
char* resreqString = "rusage[mem=20]";
|
|
char* errmsg = NULL;
|
|
int rc = 0;
|
|
xce_resreq_t resreq;
|
|
|
|
memset(&resreq, 0x0, sizeof(resreq));
|
|
|
|
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_PRIMARY);
|
|
|
|
apply_job_task_count(&resreq, 1, 1);
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, resreq.u.primary->minTasks);
|
|
TEST_ASSERT_EQUAL_INT(1, resreq.u.primary->maxTasks);
|
|
|
|
kill_resreq(&resreq);
|
|
FREEUP(errmsg);
|
|
}
|
|
|
|
/* ── COMPOUND/ALTER: nested primaries left exactly as parsed (no-op) ────── */
|
|
|
|
void test_apply_job_task_count_compound_noop(void)
|
|
{
|
|
char* resreqString = "2*{rusage[mem=100:duration=20:decay=1]} + "
|
|
"4*{rusage[mem=80:duration=30:decay=1]}";
|
|
char* errmsg = NULL;
|
|
int rc = 0;
|
|
xce_resreq_t resreq;
|
|
int count;
|
|
int before_min[8];
|
|
int before_max[8];
|
|
int i;
|
|
|
|
memset(&resreq, 0x0, sizeof(resreq));
|
|
|
|
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_COMPOUND);
|
|
|
|
count = resreq.u.compound->primaries.count;
|
|
TEST_ASSERT_TRUE(count > 0);
|
|
TEST_ASSERT_TRUE(count <= 8);
|
|
|
|
/* Snapshot whatever parse_resreq() actually produced for each nested
|
|
* primary -- the "N*{...}" compound syntax sets a nested primary's
|
|
* minTasks/maxTasks to N (not the bare-rusage default of 1), so the
|
|
* no-op guarantee is "unchanged from parse", not "equal to 1". */
|
|
for (i = 0; i < count; i++) {
|
|
xce_primary_resreq_t* p = (xce_primary_resreq_t*)xlist_get(
|
|
&(resreq.u.compound->primaries), i);
|
|
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
before_min[i] = p->minTasks;
|
|
before_max[i] = p->maxTasks;
|
|
}
|
|
|
|
apply_job_task_count(&resreq, 4, 6);
|
|
|
|
/* No-op: tag unchanged, every nested primary left exactly as
|
|
* parse_resreq() set it. Only the job's own top-level standalone
|
|
* primary is in this helper's scope (Requirement 3.3). */
|
|
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_COMPOUND);
|
|
for (i = 0; i < count; i++) {
|
|
xce_primary_resreq_t* p = (xce_primary_resreq_t*)xlist_get(
|
|
&(resreq.u.compound->primaries), i);
|
|
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
TEST_ASSERT_EQUAL_INT(before_min[i], p->minTasks);
|
|
TEST_ASSERT_EQUAL_INT(before_max[i], p->maxTasks);
|
|
}
|
|
|
|
kill_resreq(&resreq);
|
|
FREEUP(errmsg);
|
|
}
|
|
|
|
void test_apply_job_task_count_alter_noop(void)
|
|
{
|
|
char* resreqString
|
|
= "{2*{rusage[mem=100:duration=20:decay=1]} + "
|
|
"4*{rusage[mem=80:duration=30:decay=1]}} || "
|
|
"{3*{rusage[mem=150:duration=15:decay=1]} + "
|
|
"6*{rusage[mem=120:duration=20:decay=1]}} ";
|
|
char* errmsg = NULL;
|
|
int rc = 0;
|
|
xce_resreq_t resreq;
|
|
int ncompounds;
|
|
int before_min[8][8];
|
|
int before_max[8][8];
|
|
int nprimaries[8];
|
|
int i;
|
|
|
|
memset(&resreq, 0x0, sizeof(resreq));
|
|
|
|
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_ALTER);
|
|
|
|
ncompounds = resreq.u.alter->compounds.count;
|
|
TEST_ASSERT_TRUE(ncompounds > 0);
|
|
TEST_ASSERT_TRUE(ncompounds <= 8);
|
|
|
|
/* Snapshot whatever parse_resreq() actually produced for every nested
|
|
* primary across every compound branch -- the "N*{...}" compound
|
|
* syntax sets a nested primary's minTasks/maxTasks to N (not the
|
|
* bare-rusage default of 1), so the no-op guarantee is "unchanged
|
|
* from parse", not "equal to 1". */
|
|
for (i = 0; i < ncompounds; i++) {
|
|
xce_compound_resreq_t* c
|
|
= (xce_compound_resreq_t*)xlist_get(&(resreq.u.alter->compounds), i);
|
|
int j;
|
|
|
|
TEST_ASSERT_NOT_NULL(c);
|
|
nprimaries[i] = c->primaries.count;
|
|
TEST_ASSERT_TRUE(nprimaries[i] <= 8);
|
|
|
|
for (j = 0; j < nprimaries[i]; j++) {
|
|
xce_primary_resreq_t* p
|
|
= (xce_primary_resreq_t*)xlist_get(&(c->primaries), j);
|
|
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
before_min[i][j] = p->minTasks;
|
|
before_max[i][j] = p->maxTasks;
|
|
}
|
|
}
|
|
|
|
apply_job_task_count(&resreq, 4, 6);
|
|
|
|
/* No-op: tag unchanged, every nested primary left exactly as
|
|
* parse_resreq() set it (Requirement 3.3). */
|
|
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_ALTER);
|
|
for (i = 0; i < ncompounds; i++) {
|
|
xce_compound_resreq_t* c
|
|
= (xce_compound_resreq_t*)xlist_get(&(resreq.u.alter->compounds), i);
|
|
int j;
|
|
|
|
TEST_ASSERT_NOT_NULL(c);
|
|
for (j = 0; j < nprimaries[i]; j++) {
|
|
xce_primary_resreq_t* p
|
|
= (xce_primary_resreq_t*)xlist_get(&(c->primaries), j);
|
|
|
|
TEST_ASSERT_NOT_NULL(p);
|
|
TEST_ASSERT_EQUAL_INT(before_min[i][j], p->minTasks);
|
|
TEST_ASSERT_EQUAL_INT(before_max[i][j], p->maxTasks);
|
|
}
|
|
}
|
|
|
|
kill_resreq(&resreq);
|
|
FREEUP(errmsg);
|
|
}
|
|
|
|
/* ── defensive clamp: minTasks > maxTasks must not reach dispatch ───────── */
|
|
|
|
void test_apply_job_task_count_clamps_min_exceeds_max(void)
|
|
{
|
|
char* resreqString = "rusage[mem=20]";
|
|
char* errmsg = NULL;
|
|
int rc = 0;
|
|
xce_resreq_t resreq;
|
|
|
|
memset(&resreq, 0x0, sizeof(resreq));
|
|
|
|
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_PRIMARY);
|
|
|
|
/* mintasks (6) > maxtasks (4): defensively raise maxtasks to
|
|
* mintasks, mirroring convert.go's own clamp direction exactly. */
|
|
apply_job_task_count(&resreq, 6, 4);
|
|
|
|
TEST_ASSERT_EQUAL_INT(6, resreq.u.primary->minTasks);
|
|
TEST_ASSERT_EQUAL_INT(6, resreq.u.primary->maxTasks);
|
|
|
|
kill_resreq(&resreq);
|
|
FREEUP(errmsg);
|
|
}
|
|
|
|
/* ── defensive: NULL resreq must be a safe no-op, never a crash ─────────── */
|
|
|
|
void test_apply_job_task_count_null_resreq_is_noop(void)
|
|
{
|
|
apply_job_task_count(NULL, 4, 6);
|
|
}
|
|
|
|
void test_resreq_task_count(void)
|
|
{
|
|
printf("\n>> resreq task count test\n\n");
|
|
|
|
RUN_TEST(test_apply_job_task_count_standalone_primary_override);
|
|
RUN_TEST(test_apply_job_task_count_default_single_task);
|
|
RUN_TEST(test_apply_job_task_count_compound_noop);
|
|
RUN_TEST(test_apply_job_task_count_alter_noop);
|
|
RUN_TEST(test_apply_job_task_count_clamps_min_exceeds_max);
|
|
RUN_TEST(test_apply_job_task_count_null_resreq_is_noop);
|
|
}
|