diff --git a/.kiro/specs/scheduler-allocation/tasks.md b/.kiro/specs/scheduler-allocation/tasks.md index 3879c857..526f8cd5 100644 --- a/.kiro/specs/scheduler-allocation/tasks.md +++ b/.kiro/specs/scheduler-allocation/tasks.md @@ -51,7 +51,7 @@ - _Depends: 3.1_ - [ ] 4. Core: parjob combinatorial allocation -- [ ] 4.1 Implement parjob's lazy combinatorial plan generation +- [x] 4.1 Implement parjob's lazy combinatorial plan generation - Generate candidate plans as a pure function of a job's task count: ascending host-count order, one balanced partition per host-count, descending bucket order within each plan - Ensure each call advances a real cancellation point and that plan generation never inspects live worker state (a pure function of the task count alone) - Write a unit test proving one-plan-per-call generation, most-preferred-first ordering (including an uneven split, e.g. splitting 7 tasks across 3 hosts), and a stable exhaustion signal diff --git a/src/scheduler/src/plugins/parjob_allocator.c b/src/scheduler/src/plugins/parjob_allocator.c index 93be6fe5..e6d93533 100644 --- a/src/scheduler/src/plugins/parjob_allocator.c +++ b/src/scheduler/src/plugins/parjob_allocator.c @@ -8,6 +8,7 @@ #include "kvmgr.h" #include "plugin.h" #include "preason.h" +#include "sched/alloc_iter.h" #include "scheduler.h" #include "utils.h" #include "xce/types.h" @@ -74,6 +75,160 @@ static int parjob_prepare_allocation(void* arg, xce_sched_ctx_t* ctx) return 0; } +/* + * parjob's combinatorial xce_alloc_plan_iter_t (task 4.1, Req 2 AC 1-6 / + * Req 4 AC 1). Unlike seqjob's fixed single-plan iterator + * (seqjob_allocator.c), this generates the full ascending host-count + * sequence k = 1..maxTasks, one balanced partition of maxTasks into k + * buckets per host-count, most-preferred (fewest hosts) first. + * + * Pure function of maxTasks alone (Req 4 AC 1's "1host x N, 2hosts x + * ceil(N/2), ..., Nhosts x 1" sequence, generalized to real partitions + * per requirements.md's "Data Structure Changes Summary"): `sched` is + * accepted (fixed by the shared xce_alloc_plan_iter_t.init() signature) + * but never dereferenced -- plan SHAPE and ORDER never inspect + * sched->core.workers or any other live worker/resource state. Only + * try_alloc_on_host()/host_iter.c (task 4.2's job) ever look at live + * worker state, to decide whether a generated plan succeeds. + * + * Partition rule for a given host-count k (requirements.md, "Data + * Structure Changes Summary", v1 default): the canonical BALANCED split + * -- (maxTasks mod k) buckets get ceil(maxTasks/k) slots, the remaining + * (k - maxTasks mod k) buckets get floor(maxTasks/k) slots. E.g. + * maxTasks=7, k=3 -> [3,2,2]. Filling the first `rem` buckets with + * (base+1) and the rest with `base` already yields descending + * (largest-first) bucket order within the plan by construction -- no + * separate sort step is needed, and this is the v1 default bucket order + * requirements.md specifies (a real correctness property here, not a + * fail-fast heuristic -- see the Composition Algorithm's worked example). + * + * `bucket_slots` is one heap buffer sized to the tightest true upper + * bound (maxTasks, since a plan can never need more buckets than + * maxTasks -- each bucket needs >=1 slot) and reused across next() calls + * within one iterator instance, refilled from index 0 up to the current + * plan's own nbuckets each call -- matching xce_alloc_plan_t's own + * documented "valid until the next next()/fini() call" contract. + * + * plan.ctx is left NULL here, exactly like seqjob_plan_iter_init()'s own + * convention (seqjob_allocator.c): init()'s fixed (iter, resreq, sched) + * signature has no ctx parameter to thread it through. Setting + * plan->ctx = ctx on every plan returned from next(), before + * constructing a host_iter from it, is the CALLER's responsibility + * (task 4.2's my_alloc(), not this iterator) -- see alloc_iter.h's + * xce_alloc_plan_t.ctx doc comment and tasks.md's task 2.1 implementation + * note. + */ +typedef struct { + xce_alloc_plan_t plan; + int* bucket_slots; /* heap buffer, size == max_tasks */ + int max_tasks; /* N -- primary_resreq->maxTasks */ + int next_k; /* next host-count to generate, 1-based */ +} parjob_plan_iter_priv_t; + +static int parjob_plan_iter_init(xce_alloc_plan_iter_t* iter, + xce_primary_resreq_t* resreq, + xce_scheduler_t* sched) +{ + parjob_plan_iter_priv_t* priv; + int max_tasks; + + /* Req 4 AC 1 / "pure function of the task count alone": sched is part + * of the shared init() signature but never inspected -- plan + * generation must not touch live worker/resource state. */ + (void)sched; + + if ((iter == NULL) || (resreq == NULL)) { + return -EINVAL; + } + + max_tasks = resreq->maxTasks; + if (max_tasks < 1) { + return -EINVAL; + } + + priv = (parjob_plan_iter_priv_t*)calloc(1, sizeof(*priv)); + if (priv == NULL) { + return -ENOMEM; + } + + priv->bucket_slots = (int*)calloc((size_t)max_tasks, sizeof(int)); + if (priv->bucket_slots == NULL) { + free(priv); + return -ENOMEM; + } + + priv->max_tasks = max_tasks; + priv->next_k = 1; + priv->plan.nbuckets = 0; + priv->plan.bucket_slots = priv->bucket_slots; + priv->plan.ctx = NULL; + + iter->private = priv; + + return 0; +} + +static xce_alloc_plan_t* parjob_plan_iter_next(xce_alloc_plan_iter_t* iter) +{ + parjob_plan_iter_priv_t* priv; + int k; + int base; + int rem; + int i; + + /* Req 2 AC 6: a real cancellation point at least once per invocation + * -- combinatorial plan generation is fast, non-blocking pure + * computation with no cancellation point of its own. Called + * unconditionally, including once already-exhausted, matching + * host_iter.c's/seqjob_plan_iter_next()'s own convention. */ + pthread_testcancel(); + + if ((iter == NULL) || (iter->private == NULL)) { + return NULL; + } + + priv = (parjob_plan_iter_priv_t*)iter->private; + + /* Req 2 AC 4: stable end-of-iteration signal, once host-count would + * exceed maxTasks, across every repeated call thereafter. */ + if (priv->next_k > priv->max_tasks) { + return NULL; + } + + k = priv->next_k; + priv->next_k++; + + base = priv->max_tasks / k; + rem = priv->max_tasks % k; + + for (i = 0; i < rem; i++) { + priv->bucket_slots[i] = base + 1; + } + for (i = rem; i < k; i++) { + priv->bucket_slots[i] = base; + } + + priv->plan.nbuckets = k; + + return &(priv->plan); +} + +static void parjob_plan_iter_fini(xce_alloc_plan_iter_t* iter) +{ + parjob_plan_iter_priv_t* priv; + + if ((iter == NULL) || (iter->private == NULL)) { + return; + } + + priv = (parjob_plan_iter_priv_t*)iter->private; + + free(priv->bucket_slots); + free(priv); + + iter->private = NULL; +} + static int my_alloc(xce_sched_plugin_t* plugin, xce_sched_ctx_t* ctx) { xce_scheduler_t* sched = (xce_scheduler_t*)plugin->sched; diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index d845f722..09a4cb71 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -1559,6 +1559,27 @@ sched_seqjob_rewrap_equivalence_test = executable( dependencies: [scheduler_test_deps], install: false) +# Task 4.1 (scheduler-allocation spec, Req 2 AC 1-6, Req 4 AC 1): +# parjob_allocator.c's REAL combinatorial xce_alloc_plan_iter_t +# implementation -- ascending host-count plan order, one balanced +# ceil/floor partition per host-count, descending bucket order within +# each plan, a stable exhaustion signal, and a real pthread_testcancel() +# cancellation point. White-box #include of +# ../src/plugins/parjob_allocator.c itself (the file under test) -- +# simpler than sched_seqjob_allocator_test's own white-box stack since +# parjob_allocator.c's still-stubbed my_alloc()/parjob_prepare_allocation() +# bodies (untouched by this task -- task 4.2's job) make no calls into +# core.c/resource.c/schedobj.c/host_iter.c at all, so none of those need +# to be pulled in here. Only my_alloc()/prepare_allocation() themselves +# are never called by this file's tests -- they exercise +# parjob_plan_iter_init()/_next()/_fini() directly. +sched_parjob_combinatorial_test = executable( + 'sched_parjob_combinatorial_test', + ['test_parjob_combinatorial.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps], + install: false) + # Register as meson tests so `meson test --suite scheduler` runs them all. # detect_leaks=0 matches the suite convention (fixtures are not torn down). test('scheduler test', scheduler_test, suite: 'scheduler', @@ -1756,3 +1777,6 @@ test('scheduler seqjob allocator test', test('scheduler seqjob rewrap equivalence test', sched_seqjob_rewrap_equivalence_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) +test('scheduler parjob combinatorial test', + sched_parjob_combinatorial_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_parjob_combinatorial.c b/src/scheduler/testsuite/test_parjob_combinatorial.c new file mode 100644 index 00000000..248443b9 --- /dev/null +++ b/src/scheduler/testsuite/test_parjob_combinatorial.c @@ -0,0 +1,403 @@ +/* + * test_parjob_combinatorial.c - task 4.1 (scheduler-allocation spec): + * unit tests for parjob_allocator.c's REAL combinatorial + * xce_alloc_plan_iter_t implementation (Req 2 AC 1-6, Req 4 AC 1). + * + * Task 4.1's boundary is the plan-ITERATOR only -- parjob_allocator.c's + * my_alloc()/parjob_prepare_allocation() bodies are untouched stubs, + * still returning 0 unconditionally (task 4.2's job). So this file never + * calls my_alloc(); it drives parjob_plan_iter_init()/_next()/_fini() + * (static functions, hence the white-box #include below) directly, + * exactly the way a future my_alloc() will. + * + * White-box #include convention: same as test_seqjob_allocator.c + * (task 3.1), but simpler -- unlike seqjob_allocator.c's my_alloc(), + * parjob_allocator.c's current my_alloc()/parjob_prepare_allocation() + * stub bodies make no calls into resource.c/schedobj.c/core.c/host_iter.c + * at all (confirmed by reading the file: both just touch struct fields + * and return 0), so only ../src/plugins/parjob_allocator.c itself needs + * to be pulled in, plus a link-satisfying stub for plugin_register()/ + * plugin_unregister() (referenced by register_parjob_allocator(), never + * itself invoked by any test below). + * + * Covers: + * - one-plan-per-call generation (Req 2 AC 2) + * - most-preferred-first ordering: ascending host-count k = 1..N + * (Req 2 AC 3, Req 4 AC 1) + * - the canonical balanced partition rule, including an uneven split + * (N=7 across k=3 hosts -> [3,2,2]) (Req 4 AC 1) + * - descending bucket order within one plan, by construction (Req 2 + * AC 2's "not a uniform pair" + the Composition Algorithm's + * descending-order correctness property) + * - a stable end-of-iteration signal once k > N, stable across + * repeated post-exhaustion calls (Req 2 AC 4) + * - purity: plan shape/order is a pure function of maxTasks alone, + * never inspecting live worker/scheduler state (Req 4 AC 1's "never + * inspects live worker state" clause) -- proved by passing a NULL + * xce_scheduler_t* to init() and confirming plans still generate + * correctly + * - fini() releases the iterator's private state (Req 2 AC 5) + * - Req 2 AC 6: next() calls pthread_testcancel() (or an equivalent + * real cancellation point) -- proved behaviorally, same technique + * test_host_iter.c already established for host_iter_next(). + */ +#include +#include +#include +#include +#include + +#include + +#include "resreq.h" +#include "sched/alloc_iter.h" +#include "scheduler.h" + +/* Pull in parjob_allocator.c itself (the file under test) -- the plan + * iterator functions (parjob_plan_iter_init/next/fini) are `static`, so + * this white-box #include is the only way to reach them directly, + * matching test_seqjob_allocator.c's own established convention. */ +#include "../src/plugins/parjob_allocator.c" + +/* ---------------------------------------------------------------------- */ +/* Link-satisfying stubs */ +/* ---------------------------------------------------------------------- */ + +/* plugin_register()/plugin_unregister() are referenced by + * register_parjob_allocator() (parjob_allocator.c), but no test below + * calls register_parjob_allocator() -- the plan iterator is exercised + * directly. Never actually invoked by anything this file's tests + * exercise; provided only so the TU links. */ +int plugin_register(void* sched, void* plugin) +{ + (void)sched; + (void)plugin; + return 0; +} + +void plugin_unregister(void* arg) { (void)arg; } + +/* ---------------------------------------------------------------------- */ +/* Helpers */ +/* ---------------------------------------------------------------------- */ + +static void bind_plan_iter(xce_alloc_plan_iter_t* iter) +{ + memset(iter, 0, sizeof(*iter)); + iter->init = parjob_plan_iter_init; + iter->next = parjob_plan_iter_next; + iter->fini = parjob_plan_iter_fini; +} + +static void assert_plan_eq(xce_alloc_plan_t* plan, const int* expect, + int nexpect) +{ + int i; + + TEST_ASSERT_NOT_NULL(plan); + TEST_ASSERT_EQUAL_INT(nexpect, plan->nbuckets); + for (i = 0; i < nexpect; i++) { + TEST_ASSERT_EQUAL_INT(expect[i], plan->bucket_slots[i]); + } +} + +/* ---------------------------------------------------------------------- */ +/* Tests */ +/* ---------------------------------------------------------------------- */ + +/* + * Req 2 AC 2/AC 3, Req 4 AC 1: for maxTasks = 7, successive next() calls + * return exactly one plan per call, in ascending host-count order + * (k = 1..7), each plan the canonical balanced ceil/floor partition with + * buckets in descending order. k=3 is the uneven-split worked example + * from requirements.md: [3,2,2], not [2,2,3]/[2,3,2]/[5,1,1]. + */ +static void +test_plans_ordered_ascending_host_count_with_balanced_partitions(void) +{ + xce_primary_resreq_t resreq; + xce_alloc_plan_iter_t iter_storage; + xce_alloc_plan_iter_t* iter = &iter_storage; + xce_alloc_plan_t* plan; + + /* Expected balanced partition for maxTasks=7, per host-count k=1..7, + * descending bucket order within each -- worked by hand against + * requirements.md's "(N mod k) buckets get ceil(N/k), the rest get + * floor(N/k)" rule. */ + static const int expect_k1[] = { 7 }; + static const int expect_k2[] = { 4, 3 }; + static const int expect_k3[] = { 3, 2, 2 }; /* the uneven-split example */ + static const int expect_k4[] = { 2, 2, 2, 1 }; + static const int expect_k5[] = { 2, 2, 1, 1, 1 }; + static const int expect_k6[] = { 2, 1, 1, 1, 1, 1 }; + static const int expect_k7[] = { 1, 1, 1, 1, 1, 1, 1 }; + + memset(&resreq, 0, sizeof(resreq)); + resreq.minTasks = 1; + resreq.maxTasks = 7; + + bind_plan_iter(iter); + TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL)); + + plan = iter->next(iter); + assert_plan_eq(plan, expect_k1, 1); + + plan = iter->next(iter); + assert_plan_eq(plan, expect_k2, 2); + + plan = iter->next(iter); + assert_plan_eq(plan, expect_k3, 3); + + plan = iter->next(iter); + assert_plan_eq(plan, expect_k4, 4); + + plan = iter->next(iter); + assert_plan_eq(plan, expect_k5, 5); + + plan = iter->next(iter); + assert_plan_eq(plan, expect_k6, 6); + + plan = iter->next(iter); + assert_plan_eq(plan, expect_k7, 7); + + /* Req 2 AC 4: exhausted once k would exceed maxTasks (here k=8 > 7). */ + TEST_ASSERT_NULL(iter->next(iter)); + /* Stable across repeated post-exhaustion calls. */ + TEST_ASSERT_NULL(iter->next(iter)); + TEST_ASSERT_NULL(iter->next(iter)); + + iter->fini(iter); + TEST_ASSERT_NULL(iter->private); +} + +/* + * A clean divisor case (maxTasks = 6): every partition splits evenly, so + * every bucket in every plan carries the same slot count -- descending + * order is trivially satisfied (all equal), and this exercises rem == 0 + * for every k that evenly divides 6 (k=1,2,3,6) as well as k=4,5 where + * rem != 0. + */ +static void test_plans_for_evenly_divisible_task_count(void) +{ + xce_primary_resreq_t resreq; + xce_alloc_plan_iter_t iter_storage; + xce_alloc_plan_iter_t* iter = &iter_storage; + xce_alloc_plan_t* plan; + + static const int expect_k1[] = { 6 }; + static const int expect_k2[] = { 3, 3 }; + static const int expect_k3[] = { 2, 2, 2 }; + static const int expect_k4[] = { 2, 2, 1, 1 }; + static const int expect_k5[] = { 2, 1, 1, 1, 1 }; + static const int expect_k6[] = { 1, 1, 1, 1, 1, 1 }; + + memset(&resreq, 0, sizeof(resreq)); + resreq.minTasks = 1; + resreq.maxTasks = 6; + + bind_plan_iter(iter); + TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL)); + + plan = iter->next(iter); + assert_plan_eq(plan, expect_k1, 1); + plan = iter->next(iter); + assert_plan_eq(plan, expect_k2, 2); + plan = iter->next(iter); + assert_plan_eq(plan, expect_k3, 3); + plan = iter->next(iter); + assert_plan_eq(plan, expect_k4, 4); + plan = iter->next(iter); + assert_plan_eq(plan, expect_k5, 5); + plan = iter->next(iter); + assert_plan_eq(plan, expect_k6, 6); + + TEST_ASSERT_NULL(iter->next(iter)); + TEST_ASSERT_NULL(iter->next(iter)); + + iter->fini(iter); +} + +/* + * maxTasks = 1: exactly one plan (a single host, single slot), then + * stable exhaustion. Degenerate but required boundary case -- parjob is + * only ever reached for maxTasks > 1 in production (sched_make_primary_ + * alloc_()'s dispatch), but the iterator itself makes no such assumption. + */ +static void test_single_task_yields_single_plan(void) +{ + xce_primary_resreq_t resreq; + xce_alloc_plan_iter_t iter_storage; + xce_alloc_plan_iter_t* iter = &iter_storage; + xce_alloc_plan_t* plan; + static const int expect_k1[] = { 1 }; + + memset(&resreq, 0, sizeof(resreq)); + resreq.minTasks = 1; + resreq.maxTasks = 1; + + bind_plan_iter(iter); + TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL)); + + plan = iter->next(iter); + assert_plan_eq(plan, expect_k1, 1); + + TEST_ASSERT_NULL(iter->next(iter)); + + iter->fini(iter); +} + +/* + * Req 4 AC 1 ("never inspects live worker state" / "pure function of the + * task count alone"): the SAME maxTasks value must yield IDENTICAL plan + * sequences regardless of what (if anything) `sched` points to -- + * including a NULL scheduler, and a garbage-but-non-NULL one whose + * fields are never touched. Since parjob_plan_iter_init()/_next() never + * dereference `sched` at all, a non-NULL "poisoned" scheduler pointer + * that would crash on any real field access is a stronger proof of + * purity than merely passing NULL. + */ +static void test_plan_generation_ignores_scheduler_state(void) +{ + xce_primary_resreq_t resreq; + xce_alloc_plan_iter_t iter_a_storage; + xce_alloc_plan_iter_t* iter_a = &iter_a_storage; + xce_alloc_plan_iter_t iter_b_storage; + xce_alloc_plan_iter_t* iter_b = &iter_b_storage; + xce_scheduler_t* poisoned_sched = (xce_scheduler_t*)0x1; + int k; + + memset(&resreq, 0, sizeof(resreq)); + resreq.minTasks = 1; + resreq.maxTasks = 7; + + bind_plan_iter(iter_a); + TEST_ASSERT_EQUAL_INT(0, iter_a->init(iter_a, &resreq, NULL)); + + bind_plan_iter(iter_b); + /* A deliberately invalid, non-dereferenceable pointer -- init()/ + * next() must never read through it, or this test would crash + * instead of merely failing an assertion. */ + TEST_ASSERT_EQUAL_INT(0, iter_b->init(iter_b, &resreq, poisoned_sched)); + + for (k = 1; k <= 7; k++) { + xce_alloc_plan_t* plan_a = iter_a->next(iter_a); + xce_alloc_plan_t* plan_b = iter_b->next(iter_b); + int i; + + TEST_ASSERT_NOT_NULL(plan_a); + TEST_ASSERT_NOT_NULL(plan_b); + TEST_ASSERT_EQUAL_INT(plan_a->nbuckets, plan_b->nbuckets); + for (i = 0; i < plan_a->nbuckets; i++) { + TEST_ASSERT_EQUAL_INT(plan_a->bucket_slots[i], + plan_b->bucket_slots[i]); + } + } + + TEST_ASSERT_NULL(iter_a->next(iter_a)); + TEST_ASSERT_NULL(iter_b->next(iter_b)); + + iter_a->fini(iter_a); + iter_b->fini(iter_b); +} + +/* + * init() rejects invalid input rather than crashing: a NULL iter, a NULL + * resreq, and a non-positive maxTasks. + */ +static void test_init_rejects_invalid_input(void) +{ + xce_primary_resreq_t resreq; + xce_alloc_plan_iter_t iter_storage; + xce_alloc_plan_iter_t* iter = &iter_storage; + + memset(&resreq, 0, sizeof(resreq)); + resreq.maxTasks = 3; + + bind_plan_iter(iter); + TEST_ASSERT_TRUE(iter->init(NULL, &resreq, NULL) != 0); + TEST_ASSERT_TRUE(iter->init(iter, NULL, NULL) != 0); + + resreq.maxTasks = 0; + TEST_ASSERT_TRUE(iter->init(iter, &resreq, NULL) != 0); +} + +/* + * Req 2 AC 6: next() calls pthread_testcancel() (or an equivalent real + * cancellation point) at least once per invocation. Proved behaviorally, + * same technique as test_host_iter.c's test_next_has_a_real_cancellation_ + * point(): a worker thread parked in a tight loop calling next() + * repeatedly (spinning past exhaustion, which still executes the + * cancellation point on every call) is reliably torn down by + * pthread_cancel() from the main thread. If parjob_plan_iter_next() had + * no real cancellation point, this tight loop would never yield to the + * pending cancellation and pthread_join() below would hang (timing out + * the whole test binary) rather than return. + */ +typedef struct { + xce_alloc_plan_iter_t* plan_iter; +} cancel_thread_arg_t; + +static void* cancel_spin_thread(void* argp) +{ + cancel_thread_arg_t* arg = (cancel_thread_arg_t*)argp; + + for (;;) { + (void)arg->plan_iter->next(arg->plan_iter); + } + + return NULL; /* unreachable */ +} + +static void test_next_has_a_real_cancellation_point(void) +{ + xce_primary_resreq_t resreq; + xce_alloc_plan_iter_t iter_storage; + xce_alloc_plan_iter_t* iter = &iter_storage; + cancel_thread_arg_t arg; + pthread_t tid; + int rc; + + memset(&resreq, 0, sizeof(resreq)); + resreq.minTasks = 1; + resreq.maxTasks = 4; + + bind_plan_iter(iter); + TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL)); + + arg.plan_iter = iter; + + rc = pthread_create(&tid, NULL, cancel_spin_thread, &arg); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* Let the spin thread actually get running before cancelling it. */ + usleep(20000); + + rc = pthread_cancel(tid); + TEST_ASSERT_EQUAL_INT(0, rc); + + rc = pthread_join(tid, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* Safe post-cancellation cleanup: pthread_testcancel() only ever + * fires between complete logical steps (function entry), never + * mid-mutation, so the iterator's private state is always + * well-formed here regardless of exactly where cancellation + * landed. */ + iter->fini(iter); +} + +void setUp(void) { return; } + +void tearDown(void) { return; } + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_plans_ordered_ascending_host_count_with_balanced_partitions); + RUN_TEST(test_plans_for_evenly_divisible_task_count); + RUN_TEST(test_single_task_yields_single_plan); + RUN_TEST(test_plan_generation_ignores_scheduler_state); + RUN_TEST(test_init_rejects_invalid_input); + RUN_TEST(test_next_has_a_real_cancellation_point); + return UNITY_END(); +}