diff --git a/.kiro/specs/scheduler-sim-decision-trace/tasks.md b/.kiro/specs/scheduler-sim-decision-trace/tasks.md index cc17ea62..210cb18b 100644 --- a/.kiro/specs/scheduler-sim-decision-trace/tasks.md +++ b/.kiro/specs/scheduler-sim-decision-trace/tasks.md @@ -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. diff --git a/src/scheduler/include/repl.h b/src/scheduler/include/repl.h index 1abdf4c2..64018b49 100644 --- a/src/scheduler/include/repl.h +++ b/src/scheduler/include/repl.h @@ -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 diff --git a/src/scheduler/src/repl.c b/src/scheduler/src/repl.c index c9db0972..3ead469a 100644 --- a/src/scheduler/src/repl.c +++ b/src/scheduler/src/repl.c @@ -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 |all", "Show limit setting(s) and usage."); + fprintf(stdout, FIELD_FMT, "info trace ", + "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 ... [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); diff --git a/src/scheduler/src/repl_info.c b/src/scheduler/src/repl_info.c index c7200c70..5bd971dd 100644 --- a/src/scheduler/src/repl_info.c +++ b/src/scheduler/src/repl_info.c @@ -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 [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]); } diff --git a/src/scheduler/src/repl_params.c b/src/scheduler/src/repl_params.c index 3fd023af..131ea49b 100644 --- a/src/scheduler/src/repl_params.c +++ b/src/scheduler/src/repl_params.c @@ -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); diff --git a/src/scheduler/src/repl_rollup.c b/src/scheduler/src/repl_rollup.c index 9bbd1041..553ac7cd 100644 --- a/src/scheduler/src/repl_rollup.c +++ b/src/scheduler/src/repl_rollup.c @@ -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) diff --git a/src/scheduler/testsuite/fixtures/repl_trace_baseline/transcript.txt b/src/scheduler/testsuite/fixtures/repl_trace_baseline/transcript.txt index 755c7183..2dc87e54 100644 --- a/src/scheduler/testsuite/fixtures/repl_trace_baseline/transcript.txt +++ b/src/scheduler/testsuite/fixtures/repl_trace_baseline/transcript.txt @@ -25,13 +25,23 @@ State Inspection: info summary Quick overview. info config Show sim parameters. info limit |all Show limit setting(s) and usage. + info trace Re-print a job's latest decision trace. Snapshot & Parameters: snapshot [FILE] Write a standalone snapshot. set output |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 [arg] + +Trace: + trace on all [OPTIONS] Trace every job's decisions. + trace on job ... [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 [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] diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 903e2a2e..6e424a94 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -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']) diff --git a/src/scheduler/testsuite/test_repl_trace_wiring.c b/src/scheduler/testsuite/test_repl_trace_wiring.c new file mode 100644 index 00000000..c53462a0 --- /dev/null +++ b/src/scheduler/testsuite/test_repl_trace_wiring.c @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 ", + "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 "); + + 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 [key=value ...]", + "info limit |all", "info trace ", + "set units raw|human|both", "trace on all [OPTIONS]", + "trace on job ... [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 [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(); +}