/* * test_repl_trace_cmd.c - the `trace` command grammar and the trace * settings lifecycle (scheduler-sim-decision-trace spec, task 4.3; * design.md "ReplTraceControl", "Trace settings lifecycle"). * * Links the trace control (src/repl_trace.c), the store * (src/repl_trace_store.c, for the memory line of `trace status`), the * record model, the renderer and the shared REPL formatting module. The * store's job-table reads are satisfied by fixture definitions of * sched_get_job() and repl_job_state_name() (no jobs exist here). * * 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. * * Covers: * - every grammar form: `on all`, `on job SEL...`, `level`, `lines`, * `file PATH` (bare, quoted, with spaces), `off`, `status` (1.1, 1.2, * 1.5, 1.8, 1.12); * - selectors: plain job, element, element range, bare id (traced as a * plain job and as every element of an array with that id, 1.2, 1.3); * - every invalid-argument class leaves `next` untouched (1.9); * - a path longer than 63 characters, a 4095-byte path accepted and a * 4096-byte path rejected, and 20 job selectors (1.12); * - commands replace only `next` (1.4, 1.6, 1.10); * - `trace status` before, during and after a session (1.7). */ #define _GNU_SOURCE #include #include #include #include #include #include "repl_internal.h" #include "sched/trace.h" #include "sim.h" #include "utils.h" #define PLAIN_JOB MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)42) #define PLAIN_JOB2 MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)43) #define ARRAY_SEQ ((uint64_t)32768 * 5) #define ARRAY_BASE MAKE_JOBID(1, 2, 1000, 0x0, ARRAY_SEQ) #define ELEM(k) \ MAKE_JOBID(1, 2, 1000, TAG_JOB_ARRAY, ARRAY_SEQ | (uint64_t)(k)) #define ARRAY2_SEQ ((uint64_t)32768 * 6) #define ARRAY2_BASE MAKE_JOBID(1, 2, 1000, 0x0, ARRAY2_SEQ) #define ELEM2(k) \ MAKE_JOBID(1, 2, 1000, TAG_JOB_ARRAY, ARRAY2_SEQ | (uint64_t)(k)) static xce_sched_sim_driver_t g_sim; static xce_sched_repl_state_t g_rs; /* ---- 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"; } 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; } 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); g_sim.repl_state = NULL; } /* ---- helpers ------------------------------------------------------------- */ static char* fmt_id(uint64_t jobid) { static char bufs[8][64]; static int next = 0; char* b = bufs[next++ % 8]; fmt_jobid(b, 64, jobid); return b; } /* 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_cmd_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_text(const char* want, const char* got) { if (strcmp(want, got) != 0) { fprintf(stderr, "---- want ----\n%s---- got ----\n%s----\n", want, got); } TEST_ASSERT_EQUAL_STRING(want, got); } static void assert_run(const char* line, const char* want) { char* got = run(line); assert_text(want, got); free(got); } /* Runs a line that must succeed (prints no error) and returns `next`. */ static xce_repl_trace_settings_t* run_ok(const char* line) { char* got = run(line); if (strstr(got, "invalid") != NULL) { fprintf(stderr, "unexpected error for '%s': %s", line, got); } TEST_ASSERT_NULL(strstr(got, "invalid")); free(got); return g_rs.trace.next; } static const xce_sched_trace_ctl_t* next_ctl(void) { TEST_ASSERT_NOT_NULL(g_rs.trace.next); return repl_trace_settings_ctl(g_rs.trace.next); } static const char* next_path(void) { TEST_ASSERT_NOT_NULL(g_rs.trace.next); return repl_trace_settings_path(g_rs.trace.next); } /* 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; } static char* store_line(void) { static char b[160]; snprintf(b, sizeof(b), " stored traces: %zu bytes used of %zu bytes (64 MiB)\n", repl_trace_store_bytes(&g_rs.trace), XCE_REPL_TRACE_STORE_BOUND); return b; } /* ---- grammar ------------------------------------------------------------- */ /* 1.1, 1.8, 9.1: `on all` with every default. */ static void test_on_all_defaults(void) { const xce_sched_trace_ctl_t* ctl; assert_run("trace on all", "[sim] trace: enabled; applies from the next session\n"); ctl = next_ctl(); TEST_ASSERT_TRUE(ctl->all); TEST_ASSERT_EQUAL_INT(0, ctl->nsel); TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_SUMMARY, ctl->level); TEST_ASSERT_EQUAL_INT(XCE_TRACE_LINE_BOUND_DEFAULT, ctl->line_bound); TEST_ASSERT_NULL(next_path()); TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, PLAIN_JOB)); TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM(9))); } /* Level, line bound and file, in any order before `file`; extra * whitespace and tabs. */ 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 "); 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()); run_ok("trace on all lines 100000 level summary"); ctl = next_ctl(); TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_SUMMARY, ctl->level); TEST_ASSERT_EQUAL_INT(100000, ctl->line_bound); TEST_ASSERT_NULL(next_path()); } /* 1.2, 1.3: every selector form. A bare id is a plain job or an array * that may not exist yet, so it selects both. */ static void test_on_job_selectors(void) { char line[512]; const xce_sched_trace_ctl_t* ctl; snprintf(line, sizeof(line), "trace on job %s %s[3] %s[5-6] level full", fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), fmt_id(ARRAY2_BASE)); run_ok(line); ctl = next_ctl(); TEST_ASSERT_FALSE(ctl->all); TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_FULL, ctl->level); /* a bare id that is not array-aligned can only be a plain job * (parse_jobsel rejects [k] on it), so it adds no whole-array selector */ TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, PLAIN_JOB)); TEST_ASSERT_FALSE(sched_trace_ctl_wants( ctl, MAKE_JOBID(1, 2, 1000, TAG_JOB_ARRAY, (uint64_t)42))); TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, PLAIN_JOB2)); /* one element */ TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM(3))); TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM(2))); TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM(4))); /* an element range */ TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM2(4))); TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM2(5))); TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM2(6))); TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM2(7))); /* an aligned bare id is a plain job or an array that may not exist * yet: it selects the job and every element, including later ones */ snprintf(line, sizeof(line), "trace on job %s", fmt_id(ARRAY_BASE)); run_ok(line); ctl = next_ctl(); TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM(1))); TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ELEM(32767))); TEST_ASSERT_TRUE(sched_trace_ctl_wants(ctl, ARRAY_BASE)); TEST_ASSERT_FALSE(sched_trace_ctl_wants(ctl, ELEM2(1))); } /* 1.12: 20 job selectors on one line (more than the tokenizer's 16). */ static void test_twenty_selectors(void) { char line[2048]; size_t off; int i; const xce_sched_trace_ctl_t* ctl; off = (size_t)snprintf(line, sizeof(line), "trace on job"); for (i = 0; i < 20; i++) { off += (size_t)snprintf( line + off, sizeof(line) - off, " %s", fmt_id(MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)(100 + i)))); } snprintf(line + off, sizeof(line) - off, " lines 75"); run_ok(line); ctl = next_ctl(); TEST_ASSERT_EQUAL_INT(75, ctl->line_bound); for (i = 0; i < 20; i++) { TEST_ASSERT_TRUE(sched_trace_ctl_wants( ctl, MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)(100 + i)))); } TEST_ASSERT_FALSE(sched_trace_ctl_wants( ctl, MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)120))); } /* 1.12: the file path is the rest of the line: quotes, spaces, a path * longer than 63 characters and up to 4095 bytes. */ static void test_file_paths(void) { char line[8192]; 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()); /* 200 characters */ memset(path, 'p', 200); path[0] = '/'; path[200] = '\0'; 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'; snprintf(line, sizeof(line), "trace on all file \"%s\"", path); run_ok(line); TEST_ASSERT_EQUAL_STRING(path, next_path()); /* 4096 bytes rejected, and the 4095-byte setting stays */ memset(path, 'p', 4096); path[0] = '/'; path[4096] = '\0'; snprintf(line, sizeof(line), "trace on all lines 60 file %s", path); got = run(line); assert_text("[sim] trace: invalid file: path is 4096 bytes, longer than " "4095\n", got); free(got); TEST_ASSERT_EQUAL_INT(4095, (int)strlen(next_path())); TEST_ASSERT_EQUAL_INT(XCE_TRACE_LINE_BOUND_DEFAULT, next_ctl()->line_bound); } /* 1.5: `off` clears next. */ static void test_off(void) { run_ok("trace on all"); assert_run("trace off", "[sim] trace: disabled; applies from the next session\n"); TEST_ASSERT_NULL(g_rs.trace.next); assert_run(" trace off ", "[sim] trace: disabled; applies from the next session\n"); TEST_ASSERT_NULL(g_rs.trace.next); } /* ---- invalid arguments (1.9) --------------------------------------------- */ static void assert_rejected(const char* line, const char* want) { xce_repl_trace_settings_t* before = g_rs.trace.next; assert_run(line, want); TEST_ASSERT_TRUE(before == g_rs.trace.next); } 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"); assert_rejected("trace", "[sim] trace: usage: trace on all|job " "... [level summary|full] [lines N] " "[file PATH] | trace off | trace status\n"); assert_rejected("trace bogus", "[sim] trace: invalid command: bogus\n"); assert_rejected("trace on", "[sim] trace: invalid target: (missing)\n"); assert_rejected("trace on everyone", "[sim] trace: invalid target: everyone\n"); assert_rejected("trace on job", "[sim] trace: invalid job id: (missing)\n"); assert_rejected("trace on job level full", "[sim] trace: invalid job id: (missing)\n"); snprintf(want, sizeof(want), "[sim] trace: invalid job id: 1-2-x (%s)\n", jobsel_strerror(JOBSEL_ESYNTAX)); assert_rejected("trace on job 1-2-x", want); snprintf(line, sizeof(line), "trace on job %s 7", fmt_id(PLAIN_JOB)); snprintf(want, sizeof(want), "[sim] trace: invalid job id: 7 (%s)\n", jobsel_strerror(JOBSEL_ESYNTAX)); assert_rejected(line, want); assert_rejected("trace on all level verbose", "[sim] trace: invalid level: verbose\n"); assert_rejected("trace on all level", "[sim] trace: invalid level: (missing)\n"); assert_rejected("trace on all lines 49", "[sim] trace: invalid lines: 49\n"); assert_rejected("trace on all lines 100001", "[sim] trace: invalid lines: 100001\n"); assert_rejected("trace on all lines -60", "[sim] trace: invalid lines: -60\n"); assert_rejected("trace on all lines 1e3", "[sim] trace: invalid lines: 1e3\n"); assert_rejected("trace on all lines 99999999999999999999999", "[sim] trace: invalid lines: 99999999999999999999999\n"); assert_rejected("trace on all lines", "[sim] trace: invalid lines: (missing)\n"); assert_rejected("trace on all colour red", "[sim] trace: invalid option: colour\n"); snprintf(line, sizeof(line), "trace on all job %s", fmt_id(PLAIN_JOB)); assert_rejected(line, "[sim] trace: invalid option: job\n"); assert_rejected("trace on all file", "[sim] trace: invalid file: (missing)\n"); assert_rejected("trace on all file \"\"", "[sim] trace: invalid file: (missing)\n"); assert_rejected("trace off now", "[sim] trace: invalid argument: now\n"); assert_rejected("trace status x", "[sim] trace: invalid argument: x\n"); 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()); } /* ---- settings lifecycle and status (1.4, 1.6, 1.7, 1.10) ---------------- */ static void test_status_before_any_command(void) { char want[512]; snprintf(want, sizeof(want), "[sim] trace status (next session):\n" " enabled: no\n" "%s", store_line()); assert_run("trace status", want); } static void test_status_next_settings(void) { char want[1024]; char line[256]; snprintf(line, sizeof(line), "trace on job %s %s[1-3] %s level full lines 300 file /tmp/s.log", fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), fmt_id(PLAIN_JOB2)); run_ok(line); snprintf(want, sizeof(want), "[sim] trace status (next session):\n" " enabled: yes\n" " selection: jobs %s,%s[1-3],%s\n" " level: full\n" " line bound: 300\n" " file: /tmp/s.log\n" "%s", fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), fmt_id(PLAIN_JOB2), store_line()); assert_run(" trace status ", want); } /* A bare array id selects every element, including later ones; status * shows it as [*] rather than as a plain job id. */ static void test_status_whole_array_selection(void) { char want[1024]; char line[256]; snprintf(line, sizeof(line), "trace on job %s %s", fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE)); run_ok(line); snprintf(want, sizeof(want), "[sim] trace status (next session):\n" " enabled: yes\n" " selection: jobs %s,%s[*]\n" " level: summary\n" " line bound: 1000\n" " file: none\n" "%s", fmt_id(PLAIN_JOB), fmt_id(ARRAY_BASE), store_line()); assert_run("trace status", want); } /* A command during a session replaces only next; cur keeps the settings * the session started with until the session ends. */ static void test_status_before_during_after_session(void) { char want[1024]; char line[256]; xce_repl_trace_settings_t* started; xce_sched_trace_rec_t* rec; xce_sched_trace_ctl_t rctl; /* a stored trace so the memory line is non-zero */ memset(&rctl, 0, sizeof(rctl)); rctl.all = true; rctl.line_bound = 1000; rec = sched_trace_rec_new(&rctl, PLAIN_JOB, XCE_TRACE_REC_NOT_FOUND); TEST_ASSERT_NOT_NULL(rec); repl_trace_store_put(&g_rs.trace, rec); TEST_ASSERT_TRUE(repl_trace_store_bytes(&g_rs.trace) > 0); /* before */ run_ok("trace on all"); snprintf(want, sizeof(want), "[sim] trace status (next session):\n" " enabled: yes\n" " selection: all jobs\n" " level: summary\n" " line bound: 1000\n" " file: none\n" "%s", store_line()); assert_run("trace status", want); /* during, unchanged: one block */ session_begin(); started = g_rs.trace.cur; snprintf(want, sizeof(want), "[sim] trace status (in effect for the session in progress):\n" " enabled: yes\n" " selection: all jobs\n" " level: summary\n" " line bound: 1000\n" " file: none\n" "%s", store_line()); 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", fmt_id(PLAIN_JOB)); run_ok(line); TEST_ASSERT_TRUE(started == g_rs.trace.cur); TEST_ASSERT_TRUE(g_rs.trace.cur != g_rs.trace.next); TEST_ASSERT_TRUE(repl_trace_settings_ctl(g_rs.trace.cur)->all); snprintf(want, sizeof(want), "[sim] trace status (in effect for the session in progress):\n" " enabled: yes\n" " selection: all jobs\n" " level: summary\n" " line bound: 1000\n" " file: none\n" "[sim] trace status (replaces at the next session):\n" " enabled: yes\n" " selection: jobs %s\n" " level: full\n" " line bound: 1000\n" " file: /tmp/n.log\n" "%s", fmt_id(PLAIN_JOB), store_line()); assert_run("trace status", want); /* during, disabled */ run_ok("trace off"); TEST_ASSERT_TRUE(started == g_rs.trace.cur); snprintf(want, sizeof(want), "[sim] trace status (in effect for the session in progress):\n" " enabled: yes\n" " selection: all jobs\n" " level: summary\n" " line bound: 1000\n" " file: none\n" "[sim] trace status (replaces at the next session):\n" " enabled: no\n" "%s", store_line()); assert_run("trace status", want); /* after: the session's settings are released, next is shown alone */ run_ok("trace on all lines 500"); session_end(); snprintf(want, sizeof(want), "[sim] trace status (next session):\n" " enabled: yes\n" " selection: all jobs\n" " level: summary\n" " line bound: 500\n" " file: none\n" "%s", store_line()); assert_run("trace status", want); } /* A session that started with tracing off, then enabled mid-session. */ static void test_status_session_started_off(void) { char want[512]; session_begin(); TEST_ASSERT_NULL(g_rs.trace.cur); run_ok("trace on all level full"); snprintf(want, sizeof(want), "[sim] trace status (in effect for the session in progress):\n" " enabled: no\n" "[sim] trace status (replaces at the next session):\n" " enabled: yes\n" " selection: all jobs\n" " level: full\n" " line bound: 1000\n" " file: none\n" "%s", store_line()); assert_run("trace status", want); session_end(); } /* Reference counting: NULL-safe, and a settings object outlives a * replacement while another reference holds it. */ static void test_settings_refcount(void) { xce_repl_trace_settings_t* s; 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"); 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)); repl_trace_settings_unref(s); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_on_all_defaults); RUN_TEST(test_on_all_options); RUN_TEST(test_on_job_selectors); RUN_TEST(test_twenty_selectors); RUN_TEST(test_file_paths); RUN_TEST(test_off); RUN_TEST(test_invalid_arguments_leave_settings); RUN_TEST(test_status_before_any_command); RUN_TEST(test_status_next_settings); RUN_TEST(test_status_whole_array_selection); RUN_TEST(test_status_before_during_after_session); RUN_TEST(test_status_session_started_off); RUN_TEST(test_settings_refcount); return UNITY_END(); }