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) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-08 14:26:10 +00:00
parent e01ffe4c9c
commit 27cad0b0b7
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
6 changed files with 786 additions and 34 deletions

View file

@ -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 `<base>[*]`. A range expands to one exact selector per element. The wrapper frees `ctl.policy_name` on its last release, so 4.6 must heap-allocate it. Nothing frees next/cur at REPL shutdown yet, so 4.5/4.6 must add the shutdown path; routing `trace` from repl.c is 5.2.
- 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.

View file

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

View file

@ -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 <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#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;

View file

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

View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ftw.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unity/unity.h>
@ -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 <jobid> "
"... [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);
}

View file

@ -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 <dirent.h>
#include <errno.h>
#include <ftw.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unity/unity.h>
#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();
}