From 90e7e9c999a949b16371a445bc13f9d0e4752d79 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 24 Jun 2026 13:10:31 +0000 Subject: [PATCH] feat(raft-sim-conformance): action trace recorder + raft_sim_dump_trace (task 4.1) --- .kiro/specs/raft-sim-conformance/tasks.md | 2 +- src/lib/raft/raft.h | 32 ++++++++ src/lib/raft/raft_sim.c | 60 +++++++++++++++ src/lib/testsuite/test_raft.c | 92 +++++++++++++++++++++++ 4 files changed, 185 insertions(+), 1 deletion(-) diff --git a/.kiro/specs/raft-sim-conformance/tasks.md b/.kiro/specs/raft-sim-conformance/tasks.md index 30a402ef..1f5d2a20 100644 --- a/.kiro/specs/raft-sim-conformance/tasks.md +++ b/.kiro/specs/raft-sim-conformance/tasks.md @@ -97,7 +97,7 @@ ## 4. Action scheduler and trace recorder -- [ ] 4.1 RED+GREEN: trace recorder records every action before it is applied +- [x] 4.1 RED+GREEN: trace recorder records every action before it is applied - Test: after a fixed step sequence, `cluster.trace[]` holds the actions in order; `raft_sim_dump_trace` prints seed, n, and the ordered actions - Implement `raft_sim_action_t` recording (R1.4) + `raft_sim_dump_trace` (R5.1, R5.2); quiet on success unless `RAFT_SIM_VERBOSE` (R5.4) - Observable completion: a recorded trace round-trips to a readable dump diff --git a/src/lib/raft/raft.h b/src/lib/raft/raft.h index d61e6d95..ca071245 100644 --- a/src/lib/raft/raft.h +++ b/src/lib/raft/raft.h @@ -869,6 +869,38 @@ raft_sim_invariant_t raft_sim_check_all(const raft_sim_cluster_t *c); */ const char *raft_sim_invariant_name(raft_sim_invariant_t inv); +/* + * raft_sim_action_kind_name: human-readable name for a raft_sim_action_kind_t, for + * the trace dumper (task 4.1) and scheduler diagnostics. Returns a static string; + * never NULL (an out-of-range value maps to "unknown"). + */ +const char *raft_sim_action_kind_name(raft_sim_action_kind_t kind); + +/* + * raft_sim_trace_record: append one chosen action to the cluster's replayable + * action trace (c->trace[]) at c->trace_len, then advance trace_len (Req 1.4 — the + * scheduler records the chosen action BEFORE applying it). The recorder only stores + * the action; the caller (scheduler, task 4.2) is responsible for recording each + * action just before it is applied. Bounded by RAFT_SIM_MAX_STEPS: on overflow it + * fails loud (assert) rather than writing out of bounds or silently dropping. + */ +void raft_sim_trace_record(raft_sim_cluster_t *c, raft_sim_action_t action); + +/* + * raft_sim_dump_trace: print the replayable failure trace to `fp` (Req 5.1, 5.2): + * the run `seed`, the node count `n`, the failing-invariant NAME (via + * raft_sim_invariant_name(failing)), and every recorded action in c->trace[0.. + * trace_len) on its own human-readable line (kind name, actor, msg_index, term, + * index). + * + * Quiet-on-success (Req 5.4): when failing == RAFT_SIM_INV_NONE and the + * RAFT_SIM_VERBOSE environment variable is unset, the dumper writes NOTHING. A + * caller that wants the trace on a clean run sets RAFT_SIM_VERBOSE; a real failure + * (failing != RAFT_SIM_INV_NONE) always dumps regardless of the env var. + */ +void raft_sim_dump_trace(const raft_sim_cluster_t *c, FILE *fp, + raft_sim_invariant_t failing); + /* * raft_sim_advance_commit: drive raft_advance_commit on node_idx (task 6.1). * diff --git a/src/lib/raft/raft_sim.c b/src/lib/raft/raft_sim.c index 173e04d3..edb1aa6e 100644 --- a/src/lib/raft/raft_sim.c +++ b/src/lib/raft/raft_sim.c @@ -1,6 +1,8 @@ #ifdef XCE_TESTING +#include #include +#include #include #include "raft.h" @@ -937,6 +939,64 @@ const char *raft_sim_invariant_name(raft_sim_invariant_t inv) return "unknown"; } +const char *raft_sim_action_kind_name(raft_sim_action_kind_t kind) +{ + switch (kind) { + case RAFT_SIM_ACT_RESTART: return "restart"; + case RAFT_SIM_ACT_TIMEOUT: return "timeout"; + case RAFT_SIM_ACT_REQUEST_VOTE: return "request-vote"; + case RAFT_SIM_ACT_BECOME_LEADER: return "become-leader"; + case RAFT_SIM_ACT_CLIENT_REQUEST: return "client-request"; + case RAFT_SIM_ACT_APPEND_ENTRIES: return "append-entries"; + case RAFT_SIM_ACT_ADVANCE_COMMIT: return "advance-commit"; + case RAFT_SIM_ACT_RECEIVE: return "receive"; + case RAFT_SIM_ACT_DUPLICATE_MESSAGE: return "duplicate-message"; + case RAFT_SIM_ACT_DROP_MESSAGE: return "drop-message"; + } + return "unknown"; +} + +/* + * raft_sim_trace_record: append the chosen action to c->trace[] at trace_len and + * advance trace_len (Req 1.4 ordering — the scheduler calls this BEFORE applying the + * action). RAFT_SIM_MAX_STEPS bounds the buffer: an overflow is a harness bug (the + * step budget is fixed), so fail loud via assert rather than writing out of bounds + * or silently dropping the action. + */ +void raft_sim_trace_record(raft_sim_cluster_t *c, raft_sim_action_t action) +{ + /* Fail loud on overflow — a full trace means the run exceeded its fixed step + * budget, which is a harness bug, not a silently-droppable condition. */ + assert(c->trace_len < RAFT_SIM_MAX_STEPS); + if (c->trace_len >= RAFT_SIM_MAX_STEPS) + return; /* belt-and-suspenders: never write OOB if assert is compiled out */ + + c->trace[c->trace_len++] = action; +} + +void raft_sim_dump_trace(const raft_sim_cluster_t *c, FILE *fp, + raft_sim_invariant_t failing) +{ + /* Quiet-on-success (Req 5.4): a clean run dumps nothing unless verbose tracing + * was explicitly requested. A real failure always dumps. */ + if (failing == RAFT_SIM_INV_NONE && getenv("RAFT_SIM_VERBOSE") == NULL) + return; + + fprintf(fp, "raft-sim trace: seed=%llu n=%d failing-invariant=%s steps=%d\n", + (unsigned long long)c->seed, c->n, + raft_sim_invariant_name(failing), c->trace_len); + + for (int i = 0; i < c->trace_len; i++) { + const raft_sim_action_t *a = &c->trace[i]; + /* One human-readable line per action (Req 5.2): kind name, actor, + * msg_index, term, index. */ + fprintf(fp, + " [%d] %s actor=%d msg_index=%d term=%llu index=%llu\n", + i, raft_sim_action_kind_name(a->kind), a->actor, a->msg_index, + (unsigned long long)a->term, (unsigned long long)a->index); + } +} + int raft_sim_restart_node(raft_sim_cluster_t *c, int node_idx) { if (node_idx < 0 || node_idx >= c->n) diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index 21fb2f3c..12127269 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -5177,6 +5178,96 @@ static void test_sim_2_2_reorder_prng_deterministic(void) TEST_ASSERT_TRUE(raft_sim_bag_pick(&a) < 0); } +/* + * test_sim_4_1_trace_record_and_dump (raft-sim-conformance task 4.1; Req 1.4, 5.1, 5.2, 5.4) + * + * Observable completion for the action trace recorder + dumper: + * - raft_sim_trace_record appends each action to c->trace[] in ORDER and bumps + * trace_len; the recorded fields round-trip (R1.4). + * - raft_sim_dump_trace prints seed, node count, and the failing-invariant NAME, + * plus one human-readable line per recorded action containing the kind name, + * actor, and index (R5.1, R5.2). + * - Quiet-on-success (R5.4): with failing==RAFT_SIM_INV_NONE and RAFT_SIM_VERBOSE + * unset, the dumper writes nothing. + * + * The scheduler (raft_sim_step, task 4.2) does not exist yet, so the recorder is + * driven directly with a fixed hand-built action sequence. + */ +static void test_sim_4_1_trace_record_and_dump(void) +{ + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + c.seed = 0xD15EA5EULL; + c.n = 3; + + /* A fixed, ordered action sequence: TIMEOUT on node 1, CLIENT_REQUEST on node 0, + * RECEIVE of bag message index 3. */ + raft_sim_action_t a0 = { RAFT_SIM_ACT_TIMEOUT, 1, -1, 7, 0 }; + raft_sim_action_t a1 = { RAFT_SIM_ACT_CLIENT_REQUEST, 0, -1, 7, 4 }; + raft_sim_action_t a2 = { RAFT_SIM_ACT_RECEIVE, 2, 3, 7, 4 }; + + raft_sim_trace_record(&c, a0); + raft_sim_trace_record(&c, a1); + raft_sim_trace_record(&c, a2); + + /* Recorded in order, length matches, fields round-trip (R1.4). */ + TEST_ASSERT_EQUAL_INT(3, c.trace_len); + TEST_ASSERT_EQUAL_INT(RAFT_SIM_ACT_TIMEOUT, c.trace[0].kind); + TEST_ASSERT_EQUAL_INT(1, c.trace[0].actor); + TEST_ASSERT_EQUAL_INT(RAFT_SIM_ACT_CLIENT_REQUEST, c.trace[1].kind); + TEST_ASSERT_EQUAL_INT(0, c.trace[1].actor); + TEST_ASSERT_EQUAL_UINT64(4, c.trace[1].index); + TEST_ASSERT_EQUAL_INT(RAFT_SIM_ACT_RECEIVE, c.trace[2].kind); + TEST_ASSERT_EQUAL_INT(2, c.trace[2].actor); + TEST_ASSERT_EQUAL_INT(3, c.trace[2].msg_index); + + /* Dump to an in-memory FILE* and assert the text carries seed, node count, the + * failing-invariant name, and a recognizable token per action (R5.1, R5.2). */ + char buf[4096]; + memset(buf, 0, sizeof(buf)); + FILE *fp = fmemopen(buf, sizeof(buf), "w"); + TEST_ASSERT_NOT_NULL(fp); + raft_sim_dump_trace(&c, fp, RAFT_SIM_INV_LOG_MATCHING); + fclose(fp); + + TEST_ASSERT_TRUE(strlen(buf) > 0); + TEST_ASSERT_NOT_NULL(strstr(buf, "log-matching")); /* failing invariant name */ + TEST_ASSERT_NOT_NULL(strstr(buf, "timeout")); /* action kind name */ + TEST_ASSERT_NOT_NULL(strstr(buf, "client-request")); + TEST_ASSERT_NOT_NULL(strstr(buf, "receive")); + { + /* seed printed (hex, matching the dumper's own format) and node count. */ + char seedtok[32]; + snprintf(seedtok, sizeof(seedtok), "%llu", + (unsigned long long)c.seed); + char ntok[16]; + snprintf(ntok, sizeof(ntok), "%d", c.n); + TEST_ASSERT_NOT_NULL(strstr(buf, seedtok)); + TEST_ASSERT_NOT_NULL(strstr(buf, ntok)); + } + + /* Quiet-on-success (R5.4): no failure + RAFT_SIM_VERBOSE unset => nothing written. */ + char *saved_verbose = getenv("RAFT_SIM_VERBOSE"); + char saved_copy[256]; + int had_verbose = (saved_verbose != NULL); + if (had_verbose) { + strncpy(saved_copy, saved_verbose, sizeof(saved_copy) - 1); + saved_copy[sizeof(saved_copy) - 1] = '\0'; + } + unsetenv("RAFT_SIM_VERBOSE"); + + char qbuf[256]; + memset(qbuf, 0, sizeof(qbuf)); + FILE *qfp = fmemopen(qbuf, sizeof(qbuf), "w"); + TEST_ASSERT_NOT_NULL(qfp); + raft_sim_dump_trace(&c, qfp, RAFT_SIM_INV_NONE); + fclose(qfp); + TEST_ASSERT_EQUAL_INT(0, (int)strlen(qbuf)); /* silent on success */ + + if (had_verbose) + setenv("RAFT_SIM_VERBOSE", saved_copy, 1); +} + /* ---------- Group A: Basic Election Correctness (task 5.1) ---------- */ /* @@ -7759,6 +7850,7 @@ void test_raft(void) /* bag-only Drop/Duplicate/Reorder mutate only the bag (task 2.2) */ RUN_TEST(test_sim_2_2_drop_duplicate_preserve_node_state); RUN_TEST(test_sim_2_2_reorder_prng_deterministic); + RUN_TEST(test_sim_4_1_trace_record_and_dump); /* Group A: basic election correctness (task 5.1) */ RUN_TEST(test_proto_A1_single_node_self_elects);