diff --git a/.kiro/specs/scheduler-sim-decision-trace/tasks.md b/.kiro/specs/scheduler-sim-decision-trace/tasks.md index f8670700..b87bf681 100644 --- a/.kiro/specs/scheduler-sim-decision-trace/tasks.md +++ b/.kiro/specs/scheduler-sim-decision-trace/tasks.md @@ -180,7 +180,7 @@ - _Depends: 2.2, 3.3_ - [ ] 4. Simulator REPL trace surface -- [ ] 4.1 Render trace records to text, test first (P) +- [x] 4.1 Render trace records to text, test first (P) - **Hierarchy:** header, selection, requirements, cluster-wide checks and plans, positions and hosts, then the outcome. Every budgeted entry renders as exactly one line, in the order shown in the design's example. - **Summary level:** renders position summaries with per-limit names. - **Truncated records:** render a marker with the omitted line count, followed by the fixed-size totals block. @@ -364,3 +364,4 @@ - 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. - 3.3: the pre-filter (`sched_host_iter_next`) opens a host scope (`sched_trace_host_begin`) for every candidate and closes it with a verdict on each rejection: SELECT (pre-filter), INTERNAL(rc) for an uncommitted-fetch or check error, from-rows for a resource rejection (unreachable from the runner). A host the iterator returns is left in an open scope with no entry, so 3.4 must end or replace it for the accepted, already-claimed, snapshot-failure and per-host-error paths, or later rows attach to a stale host. A scan stopped with rc<0 calls `pos_scan_error`; exhaustion does not. `unsatisfy_` hands every row to `sched_trace_check_rows`. The sim thread's reconciliation reaches that hook but sees trace_rec NULL. - 3.4: runner hooks are in alloc_runner.c. The 1.5 OPEN item is settled: `sched_trace_no_plan` counts `reqs_none_generated` once and sets `scope_no_plan`, after which `search_end` makes no statement and `resreq_begin`/`search_end` reset the flag. So a declined or generator-failed requirement has a NO_PLAN entry and no SEARCH_END line, and 4.1 must render the NO_PLAN line as "no plan generated: ". The 3.3 open scope is closed: every returned candidate gets a fresh host scope and one verdict, and check_begin drops any stale host rows. When the iterator can't start, the position ends as SCAN_ERROR with err 0, so 4.1 must render a scan error with no code. trace.h is at 790 lines; split it before adding to it. +- 4.1: the renderer is `repl_trace_render(rec, units, out1, out2)`, declared in repl_internal.h, in repl_trace_render.c plus repl_trace_render_outcome.c, with a private repl_trace_render.h. It renders once per sink, so the bytes are identical. Amounts use `repl_format_numeric_resource_value` ("8.00 GB" in human mode) and times use `repl_format_ts_human`. Wording where the design is silent is fixed by the golden tests in test_trace_render.c, so 4.2/4.6/6.1 must reuse the renderer, never re-format. Pend-reason arrays may contain NULL entries (capture keeps them); every walk must skip them. The renderer is not yet in sched_sim_isolation_lib or the sim isolation scan list (5.1). diff --git a/src/scheduler/meson.build b/src/scheduler/meson.build index 56ec6683..f44b5f2f 100644 --- a/src/scheduler/meson.build +++ b/src/scheduler/meson.build @@ -29,6 +29,8 @@ scheduler_srcs = [ 'src/repl_break.c', 'src/repl_info.c', 'src/repl_params.c', + 'src/repl_trace_render.c', + 'src/repl_trace_render_outcome.c', 'src/schedobj.c', 'src/plugin.c', 'src/resource.c', diff --git a/src/scheduler/src/repl_internal.h b/src/scheduler/src/repl_internal.h index 8f5dc770..f273d998 100644 --- a/src/scheduler/src/repl_internal.h +++ b/src/scheduler/src/repl_internal.h @@ -523,4 +523,19 @@ extern int repl_count_jobs(xce_sched_core_t* core); */ extern void repl_round_rollup_process(xce_sched_sim_driver_t* sim); +/* + * repl_trace_render (repl_trace_render.c, scheduler-sim-decision-trace + * spec, task 4.1, design.md "TraceRenderer") - writes one decision trace + * record as text to `out1` and, when non-NULL, the same bytes to `out2`: + * the header, the selection block, the requirement/check/plan/host entries + * (one line per stored entry, two-space indent per level), the truncation + * marker and totals, then the outcome or the no-decision line. Amounts use + * `units` (7.7). The record is only read. NULL `rec` or `out1`: nothing. + * The record type stays opaque here (sched/trace.h has the layout). + */ +struct xce_sched_trace_rec_s; +extern void repl_trace_render(const struct xce_sched_trace_rec_s* rec, + xce_repl_units_mode_t units, FILE* out1, + FILE* out2); + #endif diff --git a/src/scheduler/src/repl_trace_render.c b/src/scheduler/src/repl_trace_render.c new file mode 100644 index 00000000..430806f1 --- /dev/null +++ b/src/scheduler/src/repl_trace_render.c @@ -0,0 +1,773 @@ +/* + * repl_trace_render.c - decision trace record to text + * (scheduler-sim-decision-trace spec, task 4.1, design.md "TraceRenderer"). + * + * One record renders as: + * + * [trace] job session position

