feat(scheduler-sim-decision-trace): wire the trace commands into the REPL

Task 5.2: trace is routed with the raw line and info trace through info;
help gains a Trace section; set output rejects open trace files; quit
and EOF shut tracing down; the continue-mode rollup reports a traced
round on its own; repl.h gains the interactive-only trace gates. The
baseline transcript changes only in the help and info-usage lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-08 15:15:04 +00:00
parent 8931833132
commit 6bac16812c
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
9 changed files with 820 additions and 3 deletions

View file

@ -276,7 +276,7 @@
- Done when all of the following pass under ASAN: a full scheduler build including the scheduler binary, the full scheduler test suite, and the simulation isolation test.
- _Requirements: 10.5, 11.3_
- [ ] 5.2 Wire the trace commands into the REPL
- [x] 5.2 Wire the trace commands into the REPL
- Route `trace` with the raw line, and route `info trace`.
- Help gains a trailing Trace section, and the info subcommand list and info usage line include `info trace`.
- `set output` rejects an open trace file. Quit and end of input shut tracing down.
@ -371,3 +371,4 @@
- 4.5: the trace FILE* and its dev/ino belong to the refcounted settings, and the last unref flushes and closes it. New functions in repl_internal.h: `repl_trace_settings_file()`, `repl_trace_file_conflicts(sim, path)` (5.2 wires it into `set output`) and `repl_trace_shutdown(sim)` (5.2 calls it on quit/EOF). Shutdown clears core.trace_ctl, releases cur/next, then calls store_fini, which memsets the trace state, so keep that order. 4.6 must emit only through `cur`'s file while it still holds `cur`, and flush after every record, because two append streams may share one file.
- 4.6: the session hooks and emitter are in repl_trace_emit.c: `repl_trace_session_begin/end(sim)` and `repl_trace_on_decision/on_trace_only(sim, d)`, declared in repl_internal.h. `sim_print_decision` is now exported from sim.h. Session begin stamps `session_no = rs->session_count`, which sim_loop.c increments in `xce_repl_check_begin_round` before `xce_core_begin_session`. 5.3 must call session_begin after a successful begin-session and before signal_go/reconciliation, and session_end after the decision stream ends. Emission moves the record to the store (d->trace=NULL); write errors are reported once per session. policy_name is not stamped; capture falls back to the core picker. repl_trace_emit.c is not yet in sched_sim_isolation_lib or its scan (5.1).
- 5.1: all 26 tests that include repl.c/sim_loop.c now `#include` the five REPL trace sources (repl_trace_render.c, repl_trace_render_outcome.c, repl_trace_store.c, repl_trace.c, repl_trace_emit.c) and the four core trace sources, exactly once. sched_sim_isolation_lib carries all nine trace sources. test_sim_isolation.c's `test_sim_trace_repl_sources_are_isolated` scans the REPL trace files plus repl_trace_render.h; stdio is allowed there, controller symbols are not. A new REPL trace file must be added to the 26 tests, the lib and the scan. test_sim_startup.c includes only sim.c, so if sim.c starts calling trace code it needs the sources too.
- 5.2: repl.h now has the interactive-only gates `xce_repl_trace_session_begin/end(sim)` and `xce_repl_check_trace/xce_repl_check_trace_only(sim, d)`; the two check gates also no-op when d->trace is NULL. 5.3 calls these from sim_loop.c. `trace` is routed with the raw line, `info trace` is in usage and help, `set output` checks `repl_trace_file_conflicts` first, and quit/EOF call `repl_trace_shutdown`. The continue-mode rollup prints a traced round on its own via `rs->trace.round_traced`. The baseline transcript changed only in the help and info-usage lines; regenerating also rewrites the .replay fixtures' wall-clock fields, so restore those. Pre-existing: repl_count_jobs (repl.c) never destroys its iterator, leaking 16 B per continue round; the rollup tests run with detect_leaks=0.

View file

