feat(scheduler-sim-decision-trace): parse the trace command and manage trace settings

Task 4.3: trace on all|job SEL... [level] [lines N] [file PATH], off and
status, parsed from the raw line; invalid arguments are named and leave
the settings unchanged; refcounted next/current settings; status shows
the settings in effect or pending, whole arrays as <base>[*], and stored
trace memory against its bound.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-08 14:05:36 +00:00
parent 73416e3e91
commit aa4c43c897
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
6 changed files with 1237 additions and 1 deletions

View file

@ -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: <reason>". 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 `<base>[*]`. 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.

View file

@ -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',

View file

@ -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 <what>:
* <arg>` 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

View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <what>: <arg>`, "(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 <base>[*] (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 <jobid> ... "
"[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;
}

View file

@ -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')

View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#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 <jobid> "
"... [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 <base>[*] 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();
}