use crafty_core::{Config, RaftNode};
use crafty_proto::{AppendEntriesReply, NodeId, RaftRpcReply, RequestVoteReply, Round, Term};
fn cfg() -> Config {
Config {
election_timeout_min: 100,
election_timeout_max: 100,
heartbeat_interval: 5,
seed: 1,
..Default::default()
}
}
fn node(id: u64, members: &[u64]) -> RaftNode {
RaftNode::new(NodeId(id), members.iter().copied().map(NodeId), cfg())
}
fn grant(n: &mut RaftNode, from: u64, term: u64) {
n.receive_reply(
NodeId(from),
RaftRpcReply::RequestVote(RequestVoteReply {
term: Term(term),
vote_granted: true,
pre_vote: false,
}),
);
let _ = n.take_outputs();
}
fn ack(n: &mut RaftNode, from: u64, term: u64, round: u64) {
n.receive_reply(
NodeId(from),
RaftRpcReply::AppendEntries(AppendEntriesReply {
term: Term(term),
success: true,
conflict_index: None,
conflict_term: None,
round: Round(round),
}),
);
let _ = n.take_outputs();
}
fn ids(nodes: &[u64]) -> Vec<NodeId> {
nodes.iter().copied().map(NodeId).collect()
}
fn leader(members: &[u64], quorum_peers: &[u64]) -> RaftNode {
let mut n = node(1, members);
n.campaign();
let _ = n.take_outputs();
for p in quorum_peers {
grant(&mut n, *p, 1);
}
assert!(n.is_leader(), "quorum_peers must form a voting majority");
n
}
#[test]
fn a_follower_reports_every_voter_as_reachable() {
let mut n = node(1, &[1, 2, 3]);
let mut got = n.reachable(50);
got.sort();
assert_eq!(got, ids(&[1, 2, 3]));
for _ in 0..200 {
n.tick();
let _ = n.take_outputs();
}
let mut got = n.reachable(50);
got.sort();
assert_eq!(got, ids(&[1, 2, 3]));
}
#[test]
fn a_leader_counts_itself_and_recently_acking_voters() {
let mut n = leader(&[1, 2, 3], &[2, 3]);
ack(&mut n, 2, 1, 1000);
ack(&mut n, 3, 1, 1000);
let mut got = n.reachable(50);
got.sort();
assert_eq!(got, ids(&[1, 2, 3]), "leader + two fresh acks");
}
#[test]
fn a_silent_voter_falls_out_of_the_reachable_set() {
let mut n = leader(&[1, 2, 3], &[2, 3]);
ack(&mut n, 2, 1, 1000);
ack(&mut n, 3, 1, 1000);
for _ in 0..80 {
n.tick();
let _ = n.take_outputs();
ack(&mut n, 2, 1, 1000);
}
assert_eq!(
n.voters(),
ids(&[1, 2, 3]),
"membership is unchanged — node 3 is still a committed voter"
);
let mut got = n.reachable(50);
got.sort();
assert_eq!(
got,
ids(&[1, 2]),
"the silent voter is dropped from the liveness set"
);
}
#[test]
fn reachability_is_earned_fresh_each_term() {
let mut n = leader(&[1, 2, 3], &[2, 3]);
ack(&mut n, 2, 1, 1000);
ack(&mut n, 3, 1, 1000);
assert_eq!(n.reachable(50).len(), 3);
n.receive_reply(
NodeId(2),
RaftRpcReply::AppendEntries(AppendEntriesReply {
term: Term(5),
success: false,
conflict_index: None,
conflict_term: None,
round: Round::ZERO,
}),
);
let _ = n.take_outputs();
assert!(!n.is_leader());
n.campaign();
let _ = n.take_outputs();
let term = n.current_term().0;
grant(&mut n, 2, term);
grant(&mut n, 3, term);
assert!(n.is_leader());
assert_eq!(
n.reachable(50),
ids(&[1]),
"no prior-term ack counts toward this term's liveness"
);
}
#[test]
fn a_single_node_leader_is_always_reachable() {
let mut n = node(1, &[1]);
n.campaign();
let _ = n.take_outputs();
assert!(n.is_leader());
assert_eq!(n.reachable(50), ids(&[1]));
assert_eq!(n.reachable_now(), ids(&[1]));
}