myxce/src/scheduler/testsuite/test_trace_render.c
Rongsong Shen e3b87d28f3
fix(scheduler): place a task-range job with the largest count that fits (bug-1931)
The parjob plan generator split only maxTasks, so a job submitted with a
task range (minTasks < maxTasks) pended until all maxTasks tasks fit,
even when minTasks did (a tasks 2..5 job pended on a host with 2 free
slots).

The runner now handles a task range in sched_alloc_range_(): it probes
minTasks first and pends after that one search when it does not fit;
otherwise it places maxTasks when that fits, or binary-searches for the
largest count that fits and places it. Fitting is monotone in the count
for balanced plans, so the search is exact for today's plan shapes.
ctx->ntasks carries the count; parjob's job-level pass scales per-task
shared wants and global limits to it, and the generator gets the request
with its task count fixed to it. Fixed counts keep the old path.

The decision trace shows each count tried (task count N (minimum) /
(maximum), placing N tasks) and releases a fitting probe's hosts.
Test fixtures that used minTasks = 1 as filler for an N-task job are now
fixed-count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 08:51:44 +00:00

1416 lines
55 KiB
C

/*
* test_trace_render.c - golden tests for the decision trace renderer
* (scheduler-sim-decision-trace spec, task 4.1; design.md "TraceRenderer").
*
* Records are built with the trace model's own entry calls
* (src/sched_trace.c, src/sched_trace_scan.c) and, where TraceCapture would
* fill a block (selection, cluster-wide limits, outcome), by writing the
* record's fields directly into its arena, so no capture source and no
* scheduler state is needed. The renderer (src/repl_trace_render.c) and
* the shared REPL formatting module (src/repl_fmt.c) are linked as they
* are.
*
* Covers:
* - the full-level hierarchy of design.md's example, one line per stored
* entry, indented two spaces per level (3.1, 4.x, 5.x, 7.6);
* - the summary-level position summary with per-limit names (5.9);
* - every units mode for resource rows, limit rows and limit charges,
* limit amounts formatted by the limited resource (7.7);
* - the truncation marker and the fixed-size totals block (9.2, 9.3);
* - the 50-host outcome cap with aggregates and the omitted-host marker,
* and the pend outcome (6.1-6.4, 9.4);
* - each no-decision kind and reconstructed records (2.4, 2.5);
* - the edge cases earlier tasks left for the renderer (tasks.md
* "Implementation Notes" 1.4, 1.5, 2.1, 2.2, 2.3, 3.2, 3.4);
* - one or two sinks, with identical bytes in each.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "preason.h"
#include "repl_internal.h"
#include "sched/alloc_iter.h"
#include "sched/trace.h"
#include "utils.h"
#define JOB_ID MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)42)
#define QUEUE_TIME ((time_t)1791367201) /* 2026-10-07T10:00:01Z */
#define GIB 1073741824.0f
void setUp(void) { return; }
void tearDown(void) { return; }
/* ---- fixtures ---------------------------------------------------------- */
static xce_sched_trace_rec_t* new_rec(xce_sched_trace_level_t level,
int bound, xce_sched_trace_rec_kind_t k)
{
xce_sched_trace_ctl_t ctl;
memset(&ctl, 0, sizeof(ctl));
ctl.all = true;
ctl.level = level;
ctl.line_bound = bound;
ctl.session_no = 7;
ctl.policy_name = "builtin_fcfs";
ctl.position = 3;
return sched_trace_rec_new(&ctl, JOB_ID, k);
}
/* What sched_trace_capture_selection() would store. */
static void set_selection(xce_sched_trace_rec_t* rec)
{
rec->selection_set = true;
rec->queue_name = sched_trace_arena_strdup(rec, "normal");
rec->queue_priority = 50;
rec->job_priority = 0;
rec->queue_time = QUEUE_TIME;
}
/* What sched_trace_capture_glimits() would store: `n` (name, scope). */
static void set_glimits(xce_sched_trace_rec_t* rec, int n,
const char* const* names, const char* const* scopes)
{
int k;
rec->glimits_set = true;
rec->nglimits = n;
if (n == 0) {
return;
}
rec->glimits = (sched_trace_limit_ref_t*)sched_trace_arena_alloc(
rec, sizeof(sched_trace_limit_ref_t) * (size_t)n);
for (k = 0; k < n; k++) {
rec->glimits[k].limit = sched_trace_arena_strdup(rec, names[k]);
rec->glimits[k].scope = sched_trace_arena_strdup(rec, scopes[k]);
}
}
/* A primary with up to three requested resources; a NULL name leaves that
* rusage slot NULL (the model then stores a NULL request name). */
typedef struct {
xce_primary_resreq_t p;
rusage_value_t v[3];
void* ptrs[3];
} fx_primary_t;
static void make_primary(fx_primary_t* f, int min_tasks, int max_tasks,
int n, const char* const* names, const float* vals,
const rusage_per_type_t* per)
{
int k;
memset(f, 0, sizeof(*f));
for (k = 0; k < n; k++) {
if (names[k] == NULL) {
f->ptrs[k] = NULL;
continue;
}
f->v[k].res = (char*)names[k];
f->v[k].value = vals[k];
f->v[k].per_flag = (per != NULL) ? per[k] : PER_TASK;
f->ptrs[k] = &f->v[k];
}
f->p.minTasks = min_tasks;
f->p.maxTasks = max_tasks;
f->p.rusages.count = n;
f->p.rusages.caps = n;
f->p.rusages.delta = 1;
f->p.rusages.data = f->ptrs;
}
static xce_alloc_plan_t make_plan(int* slots, int n)
{
xce_alloc_plan_t plan;
memset(&plan, 0, sizeof(plan));
plan.nbuckets = n;
plan.bucket_slots = slots;
return plan;
}
static xce_sched_worker_t make_worker(char* name, int idx)
{
xce_sched_worker_t w;
memset(&w, 0, sizeof(w));
w.name = name;
w.idx = idx;
return w;
}
static sched_trace_row_t res_row(const char* res, float total, float inuse,
float reserved, float requested, bool met)
{
sched_trace_row_t r;
memset(&r, 0, sizeof(r));
r.res = res;
r.total = total;
r.inuse = inuse;
r.reserved = reserved;
r.requested = requested;
r.met = met;
return r;
}
static sched_trace_row_t limit_row(const char* limit, const char* scope,
const char* limited, float total,
float inuse, float reserved,
float requested, bool met)
{
sched_trace_row_t r
= res_row("limit=x", total, inuse, reserved, requested, met);
r.flags = XCE_RESF_LIMIT;
r.limit = limit;
r.scope = scope;
r.limited_res = limited;
return r;
}
/* An allocated resource row, or (limit != NULL) a limit charge. */
static sched_trace_alloc_row_t arow(const char* res, double v,
const char* limit, const char* scope,
const char* limited)
{
sched_trace_alloc_row_t r;
r.res = res;
r.value = v;
r.limit = limit;
r.scope = scope;
r.limited_res = limited;
return r;
}
/* Copies `n` rows into the record's arena. */
static sched_trace_alloc_row_t* arena_rows(xce_sched_trace_rec_t* rec,
const sched_trace_alloc_row_t* src,
int n)
{
if (n == 0) {
return NULL;
}
return (sched_trace_alloc_row_t*)sched_trace_arena_memdup(
rec, src, sizeof(*src) * (size_t)n);
}
/* A one-host dispatch outcome. */
static void set_dispatch_one(xce_sched_trace_rec_t* rec, const char* host,
const sched_trace_alloc_row_t* res, int nres,
const sched_trace_alloc_row_t* ch, int nch,
const sched_trace_alloc_row_t* shared, int nsh,
const sched_trace_alloc_row_t* shch, int nshch)
{
sched_trace_host_out_t* h;
rec->outcome = SCHED_TRACE_OUTCOME_DISPATCH;
rec->dispatch.nhosts = 1;
rec->dispatch.hosts = (sched_trace_host_out_t*)sched_trace_arena_alloc(
rec, sizeof(sched_trace_host_out_t));
h = &rec->dispatch.hosts[0];
h->host = sched_trace_arena_strdup(rec, host);
h->nres = nres;
h->res = arena_rows(rec, res, nres);
h->ncharges = nch;
h->charges = arena_rows(rec, ch, nch);
rec->dispatch.nshared = nsh;
rec->dispatch.shared = arena_rows(rec, shared, nsh);
rec->dispatch.nshared_charges = nshch;
rec->dispatch.shared_charges = arena_rows(rec, shch, nshch);
}
/* Renders `rec` to two memory sinks, asserts both hold identical bytes and
* returns one of them (caller frees). */
static char* render(const xce_sched_trace_rec_t* rec,
xce_repl_units_mode_t units)
{
char* b1 = NULL;
char* b2 = NULL;
size_t n1 = 0;
size_t n2 = 0;
FILE* f1 = open_memstream(&b1, &n1);
FILE* f2 = open_memstream(&b2, &n2);
TEST_ASSERT_NOT_NULL(f1);
TEST_ASSERT_NOT_NULL(f2);
repl_trace_render(rec, units, f1, f2);
fclose(f1);
fclose(f2);
TEST_ASSERT_EQUAL_size_t(n1, n2);
TEST_ASSERT_EQUAL_MEMORY(b1, b2, n1);
free(b2);
return b1;
}
/* Compares a rendering with its golden text, printing both on mismatch. */
static void assert_golden(const char* want, const char* got)
{
if (strcmp(want, got) != 0) {
fprintf(stderr, "---- want ----\n%s---- got ----\n%s----\n", want,
got);
}
TEST_ASSERT_EQUAL_STRING(want, got);
}
static int count_lines(const char* s)
{
int n = 0;
for (; *s != '\0'; s++) {
if (*s == '\n') {
n++;
}
}
return n;
}
#define SEL_LINE \
" selection: policy builtin_fcfs queue normal (priority 50) job " \
"priority 0 queued 2026-10-07T10:00:01Z\n"
#define NO_LIMITS_LINE \
" cluster-wide limits: none; host-scoped limits are reported with " \
"each host's verdict\n"
/* ---- full level: the design's example ---------------------------------- */
static xce_sched_trace_rec_t* build_design_example(void)
{
static const char* const lnames[] = { "lim_user" };
static const char* const lscopes[] = { "u=alice" };
static const char* const rn[] = { "mem", "slots", "license" };
static const float v1[] = { 8.0f * GIB, 1.0f, 2.0f };
static const float v2[] = { 8.0f * GIB, 1.0f, 1.0f };
static int slots[] = { 4 };
xce_sched_trace_rec_t* rec;
fx_primary_t p1;
fx_primary_t p2;
xce_alloc_plan_t plan = make_plan(slots, 1);
xce_sched_worker_t n01 = make_worker("n01", 0);
xce_sched_worker_t n02 = make_worker("n02", 1);
xce_sched_worker_t n03 = make_worker("n03", 2);
sched_trace_row_t row;
sched_trace_reason_count_t codes[] = { { PREASON_NO_RESOURCE, 1 } };
const char* const rres[] = { "license" };
sched_trace_alloc_row_t hres[2];
sched_trace_alloc_row_t hch[1];
sched_trace_alloc_row_t sh[1];
sched_trace_alloc_row_t shch[1];
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 1, lnames, lscopes);
make_primary(&p1, 4, 4, 3, rn, v1, NULL);
make_primary(&p2, 4, 4, 3, rn, v2, NULL);
/* alternative 1/2: the cluster-wide check fails on the license */
sched_trace_path_push(rec, XCE_TRACE_PATH_ALT, 1, 2);
sched_trace_resreq_begin(rec, NULL, 1);
sched_trace_path_push(rec, XCE_TRACE_PATH_MEMBER, 1, 1);
sched_trace_resreq_begin(rec, &p1.p, 0);
sched_trace_check_begin(rec, NULL);
row = res_row("license", 10, 9, 0, 2, false);
sched_trace_row(rec, &row);
sched_trace_check_end(rec, 0, false);
sched_trace_resreq_end(rec, 0, 0);
sched_trace_path_pop(rec);
sched_trace_attempt_reasons(rec, codes, 1, NULL, 0, rres, 1);
sched_trace_resreq_end(rec, 0, 0);
sched_trace_path_pop(rec);
/* alternative 2/2: placed on n03 */
sched_trace_path_push(rec, XCE_TRACE_PATH_ALT, 2, 2);
sched_trace_resreq_begin(rec, NULL, 1);
sched_trace_path_push(rec, XCE_TRACE_PATH_MEMBER, 1, 1);
sched_trace_resreq_begin(rec, &p2.p, 0);
sched_trace_check_begin(rec, NULL);
row = res_row("license", 10, 9, 0, 1, true);
sched_trace_row(rec, &row);
row = limit_row("lim_user", "u=alice", "slots", 100, 96, 0, 4, true);
sched_trace_row(rec, &row);
sched_trace_check_end(rec, 1, true);
sched_trace_plan_begin(rec, &plan);
sched_trace_pos_begin(rec, 1, 4);
sched_trace_host_begin(rec, NULL, &n01);
row = res_row("mem", 64.0f * GIB, 60.0f * GIB, 0, 8.0f * GIB, false);
sched_trace_row(rec, &row);
rec->host_row_verdicts |= XCE_TRACE_V_SHORTFALL; /* TraceCapture's */
sched_trace_host_verdict(rec, 0, 0);
sched_trace_host_begin(rec, NULL, &n02);
sched_trace_host_verdict(rec, XCE_TRACE_V_SELECT, 0);
sched_trace_host_begin(rec, NULL, &n03);
sched_trace_host_verdict(rec, XCE_TRACE_V_ACCEPTED, 0);
sched_trace_pos_end(rec, XCE_TRACE_POS_FILLED, "n03");
sched_trace_plan_end(rec, 1, 0);
sched_trace_commit(rec);
sched_trace_search_end(rec, 1);
sched_trace_resreq_end(rec, 1, 0);
sched_trace_path_pop(rec);
sched_trace_resreq_end(rec, 1, 0);
sched_trace_path_pop(rec);
sched_trace_alt_result(rec, 2, 2);
hres[0] = arow("mem", 8.0 * GIB, NULL, NULL, NULL);
hres[1] = arow("slots", 4, NULL, NULL, NULL);
hch[0] = arow("limit=lim_host", 4, "lim_host", "h=n03", "slots");
sh[0] = arow("license", 1, NULL, NULL, NULL);
shch[0] = arow("limit=lim_user", 4, "lim_user", "u=alice", "slots");
set_dispatch_one(rec, "n03", hres, 2, hch, 1, sh, 1, shch, 1);
return rec;
}
static const char* const DESIGN_EXAMPLE_HUMAN
= "[trace] job 1-2-1000-42 session 7 position 3 level full\n" SEL_LINE
" cluster-wide limits: lim_user (u=alice); host-scoped limits are "
"reported with each host's verdict\n"
" requirement alternative 1/2 (compound, 1 member)\n"
" requirement alternative 1/2 > member 1/1 tasks 4..4 requests "
"mem=8.00 GB slots=1 license=2\n"
" cluster-wide: license total 10 in-use 9 reserved 0 requested 2"
" failed\n"
" cluster-wide check failed; no placement plans tried\n"
" member 1/1 not satisfied\n"
" failure reasons: NO_RESOURCE x1 (resources: license)\n"
" alternative 1/2 not satisfied\n"
" requirement alternative 2/2 (compound, 1 member)\n"
" requirement alternative 2/2 > member 1/1 tasks 4..4 requests "
"mem=8.00 GB slots=1 license=1\n"
" cluster-wide: license total 10 in-use 9 reserved 0 requested 1"
" passed\n"
" cluster-wide limit lim_user (u=alice) on slots: value 100 usage "
"96 reserved 0 requested 4 passed\n"
" cluster-wide check passed\n"
" plan 1 hosts 1 tasks per position [4]\n"
" position 1 (4 tasks)\n"
" host n01 rejected (per-host check): resource shortfall\n"
" mem total 64.00 GB in-use 60.00 GB reserved 0.00 B "
"requested 8.00 GB\n"
" host n02 rejected (pre-filter): selection mismatch; "
"per-host check not performed\n"
" host n03 accepted\n"
" position 1 filled by n03\n"
" plan 1 succeeded\n"
" plan 1 committed; no further plans tried\n"
" plans: 1 generated, 1 tried, 0 discarded, 0 not tried\n"
" member 1/1 satisfied\n"
" alternative 2/2 satisfied\n"
" alternative 2/2 chosen; no alternatives after it\n"
" outcome: DISPATCH\n"
" host n03: mem=8.00 GB slots=4 limit charges: lim_host (h=n03) "
"slots 4\n"
" shared: license=1 limit charges: lim_user (u=alice) slots 4\n";
static void test_full_level_design_example(void)
{
xce_sched_trace_rec_t* rec = build_design_example();
char* out = render(rec, XCE_REPL_UNITS_HUMAN);
assert_golden(DESIGN_EXAMPLE_HUMAN, out);
free(out);
sched_trace_rec_free(rec);
}
/* Every stored entry renders as exactly one line (design "Line contract"):
* header, selection and limits (3) + entries + outcome (3). */
static void test_one_line_per_stored_entry(void)
{
xce_sched_trace_rec_t* rec = build_design_example();
char* out = render(rec, XCE_REPL_UNITS_RAW);
TEST_ASSERT_EQUAL_INT(3 + rec->nentries + 3, count_lines(out));
free(out);
sched_trace_rec_free(rec);
}
/* ---- summary level ------------------------------------------------------ */
static void test_summary_level_position_summary(void)
{
static const char* const rn[] = { "slots" };
static const float rv[] = { 1.0f };
static int slots[] = { 2 };
xce_sched_trace_rec_t* rec;
fx_primary_t p;
xce_alloc_plan_t plan = make_plan(slots, 1);
xce_sched_worker_t n01 = make_worker("n01", 0);
xce_sched_worker_t n02 = make_worker("n02", 1);
xce_sched_worker_t n03 = make_worker("n03", 2);
sched_trace_alloc_row_t hres[1];
char* out;
rec = new_rec(XCE_TRACE_LEVEL_SUMMARY, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
make_primary(&p, 2, 2, 1, rn, rv, NULL);
sched_trace_path_push(rec, XCE_TRACE_PATH_SINGLE, 1, 1);
sched_trace_resreq_begin(rec, &p.p, 0);
sched_trace_check_begin(rec, NULL);
sched_trace_check_end(rec, 1, true);
sched_trace_plan_begin(rec, &plan);
sched_trace_pos_begin(rec, 1, 2);
/* n01: short of a resource and over a host-scoped limit */
sched_trace_host_begin(rec, NULL, &n01);
sched_trace_host_limit_hit(rec, "lim_host", "h=n01");
rec->host_row_verdicts |= XCE_TRACE_V_SHORTFALL;
sched_trace_host_verdict(rec, 0, 0);
sched_trace_host_begin(rec, NULL, &n02);
sched_trace_host_verdict(rec, XCE_TRACE_V_SELECT, 0);
sched_trace_host_begin(rec, NULL, &n03);
sched_trace_host_verdict(rec, XCE_TRACE_V_ACCEPTED, 0);
sched_trace_pos_end(rec, XCE_TRACE_POS_FILLED, "n03");
sched_trace_plan_end(rec, 1, 0);
sched_trace_commit(rec);
sched_trace_search_end(rec, 1);
sched_trace_resreq_end(rec, 1, 0);
sched_trace_path_pop(rec);
hres[0] = arow("slots", 2, NULL, NULL, NULL);
set_dispatch_one(rec, "n03", hres, 1, NULL, 0, NULL, 0, NULL, 0);
out = render(rec, XCE_REPL_UNITS_BOTH);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level summary\n"
SEL_LINE NO_LIMITS_LINE
" requirement single tasks 2..2 requests slots=1\n"
" cluster-wide check passed\n"
" plan 1 hosts 1 tasks per position [2]\n"
" position 1 (2 tasks)\n"
" accepted n03; rejected: selection mismatch 1, resource "
"shortfall 1, limit exceeded 1 (lim_host (h=n01) 1), already "
"claimed 0, not evaluated (internal error) 0\n"
" plan 1 succeeded\n"
" plan 1 committed; no further plans tried\n"
" plans: 1 generated, 1 tried, 0 discarded, 0 not tried\n"
" single requirement satisfied\n"
" outcome: DISPATCH\n"
" host n03: slots=2\n"
" shared: none\n",
out);
free(out);
sched_trace_rec_free(rec);
}
/* The position summary keeps the end state: a scan error (with and without
* a code) and an internal error after a host was accepted (note 1.5). */
static void test_summary_scan_error_and_internal(void)
{
xce_sched_trace_rec_t* rec;
xce_sched_worker_t n01 = make_worker("n01", 0);
xce_sched_worker_t n02 = make_worker("n02", 1);
char* out;
rec = new_rec(XCE_TRACE_LEVEL_SUMMARY, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
sched_trace_pos_begin(rec, 1, 1);
sched_trace_host_begin(rec, NULL, &n01);
sched_trace_host_verdict(rec, XCE_TRACE_V_SELECT, 0);
sched_trace_pos_scan_error(rec, -5);
sched_trace_pos_end(rec, XCE_TRACE_POS_UNFILLED, NULL);
sched_trace_pos_begin(rec, 2, 1);
sched_trace_pos_end(rec, XCE_TRACE_POS_SCAN_ERROR, NULL);
sched_trace_pos_begin(rec, 3, 1);
sched_trace_host_begin(rec, NULL, &n02);
sched_trace_host_verdict(rec, XCE_TRACE_V_ACCEPTED, 0);
sched_trace_pos_end(rec, XCE_TRACE_POS_INTERNAL, "n02");
sched_trace_pos_begin(rec, 4, 1);
sched_trace_pos_end(rec, XCE_TRACE_POS_UNFILLED, NULL);
rec->outcome = SCHED_TRACE_OUTCOME_PEND;
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level summary\n"
SEL_LINE NO_LIMITS_LINE
" position 1 (1 task)\n"
" accepted none; position not filled: internal error in the host "
"scan (err -5: Input/output error); rejected: selection mismatch 1, "
"resource shortfall 0, limit exceeded 0, already claimed 0, not "
"evaluated (internal error) 0\n"
" position 2 (1 task)\n"
" accepted none; position not filled: internal error in the host "
"scan; rejected: selection mismatch 0, resource shortfall 0, limit "
"exceeded 0, already claimed 0, not evaluated (internal error) 0\n"
" position 3 (1 task)\n"
" accepted n02; position not filled: internal error after accepting "
"it; rejected: selection mismatch 0, resource shortfall 0, limit "
"exceeded 0, already claimed 0, not evaluated (internal error) 0\n"
" position 4 (1 task)\n"
" accepted none; rejected: selection mismatch 0, resource shortfall "
"0, limit exceeded 0, already claimed 0, not evaluated (internal "
"error) 0\n"
" outcome: PEND\n"
" pend reasons: none\n",
out);
free(out);
sched_trace_rec_free(rec);
}
/* ---- units modes (7.7) -------------------------------------------------- */
static xce_sched_trace_rec_t* build_units_record(void)
{
xce_sched_trace_rec_t* rec;
sched_trace_row_t row;
sched_trace_alloc_row_t hres[1];
sched_trace_alloc_row_t hch[2];
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
sched_trace_check_begin(rec, NULL);
row = res_row("mem", 64.0f * GIB, 32.0f * GIB, 0, 8.0f * GIB, true);
sched_trace_row(rec, &row);
/* a memory limit: amounts formatted by the limited resource */
row = limit_row("lim_mem", "u=alice", "mem", 64.0f * GIB, 32.0f * GIB, 0,
8.0f * GIB, true);
sched_trace_row(rec, &row);
/* the limited resource unknown: raw amounts (note 2.2) */
row = limit_row("lim_x", "u=bob", NULL, 100, 5, 0, 1, true);
sched_trace_row(rec, &row);
sched_trace_check_end(rec, 1, true);
hres[0] = arow("mem", 8.0 * GIB, NULL, NULL, NULL);
hch[0] = arow("limit=lim_mem", 8.0 * GIB, "lim_mem", "h=n01", "mem");
hch[1] = arow("limit=lim_y", 3, "lim_y", "h=n01", NULL);
set_dispatch_one(rec, "n01", hres, 1, hch, 2, NULL, 0, NULL, 0);
return rec;
}
static void test_units_modes(void)
{
static const struct {
xce_repl_units_mode_t mode;
const char* total;
const char* inuse;
const char* zero;
const char* req;
} cases[] = {
{ XCE_REPL_UNITS_RAW, "68719476736.000000", "34359738368.000000",
"0.000000", "8589934592.000000" },
{ XCE_REPL_UNITS_HUMAN, "64.00 GB", "32.00 GB", "0.00 B", "8.00 GB" },
{ XCE_REPL_UNITS_BOTH, "68719476736.000000 (64.00 GB)",
"34359738368.000000 (32.00 GB)", "0.000000 (0.00 B)",
"8589934592.000000 (8.00 GB)" },
};
size_t k;
for (k = 0; k < sizeof(cases) / sizeof(cases[0]); k++) {
xce_sched_trace_rec_t* rec = build_units_record();
char* out = render(rec, cases[k].mode);
char want[4096];
snprintf(want, sizeof(want),
"[trace] job 1-2-1000-42 session 7 position 3 level "
"full\n" SEL_LINE NO_LIMITS_LINE
" cluster-wide: mem total %s in-use %s reserved %s "
"requested %s passed\n"
" cluster-wide limit lim_mem (u=alice) on mem: value %s "
"usage %s reserved %s requested %s passed\n"
" cluster-wide limit lim_x (u=bob): value 100 usage 5 "
"reserved 0 requested 1 passed\n"
" cluster-wide check passed\n"
" outcome: DISPATCH\n"
" host n01: mem=%s limit charges: lim_mem (h=n01) mem "
"%s, lim_y (h=n01) 3\n"
" shared: none\n",
cases[k].total, cases[k].inuse, cases[k].zero, cases[k].req,
cases[k].total, cases[k].inuse, cases[k].zero, cases[k].req,
cases[k].req, cases[k].req);
assert_golden(want, out);
free(out);
sched_trace_rec_free(rec);
}
}
/* ---- truncation (9.2, 9.3) --------------------------------------------- */
static void test_truncation_marker_and_totals(void)
{
static const char* const rn[] = { "slots" };
static const float rv[] = { 1.0f };
static int slots[] = { 1 };
xce_sched_trace_rec_t* rec;
fx_primary_t p;
xce_alloc_plan_t plan = make_plan(slots, 1);
xce_sched_worker_t w[60];
char names[60][8];
char* out;
const char* tail;
int k;
rec = new_rec(XCE_TRACE_LEVEL_FULL, 50, XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
make_primary(&p, 1, 1, 1, rn, rv, NULL);
sched_trace_path_push(rec, XCE_TRACE_PATH_SINGLE, 1, 1);
sched_trace_resreq_begin(rec, &p.p, 0);
sched_trace_plan_begin(rec, &plan);
sched_trace_pos_begin(rec, 1, 1);
for (k = 0; k < 60; k++) {
snprintf(names[k], sizeof(names[k]), "n%02d", k + 1);
w[k] = make_worker(names[k], k);
sched_trace_host_begin(rec, NULL, &w[k]);
sched_trace_host_verdict(rec, XCE_TRACE_V_SELECT, 0);
}
sched_trace_pos_end(rec, XCE_TRACE_POS_UNFILLED, NULL);
sched_trace_plan_end(rec, 0, 1);
sched_trace_search_end(rec, 0);
sched_trace_resreq_end(rec, 0, 0);
sched_trace_path_pop(rec);
rec->outcome = SCHED_TRACE_OUTCOME_PEND;
/* 3 stored lines before the hosts, 47 hosts, then 13 hosts and the 4
* ends omitted */
TEST_ASSERT_EQUAL_INT(50, rec->nentries);
TEST_ASSERT_EQUAL_INT(17, rec->counters.omitted_lines);
out = render(rec, XCE_REPL_UNITS_RAW);
TEST_ASSERT_EQUAL_INT(3 + 50 + 4 + 2, count_lines(out));
tail = strstr(out, " host n47 rejected (pre-filter)");
TEST_ASSERT_NOT_NULL(tail);
assert_golden(
" host n47 rejected (pre-filter): selection mismatch; per-host "
"check not performed\n"
" ... 17 trace lines omitted (line bound 50)\n"
" totals: plans tried 1, plans discarded 0\n"
" totals: requirements with no plan generated 0, all plans "
"discarded 0, no plan placed 1\n"
" totals: host evaluations: accepted 0, selection mismatch 60, "
"resource shortfall 0, limit exceeded 0, already claimed 0, not "
"evaluated (internal error) 0\n"
" outcome: PEND\n"
" pend reasons: none\n",
tail);
free(out);
sched_trace_rec_free(rec);
}
/* ---- outcome (6.x, 9.4) ------------------------------------------------- */
static void test_dispatch_outcome_cap(void)
{
xce_sched_trace_rec_t* rec;
sched_trace_alloc_row_t res;
sched_trace_alloc_row_t ch;
sched_trace_alloc_row_t agg[1];
sched_trace_alloc_row_t aggch[2];
char name[16];
char scope[16];
char* out;
const char* p;
int k;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
rec->outcome = SCHED_TRACE_OUTCOME_DISPATCH;
rec->dispatch.nhosts = XCE_TRACE_OUTCOME_HOSTS;
rec->dispatch.hosts = (sched_trace_host_out_t*)sched_trace_arena_alloc(
rec, sizeof(sched_trace_host_out_t) * XCE_TRACE_OUTCOME_HOSTS);
for (k = 0; k < XCE_TRACE_OUTCOME_HOSTS; k++) {
sched_trace_host_out_t* h = &rec->dispatch.hosts[k];
snprintf(name, sizeof(name), "n%03d", k + 1);
snprintf(scope, sizeof(scope), "h=n%03d", k + 1);
res = arow("slots", 1, NULL, NULL, NULL);
ch = arow("limit=lim_host", 1, "lim_host",
sched_trace_arena_strdup(rec, scope), "slots");
h->host = sched_trace_arena_strdup(rec, name);
h->nres = 1;
h->res = arena_rows(rec, &res, 1);
h->ncharges = 1;
h->charges = arena_rows(rec, &ch, 1);
}
rec->dispatch.omitted_hosts = 3;
agg[0] = arow("slots", 3, NULL, NULL, NULL);
/* limit totals over omitted hosts: scope without the host, and a NULL
* host name's "h=" (note 2.3) */
aggch[0] = arow(NULL, 3, "lim_host", "", "slots");
aggch[1] = arow(NULL, 2, "lim_z", "h=", NULL);
rec->dispatch.nagg_res = 1;
rec->dispatch.agg_res = arena_rows(rec, agg, 1);
rec->dispatch.nagg_charges = 2;
rec->dispatch.agg_charges = arena_rows(rec, aggch, 2);
out = render(rec, XCE_REPL_UNITS_RAW);
/* header block 3 + outcome line + 50 hosts + shared + aggregate +
* marker */
TEST_ASSERT_EQUAL_INT(3 + 1 + 50 + 3, count_lines(out));
TEST_ASSERT_NOT_NULL(strstr(out, " outcome: DISPATCH\n host n001: "
"slots=1 limit charges: lim_host "
"(h=n001) slots 1\n"));
p = strstr(out, " host n050: ");
TEST_ASSERT_NOT_NULL(p);
assert_golden(" host n050: slots=1 limit charges: lim_host (h=n050) "
"slots 1\n"
" shared: none\n"
" omitted hosts total: slots=3 limit charges: lim_host "
"slots 3, lim_z (h=) 2\n"
" ... 3 hosts omitted\n",
p);
free(out);
sched_trace_rec_free(rec);
}
static void test_pend_outcome(void)
{
xce_sched_trace_rec_t* rec;
xce_host_reason_t hr[2] = { { "n01", PREASON_MATCH_FAIL },
{ "n02", PREASON_MATCH_FAIL } };
xce_host_reason_t* hrp[2] = { &hr[0], &hr[1] };
xce_res_reason_t rr[2]
= { { "license", "", PREASON_NO_RESOURCE },
{ "mem", "n01", PREASON_NO_RESOURCE } };
xce_res_reason_t* rrp[2] = { &rr[0], &rr[1] };
xce_job_preason_t jr;
sched_trace_reason_count_t oc[2] = { { PREASON_MATCH_FAIL, 2 },
{ 9, 1 } };
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
memset(&jr, 0, sizeof(jr));
jr.main_reason = PREASON_NO_RESOURCE;
jr.nhost_reasons = 2;
jr.host_based_reasons = hrp;
jr.nres_reasons = 2;
jr.res_based_reasons = rrp;
rec->outcome = SCHED_TRACE_OUTCOME_PEND;
rec->pend.reasons = &jr; /* read only by the renderer */
rec->pend.omitted_hosts = 2;
rec->pend.ncodes = 2;
rec->pend.omitted_codes = oc;
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level full\n"
SEL_LINE NO_LIMITS_LINE
" outcome: PEND\n"
" main reason: NO_RESOURCE\n"
" resource license: NO_RESOURCE\n"
" host n01: MATCH_FAIL, resource mem: NO_RESOURCE\n"
" host n02: MATCH_FAIL\n"
" omitted hosts' pend reasons: MATCH_FAIL x2, code 9 x1\n"
" ... 2 hosts omitted\n",
out);
free(out);
rec->pend.reasons = NULL;
rec->pend.omitted_codes = NULL;
sched_trace_rec_free(rec);
}
/* NULL entries in the pend-reason lists are kept by the outcome copy
* (sched_trace_outcome.c); the renderer skips them everywhere, including
* when it looks back for hosts already printed. */
static void test_pend_outcome_null_entries(void)
{
xce_sched_trace_rec_t* rec;
xce_host_reason_t hr = { "n01", PREASON_MATCH_FAIL };
xce_host_reason_t* hrp[2] = { NULL, &hr };
xce_res_reason_t rr = { "mem", "n01", PREASON_NO_RESOURCE };
xce_res_reason_t* rrp[2] = { NULL, &rr };
xce_job_preason_t jr;
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
memset(&jr, 0, sizeof(jr));
jr.main_reason = PREASON_NO_RESOURCE;
jr.nhost_reasons = 2;
jr.host_based_reasons = hrp;
jr.nres_reasons = 2;
jr.res_based_reasons = rrp;
rec->outcome = SCHED_TRACE_OUTCOME_PEND;
rec->pend.reasons = &jr; /* read only by the renderer */
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level full\n"
SEL_LINE NO_LIMITS_LINE
" outcome: PEND\n"
" main reason: NO_RESOURCE\n"
" host n01: MATCH_FAIL, resource mem: NO_RESOURCE\n",
out);
free(out);
rec->pend.reasons = NULL;
sched_trace_rec_free(rec);
}
/* A pend whose set carries no main reason (0, preason.c's "none set"), as
* every pend the allocator builds from per-host and per-resource reasons
* does: the line says none instead of a bare code. */
static void test_pend_outcome_without_main_reason(void)
{
xce_sched_trace_rec_t* rec;
xce_res_reason_t rr = { "license", "", PREASON_NO_RESOURCE };
xce_res_reason_t* rrp[1] = { &rr };
xce_job_preason_t jr;
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
memset(&jr, 0, sizeof(jr));
jr.nres_reasons = 1;
jr.res_based_reasons = rrp;
rec->outcome = SCHED_TRACE_OUTCOME_PEND;
rec->pend.reasons = &jr; /* read only by the renderer */
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level full\n"
SEL_LINE NO_LIMITS_LINE
" outcome: PEND\n"
" main reason: none\n"
" resource license: NO_RESOURCE\n",
out);
free(out);
rec->pend.reasons = NULL;
sched_trace_rec_free(rec);
}
/* ---- no-decision and reconstructed records (2.4, 2.5) ------------------- */
static void test_no_decision_not_found(void)
{
xce_sched_trace_rec_t* rec;
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
sched_trace_rec_finish(rec, XCE_TRACE_REC_NOT_FOUND, 0);
out = render(rec, XCE_REPL_UNITS_BOTH);
assert_golden("[trace] job 1-2-1000-42 session 7 position 3 level "
"full\n"
" selection: ordering keys and limits unavailable (job "
"not found by the allocator)\n"
" no decision: job not found by the allocator\n",
out);
free(out);
sched_trace_rec_free(rec);
}
static void test_no_decision_no_schedobj(void)
{
xce_sched_trace_rec_t* rec;
char* out;
rec = new_rec(XCE_TRACE_LEVEL_SUMMARY, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
sched_trace_rec_finish(rec, XCE_TRACE_REC_NO_SCHEDOBJ, 0);
out = render(rec, XCE_REPL_UNITS_BOTH);
assert_golden("[trace] job 1-2-1000-42 session 7 position 3 level "
"summary\n" SEL_LINE
" cluster-wide limits: unavailable (no scheduling "
"object)\n"
" no decision: job has no scheduling object (its resource "
"requirement could not be parsed)\n",
out);
free(out);
sched_trace_rec_free(rec);
}
static void test_no_decision_internal_error_keeps_hierarchy(void)
{
static const char* const rn[] = { "slots" };
static const float rv[] = { 1.0f };
xce_sched_trace_rec_t* rec;
fx_primary_t p;
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
make_primary(&p, 1, 1, 1, rn, rv, NULL);
sched_trace_path_push(rec, XCE_TRACE_PATH_SINGLE, 1, 1);
sched_trace_resreq_begin(rec, &p.p, 0);
sched_trace_check_begin(rec, NULL);
sched_trace_check_end(rec, -5, false);
sched_trace_resreq_end(rec, -5, 0);
sched_trace_path_pop(rec);
sched_trace_rec_finish(rec, XCE_TRACE_REC_INTERNAL_ERROR, -5);
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level full\n"
SEL_LINE NO_LIMITS_LINE
" requirement single tasks 1..1 requests slots=1\n"
" cluster-wide check ended in an internal error (err -5: "
"Input/output error); no placement plans tried\n"
" single requirement ended in an internal error (err -5: "
"Input/output error)\n"
" no decision: allocation attempt ended in an internal error (err "
"-5: Input/output error)\n",
out);
free(out);
sched_trace_rec_free(rec);
}
static void test_no_decision_pend_without_reason_keeps_hierarchy(void)
{
static const char* const rn[] = { "slots" };
static const float rv[] = { 1.0f };
xce_sched_trace_rec_t* rec;
fx_primary_t p;
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
make_primary(&p, 1, 1, 1, rn, rv, NULL);
sched_trace_path_push(rec, XCE_TRACE_PATH_SINGLE, 1, 1);
sched_trace_resreq_begin(rec, &p.p, 0);
sched_trace_no_plan(rec, XCE_TRACE_NOPLAN_DECLINED);
sched_trace_resreq_end(rec, 0, 0);
sched_trace_path_pop(rec);
sched_trace_rec_finish(rec, XCE_TRACE_REC_PEND_NO_REASON, 0);
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level full\n"
SEL_LINE NO_LIMITS_LINE
" requirement single tasks 1..1 requests slots=1\n"
" no plan generated: allocator declined the requirement\n"
" single requirement not satisfied\n"
" no decision: job pended without any pend reason\n",
out);
free(out);
sched_trace_rec_free(rec);
}
/* bug-1932: a request the unsatisfiable-request cache already holds is
* skipped; the line names the job whose search proved it, or says so
* without a name when that job is unknown (0). */
static void test_known_unsatisfiable_names_source_job(void)
{
static const char* const rn[] = { "slots" };
static const float rv[] = { 1.0f };
xce_sched_trace_rec_t* rec;
fx_primary_t p;
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
make_primary(&p, 1, 1, 1, rn, rv, NULL);
sched_trace_path_push(rec, XCE_TRACE_PATH_SINGLE, 1, 1);
sched_trace_resreq_begin(rec, &p.p, 0);
/* the hit returns before the plan search: no search end follows */
sched_trace_known_unsat(rec, (uint64_t)204);
sched_trace_known_unsat(rec, 0);
sched_trace_resreq_end(rec, 0, 0);
sched_trace_path_pop(rec);
sched_trace_rec_finish(rec, XCE_TRACE_REC_PEND_NO_REASON, 0);
TEST_ASSERT_EQUAL_INT(1, rec->counters.reqs_none_generated);
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level full\n"
SEL_LINE NO_LIMITS_LINE
" requirement single tasks 1..1 requests slots=1\n"
" no plan generated: skipped, same request as 0-0-0-204 proved "
"unsatisfiable earlier this session\n"
" no plan generated: skipped, same request proved unsatisfiable "
"earlier this session\n"
" single requirement not satisfied\n"
" no decision: job pended without any pend reason\n",
out);
free(out);
sched_trace_rec_free(rec);
}
/* bug-1931: a task range names each count it tries -- the minimum, the
* maximum, a count between them, and the count it places. */
static void test_task_range_counts(void)
{
static const char* const rn[] = { "slots" };
static const float rv[] = { 1.0f };
xce_sched_trace_rec_t* rec;
fx_primary_t p;
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
make_primary(&p, 2, 9, 1, rn, rv, NULL);
sched_trace_path_push(rec, XCE_TRACE_PATH_SINGLE, 1, 1);
sched_trace_resreq_begin(rec, &p.p, 0);
sched_trace_task_count(rec, 2, 1);
sched_trace_task_count(rec, 9, 2);
sched_trace_task_count(rec, 5, 0);
sched_trace_task_count(rec, 5, 3);
sched_trace_resreq_end(rec, 0, 0);
sched_trace_path_pop(rec);
sched_trace_rec_finish(rec, XCE_TRACE_REC_PEND_NO_REASON, 0);
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level full\n"
SEL_LINE NO_LIMITS_LINE
" requirement single tasks 2..9 requests slots=1\n"
" task count 2 (minimum)\n"
" task count 9 (maximum)\n"
" task count 5\n"
" placing 5 tasks\n"
" single requirement not satisfied\n"
" no decision: job pended without any pend reason\n",
out);
free(out);
sched_trace_rec_free(rec);
}
static void test_reconstructed_record(void)
{
xce_sched_trace_rec_t* rec;
sched_trace_alloc_row_t hres[1];
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_RECONSTRUCTED);
TEST_ASSERT_NOT_NULL(rec);
hres[0] = arow("slots", 2, NULL, NULL, NULL);
set_dispatch_one(rec, "n01", hres, 1, NULL, 0, NULL, 0, NULL, 0);
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden("[trace] job 1-2-1000-42 session 7 position 3 level "
"full\n"
" dispatch reconstructed from the recorded allocation; "
"selection, requirement, plan and host sections do not "
"apply\n"
" outcome: DISPATCH\n"
" host n01: slots=2\n"
" shared: none\n",
out);
free(out);
sched_trace_rec_free(rec);
}
/* A decision record whose selection or limits were never captured. */
static void test_selection_and_limits_unavailable(void)
{
xce_sched_trace_rec_t* rec;
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden("[trace] job 1-2-1000-42 session 7 position 3 level "
"full\n"
" selection: policy builtin_fcfs; ordering keys "
"unavailable\n"
" cluster-wide limits: unavailable\n"
" outcome: none recorded\n",
out);
free(out);
sched_trace_rec_free(rec);
}
/* ---- the edge cases left for the renderer ------------------------------- */
static void test_entry_kinds_and_edge_cases(void)
{
static const char* const rn[] = { NULL, "slots" };
static const float rv[] = { 0.0f, 1.0f };
static const rusage_per_type_t per[] = { PER_TASK, PER_HOST };
static const char* const rn1[] = { "slots", "lic" };
static const float rv1[] = { 1.0f, 2.0f };
static const rusage_per_type_t per1[] = { PER_TASK, PER_JOB };
static int s11[] = { 1, 1 };
static int s1[] = { 1 };
static int s2[] = { 2 };
xce_sched_trace_rec_t* rec;
fx_primary_t p;
fx_primary_t p1;
xce_alloc_plan_t plan11 = make_plan(s11, 2);
xce_alloc_plan_t plan1 = make_plan(s1, 1);
xce_alloc_plan_t plan2 = make_plan(s2, 1);
xce_sched_worker_t n01 = make_worker("n01", 0);
xce_sched_worker_t n02 = make_worker("n02", 1);
xce_sched_worker_t n03 = make_worker("n03", 2);
xce_sched_worker_t n04 = make_worker("n04", 3);
sched_trace_row_t row;
sched_trace_reason_count_t codes[] = { { PREASON_MATCH_FAIL, 2 },
{ PREASON_NO_RESOURCE, 1 } };
const char* const hosts[] = { "n01", "n02" };
char* out;
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
set_selection(rec);
set_glimits(rec, 0, NULL, NULL);
/* a NULL rusage stores a NULL request name (note 1.4) */
make_primary(&p, 1, 2, 2, rn, rv, per);
make_primary(&p1, 1, 1, 2, rn1, rv1, per1);
/* compound job, member 1/2 */
sched_trace_path_push(rec, XCE_TRACE_PATH_COMPOUND, 1, 1);
sched_trace_resreq_begin(rec, NULL, 2);
sched_trace_path_push(rec, XCE_TRACE_PATH_MEMBER, 1, 2);
sched_trace_resreq_begin(rec, &p.p, 0);
sched_trace_check_begin(rec, NULL);
/* a row whose resource index is unknown: rendered by index (note 2.2) */
row = res_row(NULL, 5, 1, 0, 1, true);
row.idx = 7;
sched_trace_row(rec, &row);
sched_trace_check_end(rec, 1, true);
sched_trace_plan_discarded(rec, &plan11);
sched_trace_plan_begin(rec, &plan1);
sched_trace_pos_begin(rec, 1, 1);
sched_trace_host_begin(rec, NULL, &n01);
sched_trace_host_verdict(rec, XCE_TRACE_V_CLAIMED, 0);
sched_trace_host_begin(rec, NULL, &n02);
sched_trace_host_verdict(rec, XCE_TRACE_V_INTERNAL, -12);
sched_trace_host_begin(rec, NULL, &n03);
sched_trace_host_limit_hit(rec, "lim_h", "h=n03");
row = limit_row("lim_h", "h=n03", NULL, 2, 2, 0, 1, false);
sched_trace_row(rec, &row);
sched_trace_host_verdict(rec, 0, 0);
sched_trace_host_begin(rec, NULL, &n04);
sched_trace_host_verdict(rec, 0, 0); /* per-host selection branch */
sched_trace_pos_scan_error(rec, 0); /* no code (note 3.4) */
sched_trace_pos_end(rec, XCE_TRACE_POS_UNFILLED, NULL);
sched_trace_rollback_host(rec, "n09");
sched_trace_rollback_host(rec, "n10");
sched_trace_plan_end(rec, 0, 1);
sched_trace_plan_begin(rec, &plan1);
sched_trace_pos_begin(rec, 1, 1);
sched_trace_host_begin(rec, NULL, &n04);
sched_trace_host_verdict(rec, XCE_TRACE_V_ACCEPTED, 0);
sched_trace_pos_end(rec, XCE_TRACE_POS_INTERNAL, "n04");
sched_trace_plan_end(rec, -12, 1);
sched_trace_plan_not_tried(rec, &plan2, -12);
sched_trace_search_end(rec, -12);
sched_trace_resreq_end(rec, -12, 1);
sched_trace_path_pop(rec);
/* a merge failure stores reasons with no codes (notes 2.1, 3.2) */
sched_trace_attempt_reasons(rec, NULL, 0, NULL, 0, NULL, 0);
sched_trace_member_failed(rec, 1, 2, -5);
sched_trace_attempt_reasons(rec, codes, 2, hosts, 2, NULL, 0);
sched_trace_resreq_end(rec, -12, 0);
sched_trace_path_pop(rec);
/* alternatives: skipped, generator failure, results (note 3.2) */
sched_trace_alt_skipped(rec, 1, 3);
sched_trace_path_push(rec, XCE_TRACE_PATH_ALT, 2, 3);
sched_trace_resreq_begin(rec, NULL, 1);
sched_trace_path_push(rec, XCE_TRACE_PATH_MEMBER, 1, 1);
sched_trace_resreq_begin(rec, &p1.p, 0);
sched_trace_no_plan(rec, XCE_TRACE_NOPLAN_GENERATOR_FAILED);
sched_trace_resreq_end(rec, 0, 0);
sched_trace_path_pop(rec);
sched_trace_resreq_end(rec, 0, 0);
sched_trace_path_pop(rec);
sched_trace_alt_result(rec, 0, 3);
sched_trace_alt_result(rec, 1, 3);
sched_trace_alt_result(rec, 2, 3);
/* a single job with no requirement: a container of 0 members (3.2) */
sched_trace_path_push(rec, XCE_TRACE_PATH_SINGLE, 1, 1);
sched_trace_resreq_begin(rec, NULL, 0);
sched_trace_resreq_end(rec, -22, 0);
sched_trace_path_pop(rec);
/* search statements, finalize failure */
sched_trace_path_push(rec, XCE_TRACE_PATH_SINGLE, 1, 1);
sched_trace_resreq_begin(rec, &p1.p, 0);
sched_trace_no_plan(rec, XCE_TRACE_NOPLAN_NONE_GENERATED);
sched_trace_search_end(rec, 0);
sched_trace_search_end(rec, 0);
sched_trace_plan_discarded(rec, &plan1);
sched_trace_search_end(rec, 0);
sched_trace_plan_begin(rec, &plan1);
sched_trace_plan_end(rec, 0, 0);
sched_trace_search_end(rec, 0);
sched_trace_plan_begin(rec, &plan1);
sched_trace_plan_end(rec, 1, 0);
sched_trace_finalize_failed(rec, -12);
sched_trace_search_end(rec, -12);
sched_trace_resreq_end(rec, -12, 0);
sched_trace_path_pop(rec);
rec->incomplete = true; /* arena growth failed (design "Error Strategy") */
rec->outcome = SCHED_TRACE_OUTCOME_PEND;
out = render(rec, XCE_REPL_UNITS_RAW);
assert_golden(
"[trace] job 1-2-1000-42 session 7 position 3 level full\n"
SEL_LINE NO_LIMITS_LINE
" requirement compound (2 members)\n"
" requirement compound > member 1/2 tasks 1..2 requests ?=0 "
"slots=1/host\n"
" cluster-wide: resource #7 total 5 in-use 1 reserved 0 "
"requested 1 passed\n"
" cluster-wide check passed\n"
" plan 1 hosts 2 tasks per position [1, 1] discarded\n"
" plan 2 hosts 1 tasks per position [1]\n"
" position 1 (1 task)\n"
" host n01 skipped: already claimed by an earlier "
"position\n"
" host n02 not evaluated: internal error (err -12: Cannot "
"allocate memory)\n"
" host n03 rejected (per-host check): limit exceeded\n"
" limit lim_h (h=n03): value 2 usage 2 reserved 0 "
"requested 1\n"
" host n04 rejected (per-host check): selection mismatch\n"
" position 1 not filled: internal error in the host scan\n"
" released hosts: n09, n10\n"
" plan 2 failed: position 1 could not be filled\n"
" plan 3 hosts 1 tasks per position [1]\n"
" position 1 (1 task)\n"
" host n04 accepted\n"
" position 1 not filled: internal error after accepting n04\n"
" plan 3 ended in an internal error (err -12: Cannot allocate "
"memory); no further plans tried\n"
" plan 4 hosts 1 tasks per position [2] not tried: internal "
"error (err -12: Cannot allocate memory); no further plans tried\n"
" plans: 4 generated, 2 tried, 1 discarded, 1 not tried\n"
" member 1/2 ended in an internal error (err -12: Cannot "
"allocate memory); member 2/2 not tried\n"
" failure reasons: none recorded\n"
" member 1/2: merging its allocation ended in an internal error "
"(err -5: Input/output error); member 2/2 not tried\n"
" failure reasons: MATCH_FAIL x2, NO_RESOURCE x1 (hosts: n01, "
"n02)\n"
" compound requirement ended in an internal error (err -12: Cannot "
"allocate memory)\n"
" alternative 1/3 skipped: no requirement content\n"
" requirement alternative 2/3 (compound, 1 member)\n"
" requirement alternative 2/3 > member 1/1 tasks 1..1 requests "
"slots=1 lic=2/job\n"
" no plan generated: plan generator failed to start\n"
" member 1/1 not satisfied\n"
" alternative 2/3 not satisfied\n"
" no alternative chosen: none of the 3 alternatives was satisfied\n"
" alternative 1/3 chosen; alternatives 2..3 not tried\n"
" alternative 2/3 chosen; alternative 3/3 not tried\n"
" requirement single (no requirement content)\n"
" single requirement ended in an internal error (err -22: Invalid "
"argument)\n"
" requirement single tasks 1..1 requests slots=1 lic=2/job\n"
" no plan generated: generator produced no plan\n"
" plans: 0 generated, 0 tried, 0 discarded, 0 not tried\n"
" plans: 0 generated, 0 tried, 0 discarded, 0 not tried; no plan "
"generated\n"
" plan 1 hosts 1 tasks per position [1] discarded\n"
" plans: 1 generated, 0 tried, 1 discarded, 0 not tried; no plan "
"tried: all generated plans were discarded\n"
" plan 2 hosts 1 tasks per position [1]\n"
" plan 2 failed\n"
" plans: 1 generated, 1 tried, 0 discarded, 0 not tried; no plan "
"could be placed\n"
" plan 3 hosts 1 tasks per position [1]\n"
" plan 3 succeeded\n"
" plan 3 ended in an internal error building the allocation (err "
"-12: Cannot allocate memory); not committed, no further plans "
"tried\n"
" plans: 1 generated, 1 tried, 0 discarded, 0 not tried\n"
" single requirement ended in an internal error (err -12: Cannot "
"allocate memory)\n"
" ... trace incomplete (out of memory)\n"
" outcome: PEND\n"
" pend reasons: none\n",
out);
TEST_ASSERT_EQUAL_INT(3 + rec->nentries + 1 + 2, count_lines(out));
free(out);
sched_trace_rec_free(rec);
}
/* ---- sinks -------------------------------------------------------------- */
static void test_single_sink(void)
{
xce_sched_trace_rec_t* rec = build_design_example();
char* buf = NULL;
size_t len = 0;
FILE* f = open_memstream(&buf, &len);
TEST_ASSERT_NOT_NULL(f);
repl_trace_render(rec, XCE_REPL_UNITS_HUMAN, f, NULL);
fclose(f);
assert_golden(DESIGN_EXAMPLE_HUMAN, buf);
free(buf);
/* NULL record or no sink: nothing to do, no crash */
repl_trace_render(NULL, XCE_REPL_UNITS_HUMAN, stdout, NULL);
repl_trace_render(rec, XCE_REPL_UNITS_HUMAN, NULL, NULL);
sched_trace_rec_free(rec);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_full_level_design_example);
RUN_TEST(test_one_line_per_stored_entry);
RUN_TEST(test_summary_level_position_summary);
RUN_TEST(test_summary_scan_error_and_internal);
RUN_TEST(test_units_modes);
RUN_TEST(test_truncation_marker_and_totals);
RUN_TEST(test_dispatch_outcome_cap);
RUN_TEST(test_pend_outcome);
RUN_TEST(test_pend_outcome_null_entries);
RUN_TEST(test_pend_outcome_without_main_reason);
RUN_TEST(test_no_decision_not_found);
RUN_TEST(test_no_decision_no_schedobj);
RUN_TEST(test_no_decision_internal_error_keeps_hierarchy);
RUN_TEST(test_no_decision_pend_without_reason_keeps_hierarchy);
RUN_TEST(test_known_unsatisfiable_names_source_job);
RUN_TEST(test_task_range_counts);
RUN_TEST(test_reconstructed_record);
RUN_TEST(test_selection_and_limits_unavailable);
RUN_TEST(test_entry_kinds_and_edge_cases);
RUN_TEST(test_single_sink);
return UNITY_END();
}