use crafty_core::{Config, Output, RaftNode, Role};
use crafty_proto::{
AppendEntries, LogId, LogIndex, NodeId, RaftRpc, RaftRpcReply, RequestVoteReply, Round, Term,
};
fn cfg() -> Config {
Config {
election_timeout_min: 100,
election_timeout_max: 100,
heartbeat_interval: 3,
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,
}),
);
}
fn deny(n: &mut RaftNode, from: u64, term: u64) {
n.receive_reply(
NodeId(from),
RaftRpcReply::RequestVote(RequestVoteReply {
term: Term(term),
vote_granted: false,
pre_vote: false,
}),
);
}
fn pre_grant(n: &mut RaftNode, from: u64, term: u64) {
n.receive_reply(
NodeId(from),
RaftRpcReply::RequestVote(RequestVoteReply {
term: Term(term),
vote_granted: true,
pre_vote: true,
}),
);
}
fn count_vote_requests(outs: &[Output]) -> usize {
outs.iter()
.filter(|o| matches!(o, Output::Send(_, RaftRpc::RequestVote(_))))
.count()
}
#[test]
fn starts_fresh_as_follower() {
let n = node(1, &[1, 2, 3]);
assert_eq!(n.role(), Role::Follower);
assert_eq!(n.current_term(), Term(0));
assert_eq!(n.voted_for(), None);
assert!(!n.is_leader());
}
#[test]
fn election_timeout_starts_prevote_then_real_election() {
let mut n = node(1, &[1, 2, 3]);
for _ in 0..99 {
n.tick();
}
assert_eq!(n.role(), Role::Follower, "not yet at timeout");
n.tick(); assert_eq!(n.role(), Role::PreCandidate);
assert_eq!(n.current_term(), Term(0), "pre-vote does not bump the term");
assert_eq!(n.voted_for(), None, "pre-vote does not record a vote");
let outs = n.take_outputs();
assert_eq!(count_vote_requests(&outs), 2, "pre-vote asks both peers");
pre_grant(&mut n, 2, 0);
assert_eq!(n.role(), Role::Candidate);
assert_eq!(n.current_term(), Term(1));
assert_eq!(n.voted_for(), Some(NodeId(1)), "now votes for self");
assert_eq!(
count_vote_requests(&n.take_outputs()),
2,
"real vote asks both peers"
);
}
#[test]
fn prevote_does_not_promote_without_majority() {
let mut n = node(1, &[1, 2, 3, 4, 5]);
for _ in 0..100 {
n.tick();
}
assert_eq!(n.role(), Role::PreCandidate);
pre_grant(&mut n, 2, 0); assert_eq!(
n.role(),
Role::PreCandidate,
"still short of a pre-vote majority"
);
assert_eq!(n.current_term(), Term(0), "term untouched while pre-voting");
}
#[test]
fn single_node_cluster_elects_itself_immediately() {
let mut n = node(1, &[1]);
n.campaign();
assert!(n.is_leader());
assert_eq!(n.current_term(), Term(1));
assert_eq!(n.commit_index(), LogIndex(1));
assert_eq!(n.last_applied(), LogIndex(1));
let applied: Vec<_> = n
.take_outputs()
.into_iter()
.filter(|o| matches!(o, Output::Apply(_)))
.collect();
assert!(applied.is_empty(), "no-op produces no Apply");
}
#[test]
fn wins_election_with_majority() {
let mut n = node(1, &[1, 2, 3]);
n.campaign();
let _ = n.take_outputs();
assert_eq!(n.role(), Role::Candidate);
grant(&mut n, 2, 1); assert!(n.is_leader());
let outs = n.take_outputs();
let appends = outs
.iter()
.filter(|o| matches!(o, Output::Send(_, RaftRpc::AppendEntries(_))))
.count();
assert_eq!(appends, 2, "new leader heartbeats both peers");
}
#[test]
fn stays_candidate_without_majority() {
let mut n = node(1, &[1, 2, 3, 4, 5]);
n.campaign();
let _ = n.take_outputs();
grant(&mut n, 2, 1); assert_eq!(n.role(), Role::Candidate);
deny(&mut n, 3, 1);
deny(&mut n, 4, 1);
assert_eq!(n.role(), Role::Candidate, "denials do not elect");
}
#[test]
fn duplicate_grants_do_not_over_count() {
let mut n = node(1, &[1, 2, 3, 4, 5]);
n.campaign();
let _ = n.take_outputs();
grant(&mut n, 2, 1);
grant(&mut n, 2, 1); assert_eq!(
n.role(),
Role::Candidate,
"still only 2 distinct votes of 5"
);
grant(&mut n, 3, 1);
assert!(n.is_leader(), "third distinct vote wins");
}
#[test]
fn higher_term_reply_forces_step_down() {
let mut n = node(1, &[1, 2, 3]);
n.campaign();
let _ = n.take_outputs();
n.receive_reply(
NodeId(2),
RaftRpcReply::RequestVote(RequestVoteReply {
term: Term(9),
vote_granted: false,
pre_vote: false,
}),
);
assert_eq!(n.role(), Role::Follower);
assert_eq!(n.current_term(), Term(9));
}
#[test]
fn stale_vote_reply_is_ignored() {
let mut n = node(1, &[1, 2, 3]);
n.campaign(); let _ = n.take_outputs();
n.campaign(); let _ = n.take_outputs();
grant(&mut n, 2, 1); assert_eq!(n.role(), Role::Candidate, "stale-term grant ignored");
grant(&mut n, 2, 2); assert!(n.is_leader());
}
#[test]
fn new_leader_append_ends_candidacy() {
let mut n = node(1, &[1, 2, 3]);
n.campaign();
let _ = n.take_outputs();
let term = n.current_term().0;
let ae = AppendEntries {
term: Term(term),
leader_id: NodeId(2),
prev_log: LogId::ZERO,
entries: vec![],
leader_commit: LogIndex(0),
round: Round::ZERO,
};
n.receive(NodeId(2), RaftRpc::AppendEntries(ae));
assert_eq!(n.role(), Role::Follower);
assert_eq!(n.leader_id(), Some(NodeId(2)));
}