feat(scheduler-sim-decision-trace): capture decision outcomes and reconstructed dispatches

Task 2.3: dispatch rows with limit charges listed separately and
resolved through the job's matched limits (raw-number fallback), pend
reason set deep-copied, 50 hosts in detail with the rest aggregated,
and RECONSTRUCTED records holding the dispatch outcome.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-08 09:58:21 +00:00
parent 79c5a0cbda
commit 8efe142bad
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
4 changed files with 1269 additions and 68 deletions

View file

@ -27,6 +27,19 @@
* unmet rows, stored after the host's verdict line, and they set its
* SHORTFALL and/or LIMIT verdict.
*
* Task 2.3 covers the decision outcomes (Requirements 6.1-6.4, 9.4) and the
* reconstructed-dispatch record (2.5), over the same fixture plus a workers
* table:
* - a dispatch keeps per-host and shared resource rows apart from the
* limit charges, each charge resolved through the job's matched limits
* (or, unresolved, parsed from its name with an unknown limited
* resource: the raw-number fallback);
* - past 50 hosts, the rest become per-resource and per-limit totals and
* an omitted-host count; a pend keeps the first 50 hosts' entries and
* the host-less ones and counts the rest per pend-reason code;
* - with 50 or fewer hosts the pend copy equals its source under
* job_preason_eq(), the equality libxce defines for pend reasons.
*
* Links src/sched_trace.c and src/sched_trace_capture.c, plus libxce for
* the pend-reason, resmgr and container helpers.
*/
@ -525,6 +538,7 @@ typedef struct {
xce_vres_t vres_lic;
xce_vres_t vres_host;
xce_vres_t vres_hcap;
xce_sched_worker_t w_bare; /* n02: registered, no matched host limits */
} rows_fx_t;
#define ROWS_HOST_IDX 2
@ -593,6 +607,25 @@ static rows_fx_t* rows_fx_new(void)
fx->w.name = "n01";
fx->w.idx = ROWS_HOST_IDX;
/* workers by name, for the dispatch capture's host-limit lookup */
{
kvmgr_params_t wk_params = { .key_type = KVMGR_KEY_STRING,
.nslots = 16,
.delta = 8,
.datakiller = NULL };
fx->core.workers = makeInstance(&xkvmgr_type, &wk_params);
TEST_ASSERT_NOT_NULL(fx->core.workers);
fx->w_bare.name = "n02";
TEST_ASSERT_TRUE(key_add(fx->core.workers, (void*)fx->w.name, &fx->w)
>= 0);
TEST_ASSERT_TRUE(key_add(fx->core.workers, (void*)fx->w_bare.name,
&fx->w_bare)
>= 0);
fx->w.idx = ROWS_HOST_IDX; /* key_add's slot is not the idx */
fx->w_bare.idx = ROWS_HOST_IDX + 1;
}
return fx;
}
@ -1023,6 +1056,510 @@ static void test_check_rows_host_rows_count_against_budget(void)
rows_fx_free(fx);
}
/* ---- decision outcomes (6.1-6.4, 9.4) and reconstructed records (2.5) - */
typedef struct {
const char* res;
double value;
} t_av_t;
static xce_alloc_t* t_alloc(const char* res, double value)
{
xce_alloc_t* a = (xce_alloc_t*)calloc(1, sizeof(*a));
TEST_ASSERT_NOT_NULL(a);
a->resname = strdup(res);
a->reserve = strdup("");
a->value = value;
return a;
}
static xce_alloc_t** t_allocs(int n, const t_av_t* av)
{
xce_alloc_t** v = (xce_alloc_t**)calloc((size_t)n + 1, sizeof(*v));
int i;
TEST_ASSERT_NOT_NULL(v);
for (i = 0; i < n; i++) {
v[i] = t_alloc(av[i].res, av[i].value);
}
return v;
}
static xce_host_alloc_t* t_host(const char* host, int n, const t_av_t* av)
{
xce_host_alloc_t* h = (xce_host_alloc_t*)calloc(1, sizeof(*h));
TEST_ASSERT_NOT_NULL(h);
h->host = strdup(host);
h->nallocs = n;
h->allocs = t_allocs(n, av);
return h;
}
static void t_free_allocs(int n, xce_alloc_t** v)
{
int i;
for (i = 0; (v != NULL) && (i < n); i++) {
if (v[i] != NULL) {
free(v[i]->resname);
free(v[i]->reserve);
free(v[i]);
}
}
free(v);
}
static void t_free_job(xce_job_alloc_t* j)
{
int i;
for (i = 0; i < j->nhosts; i++) {
t_free_allocs(j->allocs[i]->nallocs, j->allocs[i]->allocs);
free(j->allocs[i]->host);
free(j->allocs[i]);
}
free(j->allocs);
t_free_allocs(j->nres_shared, j->allocs_shared);
free(j);
}
static xce_job_alloc_t* t_job(int nhosts)
{
xce_job_alloc_t* j = (xce_job_alloc_t*)calloc(1, sizeof(*j));
TEST_ASSERT_NOT_NULL(j);
j->jobid = 0x77;
j->nhosts = nhosts;
j->allocs = (xce_host_alloc_t**)calloc((size_t)nhosts + 1,
sizeof(*j->allocs));
TEST_ASSERT_NOT_NULL(j->allocs);
return j;
}
static void assert_alloc_row(const sched_trace_alloc_row_t* row,
const char* res, double value)
{
TEST_ASSERT_EQUAL_STRING(res, row->res);
TEST_ASSERT_EQUAL_FLOAT((float)value, (float)row->value);
TEST_ASSERT_NULL(row->limit);
TEST_ASSERT_NULL(row->scope);
TEST_ASSERT_NULL(row->limited_res);
}
static void assert_charge(const sched_trace_alloc_row_t* row, const char* res,
double value, const char* limit, const char* scope,
const char* limited)
{
if (res == NULL) {
TEST_ASSERT_NULL(row->res);
} else {
TEST_ASSERT_EQUAL_STRING(res, row->res);
}
TEST_ASSERT_EQUAL_FLOAT((float)value, (float)row->value);
TEST_ASSERT_EQUAL_STRING(limit, row->limit);
TEST_ASSERT_EQUAL_STRING(scope, row->scope);
if (limited == NULL) {
TEST_ASSERT_NULL(row->limited_res);
} else {
TEST_ASSERT_EQUAL_STRING(limited, row->limited_res);
}
}
/* Per-host rows and shared rows, limit charges listed apart: resolved
* through obj->hlimits[w->idx] (w by host name) and obj->glimits; a
* registered limit flag or, for an unregistered name, the "limit=" prefix
* makes a charge; an unresolved charge keeps its parsed name and scope and
* an unknown limited resource, so it renders as a raw number (6.1-6.3). */
static void test_dispatch_separates_limit_charges(void)
{
rows_fx_t* fx = rows_fx_new();
xce_sched_trace_ctl_t ctl = make_ctl("fcfs");
xce_sched_trace_rec_t* rec;
xce_job_alloc_t* job = t_job(2);
const t_av_t h1[] = { { "mem", 8 }, { "limit=host_slots/u=alice", 4 },
{ "slots", 4 }, { "limit=hcap", 4 } };
const t_av_t h2[] = { { "mem", 2 }, { "limit=host_slots/u=alice", 1 },
{ "gpu", 1 }, { "limit=ghost/u=alice", 1 } };
const t_av_t sh[] = { { "lic", 1 },
{ "limit=lic_cap/u=alice/q=normal", 1 },
{ "limit=site_total/p=proj1", 1 } };
const sched_trace_dispatch_out_t* d;
job->allocs[0] = t_host("n01", 4, h1);
job->allocs[1] = t_host("n02", 4, h2);
job->nres_shared = 3;
job->allocs_shared = t_allocs(3, sh);
rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION);
TEST_ASSERT_EQUAL_INT(SCHED_TRACE_OUTCOME_NONE, rec->outcome);
sched_trace_capture_dispatch(rec, &fx->core, &fx->obj, job);
TEST_ASSERT_EQUAL_INT(SCHED_TRACE_OUTCOME_DISPATCH, rec->outcome);
TEST_ASSERT_EQUAL_INT(0, rec->nentries); /* outside the line budget */
d = &rec->dispatch;
TEST_ASSERT_EQUAL_INT(2, d->nhosts);
TEST_ASSERT_EQUAL_INT(0, d->omitted_hosts);
TEST_ASSERT_EQUAL_INT(0, d->nagg_res);
TEST_ASSERT_EQUAL_INT(0, d->nagg_charges);
/* n01: resources in allocation order, charges resolved through its
* host limits, the host appended to the scope */
TEST_ASSERT_EQUAL_STRING("n01", d->hosts[0].host);
TEST_ASSERT_EQUAL_INT(2, d->hosts[0].nres);
assert_alloc_row(&d->hosts[0].res[0], "mem", 8);
assert_alloc_row(&d->hosts[0].res[1], "slots", 4);
TEST_ASSERT_EQUAL_INT(2, d->hosts[0].ncharges);
assert_charge(&d->hosts[0].charges[0], "limit=host_slots/u=alice", 4,
"host_slots", "u=alice,h=n01", "slots");
assert_charge(&d->hosts[0].charges[1], "limit=hcap", 4, "hcap", "h=n01",
"slots");
/* n02 has no matched host limits: the charge is unresolved; "gpu" is
* not registered and has no limit prefix: a resource; "limit=ghost..."
* is not registered: a charge by its prefix */
TEST_ASSERT_EQUAL_STRING("n02", d->hosts[1].host);
TEST_ASSERT_EQUAL_INT(2, d->hosts[1].nres);
assert_alloc_row(&d->hosts[1].res[0], "mem", 2);
assert_alloc_row(&d->hosts[1].res[1], "gpu", 1);
TEST_ASSERT_EQUAL_INT(2, d->hosts[1].ncharges);
assert_charge(&d->hosts[1].charges[0], "limit=host_slots/u=alice", 1,
"host_slots", "u=alice,h=n02", NULL);
assert_charge(&d->hosts[1].charges[1], "limit=ghost/u=alice", 1, "ghost",
"u=alice,h=n02", NULL);
/* shared: once for the job, charges resolved through glimits */
TEST_ASSERT_EQUAL_INT(1, d->nshared);
assert_alloc_row(&d->shared[0], "lic", 1);
TEST_ASSERT_EQUAL_INT(2, d->nshared_charges);
assert_charge(&d->shared_charges[0], "limit=lic_cap/u=alice/q=normal", 1,
"lic_cap", "u=alice,q=normal", "lic");
assert_charge(&d->shared_charges[1], "limit=site_total/p=proj1", 1,
"site_total", "p=proj1", NULL);
/* a self-contained copy */
TEST_ASSERT_TRUE(in_arena(rec, d->hosts));
TEST_ASSERT_TRUE(in_arena(rec, d->hosts[0].host));
TEST_ASSERT_TRUE(in_arena(rec, d->hosts[0].res[0].res));
TEST_ASSERT_TRUE(in_arena(rec, d->hosts[0].charges[0].limit));
TEST_ASSERT_TRUE(in_arena(rec, d->hosts[0].charges[0].scope));
TEST_ASSERT_TRUE(in_arena(rec, d->hosts[0].charges[0].limited_res));
TEST_ASSERT_TRUE(in_arena(rec, d->shared_charges[0].limited_res));
t_free_job(job);
rows_fx_free(fx);
TEST_ASSERT_EQUAL_STRING("n01", d->hosts[0].host);
TEST_ASSERT_EQUAL_STRING("u=alice,h=n01", d->hosts[0].charges[0].scope);
TEST_ASSERT_EQUAL_STRING("lic", d->shared_charges[0].limited_res);
TEST_ASSERT_FALSE(rec->incomplete);
sched_trace_rec_free(rec);
}
/* No schedobj (a job this replay never evaluated): every charge is still
* listed as a charge, by its flag, with the name and scope parsed from the
* resource name and the amount left as a raw number (design "TraceCapture",
* the raw-number fallback). */
static void test_dispatch_raw_number_fallback(void)
{
rows_fx_t* fx = rows_fx_new();
xce_sched_trace_ctl_t ctl = make_ctl("fcfs");
xce_sched_trace_rec_t* rec;
xce_job_alloc_t* job = t_job(1);
const t_av_t h1[] = { { "limit=host_slots/u=alice", 3 }, { "mem", 1 } };
const t_av_t sh[] = { { "limit=lic_cap/u=alice/q=normal", 2 } };
const sched_trace_dispatch_out_t* d;
job->allocs[0] = t_host("n01", 2, h1);
job->nres_shared = 1;
job->allocs_shared = t_allocs(1, sh);
rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION);
sched_trace_capture_dispatch(rec, &fx->core, NULL, job);
d = &rec->dispatch;
TEST_ASSERT_EQUAL_INT(1, d->hosts[0].nres);
assert_alloc_row(&d->hosts[0].res[0], "mem", 1);
TEST_ASSERT_EQUAL_INT(1, d->hosts[0].ncharges);
assert_charge(&d->hosts[0].charges[0], "limit=host_slots/u=alice", 3,
"host_slots", "u=alice,h=n01", NULL);
TEST_ASSERT_EQUAL_INT(0, d->nshared);
TEST_ASSERT_EQUAL_INT(1, d->nshared_charges);
assert_charge(&d->shared_charges[0], "limit=lic_cap/u=alice/q=normal", 2,
"lic_cap", "u=alice,q=normal", NULL);
sched_trace_rec_free(rec);
/* no resmgr entry and no prefix either way: no core at all */
rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION);
sched_trace_capture_dispatch(rec, NULL, NULL, job);
d = &rec->dispatch;
TEST_ASSERT_EQUAL_INT(1, d->hosts[0].nres);
TEST_ASSERT_EQUAL_INT(1, d->hosts[0].ncharges);
assert_charge(&d->hosts[0].charges[0], "limit=host_slots/u=alice", 3,
"host_slots", "u=alice,h=n01", NULL);
sched_trace_rec_free(rec);
t_free_job(job);
rows_fx_free(fx);
}
/* A reconstructed dispatch (2.5): a RECONSTRUCTED record with the session
* header, no entries and the dispatch outcome, its charges resolved the same
* way. */
static void test_reconstructed_record_holds_dispatch(void)
{
rows_fx_t* fx = rows_fx_new();
xce_sched_trace_ctl_t ctl = make_ctl("fcfs");
xce_sched_trace_rec_t* rec;
xce_job_alloc_t* job = t_job(1);
const t_av_t h1[] = { { "slots", 2 }, { "limit=host_slots/u=alice", 2 } };
job->allocs[0] = t_host("n01", 2, h1);
TEST_ASSERT_NULL(
sched_trace_rec_reconstructed(NULL, &fx->core, 0x99, &fx->obj, job));
rec = sched_trace_rec_reconstructed(&ctl, &fx->core, 0x99, &fx->obj, job);
TEST_ASSERT_NOT_NULL(rec);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_RECONSTRUCTED, rec->kind);
TEST_ASSERT_EQUAL_UINT64(0x99, rec->jobid);
TEST_ASSERT_EQUAL_UINT64(2, rec->session_no);
TEST_ASSERT_EQUAL_INT(0, rec->nentries);
TEST_ASSERT_EQUAL_INT(SCHED_TRACE_OUTCOME_DISPATCH, rec->outcome);
TEST_ASSERT_EQUAL_INT(1, rec->dispatch.nhosts);
assert_alloc_row(&rec->dispatch.hosts[0].res[0], "slots", 2);
assert_charge(&rec->dispatch.hosts[0].charges[0],
"limit=host_slots/u=alice", 2, "host_slots", "u=alice,h=n01",
"slots");
t_free_job(job);
rows_fx_free(fx);
TEST_ASSERT_EQUAL_STRING("n01", rec->dispatch.hosts[0].host);
sched_trace_rec_free(rec);
/* a job with no schedobj: still built, charges unresolved */
fx = rows_fx_new();
job = t_job(1);
job->allocs[0] = t_host("n01", 2, h1);
rec = sched_trace_rec_reconstructed(&ctl, &fx->core, 0x99, NULL, job);
TEST_ASSERT_NOT_NULL(rec);
TEST_ASSERT_NULL(rec->dispatch.hosts[0].charges[0].limited_res);
sched_trace_rec_free(rec);
t_free_job(job);
rows_fx_free(fx);
}
/* More than 50 hosts: the first 50 in detail, the rest as an omitted-host
* count, per-resource totals without the limits, and per-limit totals with
* the host-free scope (9.4). */
static void test_dispatch_aggregates_hosts_past_50(void)
{
rows_fx_t* fx = rows_fx_new();
xce_sched_trace_ctl_t ctl = make_ctl("fcfs");
xce_sched_trace_rec_t* rec;
int nh;
for (nh = XCE_TRACE_OUTCOME_HOSTS + 1; nh <= XCE_TRACE_OUTCOME_HOSTS + 3;
nh += 2) {
xce_job_alloc_t* job = t_job(nh);
const sched_trace_dispatch_out_t* d;
int i;
for (i = 0; i < nh; i++) {
char name[16];
t_av_t av[3] = { { "mem", 2 + i },
{ "limit=host_slots/u=alice", 1 },
{ "slots", 1 } };
snprintf(name, sizeof(name), "h%02d", i);
job->allocs[i] = t_host(name, 3, av);
}
rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION);
sched_trace_capture_dispatch(rec, &fx->core, &fx->obj, job);
d = &rec->dispatch;
TEST_ASSERT_EQUAL_INT(XCE_TRACE_OUTCOME_HOSTS, d->nhosts);
TEST_ASSERT_EQUAL_STRING("h49", d->hosts[49].host);
TEST_ASSERT_EQUAL_INT(1, d->hosts[49].ncharges);
TEST_ASSERT_EQUAL_INT(nh - XCE_TRACE_OUTCOME_HOSTS, d->omitted_hosts);
TEST_ASSERT_EQUAL_INT(2, d->nagg_res);
if (nh == XCE_TRACE_OUTCOME_HOSTS + 1) {
assert_alloc_row(&d->agg_res[0], "mem", 52); /* h50 only */
} else {
assert_alloc_row(&d->agg_res[0], "mem", 52 + 53 + 54);
}
assert_alloc_row(&d->agg_res[1], "slots", nh - XCE_TRACE_OUTCOME_HOSTS);
TEST_ASSERT_EQUAL_INT(1, d->nagg_charges);
assert_charge(&d->agg_charges[0], NULL, nh - XCE_TRACE_OUTCOME_HOSTS,
"host_slots", "u=alice", NULL);
TEST_ASSERT_TRUE(in_arena(rec, d->agg_res[0].res));
TEST_ASSERT_TRUE(in_arena(rec, d->agg_charges[0].limit));
t_free_job(job);
TEST_ASSERT_EQUAL_STRING("mem", d->agg_res[0].res);
sched_trace_rec_free(rec);
}
/* exactly 50: nothing omitted */
{
xce_job_alloc_t* job = t_job(XCE_TRACE_OUTCOME_HOSTS);
int i;
for (i = 0; i < XCE_TRACE_OUTCOME_HOSTS; i++) {
char name[16];
t_av_t av[1] = { { "mem", 1 } };
snprintf(name, sizeof(name), "h%02d", i);
job->allocs[i] = t_host(name, 1, av);
}
rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION);
sched_trace_capture_dispatch(rec, &fx->core, &fx->obj, job);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_OUTCOME_HOSTS, rec->dispatch.nhosts);
TEST_ASSERT_EQUAL_INT(0, rec->dispatch.omitted_hosts);
TEST_ASSERT_EQUAL_INT(0, rec->dispatch.nagg_res);
sched_trace_rec_free(rec);
t_free_job(job);
}
rows_fx_free(fx);
}
static void test_dispatch_null_alloc_is_empty(void)
{
xce_sched_trace_ctl_t ctl = make_ctl("fcfs");
xce_sched_trace_rec_t* rec = sched_trace_rec_new(&ctl, 0x77,
XCE_TRACE_REC_DECISION);
sched_trace_capture_dispatch(rec, NULL, NULL, NULL);
TEST_ASSERT_EQUAL_INT(SCHED_TRACE_OUTCOME_DISPATCH, rec->outcome);
TEST_ASSERT_EQUAL_INT(0, rec->dispatch.nhosts);
TEST_ASSERT_EQUAL_INT(0, rec->dispatch.nshared);
TEST_ASSERT_FALSE(rec->incomplete);
sched_trace_rec_free(rec);
}
/* 50 or fewer hosts: the pend copy is the decision's set, equal under the
* scheduler's own pend-reason equality, deep-copied into the arena (6.4). */
static void test_pend_copy_equals_source(void)
{
xce_sched_trace_ctl_t ctl = make_ctl("fcfs");
xce_sched_trace_rec_t* rec = sched_trace_rec_new(&ctl, 0x55,
XCE_TRACE_REC_DECISION);
xce_pendreason_t* r = make_pendreason(0x55);
char* why = NULL;
int i;
add_job_preason(r->reasons, PREASON_BAD_RESREQ, NULL, NULL); /* main */
for (i = 0; i < XCE_TRACE_OUTCOME_HOSTS; i++) {
char name[16];
snprintf(name, sizeof(name), "h%02d", i);
add_job_preason(r->reasons, PREASON_MATCH_FAIL, name, NULL);
if ((i % 7) == 0) {
add_job_preason(r->reasons, PREASON_NO_RESOURCE, name, "mem");
}
}
add_job_preason(r->reasons, PREASON_NO_RESOURCE, NULL, "lic"); /* shared */
sched_trace_capture_pend(rec, r);
TEST_ASSERT_EQUAL_INT(SCHED_TRACE_OUTCOME_PEND, rec->outcome);
TEST_ASSERT_EQUAL_INT(0, rec->nentries);
TEST_ASSERT_NOT_NULL(rec->pend.reasons);
TEST_ASSERT_TRUE_MESSAGE(job_preason_eq(rec->pend.reasons, r->reasons,
&why),
(why != NULL) ? why : "");
TEST_ASSERT_EQUAL_INT(0, rec->pend.omitted_hosts);
TEST_ASSERT_EQUAL_INT(0, rec->pend.ncodes);
/* no aliasing: every level is the record's own */
TEST_ASSERT_TRUE(in_arena(rec, rec->pend.reasons));
TEST_ASSERT_TRUE(in_arena(rec, rec->pend.reasons->host_based_reasons));
TEST_ASSERT_TRUE(in_arena(rec, rec->pend.reasons->host_based_reasons[0]));
TEST_ASSERT_TRUE(
in_arena(rec, rec->pend.reasons->host_based_reasons[0]->host));
TEST_ASSERT_TRUE(in_arena(rec, rec->pend.reasons->res_based_reasons[0]));
TEST_ASSERT_TRUE(
in_arena(rec, rec->pend.reasons->res_based_reasons[0]->resource));
free_pendreason(r);
TEST_ASSERT_EQUAL_INT(PREASON_BAD_RESREQ, rec->pend.reasons->main_reason);
TEST_ASSERT_EQUAL_STRING("h49",
rec->pend.reasons->host_based_reasons[49]->host);
sched_trace_rec_free(rec);
/* main reason only, and an empty set: still equal */
r = make_pendreason(0x55);
rec = sched_trace_rec_new(&ctl, 0x55, XCE_TRACE_REC_DECISION);
sched_trace_capture_pend(rec, r);
TEST_ASSERT_TRUE(job_preason_eq(rec->pend.reasons, r->reasons, NULL));
sched_trace_rec_free(rec);
free_pendreason(r);
}
/* More than 50 distinct hosts: the main reason, host-less entries and
* every entry of the first 50 hosts (also later ones) are kept; the other
* hosts' entries are counted per pend-reason code (9.4). */
static void test_pend_aggregates_hosts_past_50(void)
{
xce_sched_trace_ctl_t ctl = make_ctl("fcfs");
xce_sched_trace_rec_t* rec = sched_trace_rec_new(&ctl, 0x55,
XCE_TRACE_REC_DECISION);
xce_pendreason_t* r = make_pendreason(0x55);
const xce_job_preason_t* c;
int i;
add_job_preason(r->reasons, PREASON_BAD_RESREQ, NULL, NULL);
for (i = 0; i < XCE_TRACE_OUTCOME_HOSTS + 1; i++) {
char name[16];
snprintf(name, sizeof(name), "h%02d", i);
add_job_preason(r->reasons, PREASON_MATCH_FAIL, name, NULL);
}
/* resource entries: a kept host, the omitted host, a host-less one */
add_job_preason(r->reasons, PREASON_NO_RESOURCE, "h03", "mem");
add_job_preason(r->reasons, PREASON_NO_RESOURCE, "h50", "mem");
add_job_preason(r->reasons, PREASON_NO_RESOURCE, NULL, "lic");
sched_trace_capture_pend(rec, r);
c = rec->pend.reasons;
TEST_ASSERT_NOT_NULL(c);
TEST_ASSERT_EQUAL_INT(PREASON_BAD_RESREQ, c->main_reason);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_OUTCOME_HOSTS, c->nhost_reasons);
TEST_ASSERT_EQUAL_STRING("h00", c->host_based_reasons[0]->host);
TEST_ASSERT_EQUAL_STRING("h49", c->host_based_reasons[49]->host);
TEST_ASSERT_EQUAL_INT(2, c->nres_reasons);
TEST_ASSERT_EQUAL_STRING("h03", c->res_based_reasons[0]->host);
TEST_ASSERT_EQUAL_STRING("mem", c->res_based_reasons[0]->resource);
TEST_ASSERT_EQUAL_STRING("", c->res_based_reasons[1]->host); /* shared */
TEST_ASSERT_EQUAL_STRING("lic", c->res_based_reasons[1]->resource);
TEST_ASSERT_EQUAL_INT(PREASON_NO_RESOURCE, c->res_based_reasons[1]->reason);
TEST_ASSERT_EQUAL_INT(1, rec->pend.omitted_hosts);
TEST_ASSERT_EQUAL_INT(2, rec->pend.ncodes);
TEST_ASSERT_EQUAL_INT(PREASON_MATCH_FAIL, rec->pend.omitted_codes[0].code);
TEST_ASSERT_EQUAL_INT(1, rec->pend.omitted_codes[0].count);
TEST_ASSERT_EQUAL_INT(PREASON_NO_RESOURCE, rec->pend.omitted_codes[1].code);
TEST_ASSERT_EQUAL_INT(1, rec->pend.omitted_codes[1].count);
TEST_ASSERT_TRUE(in_arena(rec, rec->pend.omitted_codes));
free_pendreason(r);
TEST_ASSERT_EQUAL_STRING("h49", c->host_based_reasons[49]->host);
sched_trace_rec_free(rec);
}
static void test_pend_null_reason_has_no_set(void)
{
xce_sched_trace_ctl_t ctl = make_ctl("fcfs");
xce_sched_trace_rec_t* rec = sched_trace_rec_new(&ctl, 0x55,
XCE_TRACE_REC_DECISION);
xce_pendreason_t empty;
sched_trace_capture_pend(rec, NULL);
TEST_ASSERT_EQUAL_INT(SCHED_TRACE_OUTCOME_PEND, rec->outcome);
TEST_ASSERT_NULL(rec->pend.reasons);
sched_trace_rec_free(rec);
memset(&empty, 0, sizeof(empty));
rec = sched_trace_rec_new(&ctl, 0x55, XCE_TRACE_REC_DECISION);
sched_trace_capture_pend(rec, &empty);
TEST_ASSERT_NULL(rec->pend.reasons);
TEST_ASSERT_FALSE(rec->incomplete);
sched_trace_rec_free(rec);
}
int main(void)
{
xstring_table_init(1024);
@ -1048,5 +1585,13 @@ int main(void)
RUN_TEST(test_check_rows_host_summary_tallies_limit);
RUN_TEST(test_check_rows_scope_edges);
RUN_TEST(test_check_rows_host_rows_count_against_budget);
RUN_TEST(test_dispatch_separates_limit_charges);
RUN_TEST(test_dispatch_raw_number_fallback);
RUN_TEST(test_reconstructed_record_holds_dispatch);
RUN_TEST(test_dispatch_aggregates_hosts_past_50);
RUN_TEST(test_dispatch_null_alloc_is_empty);
RUN_TEST(test_pend_copy_equals_source);
RUN_TEST(test_pend_aggregates_hosts_past_50);
RUN_TEST(test_pend_null_reason_has_no_set);
return UNITY_END();
}