level + * selection: ... (2.2; or "unavailable", 2.4) + * cluster-wide limits: ... (2.3; or "unavailable", 2.4) + * (3.x, 4.x, 5.x) + * ... trace lines omitted (line bound N) + 3 totals lines (9.2, 9.3) + * ... trace incomplete (out of memory) (design "Error Strategy") + * outcome: ... (6.x, 9.4) + * + * Every stored entry is exactly one line, indented two spaces per nesting + * level below the header (7.6), so the requirement, cluster-wide check, + * plan and host sections never print more lines than the record's line + * bound (design "Line contract"). A no-decision record (2.4) ends with one + * line naming its kind and error in place of the outcome; a reconstructed + * record (2.5) has a notice in place of the selection and entries. + * + * Amounts are formatted by the resource they measure (a limit amount by the + * resource the limit limits) with the units mode passed in, through + * repl_fmt.c's repl_format_numeric_resource_value() (7.7); job ids use + * fmt_jobid(). The same bytes go to each sink: the record is rendered once + * per sink by the same deterministic code. + * + * REPL side (design "Allowed Dependencies"): reads the record only (layout + * in sched/trace.h), touches no scheduler state and calls no scheduler + * API. The outcome section and the helpers both files use (indent, + * amounts, error text, limit names, pend-reason names) live in + * repl_trace_render_outcome.c, declared in repl_trace_render.h. + */ +#include + +#include "repl_trace_render.h" +#include "utils.h" + +/* Verdict kind names, indexed by the kind's bit position (5.10). */ +static const char* const KIND_NAMES[XCE_TRACE_V_NKINDS] = { + "accepted", "selection mismatch", "resource shortfall", + "limit exceeded", "already claimed", "not evaluated (internal error)" +}; + +/* ---- requirement labels (3.1) ------------------------------------------- */ + +static void put_level(FILE* f, xce_sched_trace_path_kind_t kind, int i, int n) +{ + switch (kind) { + case XCE_TRACE_PATH_SINGLE: + fputs("single", f); + break; + case XCE_TRACE_PATH_COMPOUND: + fputs("compound", f); + break; + case XCE_TRACE_PATH_ALT: + fprintf(f, "alternative %d/%d", i, n); + break; + case XCE_TRACE_PATH_MEMBER: + default: + fprintf(f, "member %d/%d", i, n); + break; + } +} + +/* "requirement > ": the full nesting path. */ +static void put_requirement(FILE* f, const sched_trace_path_t* p) +{ + int k; + + fputs("requirement", f); + for (k = 0; k < p->depth; k++) { + fputs((k == 0) ? " " : " > ", f); + put_level(f, p->lvl[k].kind, p->lvl[k].i, p->lvl[k].n); + } +} + +/* The innermost level as the subject of an outcome line. */ +static void put_subject(FILE* f, const sched_trace_path_t* p) +{ + if (p->depth <= 0) { + fputs("requirement", f); + return; + } + switch (p->lvl[p->depth - 1].kind) { + case XCE_TRACE_PATH_SINGLE: + fputs("single requirement", f); + break; + case XCE_TRACE_PATH_COMPOUND: + fputs("compound requirement", f); + break; + default: + put_level(f, p->lvl[p->depth - 1].kind, p->lvl[p->depth - 1].i, + p->lvl[p->depth - 1].n); + break; + } +} + +/* "; member 3/3 not tried" or "; members 2..3 not tried": the `count` + * siblings after sibling `i` of `n` (3.3, 3.4). */ +static void put_not_tried(FILE* f, const char* word, int i, int n, int count) +{ + int first = i + 1; + int last = i + count; + + if (count <= 0) { + return; + } + if (first == last) { + fprintf(f, "; %s %d/%d not tried", word, first, n); + } else { + fprintf(f, "; %ss %d..%d not tried", word, first, last); + } +} + +static void put_outcome_word(FILE* f, int rc, int err) +{ + char eb[RTR_BUFSZ]; + + if (rc > 0) { + fputs(" satisfied", f); + } else if (rc == 0) { + fputs(" not satisfied", f); + } else { + fprintf(f, " ended in an internal error%s", + rtr_err(err, eb, sizeof(eb))); + } +} + +static const char* member_suffix(int n) +{ + return (n == 1) ? "" : "s"; +} + +/* ---- entry lines --------------------------------------------------------- */ + +static void put_resreq_begin(FILE* f, const sched_trace_entry_t* e, + xce_repl_units_mode_t units) +{ + const sched_trace_path_t* p = &e->path; + char buf[RTR_BUFSZ]; + int n = e->u.resreq_begin.nmembers; + int k; + + put_requirement(f, p); + + if (e->u.resreq_begin.container) { + xce_sched_trace_path_kind_t kind + = (p->depth > 0) ? p->lvl[p->depth - 1].kind + : XCE_TRACE_PATH_SINGLE; + + if (kind == XCE_TRACE_PATH_ALT) { + fprintf(f, " (compound, %d member%s)", n, member_suffix(n)); + } else if ((kind == XCE_TRACE_PATH_COMPOUND) || (n > 0)) { + fprintf(f, " (%d member%s)", n, member_suffix(n)); + } else { + /* a single job without a requirement (task 3.2) */ + fputs(" (no requirement content)", f); + } + return; + } + + fprintf(f, " tasks %d..%d requests", e->u.resreq_begin.min_tasks, + e->u.resreq_begin.max_tasks); + if ((e->u.resreq_begin.req == NULL) || (e->u.resreq_begin.nreq <= 0)) { + fputs(" none", f); + return; + } + for (k = 0; k < e->u.resreq_begin.nreq; k++) { + const sched_trace_req_t* r = &e->u.resreq_begin.req[k]; + + /* a NULL rusage left a NULL name (task 1.4) */ + fprintf(f, " %s=%s", (r->name != NULL) ? r->name : "?", + rtr_value(units, r->name, r->value, buf, sizeof(buf))); + if (r->per == PER_JOB) { + fputs("/job", f); + } else if (r->per == PER_HOST) { + fputs("/host", f); + } + } +} + +static void put_resreq_end(FILE* f, const sched_trace_entry_t* e) +{ + const sched_trace_path_t* p = &e->path; + + put_subject(f, p); + put_outcome_word(f, e->u.resreq_end.rc, e->u.resreq_end.rc); + if ((p->depth > 0) + && (p->lvl[p->depth - 1].kind == XCE_TRACE_PATH_MEMBER)) { + put_not_tried(f, "member", p->lvl[p->depth - 1].i, + p->lvl[p->depth - 1].n, e->u.resreq_end.not_tried_after); + } +} + +static void put_alt_result(FILE* f, int i, int n) +{ + if (i <= 0) { + fprintf(f, "no alternative chosen: none of the %d alternatives was " + "satisfied", + n); + return; + } + fprintf(f, "alternative %d/%d chosen", i, n); + if (i >= n) { + fputs("; no alternatives after it", f); + } else { + put_not_tried(f, "alternative", i, n, n - i); + } +} + +static void put_reasons(FILE* f, const sched_trace_entry_t* e) +{ + char buf[RTR_BUFSZ]; + int nh = (e->u.reasons.hosts != NULL) ? e->u.reasons.nhosts : 0; + int nr = (e->u.reasons.res != NULL) ? e->u.reasons.nres : 0; + int k; + + fputs("failure reasons: ", f); + if ((e->u.reasons.codes == NULL) || (e->u.reasons.ncodes <= 0)) { + /* a merge failure or an empty reason set (tasks 2.1, 3.2) */ + fputs("none recorded", f); + } + for (k = 0; (e->u.reasons.codes != NULL) && (k < e->u.reasons.ncodes); + k++) { + fprintf(f, "%s%s x%d", (k > 0) ? ", " : "", + rtr_reason_name(e->u.reasons.codes[k].code, buf, sizeof(buf)), + e->u.reasons.codes[k].count); + } + if ((nh == 0) && (nr == 0)) { + return; + } + fputs(" (", f); + for (k = 0; k < nh; k++) { + fprintf(f, "%s%s", (k == 0) ? "hosts: " : ", ", + (e->u.reasons.hosts[k] != NULL) ? e->u.reasons.hosts[k] : "?"); + } + for (k = 0; k < nr; k++) { + const char* sep = (k > 0) ? ", " + : (nh > 0) ? "; resources: " + : "resources: "; + + fprintf(f, "%s%s", sep, + (e->u.reasons.res[k] != NULL) ? e->u.reasons.res[k] : "?"); + } + fputc(')', f); +} + +/* A satisfaction-check row: a resource row (4.1, 5.4) or a limit row (4.2, + * 5.5), never both. A limit row's amounts use the limited resource, or + * the raw number when it is unknown (task 2.2); a row whose resource index + * is unknown is named by that index (task 2.2). */ +static void put_row(FILE* f, const sched_trace_row_t* r, bool cluster, + xce_repl_units_mode_t units) +{ + char b1[RTR_BUFSZ]; + char b2[RTR_BUFSZ]; + char b3[RTR_BUFSZ]; + char b4[RTR_BUFSZ]; + const char* by; + + if ((r->flags & XCE_RESF_LIMIT) != 0) { + by = r->limited_res; + fputs(cluster ? "cluster-wide limit " : "limit ", f); + rtr_limit_ref(f, (r->limit != NULL) ? r->limit : r->res, r->scope); + if (by != NULL) { + fprintf(f, " on %s", by); + } + fprintf(f, ": value %s usage %s reserved %s requested %s", + rtr_value(units, by, r->total, b1, sizeof(b1)), + rtr_value(units, by, r->inuse, b2, sizeof(b2)), + rtr_value(units, by, r->reserved, b3, sizeof(b3)), + rtr_value(units, by, r->requested, b4, sizeof(b4))); + } else { + by = r->res; + if (cluster) { + fputs("cluster-wide: ", f); + } + if (by != NULL) { + fputs(by, f); + } else { + fprintf(f, "resource #%d", r->idx); + } + fprintf(f, " total %s in-use %s reserved %s requested %s", + rtr_value(units, by, r->total, b1, sizeof(b1)), + rtr_value(units, by, r->inuse, b2, sizeof(b2)), + rtr_value(units, by, r->reserved, b3, sizeof(b3)), + rtr_value(units, by, r->requested, b4, sizeof(b4))); + } + if (cluster) { + fputs(r->met ? " passed" : " failed", f); + } +} + +static void put_check_end(FILE* f, int rc) +{ + char eb[RTR_BUFSZ]; + + if (rc > 0) { + fputs("cluster-wide check passed", f); + } else if (rc == 0) { + fputs("cluster-wide check failed; no placement plans tried", f); + } else { + fprintf(f, "cluster-wide check ended in an internal error%s; no " + "placement plans tried", + rtr_err(rc, eb, sizeof(eb))); + } +} + +static void put_no_plan(FILE* f, xce_sched_trace_noplan_t why) +{ + fputs("no plan generated: ", f); + switch (why) { + case XCE_TRACE_NOPLAN_DECLINED: + fputs("allocator declined the requirement", f); + break; + case XCE_TRACE_NOPLAN_GENERATOR_FAILED: + fputs("plan generator failed to start", f); + break; + case XCE_TRACE_NOPLAN_NONE_GENERATED: + default: + fputs("generator produced no plan", f); + break; + } +} + +/* "plan hosts tasks per position [a, b]" (4.3). */ +static void put_plan(FILE* f, const sched_trace_entry_t* e) +{ + int k; + + fprintf(f, "plan %d hosts %d tasks per position [", e->u.plan.plan_no, + e->u.plan.nbuckets); + for (k = 0; (e->u.plan.slots != NULL) && (k < e->u.plan.nbuckets); k++) { + fprintf(f, "%s%d", (k > 0) ? ", " : "", e->u.plan.slots[k]); + } + fputc(']', f); +} + +static void put_plan_end(FILE* f, const sched_trace_entry_t* e) +{ + char eb[RTR_BUFSZ]; + int rc = e->u.plan_end.rc; + + fprintf(f, "plan %d", e->u.plan_end.plan_no); + if (rc > 0) { + fputs(" succeeded", f); + } else if (rc == 0) { + fputs(" failed", f); + if (e->u.plan_end.failed_pos > 0) { + fprintf(f, ": position %d could not be filled", + e->u.plan_end.failed_pos); + } + } else { + fprintf(f, " ended in an internal error%s; no further plans tried", + rtr_err(rc, eb, sizeof(eb))); + } +} + +static void put_pos_end(FILE* f, const sched_trace_entry_t* e) +{ + char eb[RTR_BUFSZ]; + const char* host = e->u.pos_end.host; + + fprintf(f, "position %d ", e->u.pos_end.pos); + switch (e->u.pos_end.how) { + case XCE_TRACE_POS_FILLED: + fprintf(f, "filled by %s", (host != NULL) ? host : "?"); + break; + case XCE_TRACE_POS_UNFILLED: + fputs("not filled: no host accepted", f); + break; + case XCE_TRACE_POS_SCAN_ERROR: + /* err 0: the scan could not start (task 3.4) */ + fprintf(f, "not filled: internal error in the host scan%s", + rtr_err(e->u.pos_end.err, eb, sizeof(eb))); + break; + case XCE_TRACE_POS_INTERNAL: + default: + fprintf(f, "not filled: internal error after accepting %s%s", + (host != NULL) ? host : "?", + rtr_err(e->u.pos_end.err, eb, sizeof(eb))); + break; + } +} + +/* Summary level (5.9): the position's end with its host tallies. */ +static void put_pos_summary(FILE* f, const sched_trace_entry_t* e) +{ + char eb[RTR_BUFSZ]; + const char* host = e->u.pos_summary.host; + const int* c = e->u.pos_summary.counts; + int k; + + switch (e->u.pos_summary.how) { + case XCE_TRACE_POS_FILLED: + fprintf(f, "accepted %s", (host != NULL) ? host : "?"); + break; + case XCE_TRACE_POS_SCAN_ERROR: + /* the scan error survives into the summary (task 1.5) */ + fprintf(f, "accepted none; position not filled: internal error in " + "the host scan%s", + rtr_err(e->u.pos_summary.err, eb, sizeof(eb))); + break; + case XCE_TRACE_POS_INTERNAL: + fprintf(f, "accepted %s; position not filled: internal error after " + "accepting it%s", + (host != NULL) ? host : "?", + rtr_err(e->u.pos_summary.err, eb, sizeof(eb))); + break; + case XCE_TRACE_POS_UNFILLED: + default: + fputs("accepted none", f); + break; + } + + fprintf(f, "; rejected: %s %d, %s %d, %s %d", KIND_NAMES[1], c[1], + KIND_NAMES[2], c[2], KIND_NAMES[3], c[3]); + if ((e->u.pos_summary.limits != NULL) && (e->u.pos_summary.nlimits > 0)) { + fputs(" (", f); + for (k = 0; k < e->u.pos_summary.nlimits; k++) { + const sched_trace_limit_tally_t* t = &e->u.pos_summary.limits[k]; + + if (k > 0) { + fputs(", ", f); + } + rtr_limit_ref(f, t->limit, t->scope); + fprintf(f, " %d", t->count); + } + fputc(')', f); + } + fprintf(f, ", %s %d, %s %d", KIND_NAMES[4], c[4], KIND_NAMES[5], c[5]); +} + +/* Full level: one host and its verdict (5.1-5.7, 5.10). */ +static void put_host(FILE* f, const sched_trace_entry_t* e) +{ + char eb[RTR_BUFSZ]; + unsigned v = e->u.host.verdicts; + int n = 0; + int k; + + fprintf(f, "host %s ", + (e->u.host.host != NULL) ? e->u.host.host : "?"); + if ((v & XCE_TRACE_V_ACCEPTED) != 0) { + fputs("accepted", f); + return; + } + if ((v & XCE_TRACE_V_CLAIMED) != 0) { + fputs("skipped: already claimed by an earlier position", f); + return; + } + if ((v & XCE_TRACE_V_INTERNAL) != 0) { + fprintf(f, "not evaluated: internal error%s", + rtr_err(e->u.host.err, eb, sizeof(eb))); + return; + } + + fputs("rejected", f); + if (e->u.host.phase == SCHED_TRACE_PHASE_PREFILTER) { + fputs(" (pre-filter)", f); + } else if (e->u.host.phase == SCHED_TRACE_PHASE_PERHOST) { + fputs(" (per-host check)", f); + } + fputs(": ", f); + for (k = 1; k <= 3; k++) { /* selection, shortfall, limit */ + if ((v & (1u << k)) != 0) { + fprintf(f, "%s%s", (n++ > 0) ? ", " : "", KIND_NAMES[k]); + } + } + if (n == 0) { + fputs("no reason recorded", f); + } + if (e->u.host.phase == SCHED_TRACE_PHASE_PREFILTER) { + fputs("; per-host check not performed", f); + } +} + +static void put_search_end(FILE* f, const sched_trace_entry_t* e) +{ + fprintf(f, "plans: %d generated, %d tried, %d discarded, %d not tried", + e->u.search_end.generated, e->u.search_end.tried, + e->u.search_end.discarded, e->u.search_end.not_tried); + switch (e->u.search_end.stmt) { + case SCHED_TRACE_SEARCH_NONE_GENERATED: + fputs("; no plan generated", f); + break; + case SCHED_TRACE_SEARCH_ALL_DISCARDED: + fputs("; no plan tried: all generated plans were discarded", f); + break; + case SCHED_TRACE_SEARCH_NONE_PLACED: + fputs("; no plan could be placed", f); + break; + case SCHED_TRACE_SEARCH_NO_STMT: + default: + break; + } +} + +/* One stored entry, one line. */ +static void put_entry(FILE* f, const sched_trace_entry_t* e, + xce_repl_units_mode_t units) +{ + char eb[RTR_BUFSZ]; + int k; + + rtr_indent(f, e->depth + 1); + switch (e->type) { + case SCHED_TRACE_E_RESREQ_BEGIN: + put_resreq_begin(f, e, units); + break; + case SCHED_TRACE_E_RESREQ_END: + put_resreq_end(f, e); + break; + case SCHED_TRACE_E_ALT_SKIPPED: + fprintf(f, "alternative %d/%d skipped: no requirement content", + e->u.alt.i, e->u.alt.n); + break; + case SCHED_TRACE_E_ALT_RESULT: + put_alt_result(f, e->u.alt.i, e->u.alt.n); + break; + case SCHED_TRACE_E_MEMBER_FAILED: + fprintf(f, "member %d/%d: merging its allocation ended in an " + "internal error%s", + e->u.member_failed.j, e->u.member_failed.m, + rtr_err(e->u.member_failed.err, eb, sizeof(eb))); + put_not_tried(f, "member", e->u.member_failed.j, e->u.member_failed.m, + e->u.member_failed.m - e->u.member_failed.j); + break; + case SCHED_TRACE_E_ATTEMPT_REASONS: + put_reasons(f, e); + break; + case SCHED_TRACE_E_CHECK_END: + put_check_end(f, e->u.check_end.rc); + break; + case SCHED_TRACE_E_NO_PLAN: + put_no_plan(f, e->u.no_plan.why); + break; + case SCHED_TRACE_E_PLAN_BEGIN: + put_plan(f, e); + break; + case SCHED_TRACE_E_PLAN_DISCARDED: + put_plan(f, e); + fputs(" discarded", f); + break; + case SCHED_TRACE_E_PLAN_NOT_TRIED: + put_plan(f, e); + fprintf(f, " not tried: internal error%s; no further plans tried", + rtr_err(e->u.plan.err, eb, sizeof(eb))); + break; + case SCHED_TRACE_E_PLAN_END: + put_plan_end(f, e); + break; + case SCHED_TRACE_E_POS_BEGIN: + fprintf(f, "position %d (%d task%s)", e->u.pos_begin.pos, + e->u.pos_begin.slots, (e->u.pos_begin.slots == 1) ? "" : "s"); + break; + case SCHED_TRACE_E_POS_END: + put_pos_end(f, e); + break; + case SCHED_TRACE_E_POS_SUMMARY: + put_pos_summary(f, e); + break; + case SCHED_TRACE_E_HOST: + put_host(f, e); + break; + case SCHED_TRACE_E_COMMIT: + fprintf(f, "plan %d committed; no further plans tried", + e->u.commit.plan_no); + break; + case SCHED_TRACE_E_FINALIZE_FAILED: + fprintf(f, "plan %d ended in an internal error building the " + "allocation%s; not committed, no further plans tried", + e->u.commit.plan_no, rtr_err(e->u.commit.err, eb, sizeof(eb))); + break; + case SCHED_TRACE_E_SEARCH_END: + put_search_end(f, e); + break; + case SCHED_TRACE_E_ROLLBACK: + fputs("released hosts: ", f); + for (k = 0; (e->u.rollback.hosts != NULL) && (k < e->u.rollback.nhosts); + k++) { + fprintf(f, "%s%s", (k > 0) ? ", " : "", + (e->u.rollback.hosts[k] != NULL) ? e->u.rollback.hosts[k] + : "?"); + } + break; + case SCHED_TRACE_E_CHECK_ROW: + put_row(f, &e->u.row, true, units); + break; + case SCHED_TRACE_E_HOST_ROW: + /* stored one level below its host line (task 2.2) */ + put_row(f, &e->u.row, false, units); + break; + case SCHED_TRACE_E_NTYPES: + default: + fprintf(f, "unknown trace entry %d", (int)e->type); + break; + } + fputc('\n', f); +} + +/* ---- header, selection, totals ------------------------------------------ */ + +static void put_header(FILE* f, const xce_sched_trace_rec_t* rec) +{ + char job[RTR_BUFSZ]; + + if (fmt_jobid(job, sizeof(job), rec->jobid) < 0) { + snprintf(job, sizeof(job), "%llu", (unsigned long long)rec->jobid); + } + fprintf(f, "[trace] job %s session %llu position %d level %s\n", job, + (unsigned long long)rec->session_no, rec->position, + (rec->level == XCE_TRACE_LEVEL_FULL) ? "full" : "summary"); +} + +/* The selection line (2.2) and the cluster-wide limits line (2.3); either + * says "unavailable" when it was never captured (2.4, task 2.1). */ +static void put_selection(FILE* f, const xce_sched_trace_rec_t* rec) +{ + const char* policy = (rec->policy_name != NULL) ? rec->policy_name + : "unknown"; + char ts[64]; + int k; + + rtr_indent(f, 1); + if (!rec->selection_set) { + fprintf(f, "selection: policy %s; ordering keys unavailable\n", + policy); + } else { + repl_format_ts_human((uint64_t)rec->queue_time * 1000000000ULL, ts, + sizeof(ts)); + fprintf(f, "selection: policy %s queue ", policy); + if (rec->queue_name != NULL) { + fprintf(f, "%s (priority %d)", rec->queue_name, + (int)rec->queue_priority); + } else { + fputs("none", f); + } + fprintf(f, " job priority %d queued %s\n", (int)rec->job_priority, + ts); + } + + rtr_indent(f, 1); + if (!rec->glimits_set) { + fprintf(f, "cluster-wide limits: unavailable%s\n", + (rec->kind == XCE_TRACE_REC_NO_SCHEDOBJ) + ? " (no scheduling object)" + : ""); + return; + } + fputs("cluster-wide limits: ", f); + if ((rec->glimits == NULL) || (rec->nglimits <= 0)) { + fputs("none", f); + } + for (k = 0; (rec->glimits != NULL) && (k < rec->nglimits); k++) { + if (k > 0) { + fputs(", ", f); + } + rtr_limit_ref(f, rec->glimits[k].limit, rec->glimits[k].scope); + } + fputs("; host-scoped limits are reported with each host's verdict\n", f); +} + +/* After truncation: the marker and the fixed-size totals (9.2, 9.3). */ +static void put_truncation(FILE* f, const xce_sched_trace_rec_t* rec) +{ + const sched_trace_counters_t* c = &rec->counters; + int k; + + if (c->omitted_lines <= 0) { + return; + } + rtr_indent(f, 1); + fprintf(f, "... %d trace lines omitted (line bound %d)\n", + c->omitted_lines, rec->line_bound); + rtr_indent(f, 1); + fprintf(f, "totals: plans tried %d, plans discarded %d\n", c->plans_tried, + c->plans_discarded); + rtr_indent(f, 1); + fprintf(f, "totals: requirements with no plan generated %d, all plans " + "discarded %d, no plan placed %d\n", + c->reqs_none_generated, c->reqs_all_discarded, + c->reqs_none_placed); + rtr_indent(f, 1); + fputs("totals: host evaluations: ", f); + for (k = 0; k < XCE_TRACE_V_NKINDS; k++) { + fprintf(f, "%s%s %d", (k > 0) ? ", " : "", KIND_NAMES[k], + c->host_evals[k]); + } + fputc('\n', f); +} + +/* The line a no-decision record ends with, in place of the outcome (2.4). */ +static void put_no_decision(FILE* f, const xce_sched_trace_rec_t* rec) +{ + char eb[RTR_BUFSZ]; + const char* what; + + switch (rec->kind) { + case XCE_TRACE_REC_NOT_FOUND: + what = "job not found by the allocator"; + break; + case XCE_TRACE_REC_NO_SCHEDOBJ: + what = "job has no scheduling object (its resource requirement " + "could not be parsed)"; + break; + case XCE_TRACE_REC_INTERNAL_ERROR: + what = "allocation attempt ended in an internal error"; + break; + case XCE_TRACE_REC_PEND_NO_REASON: + default: + what = "job pended without any pend reason"; + break; + } + rtr_indent(f, 1); + fprintf(f, "no decision: %s%s\n", what, rtr_err(rec->err, eb, sizeof(eb))); +} + +static void render_one(const xce_sched_trace_rec_t* rec, + xce_repl_units_mode_t units, FILE* f) +{ + const sched_trace_entry_t* e; + + put_header(f, rec); + + if (rec->kind == XCE_TRACE_REC_RECONSTRUCTED) { + /* 2.5: no selection, requirement, plan or host sections */ + rtr_indent(f, 1); + fputs("dispatch reconstructed from the recorded allocation; " + "selection, requirement, plan and host sections do not " + "apply\n", + f); + rtr_outcome(f, rec, units); + return; + } + + if (rec->kind == XCE_TRACE_REC_NOT_FOUND) { + rtr_indent(f, 1); + fputs("selection: ordering keys and limits unavailable (job not " + "found by the allocator)\n", + f); + } else { + put_selection(f, rec); + } + + for (e = rec->entries; e != NULL; e = e->next) { + put_entry(f, e, units); + } + put_truncation(f, rec); + if (rec->incomplete) { + rtr_indent(f, 1); + fputs("... trace incomplete (out of memory)\n", f); + } + + if (rec->kind == XCE_TRACE_REC_DECISION) { + rtr_outcome(f, rec, units); + } else { + put_no_decision(f, rec); + } +} + +void repl_trace_render(const xce_sched_trace_rec_t* rec, + xce_repl_units_mode_t units, FILE* out1, FILE* out2) +{ + if ((rec == NULL) || (out1 == NULL)) { + return; + } + render_one(rec, units, out1); + if ((out2 != NULL) && (out2 != out1)) { + render_one(rec, units, out2); + } +} diff --git a/src/scheduler/src/repl_trace_render.h b/src/scheduler/src/repl_trace_render.h new file mode 100644 index 00000000..9a1b7360 --- /dev/null +++ b/src/scheduler/src/repl_trace_render.h @@ -0,0 +1,53 @@ +#if !defined(_REPL_TRACE_RENDER_H_) + +#define _REPL_TRACE_RENDER_H_ 1 + +/* + * repl_trace_render.h - helpers shared by the two decision trace renderer + * sources (scheduler-sim-decision-trace spec, task 4.1, design.md + * "TraceRenderer"). NOT installed and NOT included outside + * src/scheduler/src/repl_trace_render*.c; the entry point, + * repl_trace_render(), is declared in repl_internal.h. + * + * - repl_trace_render.c: the entry point, header and selection block, + * the entry hierarchy, the truncation marker and totals, and the + * no-decision and reconstructed lines. + * - repl_trace_render_outcome.c: the dispatch and pend outcome, and the + * helpers below. + * + * REPL side: stdio sinks only, records read and never changed, no + * scheduler state (design "Allowed Dependencies"). + */ +#include +#include + +#include "repl_internal.h" +#include "sched/trace.h" + +/* Big enough for any one formatted amount, error text or job id. */ +#define RTR_BUFSZ 128 + +/* Two spaces per level (7.6). */ +extern void rtr_indent(FILE* f, int level); + +/* An amount formatted by the resource it measures, in `units` (7.7); a + * NULL resource name formats the raw number. Returns `buf`. */ +extern const char* rtr_value(xce_repl_units_mode_t units, const char* res, + double value, char* buf, size_t bufsz); + +/* "" for err == 0, else " (err : )". Returns `buf`. */ +extern const char* rtr_err(int err, char* buf, size_t bufsz); + +/* A limit and its scope instance: " ()", or "" when + * the scope is empty. A NULL limit name prints as "?". */ +extern void rtr_limit_ref(FILE* f, const char* limit, const char* scope); + +/* A pend-reason code's name (preason.h), or "code ". Returns a static + * string or `buf`. */ +extern const char* rtr_reason_name(int code, char* buf, size_t bufsz); + +/* The outcome section (6.1-6.4, 9.4), at indent level 1. */ +extern void rtr_outcome(FILE* f, const xce_sched_trace_rec_t* rec, + xce_repl_units_mode_t units); + +#endif /* _REPL_TRACE_RENDER_H_ */ diff --git a/src/scheduler/src/repl_trace_render_outcome.c b/src/scheduler/src/repl_trace_render_outcome.c new file mode 100644 index 00000000..b5ac425e --- /dev/null +++ b/src/scheduler/src/repl_trace_render_outcome.c @@ -0,0 +1,326 @@ +/* + * repl_trace_render_outcome.c - the outcome section of a rendered decision + * trace (scheduler-sim-decision-trace spec, task 4.1, design.md + * "TraceRenderer", Outcome), split from repl_trace_render.c to keep each + * file under the 800-line cap (repl_trace_render.h). + * + * A dispatch lists each host kept in detail (at most + * XCE_TRACE_OUTCOME_HOSTS) with its allocated resources and, separately, + * its host-scoped limit charges, then the job's shared resources and + * cluster-wide limit charges, then, when hosts were omitted, their totals + * per resource and per limit and a marker counting them (6.1-6.3, 9.4). A + * pend lists the main reason, the entries that name no host, then each + * host's entries, then the omitted hosts' counts per code and their marker + * (6.4, 9.4). TraceCapture already applied the host cap; this file only + * prints what the record holds. + * + * It also holds the small formatting helpers both renderer files share + * (repl_trace_render.h): indentation, amounts in the units mode passed in + * (repl_fmt.c, 7.7), error text, limit names and pend-reason names. + */ +#include +#include + +#include "preason.h" +#include "repl_trace_render.h" + +/* ---- shared helpers (repl_trace_render.h) ------------------------------- */ + +void rtr_indent(FILE* f, int level) +{ + int k; + + for (k = 0; k < level; k++) { + fputs(" ", f); + } +} + +const char* rtr_value(xce_repl_units_mode_t units, const char* res, + double value, char* buf, size_t bufsz) +{ + repl_format_numeric_resource_value(units, res, value, buf, bufsz); + return buf; +} + +const char* rtr_err(int err, char* buf, size_t bufsz) +{ + if (err == 0) { + buf[0] = '\0'; + } else { + snprintf(buf, bufsz, " (err %d: %s)", err, + strerror((err < 0) ? -err : err)); + } + return buf; +} + +void rtr_limit_ref(FILE* f, const char* limit, const char* scope) +{ + fputs((limit != NULL) ? limit : "?", f); + if ((scope != NULL) && (scope[0] != '\0')) { + fprintf(f, " (%s)", scope); + } +} + +const char* rtr_reason_name(int code, char* buf, size_t bufsz) +{ + switch (code) { + case PREASON_BAD_RESREQ: + return "BAD_RESREQ"; + case PREASON_NO_RESOURCE: + return "NO_RESOURCE"; + case PREASON_MATCH_FAIL: + return "MATCH_FAIL"; + default: + snprintf(buf, bufsz, "code %d", code); + return buf; + } +} + +/* "name=value ..." for allocated resources; "none" when there are none. */ +static void put_res_list(FILE* f, const sched_trace_alloc_row_t* rows, int n, + xce_repl_units_mode_t units) +{ + char buf[RTR_BUFSZ]; + int k; + + if ((rows == NULL) || (n <= 0)) { + fputs("none", f); + return; + } + for (k = 0; k < n; k++) { + const char* res = rows[k].res; + + fprintf(f, "%s%s=%s", (k > 0) ? " " : "", + (res != NULL) ? res : "?", + rtr_value(units, res, rows[k].value, buf, sizeof(buf))); + } +} + +/* " limit charges: () , ..." -- the + * amount formatted by the limited resource, or raw when it is unknown + * (7.7). Nothing when there are no charges (6.3: never a resource row). */ +static void put_charges(FILE* f, const sched_trace_alloc_row_t* rows, int n, + xce_repl_units_mode_t units) +{ + char buf[RTR_BUFSZ]; + int k; + + if ((rows == NULL) || (n <= 0)) { + return; + } + fputs(" limit charges: ", f); + for (k = 0; k < n; k++) { + const sched_trace_alloc_row_t* c = &rows[k]; + + if (k > 0) { + fputs(", ", f); + } + rtr_limit_ref(f, (c->limit != NULL) ? c->limit : c->res, c->scope); + if (c->limited_res != NULL) { + fprintf(f, " %s", c->limited_res); + } + fprintf(f, " %s", + rtr_value(units, c->limited_res, c->value, buf, sizeof(buf))); + } +} + +static void put_dispatch(FILE* f, const sched_trace_dispatch_out_t* d, + xce_repl_units_mode_t units) +{ + int k; + + for (k = 0; (d->hosts != NULL) && (k < d->nhosts); k++) { + const sched_trace_host_out_t* h = &d->hosts[k]; + + rtr_indent(f, 2); + fprintf(f, "host %s: ", (h->host != NULL) ? h->host : "?"); + put_res_list(f, h->res, h->nres, units); + put_charges(f, h->charges, h->ncharges, units); + fputc('\n', f); + } + + rtr_indent(f, 2); + fputs("shared: ", f); + put_res_list(f, d->shared, d->nshared, units); + put_charges(f, d->shared_charges, d->nshared_charges, units); + fputc('\n', f); + + if (d->omitted_hosts <= 0) { + return; + } + /* 9.4: the omitted hosts' totals, limits totalled per limit */ + rtr_indent(f, 2); + fputs("omitted hosts total: ", f); + put_res_list(f, d->agg_res, d->nagg_res, units); + put_charges(f, d->agg_charges, d->nagg_charges, units); + fputc('\n', f); + rtr_indent(f, 2); + fprintf(f, "... %d hosts omitted\n", d->omitted_hosts); +} + +static bool hostless(const char* host) +{ + return (host == NULL) || (host[0] == '\0'); +} + +static bool same_host(const char* a, const char* b) +{ + return !hostless(a) && !hostless(b) && (strcmp(a, b) == 0); +} + +/* The host named by entry `k` of the combined list (host-based entries + * first, then resource-based ones), or NULL when that entry is NULL: the + * outcome copy keeps NULL entries (sched_trace_outcome.c), so every walk + * over these lists goes through here and never dereferences one. */ +static const char* entry_host(const xce_job_preason_t* r, int k) +{ + if (k < r->nhost_reasons) { + const xce_host_reason_t* h = (r->host_based_reasons != NULL) + ? r->host_based_reasons[k] + : NULL; + + return (h != NULL) ? h->host : NULL; + } else { + const xce_res_reason_t* s + = (r->res_based_reasons != NULL) + ? r->res_based_reasons[k - r->nhost_reasons] + : NULL; + + return (s != NULL) ? s->host : NULL; + } +} + +/* True when an entry before `idx` of the combined list names `host`. */ +static bool host_seen(const xce_job_preason_t* r, int idx, const char* host) +{ + int k; + + for (k = 0; k < idx; k++) { + if (same_host(entry_host(r, k), host)) { + return true; + } + } + return false; +} + +/* One line per host: its host-based reasons, then its resource reasons. */ +static void put_pend_host(FILE* f, const xce_job_preason_t* r, + const char* host) +{ + char buf[RTR_BUFSZ]; + int n = 0; + int k; + + rtr_indent(f, 2); + fprintf(f, "host %s: ", host); + for (k = 0; (r->host_based_reasons != NULL) && (k < r->nhost_reasons); + k++) { + const xce_host_reason_t* h = r->host_based_reasons[k]; + + if ((h != NULL) && same_host(h->host, host)) { + fprintf(f, "%s%s", (n++ > 0) ? ", " : "", + rtr_reason_name(h->reason, buf, sizeof(buf))); + } + } + for (k = 0; (r->res_based_reasons != NULL) && (k < r->nres_reasons); + k++) { + const xce_res_reason_t* s = r->res_based_reasons[k]; + + if ((s != NULL) && same_host(s->host, host)) { + fprintf(f, "%sresource %s: %s", (n++ > 0) ? ", " : "", + (s->resource != NULL) ? s->resource : "?", + rtr_reason_name(s->reason, buf, sizeof(buf))); + } + } + fputc('\n', f); +} + +static void put_pend(FILE* f, const sched_trace_pend_out_t* p) +{ + const xce_job_preason_t* r = p->reasons; + char buf[RTR_BUFSZ]; + int total; + int k; + + if (r == NULL) { + rtr_indent(f, 2); + fputs("pend reasons: none\n", f); + return; + } + + rtr_indent(f, 2); + fprintf(f, "main reason: %s\n", + rtr_reason_name(r->main_reason, buf, sizeof(buf))); + + /* entries tied to no host (a shared resource carries host "") */ + for (k = 0; (r->host_based_reasons != NULL) && (k < r->nhost_reasons); + k++) { + const xce_host_reason_t* h = r->host_based_reasons[k]; + + if ((h != NULL) && hostless(h->host)) { + rtr_indent(f, 2); + fprintf(f, "reason: %s\n", + rtr_reason_name(h->reason, buf, sizeof(buf))); + } + } + for (k = 0; (r->res_based_reasons != NULL) && (k < r->nres_reasons); + k++) { + const xce_res_reason_t* s = r->res_based_reasons[k]; + + if ((s != NULL) && hostless(s->host)) { + rtr_indent(f, 2); + fprintf(f, "resource %s: %s\n", + (s->resource != NULL) ? s->resource : "?", + rtr_reason_name(s->reason, buf, sizeof(buf))); + } + } + + /* each host once, in the order first named */ + total = r->nhost_reasons + r->nres_reasons; + for (k = 0; k < total; k++) { + const char* host = entry_host(r, k); /* NULL entry: hostless */ + + if (hostless(host) || host_seen(r, k, host)) { + continue; + } + put_pend_host(f, r, host); + } + + if (p->omitted_hosts <= 0) { + return; + } + /* 9.4: the omitted hosts' entries counted per code */ + rtr_indent(f, 2); + fputs("omitted hosts' pend reasons: ", f); + if ((p->omitted_codes == NULL) || (p->ncodes <= 0)) { + fputs("none", f); + } + for (k = 0; (p->omitted_codes != NULL) && (k < p->ncodes); k++) { + fprintf(f, "%s%s x%d", (k > 0) ? ", " : "", + rtr_reason_name(p->omitted_codes[k].code, buf, sizeof(buf)), + p->omitted_codes[k].count); + } + fputc('\n', f); + rtr_indent(f, 2); + fprintf(f, "... %d hosts omitted\n", p->omitted_hosts); +} + +void rtr_outcome(FILE* f, const xce_sched_trace_rec_t* rec, + xce_repl_units_mode_t units) +{ + rtr_indent(f, 1); + switch (rec->outcome) { + case SCHED_TRACE_OUTCOME_DISPATCH: + fputs("outcome: DISPATCH\n", f); + put_dispatch(f, &rec->dispatch, units); + break; + case SCHED_TRACE_OUTCOME_PEND: + fputs("outcome: PEND\n", f); + put_pend(f, &rec->pend); + break; + case SCHED_TRACE_OUTCOME_NONE: + default: + fputs("outcome: none recorded\n", f); + break; + } +} diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 5a24ab3c..0c3c06bf 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -2716,3 +2716,22 @@ sched_trace_hooks_runner_test = executable( test('scheduler trace hooks runner test', sched_trace_hooks_runner_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) + +# scheduler-sim-decision-trace task 4.1: the trace renderer (design.md +# "TraceRenderer"; Requirements 2.4, 2.5, 3.x, 4.x, 5.x, 6.x, 7.6, 7.7, +# 9.2-9.4). Golden text for full and summary levels, every units mode, +# truncation, the outcome cap, the no-decision kinds and reconstructed +# records. Links only the record model, the renderer and the shared REPL +# formatting module; records are built without the capture sources. +sched_trace_render_test = executable( + 'sched_trace_render_test', + ['test_trace_render.c', '../src/sched_trace.c', + '../src/sched_trace_scan.c', '../src/repl_trace_render.c', + '../src/repl_trace_render_outcome.c', '../src/repl_fmt.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps], + install: false) + +test('scheduler trace render test', + sched_trace_render_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_trace_render.c b/src/scheduler/testsuite/test_trace_render.c new file mode 100644 index 00000000..3a8541c6 --- /dev/null +++ b/src/scheduler/testsuite/test_trace_render.c @@ -0,0 +1,1289 @@ +/* + * 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 +#include +#include + +#include + +#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); +} + +/* ---- 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); +} + +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_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_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(); +}