@ -154,4 +154,58 @@ static inline void xce_repl_check_replay_complete(xce_sched_sim_driver_t* sim)
}
}
/*
* Decision trace gates (scheduler-sim-decision-trace spec, task 5.2;
* design.md "SimIntegration"; Requirements 7.1-7.3, 11.3). The entry
* points live in repl_trace_emit.c and are declared here, like the
* xce_repl_on_* hooks above, so this header stays self-contained. Each
* gate forwards only when the REPL is interactive, so a non-interactive
* simulation never publishes trace settings or prints a trace (11.3):
* - xce_repl_trace_session_begin: after a successful core begin-session,
* before reconciliation; makes the next settings current and
* publishes them to the scheduling core.
* - xce_repl_trace_session_end: after the decision stream ends;
* withdraws the published settings.
* - xce_repl_check_trace: a decision carrying a trace record, after the
* echo gate and before the breakpoint check; takes d->trace.
* - xce_repl_check_trace_only: an XCE_DECISION_TRACE decision (a traced
* selection with no decision), handled before anything else.
*/
extern void repl_trace_session_begin(xce_sched_sim_driver_t* sim);
extern void repl_trace_session_end(xce_sched_sim_driver_t* sim);
extern void repl_trace_on_decision(xce_sched_sim_driver_t* sim,
xce_decision_t* d);
extern void repl_trace_on_trace_only(xce_sched_sim_driver_t* sim,
xce_decision_t* d);
static inline void xce_repl_trace_session_begin(xce_sched_sim_driver_t* sim)
{
if (sim->interactive) {
repl_trace_session_begin(sim);
}
}
static inline void xce_repl_trace_session_end(xce_sched_sim_driver_t* sim)
{
if (sim->interactive) {
repl_trace_session_end(sim);
}
}
static inline void xce_repl_check_trace(xce_sched_sim_driver_t* sim,
xce_decision_t* d)
{
if (sim->interactive && (d->trace != NULL)) {
repl_trace_on_decision(sim, d);
}
}
static inline void xce_repl_check_trace_only(xce_sched_sim_driver_t* sim,
xce_decision_t* d)
{
if (sim->interactive && (d->trace != NULL)) {
repl_trace_on_trace_only(sim, d);
}
}
#endif

View file

@ -304,6 +304,8 @@ static void repl_print_help(void)
fprintf(stdout, FIELD_FMT, "info config", "Show sim parameters.");
fprintf(stdout, FIELD_FMT, "info limit <name>|all",
"Show limit setting(s) and usage.");
fprintf(stdout, FIELD_FMT, "info trace <jobid>",
"Re-print a job's latest decision trace.");
fprintf(stdout, "\nSnapshot & Parameters:\n");
fprintf(stdout, FIELD_FMT, "snapshot [FILE]",
@ -315,6 +317,25 @@ static void repl_print_help(void)
fprintf(stdout, FIELD_FMT, "set units raw|human|both",
"Show resource values raw, human-scaled, or both (default: "
"both).");
/* scheduler-sim-decision-trace (1.11): settings apply from the next
* session; the option lines below belong to both `trace on` forms. */
fprintf(stdout, "\nTrace:\n");
fprintf(stdout, FIELD_FMT, "trace on all [OPTIONS]",
"Trace every job's decisions.");
fprintf(stdout, FIELD_FMT, "trace on job <jobid> ... [OPTIONS]",
"Trace the listed jobs (id, id[k] or array id).");
fprintf(stdout, FIELD_FMT, " level summary|full",
"Host detail: counts per position or every host (default: "
"summary).");
fprintf(stdout, FIELD_FMT, " lines N",
"Line bound per trace, 50-100000 (default: 1000).");
fprintf(stdout, FIELD_FMT, " file PATH",
"Also append each trace to PATH (last option).");
fprintf(stdout, FIELD_FMT, "trace off",
"Stop tracing from the next session.");
fprintf(stdout, FIELD_FMT, "trace status",
"Show trace settings and stored-trace memory.");
}
/*
@ -329,6 +350,7 @@ static void repl_print_help(void)
*/
static void repl_cmd_quit(xce_sched_sim_driver_t* sim)
{
repl_trace_shutdown(sim); /* flush and close trace files (8.6) */
if (sim->record_file != NULL) {
fflush(sim->record_file);
fclose(sim->record_file);
@ -458,6 +480,9 @@ static bool repl_dispatch(xce_sched_sim_driver_t* sim, const char* line)
if (strcmp(cmd, "set") == 0) {
return repl_cmd_set(sim, line);
}
if (strcmp(cmd, "trace") == 0) {
return repl_trace_cmd(sim, line); /* raw line: no token limits */
}
fprintf(stdout, "[sim] unknown command: '%s' (type 'help' for a list)\n", cmd);

View file

@ -1725,7 +1725,7 @@ bool repl_cmd_info(xce_sched_sim_driver_t* sim, const char* line)
if (ntok < 2) {
fprintf(stdout, "[sim] info: usage: info <break|session|config|job|jobs|queue|"
"worker|resource|summary|limit> [arg]\n");
"worker|resource|summary|limit|trace> [arg]\n");
return false;
}
@ -1769,6 +1769,8 @@ bool repl_cmd_info(xce_sched_sim_driver_t* sim, const char* line)
} else {
repl_info_limit(sim, tokens[2]);
}
} else if (strcmp(tokens[1], "trace") == 0) {
repl_cmd_info_trace(sim, tokens, ntok);
} else {
fprintf(stdout, "[sim] info: unrecognized subcommand '%s'\n", tokens[1]);
}

