radicle: Announcer ensures local node is ignored

Adds test cases to ensure that the local node is ignored from the announcement
process.
This commit is contained in:
Fintan Halpenny 2025-08-06 10:21:40 +01:00 committed by Lorenz Leutgeb
parent 8a6e55502c
commit 84f11f5e31
1 changed files with 246 additions and 4 deletions

View File

@ -399,7 +399,7 @@ impl Success {
}
/// Error in constructing the [`Announcer`].
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub enum AnnouncerError {
/// Both sets of already synchronized and un-synchronized nodes were empty
/// of nodes were empty.
@ -433,7 +433,7 @@ impl From<AlreadySynced> for AnnouncerError {
}
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub struct AlreadySynced {
/// The number of preferred nodes that are synchronized.
preferred: usize,
@ -491,7 +491,7 @@ impl Progress {
}
}
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
#[error("a minimum number of replicas or set of preferred seeds must be provided")]
pub struct TargetError;
@ -548,7 +548,7 @@ pub enum SuccessfulOutcome {
#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod test {
use crate::test::arbitrary;
use crate::{assert_matches, test::arbitrary};
use super::*;
@ -976,4 +976,246 @@ mod test {
"synced + unsynced should equal the total nodes we started with"
);
}
#[test]
fn local_node_in_preferred_seeds() {
let local = arbitrary::gen::<NodeId>(0);
let other_seeds = arbitrary::set::<NodeId>(5..=5);
// Include local node in preferred seeds
let mut preferred_seeds = other_seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
preferred_seeds.insert(local);
let unsynced = other_seeds.iter().skip(2).copied().collect::<BTreeSet<_>>();
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(3),
preferred_seeds.clone(),
BTreeSet::new(),
unsynced.clone(),
);
let announcer = Announcer::new(config).unwrap();
// Verify local node was removed from target's preferred seeds
assert!(
!announcer.target().preferred_seeds().contains(&local),
"Local node should be removed from preferred seeds in target"
);
// Verify local node is not in to_sync
assert!(
!announcer.to_sync().contains(&local),
"Local node should not be in to_sync set"
);
// The preferred seeds in the target should be one less than what we passed in
assert_eq!(
announcer.target().preferred_seeds().len(),
preferred_seeds.len() - 1,
"Target should have local node removed from preferred seeds"
);
}
#[test]
fn local_node_in_synced_set() {
let local = arbitrary::gen::<NodeId>(0);
let other_seeds = arbitrary::set::<NodeId>(5..=5);
// Include local node in synced set
let mut synced = other_seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
synced.insert(local);
let unsynced = other_seeds.iter().skip(2).copied().collect::<BTreeSet<_>>();
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(4),
BTreeSet::new(),
synced.clone(),
unsynced.clone(),
);
let announcer = Announcer::new(config).unwrap();
// Verify local node is not counted in synced nodes
assert!(
!announcer.synced.contains_key(&local),
"Local node should not be in internal synced map"
);
// Progress should reflect only the non-local synced nodes
assert_eq!(
announcer.progress().synced(),
synced.len() - 1,
"Progress should not count local node as synced"
);
}
#[test]
fn local_node_in_unsynced_set() {
let local = arbitrary::gen::<NodeId>(0);
let other_seeds = arbitrary::set::<NodeId>(5..=5);
let synced = other_seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
// Include local node in unsynced set
let mut unsynced = other_seeds.iter().skip(2).copied().collect::<BTreeSet<_>>();
unsynced.insert(local);
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(4),
BTreeSet::new(),
synced.clone(),
unsynced.clone(),
);
let announcer = Announcer::new(config).unwrap();
// Verify local node is not in to_sync
assert!(
!announcer.to_sync().contains(&local),
"Local node should not be in to_sync set"
);
// The internal to_sync should not contain local node
assert!(
!announcer.to_sync.contains(&local),
"Internal to_sync should not contain local node"
);
// Progress unsynced count should not include local node
assert_eq!(
announcer.progress().unsynced(),
unsynced.len() - 1,
"Progress unsynced should not count local node"
);
}
#[test]
fn local_node_in_multiple_sets() {
let local = arbitrary::gen::<NodeId>(0);
let other_seeds = arbitrary::set::<NodeId>(5..=5);
// Include local node in ALL sets
let mut preferred_seeds = other_seeds.iter().take(2).copied().collect::<BTreeSet<_>>();
preferred_seeds.insert(local);
let mut synced = other_seeds
.iter()
.skip(2)
.take(1)
.copied()
.collect::<BTreeSet<_>>();
synced.insert(local);
let mut unsynced = other_seeds.iter().skip(3).copied().collect::<BTreeSet<_>>();
unsynced.insert(local);
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(3),
preferred_seeds.clone(),
synced.clone(),
unsynced.clone(),
);
let announcer = Announcer::new(config).unwrap();
// Verify local node is completely absent from all internal structures
assert!(
!announcer.target().preferred_seeds().contains(&local),
"Local node should be removed from preferred seeds"
);
assert!(
!announcer.synced.contains_key(&local),
"Local node should not be in synced map"
);
assert!(
!announcer.to_sync().contains(&local),
"Local node should not be in to_sync"
);
assert!(
!announcer.to_sync.contains(&local),
"Local node should not be in internal to_sync"
);
// Verify counts are correct (excluding local node from all)
assert_eq!(
announcer.target().preferred_seeds().len(),
preferred_seeds.len() - 1
);
assert_eq!(announcer.progress().synced(), synced.len() - 1);
// The unsynced nodes includes the preferred seeds, since they are not
// in the synced set, and `- 1` from each for the local node
assert_eq!(
announcer.progress().unsynced(),
(unsynced.len() - 1) + (preferred_seeds.len() - 1)
);
}
#[test]
fn synced_with_local_node_is_ignored() {
let local = arbitrary::gen::<NodeId>(0);
let unsynced = arbitrary::set::<NodeId>(3..=3).into_iter().collect();
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(2),
BTreeSet::new(),
BTreeSet::new(),
unsynced,
);
let mut announcer = Announcer::new(config).unwrap();
let initial_progress = announcer.progress();
// Try to sync with the local node - this should be ignored
let duration = time::Duration::from_secs(1);
match announcer.synced_with(local, duration) {
ControlFlow::Continue(progress) => {
// Progress should be unchanged
assert_eq!(
progress.synced(),
initial_progress.synced(),
"Syncing with local node should not change synced count"
);
assert_eq!(
progress.unsynced(),
initial_progress.unsynced(),
"Syncing with local node should not change unsynced count"
);
}
ControlFlow::Break(_) => panic!("Should not reach target by syncing with local node"),
}
// Verify local node is still not in synced map
assert!(
!announcer.synced.contains_key(&local),
"Local node should not be added to synced map"
);
}
#[test]
fn local_node_only_in_all_sets_results_in_no_seeds_error() {
let local = arbitrary::gen::<NodeId>(0);
// Create sets that contain ONLY the local node
let preferred_seeds = [local].iter().copied().collect::<BTreeSet<_>>();
let synced = [local].iter().copied().collect::<BTreeSet<_>>();
let unsynced = [local].iter().copied().collect::<BTreeSet<_>>();
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(1),
preferred_seeds,
synced,
unsynced,
);
// After removing local node from all sets, we should get NoSeeds error
assert_matches!(Announcer::new(config), Err(AnnouncerError::NoSeeds));
}
}