From 27cad0b0b71e745414dbd4c400af495285a7a14d Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 8 Oct 2026 14:26:10 +0000 Subject: [PATCH] feat(scheduler-sim-decision-trace): manage the trace file copy and output-file conflicts Task 4.5: a named trace file is opened for appending at command time; an open failure or a path that is the sim output file or the scheduler log (same device and inode) is rejected with settings unchanged; files close when their last settings holder releases them; set output gets a conflict check; shutdown flushes and closes every trace file and frees the store. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../scheduler-sim-decision-trace/tasks.md | 3 +- src/scheduler/src/repl_internal.h | 25 + src/scheduler/src/repl_trace.c | 136 ++++- src/scheduler/testsuite/meson.build | 21 + src/scheduler/testsuite/test_repl_trace_cmd.c | 104 +++- .../testsuite/test_repl_trace_file.c | 531 ++++++++++++++++++ 6 files changed, 786 insertions(+), 34 deletions(-) create mode 100644 src/scheduler/testsuite/test_repl_trace_file.c diff --git a/.kiro/specs/scheduler-sim-decision-trace/tasks.md b/.kiro/specs/scheduler-sim-decision-trace/tasks.md index 89c7abd2..9a4fd502 100644 --- a/.kiro/specs/scheduler-sim-decision-trace/tasks.md +++ b/.kiro/specs/scheduler-sim-decision-trace/tasks.md @@ -231,7 +231,7 @@ - _Requirements: 8.3_ - _Boundary: SimIntegration (driver arguments)_ -- [ ] 4.5 Manage the trace file copy and output-file conflicts, test first +- [x] 4.5 Manage the trace file copy and output-file conflicts, test first - When a command names a file, it is opened for appending at once. An open failure names the file and the cause, and the settings stay unchanged. - A path that is the same file (device and inode) as the current simulation output file or the scheduler log is rejected. - Expose a conflict check, so `set output` can reject any trace file that is still open. @@ -368,3 +368,4 @@ - 4.2: the store API is `repl_trace_store_put`, `repl_trace_store_bytes`, `repl_cmd_info_trace` and `repl_trace_store_fini` (an addition; nothing calls it at shutdown yet, so 4.5/4.6/5.2 must). `xce_repl_trace_state_t` (repl_internal.h) holds only the store fields; 4.3 adds next/cur/session flags to it. The 1.3 "file tag" is dropped: `info trace` is console-only (Req 8.1) and emission writes through the session's `cur` settings. `info trace` is not yet routed from repl_cmd_info (5.2). Open (6.2): an `id_list_add` OOM can silently drop array elements from a listing. - 4.3: `trace` is parsed from the raw line by repl_trace.c. The settings are a refcounted REPL-side wrapper around xce_sched_trace_ctl_t (trace.h untouched), held as `next`/`cur` in xce_repl_trace_state_t together with session_in_progress, round_traced and round_flushed. The 1.3 bare-id item is settled: an array-aligned bare id adds both an exact and a whole-array selector, so later elements match; status shows that as `[*]`. A range expands to one exact selector per element. The wrapper frees `ctl.policy_name` on its last release, so 4.6 must heap-allocate it. Nothing frees next/cur at REPL shutdown yet, so 4.5/4.6 must add the shutdown path; routing `trace` from repl.c is 5.2. - 4.4: the driver holds its own copy of the scheduler log path in `sim->log_path` (from `sched_sim_args_t.log_path`, else `SCHED_DEFAULT_LOG_PATH` in sim.h, "/tmp/xce_scheduler.log"). The log is always open in sim mode, so 4.5 can `stat(sim->log_path)` for its dev/ino same-file check. +- 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. diff --git a/src/scheduler/src/repl_internal.h b/src/scheduler/src/repl_internal.h index 54c0cd0b..3292c573 100644 --- a/src/scheduler/src/repl_internal.h +++ b/src/scheduler/src/repl_internal.h @@ -647,4 +647,29 @@ repl_trace_settings_ctl(xce_repl_trace_settings_t* s); extern const char* repl_trace_settings_path(const xce_repl_trace_settings_t* s); +/* + * Trace file copy (repl_trace.c, task 4.5, design.md "ReplTraceControl" + * file rules; Requirements 8.2-8.7): + * + * repl_trace_settings_file - the settings object's trace file, opened for + * appending when `trace on ... file PATH` ran (8.2), or NULL. The FILE* + * belongs to the settings object: it is flushed and closed by the last + * repl_trace_settings_unref(), so a file replaced or disabled while a + * session holds `cur` stays open until that session ends, and is closed at + * once otherwise (8.5). + * + * repl_trace_file_conflicts - true when `path` names, by device and inode, + * the file of `next` or `cur` (every trace file still open). `set output` + * rejects such a path (8.4). False for a NULL or nonexistent path. + * + * repl_trace_shutdown - for quit, end of input and replay completion: + * withdraws any published control block, releases `next` and `cur` + * (flushing and closing their files, 8.6), clears the session flags and + * frees the stored traces. NULL-safe and idempotent. + */ +extern FILE* repl_trace_settings_file(const xce_repl_trace_settings_t* s); +extern bool repl_trace_file_conflicts(const xce_sched_sim_driver_t* sim, + const char* path); +extern void repl_trace_shutdown(xce_sched_sim_driver_t* sim); + #endif diff --git a/src/scheduler/src/repl_trace.c b/src/scheduler/src/repl_trace.c index 2c2e1007..a24bf36e 100644 --- a/src/scheduler/src/repl_trace.c +++ b/src/scheduler/src/repl_trace.c @@ -25,15 +25,25 @@ * 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). + * session hooks that move `next` into `cur` are task 4.6. + * + * Trace file (task 4.5, 8.2-8.7): a named file is opened for appending + * when the command runs, after a stat() check that it is not the + * simulation output file or the scheduler log (by device and inode, so a + * hard link or symlink is caught too). The FILE* belongs to the settings + * object and is flushed and closed by its last unref, so a file replaced + * or disabled during a session stays open until that session drops `cur`, + * and is closed at once otherwise. repl_trace_shutdown() releases both + * references and the store at REPL exit. * * Runs on the sim thread at the REPL prompt only; writes nothing through * tlog (8.7). */ +#include #include #include #include +#include #include "repl_internal.h" #include "sched/trace.h" @@ -46,6 +56,9 @@ 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 */ + FILE* file; /* open for appending when path is set */ + dev_t dev; /* file's device and inode, for the */ + ino_t ino; /* same-file checks (8.3, 8.4) */ int cap; /* allocated length of ctl.sel */ }; @@ -78,6 +91,10 @@ void repl_trace_settings_unref(xce_repl_trace_settings_t* s) if ((s == NULL) || (--s->refs > 0)) { return; } + if (s->file != NULL) { + fflush(s->file); /* 8.6: every printed trace reaches the file */ + fclose(s->file); + } free(s->ctl.sel); free((char*)s->ctl.policy_name); free(s->path); @@ -94,6 +111,11 @@ const char* repl_trace_settings_path(const xce_repl_trace_settings_t* s) return (s != NULL) ? s->path : NULL; } +FILE* repl_trace_settings_file(const xce_repl_trace_settings_t* s) +{ + return (s != NULL) ? s->file : NULL; +} + static bool sel_add(xce_repl_trace_settings_t* s, uint64_t key, bool whole_array) { @@ -350,6 +372,112 @@ fail: return NULL; } +/* ---- the trace file (task 4.5) ------------------------------------------ */ + +static bool same_file(const struct stat* a, const struct stat* b) +{ + return (a->st_dev == b->st_dev) && (a->st_ino == b->st_ino); +} + +/* 8.3: rejects a path that is the simulation output file in effect or the + * scheduler log. A path that does not exist yet is neither; any other + * stat() failure is left for fopen() to report. */ +static bool reject_reserved_file(const xce_sched_sim_driver_t* sim, + const char* path) +{ + struct stat want; + struct stat st; + + if (stat(path, &want) != 0) { + return false; + } + if ((sim->record_file != NULL) && + (fstat(fileno(sim->record_file), &st) == 0) && same_file(&want, &st)) { + fprintf(stdout, + "[sim] trace: file %s is the simulation output file; " + "choose another file\n", + path); + return true; + } + if ((sim->log_path != NULL) && (stat(sim->log_path, &st) == 0) && + same_file(&want, &st)) { + fprintf(stdout, + "[sim] trace: file %s is the scheduler log file; choose " + "another file\n", + path); + return true; + } + return false; +} + +/* 8.2: opens s->path for appending now, so an error is reported while the + * old settings are still in place. Returns false after printing it. */ +static bool open_file(const xce_sched_sim_driver_t* sim, + xce_repl_trace_settings_t* s) +{ + struct stat st; + + if (reject_reserved_file(sim, s->path)) { + return false; + } + s->file = fopen(s->path, "a"); + if (s->file == NULL) { + fprintf(stdout, "[sim] trace: cannot open file %s: %s\n", s->path, + strerror(errno)); + return false; + } + if (fstat(fileno(s->file), &st) != 0) { + fprintf(stdout, "[sim] trace: cannot open file %s: %s\n", s->path, + strerror(errno)); + fclose(s->file); + s->file = NULL; + return false; + } + s->dev = st.st_dev; + s->ino = st.st_ino; + return true; +} + +static bool settings_hold(const xce_repl_trace_settings_t* s, + const struct stat* st) +{ + return (s != NULL) && (s->file != NULL) && (s->dev == st->st_dev) && + (s->ino == st->st_ino); +} + +bool repl_trace_file_conflicts(const xce_sched_sim_driver_t* sim, + const char* path) +{ + const xce_repl_trace_state_t* ts; + struct stat st; + + if ((sim == NULL) || (sim->repl_state == NULL) || (path == NULL) || + (stat(path, &st) != 0)) { + return false; + } + ts = &(sim->repl_state->trace); + return settings_hold(ts->next, &st) || settings_hold(ts->cur, &st); +} + +void repl_trace_shutdown(xce_sched_sim_driver_t* sim) +{ + xce_repl_trace_state_t* ts; + + if ((sim == NULL) || (sim->repl_state == NULL)) { + return; + } + ts = &(sim->repl_state->trace); + sim->core.trace_ctl = NULL; /* never leave a released ctl published */ + repl_trace_settings_unref(ts->cur); + repl_trace_settings_unref(ts->next); + ts->cur = NULL; + ts->next = NULL; + ts->session_in_progress = false; + ts->round_traced = false; + ts->round_flushed = false; + repl_trace_store_fini(ts); /* frees the stored traces, zeroes ts */ +} + /* ---- `trace status` ------------------------------------------------------- */ static bool has_whole_array(const xce_sched_trace_ctl_t* ctl, uint64_t key) @@ -494,6 +622,10 @@ bool repl_trace_cmd(xce_sched_sim_driver_t* sim, const char* raw_line) "trace off | trace status\n"); } else if (word_is(w, "on")) { s = parse_on(p); + if ((s != NULL) && (s->path != NULL) && !open_file(sim, s)) { + repl_trace_settings_unref(s); /* 8.2, 8.3: nothing changes */ + s = NULL; + } if (s != NULL) { repl_trace_settings_unref(ts->next); ts->next = s; diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 5c4272ae..bee2f89b 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -2776,3 +2776,24 @@ sched_repl_trace_cmd_test = executable( test('scheduler repl trace cmd test', sched_repl_trace_cmd_test, suite: 'scheduler') + +# scheduler-sim-decision-trace task 4.5: the trace file copy and +# output-file conflicts (design.md "ReplTraceControl" file rules, "Trace +# settings lifecycle"; Requirements 8.2-8.7). Open at command time and open +# failures, same-file rejection against the simulation output file and the +# scheduler log (direct, hard link, symlink), the `set output` conflict +# check, a replaced or disabled file kept open until the session ends or +# closed at once, and shutdown flushing and closing every trace file. Same +# source set as the trace cmd test; leak detection stays on. +sched_repl_trace_file_test = executable( + 'sched_repl_trace_file_test', + ['test_repl_trace_file.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 file test', + sched_repl_trace_file_test, suite: 'scheduler') diff --git a/src/scheduler/testsuite/test_repl_trace_cmd.c b/src/scheduler/testsuite/test_repl_trace_cmd.c index f705b72f..efb0cd7c 100644 --- a/src/scheduler/testsuite/test_repl_trace_cmd.c +++ b/src/scheduler/testsuite/test_repl_trace_cmd.c @@ -12,6 +12,9 @@ * 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. + * Since task 4.5 a named trace file is opened at command time, so every + * test runs in its own scratch directory with relative paths, and + * tearDown releases everything through repl_trace_shutdown(). * * Covers: * - every grammar form: `on all`, `on job SEL...`, `level`, `lines`, @@ -29,6 +32,8 @@ #include #include #include +#include +#include #include #include @@ -51,6 +56,8 @@ static xce_sched_sim_driver_t g_sim; static xce_sched_repl_state_t g_rs; +static char g_dir[64]; /* per-test scratch directory */ +static char g_cwd[4096]; /* restored at tearDown */ /* ---- fixture job table (the store's no-trace replies; unused here) ------ */ @@ -73,16 +80,48 @@ void setUp(void) memset(&g_rs, 0, sizeof(g_rs)); g_rs.units_mode = XCE_REPL_UNITS_BOTH; g_sim.repl_state = &g_rs; + + /* Task 4.5 opens a named trace file at command time, so every test runs + * in its own scratch directory and uses relative paths. */ + TEST_ASSERT_NOT_NULL(getcwd(g_cwd, sizeof(g_cwd))); + snprintf(g_dir, sizeof(g_dir), "/tmp/xce_trace_cmd_XXXXXX"); + TEST_ASSERT_NOT_NULL(mkdtemp(g_dir)); + TEST_ASSERT_EQUAL_INT(0, chdir(g_dir)); + TEST_ASSERT_EQUAL_INT(0, mkdir("a dir", 0700)); +} + +static int rm_entry(const char* path, const struct stat* st, int flag, + struct FTW* ftw) +{ + (void)st; + (void)flag; + (void)ftw; + return remove(path); } 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); + repl_trace_shutdown(&g_sim); /* releases next/cur and the store */ + TEST_ASSERT_NULL(g_rs.trace.next); + TEST_ASSERT_NULL(g_rs.trace.cur); g_sim.repl_state = NULL; + TEST_ASSERT_EQUAL_INT(0, chdir(g_cwd)); + nftw(g_dir, rm_entry, 16, FTW_DEPTH | FTW_PHYS); +} + +/* A path of exactly `len` bytes naming a file in the scratch directory: + * "./" repeated, then a 5- or 6-character file name. */ +static void long_path(char* buf, size_t len) +{ + const char* name = ((len - 5) % 2 == 0) ? "p.log" : "pp.log"; + size_t n = strlen(name); + size_t i; + + for (i = 0; i + n < len; i += 2) { + buf[i] = '.'; + buf[i + 1] = '/'; + } + memcpy(buf + i, name, n + 1); } /* ---- helpers ------------------------------------------------------------- */ @@ -149,15 +188,20 @@ static void assert_run(const char* line, const char* want) free(got); } -/* Runs a line that must succeed (prints no error) and returns `next`. */ +/* Runs a line that must succeed (prints only the enabled/disabled + * confirmation) and returns `next`. */ static xce_repl_trace_settings_t* run_ok(const char* line) { char* got = run(line); + bool ok = (strcmp(got, "[sim] trace: enabled; applies from the next " + "session\n") == 0) || + (strcmp(got, "[sim] trace: disabled; applies from the next " + "session\n") == 0); - if (strstr(got, "invalid") != NULL) { - fprintf(stderr, "unexpected error for '%s': %s", line, got); + if (!ok) { + fprintf(stderr, "unexpected output for '%s': %s", line, got); } - TEST_ASSERT_NULL(strstr(got, "invalid")); + TEST_ASSERT_TRUE(ok); free(got); return g_rs.trace.next; } @@ -223,12 +267,12 @@ 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 "); + run_ok(" trace\ton all level full lines 50 file 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()); + TEST_ASSERT_EQUAL_STRING("t.log", next_path()); run_ok("trace on all lines 100000 level summary"); ctl = next_ctl(); @@ -312,25 +356,23 @@ static void test_file_paths(void) 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()); + run_ok("trace on all file \"a dir/t.log\" "); + TEST_ASSERT_EQUAL_STRING("a dir/t.log", next_path()); + run_ok("trace on all file 'q.log'"); + TEST_ASSERT_EQUAL_STRING("q.log", next_path()); + run_ok("trace on all level full file x y.log"); + TEST_ASSERT_EQUAL_STRING("x y.log", next_path()); /* 200 characters */ - memset(path, 'p', 200); - path[0] = '/'; - path[200] = '\0'; + long_path(path, 200); + TEST_ASSERT_EQUAL_INT(200, (int)strlen(path)); 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'; + long_path(path, 4095); + TEST_ASSERT_EQUAL_INT(4095, (int)strlen(path)); snprintf(line, sizeof(line), "trace on all file \"%s\"", path); run_ok(line); TEST_ASSERT_EQUAL_STRING(path, next_path()); @@ -376,7 +418,7 @@ 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"); + run_ok("trace on all level full lines 70 file keep.log"); assert_rejected("trace", "[sim] trace: usage: trace on all|job " "... [level summary|full] [lines N] " @@ -426,7 +468,7 @@ static void test_invalid_arguments_leave_settings(void) 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()); + TEST_ASSERT_EQUAL_STRING("keep.log", next_path()); } /* ---- settings lifecycle and status (1.4, 1.6, 1.7, 1.10) ---------------- */ @@ -449,7 +491,7 @@ static void test_status_next_settings(void) char line[256]; snprintf(line, sizeof(line), - "trace on job %s %s[1-3] %s level full lines 300 file /tmp/s.log", + "trace on job %s %s[1-3] %s level full lines 300 file s.log", fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), fmt_id(PLAIN_JOB2)); run_ok(line); snprintf(want, sizeof(want), @@ -458,7 +500,7 @@ static void test_status_next_settings(void) " selection: jobs %s,%s[1-3],%s\n" " level: full\n" " line bound: 300\n" - " file: /tmp/s.log\n" + " file: s.log\n" "%s", fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), fmt_id(PLAIN_JOB2), store_line()); @@ -534,7 +576,7 @@ static void test_status_before_during_after_session(void) 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", + snprintf(line, sizeof(line), "trace on job %s level full file n.log", fmt_id(PLAIN_JOB)); run_ok(line); TEST_ASSERT_TRUE(started == g_rs.trace.cur); @@ -552,7 +594,7 @@ static void test_status_before_during_after_session(void) " selection: jobs %s\n" " level: full\n" " line bound: 1000\n" - " file: /tmp/n.log\n" + " file: n.log\n" "%s", fmt_id(PLAIN_JOB), store_line()); assert_run("trace status", want); @@ -620,12 +662,12 @@ static void test_settings_refcount(void) 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"); + s = run_ok("trace on all lines 123 file 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)); + TEST_ASSERT_EQUAL_STRING("r.log", repl_trace_settings_path(s)); repl_trace_settings_unref(s); } diff --git a/src/scheduler/testsuite/test_repl_trace_file.c b/src/scheduler/testsuite/test_repl_trace_file.c new file mode 100644 index 00000000..6b543a04 --- /dev/null +++ b/src/scheduler/testsuite/test_repl_trace_file.c @@ -0,0 +1,531 @@ +/* + * test_repl_trace_file.c - the trace file copy and output-file conflicts + * (scheduler-sim-decision-trace spec, task 4.5; design.md + * "ReplTraceControl" file rules, "Trace settings lifecycle", "Error + * Handling"; Requirements 8.2-8.7). + * + * Links the trace control (src/repl_trace.c), the store, the record model, + * the renderer and the shared REPL formatting module, like + * test_repl_trace_cmd.c. Every test runs in its own scratch directory. + * A session in progress is simulated through the state the session hooks + * (task 4.6) maintain: `cur` takes a reference to `next` and + * `session_in_progress` is set; the end drops `cur` and clears the flag. + * + * Covers: + * - a named file is opened for appending at command time; an open + * failure names the file and the cause and leaves `next` unchanged + * (8.2); + * - a path that is the simulation output file or the scheduler log, + * directly, through a hard link or through a symlink, is rejected and + * leaves `next` unchanged (8.3); + * - repl_trace_file_conflicts() matches the files of `next` and `cur` + * by device and inode, for `set output` (8.4); + * - a replaced or disabled file stays open until the session in progress + * ends, and is closed at once when no session is in progress (8.5); + * - repl_trace_shutdown() flushes and closes every open trace file and + * releases the settings and the store (8.6); + * - nothing is written to the scheduler log (8.7). + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "repl_internal.h" +#include "sched/trace.h" +#include "sim.h" +#include "utils.h" + +#define ENABLED "[sim] trace: enabled; applies from the next session\n" +#define DISABLED "[sim] trace: disabled; applies from the next session\n" + +static xce_sched_sim_driver_t g_sim; +static xce_sched_repl_state_t g_rs; +static char g_dir[64]; +static char g_cwd[4096]; + +/* ---- 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"; +} + +static int rm_entry(const char* path, const struct stat* st, int flag, + struct FTW* ftw) +{ + (void)st; + (void)flag; + (void)ftw; + return remove(path); +} + +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; + + TEST_ASSERT_NOT_NULL(getcwd(g_cwd, sizeof(g_cwd))); + snprintf(g_dir, sizeof(g_dir), "/tmp/xce_trace_file_XXXXXX"); + TEST_ASSERT_NOT_NULL(mkdtemp(g_dir)); + TEST_ASSERT_EQUAL_INT(0, chdir(g_dir)); +} + +void tearDown(void) +{ + repl_trace_shutdown(&g_sim); + if (g_sim.record_file != NULL) { + fclose(g_sim.record_file); + g_sim.record_file = NULL; + } + free(g_sim.log_path); + g_sim.log_path = NULL; + g_sim.repl_state = NULL; + TEST_ASSERT_EQUAL_INT(0, chdir(g_cwd)); + nftw(g_dir, rm_entry, 16, FTW_DEPTH | FTW_PHYS); +} + +/* ---- helpers ------------------------------------------------------------- */ + +/* 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_file_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_run(const char* line, const char* want) +{ + char* got = run(line); + + if (strcmp(want, got) != 0) { + fprintf(stderr, "---- want ----\n%s---- got ----\n%s----\n", want, + got); + } + TEST_ASSERT_EQUAL_STRING(want, got); + free(got); +} + +/* How many of this process's descriptors are open on `path` (by device + * and inode, so a hard link counts as the same file). */ +static int open_count(const char* path) +{ + struct stat want; + struct stat st; + DIR* d; + struct dirent* e; + char fdpath[PATH_MAX]; + int n = 0; + + if (stat(path, &want) != 0) { + return 0; + } + d = opendir("/proc/self/fd"); + TEST_ASSERT_NOT_NULL(d); + while ((e = readdir(d)) != NULL) { + if (e->d_name[0] == '.') { + continue; + } + snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%s", e->d_name); + if ((stat(fdpath, &st) == 0) && (st.st_dev == want.st_dev) && + (st.st_ino == want.st_ino)) { + n++; + } + } + closedir(d); + return n; +} + +static void touch(const char* path) +{ + FILE* f = fopen(path, "a"); + + TEST_ASSERT_NOT_NULL(f); + fclose(f); +} + +static long file_size(const char* path) +{ + struct stat st; + + TEST_ASSERT_EQUAL_INT(0, stat(path, &st)); + return (long)st.st_size; +} + +static char* slurp(const char* path) +{ + long n = file_size(path); + char* b = (char*)calloc(1, (size_t)n + 1); + FILE* f = fopen(path, "r"); + + TEST_ASSERT_NOT_NULL(b); + TEST_ASSERT_NOT_NULL(f); + TEST_ASSERT_EQUAL_INT((int)n, (int)fread(b, 1, (size_t)n, f)); + fclose(f); + return b; +} + +/* 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; +} + +/* An --sim-output / `set output` file in effect. */ +static void set_output(const char* path) +{ + g_sim.record_file = fopen(path, "w"); + TEST_ASSERT_NOT_NULL(g_sim.record_file); +} + +/* ---- 8.2: open at command time, open failure ----------------------------- */ + +static void test_file_opened_for_append_at_command_time(void) +{ + FILE* f = fopen("t.log", "w"); + + TEST_ASSERT_NOT_NULL(f); + fputs("earlier\n", f); + fclose(f); + + assert_run("trace on all file t.log", ENABLED); + TEST_ASSERT_NOT_NULL(g_rs.trace.next); + TEST_ASSERT_NOT_NULL(repl_trace_settings_file(g_rs.trace.next)); + TEST_ASSERT_EQUAL_INT(1, open_count("t.log")); + + /* appended, never truncated */ + fputs("traced\n", repl_trace_settings_file(g_rs.trace.next)); + fflush(repl_trace_settings_file(g_rs.trace.next)); + { + char* got = slurp("t.log"); + + TEST_ASSERT_EQUAL_STRING("earlier\ntraced\n", got); + free(got); + } + + /* a new file is created at once */ + assert_run("trace on all file new.log", ENABLED); + TEST_ASSERT_EQUAL_INT(0, access("new.log", F_OK)); + TEST_ASSERT_EQUAL_INT(1, open_count("new.log")); + + /* no file named: none opened */ + assert_run("trace on all", ENABLED); + TEST_ASSERT_NULL(repl_trace_settings_file(g_rs.trace.next)); +} + +static void test_open_failure_leaves_settings_unchanged(void) +{ + xce_repl_trace_settings_t* before; + char want[256]; + + assert_run("trace on all lines 77 file keep.log", ENABLED); + before = g_rs.trace.next; + + snprintf(want, sizeof(want), + "[sim] trace: cannot open file no such dir/t.log: %s\n", + strerror(ENOENT)); + assert_run("trace on all level full file \"no such dir/t.log\"", want); + TEST_ASSERT_TRUE(g_rs.trace.next == before); + TEST_ASSERT_EQUAL_INT(77, repl_trace_settings_ctl(before)->line_bound); + TEST_ASSERT_EQUAL_STRING("keep.log", repl_trace_settings_path(before)); + TEST_ASSERT_EQUAL_INT(1, open_count("keep.log")); + + /* a directory cannot be opened for writing either */ + TEST_ASSERT_EQUAL_INT(0, mkdir("adir", 0700)); + snprintf(want, sizeof(want), "[sim] trace: cannot open file adir: %s\n", + strerror(EISDIR)); + assert_run("trace on job 1-2-1000-42 file adir", want); + TEST_ASSERT_TRUE(g_rs.trace.next == before); + + /* also with tracing off: it stays off */ + assert_run("trace off", DISABLED); + assert_run("trace on all file \"no such dir/t.log\"", + "[sim] trace: cannot open file no such dir/t.log: " + "No such file or directory\n"); + TEST_ASSERT_NULL(g_rs.trace.next); +} + +/* ---- 8.3: same file as the simulation output or the scheduler log ------- */ + +static void test_rejects_sim_output_file(void) +{ + xce_repl_trace_settings_t* before; + const char* want = "[sim] trace: file out.replay is the simulation " + "output file; choose another file\n"; + + set_output("out.replay"); + assert_run("trace on all file keep.log", ENABLED); + before = g_rs.trace.next; + + assert_run("trace on all file out.replay", want); + TEST_ASSERT_TRUE(g_rs.trace.next == before); + TEST_ASSERT_EQUAL_INT(1, open_count("out.replay")); /* only record_file */ + + /* through a hard link and a symlink */ + TEST_ASSERT_EQUAL_INT(0, link("out.replay", "hard.replay")); + assert_run("trace on all file hard.replay", + "[sim] trace: file hard.replay is the simulation output file; " + "choose another file\n"); + TEST_ASSERT_EQUAL_INT(0, symlink("out.replay", "soft.replay")); + assert_run("trace on all file soft.replay", + "[sim] trace: file soft.replay is the simulation output file; " + "choose another file\n"); + TEST_ASSERT_TRUE(g_rs.trace.next == before); + TEST_ASSERT_EQUAL_INT(0, (int)file_size("out.replay")); + + /* with no output file in effect, the same path is accepted */ + fclose(g_sim.record_file); + g_sim.record_file = NULL; + assert_run("trace on all file out.replay", ENABLED); +} + +static void test_rejects_scheduler_log(void) +{ + xce_repl_trace_settings_t* before; + + touch("sched.log"); + g_sim.log_path = strdup("sched.log"); + TEST_ASSERT_NOT_NULL(g_sim.log_path); + assert_run("trace on all file keep.log", ENABLED); + before = g_rs.trace.next; + + assert_run("trace on all file sched.log", + "[sim] trace: file sched.log is the scheduler log file; " + "choose another file\n"); + TEST_ASSERT_TRUE(g_rs.trace.next == before); + TEST_ASSERT_EQUAL_INT(0, open_count("sched.log")); + + TEST_ASSERT_EQUAL_INT(0, link("sched.log", "hard.log")); + assert_run("trace on job 1-2-1000-43 file hard.log", + "[sim] trace: file hard.log is the scheduler log file; " + "choose another file\n"); + TEST_ASSERT_TRUE(g_rs.trace.next == before); + TEST_ASSERT_EQUAL_INT(0, open_count("sched.log")); + + /* 8.7: tracing never writes to the scheduler log */ + assert_run("trace off", DISABLED); + TEST_ASSERT_EQUAL_INT(0, (int)file_size("sched.log")); + + /* a log path that does not exist yet conflicts with nothing */ + free(g_sim.log_path); + g_sim.log_path = strdup("missing.log"); + assert_run("trace on all file other.log", ENABLED); +} + +/* ---- 8.4: the conflict check for `set output` ----------------------------- */ + +static void test_conflict_check(void) +{ + TEST_ASSERT_FALSE(repl_trace_file_conflicts(&g_sim, "a.log")); + TEST_ASSERT_FALSE(repl_trace_file_conflicts(&g_sim, NULL)); + + assert_run("trace on all file a.log", ENABLED); + session_begin(); + assert_run("trace on all file b.log", ENABLED); /* a.log kept by cur */ + + TEST_ASSERT_TRUE(repl_trace_file_conflicts(&g_sim, "a.log")); + TEST_ASSERT_TRUE(repl_trace_file_conflicts(&g_sim, "b.log")); + TEST_ASSERT_TRUE(repl_trace_file_conflicts(&g_sim, "./b.log")); + TEST_ASSERT_EQUAL_INT(0, link("a.log", "a_hard.log")); + TEST_ASSERT_TRUE(repl_trace_file_conflicts(&g_sim, "a_hard.log")); + TEST_ASSERT_FALSE(repl_trace_file_conflicts(&g_sim, "c.log")); + touch("c.log"); + TEST_ASSERT_FALSE(repl_trace_file_conflicts(&g_sim, "c.log")); + + /* once the session ends, a.log is closed and no longer conflicts */ + session_end(); + TEST_ASSERT_FALSE(repl_trace_file_conflicts(&g_sim, "a.log")); + TEST_ASSERT_TRUE(repl_trace_file_conflicts(&g_sim, "b.log")); + + assert_run("trace off", DISABLED); + TEST_ASSERT_FALSE(repl_trace_file_conflicts(&g_sim, "b.log")); +} + +/* ---- 8.5: replaced and disabled files ------------------------------------ */ + +static void test_replaced_file_kept_open_until_session_ends(void) +{ + FILE* old; + + assert_run("trace on all file a.log", ENABLED); + session_begin(); + old = repl_trace_settings_file(g_rs.trace.cur); + TEST_ASSERT_NOT_NULL(old); + + assert_run("trace on job 1-2-1000-44 file b.log", ENABLED); + TEST_ASSERT_EQUAL_INT(1, open_count("a.log")); + TEST_ASSERT_EQUAL_INT(1, open_count("b.log")); + TEST_ASSERT_TRUE(repl_trace_settings_file(g_rs.trace.cur) == old); + /* traces captured under the old settings still go to the old file */ + fputs("late trace\n", old); + + session_end(); + TEST_ASSERT_EQUAL_INT(0, open_count("a.log")); + TEST_ASSERT_EQUAL_INT(1, open_count("b.log")); + { + char* got = slurp("a.log"); + + TEST_ASSERT_EQUAL_STRING("late trace\n", got); /* flushed by close */ + free(got); + } +} + +static void test_disabled_file_kept_open_until_session_ends(void) +{ + assert_run("trace on all file a.log", ENABLED); + session_begin(); + assert_run("trace off", DISABLED); + TEST_ASSERT_EQUAL_INT(1, open_count("a.log")); + session_end(); + TEST_ASSERT_EQUAL_INT(0, open_count("a.log")); +} + +static void test_immediate_close_without_session(void) +{ + assert_run("trace on all file a.log", ENABLED); + TEST_ASSERT_EQUAL_INT(1, open_count("a.log")); + assert_run("trace on all file b.log", ENABLED); + TEST_ASSERT_EQUAL_INT(0, open_count("a.log")); + TEST_ASSERT_EQUAL_INT(1, open_count("b.log")); + assert_run("trace off", DISABLED); + TEST_ASSERT_EQUAL_INT(0, open_count("b.log")); + + /* a session that ended leaves nothing held either */ + assert_run("trace on all file c.log", ENABLED); + session_begin(); + session_end(); + TEST_ASSERT_EQUAL_INT(1, open_count("c.log")); + assert_run("trace off", DISABLED); + TEST_ASSERT_EQUAL_INT(0, open_count("c.log")); +} + +/* ---- 8.6: shutdown -------------------------------------------------------- */ + +static void test_shutdown_flushes_and_closes_every_file(void) +{ + assert_run("trace on all file a.log", ENABLED); + session_begin(); + assert_run("trace on all file b.log", ENABLED); + g_sim.core.trace_ctl = repl_trace_settings_ctl(g_rs.trace.cur); + + /* buffered, not yet flushed */ + fputs("trace in a\n", repl_trace_settings_file(g_rs.trace.cur)); + fputs("trace in b\n", repl_trace_settings_file(g_rs.trace.next)); + TEST_ASSERT_EQUAL_INT(0, (int)file_size("a.log")); + TEST_ASSERT_EQUAL_INT(0, (int)file_size("b.log")); + + repl_trace_shutdown(&g_sim); + + TEST_ASSERT_NULL(g_rs.trace.next); + TEST_ASSERT_NULL(g_rs.trace.cur); + TEST_ASSERT_FALSE(g_rs.trace.session_in_progress); + TEST_ASSERT_NULL(g_sim.core.trace_ctl); /* nothing left published */ + TEST_ASSERT_EQUAL_INT(0, open_count("a.log")); + TEST_ASSERT_EQUAL_INT(0, open_count("b.log")); + { + char* a = slurp("a.log"); + char* b = slurp("b.log"); + + TEST_ASSERT_EQUAL_STRING("trace in a\n", a); + TEST_ASSERT_EQUAL_STRING("trace in b\n", b); + free(a); + free(b); + } + + /* idempotent, and safe with nothing set */ + repl_trace_shutdown(&g_sim); + g_sim.repl_state = NULL; + repl_trace_shutdown(&g_sim); + repl_trace_shutdown(NULL); + g_sim.repl_state = &g_rs; +} + +/* Shutdown also frees the stored traces (task 4.2's store) so nothing + * leaks at REPL exit; leak detection is on for this test binary. */ +static void test_shutdown_releases_store(void) +{ + 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 = XCE_TRACE_LINE_BOUND_DEFAULT; + rec = sched_trace_rec_new(&ctl, (uint64_t)42, XCE_TRACE_REC_DECISION); + TEST_ASSERT_NOT_NULL(rec); + repl_trace_store_put(&g_rs.trace, rec); + TEST_ASSERT_TRUE(repl_trace_store_bytes(&g_rs.trace) > 0); + + repl_trace_shutdown(&g_sim); + TEST_ASSERT_EQUAL_INT(0, (int)repl_trace_store_bytes(&g_rs.trace)); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_file_opened_for_append_at_command_time); + RUN_TEST(test_open_failure_leaves_settings_unchanged); + RUN_TEST(test_rejects_sim_output_file); + RUN_TEST(test_rejects_scheduler_log); + RUN_TEST(test_conflict_check); + RUN_TEST(test_replaced_file_kept_open_until_session_ends); + RUN_TEST(test_disabled_file_kept_open_until_session_ends); + RUN_TEST(test_immediate_close_without_session); + RUN_TEST(test_shutdown_flushes_and_closes_every_file); + RUN_TEST(test_shutdown_releases_store); + return UNITY_END(); +}