View file

@ -245,6 +245,11 @@ static bool repl_set_output_file(xce_sched_sim_driver_t* sim, const char* path)
uint64_t ts;
int rc;
if (repl_trace_file_conflicts(sim, path)) {
fprintf(stdout, "[sim] set output: %s is an open trace file\n", path);
return false; /* 8.4: the output file stays as it is */
}
if (sim->record_file != NULL) {
fflush(sim->record_file);
fclose(sim->record_file);

View file

@ -307,6 +307,22 @@ void repl_round_rollup_process(xce_sched_sim_driver_t* sim)
return;
}
if (rs->trace.round_traced) {
/* scheduler-sim-decision-trace (7.3, 7.4): a traced round is
* reported on its own and never starts or extends a range. The
* flush is a no-op when emission already flushed, and covers a
* round traced only in step mode. `idle` follows the flush rule,
* so a pend-only round is not idle. */
repl_round_collapse_flush(sim);
repl_round_emit(round_no, round_no, round_ts, round_ts,
dispatch_count, preempt_count, pend_jobids,
n_pend_jobids,
(dispatch_count == 0) && (preempt_count == 0)
&& (n_pend_jobids == 0));
free(pend_jobids);
return;
}
if (rs->collapse_active
&& (rs->collapse_dispatch_count == dispatch_count)
&& (rs->collapse_preempt_count == preempt_count)

View file

@ -25,13 +25,23 @@ State Inspection:
info summary Quick overview.
info config Show sim parameters.
info limit <name>|all Show limit setting(s) and usage.
info trace <jobid> Re-print a job's latest decision trace.
Snapshot & Parameters:
snapshot [FILE] Write a standalone snapshot.
set output <FILE>|off Start/stop writing decisions.
set verify on|off Toggle decision verification.
set units raw|human|both Show resource values raw, human-scaled, or both (default: both).
[sim] info: usage: info <break|session|config|job|jobs|queue|worker|resource|summary|limit> [arg]
Trace:
trace on all [OPTIONS] Trace every job's decisions.
trace on job <jobid> ... [OPTIONS] Trace the listed jobs (id, id[k] or array id).
level summary|full Host detail: counts per position or every host (default: summary).
lines N Line bound per trace, 50-100000 (default: 1000).
file PATH Also append each trace to PATH (last option).
trace off Stop tracing from the next session.
trace status Show trace settings and stored-trace memory.
[sim] info: usage: info <break|session|config|job|jobs|queue|worker|resource|summary|limit|trace> [arg]
[sim] config: output=(active, path unknown) verify=on events_processed=0 current_ts=1000 (1970-01-01T00:00:00Z) sessions=0
[sim] t=1001 (1970-01-01T00:00:00Z) session=1
[sim] event: WORKERS_PUSH nworkers=2 names=[w01,w02]

View file

@ -2841,3 +2841,30 @@ sched_repl_trace_emit_test = executable(
test('scheduler repl trace emit test',
sched_repl_trace_emit_test, suite: 'scheduler')
# scheduler-sim-decision-trace task 5.2: the trace commands wired into the
# REPL (design.md "ReplIntegration", "SimIntegration" (repl.h gates),
# "ReplTraceControl"; Requirements 1.11, 7.3, 7.4, 8.4, 8.6, 10.4, 10.5).
# The help Trace section and alignment, `trace`/`info trace` routing,
# `set output` rejecting an open trace file, quit/EOF shutdown, the
# traced-round rollup and the interactive-only gates. Includes repl.c (for
# its static help, dispatch and quit) and links every other scheduler
# source except main.c. Leak detection is off like the other rollup tests:
# repl_count_jobs() (repl.c) never destroys its iterator.
sched_repl_trace_wiring_test_srcs = ['test_repl_trace_wiring.c']
foreach f : scheduler_srcs
if f != 'src/main.c' and f != 'src/repl.c'
sched_repl_trace_wiring_test_srcs += ['../' + f]
endif
endforeach
sched_repl_trace_wiring_test = executable(
'sched_repl_trace_wiring_test',
sched_repl_trace_wiring_test_srcs,
include_directories:[incdir],
dependencies: [scheduler_deps, unity_dep],
install: false)
test('scheduler repl trace wiring test',
sched_repl_trace_wiring_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -0,0 +1,677 @@
/*
* test_repl_trace_wiring.c - the trace commands wired into the REPL
* (scheduler-sim-decision-trace spec, task 5.2; design.md "ReplIntegration",
* "SimIntegration" (repl.h gates), "ReplTraceControl", "System Flows";
* Requirements 1.11, 7.3, 7.4, 8.4, 8.6, 10.4, 10.5).
*
* Links every scheduler source except main.c and repl.c, and includes
* repl.c here so its static help printer, dispatcher and quit path are
* reachable. The sim driver and REPL state are zeroed fixtures with a small
* fixture job table; nothing is started.
*
* Covers:
* - help: a trailing Trace section listing the trace commands, `info
* trace` among the State Inspection lines, one description column for
* every line including the new ones (1.11);
* - routing: `trace` gets the raw line, so a command longer than the
* tokenizer's 16 tokens / 64 characters is not truncated; `info trace`
* reaches the store and the `info` usage line lists it (1.11, 10.5);
* - `set output` rejects a trace file still open for writing, including
* one kept open by the session in progress and a hard link to it, and
* leaves the output file unchanged (8.4);
* - quit and end of input shut tracing down (8.6);
* - the continue-mode rollup reports a traced round on its own, with the
* range-flush idle rule, and never starts a collapsed range from it
* (7.3, 7.4);
* - the repl.h gates forward only when the REPL is interactive.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <unity/unity.h>
#include "kvmgr.h"
#include "sched/trace.h"
#include "utils.h"
#include "../src/repl.c"
#define PEND_JOB MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)42)
static xce_sched_sim_driver_t g_sim;
static xce_sched_repl_state_t g_rs;
static xce_sched_job_t g_pend_job;
static char g_dir[64];
static char g_cwd[4096];
static int g_report_fd = -1;
/* stdout capture */
static int g_cap_fd = -1;
static int g_cap_saved = -1;
static char g_cap_path[64];
void setUp(void)
{
kvmgr_params_t p = { .key_type = KVMGR_KEY_INT,
.nslots = 59,
.delta = 16,
.datakiller = NULL };
memset(&g_sim, 0, sizeof(g_sim));
memset(&g_rs, 0, sizeof(g_rs));
g_rs.units_mode = XCE_REPL_UNITS_BOTH;
g_rs.mode = XCE_REPL_MODE_STEP;
g_sim.interactive = true;
g_sim.repl_state = &g_rs;
g_sim.core.jobs = makeInstance(&xkvmgr_type, &p);
TEST_ASSERT_NOT_NULL(g_sim.core.jobs);
TEST_ASSERT_NOT_NULL(getcwd(g_cwd, sizeof(g_cwd)));
snprintf(g_dir, sizeof(g_dir), "/tmp/xce_trace_wiring_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(g_dir));
TEST_ASSERT_EQUAL_INT(0, chdir(g_dir));
}
static void fixture_fini(void)
{
repl_trace_shutdown(&g_sim);
free(g_rs.collapse_pend_jobids);
g_rs.collapse_pend_jobids = NULL;
if (g_sim.record_file != NULL) {
fclose(g_sim.record_file);
g_sim.record_file = NULL;
}
if (g_sim.core.jobs != NULL) {
destroy(g_sim.core.jobs);
g_sim.core.jobs = NULL;
}
}
void tearDown(void)
{
fixture_fini();
g_sim.repl_state = NULL;
unlink("a.trace");
unlink("b.trace");
unlink("rec.replay");
TEST_ASSERT_EQUAL_INT(0, chdir(g_cwd));
rmdir(g_dir);
}
/* ---- helpers ------------------------------------------------------------- */
static void cap_begin(void)
{
snprintf(g_cap_path, sizeof(g_cap_path), "/tmp/xce_trace_wire_out_XXXXXX");
g_cap_fd = mkstemp(g_cap_path);
TEST_ASSERT_TRUE(g_cap_fd >= 0);
fflush(stdout);
g_cap_saved = dup(STDOUT_FILENO);
TEST_ASSERT_TRUE(g_cap_saved >= 0);
TEST_ASSERT_TRUE(dup2(g_cap_fd, STDOUT_FILENO) >= 0);
}
/* Ends the capture and returns what was printed (caller frees). */
static char* cap_end(void)
{
off_t len;
char* buf;
fflush(stdout);
dup2(g_cap_saved, STDOUT_FILENO);
close(g_cap_saved);
len = lseek(g_cap_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(g_cap_fd, 0, SEEK_SET) == 0);
if (len > 0) {
TEST_ASSERT_TRUE(read(g_cap_fd, buf, (size_t)len) == len);
}
close(g_cap_fd);
unlink(g_cap_path);
return buf;
}
/* Dispatches `line` as the REPL would and returns its output. */
static char* dispatch(const char* line)
{
cap_begin();
TEST_ASSERT_FALSE(repl_dispatch(&g_sim, line));
return cap_end();
}
static void dispatch_quiet(const char* line) { free(dispatch(line)); }
static char* help_text(void)
{
cap_begin();
repl_print_help();
return cap_end();
}
static void write_file(const char* path, const char* text)
{
FILE* f = fopen(path, "w");
TEST_ASSERT_NOT_NULL(f);
fputs(text, f);
fclose(f);
}
static char* read_file(const char* path)
{
FILE* f = fopen(path, "r");
char* buf;
long len;
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_EQUAL_INT(0, fseek(f, 0, SEEK_END));
len = ftell(f);
TEST_ASSERT_TRUE(len >= 0);
rewind(f);
buf = (char*)calloc(1, (size_t)len + 1);
TEST_ASSERT_NOT_NULL(buf);
if (len > 0) {
TEST_ASSERT_EQUAL_size_t((size_t)len, fread(buf, 1, (size_t)len, f));
}
fclose(f);
return buf;
}
/* The column at which the description of the help line whose command field
* is exactly `field` begins (the field starts the line after indentation). */
static int desc_column(const char* text, const char* field)
{
size_t flen = strlen(field);
const char* p = text;
while ((p = strstr(p, field)) != NULL) {
const char* ls = p;
const char* d = p + flen;
while ((ls > text) && (*(ls - 1) == ' ')) {
ls--;
}
if (((ls == text) || (*(ls - 1) == '\n')) && (*d == ' ')) {
while ((ls > text) && (*(ls - 1) != '\n')) {
ls--;
}
while (*d == ' ') {
d++;
}
return (int)(d - ls);
}
p += flen;
}
TEST_FAIL_MESSAGE(field);
return -1;
}
/* A traced decision record for `jobid` (DECISION kind, no entries). */
static xce_sched_trace_rec_t* new_rec(uint64_t jobid)
{
xce_sched_trace_ctl_t ctl;
xce_sched_trace_rec_t* rec;
memset(&ctl, 0, sizeof(ctl));
ctl.all = true;
ctl.level = XCE_TRACE_LEVEL_SUMMARY;
ctl.line_bound = 1000;
ctl.session_no = 3;
ctl.position = 1;
rec = sched_trace_rec_new(&ctl, jobid, XCE_TRACE_REC_DECISION);
TEST_ASSERT_NOT_NULL(rec);
return rec;
}
typedef struct {
xce_decision_t d;
xce_job_alloc_t alloc;
} fixture_decision_t;
static void dispatch_of(fixture_decision_t* f, uint64_t jobid)
{
memset(f, 0, sizeof(*f));
f->alloc.jobid = jobid;
f->d.op = XCE_DECISION_DISPATCH;
f->d.u.alloc = &(f->alloc);
f->d.trace = new_rec(jobid);
}
static void add_pend_job(void)
{
memset(&g_pend_job, 0, sizeof(g_pend_job));
g_pend_job.jobid = PEND_JOB;
g_pend_job.state = JOB_STAT_PEND;
TEST_ASSERT_TRUE(
key_add(g_sim.core.jobs, (void*)g_pend_job.jobid, &g_pend_job) >= 0);
}
/* An active, non-idle collapsed range over rounds 3-4 with one dispatch
* per round and no pend job. */
static void open_dispatch_range(void)
{
g_rs.mode = XCE_REPL_MODE_CONTINUE;
g_rs.collapse_active = true;
g_rs.collapse_start_round = 3;
g_rs.collapse_end_round = 4;
g_rs.collapse_dispatch_count = 1;
}
/* Runs the round-boundary rollup for round 5 with the given counts. */
static char* rollup_round5(int ndispatch, bool traced)
{
g_rs.session_count = 5;
g_rs.last_n_dispatch = ndispatch;
g_rs.last_n_preempt = 0;
g_rs.trace.round_traced = traced;
cap_begin();
repl_round_rollup_process(&g_sim);
return cap_end();
}
/* ---- help (1.11) --------------------------------------------------------- */
static void test_help_trace_section_is_last_and_lists_trace_commands(void)
{
char* h = help_text();
char* snap = strstr(h, "\nSnapshot & Parameters:\n");
char* trace = strstr(h, "\nTrace:\n");
const char* cmds[] = { "trace on all", "trace on job <jobid>",
"trace off", "trace status" };
size_t i;
TEST_ASSERT_NOT_NULL(snap);
TEST_ASSERT_NOT_NULL_MESSAGE(trace, "help must have a Trace section");
TEST_ASSERT_TRUE_MESSAGE(snap < trace, "Trace must follow Snapshot");
TEST_ASSERT_NULL_MESSAGE(strstr(trace + 1, "\n\n"),
"Trace must be the last section");
for (i = 0; i < sizeof(cmds) / sizeof(cmds[0]); i++) {
char* pos = strstr(h, cmds[i]);
TEST_ASSERT_NOT_NULL_MESSAGE(pos, cmds[i]);
TEST_ASSERT_TRUE_MESSAGE(pos > trace, cmds[i]);
}
TEST_ASSERT_NOT_NULL(strstr(trace, "level summary|full"));
TEST_ASSERT_NOT_NULL(strstr(trace, "lines N"));
TEST_ASSERT_NOT_NULL(strstr(trace, "file PATH"));
free(h);
}
static void test_help_info_trace_in_state_inspection(void)
{
char* h = help_text();
char* state = strstr(h, "\nState Inspection:\n");
char* snap = strstr(h, "\nSnapshot & Parameters:\n");
char* info = strstr(h, "info trace <jobid>");
TEST_ASSERT_NOT_NULL(state);
TEST_ASSERT_NOT_NULL(snap);
TEST_ASSERT_NOT_NULL_MESSAGE(info, "help must list `info trace`");
TEST_ASSERT_TRUE((state < info) && (info < snap));
free(h);
}
static void test_help_one_description_column_with_trace_lines(void)
{
char* h = help_text();
const char* fields[] = {
"step [N]", "break <EVENT_TYPE> [key=value ...]",
"info limit <name>|all", "info trace <jobid>",
"set units raw|human|both", "trace on all [OPTIONS]",
"trace on job <jobid> ... [OPTIONS]", "level summary|full",
"lines N", "file PATH",
"trace off", "trace status",
};
int col0 = desc_column(h, fields[0]);
size_t i;
for (i = 1; i < sizeof(fields) / sizeof(fields[0]); i++) {
TEST_ASSERT_EQUAL_INT_MESSAGE(col0, desc_column(h, fields[i]),
fields[i]);
}
free(h);
}
/* ---- routing (1.11, 10.5) ------------------------------------------------ */
/* 30 job ids and a path longer than 63 bytes: far past the tokenizer's
* 16 tokens / 64 characters, so only the raw line can carry them. */
static void test_trace_routed_with_raw_line(void)
{
char line[2048];
char path[256];
size_t n;
int i;
char* out;
xce_sched_trace_ctl_t* ctl;
snprintf(path, sizeof(path),
"%s/a_trace_file_with_a_name_well_beyond_sixty_four_chars.trace",
g_dir);
n = (size_t)snprintf(line, sizeof(line), "trace on job");
for (i = 0; i < 30; i++) {
n += (size_t)snprintf(line + n, sizeof(line) - n, " 1-2-1000-%d", 100 + i);
}
snprintf(line + n, sizeof(line) - n, " level full lines 200 file %s",
path);
out = dispatch(line);
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "[sim] trace: enabled"), out);
free(out);
ctl = repl_trace_settings_ctl(g_rs.trace.next);
TEST_ASSERT_NOT_NULL(ctl);
TEST_ASSERT_TRUE(ctl->nsel >= 30);
TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_FULL, ctl->level);
TEST_ASSERT_EQUAL_UINT32(200, ctl->line_bound);
TEST_ASSERT_EQUAL_STRING(path,
repl_trace_settings_path(g_rs.trace.next));
out = dispatch("trace status");
TEST_ASSERT_NOT_NULL(strstr(out, "[sim] trace status"));
free(out);
out = dispatch("trace off");
TEST_ASSERT_NOT_NULL(strstr(out, "[sim] trace: disabled"));
TEST_ASSERT_NULL(g_rs.trace.next);
free(out);
unlink(path);
}
static void test_info_trace_routed_and_listed_in_usage(void)
{
char* out = dispatch("info trace");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out, "[sim] info trace: usage: info trace"), out);
free(out);
out = dispatch("info trace 1-2-1000-77");
TEST_ASSERT_NULL_MESSAGE(strstr(out, "unrecognized subcommand"), out);
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "[sim] info trace:"), out);
free(out);
out = dispatch("info");
TEST_ASSERT_EQUAL_STRING(
"[sim] info: usage: info <break|session|config|job|jobs|queue|"
"worker|resource|summary|limit|trace> [arg]\n",
out);
free(out);
}
/* ---- set output (8.4) ---------------------------------------------------- */
static void assert_set_output_rejected(const char* path)
{
char want[256];
FILE* before = g_sim.record_file;
char* out;
char* content;
char line[256];
snprintf(want, sizeof(want),
"[sim] set output: %s is an open trace file\n", path);
snprintf(line, sizeof(line), "set output %s", path);
out = dispatch(line);
TEST_ASSERT_EQUAL_STRING(want, out);
free(out);
TEST_ASSERT_TRUE(g_sim.record_file == before); /* output unchanged */
content = read_file("a.trace");
TEST_ASSERT_EQUAL_STRING("keep\n", content); /* not truncated */
free(content);
}
static void test_set_output_rejects_open_trace_file(void)
{
write_file("a.trace", "keep\n");
g_sim.record_file = fopen("rec.replay", "w");
TEST_ASSERT_NOT_NULL(g_sim.record_file);
dispatch_quiet("trace on all file a.trace");
TEST_ASSERT_NOT_NULL(repl_trace_settings_file(g_rs.trace.next));
assert_set_output_rejected("a.trace");
TEST_ASSERT_EQUAL_INT(0, link("a.trace", "b.trace"));
assert_set_output_rejected("b.trace");
}
/* A file a command replaced during the session stays open, and rejected,
* until the session ends (8.4 with 8.5). */
static void test_set_output_rejects_file_kept_open_by_session(void)
{
write_file("a.trace", "keep\n");
dispatch_quiet("trace on all file a.trace");
repl_trace_session_begin(&g_sim);
dispatch_quiet("trace off");
TEST_ASSERT_NULL(g_rs.trace.next);
assert_set_output_rejected("a.trace");
repl_trace_session_end(&g_sim);
TEST_ASSERT_FALSE(repl_trace_file_conflicts(&g_sim, "a.trace"));
}
/* ---- quit and end of input (8.6) ----------------------------------------- */
/* Runs in the child at exit(), after the quit path ran: reports whether
* tracing was shut down, then frees the fixture so the child exits clean. */
static void report_at_exit(void)
{
char ok = ((g_rs.trace.next == NULL) && (g_rs.trace.cur == NULL)
&& (g_sim.core.trace_ctl == NULL))
? 'Y'
: 'N';
(void)!write(g_report_fd, &ok, 1);
close(g_report_fd);
fixture_fini();
}
static void assert_child_shuts_down(bool eof)
{
int fds[2];
pid_t pid;
int status = 0;
char got = '?';
write_file("a.trace", "");
dispatch_quiet("trace on all file a.trace");
repl_trace_session_begin(&g_sim);
TEST_ASSERT_NOT_NULL(g_sim.core.trace_ctl);
TEST_ASSERT_EQUAL_INT(0, pipe(fds));
fflush(stdout);
pid = fork();
TEST_ASSERT_TRUE(pid >= 0);
if (pid == 0) {
int devnull = open("/dev/null", O_RDWR);
close(fds[0]);
g_report_fd = fds[1];
dup2(devnull, STDIN_FILENO);
dup2(devnull, STDOUT_FILENO);
atexit(report_at_exit);
if (eof) {
repl_command_loop(&g_sim); /* readline() hits EOF */
} else {
(void)repl_dispatch(&g_sim, "quit");
}
_exit(99); /* both paths must exit(0) */
}
close(fds[1]);
TEST_ASSERT_EQUAL_INT(1, (int)read(fds[0], &got, 1));
close(fds[0]);
TEST_ASSERT_EQUAL_INT(pid, waitpid(pid, &status, 0));
TEST_ASSERT_TRUE(WIFEXITED(status));
TEST_ASSERT_EQUAL_INT(0, WEXITSTATUS(status));
TEST_ASSERT_EQUAL_CHAR_MESSAGE('Y', got, "tracing still active at exit");
}
static void test_quit_shuts_tracing_down(void) { assert_child_shuts_down(false); }
static void test_eof_shuts_tracing_down(void) { assert_child_shuts_down(true); }
/* ---- continue-mode rollup (7.3, 7.4) ------------------------------------- */
/* An outcome identical to the active range would extend it, but a traced
* round flushes the range and is reported on its own. */
static void test_rollup_traced_round_emitted_alone_after_flush(void)
{
char* out;
char* range;
char* alone;
open_dispatch_range();
out = rollup_round5(1, true);
range = strstr(out, "[sim] round 3-4 (2 round(s)");
alone = strstr(out, "[sim] round 5-5 (1 round(s)");
TEST_ASSERT_NOT_NULL_MESSAGE(range, out);
TEST_ASSERT_NOT_NULL_MESSAGE(alone, out);
TEST_ASSERT_TRUE(range < alone);
TEST_ASSERT_NOT_NULL(strstr(alone, "1 dispatch, 0 pend, 0 preempt"));
TEST_ASSERT_FALSE_MESSAGE(g_rs.collapse_active,
"a traced round must not start a range");
TEST_ASSERT_NULL(g_rs.collapse_pend_jobids);
free(out);
}
/* A traced round with no dispatch, preempt or pend job is idle, and is
* reported even though it is a single round. */
static void test_rollup_traced_idle_round_reported_as_idle(void)
{
char* out = rollup_round5(0, true);
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "[sim] round 5-5 (1 round(s)"),
out);
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "): idle\n"), out);
TEST_ASSERT_FALSE(g_rs.collapse_active);
free(out);
}
/* The flush idle rule, not the allocation-failure fallback: a pend-only
* traced round is not idle. */
static void test_rollup_traced_pend_only_round_not_idle(void)
{
char* out;
add_pend_job();
out = rollup_round5(0, true);
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "0 dispatch, 1 pend, 0 preempt"),
out);
TEST_ASSERT_NULL_MESSAGE(strstr(out, "idle"), out);
TEST_ASSERT_FALSE(g_rs.collapse_active);
free(out);
}
/* The round after a traced one starts a fresh range as before; an
* untraced round is unaffected. */
static void test_rollup_untraced_round_after_traced_starts_range(void)
{
char* out;
open_dispatch_range();
free(rollup_round5(1, true));
g_rs.session_count = 6;
g_rs.trace.round_traced = false; /* reset by the next session begin */
cap_begin();
repl_round_rollup_process(&g_sim);
out = cap_end();
TEST_ASSERT_EQUAL_STRING("", out);
TEST_ASSERT_TRUE(g_rs.collapse_active);
TEST_ASSERT_EQUAL_UINT64(6, g_rs.collapse_start_round);
free(out);
}
/* ---- repl.h gates -------------------------------------------------------- */
static void test_gates_noop_when_not_interactive(void)
{
fixture_decision_t f;
char* out;
dispatch_quiet("trace on all");
g_sim.interactive = false;
dispatch_of(&f, PEND_JOB);
cap_begin();
xce_repl_trace_session_begin(&g_sim);
xce_repl_check_trace(&g_sim, &f.d);
xce_repl_check_trace_only(&g_sim, &f.d);
xce_repl_trace_session_end(&g_sim);
out = cap_end();
TEST_ASSERT_EQUAL_STRING("", out);
TEST_ASSERT_NULL(g_sim.core.trace_ctl); /* 11.3: never published */
TEST_ASSERT_NULL(g_rs.trace.cur);
TEST_ASSERT_FALSE(g_rs.trace.session_in_progress);
TEST_ASSERT_NOT_NULL(f.d.trace); /* not consumed */
sched_trace_rec_free(f.d.trace);
free(out);
}
static void test_gates_forward_when_interactive(void)
{
fixture_decision_t f;
fixture_decision_t t;
char* out;
dispatch_quiet("trace on all");
xce_repl_trace_session_begin(&g_sim);
TEST_ASSERT_NOT_NULL(g_sim.core.trace_ctl);
TEST_ASSERT_TRUE(g_rs.trace.session_in_progress);
dispatch_of(&f, PEND_JOB);
dispatch_of(&t, PEND_JOB);
t.d.op = XCE_DECISION_TRACE;
cap_begin();
xce_repl_check_trace(&g_sim, &f.d);
xce_repl_check_trace_only(&g_sim, &t.d);
out = cap_end();
TEST_ASSERT_NULL(f.d.trace); /* moved to the store */
TEST_ASSERT_NULL(t.d.trace);
TEST_ASSERT_TRUE(strlen(out) > 0);
TEST_ASSERT_TRUE(g_rs.trace.round_traced);
free(out);
/* no record: nothing printed */
cap_begin();
xce_repl_check_trace(&g_sim, &f.d);
out = cap_end();
TEST_ASSERT_EQUAL_STRING("", out);
free(out);
xce_repl_trace_session_end(&g_sim);
TEST_ASSERT_NULL(g_sim.core.trace_ctl);
TEST_ASSERT_FALSE(g_rs.trace.session_in_progress);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_help_trace_section_is_last_and_lists_trace_commands);
RUN_TEST(test_help_info_trace_in_state_inspection);
RUN_TEST(test_help_one_description_column_with_trace_lines);
RUN_TEST(test_trace_routed_with_raw_line);
RUN_TEST(test_info_trace_routed_and_listed_in_usage);
RUN_TEST(test_set_output_rejects_open_trace_file);
RUN_TEST(test_set_output_rejects_file_kept_open_by_session);
RUN_TEST(test_quit_shuts_tracing_down);
RUN_TEST(test_eof_shuts_tracing_down);
RUN_TEST(test_rollup_traced_round_emitted_alone_after_flush);
RUN_TEST(test_rollup_traced_idle_round_reported_as_idle);
RUN_TEST(test_rollup_traced_pend_only_round_not_idle);
RUN_TEST(test_rollup_untraced_round_after_traced_starts_range);
RUN_TEST(test_gates_noop_when_not_interactive);
RUN_TEST(test_gates_forward_when_interactive);
return UNITY_END();
}