radicle: introduce sync::announce tests
Test various cases for the `Announcer` to help ensure correct behaviour.
This commit is contained in:
parent
aa7dcd92db
commit
d1ba665ebf
|
|
@ -8,6 +8,7 @@ use crate::node::NodeId;
|
||||||
|
|
||||||
use super::{PrivateNetwork, ReplicationFactor};
|
use super::{PrivateNetwork, ReplicationFactor};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Announcer {
|
pub struct Announcer {
|
||||||
local_node: NodeId,
|
local_node: NodeId,
|
||||||
target: Target,
|
target: Target,
|
||||||
|
|
@ -283,6 +284,7 @@ impl AnnouncerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of running an [`Announcer`] process.
|
/// Result of running an [`Announcer`] process.
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum AnnouncerResult {
|
pub enum AnnouncerResult {
|
||||||
/// The target of the [`Announcer`] was successfully met.
|
/// The target of the [`Announcer`] was successfully met.
|
||||||
Success(Success),
|
Success(Success),
|
||||||
|
|
@ -332,6 +334,7 @@ impl From<NoNodes> for AnnouncerResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct NoNodes {
|
pub struct NoNodes {
|
||||||
synced: BTreeMap<NodeId, SyncStatus>,
|
synced: BTreeMap<NodeId, SyncStatus>,
|
||||||
}
|
}
|
||||||
|
|
@ -343,6 +346,7 @@ impl NoNodes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct TimedOut {
|
pub struct TimedOut {
|
||||||
synced: BTreeMap<NodeId, SyncStatus>,
|
synced: BTreeMap<NodeId, SyncStatus>,
|
||||||
timed_out: BTreeSet<NodeId>,
|
timed_out: BTreeSet<NodeId>,
|
||||||
|
|
@ -360,6 +364,7 @@ impl TimedOut {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Success {
|
pub struct Success {
|
||||||
outcome: SuccessfulOutcome,
|
outcome: SuccessfulOutcome,
|
||||||
synced: BTreeMap<NodeId, SyncStatus>,
|
synced: BTreeMap<NodeId, SyncStatus>,
|
||||||
|
|
@ -378,6 +383,7 @@ impl Success {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error in constructing the [`Announcer`].
|
/// Error in constructing the [`Announcer`].
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum AnnouncerError {
|
pub enum AnnouncerError {
|
||||||
/// Both sets of already synchronized and un-synchronized nodes were empty
|
/// Both sets of already synchronized and un-synchronized nodes were empty
|
||||||
/// of nodes were empty.
|
/// of nodes were empty.
|
||||||
|
|
@ -394,6 +400,7 @@ impl From<AlreadySynced> for AnnouncerError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct AlreadySynced {
|
pub struct AlreadySynced {
|
||||||
preferred: usize,
|
preferred: usize,
|
||||||
synced: usize,
|
synced: usize,
|
||||||
|
|
@ -412,7 +419,7 @@ impl AlreadySynced {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The status of the synchronized node.
|
/// The status of the synchronized node.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum SyncStatus {
|
pub enum SyncStatus {
|
||||||
/// The node was already synchronized before starting the [`Announcer`]
|
/// The node was already synchronized before starting the [`Announcer`]
|
||||||
/// process.
|
/// process.
|
||||||
|
|
@ -490,3 +497,370 @@ pub enum SuccessfulOutcome {
|
||||||
MinReplicationFactor { preferred: usize, synced: usize },
|
MinReplicationFactor { preferred: usize, synced: usize },
|
||||||
MaxReplicationFactor { preferred: usize, synced: usize },
|
MaxReplicationFactor { preferred: usize, synced: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::test::arbitrary;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn announcer_reached_min_replication_target() {
|
||||||
|
let local = arbitrary::gen::<NodeId>(0);
|
||||||
|
let seeds = arbitrary::set::<NodeId>(10..=10);
|
||||||
|
let unsynced = seeds.iter().skip(3).copied().collect::<BTreeSet<_>>();
|
||||||
|
let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
ReplicationFactor::must_reach(3),
|
||||||
|
preferred_seeds.clone(),
|
||||||
|
BTreeSet::new(),
|
||||||
|
unsynced.clone(),
|
||||||
|
);
|
||||||
|
let mut announcer = Announcer::new(config).unwrap();
|
||||||
|
let to_sync = announcer.to_sync();
|
||||||
|
assert_eq!(to_sync, unsynced.union(&preferred_seeds).copied().collect());
|
||||||
|
|
||||||
|
let mut synced_result = BTreeMap::new();
|
||||||
|
let mut success = None;
|
||||||
|
let mut successes = 0;
|
||||||
|
|
||||||
|
for node in preferred_seeds.iter() {
|
||||||
|
let t = time::Duration::from_secs(1);
|
||||||
|
synced_result.insert(*node, SyncStatus::Synced { duration: t });
|
||||||
|
successes += 1;
|
||||||
|
match announcer.synced_with(*node, t) {
|
||||||
|
ControlFlow::Continue(progress) => {
|
||||||
|
assert_eq!(progress.synced(), successes)
|
||||||
|
}
|
||||||
|
ControlFlow::Break(stop) => {
|
||||||
|
success = Some(stop);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for node in unsynced.iter() {
|
||||||
|
assert_ne!(*node, local);
|
||||||
|
let t = time::Duration::from_secs(1);
|
||||||
|
synced_result.insert(*node, SyncStatus::Synced { duration: t });
|
||||||
|
successes += 1;
|
||||||
|
match announcer.synced_with(*node, t) {
|
||||||
|
ControlFlow::Continue(progress) => {
|
||||||
|
assert_eq!(progress.synced(), successes)
|
||||||
|
}
|
||||||
|
ControlFlow::Break(stop) => {
|
||||||
|
success = Some(stop);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(*success.as_ref().unwrap().synced(), synced_result);
|
||||||
|
assert_eq!(
|
||||||
|
success.as_ref().unwrap().outcome(),
|
||||||
|
SuccessfulOutcome::MinReplicationFactor {
|
||||||
|
preferred: 2,
|
||||||
|
synced: 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn announcer_reached_max_replication_target() {
|
||||||
|
let local = arbitrary::gen::<NodeId>(0);
|
||||||
|
let seeds = arbitrary::set::<NodeId>(10..=10);
|
||||||
|
let unsynced = seeds.iter().skip(3).copied().collect::<BTreeSet<_>>();
|
||||||
|
let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
ReplicationFactor::range(3, 6),
|
||||||
|
preferred_seeds.clone(),
|
||||||
|
BTreeSet::new(),
|
||||||
|
unsynced.clone(),
|
||||||
|
);
|
||||||
|
let mut announcer = Announcer::new(config).unwrap();
|
||||||
|
let to_sync = announcer.to_sync();
|
||||||
|
assert_eq!(to_sync, unsynced.union(&preferred_seeds).copied().collect());
|
||||||
|
|
||||||
|
let mut synced_result = BTreeMap::new();
|
||||||
|
let mut success = None;
|
||||||
|
let mut successes = 0;
|
||||||
|
|
||||||
|
for node in preferred_seeds.iter() {
|
||||||
|
let t = time::Duration::from_secs(1);
|
||||||
|
synced_result.insert(*node, SyncStatus::Synced { duration: t });
|
||||||
|
successes += 1;
|
||||||
|
match announcer.synced_with(*node, t) {
|
||||||
|
ControlFlow::Continue(progress) => {
|
||||||
|
assert_eq!(progress.synced(), successes)
|
||||||
|
}
|
||||||
|
ControlFlow::Break(stop) => {
|
||||||
|
success = Some(stop);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for node in unsynced.iter() {
|
||||||
|
assert_ne!(*node, local);
|
||||||
|
let t = time::Duration::from_secs(1);
|
||||||
|
synced_result.insert(*node, SyncStatus::Synced { duration: t });
|
||||||
|
successes += 1;
|
||||||
|
match announcer.synced_with(*node, t) {
|
||||||
|
ControlFlow::Continue(progress) => {
|
||||||
|
assert_eq!(progress.synced(), successes)
|
||||||
|
}
|
||||||
|
ControlFlow::Break(stop) => {
|
||||||
|
success = Some(stop);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(*success.as_ref().unwrap().synced(), synced_result);
|
||||||
|
assert_eq!(
|
||||||
|
success.as_ref().unwrap().outcome(),
|
||||||
|
SuccessfulOutcome::MaxReplicationFactor {
|
||||||
|
preferred: 2,
|
||||||
|
synced: 6,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn announcer_must_reach_preferred_seeds() {
|
||||||
|
let local = arbitrary::gen::<NodeId>(0);
|
||||||
|
let seeds = arbitrary::set::<NodeId>(10..=10);
|
||||||
|
let unsynced = seeds.iter().skip(2).copied().collect::<BTreeSet<_>>();
|
||||||
|
let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
ReplicationFactor::range(3, 6),
|
||||||
|
preferred_seeds.clone(),
|
||||||
|
BTreeSet::new(),
|
||||||
|
unsynced.clone(),
|
||||||
|
);
|
||||||
|
let mut announcer = Announcer::new(config).unwrap();
|
||||||
|
let to_sync = announcer.to_sync();
|
||||||
|
assert_eq!(to_sync, unsynced.union(&preferred_seeds).copied().collect());
|
||||||
|
|
||||||
|
let mut synced_result = BTreeMap::new();
|
||||||
|
let mut success = None;
|
||||||
|
let mut successes = 0;
|
||||||
|
|
||||||
|
for node in unsynced.iter() {
|
||||||
|
assert_ne!(*node, local);
|
||||||
|
let t = time::Duration::from_secs(1);
|
||||||
|
synced_result.insert(*node, SyncStatus::Synced { duration: t });
|
||||||
|
successes += 1;
|
||||||
|
match announcer.synced_with(*node, t) {
|
||||||
|
ControlFlow::Continue(progress) => {
|
||||||
|
assert_eq!(progress.synced(), successes)
|
||||||
|
}
|
||||||
|
ControlFlow::Break(stop) => {
|
||||||
|
success = Some(stop);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for node in preferred_seeds.iter() {
|
||||||
|
let t = time::Duration::from_secs(1);
|
||||||
|
synced_result.insert(*node, SyncStatus::Synced { duration: t });
|
||||||
|
successes += 1;
|
||||||
|
match announcer.synced_with(*node, t) {
|
||||||
|
ControlFlow::Continue(progress) => {
|
||||||
|
assert_eq!(progress.synced(), successes)
|
||||||
|
}
|
||||||
|
ControlFlow::Break(stop) => {
|
||||||
|
success = Some(stop);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(*success.as_ref().unwrap().synced(), synced_result);
|
||||||
|
assert_eq!(
|
||||||
|
success.as_ref().unwrap().outcome(),
|
||||||
|
SuccessfulOutcome::MaxReplicationFactor {
|
||||||
|
preferred: 2,
|
||||||
|
synced: 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn announcer_will_minimise_replication_factor() {
|
||||||
|
let local = arbitrary::gen::<NodeId>(0);
|
||||||
|
let seeds = arbitrary::set::<NodeId>(10..=10);
|
||||||
|
let unsynced = seeds.iter().skip(2).copied().collect::<BTreeSet<_>>();
|
||||||
|
let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
ReplicationFactor::must_reach(11),
|
||||||
|
preferred_seeds.clone(),
|
||||||
|
BTreeSet::new(),
|
||||||
|
unsynced.clone(),
|
||||||
|
);
|
||||||
|
let mut announcer = Announcer::new(config).unwrap();
|
||||||
|
let to_sync = announcer.to_sync();
|
||||||
|
assert_eq!(to_sync, unsynced.union(&preferred_seeds).copied().collect());
|
||||||
|
|
||||||
|
let mut synced_result = BTreeMap::new();
|
||||||
|
let mut success = None;
|
||||||
|
let mut successes = 0;
|
||||||
|
|
||||||
|
// Simulate not being able to reach all nodes
|
||||||
|
for node in to_sync.iter() {
|
||||||
|
assert_ne!(*node, local);
|
||||||
|
let t = time::Duration::from_secs(1);
|
||||||
|
synced_result.insert(*node, SyncStatus::Synced { duration: t });
|
||||||
|
successes += 1;
|
||||||
|
match announcer.synced_with(*node, t) {
|
||||||
|
ControlFlow::Continue(progress) => {
|
||||||
|
assert_eq!(progress.synced(), successes)
|
||||||
|
}
|
||||||
|
ControlFlow::Break(stop) => {
|
||||||
|
success = Some(stop);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(*success.as_ref().unwrap().synced(), synced_result);
|
||||||
|
assert_eq!(
|
||||||
|
success.as_ref().unwrap().outcome(),
|
||||||
|
SuccessfulOutcome::MinReplicationFactor {
|
||||||
|
preferred: 2,
|
||||||
|
synced: 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn announcer_timed_out() {
|
||||||
|
let local = arbitrary::gen::<NodeId>(0);
|
||||||
|
let seeds = arbitrary::set::<NodeId>(10..=10);
|
||||||
|
let unsynced = seeds.iter().skip(2).copied().collect::<BTreeSet<_>>();
|
||||||
|
let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
ReplicationFactor::must_reach(11),
|
||||||
|
preferred_seeds.clone(),
|
||||||
|
BTreeSet::new(),
|
||||||
|
unsynced.clone(),
|
||||||
|
);
|
||||||
|
let mut announcer = Announcer::new(config).unwrap();
|
||||||
|
let to_sync = announcer.to_sync();
|
||||||
|
assert_eq!(to_sync, unsynced.union(&preferred_seeds).copied().collect());
|
||||||
|
|
||||||
|
let mut synced_result = BTreeMap::new();
|
||||||
|
let mut announcer_result = None;
|
||||||
|
let mut successes = 0;
|
||||||
|
|
||||||
|
// Simulate not being able to reach all nodes
|
||||||
|
for node in to_sync.iter() {
|
||||||
|
assert_ne!(*node, local);
|
||||||
|
if successes > 5 {
|
||||||
|
announcer_result = Some(announcer.timed_out());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let t = time::Duration::from_secs(1);
|
||||||
|
synced_result.insert(*node, SyncStatus::Synced { duration: t });
|
||||||
|
successes += 1;
|
||||||
|
match announcer.synced_with(*node, t) {
|
||||||
|
ControlFlow::Continue(progress) => {
|
||||||
|
assert_eq!(progress.synced(), successes)
|
||||||
|
}
|
||||||
|
ControlFlow::Break(stop) => {
|
||||||
|
announcer_result = Some(stop.into());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match announcer_result {
|
||||||
|
Some(AnnouncerResult::TimedOut(timeout)) => {
|
||||||
|
assert_eq!(timeout.synced, synced_result);
|
||||||
|
assert_eq!(
|
||||||
|
timeout.timed_out,
|
||||||
|
to_sync
|
||||||
|
.difference(&synced_result.keys().copied().collect())
|
||||||
|
.copied()
|
||||||
|
.collect()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
unexpected => panic!("Expected AnnouncerResult::TimedOut, found: {unexpected:#?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_construct_announcer() {
|
||||||
|
let local = arbitrary::gen::<NodeId>(0);
|
||||||
|
let seeds = arbitrary::set::<NodeId>(10..=10);
|
||||||
|
let synced = seeds.iter().take(3).copied().collect::<BTreeSet<_>>();
|
||||||
|
let unsynced = seeds.iter().skip(3).copied().collect::<BTreeSet<_>>();
|
||||||
|
let preferred_seeds = seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
|
||||||
|
let replicas = ReplicationFactor::default();
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
ReplicationFactor::default(),
|
||||||
|
BTreeSet::new(),
|
||||||
|
BTreeSet::new(),
|
||||||
|
BTreeSet::new(),
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
Announcer::new(config),
|
||||||
|
Err(AnnouncerError::NoSeeds)
|
||||||
|
));
|
||||||
|
|
||||||
|
// No nodes to sync
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
replicas,
|
||||||
|
preferred_seeds.clone(),
|
||||||
|
synced.clone(),
|
||||||
|
BTreeSet::new(),
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
Announcer::new(config),
|
||||||
|
Err(AnnouncerError::AlreadySynced { .. })
|
||||||
|
));
|
||||||
|
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
ReplicationFactor::must_reach(0),
|
||||||
|
BTreeSet::new(),
|
||||||
|
synced.clone(),
|
||||||
|
unsynced.clone(),
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
Announcer::new(config),
|
||||||
|
Err(AnnouncerError::Target(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
ReplicationFactor::MustReach(2),
|
||||||
|
preferred_seeds.clone(),
|
||||||
|
synced.clone(),
|
||||||
|
unsynced.clone(),
|
||||||
|
);
|
||||||
|
// Min replication factor
|
||||||
|
assert!(matches!(
|
||||||
|
Announcer::new(config),
|
||||||
|
Err(AnnouncerError::AlreadySynced { .. })
|
||||||
|
));
|
||||||
|
let config = AnnouncerConfig::public(
|
||||||
|
local,
|
||||||
|
ReplicationFactor::range(2, 3),
|
||||||
|
preferred_seeds,
|
||||||
|
synced,
|
||||||
|
unsynced,
|
||||||
|
);
|
||||||
|
// Max replication factor
|
||||||
|
assert!(matches!(
|
||||||
|
Announcer::new(config),
|
||||||
|
Err(AnnouncerError::AlreadySynced { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue