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();
+}