myxce/src/lib/testsuite/test_resreq_task_count.c
Rongsong Shen 799a71ecb1
feat(resreq-maxtasks-wiring): add task-count derivation helper
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
2026-09-06 03:26:32 +00:00

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);
}