xce_alloc_plan_iter_t.init takes xce_sched_core_t* instead of xce_scheduler_t*; both built-in generators, both allocators' call sites and the contract-test stub follow. The parjob purity test now shows an identical plan sequence for a NULL and a poisoned core. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
316 lines
10 KiB
C
316 lines
10 KiB
C
/*
|
|
* test_alloc_plan_iter.c - contract test for the lazy allocation-plan and
|
|
* host-candidate iterator interfaces (scheduler-allocation spec, task
|
|
* 1.1, Req 2 AC 1 / Req 3 AC 1).
|
|
*
|
|
* This task only adds a header (sched/alloc_iter.h) declaring
|
|
* xce_alloc_plan_t/xce_alloc_plan_iter_t/xce_host_iter_t -- no allocator
|
|
* is wired onto them yet (parjob_allocator/seqjob_allocator rewrap is
|
|
* later tasks' work). So the RED/GREEN evidence for this task is:
|
|
*
|
|
* RED: before sched/alloc_iter.h existed, this file failed to compile
|
|
* (unknown types xce_alloc_plan_t/xce_alloc_plan_iter_t/
|
|
* xce_host_iter_t).
|
|
* GREEN: with the header in place, a trivial, no-op reference
|
|
* implementation of each interface (below) can be constructed,
|
|
* stack-allocated and initialized in place per the documented
|
|
* convention, and driven through a full init()/next()/fini()
|
|
* cycle -- including the exact plan -> per-bucket host_iter
|
|
* composition shape requirements.md's Composition Algorithm
|
|
* describes -- proving the contract shape actually compiles and
|
|
* is usable, not just that a struct literal type-checks.
|
|
*
|
|
* Both stub implementations ignore the real xce_primary_resreq_t/
|
|
* xce_sched_core_t/plan/workers arguments (ok, per the header's own
|
|
* "each concrete plan-iterator implementation carries whatever it needs"
|
|
* note) and always yield exactly one candidate before signaling stable
|
|
* exhaustion.
|
|
*/
|
|
#include <errno.h>
|
|
#include <pthread.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "sched/alloc_iter.h"
|
|
|
|
/* ---- trivial no-op xce_alloc_plan_iter_t reference implementation ---- */
|
|
|
|
typedef struct {
|
|
int served; /* 0 = not yet served, 1 = exhausted */
|
|
int bucket_slots[1];
|
|
xce_alloc_plan_t plan;
|
|
} stub_plan_priv_t;
|
|
|
|
static int stub_plan_iter_init(xce_alloc_plan_iter_t* iter,
|
|
xce_primary_resreq_t* resreq,
|
|
xce_sched_core_t* core)
|
|
{
|
|
stub_plan_priv_t* priv;
|
|
|
|
/* Stub: ignores the real resreq/core state -- a real implementation
|
|
* (parjob's combinatorial one, seqjob's trivial one) would derive its
|
|
* plan space from resreq->maxTasks here. */
|
|
(void)resreq;
|
|
(void)core;
|
|
|
|
if (iter == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
priv = (stub_plan_priv_t*)calloc(1, sizeof(*priv));
|
|
if (priv == NULL) {
|
|
return -ENOMEM;
|
|
}
|
|
|
|
priv->served = 0;
|
|
priv->bucket_slots[0] = 1;
|
|
priv->plan.nbuckets = 1;
|
|
priv->plan.bucket_slots = priv->bucket_slots;
|
|
|
|
iter->private = priv;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static xce_alloc_plan_t* stub_plan_iter_next(xce_alloc_plan_iter_t* iter)
|
|
{
|
|
stub_plan_priv_t* priv;
|
|
|
|
/* Req 2 AC 6: a real cancellation point at least once per invocation
|
|
* -- combinatorial generation has none of its own. */
|
|
pthread_testcancel();
|
|
|
|
if (iter == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
priv = (stub_plan_priv_t*)iter->private;
|
|
if (priv == NULL || priv->served) {
|
|
return NULL; /* Req 2 AC 4: stable exhaustion signal */
|
|
}
|
|
|
|
priv->served = 1;
|
|
|
|
return &priv->plan;
|
|
}
|
|
|
|
static void stub_plan_iter_fini(xce_alloc_plan_iter_t* iter)
|
|
{
|
|
if (iter == NULL) {
|
|
return;
|
|
}
|
|
|
|
free(iter->private);
|
|
iter->private = NULL;
|
|
}
|
|
|
|
/* ---- trivial no-op xce_host_iter_t reference implementation ---- */
|
|
|
|
typedef struct {
|
|
int served; /* 0 = not yet served, 1 = exhausted */
|
|
xce_sched_worker_t worker;
|
|
} stub_host_priv_t;
|
|
|
|
static int stub_host_iter_init(xce_host_iter_t* iter, xce_alloc_plan_t* plan,
|
|
var workers)
|
|
{
|
|
stub_host_priv_t* priv;
|
|
|
|
/* Stub: ignores the real plan/workers state -- the one shared real
|
|
* implementation (host_iter.c, Req 3) would filter `workers` by
|
|
* plan's select expression + slot availability here. */
|
|
(void)plan;
|
|
(void)workers;
|
|
|
|
if (iter == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
priv = (stub_host_priv_t*)calloc(1, sizeof(*priv));
|
|
if (priv == NULL) {
|
|
return -ENOMEM;
|
|
}
|
|
|
|
priv->served = 0;
|
|
priv->worker.name = "stub-host";
|
|
|
|
iter->private = priv;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static xce_sched_worker_t* stub_host_iter_next(xce_host_iter_t* iter)
|
|
{
|
|
stub_host_priv_t* priv;
|
|
|
|
/* Req 3 AC 6: a real cancellation point at least once per invocation
|
|
* -- scanning/filtering has none of its own. */
|
|
pthread_testcancel();
|
|
|
|
if (iter == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
priv = (stub_host_priv_t*)iter->private;
|
|
if (priv == NULL || priv->served) {
|
|
return NULL; /* Req 3 AC 3: exhausted for this instance */
|
|
}
|
|
|
|
priv->served = 1;
|
|
|
|
return &priv->worker;
|
|
}
|
|
|
|
static void stub_host_iter_fini(xce_host_iter_t* iter)
|
|
{
|
|
if (iter == NULL) {
|
|
return;
|
|
}
|
|
|
|
free(iter->private);
|
|
iter->private = NULL;
|
|
}
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
/*
|
|
* Req 2 AC 1/2/4/5: the plan iterator is stack-allocated by the caller
|
|
* and initialized in place (not a heap-allocating factory -- see the
|
|
* header's own convention note), yields exactly one plan, then a stable
|
|
* NULL on every subsequent next() call, and fini() releases it cleanly.
|
|
*/
|
|
static void test_plan_iter_stub_produces_one_plan_then_stable_null(void)
|
|
{
|
|
xce_alloc_plan_iter_t iter_storage;
|
|
xce_alloc_plan_iter_t* iter = &iter_storage;
|
|
xce_alloc_plan_t* plan;
|
|
|
|
memset(iter, 0, sizeof(*iter));
|
|
iter->init = stub_plan_iter_init;
|
|
iter->next = stub_plan_iter_next;
|
|
iter->fini = stub_plan_iter_fini;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, iter->init(iter, NULL, NULL));
|
|
|
|
plan = iter->next(iter);
|
|
TEST_ASSERT_NOT_NULL(plan);
|
|
TEST_ASSERT_EQUAL_INT(1, plan->nbuckets);
|
|
TEST_ASSERT_NOT_NULL(plan->bucket_slots);
|
|
TEST_ASSERT_EQUAL_INT(1, plan->bucket_slots[0]);
|
|
|
|
/* Req 2 AC 4: end-of-iteration signal, stable across repeated calls. */
|
|
TEST_ASSERT_NULL(iter->next(iter));
|
|
TEST_ASSERT_NULL(iter->next(iter));
|
|
|
|
iter->fini(iter);
|
|
TEST_ASSERT_NULL(iter->private);
|
|
}
|
|
|
|
/*
|
|
* Req 3 AC 1/2/3/5: the host iterator is likewise stack-allocated and
|
|
* initialized in place from a plan, yields exactly one candidate host,
|
|
* then a stable NULL, and fini() is independent of the owning plan
|
|
* iterator's own lifecycle -- proved by finiing the host iterator first
|
|
* and then confirming plan_iter's own exhaustion/fini() sequence is
|
|
* completely unaffected.
|
|
*/
|
|
static void test_host_iter_stub_produces_one_worker_then_stable_null(void)
|
|
{
|
|
xce_alloc_plan_iter_t plan_iter_storage;
|
|
xce_alloc_plan_iter_t* plan_iter = &plan_iter_storage;
|
|
xce_alloc_plan_t* plan;
|
|
xce_host_iter_t host_iter_storage;
|
|
xce_host_iter_t* host_iter = &host_iter_storage;
|
|
xce_sched_worker_t* w;
|
|
|
|
memset(plan_iter, 0, sizeof(*plan_iter));
|
|
plan_iter->init = stub_plan_iter_init;
|
|
plan_iter->next = stub_plan_iter_next;
|
|
plan_iter->fini = stub_plan_iter_fini;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, plan_iter->init(plan_iter, NULL, NULL));
|
|
plan = plan_iter->next(plan_iter);
|
|
TEST_ASSERT_NOT_NULL(plan);
|
|
|
|
memset(host_iter, 0, sizeof(*host_iter));
|
|
host_iter->init = stub_host_iter_init;
|
|
host_iter->next = stub_host_iter_next;
|
|
host_iter->fini = stub_host_iter_fini;
|
|
|
|
/* `plan` is still valid here -- neither next() nor fini() has been
|
|
* called again on plan_iter since it was returned above, per the
|
|
* header's own "valid until the next next()/fini() call" contract. */
|
|
TEST_ASSERT_EQUAL_INT(0, host_iter->init(host_iter, plan, NULL));
|
|
|
|
w = host_iter->next(host_iter);
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
TEST_ASSERT_EQUAL_STRING("stub-host", w->name);
|
|
|
|
/* Req 3 AC 3: exhausted for this instance's bucket search, stable. */
|
|
TEST_ASSERT_NULL(host_iter->next(host_iter));
|
|
TEST_ASSERT_NULL(host_iter->next(host_iter));
|
|
|
|
/* Req 3 AC 5: fini() on host_iter is independent of plan_iter's own
|
|
* lifecycle -- finiing it here must not disturb plan_iter, which is
|
|
* still fini()'d separately, afterward, below. */
|
|
host_iter->fini(host_iter);
|
|
TEST_ASSERT_NULL(host_iter->private);
|
|
|
|
/* plan_iter is unaffected by host_iter->fini() -- still stably
|
|
* exhausted (it already served its one plan above). Only now is its
|
|
* own storage (and the `plan` pointer into it) released. */
|
|
TEST_ASSERT_NULL(plan_iter->next(plan_iter));
|
|
plan_iter->fini(plan_iter);
|
|
TEST_ASSERT_NULL(plan_iter->private);
|
|
}
|
|
|
|
/*
|
|
* Req 3 AC 4 / Req 4 AC 2 composition shape: a fresh xce_host_iter_t
|
|
* instance is constructed from the SAME plan once per bucket, each a
|
|
* fully independent instance -- the exact plan -> bucket -> host_iter
|
|
* nesting requirements.md's Composition Algorithm pseudocode describes
|
|
* for allocate_primary(), exercised here end to end against the stub
|
|
* implementations (a 2-bucket plan, each bucket getting its own fresh
|
|
* host_iter instance that is init()'d and fini()'d independently).
|
|
*/
|
|
static void test_fresh_host_iter_per_bucket_composes_with_one_plan(void)
|
|
{
|
|
int bucket_slots[2] = { 2, 1 };
|
|
xce_alloc_plan_t two_bucket_plan
|
|
= { .nbuckets = 2, .bucket_slots = bucket_slots };
|
|
int b;
|
|
|
|
for (b = 0; b < two_bucket_plan.nbuckets; b++) {
|
|
xce_host_iter_t host_iter_storage;
|
|
xce_host_iter_t* host_iter = &host_iter_storage;
|
|
xce_sched_worker_t* w;
|
|
|
|
memset(host_iter, 0, sizeof(*host_iter));
|
|
host_iter->init = stub_host_iter_init;
|
|
host_iter->next = stub_host_iter_next;
|
|
host_iter->fini = stub_host_iter_fini;
|
|
|
|
/* A brand-new init() per bucket -- never one instance reused
|
|
* across the loop (Req 3's "fresh per bucket" requirement). */
|
|
TEST_ASSERT_EQUAL_INT(
|
|
0, host_iter->init(host_iter, &two_bucket_plan, NULL));
|
|
|
|
w = host_iter->next(host_iter);
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
|
|
host_iter->fini(host_iter);
|
|
TEST_ASSERT_NULL(host_iter->private);
|
|
}
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_plan_iter_stub_produces_one_plan_then_stable_null);
|
|
RUN_TEST(test_host_iter_stub_produces_one_worker_then_stable_null);
|
|
RUN_TEST(test_fresh_host_iter_per_bucket_composes_with_one_plan);
|
|
return UNITY_END();
|
|
}
|