bug-1928: repl_count_jobs() and repl_info_jobs() never destroyed their job-table iterators (16 B per continue round); both now do, and the baseline and trace wiring tests run with leak detection on. bug-1929: continue-mode round summaries listed only pending jobs, so a dispatch-only round printed an empty jobids= and rounds dispatching different jobs collapsed together; rounds now record and list every affected job (scheduler-sim-repl-verbosity Req 2.1, 3.2). The baseline transcript changes only in its two jobids= lines. bug-1930: trace on job now accepts the raw decimal jobid form that info trace and info job accept, through a shared parser. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
773 lines
24 KiB
C
773 lines
24 KiB
C
/*
|
|
* test_repl_trace_wiring.c - the trace commands wired into the REPL
|
|
* (scheduler-sim-decision-trace spec, task 5.2; design.md "ReplIntegration",
|
|
* "SimIntegration" (repl.h gates), "ReplTraceControl", "System Flows";
|
|
* Requirements 1.11, 7.3, 7.4, 8.4, 8.6, 10.4, 10.5).
|
|
*
|
|
* Links every scheduler source except main.c and repl.c, and includes
|
|
* repl.c here so its static help printer, dispatcher and quit path are
|
|
* reachable. The sim driver and REPL state are zeroed fixtures with a small
|
|
* fixture job table; nothing is started.
|
|
*
|
|
* Covers:
|
|
* - help: a trailing Trace section listing the trace commands, `info
|
|
* trace` among the State Inspection lines, one description column for
|
|
* every line including the new ones (1.11);
|
|
* - routing: `trace` gets the raw line, so a command longer than the
|
|
* tokenizer's 16 tokens / 64 characters is not truncated; `info trace`
|
|
* reaches the store and the `info` usage line lists it (1.11, 10.5);
|
|
* - `set output` rejects a trace file still open for writing, including
|
|
* one kept open by the session in progress and a hard link to it, and
|
|
* leaves the output file unchanged (8.4);
|
|
* - quit and end of input shut tracing down (8.6), and only after the
|
|
* picker and allocator threads are stopped (bug-1925);
|
|
* - the continue-mode rollup reports a traced round on its own, with the
|
|
* range-flush idle rule, and never starts a collapsed range from it
|
|
* (7.3, 7.4);
|
|
* - the repl.h gates forward only when the REPL is interactive.
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "kvmgr.h"
|
|
#include "sched/trace.h"
|
|
#include "utils.h"
|
|
|
|
#include "../src/repl.c"
|
|
|
|
#define PEND_JOB MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)42)
|
|
|
|
static xce_sched_sim_driver_t g_sim;
|
|
static xce_sched_repl_state_t g_rs;
|
|
static xce_sched_job_t g_pend_job;
|
|
static char g_dir[64];
|
|
static char g_cwd[4096];
|
|
static int g_report_fd = -1;
|
|
|
|
/* bug-1925: what the first repl_trace_shutdown() call saw (-1: not called
|
|
* yet; 0: no thread live; 1: a picker/allocator thread still live), and how
|
|
* many stand-in threads have been cancelled. */
|
|
static int g_shutdown_saw_live = -1;
|
|
static int g_standins_started = 0;
|
|
static int g_standins_exited = 0;
|
|
|
|
/* stdout capture */
|
|
static int g_cap_fd = -1;
|
|
static int g_cap_saved = -1;
|
|
static char g_cap_path[64];
|
|
|
|
void setUp(void)
|
|
{
|
|
kvmgr_params_t p = { .key_type = KVMGR_KEY_INT,
|
|
.nslots = 59,
|
|
.delta = 16,
|
|
.datakiller = NULL };
|
|
|
|
memset(&g_sim, 0, sizeof(g_sim));
|
|
memset(&g_rs, 0, sizeof(g_rs));
|
|
g_shutdown_saw_live = -1;
|
|
g_standins_started = 0;
|
|
g_standins_exited = 0;
|
|
g_rs.units_mode = XCE_REPL_UNITS_BOTH;
|
|
g_rs.mode = XCE_REPL_MODE_STEP;
|
|
g_sim.interactive = true;
|
|
g_sim.repl_state = &g_rs;
|
|
g_sim.core.jobs = makeInstance(&xkvmgr_type, &p);
|
|
TEST_ASSERT_NOT_NULL(g_sim.core.jobs);
|
|
|
|
TEST_ASSERT_NOT_NULL(getcwd(g_cwd, sizeof(g_cwd)));
|
|
snprintf(g_dir, sizeof(g_dir), "/tmp/xce_trace_wiring_XXXXXX");
|
|
TEST_ASSERT_NOT_NULL(mkdtemp(g_dir));
|
|
TEST_ASSERT_EQUAL_INT(0, chdir(g_dir));
|
|
}
|
|
|
|
static void fixture_fini(void)
|
|
{
|
|
repl_trace_shutdown(&g_sim);
|
|
free(g_rs.collapse_pend_jobids);
|
|
g_rs.collapse_pend_jobids = NULL;
|
|
free(g_rs.collapse_decided_jobids); /* bug-1929 */
|
|
g_rs.collapse_decided_jobids = NULL;
|
|
repl_round_reset_decided(&g_rs);
|
|
if (g_sim.record_file != NULL) {
|
|
fclose(g_sim.record_file);
|
|
g_sim.record_file = NULL;
|
|
}
|
|
if (g_sim.core.jobs != NULL) {
|
|
destroy(g_sim.core.jobs);
|
|
g_sim.core.jobs = NULL;
|
|
}
|
|
}
|
|
|
|
void tearDown(void)
|
|
{
|
|
fixture_fini();
|
|
g_sim.repl_state = NULL;
|
|
unlink("a.trace");
|
|
unlink("b.trace");
|
|
unlink("rec.replay");
|
|
TEST_ASSERT_EQUAL_INT(0, chdir(g_cwd));
|
|
rmdir(g_dir);
|
|
}
|
|
|
|
/* ---- helpers ------------------------------------------------------------- */
|
|
|
|
static void cap_begin(void)
|
|
{
|
|
snprintf(g_cap_path, sizeof(g_cap_path), "/tmp/xce_trace_wire_out_XXXXXX");
|
|
g_cap_fd = mkstemp(g_cap_path);
|
|
TEST_ASSERT_TRUE(g_cap_fd >= 0);
|
|
fflush(stdout);
|
|
g_cap_saved = dup(STDOUT_FILENO);
|
|
TEST_ASSERT_TRUE(g_cap_saved >= 0);
|
|
TEST_ASSERT_TRUE(dup2(g_cap_fd, STDOUT_FILENO) >= 0);
|
|
}
|
|
|
|
/* Ends the capture and returns what was printed (caller frees). */
|
|
static char* cap_end(void)
|
|
{
|
|
off_t len;
|
|
char* buf;
|
|
|
|
fflush(stdout);
|
|
dup2(g_cap_saved, STDOUT_FILENO);
|
|
close(g_cap_saved);
|
|
|
|
len = lseek(g_cap_fd, 0, SEEK_CUR);
|
|
TEST_ASSERT_TRUE(len >= 0);
|
|
buf = (char*)calloc(1, (size_t)len + 1);
|
|
TEST_ASSERT_NOT_NULL(buf);
|
|
TEST_ASSERT_TRUE(lseek(g_cap_fd, 0, SEEK_SET) == 0);
|
|
if (len > 0) {
|
|
TEST_ASSERT_TRUE(read(g_cap_fd, buf, (size_t)len) == len);
|
|
}
|
|
close(g_cap_fd);
|
|
unlink(g_cap_path);
|
|
return buf;
|
|
}
|
|
|
|
/* Dispatches `line` as the REPL would and returns its output. */
|
|
static char* dispatch(const char* line)
|
|
{
|
|
cap_begin();
|
|
TEST_ASSERT_FALSE(repl_dispatch(&g_sim, line));
|
|
return cap_end();
|
|
}
|
|
|
|
static void dispatch_quiet(const char* line) { free(dispatch(line)); }
|
|
|
|
static char* help_text(void)
|
|
{
|
|
cap_begin();
|
|
repl_print_help();
|
|
return cap_end();
|
|
}
|
|
|
|
static void write_file(const char* path, const char* text)
|
|
{
|
|
FILE* f = fopen(path, "w");
|
|
|
|
TEST_ASSERT_NOT_NULL(f);
|
|
fputs(text, f);
|
|
fclose(f);
|
|
}
|
|
|
|
static char* read_file(const char* path)
|
|
{
|
|
FILE* f = fopen(path, "r");
|
|
char* buf;
|
|
long len;
|
|
|
|
TEST_ASSERT_NOT_NULL(f);
|
|
TEST_ASSERT_EQUAL_INT(0, fseek(f, 0, SEEK_END));
|
|
len = ftell(f);
|
|
TEST_ASSERT_TRUE(len >= 0);
|
|
rewind(f);
|
|
buf = (char*)calloc(1, (size_t)len + 1);
|
|
TEST_ASSERT_NOT_NULL(buf);
|
|
if (len > 0) {
|
|
TEST_ASSERT_EQUAL_size_t((size_t)len, fread(buf, 1, (size_t)len, f));
|
|
}
|
|
fclose(f);
|
|
return buf;
|
|
}
|
|
|
|
/* The column at which the description of the help line whose command field
|
|
* is exactly `field` begins (the field starts the line after indentation). */
|
|
static int desc_column(const char* text, const char* field)
|
|
{
|
|
size_t flen = strlen(field);
|
|
const char* p = text;
|
|
|
|
while ((p = strstr(p, field)) != NULL) {
|
|
const char* ls = p;
|
|
const char* d = p + flen;
|
|
|
|
while ((ls > text) && (*(ls - 1) == ' ')) {
|
|
ls--;
|
|
}
|
|
if (((ls == text) || (*(ls - 1) == '\n')) && (*d == ' ')) {
|
|
while ((ls > text) && (*(ls - 1) != '\n')) {
|
|
ls--;
|
|
}
|
|
while (*d == ' ') {
|
|
d++;
|
|
}
|
|
return (int)(d - ls);
|
|
}
|
|
p += flen;
|
|
}
|
|
TEST_FAIL_MESSAGE(field);
|
|
return -1;
|
|
}
|
|
|
|
/* A traced decision record for `jobid` (DECISION kind, no entries). */
|
|
static xce_sched_trace_rec_t* new_rec(uint64_t jobid)
|
|
{
|
|
xce_sched_trace_ctl_t ctl;
|
|
xce_sched_trace_rec_t* rec;
|
|
|
|
memset(&ctl, 0, sizeof(ctl));
|
|
ctl.all = true;
|
|
ctl.level = XCE_TRACE_LEVEL_SUMMARY;
|
|
ctl.line_bound = 1000;
|
|
ctl.session_no = 3;
|
|
ctl.position = 1;
|
|
rec = sched_trace_rec_new(&ctl, jobid, XCE_TRACE_REC_DECISION);
|
|
TEST_ASSERT_NOT_NULL(rec);
|
|
return rec;
|
|
}
|
|
|
|
typedef struct {
|
|
xce_decision_t d;
|
|
xce_job_alloc_t alloc;
|
|
} fixture_decision_t;
|
|
|
|
static void dispatch_of(fixture_decision_t* f, uint64_t jobid)
|
|
{
|
|
memset(f, 0, sizeof(*f));
|
|
f->alloc.jobid = jobid;
|
|
f->d.op = XCE_DECISION_DISPATCH;
|
|
f->d.u.alloc = &(f->alloc);
|
|
f->d.trace = new_rec(jobid);
|
|
}
|
|
|
|
static void add_pend_job(void)
|
|
{
|
|
memset(&g_pend_job, 0, sizeof(g_pend_job));
|
|
g_pend_job.jobid = PEND_JOB;
|
|
g_pend_job.state = JOB_STAT_PEND;
|
|
TEST_ASSERT_TRUE(
|
|
key_add(g_sim.core.jobs, (void*)g_pend_job.jobid, &g_pend_job) >= 0);
|
|
}
|
|
|
|
/* An active, non-idle collapsed range over rounds 3-4 with one dispatch
|
|
* per round and no pend job. */
|
|
static void open_dispatch_range(void)
|
|
{
|
|
g_rs.mode = XCE_REPL_MODE_CONTINUE;
|
|
g_rs.collapse_active = true;
|
|
g_rs.collapse_start_round = 3;
|
|
g_rs.collapse_end_round = 4;
|
|
g_rs.collapse_dispatch_count = 1;
|
|
}
|
|
|
|
/* Runs the round-boundary rollup for round 5 with the given counts. */
|
|
static char* rollup_round5(int ndispatch, bool traced)
|
|
{
|
|
g_rs.session_count = 5;
|
|
g_rs.last_n_dispatch = ndispatch;
|
|
g_rs.last_n_preempt = 0;
|
|
g_rs.trace.round_traced = traced;
|
|
cap_begin();
|
|
repl_round_rollup_process(&g_sim);
|
|
return cap_end();
|
|
}
|
|
|
|
/* ---- help (1.11) --------------------------------------------------------- */
|
|
|
|
static void test_help_trace_section_is_last_and_lists_trace_commands(void)
|
|
{
|
|
char* h = help_text();
|
|
char* snap = strstr(h, "\nSnapshot & Parameters:\n");
|
|
char* trace = strstr(h, "\nTrace:\n");
|
|
const char* cmds[] = { "trace on all", "trace on job <jobid>",
|
|
"trace off", "trace status" };
|
|
size_t i;
|
|
|
|
TEST_ASSERT_NOT_NULL(snap);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(trace, "help must have a Trace section");
|
|
TEST_ASSERT_TRUE_MESSAGE(snap < trace, "Trace must follow Snapshot");
|
|
TEST_ASSERT_NULL_MESSAGE(strstr(trace + 1, "\n\n"),
|
|
"Trace must be the last section");
|
|
for (i = 0; i < sizeof(cmds) / sizeof(cmds[0]); i++) {
|
|
char* pos = strstr(h, cmds[i]);
|
|
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(pos, cmds[i]);
|
|
TEST_ASSERT_TRUE_MESSAGE(pos > trace, cmds[i]);
|
|
}
|
|
TEST_ASSERT_NOT_NULL(strstr(trace, "level summary|full"));
|
|
TEST_ASSERT_NOT_NULL(strstr(trace, "lines N"));
|
|
TEST_ASSERT_NOT_NULL(strstr(trace, "file PATH"));
|
|
free(h);
|
|
}
|
|
|
|
static void test_help_info_trace_in_state_inspection(void)
|
|
{
|
|
char* h = help_text();
|
|
char* state = strstr(h, "\nState Inspection:\n");
|
|
char* snap = strstr(h, "\nSnapshot & Parameters:\n");
|
|
char* info = strstr(h, "info trace <jobid>");
|
|
|
|
TEST_ASSERT_NOT_NULL(state);
|
|
TEST_ASSERT_NOT_NULL(snap);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(info, "help must list `info trace`");
|
|
TEST_ASSERT_TRUE((state < info) && (info < snap));
|
|
free(h);
|
|
}
|
|
|
|
static void test_help_one_description_column_with_trace_lines(void)
|
|
{
|
|
char* h = help_text();
|
|
const char* fields[] = {
|
|
"step [N]", "break <EVENT_TYPE> [key=value ...]",
|
|
"info limit <name>|all", "info trace <jobid>",
|
|
"set units raw|human|both", "trace on all [OPTIONS]",
|
|
"trace on job <jobid> ... [OPTIONS]", "level summary|full",
|
|
"lines N", "file PATH",
|
|
"trace off", "trace status",
|
|
};
|
|
int col0 = desc_column(h, fields[0]);
|
|
size_t i;
|
|
|
|
for (i = 1; i < sizeof(fields) / sizeof(fields[0]); i++) {
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(col0, desc_column(h, fields[i]),
|
|
fields[i]);
|
|
}
|
|
free(h);
|
|
}
|
|
|
|
/* ---- routing (1.11, 10.5) ------------------------------------------------ */
|
|
|
|
/* 30 job ids and a path longer than 63 bytes: far past the tokenizer's
|
|
* 16 tokens / 64 characters, so only the raw line can carry them. */
|
|
static void test_trace_routed_with_raw_line(void)
|
|
{
|
|
char line[2048];
|
|
char path[256];
|
|
size_t n;
|
|
int i;
|
|
char* out;
|
|
xce_sched_trace_ctl_t* ctl;
|
|
|
|
snprintf(path, sizeof(path),
|
|
"%s/a_trace_file_with_a_name_well_beyond_sixty_four_chars.trace",
|
|
g_dir);
|
|
n = (size_t)snprintf(line, sizeof(line), "trace on job");
|
|
for (i = 0; i < 30; i++) {
|
|
n += (size_t)snprintf(line + n, sizeof(line) - n, " 1-2-1000-%d", 100 + i);
|
|
}
|
|
snprintf(line + n, sizeof(line) - n, " level full lines 200 file %s",
|
|
path);
|
|
|
|
out = dispatch(line);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "[sim] trace: enabled"), out);
|
|
free(out);
|
|
|
|
ctl = repl_trace_settings_ctl(g_rs.trace.next);
|
|
TEST_ASSERT_NOT_NULL(ctl);
|
|
TEST_ASSERT_TRUE(ctl->nsel >= 30);
|
|
TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_FULL, ctl->level);
|
|
TEST_ASSERT_EQUAL_UINT32(200, ctl->line_bound);
|
|
TEST_ASSERT_EQUAL_STRING(path,
|
|
repl_trace_settings_path(g_rs.trace.next));
|
|
|
|
out = dispatch("trace status");
|
|
TEST_ASSERT_NOT_NULL(strstr(out, "[sim] trace status"));
|
|
free(out);
|
|
out = dispatch("trace off");
|
|
TEST_ASSERT_NOT_NULL(strstr(out, "[sim] trace: disabled"));
|
|
TEST_ASSERT_NULL(g_rs.trace.next);
|
|
free(out);
|
|
unlink(path);
|
|
}
|
|
|
|
static void test_info_trace_routed_and_listed_in_usage(void)
|
|
{
|
|
char* out = dispatch("info trace");
|
|
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(
|
|
strstr(out, "[sim] info trace: usage: info trace"), out);
|
|
free(out);
|
|
|
|
out = dispatch("info trace 1-2-1000-77");
|
|
TEST_ASSERT_NULL_MESSAGE(strstr(out, "unrecognized subcommand"), out);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "[sim] info trace:"), out);
|
|
free(out);
|
|
|
|
out = dispatch("info");
|
|
TEST_ASSERT_EQUAL_STRING(
|
|
"[sim] info: usage: info <break|session|config|job|jobs|queue|"
|
|
"worker|resource|summary|limit|trace> [arg]\n",
|
|
out);
|
|
free(out);
|
|
}
|
|
|
|
/* ---- set output (8.4) ---------------------------------------------------- */
|
|
|
|
static void assert_set_output_rejected(const char* path)
|
|
{
|
|
char want[256];
|
|
FILE* before = g_sim.record_file;
|
|
char* out;
|
|
char* content;
|
|
|
|
char line[256];
|
|
|
|
snprintf(want, sizeof(want),
|
|
"[sim] set output: %s is an open trace file\n", path);
|
|
snprintf(line, sizeof(line), "set output %s", path);
|
|
out = dispatch(line);
|
|
TEST_ASSERT_EQUAL_STRING(want, out);
|
|
free(out);
|
|
TEST_ASSERT_TRUE(g_sim.record_file == before); /* output unchanged */
|
|
content = read_file("a.trace");
|
|
TEST_ASSERT_EQUAL_STRING("keep\n", content); /* not truncated */
|
|
free(content);
|
|
}
|
|
|
|
static void test_set_output_rejects_open_trace_file(void)
|
|
{
|
|
write_file("a.trace", "keep\n");
|
|
g_sim.record_file = fopen("rec.replay", "w");
|
|
TEST_ASSERT_NOT_NULL(g_sim.record_file);
|
|
dispatch_quiet("trace on all file a.trace");
|
|
TEST_ASSERT_NOT_NULL(repl_trace_settings_file(g_rs.trace.next));
|
|
|
|
assert_set_output_rejected("a.trace");
|
|
TEST_ASSERT_EQUAL_INT(0, link("a.trace", "b.trace"));
|
|
assert_set_output_rejected("b.trace");
|
|
}
|
|
|
|
/* A file a command replaced during the session stays open, and rejected,
|
|
* until the session ends (8.4 with 8.5). */
|
|
static void test_set_output_rejects_file_kept_open_by_session(void)
|
|
{
|
|
write_file("a.trace", "keep\n");
|
|
dispatch_quiet("trace on all file a.trace");
|
|
repl_trace_session_begin(&g_sim);
|
|
dispatch_quiet("trace off");
|
|
TEST_ASSERT_NULL(g_rs.trace.next);
|
|
|
|
assert_set_output_rejected("a.trace");
|
|
|
|
repl_trace_session_end(&g_sim);
|
|
TEST_ASSERT_FALSE(repl_trace_file_conflicts(&g_sim, "a.trace"));
|
|
}
|
|
|
|
/* ---- quit and end of input (8.6, bug-1925) ------------------------------ */
|
|
|
|
/* Linked with -Wl,--wrap=repl_trace_shutdown: notes whether a picker or
|
|
* allocator thread was still live when the trace settings were released.
|
|
* The allocator reads core->trace_ctl unlocked on every job pop
|
|
* (sched_allocator()), so a live one could use the ctl after the free. */
|
|
void __real_repl_trace_shutdown(xce_sched_sim_driver_t* sim);
|
|
void __wrap_repl_trace_shutdown(xce_sched_sim_driver_t* sim)
|
|
{
|
|
if (g_shutdown_saw_live < 0) {
|
|
g_shutdown_saw_live =
|
|
(sim->allocator_thread_live
|
|
|| (__atomic_load_n(&g_standins_exited, __ATOMIC_SEQ_CST)
|
|
< g_standins_started))
|
|
? 1
|
|
: 0;
|
|
}
|
|
__real_repl_trace_shutdown(sim);
|
|
}
|
|
|
|
static void standin_exited(void* arg)
|
|
{
|
|
(void)arg;
|
|
__atomic_add_fetch(&g_standins_exited, 1, __ATOMIC_SEQ_CST);
|
|
}
|
|
|
|
/* Stands in for the picker or allocator of a session paused at the REPL:
|
|
* parked in a cancellation point, as the real ones are in cbuf_get/put. */
|
|
static void* standin_thread(void* arg)
|
|
{
|
|
(void)arg;
|
|
pthread_cleanup_push(standin_exited, NULL);
|
|
for (;;) {
|
|
pause();
|
|
}
|
|
pthread_cleanup_pop(0);
|
|
return NULL;
|
|
}
|
|
|
|
/* Gives the child a live picker and allocator, as a session paused at a
|
|
* breakpoint has (sim_loop.c sets allocator_thread_live once both run). */
|
|
static void start_standin_threads(void)
|
|
{
|
|
if ((pthread_create(&(g_sim.core.jobpicker), NULL, standin_thread, NULL)
|
|
!= 0)
|
|
|| (pthread_create(&(g_sim.core.allocator), NULL, standin_thread,
|
|
NULL)
|
|
!= 0)) {
|
|
_exit(98);
|
|
}
|
|
g_standins_started = 2;
|
|
g_sim.allocator_thread_live = true;
|
|
}
|
|
|
|
/* Runs in the child at exit(), after the quit path ran: reports whether
|
|
* tracing was shut down ('N' if not) and, with threads, whether they were
|
|
* stopped before it ('L' if one was still live), then frees the fixture so
|
|
* the child exits clean. */
|
|
static void report_at_exit(void)
|
|
{
|
|
char ok = ((g_rs.trace.next == NULL) && (g_rs.trace.cur == NULL)
|
|
&& (g_sim.core.trace_ctl == NULL))
|
|
? 'Y'
|
|
: 'N';
|
|
|
|
if ((ok == 'Y') && (g_shutdown_saw_live != 0)) {
|
|
ok = 'L';
|
|
}
|
|
(void)!write(g_report_fd, &ok, 1);
|
|
close(g_report_fd);
|
|
fixture_fini();
|
|
}
|
|
|
|
static void assert_child_shuts_down(bool eof, bool threads)
|
|
{
|
|
int fds[2];
|
|
pid_t pid;
|
|
int status = 0;
|
|
char got = '?';
|
|
|
|
write_file("a.trace", "");
|
|
dispatch_quiet("trace on all file a.trace");
|
|
repl_trace_session_begin(&g_sim);
|
|
TEST_ASSERT_NOT_NULL(g_sim.core.trace_ctl);
|
|
TEST_ASSERT_EQUAL_INT(0, pipe(fds));
|
|
|
|
fflush(stdout);
|
|
pid = fork();
|
|
TEST_ASSERT_TRUE(pid >= 0);
|
|
if (pid == 0) {
|
|
int devnull = open("/dev/null", O_RDWR);
|
|
|
|
close(fds[0]);
|
|
g_report_fd = fds[1];
|
|
dup2(devnull, STDIN_FILENO);
|
|
dup2(devnull, STDOUT_FILENO);
|
|
if (threads) {
|
|
start_standin_threads();
|
|
}
|
|
atexit(report_at_exit);
|
|
if (eof) {
|
|
repl_command_loop(&g_sim); /* readline() hits EOF */
|
|
} else {
|
|
(void)repl_dispatch(&g_sim, "quit");
|
|
}
|
|
_exit(99); /* both paths must exit(0) */
|
|
}
|
|
close(fds[1]);
|
|
TEST_ASSERT_EQUAL_INT(1, (int)read(fds[0], &got, 1));
|
|
close(fds[0]);
|
|
TEST_ASSERT_EQUAL_INT(pid, waitpid(pid, &status, 0));
|
|
TEST_ASSERT_TRUE(WIFEXITED(status));
|
|
TEST_ASSERT_EQUAL_INT(0, WEXITSTATUS(status));
|
|
TEST_ASSERT_NOT_EQUAL_MESSAGE(
|
|
'L', got, "trace settings released while threads were live");
|
|
TEST_ASSERT_EQUAL_CHAR_MESSAGE('Y', got, "tracing still active at exit");
|
|
}
|
|
|
|
static void test_quit_shuts_tracing_down(void)
|
|
{
|
|
assert_child_shuts_down(false, false);
|
|
}
|
|
|
|
static void test_eof_shuts_tracing_down(void)
|
|
{
|
|
assert_child_shuts_down(true, false);
|
|
}
|
|
|
|
/* bug-1925: `quit` at a breakpoint mid-session, picker and allocator live. */
|
|
static void test_quit_stops_threads_before_trace_shutdown(void)
|
|
{
|
|
assert_child_shuts_down(false, true);
|
|
}
|
|
|
|
static void test_eof_stops_threads_before_trace_shutdown(void)
|
|
{
|
|
assert_child_shuts_down(true, true);
|
|
}
|
|
|
|
/* ---- continue-mode rollup (7.3, 7.4) ------------------------------------- */
|
|
|
|
/* An outcome identical to the active range would extend it, but a traced
|
|
* round flushes the range and is reported on its own. */
|
|
static void test_rollup_traced_round_emitted_alone_after_flush(void)
|
|
{
|
|
char* out;
|
|
char* range;
|
|
char* alone;
|
|
|
|
open_dispatch_range();
|
|
out = rollup_round5(1, true);
|
|
range = strstr(out, "[sim] round 3-4 (2 round(s)");
|
|
alone = strstr(out, "[sim] round 5-5 (1 round(s)");
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(range, out);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(alone, out);
|
|
TEST_ASSERT_TRUE(range < alone);
|
|
TEST_ASSERT_NOT_NULL(strstr(alone, "1 dispatch, 0 pend, 0 preempt"));
|
|
TEST_ASSERT_FALSE_MESSAGE(g_rs.collapse_active,
|
|
"a traced round must not start a range");
|
|
TEST_ASSERT_NULL(g_rs.collapse_pend_jobids);
|
|
free(out);
|
|
}
|
|
|
|
/* A traced round with no dispatch, preempt or pend job is idle, and is
|
|
* reported even though it is a single round. */
|
|
static void test_rollup_traced_idle_round_reported_as_idle(void)
|
|
{
|
|
char* out = rollup_round5(0, true);
|
|
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "[sim] round 5-5 (1 round(s)"),
|
|
out);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "): idle\n"), out);
|
|
TEST_ASSERT_FALSE(g_rs.collapse_active);
|
|
free(out);
|
|
}
|
|
|
|
/* The flush idle rule, not the allocation-failure fallback: a pend-only
|
|
* traced round is not idle. */
|
|
static void test_rollup_traced_pend_only_round_not_idle(void)
|
|
{
|
|
char* out;
|
|
|
|
add_pend_job();
|
|
out = rollup_round5(0, true);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "0 dispatch, 1 pend, 0 preempt"),
|
|
out);
|
|
TEST_ASSERT_NULL_MESSAGE(strstr(out, "idle"), out);
|
|
TEST_ASSERT_FALSE(g_rs.collapse_active);
|
|
free(out);
|
|
}
|
|
|
|
/* The round after a traced one starts a fresh range as before; an
|
|
* untraced round is unaffected. */
|
|
static void test_rollup_untraced_round_after_traced_starts_range(void)
|
|
{
|
|
char* out;
|
|
|
|
open_dispatch_range();
|
|
free(rollup_round5(1, true));
|
|
|
|
g_rs.session_count = 6;
|
|
g_rs.trace.round_traced = false; /* reset by the next session begin */
|
|
cap_begin();
|
|
repl_round_rollup_process(&g_sim);
|
|
out = cap_end();
|
|
TEST_ASSERT_EQUAL_STRING("", out);
|
|
TEST_ASSERT_TRUE(g_rs.collapse_active);
|
|
TEST_ASSERT_EQUAL_UINT64(6, g_rs.collapse_start_round);
|
|
free(out);
|
|
}
|
|
|
|
/* ---- repl.h gates -------------------------------------------------------- */
|
|
|
|
static void test_gates_noop_when_not_interactive(void)
|
|
{
|
|
fixture_decision_t f;
|
|
char* out;
|
|
|
|
dispatch_quiet("trace on all");
|
|
g_sim.interactive = false;
|
|
dispatch_of(&f, PEND_JOB);
|
|
|
|
cap_begin();
|
|
xce_repl_trace_session_begin(&g_sim);
|
|
xce_repl_check_trace(&g_sim, &f.d);
|
|
xce_repl_check_trace_only(&g_sim, &f.d);
|
|
xce_repl_trace_session_end(&g_sim);
|
|
out = cap_end();
|
|
|
|
TEST_ASSERT_EQUAL_STRING("", out);
|
|
TEST_ASSERT_NULL(g_sim.core.trace_ctl); /* 11.3: never published */
|
|
TEST_ASSERT_NULL(g_rs.trace.cur);
|
|
TEST_ASSERT_FALSE(g_rs.trace.session_in_progress);
|
|
TEST_ASSERT_NOT_NULL(f.d.trace); /* not consumed */
|
|
sched_trace_rec_free(f.d.trace);
|
|
free(out);
|
|
}
|
|
|
|
static void test_gates_forward_when_interactive(void)
|
|
{
|
|
fixture_decision_t f;
|
|
fixture_decision_t t;
|
|
char* out;
|
|
|
|
dispatch_quiet("trace on all");
|
|
xce_repl_trace_session_begin(&g_sim);
|
|
TEST_ASSERT_NOT_NULL(g_sim.core.trace_ctl);
|
|
TEST_ASSERT_TRUE(g_rs.trace.session_in_progress);
|
|
|
|
dispatch_of(&f, PEND_JOB);
|
|
dispatch_of(&t, PEND_JOB);
|
|
t.d.op = XCE_DECISION_TRACE;
|
|
cap_begin();
|
|
xce_repl_check_trace(&g_sim, &f.d);
|
|
xce_repl_check_trace_only(&g_sim, &t.d);
|
|
out = cap_end();
|
|
TEST_ASSERT_NULL(f.d.trace); /* moved to the store */
|
|
TEST_ASSERT_NULL(t.d.trace);
|
|
TEST_ASSERT_TRUE(strlen(out) > 0);
|
|
TEST_ASSERT_TRUE(g_rs.trace.round_traced);
|
|
free(out);
|
|
|
|
/* no record: nothing printed */
|
|
cap_begin();
|
|
xce_repl_check_trace(&g_sim, &f.d);
|
|
out = cap_end();
|
|
TEST_ASSERT_EQUAL_STRING("", out);
|
|
free(out);
|
|
|
|
xce_repl_trace_session_end(&g_sim);
|
|
TEST_ASSERT_NULL(g_sim.core.trace_ctl);
|
|
TEST_ASSERT_FALSE(g_rs.trace.session_in_progress);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_help_trace_section_is_last_and_lists_trace_commands);
|
|
RUN_TEST(test_help_info_trace_in_state_inspection);
|
|
RUN_TEST(test_help_one_description_column_with_trace_lines);
|
|
RUN_TEST(test_trace_routed_with_raw_line);
|
|
RUN_TEST(test_info_trace_routed_and_listed_in_usage);
|
|
RUN_TEST(test_set_output_rejects_open_trace_file);
|
|
RUN_TEST(test_set_output_rejects_file_kept_open_by_session);
|
|
RUN_TEST(test_quit_shuts_tracing_down);
|
|
RUN_TEST(test_eof_shuts_tracing_down);
|
|
RUN_TEST(test_quit_stops_threads_before_trace_shutdown);
|
|
RUN_TEST(test_eof_stops_threads_before_trace_shutdown);
|
|
RUN_TEST(test_rollup_traced_round_emitted_alone_after_flush);
|
|
RUN_TEST(test_rollup_traced_idle_round_reported_as_idle);
|
|
RUN_TEST(test_rollup_traced_pend_only_round_not_idle);
|
|
RUN_TEST(test_rollup_untraced_round_after_traced_starts_range);
|
|
RUN_TEST(test_gates_noop_when_not_interactive);
|
|
RUN_TEST(test_gates_forward_when_interactive);
|
|
return UNITY_END();
|
|
}
|