/* * 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); } /* 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(); }