From 542c6e602a1ff0ca6bc2402fd45bcb18e9cec16c Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 8 Oct 2026 11:23:16 +0000 Subject: [PATCH] feat(scheduler-sim-decision-trace): hook resource-requirement branching Task 3.2: single requirements, top-level compounds, alternatives and members push their path level and record one begin and one end; member failures record the members not tried, merge failures and skipped alternatives are separate entries, the chosen alternative or all-failed is recorded, and each compound attempt's reasons are captured before the job's stored reason is replaced. Scheduling results are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../scheduler-sim-decision-trace/tasks.md | 3 +- src/scheduler/src/scheduler.c | 62 +- src/scheduler/testsuite/meson.build | 26 + .../testsuite/test_trace_hooks_resreq.c | 592 ++++++++++++++++++ 4 files changed, 680 insertions(+), 3 deletions(-) create mode 100644 src/scheduler/testsuite/test_trace_hooks_resreq.c diff --git a/.kiro/specs/scheduler-sim-decision-trace/tasks.md b/.kiro/specs/scheduler-sim-decision-trace/tasks.md index 4c1b81b6..4c065a56 100644 --- a/.kiro/specs/scheduler-sim-decision-trace/tasks.md +++ b/.kiro/specs/scheduler-sim-decision-trace/tasks.md @@ -138,7 +138,7 @@ - _Boundary: TraceHooks (allocator loop)_ - _Depends: 2.1, 2.3_ -- [ ] 3.2 Hook resource-requirement branching (P) +- [x] 3.2 Hook resource-requirement branching (P) - Single, top-level compound, each alternative and each member push their path level and record exactly one begin and one end. - The begin carries task counts and requested resources for single requirements and members, and the member count for containers. - A member that fails or errors records the number of members not tried. A merge failure after a member is a separate entry. @@ -361,3 +361,4 @@ - 2.3: the outcome capture functions are `sched_trace_capture_dispatch`, `sched_trace_capture_pend` and `sched_trace_rec_reconstructed`, declared in trace.h; 3.x and 5.3 call them. Limit charges are classified by the limit flag or a `limit=` prefix and resolved via glimits (shared) or the worker's hlimits (host, worker looked up by host name). An unresolved charge has `limited_res == NULL`, which is the raw-number fallback. Shared pend entries carry host "" and are treated as host-less. For 4.1: limit totals over omitted hosts have a scope without the host, and a NULL host name yields the scope "h=". Follow-up (low): capture's `key_find`/`vget` on resmgr/workers run without the RTTI read lock that `sched_get_worker` takes. That is safe while the sim runs in lock-step; the 2.4 split may wrap them. - 2.4: the trace sources are now sched_trace.c (record, arena, entries, budget, requirement/plan/check), sched_trace_scan.c (positions, host verdicts/rows, tallies, rollback, commit, search end), sched_trace_capture.c, sched_trace_outcome.c (dispatch, pend, reconstructed) and sched_trace_internal.h (12 shared `sti_` helpers). Every target that compiles one trace .c file needs all four. Later tasks that extend the trace must keep each file under 800 lines and add new files to the core isolation scan. - 3.1: allocator-loop hooks live in `sched_allocator()` (sched_election.c), with helpers `sched_trace_push_no_decision()` and `sched_trace_attach()`. Selection is captured at job lookup, before election, so NO_SCHEDOBJ still has its selection; cluster-wide limits are captured after election. The position counter (`ctl->position`) is not SCHED_TRACE-gated (it creates the record) and counts only real pops. `sched_sim_isolation_lib` stays without the trace sources (unresolved references in a static archive are harmless); 5.1 decides whether to add them. `core->decisions` holds 5 items, so trace-only decisions take FIFO slots and cannot reorder real ones. +- 3.2: requirement-branch hooks are in scheduler.c (`sched_make_allocation`, `sched_make_alter_allocation`, `sched_make_compound_alloc_`). The alternatives after the chosen one are not stored as entries; 4.1 renders "alternatives i+1..n not tried" from the alt_result (i,n). A merge failure stores an ATTEMPT_REASONS entry with ncodes==0. A failing single requirement records no attempt reasons; its pend outcome carries them. A single job with a NULL resreq stores its begin as a container with 0 members before returning -EINVAL, and 4.1 must tolerate that. diff --git a/src/scheduler/src/scheduler.c b/src/scheduler/src/scheduler.c index e538c20e..bae5a8c2 100644 --- a/src/scheduler/src/scheduler.c +++ b/src/scheduler/src/scheduler.c @@ -20,6 +20,7 @@ #include "resobj.h" #include "resreq.h" #include "sched/alloc_cache.h" +#include "sched/trace.h" #include "scheduler.h" #include "utils.h" #include "xcealloc.h" @@ -343,16 +344,29 @@ static int sched_make_compound_alloc_(xce_sched_core_t* core, int i; xce_sched_ctx_t ctx; xce_job_alloc_t* merged = NULL; + int m = resreq->primaries.count; int rc = 0; sched_ctx_init(&ctx, core, obj); - for (i = 0; i < resreq->primaries.count; i++) { + for (i = 0; i < m; i++) { xce_primary_resreq_t* primary = (xce_primary_resreq_t*)xlist_get(&(resreq->primaries), i); sched_ctx_set_primary(&ctx, primary); + /* Decision trace (3.1-3.3): member i+1 of m under the container the + * caller pushed; a failing member lists the ones after it as not + * tried. */ + SCHED_TRACE(core, + sched_trace_path_push(core->trace_rec, + XCE_TRACE_PATH_MEMBER, i + 1, m)); + SCHED_TRACE(core, + sched_trace_resreq_begin(core->trace_rec, primary, 0)); rc = sched_make_primary_alloc_(core, &ctx); + SCHED_TRACE(core, + sched_trace_resreq_end(core->trace_rec, rc, + (rc <= 0) ? (m - (i + 1)) : 0)); + SCHED_TRACE(core, sched_trace_path_pop(core->trace_rec)); if (rc <= 0) { break; } @@ -368,6 +382,10 @@ static int sched_make_compound_alloc_(xce_sched_core_t* core, free_JobAlloc_ptr(&(ctx.alloc)); if (mrc < 0) { + /* member i+1 was satisfied; the merge failure is a + * separate entry, not a second end (3.3) */ + SCHED_TRACE(core, sched_trace_member_failed(core->trace_rec, + i + 1, m, mrc)); rc = mrc; break; } @@ -388,6 +406,13 @@ static int sched_make_compound_alloc_(xce_sched_core_t* core, } } + /* Decision trace (3.5): a failed attempt's reasons, before obj->reason + * is replaced by them (or by a later alternative's). */ + SCHED_TRACE( + core, if (rc <= 0) { + sched_trace_capture_attempt_reasons(core->trace_rec, ctx.reason); + }); + if (ctx.reason != NULL) { /* bug-1884: the previous attempt's reason (its decision has * been published) or an earlier alternative's */ @@ -410,8 +435,9 @@ static int sched_make_alter_allocation(xce_sched_core_t* core, xce_sched_obj_t* obj) { int i; + int n = obj->u.alters.count; - for (i = 0; i < obj->u.alters.count; i++) { + for (i = 0; i < n; i++) { /* bug-1769: u.alters holds xce_compound_schedobj_t* (every builder: * core_events.c, snapshot_load.c, schedobj.c), not xce_sched_obj_t* * -- reading them as the latter overran the 24-byte allocation. @@ -421,15 +447,31 @@ static int sched_make_alter_allocation(xce_sched_core_t* core, int rc; if ((alt == NULL) || (alt->resreq == NULL)) { + /* Decision trace (3.6): skipped in its position */ + SCHED_TRACE(core, + sched_trace_alt_skipped(core->trace_rec, i + 1, n)); continue; } + SCHED_TRACE(core, sched_trace_path_push(core->trace_rec, + XCE_TRACE_PATH_ALT, i + 1, n)); + SCHED_TRACE(core, + sched_trace_resreq_begin(core->trace_rec, NULL, + alt->resreq->primaries.count)); rc = sched_make_compound_alloc_(core, obj, alt->resreq); + SCHED_TRACE(core, sched_trace_resreq_end(core->trace_rec, rc, 0)); + SCHED_TRACE(core, sched_trace_path_pop(core->trace_rec)); if (rc > 0) { + /* Decision trace (3.4): chosen; the rest are not tried */ + SCHED_TRACE(core, + sched_trace_alt_result(core->trace_rec, i + 1, n)); return rc; } } + /* Decision trace (3.5): every alternative failed */ + SCHED_TRACE(core, sched_trace_alt_result(core->trace_rec, 0, n)); + /* FIXME: merge pend reason from all children */ return 0; @@ -446,12 +488,28 @@ int sched_make_allocation(xce_sched_core_t* core, xce_sched_obj_t* obj) switch (obj->tag) { case XCE_RESREQ_PRIMARY: + /* Decision trace (3.1, 3.2): the single requirement */ + SCHED_TRACE(core, sched_trace_path_push(core->trace_rec, + XCE_TRACE_PATH_SINGLE, 1, 1)); + SCHED_TRACE(core, sched_trace_resreq_begin(core->trace_rec, + obj->u.primary->resreq, 0)); rc = sched_make_primary_allocation(core, obj); + SCHED_TRACE(core, sched_trace_resreq_end(core->trace_rec, rc, 0)); + SCHED_TRACE(core, sched_trace_path_pop(core->trace_rec)); break; case XCE_RESREQ_COMPOUND: + /* Decision trace (3.1, 3.2): the top-level compound container */ + SCHED_TRACE(core, sched_trace_path_push(core->trace_rec, + XCE_TRACE_PATH_COMPOUND, 1, 1)); + SCHED_TRACE(core, sched_trace_resreq_begin( + core->trace_rec, NULL, + obj->u.compound->resreq->primaries.count)); rc = sched_make_compound_allocation(core, obj); + SCHED_TRACE(core, sched_trace_resreq_end(core->trace_rec, rc, 0)); + SCHED_TRACE(core, sched_trace_path_pop(core->trace_rec)); break; case XCE_RESREQ_ALTER: + /* Decision trace (3.4-3.6): per-alternative hooks in the loop */ rc = sched_make_alter_allocation(core, obj); break; default: diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 4828f765..924beba4 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -2654,3 +2654,29 @@ sched_trace_hooks_alloc_loop_test = executable( test('scheduler trace hooks alloc loop test', sched_trace_hooks_alloc_loop_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) + +# scheduler-sim-decision-trace task 3.2: the resource-requirement branching +# trace hooks (design.md "TraceHooks", PRIMARY / COMPOUND / ALTER / member +# rows; Requirements 3.1-3.6) on the real sched_make_allocation() chain over +# a bare simulation core. Real-scheduler_srcs link technique; libxce's +# merge_job_alloc() is wrapped for the merge failure. +sched_trace_hooks_resreq_test_srcs = ['test_trace_hooks_resreq.c'] +foreach f : scheduler_srcs + if f != 'src/main.c' + sched_trace_hooks_resreq_test_srcs += ['../' + f] + endif +endforeach + +sched_trace_hooks_resreq_test = executable( + 'sched_trace_hooks_resreq_test', + sched_trace_hooks_resreq_test_srcs, + include_directories:[incdir], + dependencies: [scheduler_deps, unity_dep], + link_args: ['-Wl,--wrap=xcecli_start', + '-Wl,--wrap=xcecli_join', + '-Wl,--wrap=merge_job_alloc'], + install: false) + +test('scheduler trace hooks resreq test', + sched_trace_hooks_resreq_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_trace_hooks_resreq.c b/src/scheduler/testsuite/test_trace_hooks_resreq.c new file mode 100644 index 00000000..0ffa96cc --- /dev/null +++ b/src/scheduler/testsuite/test_trace_hooks_resreq.c @@ -0,0 +1,592 @@ +/* + * test_trace_hooks_resreq.c - the resource-requirement branching trace hooks + * (scheduler-sim-decision-trace spec, task 3.2; design.md "TraceHooks", + * PRIMARY / top-level COMPOUND / ALTER loop / compound member rows; + * Requirements 3.1-3.6). + * + * Drives the real sched_make_allocation() chain and the real seqjob/parjob + * allocator plugins on a bare simulation core (same fixture as + * test_sched_alloc_from_stamp.c's simulation-mode test: no threads, the + * allocators bootstrapped with the sim wrappers), with a record set on + * core->trace_rec for the call, and checks the requirement-branch entries + * the record holds: + * + * - a single requirement: SINGLE 1/1, one begin with its task counts and + * requested resources, one end (3.1, 3.2); + * - a compound whose second member cannot be satisfied: each member's + * begin/end under COMPOUND 1/1, the failing member listing the members + * after it as not tried, then the attempt's failure reasons (3.2, 3.3); + * - a compound whose merge fails after a satisfied member: the member's + * end stays satisfied and a separate merge-failure entry follows (3.3); + * - an alternative job with a skipped, a failed and a chosen alternative, + * and one after it: the skip in its position, the failed alternative's + * reasons, the chosen one and nothing for the rest (3.4, 3.6); + * - an alternative job where every alternative fails: each alternative's + * own failure reasons, then "all failed" (3.5). + * + * Every scenario first runs without a record and then with one, and the + * return code, allocation and pend reason left on the scheduling object + * must be identical (Requirement 10: tracing never changes scheduling). + * + * Only the requirement-branch entry types are inspected, so entries other + * hooks add inside the allocator (plans, positions, hosts) do not matter. + * merge_job_alloc() (libxce) is wrapped at link time for the merge failure. + */ +#include +#include +#include +#include +#include +#include + +#include + +#include "client.h" +#include "controller.capnp.h" +#include "core.h" +#include "decision.h" +#include "plugin.h" +#include "preason.h" +#include "resobj.h" +#include "resreq.h" +#include "sched/trace.h" +#include "sched/types.h" +#include "scheduler.h" +#include "sim.h" +#include "xcealloc.h" +#include "xstring.h" + +extern void sim_bootstrap_seqjob_allocator(xce_sched_sim_driver_t* sim); +extern void sim_bootstrap_parjob_allocator(xce_sched_sim_driver_t* sim); + +#define WORKER_A_NAME "resreq-host-a" +#define WORKER_B_NAME "resreq-host-b" +#define WORKER_SLOTS 2.0 /* 4 slots in the cluster */ +#define QUEUE_NAME "resreq_queue" + +/* ── link seams ─────────────────────────────────────────────────────── */ + +int __wrap_xcecli_start(xcecli_ctx_t c) +{ + (void)c; + return 0; +} + +int __wrap_xcecli_join(xcecli_ctx_t c, int role, hostattrs_t* attrs) +{ + (void)c; + (void)role; + (void)attrs; + return 0; +} + +extern int __real_merge_job_alloc(xce_job_alloc_t*, const xce_job_alloc_t*); + +/* > 0: that many merges succeed, then the next one fails with -ENOMEM and + * the counter is disarmed. 0: every merge is real. */ +static int g_merge_fail_after; + +int __wrap_merge_job_alloc(xce_job_alloc_t* dst, const xce_job_alloc_t* src) +{ + if (g_merge_fail_after > 0) { + if (--g_merge_fail_after == 0) { + return -ENOMEM; + } + } + return __real_merge_job_alloc(dst, src); +} + +void setUp(void) { return; } +void tearDown(void) { return; } + +/* ── fixture ────────────────────────────────────────────────────────── */ + +static xce_sched_sim_driver_t g_sim; +static xce_scheduler_t g_job_wrapper; /* make_sched_job() only */ +static xce_sched_trace_ctl_t g_ctl; + +static void add_sim_worker(xce_sched_core_t* core, const char* name, + double slots) +{ + xce_sched_worker_t* worker + = (xce_sched_worker_t*)calloc(1, sizeof(*worker)); + xce_resource_t* r = NULL; + xce_val_t* val = NULL; + + TEST_ASSERT_NOT_NULL(worker); + worker->name = strdup(name); + worker->status = 0; + worker->idx = -1; + zarray_init(&(worker->resvals), 128, + (zarray_item_killer_t)kill_sched_resval); + zarray_init(&(worker->resInUse), 128, + (zarray_item_killer_t)kill_sched_resval); + + TEST_ASSERT_TRUE(key_find(core->resmgr, (void*)"slots", (void**)&r) >= 0); + TEST_ASSERT_NOT_NULL(r); + + val = (xce_val_t*)calloc(1, sizeof(*val)); + TEST_ASSERT_NOT_NULL(val); + val->which = Value_u_fvalue; + val->u.fvalue = slots; + zarray_put(&(worker->resvals), r->idx, val); + + TEST_ASSERT_TRUE(sched_add_worker(core, worker) >= 0); +} + +static void start_sim(void) +{ + xce_resource_t res; + xce_sched_queue_t* queue; + + memset(&g_sim, 0, sizeof(g_sim)); + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(g_sim.core))); + + memset(&res, 0, sizeof(res)); + res.name = strdup("slots"); + TEST_ASSERT_NOT_NULL(makeResource(g_sim.core.resmgr, &res)); + + sim_bootstrap_seqjob_allocator(&g_sim); + sim_bootstrap_parjob_allocator(&g_sim); + TEST_ASSERT_NOT_NULL(g_sim.core.seqjob_allocator); + TEST_ASSERT_NOT_NULL(g_sim.core.parjob_allocator); + + queue = (xce_sched_queue_t*)calloc(1, sizeof(*queue)); + TEST_ASSERT_NOT_NULL(queue); + queue->name = makeString(QUEUE_NAME); + TEST_ASSERT_TRUE(sched_add_queue(&(g_sim.core), queue) >= 0); + + add_sim_worker(&(g_sim.core), WORKER_A_NAME, WORKER_SLOTS); + add_sim_worker(&(g_sim.core), WORKER_B_NAME, WORKER_SLOTS); + + memset(&g_job_wrapper, 0, sizeof(g_job_wrapper)); + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(g_job_wrapper.core))); + g_job_wrapper.core.resmgr = g_sim.core.resmgr; + g_job_wrapper.core.workers = g_sim.core.workers; + g_job_wrapper.core.queues = g_sim.core.queues; + + TEST_ASSERT_EQUAL_INT(0, xce_core_begin_session(&(g_sim.core))); + + memset(&g_ctl, 0, sizeof(g_ctl)); + g_ctl.all = true; + g_ctl.level = XCE_TRACE_LEVEL_FULL; + g_ctl.line_bound = XCE_TRACE_LINE_BOUND_DEFAULT; + g_ctl.session_no = 1; + g_ctl.policy_name = "builtin_fcfs"; +} + +static xce_sched_job_t* make_job(uint64_t jobid, const char* resreq, + xce_resreq_tag_t want_tag) +{ + xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j)); + job_submit_data_t* submit = (job_submit_data_t*)calloc(1, sizeof(*submit)); + xce_sched_job_t* job; + + TEST_ASSERT_NOT_NULL(j); + TEST_ASSERT_NOT_NULL(submit); + submit->queue = (char*)QUEUE_NAME; + submit->user = (char*)"resreq_user"; + submit->project_name = (char*)"resreq_proj"; + submit->resreq = (char*)resreq; + submit->mintasks = 1; + submit->maxtasks = 1; + j->jobid = jobid; + j->state = JOB_STAT_PEND; + j->submit = submit; + + job = make_sched_job(&g_job_wrapper, j); + TEST_ASSERT_NOT_NULL(job); + TEST_ASSERT_NOT_NULL(job->schedobj); + TEST_ASSERT_EQUAL_INT_MESSAGE(want_tag, job->schedobj->tag, + "fixture: unexpected resreq kind"); + if (want_tag == XCE_RESREQ_PRIMARY) { + job->schedobj->u.primary->resreq->minTasks = 1; + job->schedobj->u.primary->resreq->maxTasks = 1; + } + TEST_ASSERT_TRUE(sched_add_job(&(g_sim.core), job) >= 0); + return job; +} + +/* What one sched_make_allocation() call left on the scheduling object. */ +typedef struct { + int rc; + xce_job_alloc_t* alloc; + xce_pendreason_t* reason; +} run_result_t; + +/* Runs the real allocation chain on obj, with a record on the core when + * `rec` is non-NULL, and takes the allocation and reason off obj. */ +static run_result_t run_alloc(xce_sched_obj_t* obj, xce_sched_trace_rec_t* rec, + int merge_fail_after) +{ + run_result_t r; + + obj->alloc = NULL; + obj->reason = NULL; + + g_merge_fail_after = merge_fail_after; + g_sim.core.trace_rec = rec; + r.rc = sched_make_allocation(&(g_sim.core), obj); + g_sim.core.trace_rec = NULL; + TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_merge_fail_after, + "fixture: the merge failure was not hit"); + + r.alloc = obj->alloc; + r.reason = obj->reason; + obj->alloc = NULL; + obj->reason = NULL; + return r; +} + +/* Runs obj without and then with a record, asserts the scheduling results + * are identical, and returns the record. */ +static xce_sched_trace_rec_t* run_traced(xce_sched_job_t* job, + int merge_fail_after, int* rc_out) +{ + xce_sched_trace_rec_t* rec; + run_result_t off; + run_result_t on; + char* why = NULL; + + off = run_alloc(job->schedobj, NULL, merge_fail_after); + + rec = sched_trace_rec_new(&g_ctl, job->jobid, XCE_TRACE_REC_DECISION); + TEST_ASSERT_NOT_NULL(rec); + on = run_alloc(job->schedobj, rec, merge_fail_after); + + TEST_ASSERT_EQUAL_INT_MESSAGE(off.rc, on.rc, + "tracing changed the allocation result"); + TEST_ASSERT_TRUE_MESSAGE(job_alloc_eq(off.alloc, on.alloc, &why), + why != NULL ? why : "allocation differs"); + TEST_ASSERT_TRUE_MESSAGE((off.reason == NULL) == (on.reason == NULL), + "tracing changed whether a reason is left"); + if (off.reason != NULL) { + TEST_ASSERT_TRUE_MESSAGE( + job_preason_eq(off.reason->reasons, on.reason->reasons, &why), + why != NULL ? why : "pend reason differs"); + } + TEST_ASSERT_EQUAL_INT_MESSAGE(0, rec->resreq_open, + "a requirement begin has no end"); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, rec->path.depth, "a path push has no pop"); + + free_JobAlloc_ptr(&(off.alloc)); + free_JobAlloc_ptr(&(on.alloc)); + free_PendReason_ptr(&(off.reason)); + free_PendReason_ptr(&(on.reason)); + *rc_out = on.rc; + return rec; +} + +/* ── entry checks ───────────────────────────────────────────────────── */ + +#define MAX_BRANCH 32 + +static bool is_branch_entry(sched_trace_entry_type_t t) +{ + return (t == SCHED_TRACE_E_RESREQ_BEGIN) || (t == SCHED_TRACE_E_RESREQ_END) + || (t == SCHED_TRACE_E_ALT_SKIPPED) || (t == SCHED_TRACE_E_ALT_RESULT) + || (t == SCHED_TRACE_E_MEMBER_FAILED) + || (t == SCHED_TRACE_E_ATTEMPT_REASONS); +} + +/* The requirement-branch entries of rec, in the order made. */ +static int branch_entries(const xce_sched_trace_rec_t* rec, + const sched_trace_entry_t** out) +{ + const sched_trace_entry_t* e; + int n = 0; + + for (e = rec->entries; e != NULL; e = e->next) { + if (is_branch_entry(e->type)) { + TEST_ASSERT_TRUE(n < MAX_BRANCH); + out[n++] = e; + } + } + return n; +} + +#define K_NONE (-1) + +static void assert_path(const sched_trace_entry_t* e, int k0, int i0, int n0, + int k1, int i1, int n1) +{ + int depth = (k0 == K_NONE) ? 0 : ((k1 == K_NONE) ? 1 : 2); + + TEST_ASSERT_EQUAL_INT_MESSAGE(depth, e->path.depth, "path depth"); + if (depth >= 1) { + TEST_ASSERT_EQUAL_INT(k0, e->path.lvl[0].kind); + TEST_ASSERT_EQUAL_INT(i0, e->path.lvl[0].i); + TEST_ASSERT_EQUAL_INT(n0, e->path.lvl[0].n); + } + if (depth == 2) { + TEST_ASSERT_EQUAL_INT(k1, e->path.lvl[1].kind); + TEST_ASSERT_EQUAL_INT(i1, e->path.lvl[1].i); + TEST_ASSERT_EQUAL_INT(n1, e->path.lvl[1].n); + } +} + +static void assert_type(const sched_trace_entry_t* e, + sched_trace_entry_type_t t) +{ + TEST_ASSERT_EQUAL_INT_MESSAGE(t, e->type, "unexpected entry type"); +} + +/* A member/single begin: task counts and one requested resource `res`. */ +static void assert_primary_begin(const sched_trace_entry_t* e, int min_tasks, + int max_tasks, const char* res, float value) +{ + int k; + + assert_type(e, SCHED_TRACE_E_RESREQ_BEGIN); + TEST_ASSERT_FALSE(e->u.resreq_begin.container); + TEST_ASSERT_EQUAL_INT(min_tasks, e->u.resreq_begin.min_tasks); + TEST_ASSERT_EQUAL_INT(max_tasks, e->u.resreq_begin.max_tasks); + TEST_ASSERT_TRUE(e->u.resreq_begin.nreq > 0); + for (k = 0; k < e->u.resreq_begin.nreq; k++) { + const sched_trace_req_t* r = &e->u.resreq_begin.req[k]; + + if ((r->name != NULL) && (strcmp(r->name, res) == 0)) { + TEST_ASSERT_EQUAL_FLOAT(value, r->value); + return; + } + } + TEST_FAIL_MESSAGE("the requested resource is not in the begin entry"); +} + +static void assert_container_begin(const sched_trace_entry_t* e, int nmembers) +{ + assert_type(e, SCHED_TRACE_E_RESREQ_BEGIN); + TEST_ASSERT_TRUE(e->u.resreq_begin.container); + TEST_ASSERT_EQUAL_INT(nmembers, e->u.resreq_begin.nmembers); +} + +/* rc_sign: 1 satisfied, 0 not satisfied, -1 internal error */ +static void assert_end(const sched_trace_entry_t* e, int rc_sign, + int not_tried_after) +{ + assert_type(e, SCHED_TRACE_E_RESREQ_END); + if (rc_sign > 0) { + TEST_ASSERT_TRUE_MESSAGE(e->u.resreq_end.rc > 0, "expected satisfied"); + } else if (rc_sign == 0) { + TEST_ASSERT_EQUAL_INT_MESSAGE(0, e->u.resreq_end.rc, + "expected not satisfied"); + } else { + TEST_ASSERT_TRUE_MESSAGE(e->u.resreq_end.rc < 0, "expected an error"); + } + TEST_ASSERT_EQUAL_INT(not_tried_after, e->u.resreq_end.not_tried_after); +} + +/* ── tests ──────────────────────────────────────────────────────────── */ + +static void test_single_requirement(void) +{ + xce_sched_job_t* job + = make_job(632001, "rusage[mem=1]", XCE_RESREQ_PRIMARY); + const sched_trace_entry_t* b[MAX_BRANCH]; + xce_sched_trace_rec_t* rec; + int rc; + int n; + + rec = run_traced(job, 0, &rc); + TEST_ASSERT_TRUE(rc > 0); + n = branch_entries(rec, b); + TEST_ASSERT_EQUAL_INT_MESSAGE(2, n, "a single requirement: begin + end"); + + assert_primary_begin(b[0], 1, 1, "mem", 1.0f); + assert_path(b[0], XCE_TRACE_PATH_SINGLE, 1, 1, K_NONE, 0, 0); + assert_end(b[1], 1, 0); + assert_path(b[1], XCE_TRACE_PATH_SINGLE, 1, 1, K_NONE, 0, 0); + + sched_trace_rec_free(rec); +} + +static void test_compound_failing_member(void) +{ + xce_sched_job_t* job = make_job( + 632002, "1*{rusage[mem=1]} + 9*{rusage[mem=2]} + 1*{rusage[mem=3]}", + XCE_RESREQ_COMPOUND); + const sched_trace_entry_t* b[MAX_BRANCH]; + xce_sched_trace_rec_t* rec; + int rc; + int n; + + rec = run_traced(job, 0, &rc); + TEST_ASSERT_EQUAL_INT(0, rc); + n = branch_entries(rec, b); + TEST_ASSERT_EQUAL_INT(7, n); + + assert_container_begin(b[0], 3); + assert_path(b[0], XCE_TRACE_PATH_COMPOUND, 1, 1, K_NONE, 0, 0); + + assert_primary_begin(b[1], 1, 1, "mem", 1.0f); + assert_path(b[1], XCE_TRACE_PATH_COMPOUND, 1, 1, XCE_TRACE_PATH_MEMBER, 1, + 3); + assert_end(b[2], 1, 0); + + assert_primary_begin(b[3], 9, 9, "mem", 2.0f); + assert_path(b[3], XCE_TRACE_PATH_COMPOUND, 1, 1, XCE_TRACE_PATH_MEMBER, 2, + 3); + assert_end(b[4], 0, 1); /* point of failure: member 3/3 not tried */ + assert_path(b[4], XCE_TRACE_PATH_COMPOUND, 1, 1, XCE_TRACE_PATH_MEMBER, 2, + 3); + + assert_type(b[5], SCHED_TRACE_E_ATTEMPT_REASONS); + TEST_ASSERT_TRUE_MESSAGE(b[5]->u.reasons.ncodes > 0, + "the failed attempt's reasons are not captured"); + assert_path(b[5], XCE_TRACE_PATH_COMPOUND, 1, 1, K_NONE, 0, 0); + + assert_end(b[6], 0, 0); + assert_path(b[6], XCE_TRACE_PATH_COMPOUND, 1, 1, K_NONE, 0, 0); + + sched_trace_rec_free(rec); +} + +static void test_compound_merge_failure(void) +{ + xce_sched_job_t* job = make_job( + 632003, "1*{rusage[mem=1]} + 1*{rusage[mem=2]} + 1*{rusage[mem=3]}", + XCE_RESREQ_COMPOUND); + const sched_trace_entry_t* b[MAX_BRANCH]; + xce_sched_trace_rec_t* rec; + int rc; + int n; + + /* the first merge (after member 2) fails */ + rec = run_traced(job, 1, &rc); + TEST_ASSERT_EQUAL_INT(-ENOMEM, rc); + n = branch_entries(rec, b); + TEST_ASSERT_EQUAL_INT(8, n); + + assert_container_begin(b[0], 3); + assert_primary_begin(b[1], 1, 1, "mem", 1.0f); + assert_end(b[2], 1, 0); + assert_primary_begin(b[3], 1, 1, "mem", 2.0f); + assert_path(b[3], XCE_TRACE_PATH_COMPOUND, 1, 1, XCE_TRACE_PATH_MEMBER, 2, + 3); + assert_end(b[4], 1, 0); /* the member itself was satisfied */ + + assert_type(b[5], SCHED_TRACE_E_MEMBER_FAILED); + TEST_ASSERT_EQUAL_INT(2, b[5]->u.member_failed.j); + TEST_ASSERT_EQUAL_INT(3, b[5]->u.member_failed.m); + TEST_ASSERT_EQUAL_INT(-ENOMEM, b[5]->u.member_failed.err); + + assert_type(b[6], SCHED_TRACE_E_ATTEMPT_REASONS); + assert_end(b[7], -1, 0); + TEST_ASSERT_EQUAL_INT(-ENOMEM, b[7]->u.resreq_end.rc); + + sched_trace_rec_free(rec); +} + +static void test_alternatives_skipped_failed_chosen(void) +{ + xce_sched_job_t* job = make_job( + 632004, + "{1*{rusage[mem=1]}} || {9*{rusage[mem=2]}} || " + "{1*{rusage[mem=3]} + 1*{rusage[mem=4]}} || {1*{rusage[mem=5]}}", + XCE_RESREQ_ALTER); + xce_compound_schedobj_t* alt1; + xce_compound_resreq_t* saved; + const sched_trace_entry_t* b[MAX_BRANCH]; + xce_sched_trace_rec_t* rec; + int rc; + int n; + + TEST_ASSERT_EQUAL_INT(4, job->schedobj->u.alters.count); + /* alternative 1 has no requirement content: skipped */ + alt1 = (xce_compound_schedobj_t*)xlist_get(&(job->schedobj->u.alters), 0); + TEST_ASSERT_NOT_NULL(alt1); + saved = alt1->resreq; + alt1->resreq = NULL; + rec = run_traced(job, 0, &rc); + alt1->resreq = saved; + + TEST_ASSERT_TRUE(rc > 0); + n = branch_entries(rec, b); + TEST_ASSERT_EQUAL_INT(13, n); + + assert_type(b[0], SCHED_TRACE_E_ALT_SKIPPED); + TEST_ASSERT_EQUAL_INT(1, b[0]->u.alt.i); + TEST_ASSERT_EQUAL_INT(4, b[0]->u.alt.n); + assert_path(b[0], K_NONE, 0, 0, K_NONE, 0, 0); + + /* alternative 2/4: not satisfied, with its reasons */ + assert_container_begin(b[1], 1); + assert_path(b[1], XCE_TRACE_PATH_ALT, 2, 4, K_NONE, 0, 0); + assert_primary_begin(b[2], 9, 9, "mem", 2.0f); + assert_path(b[2], XCE_TRACE_PATH_ALT, 2, 4, XCE_TRACE_PATH_MEMBER, 1, 1); + assert_end(b[3], 0, 0); + assert_type(b[4], SCHED_TRACE_E_ATTEMPT_REASONS); + TEST_ASSERT_TRUE(b[4]->u.reasons.ncodes > 0); + assert_path(b[4], XCE_TRACE_PATH_ALT, 2, 4, K_NONE, 0, 0); + assert_end(b[5], 0, 0); + assert_path(b[5], XCE_TRACE_PATH_ALT, 2, 4, K_NONE, 0, 0); + + /* alternative 3/4: two members, satisfied */ + assert_container_begin(b[6], 2); + assert_path(b[6], XCE_TRACE_PATH_ALT, 3, 4, K_NONE, 0, 0); + assert_primary_begin(b[7], 1, 1, "mem", 3.0f); + assert_path(b[7], XCE_TRACE_PATH_ALT, 3, 4, XCE_TRACE_PATH_MEMBER, 1, 2); + assert_end(b[8], 1, 0); + assert_primary_begin(b[9], 1, 1, "mem", 4.0f); + assert_path(b[9], XCE_TRACE_PATH_ALT, 3, 4, XCE_TRACE_PATH_MEMBER, 2, 2); + assert_end(b[10], 1, 0); + assert_end(b[11], 1, 0); + assert_path(b[11], XCE_TRACE_PATH_ALT, 3, 4, K_NONE, 0, 0); + + /* chosen: 3/4, alternative 4 not tried (nothing recorded for it) */ + assert_type(b[12], SCHED_TRACE_E_ALT_RESULT); + TEST_ASSERT_EQUAL_INT(3, b[12]->u.alt.i); + TEST_ASSERT_EQUAL_INT(4, b[12]->u.alt.n); + assert_path(b[12], K_NONE, 0, 0, K_NONE, 0, 0); + + sched_trace_rec_free(rec); +} + +static void test_all_alternatives_fail_each_has_reasons(void) +{ + xce_sched_job_t* job = make_job( + 632005, "{9*{rusage[mem=1]}} || {7*{rusage[mem=2]}}", XCE_RESREQ_ALTER); + const sched_trace_entry_t* b[MAX_BRANCH]; + xce_sched_trace_rec_t* rec; + int rc; + int n; + int a; + + rec = run_traced(job, 0, &rc); + TEST_ASSERT_EQUAL_INT(0, rc); + n = branch_entries(rec, b); + TEST_ASSERT_EQUAL_INT(11, n); + + for (a = 0; a < 2; a++) { + const sched_trace_entry_t** e = &b[a * 5]; + + assert_container_begin(e[0], 1); + assert_path(e[0], XCE_TRACE_PATH_ALT, a + 1, 2, K_NONE, 0, 0); + assert_primary_begin(e[1], (a == 0) ? 9 : 7, (a == 0) ? 9 : 7, "mem", + (a == 0) ? 1.0f : 2.0f); + assert_end(e[2], 0, 0); + assert_type(e[3], SCHED_TRACE_E_ATTEMPT_REASONS); + TEST_ASSERT_TRUE_MESSAGE(e[3]->u.reasons.ncodes > 0, + "an alternative's own reasons are missing"); + assert_path(e[3], XCE_TRACE_PATH_ALT, a + 1, 2, K_NONE, 0, 0); + assert_end(e[4], 0, 0); + } + + assert_type(b[10], SCHED_TRACE_E_ALT_RESULT); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, b[10]->u.alt.i, "expected all failed"); + TEST_ASSERT_EQUAL_INT(2, b[10]->u.alt.n); + + sched_trace_rec_free(rec); +} + +int main(void) +{ + UNITY_BEGIN(); + start_sim(); + RUN_TEST(test_single_requirement); + RUN_TEST(test_compound_failing_member); + RUN_TEST(test_compound_merge_failure); + RUN_TEST(test_alternatives_skipped_failed_chosen); + RUN_TEST(test_all_alternatives_fail_each_has_reasons); + return UNITY_END(); +}