/* 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 #include #include #include #include #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); }