feat(raft-sim-conformance): action trace recorder + raft_sim_dump_trace (task 4.1)
This commit is contained in:
parent
f0be46fb32
commit
90e7e9c999
4 changed files with 185 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#ifdef XCE_TESTING
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue