diff --git a/.kiro/specs/scheduler-sim-decision-trace/tasks.md b/.kiro/specs/scheduler-sim-decision-trace/tasks.md index 03d74ec7..bca1970d 100644 --- a/.kiro/specs/scheduler-sim-decision-trace/tasks.md +++ b/.kiro/specs/scheduler-sim-decision-trace/tasks.md @@ -207,7 +207,7 @@ - _Requirements: 1.7, 7.8, 7.9, 7.10, 7.11, 9.5, 9.6_ - _Depends: 4.1_ -- [ ] 4.3 Parse the trace command and manage trace settings, test first +- [x] 4.3 Parse the trace command and manage trace settings, test first - `trace` re-parses the raw input line, so it is not limited by the normal tokenizer. It accepts: - `on all`, or `on job` followed by any number of job selectors; - an optional level (default summary) and an optional line bound (50 to 100000, default 1000); @@ -366,3 +366,4 @@ - 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). - 4.2: the store API is `repl_trace_store_put`, `repl_trace_store_bytes`, `repl_cmd_info_trace` and `repl_trace_store_fini` (an addition; nothing calls it at shutdown yet, so 4.5/4.6/5.2 must). `xce_repl_trace_state_t` (repl_internal.h) holds only the store fields; 4.3 adds next/cur/session flags to it. The 1.3 "file tag" is dropped: `info trace` is console-only (Req 8.1) and emission writes through the session's `cur` settings. `info trace` is not yet routed from repl_cmd_info (5.2). Open (6.2): an `id_list_add` OOM can silently drop array elements from a listing. +- 4.3: `trace` is parsed from the raw line by repl_trace.c. The settings are a refcounted REPL-side wrapper around xce_sched_trace_ctl_t (trace.h untouched), held as `next`/`cur` in xce_repl_trace_state_t together with session_in_progress, round_traced and round_flushed. The 1.3 bare-id item is settled: an array-aligned bare id adds both an exact and a whole-array selector, so later elements match; status shows that as `[*]`. A range expands to one exact selector per element. The wrapper frees `ctl.policy_name` on its last release, so 4.6 must heap-allocate it. Nothing frees next/cur at REPL shutdown yet, so 4.5/4.6 must add the shutdown path; routing `trace` from repl.c is 5.2. diff --git a/src/scheduler/meson.build b/src/scheduler/meson.build index ad160970..9fcb9af5 100644 --- a/src/scheduler/meson.build +++ b/src/scheduler/meson.build @@ -32,6 +32,7 @@ scheduler_srcs = [ 'src/repl_trace_render.c', 'src/repl_trace_render_outcome.c', 'src/repl_trace_store.c', + 'src/repl_trace.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 e1aaa38d..54c0cd0b 100644 --- a/src/scheduler/src/repl_internal.h +++ b/src/scheduler/src/repl_internal.h @@ -167,8 +167,28 @@ typedef enum { */ #define XCE_REPL_TRACE_STORE_BOUND ((size_t)64 * 1024 * 1024) /* 9.5 */ +/* + * Trace settings (repl_trace.c, task 4.3, design.md "ReplTraceControl" and + * "Trace settings lifecycle"): a reference-counted object holding the + * selection, level, line bound, file path and the xce_sched_trace_ctl_t it + * publishes to the core. `next` is what the next session will use (NULL = + * tracing off) and `cur` is what the session in progress started with + * (NULL = none, or that session started with tracing off). Commands only + * replace `next`; the session hooks (task 4.6) set `cur = ref(next)` at + * session begin and drop `cur` at session end. An object is freed when + * neither holds it. `session_in_progress`, `round_traced` and + * `round_flushed` are maintained by the session hooks and inline emission + * (tasks 4.6, 5.2). + */ +typedef struct xce_repl_trace_settings_s xce_repl_trace_settings_t; + struct repl_trace_entry_s; typedef struct { + xce_repl_trace_settings_t* next; + xce_repl_trace_settings_t* cur; + bool session_in_progress; + bool round_traced; + bool round_flushed; htab_t by_job; /* jobid -> repl_trace_entry_s* */ htab_t discarded; /* jobid -> eviction seq */ bool tables_ready; @@ -600,4 +620,31 @@ extern void repl_trace_store_fini(xce_repl_trace_state_t* ts); extern bool repl_cmd_info_trace(xce_sched_sim_driver_t* sim, char tokens[][XCE_REPL_TOKEN_LEN], int ntok); +/* + * ReplTraceControl (repl_trace.c, scheduler-sim-decision-trace spec, task + * 4.3, design.md "ReplTraceControl"): + * + * repl_trace_cmd - `trace on all|job SEL... [level summary|full] [lines N] + * [file PATH]`, `trace off`, `trace status`. Parses the raw input line, so + * neither the 16-token nor the 64-character tokenizer limit applies (1.12). + * A bare job id selects both the plain job and every element of an array + * with that id (1.2, 1.3). On success it replaces rs->trace.next only + * (1.4-1.6); an invalid argument prints `[sim] trace: invalid : + * ` and changes nothing (1.9). Always returns false (re-prompt). + * + * repl_trace_settings_ref/unref - NULL-safe reference counting; ref returns + * its argument. The last unref frees the selection, path and policy name. + * repl_trace_settings_ctl/path - the published control block and the file + * path (NULL when none). + */ +struct xce_sched_trace_ctl_s; +extern bool repl_trace_cmd(xce_sched_sim_driver_t* sim, const char* raw_line); +extern xce_repl_trace_settings_t* + repl_trace_settings_ref(xce_repl_trace_settings_t* s); +extern void repl_trace_settings_unref(xce_repl_trace_settings_t* s); +extern struct xce_sched_trace_ctl_s* +repl_trace_settings_ctl(xce_repl_trace_settings_t* s); +extern const char* +repl_trace_settings_path(const xce_repl_trace_settings_t* s); + #endif diff --git a/src/scheduler/src/repl_trace.c b/src/scheduler/src/repl_trace.c new file mode 100644 index 00000000..2c2e1007 --- /dev/null +++ b/src/scheduler/src/repl_trace.c @@ -0,0 +1,518 @@ +/* + * repl_trace.c - the simulator REPL's `trace` command and the trace + * settings lifecycle (scheduler-sim-decision-trace spec, task 4.3; + * design.md "ReplTraceControl", "Trace settings lifecycle"). + * + * Grammar (1.1, 1.2, 1.5, 1.7, 1.8, 1.12), parsed from the raw input line + * so neither the REPL tokenizer's 16-token nor its 64-character limit + * applies: + * trace on all [level summary|full] [lines N] [file PATH] + * trace on job JOBSEL [JOBSEL ...] [level summary|full] [lines N] + * [file PATH] + * trace off + * trace status + * JOBSEL is parse_jobsel() syntax: a job, `array[k]`, `array[a-b]`, or a + * bare id. An array-aligned bare id may name a plain job or an array that + * does not exist yet (1.3), so it becomes two selectors: the exact id and + * the whole array (sched_trace_ctl_wants() matches whole-array selectors + * against elements only). An element range becomes one selector per + * element. `level` and `lines` may come in any order; `file` must be last + * and PATH is the rest of the line, trimmed, with one pair of surrounding + * quotes removed, at most 4095 bytes. + * + * Settings objects are reference-counted and held by rs->trace.next (what + * the next session uses) and rs->trace.cur (what the session in progress + * started with). Commands build a complete new object and only then + * replace `next`, so an invalid argument leaves everything unchanged + * (1.9), and a session in progress keeps its own settings (1.6). The + * session hooks that move `next` into `cur` are task 4.6; opening the file + * and the same-file checks are task 4.5 (only the path is kept here). + * + * Runs on the sim thread at the REPL prompt only; writes nothing through + * tlog (8.7). + */ +#include +#include +#include + +#include "repl_internal.h" +#include "sched/trace.h" +#include "sim.h" +#include "utils.h" + +#define TRACE_PATH_MAX 4095 /* 1.9, 1.12 */ + +struct xce_repl_trace_settings_s { + int refs; + xce_sched_trace_ctl_t ctl; /* ctl.sel and ctl.policy_name owned here */ + char* path; /* trace file path, NULL when none */ + int cap; /* allocated length of ctl.sel */ +}; + +/* ---- settings objects ----------------------------------------------------- */ + +static xce_repl_trace_settings_t* settings_new(void) +{ + xce_repl_trace_settings_t* s = + (xce_repl_trace_settings_t*)calloc(1, sizeof(*s)); + + if (s == NULL) { + return NULL; + } + s->refs = 1; + s->ctl.level = XCE_TRACE_LEVEL_SUMMARY; /* 1.8 */ + s->ctl.line_bound = XCE_TRACE_LINE_BOUND_DEFAULT; /* 9.1 */ + return s; +} + +xce_repl_trace_settings_t* repl_trace_settings_ref(xce_repl_trace_settings_t* s) +{ + if (s != NULL) { + s->refs++; + } + return s; +} + +void repl_trace_settings_unref(xce_repl_trace_settings_t* s) +{ + if ((s == NULL) || (--s->refs > 0)) { + return; + } + free(s->ctl.sel); + free((char*)s->ctl.policy_name); + free(s->path); + free(s); +} + +xce_sched_trace_ctl_t* repl_trace_settings_ctl(xce_repl_trace_settings_t* s) +{ + return (s != NULL) ? &(s->ctl) : NULL; +} + +const char* repl_trace_settings_path(const xce_repl_trace_settings_t* s) +{ + return (s != NULL) ? s->path : NULL; +} + +static bool sel_add(xce_repl_trace_settings_t* s, uint64_t key, + bool whole_array) +{ + if (s->ctl.nsel == s->cap) { + int cap = (s->cap == 0) ? 16 : (s->cap * 2); + xce_sched_trace_sel_t* sel = (xce_sched_trace_sel_t*)realloc( + s->ctl.sel, (size_t)cap * sizeof(*sel)); + + if (sel == NULL) { + return false; + } + s->ctl.sel = sel; + s->cap = cap; + } + s->ctl.sel[s->ctl.nsel].key = key; + s->ctl.sel[s->ctl.nsel].whole_array = whole_array; + s->ctl.nsel++; + return true; +} + +/* ---- raw-line scanning ---------------------------------------------------- */ + +typedef struct { + const char* p; /* start of the word */ + size_t n; /* its length, 0 at end of line */ +} word_t; + +static bool is_space(char c) { return (c == ' ') || (c == '\t'); } + +/* Reads the next whitespace-delimited word at *pp and advances past it. */ +static word_t next_word(const char** pp) +{ + const char* p = *pp; + word_t w; + + while (is_space(*p)) { + p++; + } + w.p = p; + while ((*p != '\0') && !is_space(*p) && (*p != '\n') && (*p != '\r')) { + p++; + } + w.n = (size_t)(p - w.p); + *pp = p; + return w; +} + +static bool word_is(word_t w, const char* kw) +{ + return (w.n == strlen(kw)) && (strncmp(w.p, kw, w.n) == 0); +} + +static bool is_option(word_t w) +{ + return word_is(w, "level") || word_is(w, "lines") || word_is(w, "file"); +} + +/* `[sim] trace: invalid : `, "(missing)" for an absent arg. */ +static void invalid(const char* what, word_t w, const char* why) +{ + if (w.n == 0) { + fprintf(stdout, "[sim] trace: invalid %s: (missing)\n", what); + } else if (why != NULL) { + fprintf(stdout, "[sim] trace: invalid %s: %.*s (%s)\n", what, + (int)w.n, w.p, why); + } else { + fprintf(stdout, "[sim] trace: invalid %s: %.*s\n", what, (int)w.n, + w.p); + } +} + +static void out_of_memory(void) +{ + fprintf(stdout, "[sim] trace: out of memory\n"); +} + +/* ---- `trace on` ----------------------------------------------------------- */ + +/* One JOBSEL word. Returns false after printing the error. */ +static bool parse_sel(xce_repl_trace_settings_t* s, word_t w) +{ + char* text = strndup(w.p, w.n); + jobsel_t js; + jobsel_err_t err; + bool ok = true; + uint32_t k; + + if (text == NULL) { + out_of_memory(); + return false; + } + err = parse_jobsel(text, &js); + free(text); + if (err != JOBSEL_OK) { + invalid("job id", w, jobsel_strerror(err)); + return false; + } + + switch (js.form) { + case JOBSEL_BARE: + ok = sel_add(s, js.key, false); + /* only an array-aligned id can also be an array (1.2, 1.3) */ + if (ok && (EXTRACT_ARRAY_IDX(js.key) == 0)) { + ok = sel_add(s, js.key, true); + } + break; + case JOBSEL_SINGLE: + case JOBSEL_RANGE: + for (k = js.lo; ok && (k <= js.hi); k++) { + ok = sel_add(s, jobsel_element_id(&js, k), false); + } + break; + } + if (!ok) { + out_of_memory(); + } + return ok; +} + +/* `lines N`: a whole number from 50 to 100000 (1.9, 9.1). */ +static bool parse_lines(xce_repl_trace_settings_t* s, word_t w) +{ + long v = 0; + size_t i; + + if ((w.n == 0) || (w.n > 6)) { + invalid("lines", w, NULL); + return false; + } + for (i = 0; i < w.n; i++) { + if ((w.p[i] < '0') || (w.p[i] > '9')) { + invalid("lines", w, NULL); + return false; + } + v = (v * 10) + (w.p[i] - '0'); + } + if ((v < XCE_TRACE_LINE_BOUND_MIN) || (v > XCE_TRACE_LINE_BOUND_MAX)) { + invalid("lines", w, NULL); + return false; + } + s->ctl.line_bound = (int)v; + return true; +} + +/* `file PATH`: the rest of the line (1.12). */ +static bool parse_file(xce_repl_trace_settings_t* s, const char* rest) +{ + const char* b = rest; + const char* e = rest + strlen(rest); + word_t none = { rest, 0 }; + size_t n; + + while (is_space(*b)) { + b++; + } + while ((e > b) && (is_space(e[-1]) || (e[-1] == '\n') || (e[-1] == '\r'))) { + e--; + } + if (((e - b) >= 2) && ((*b == '"') || (*b == '\'')) && (e[-1] == *b)) { + b++; + e--; + } + n = (size_t)(e - b); + if (n == 0) { + invalid("file", none, NULL); + return false; + } + if (n > TRACE_PATH_MAX) { + fprintf(stdout, + "[sim] trace: invalid file: path is %zu bytes, longer than " + "%d\n", + n, TRACE_PATH_MAX); + return false; + } + s->path = strndup(b, n); + if (s->path == NULL) { + out_of_memory(); + return false; + } + return true; +} + +/* Everything after `on`. Returns the new settings, or NULL after printing + * the error. */ +static xce_repl_trace_settings_t* parse_on(const char* p) +{ + xce_repl_trace_settings_t* s = settings_new(); + word_t w; + const char* before; + + if (s == NULL) { + out_of_memory(); + return NULL; + } + + w = next_word(&p); + if (word_is(w, "all")) { + s->ctl.all = true; + } else if (word_is(w, "job")) { + for (;;) { + before = p; + w = next_word(&p); + if ((w.n == 0) || is_option(w)) { + p = before; + break; + } + if (!parse_sel(s, w)) { + goto fail; + } + } + if (s->ctl.nsel == 0) { + w.n = 0; + invalid("job id", w, NULL); + goto fail; + } + } else { + invalid("target", w, NULL); + goto fail; + } + + for (;;) { + w = next_word(&p); + if (w.n == 0) { + break; + } + if (word_is(w, "level")) { + w = next_word(&p); + if (word_is(w, "summary")) { + s->ctl.level = XCE_TRACE_LEVEL_SUMMARY; + } else if (word_is(w, "full")) { + s->ctl.level = XCE_TRACE_LEVEL_FULL; + } else { + invalid("level", w, NULL); + goto fail; + } + } else if (word_is(w, "lines")) { + if (!parse_lines(s, next_word(&p))) { + goto fail; + } + } else if (word_is(w, "file")) { + if (!parse_file(s, p)) { + goto fail; + } + break; /* the path is the rest of the line */ + } else { + invalid("option", w, NULL); + goto fail; + } + } + return s; + +fail: + repl_trace_settings_unref(s); + return NULL; +} + +/* ---- `trace status` ------------------------------------------------------- */ + +static bool has_whole_array(const xce_sched_trace_ctl_t* ctl, uint64_t key) +{ + int i; + + for (i = 0; i < ctl->nsel; i++) { + if (ctl->sel[i].whole_array && (ctl->sel[i].key == key)) { + return true; + } + } + return false; +} + +static void print_whole_arrays(const xce_sched_trace_ctl_t* ctl, bool sep) +{ + char id[64]; + int i; + + for (i = 0; i < ctl->nsel; i++) { + if (!ctl->sel[i].whole_array) { + continue; + } + if (fmt_jobid(id, sizeof(id), ctl->sel[i].key) < 0) { + snprintf(id, sizeof(id), "%llu", + (unsigned long long)ctl->sel[i].key); + } + fprintf(stdout, "%s%s[*]", sep ? "," : "", id); + sep = true; + } +} + +/* The selection as a compressed jobid listing (repl_fmt_jobid_range), + * then each whole-array selector as [*] (every element, including + * ones created later). A whole-array selector's exact-id twin from the + * same bare id is not listed separately. */ +static void print_selection(const xce_sched_trace_ctl_t* ctl) +{ + uint64_t* ids; + int n = 0; + int i; + size_t sz; + char* buf = NULL; + + if (ctl->all) { + fprintf(stdout, " selection: all jobs\n"); + return; + } + ids = (uint64_t*)malloc(((size_t)ctl->nsel + 1) * sizeof(*ids)); + if (ids == NULL) { + fprintf(stdout, " selection: %d selectors\n", ctl->nsel); + return; + } + for (i = 0; i < ctl->nsel; i++) { + if (!ctl->sel[i].whole_array && !has_whole_array(ctl, ctl->sel[i].key)) { + ids[n++] = ctl->sel[i].key; + } + } + for (sz = 256; sz <= ((size_t)1 << 24); sz *= 2) { + char* b = (char*)realloc(buf, sz); + + if (b == NULL) { + break; + } + buf = b; + if (repl_fmt_jobid_range(buf, sz, ids, n) >= 0) { + fprintf(stdout, " selection: jobs %s", buf); + print_whole_arrays(ctl, n > 0); + fputc('\n', stdout); + free(buf); + free(ids); + return; + } + } + fprintf(stdout, " selection: %d selectors\n", ctl->nsel); + free(buf); + free(ids); +} + +static void print_block(const char* title, const xce_repl_trace_settings_t* s) +{ + fprintf(stdout, "[sim] trace status (%s):\n", title); + if (s == NULL) { + fprintf(stdout, " enabled: no\n"); + return; + } + fprintf(stdout, " enabled: yes\n"); + print_selection(&(s->ctl)); + fprintf(stdout, " level: %s\n", + (s->ctl.level == XCE_TRACE_LEVEL_FULL) ? "full" : "summary"); + fprintf(stdout, " line bound: %d\n", s->ctl.line_bound); + fprintf(stdout, " file: %s\n", (s->path != NULL) ? s->path : "none"); +} + +/* 1.7: the settings in effect (or the next ones when no session runs), any + * replacement waiting for the next session, and stored-trace memory. */ +static void trace_status(const xce_repl_trace_state_t* ts) +{ + if (ts->session_in_progress) { + print_block("in effect for the session in progress", ts->cur); + if (ts->next != ts->cur) { + print_block("replaces at the next session", ts->next); + } + } else { + print_block("next session", ts->next); + } + fprintf(stdout, " stored traces: %zu bytes used of %zu bytes (64 MiB)\n", + repl_trace_store_bytes(ts), XCE_REPL_TRACE_STORE_BOUND); +} + +/* ---- the command ---------------------------------------------------------- */ + +/* `off` and `status` take no arguments. */ +static bool no_more_args(const char* p) +{ + word_t w = next_word(&p); + + if (w.n != 0) { + invalid("argument", w, NULL); + return false; + } + return true; +} + +bool repl_trace_cmd(xce_sched_sim_driver_t* sim, const char* raw_line) +{ + xce_repl_trace_state_t* ts; + xce_repl_trace_settings_t* s; + const char* p = raw_line; + word_t w; + + if ((sim == NULL) || (sim->repl_state == NULL) || (raw_line == NULL)) { + return false; + } + ts = &(sim->repl_state->trace); + + (void)next_word(&p); /* "trace" */ + w = next_word(&p); + if (w.n == 0) { + fprintf(stdout, "[sim] trace: usage: trace on all|job ... " + "[level summary|full] [lines N] [file PATH] | " + "trace off | trace status\n"); + } else if (word_is(w, "on")) { + s = parse_on(p); + if (s != NULL) { + repl_trace_settings_unref(ts->next); + ts->next = s; + fprintf(stdout, + "[sim] trace: enabled; applies from the next session\n"); + } + } else if (word_is(w, "off")) { + if (no_more_args(p)) { + repl_trace_settings_unref(ts->next); + ts->next = NULL; + fprintf(stdout, + "[sim] trace: disabled; applies from the next session\n"); + } + } else if (word_is(w, "status")) { + if (no_more_args(p)) { + trace_status(ts); + } + } else { + invalid("command", w, NULL); + } + return false; +} diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 7ce2bd20..5c4272ae 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -2756,3 +2756,23 @@ sched_trace_store_test = executable( test('scheduler trace store test', sched_trace_store_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) + +# scheduler-sim-decision-trace task 4.3: the `trace` command grammar and the +# trace settings lifecycle (design.md "ReplTraceControl"; Requirements +# 1.1-1.5, 1.7-1.10, 1.12). Every grammar form, every invalid-argument +# class, a path longer than 63 characters, 20 job selectors, and `trace +# status` before, during and after a (simulated) session. Links the trace +# control, the store, the record model, the renderer and the shared REPL +# formatting module; the store's job-table reads are fixture stubs. +sched_repl_trace_cmd_test = executable( + 'sched_repl_trace_cmd_test', + ['test_repl_trace_cmd.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', + '../src/repl_trace_store.c', '../src/repl_trace.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps], + install: false) + +test('scheduler repl trace cmd test', + sched_repl_trace_cmd_test, suite: 'scheduler') diff --git a/src/scheduler/testsuite/test_repl_trace_cmd.c b/src/scheduler/testsuite/test_repl_trace_cmd.c new file mode 100644 index 00000000..f705b72f --- /dev/null +++ b/src/scheduler/testsuite/test_repl_trace_cmd.c @@ -0,0 +1,649 @@ +/* + * test_repl_trace_cmd.c - the `trace` command grammar and the trace + * settings lifecycle (scheduler-sim-decision-trace spec, task 4.3; + * design.md "ReplTraceControl", "Trace settings lifecycle"). + * + * Links the trace control (src/repl_trace.c), the store + * (src/repl_trace_store.c, for the memory line of `trace status`), the + * record model, the renderer and the shared REPL formatting module. The + * store's job-table reads are satisfied by fixture definitions of + * sched_get_job() and repl_job_state_name() (no jobs exist here). + * + * A session in progress is simulated through the state the session hooks + * (task 4.6) will maintain: `cur` takes a reference to `next` and + * `session_in_progress` is set; the end drops `cur` and clears the flag. + * + * Covers: + * - every grammar form: `on all`, `on job SEL...`, `level`, `lines`, + * `file PATH` (bare, quoted, with spaces), `off`, `status` (1.1, 1.2, + * 1.5, 1.8, 1.12); + * - selectors: plain job, element, element range, bare id (traced as a + * plain job and as every element of an array with that id, 1.2, 1.3); + * - every invalid-argument class leaves `next` untouched (1.9); + * - a path longer than 63 characters, a 4095-byte path accepted and a + * 4096-byte path rejected, and 20 job selectors (1.12); + * - commands replace only `next` (1.4, 1.6, 1.10); + * - `trace status` before, during and after a session (1.7). + */ +#define _GNU_SOURCE +#include +#include +#include +#include + +#include + +#include "repl_internal.h" +#include "sched/trace.h" +#include "sim.h" +#include "utils.h" + +#define PLAIN_JOB MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)42) +#define PLAIN_JOB2 MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)43) +#define ARRAY_SEQ ((uint64_t)32768 * 5) +#define ARRAY_BASE MAKE_JOBID(1, 2, 1000, 0x0, ARRAY_SEQ) +#define ELEM(k) \ + MAKE_JOBID(1, 2, 1000, TAG_JOB_ARRAY, ARRAY_SEQ | (uint64_t)(k)) +#define ARRAY2_SEQ ((uint64_t)32768 * 6) +#define ARRAY2_BASE MAKE_JOBID(1, 2, 1000, 0x0, ARRAY2_SEQ) +#define ELEM2(k) \ + MAKE_JOBID(1, 2, 1000, TAG_JOB_ARRAY, ARRAY2_SEQ | (uint64_t)(k)) + +static xce_sched_sim_driver_t g_sim; +static xce_sched_repl_state_t g_rs; + +/* ---- fixture job table (the store's no-trace replies; unused here) ------ */ + +xce_sched_job_t* sched_get_job(xce_sched_core_t* core, uint64_t jobid) +{ + (void)core; + (void)jobid; + return NULL; +} + +const char* repl_job_state_name(uint32_t state) +{ + (void)state; + return "UNKNOWN"; +} + +void setUp(void) +{ + memset(&g_sim, 0, sizeof(g_sim)); + memset(&g_rs, 0, sizeof(g_rs)); + g_rs.units_mode = XCE_REPL_UNITS_BOTH; + g_sim.repl_state = &g_rs; +} + +void tearDown(void) +{ + repl_trace_settings_unref(g_rs.trace.next); + repl_trace_settings_unref(g_rs.trace.cur); + g_rs.trace.next = NULL; + g_rs.trace.cur = NULL; + repl_trace_store_fini(&g_rs.trace); + g_sim.repl_state = NULL; +} + +/* ---- helpers ------------------------------------------------------------- */ + +static char* fmt_id(uint64_t jobid) +{ + static char bufs[8][64]; + static int next = 0; + char* b = bufs[next++ % 8]; + + fmt_jobid(b, 64, jobid); + return b; +} + +/* Runs one raw `trace ...` line with stdout redirected to a temp file and + * returns what it printed (caller frees). */ +static char* run(const char* line) +{ + char tmp[] = "/tmp/xce_trace_cmd_stdout_XXXXXX"; + int fd = mkstemp(tmp); + int saved; + off_t len; + char* buf; + + TEST_ASSERT_TRUE(fd >= 0); + fflush(stdout); + saved = dup(STDOUT_FILENO); + TEST_ASSERT_TRUE(saved >= 0); + TEST_ASSERT_TRUE(dup2(fd, STDOUT_FILENO) >= 0); + + TEST_ASSERT_FALSE(repl_trace_cmd(&g_sim, line)); + + fflush(stdout); + dup2(saved, STDOUT_FILENO); + close(saved); + + len = lseek(fd, 0, SEEK_CUR); + TEST_ASSERT_TRUE(len >= 0); + buf = (char*)calloc(1, (size_t)len + 1); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0); + if (len > 0) { + TEST_ASSERT_TRUE(read(fd, buf, (size_t)len) == len); + } + close(fd); + unlink(tmp); + return buf; +} + +static void assert_text(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 void assert_run(const char* line, const char* want) +{ + char* got = run(line); + + assert_text(want, got); + free(got); +} + +/* Runs a line that must succeed (prints no error) and returns `next`. */ +static xce_repl_trace_settings_t* run_ok(const char* line) +{ + char* got = run(line); + + if (strstr(got, "invalid") != NULL) { + fprintf(stderr, "unexpected error for '%s': %s", line, got); + } + TEST_ASSERT_NULL(strstr(got, "invalid")); + free(got); + return g_rs.trace.next; +} + +static const xce_sched_trace_ctl_t* next_ctl(void) +{ + TEST_ASSERT_NOT_NULL(g_rs.trace.next); + return repl_trace_settings_ctl(g_rs.trace.next); +} + +static const char* next_path(void) +{ + TEST_ASSERT_NOT_NULL(g_rs.trace.next); + return repl_trace_settings_path(g_rs.trace.next); +} + +/* The simulated session hooks (task 4.6 owns the real ones). */ +static void session_begin(void) +{ + g_rs.trace.cur = repl_trace_settings_ref(g_rs.trace.next); + g_rs.trace.session_in_progress = true; +} + +static void session_end(void) +{ + repl_trace_settings_unref(g_rs.trace.cur); + g_rs.trace.cur = NULL; + g_rs.trace.session_in_progress = false; +} + +static char* store_line(void) +{ + static char b[160]; + + snprintf(b, sizeof(b), + " stored traces: %zu bytes used of %zu bytes (64 MiB)\n", + repl_trace_store_bytes(&g_rs.trace), XCE_REPL_TRACE_STORE_BOUND); + return b; +} + +/* ---- grammar ------------------------------------------------------------- */ + +/* 1.1, 1.8, 9.1: `on all` with every default. */ +static void test_on_all_defaults(void) +{ + const xce_sched_trace_ctl_t* ctl; + + assert_run("trace on all", + "[sim] trace: enabled; applies from the next session\n"); + ctl = next_ctl(); + TEST_ASSERT_TRUE(ctl->all); + TEST_ASSERT_EQUAL_INT(0, ctl->nsel); + TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_SUMMARY, ctl->level); + TEST_ASSERT_EQUAL_INT(XCE_TRACE_LINE_BOUND_DEFAULT, ctl->line_bound); + TEST_ASSERT_NULL(next_path()); + TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, PLAIN_JOB)); + TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM(9))); +} + +/* Level, line bound and file, in any order before `file`; extra + * whitespace and tabs. */ +static void test_on_all_options(void) +{ + const xce_sched_trace_ctl_t* ctl; + + run_ok(" trace\ton all level full lines 50 file /tmp/t.log "); + ctl = next_ctl(); + TEST_ASSERT_TRUE(ctl->all); + TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_FULL, ctl->level); + TEST_ASSERT_EQUAL_INT(50, ctl->line_bound); + TEST_ASSERT_EQUAL_STRING("/tmp/t.log", next_path()); + + run_ok("trace on all lines 100000 level summary"); + ctl = next_ctl(); + TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_SUMMARY, ctl->level); + TEST_ASSERT_EQUAL_INT(100000, ctl->line_bound); + TEST_ASSERT_NULL(next_path()); +} + +/* 1.2, 1.3: every selector form. A bare id is a plain job or an array + * that may not exist yet, so it selects both. */ +static void test_on_job_selectors(void) +{ + char line[512]; + const xce_sched_trace_ctl_t* ctl; + + snprintf(line, sizeof(line), "trace on job %s %s[3] %s[5-6] level full", + fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), fmt_id(ARRAY2_BASE)); + run_ok(line); + ctl = next_ctl(); + TEST_ASSERT_FALSE(ctl->all); + TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_FULL, ctl->level); + + /* a bare id that is not array-aligned can only be a plain job + * (parse_jobsel rejects [k] on it), so it adds no whole-array selector */ + TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, PLAIN_JOB)); + TEST_ASSERT_FALSE(sched_trace_ctl_wants( + ctl, MAKE_JOBID(1, 2, 1000, TAG_JOB_ARRAY, (uint64_t)42))); + TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, PLAIN_JOB2)); + /* one element */ + TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM(3))); + TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM(2))); + TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM(4))); + /* an element range */ + TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM2(4))); + TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM2(5))); + TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM2(6))); + TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM2(7))); + + /* an aligned bare id is a plain job or an array that may not exist + * yet: it selects the job and every element, including later ones */ + snprintf(line, sizeof(line), "trace on job %s", fmt_id(ARRAY_BASE)); + run_ok(line); + ctl = next_ctl(); + TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM(1))); + TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM(32767))); + TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ARRAY_BASE)); + TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM2(1))); +} + +/* 1.12: 20 job selectors on one line (more than the tokenizer's 16). */ +static void test_twenty_selectors(void) +{ + char line[2048]; + size_t off; + int i; + const xce_sched_trace_ctl_t* ctl; + + off = (size_t)snprintf(line, sizeof(line), "trace on job"); + for (i = 0; i < 20; i++) { + off += (size_t)snprintf( + line + off, sizeof(line) - off, " %s", + fmt_id(MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)(100 + i)))); + } + snprintf(line + off, sizeof(line) - off, " lines 75"); + run_ok(line); + ctl = next_ctl(); + TEST_ASSERT_EQUAL_INT(75, ctl->line_bound); + for (i = 0; i < 20; i++) { + TEST_ASSERT_TRUE(sched_trace_ctl_wants( + ctl, MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)(100 + i)))); + } + TEST_ASSERT_FALSE(sched_trace_ctl_wants( + ctl, MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)120))); +} + +/* 1.12: the file path is the rest of the line: quotes, spaces, a path + * longer than 63 characters and up to 4095 bytes. */ +static void test_file_paths(void) +{ + char line[8192]; + char path[4097]; + char* got; + + run_ok("trace on all file \"/tmp/a dir/t.log\" "); + TEST_ASSERT_EQUAL_STRING("/tmp/a dir/t.log", next_path()); + run_ok("trace on all file '/tmp/q.log'"); + TEST_ASSERT_EQUAL_STRING("/tmp/q.log", next_path()); + run_ok("trace on all level full file /tmp/x y.log"); + TEST_ASSERT_EQUAL_STRING("/tmp/x y.log", next_path()); + + /* 200 characters */ + memset(path, 'p', 200); + path[0] = '/'; + path[200] = '\0'; + snprintf(line, sizeof(line), "trace on all file %s", path); + run_ok(line); + TEST_ASSERT_EQUAL_STRING(path, next_path()); + + /* 4095 bytes accepted, quoted */ + memset(path, 'p', 4095); + path[0] = '/'; + path[4095] = '\0'; + snprintf(line, sizeof(line), "trace on all file \"%s\"", path); + run_ok(line); + TEST_ASSERT_EQUAL_STRING(path, next_path()); + + /* 4096 bytes rejected, and the 4095-byte setting stays */ + memset(path, 'p', 4096); + path[0] = '/'; + path[4096] = '\0'; + snprintf(line, sizeof(line), "trace on all lines 60 file %s", path); + got = run(line); + assert_text("[sim] trace: invalid file: path is 4096 bytes, longer than " + "4095\n", + got); + free(got); + TEST_ASSERT_EQUAL_INT(4095, (int)strlen(next_path())); + TEST_ASSERT_EQUAL_INT(XCE_TRACE_LINE_BOUND_DEFAULT, next_ctl()->line_bound); +} + +/* 1.5: `off` clears next. */ +static void test_off(void) +{ + run_ok("trace on all"); + assert_run("trace off", + "[sim] trace: disabled; applies from the next session\n"); + TEST_ASSERT_NULL(g_rs.trace.next); + assert_run(" trace off ", + "[sim] trace: disabled; applies from the next session\n"); + TEST_ASSERT_NULL(g_rs.trace.next); +} + +/* ---- invalid arguments (1.9) --------------------------------------------- */ + +static void assert_rejected(const char* line, const char* want) +{ + xce_repl_trace_settings_t* before = g_rs.trace.next; + + assert_run(line, want); + TEST_ASSERT_TRUE(before == g_rs.trace.next); +} + +static void test_invalid_arguments_leave_settings(void) +{ + char line[256]; + char want[512]; + + run_ok("trace on all level full lines 70 file /tmp/keep.log"); + + assert_rejected("trace", "[sim] trace: usage: trace on all|job " + "... [level summary|full] [lines N] " + "[file PATH] | trace off | trace status\n"); + assert_rejected("trace bogus", "[sim] trace: invalid command: bogus\n"); + assert_rejected("trace on", "[sim] trace: invalid target: (missing)\n"); + assert_rejected("trace on everyone", + "[sim] trace: invalid target: everyone\n"); + assert_rejected("trace on job", + "[sim] trace: invalid job id: (missing)\n"); + assert_rejected("trace on job level full", + "[sim] trace: invalid job id: (missing)\n"); + snprintf(want, sizeof(want), "[sim] trace: invalid job id: 1-2-x (%s)\n", + jobsel_strerror(JOBSEL_ESYNTAX)); + assert_rejected("trace on job 1-2-x", want); + snprintf(line, sizeof(line), "trace on job %s 7", fmt_id(PLAIN_JOB)); + snprintf(want, sizeof(want), "[sim] trace: invalid job id: 7 (%s)\n", + jobsel_strerror(JOBSEL_ESYNTAX)); + assert_rejected(line, want); + assert_rejected("trace on all level verbose", + "[sim] trace: invalid level: verbose\n"); + assert_rejected("trace on all level", + "[sim] trace: invalid level: (missing)\n"); + assert_rejected("trace on all lines 49", + "[sim] trace: invalid lines: 49\n"); + assert_rejected("trace on all lines 100001", + "[sim] trace: invalid lines: 100001\n"); + assert_rejected("trace on all lines -60", + "[sim] trace: invalid lines: -60\n"); + assert_rejected("trace on all lines 1e3", + "[sim] trace: invalid lines: 1e3\n"); + assert_rejected("trace on all lines 99999999999999999999999", + "[sim] trace: invalid lines: 99999999999999999999999\n"); + assert_rejected("trace on all lines", + "[sim] trace: invalid lines: (missing)\n"); + assert_rejected("trace on all colour red", + "[sim] trace: invalid option: colour\n"); + snprintf(line, sizeof(line), "trace on all job %s", fmt_id(PLAIN_JOB)); + assert_rejected(line, "[sim] trace: invalid option: job\n"); + assert_rejected("trace on all file", + "[sim] trace: invalid file: (missing)\n"); + assert_rejected("trace on all file \"\"", + "[sim] trace: invalid file: (missing)\n"); + assert_rejected("trace off now", "[sim] trace: invalid argument: now\n"); + assert_rejected("trace status x", "[sim] trace: invalid argument: x\n"); + + TEST_ASSERT_TRUE(next_ctl()->all); + TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_FULL, next_ctl()->level); + TEST_ASSERT_EQUAL_INT(70, next_ctl()->line_bound); + TEST_ASSERT_EQUAL_STRING("/tmp/keep.log", next_path()); +} + +/* ---- settings lifecycle and status (1.4, 1.6, 1.7, 1.10) ---------------- */ + +static void test_status_before_any_command(void) +{ + char want[512]; + + snprintf(want, sizeof(want), + "[sim] trace status (next session):\n" + " enabled: no\n" + "%s", + store_line()); + assert_run("trace status", want); +} + +static void test_status_next_settings(void) +{ + char want[1024]; + char line[256]; + + snprintf(line, sizeof(line), + "trace on job %s %s[1-3] %s level full lines 300 file /tmp/s.log", + fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), fmt_id(PLAIN_JOB2)); + run_ok(line); + snprintf(want, sizeof(want), + "[sim] trace status (next session):\n" + " enabled: yes\n" + " selection: jobs %s,%s[1-3],%s\n" + " level: full\n" + " line bound: 300\n" + " file: /tmp/s.log\n" + "%s", + fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), fmt_id(PLAIN_JOB2), + store_line()); + assert_run(" trace status ", want); +} + +/* A bare array id selects every element, including later ones; status + * shows it as [*] rather than as a plain job id. */ +static void test_status_whole_array_selection(void) +{ + char want[1024]; + char line[256]; + + snprintf(line, sizeof(line), "trace on job %s %s", fmt_id(PLAIN_JOB), + fmt_id(ARRAY_BASE)); + run_ok(line); + snprintf(want, sizeof(want), + "[sim] trace status (next session):\n" + " enabled: yes\n" + " selection: jobs %s,%s[*]\n" + " level: summary\n" + " line bound: 1000\n" + " file: none\n" + "%s", + fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), store_line()); + assert_run("trace status", want); +} + +/* A command during a session replaces only next; cur keeps the settings + * the session started with until the session ends. */ +static void test_status_before_during_after_session(void) +{ + char want[1024]; + char line[256]; + xce_repl_trace_settings_t* started; + xce_sched_trace_rec_t* rec; + xce_sched_trace_ctl_t rctl; + + /* a stored trace so the memory line is non-zero */ + memset(&rctl, 0, sizeof(rctl)); + rctl.all = true; + rctl.line_bound = 1000; + rec = sched_trace_rec_new(&rctl, PLAIN_JOB, XCE_TRACE_REC_NOT_FOUND); + TEST_ASSERT_NOT_NULL(rec); + repl_trace_store_put(&g_rs.trace, rec); + TEST_ASSERT_TRUE(repl_trace_store_bytes(&g_rs.trace) > 0); + + /* before */ + run_ok("trace on all"); + snprintf(want, sizeof(want), + "[sim] trace status (next session):\n" + " enabled: yes\n" + " selection: all jobs\n" + " level: summary\n" + " line bound: 1000\n" + " file: none\n" + "%s", + store_line()); + assert_run("trace status", want); + + /* during, unchanged: one block */ + session_begin(); + started = g_rs.trace.cur; + snprintf(want, sizeof(want), + "[sim] trace status (in effect for the session in progress):\n" + " enabled: yes\n" + " selection: all jobs\n" + " level: summary\n" + " line bound: 1000\n" + " file: none\n" + "%s", + store_line()); + assert_run("trace status", want); + + /* during, replaced: cur is untouched, the replacement is shown */ + snprintf(line, sizeof(line), "trace on job %s level full file /tmp/n.log", + fmt_id(PLAIN_JOB)); + run_ok(line); + TEST_ASSERT_TRUE(started == g_rs.trace.cur); + TEST_ASSERT_TRUE(g_rs.trace.cur != g_rs.trace.next); + TEST_ASSERT_TRUE(repl_trace_settings_ctl(g_rs.trace.cur)->all); + snprintf(want, sizeof(want), + "[sim] trace status (in effect for the session in progress):\n" + " enabled: yes\n" + " selection: all jobs\n" + " level: summary\n" + " line bound: 1000\n" + " file: none\n" + "[sim] trace status (replaces at the next session):\n" + " enabled: yes\n" + " selection: jobs %s\n" + " level: full\n" + " line bound: 1000\n" + " file: /tmp/n.log\n" + "%s", + fmt_id(PLAIN_JOB), store_line()); + assert_run("trace status", want); + + /* during, disabled */ + run_ok("trace off"); + TEST_ASSERT_TRUE(started == g_rs.trace.cur); + snprintf(want, sizeof(want), + "[sim] trace status (in effect for the session in progress):\n" + " enabled: yes\n" + " selection: all jobs\n" + " level: summary\n" + " line bound: 1000\n" + " file: none\n" + "[sim] trace status (replaces at the next session):\n" + " enabled: no\n" + "%s", + store_line()); + assert_run("trace status", want); + + /* after: the session's settings are released, next is shown alone */ + run_ok("trace on all lines 500"); + session_end(); + snprintf(want, sizeof(want), + "[sim] trace status (next session):\n" + " enabled: yes\n" + " selection: all jobs\n" + " level: summary\n" + " line bound: 500\n" + " file: none\n" + "%s", + store_line()); + assert_run("trace status", want); +} + +/* A session that started with tracing off, then enabled mid-session. */ +static void test_status_session_started_off(void) +{ + char want[512]; + + session_begin(); + TEST_ASSERT_NULL(g_rs.trace.cur); + run_ok("trace on all level full"); + snprintf(want, sizeof(want), + "[sim] trace status (in effect for the session in progress):\n" + " enabled: no\n" + "[sim] trace status (replaces at the next session):\n" + " enabled: yes\n" + " selection: all jobs\n" + " level: full\n" + " line bound: 1000\n" + " file: none\n" + "%s", + store_line()); + assert_run("trace status", want); + session_end(); +} + +/* Reference counting: NULL-safe, and a settings object outlives a + * replacement while another reference holds it. */ +static void test_settings_refcount(void) +{ + xce_repl_trace_settings_t* s; + + TEST_ASSERT_NULL(repl_trace_settings_ref(NULL)); + repl_trace_settings_unref(NULL); + + s = run_ok("trace on all lines 123 file /tmp/r.log"); + TEST_ASSERT_TRUE(repl_trace_settings_ref(s) == s); + run_ok("trace off"); + /* still alive through the extra reference (ASAN would flag a free) */ + TEST_ASSERT_EQUAL_INT(123, repl_trace_settings_ctl(s)->line_bound); + TEST_ASSERT_EQUAL_STRING("/tmp/r.log", repl_trace_settings_path(s)); + repl_trace_settings_unref(s); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_on_all_defaults); + RUN_TEST(test_on_all_options); + RUN_TEST(test_on_job_selectors); + RUN_TEST(test_twenty_selectors); + RUN_TEST(test_file_paths); + RUN_TEST(test_off); + RUN_TEST(test_invalid_arguments_leave_settings); + RUN_TEST(test_status_before_any_command); + RUN_TEST(test_status_next_settings); + RUN_TEST(test_status_whole_array_selection); + RUN_TEST(test_status_before_during_after_session); + RUN_TEST(test_status_session_started_off); + RUN_TEST(test_settings_refcount); + return UNITY_END(); +}