radicle: Announcer test set constructions

Ensures that preferred seeds that are not synced are made part of the `to_sync`
set, and the Announcer is constructed without an error.

As well as this, ensure that the `synced` and `to_sync` sets do not overlap by
constructing the `to_sync` and removing any `synced` elements from it.
This commit is contained in:
Fintan Halpenny 2025-08-06 12:52:55 +01:00 committed by Lorenz Leutgeb
parent c15288a690
commit efa7efacc4
1 changed files with 83 additions and 10 deletions

View File

@ -38,11 +38,28 @@ impl Announcer {
config.synced.remove(&config.local_node);
config.unsynced.remove(&config.local_node);
if config.synced.is_empty() && config.unsynced.is_empty() {
// N.b extend the unsynced set with any preferred seeds that are not yet
// synced
let unsynced_preferred = config
.preferred_seeds
.difference(&config.synced)
.copied()
.collect::<BTreeSet<_>>();
config.unsynced.extend(unsynced_preferred);
// Ensure that the unsynced set does not contain any of the synced set
// we trust that the synced nodes are already synced with
let to_sync = config
.unsynced
.difference(&config.synced)
.copied()
.collect::<BTreeSet<_>>();
if config.synced.is_empty() && to_sync.is_empty() {
return Err(AnnouncerError::NoSeeds);
}
if config.unsynced.is_empty() {
if to_sync.is_empty() {
let preferred = config.synced.intersection(&config.preferred_seeds).count();
return Err(AlreadySynced {
preferred,
@ -51,13 +68,7 @@ impl Announcer {
.into());
}
// N.b extend the unsynced set with any preferred seeds that are not yet
// synced
config
.unsynced
.extend(config.preferred_seeds.difference(&config.synced).copied());
let replicas = config.replicas.min(config.unsynced.len());
let replicas = config.replicas.min(to_sync.len());
let announcer = Self {
local_node: config.local_node,
target: Target::new(config.preferred_seeds, replicas)
@ -67,7 +78,7 @@ impl Announcer {
.into_iter()
.map(|nid| (nid, SyncStatus::AlreadySynced))
.collect(),
to_sync: config.unsynced,
to_sync,
};
match announcer.is_target_reached() {
None => Ok(announcer),
@ -1155,6 +1166,68 @@ mod test {
}
}
#[test]
fn construct_only_preferred_seeds_provided() {
// Test: preferred_seeds non-empty, synced and unsynced empty
// Expected: preferred seeds should be moved to to_sync, constructor succeeds
let local = arbitrary::gen::<NodeId>(0);
let preferred_seeds = arbitrary::set::<NodeId>(2..=2)
.into_iter()
.collect::<BTreeSet<_>>();
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(1),
preferred_seeds.clone(),
BTreeSet::new(),
BTreeSet::new(),
);
let announcer = Announcer::new(config).unwrap();
// Constructor should move unsynced preferred seeds to to_sync
assert_eq!(announcer.to_sync, preferred_seeds);
assert_eq!(announcer.target().preferred_seeds(), &preferred_seeds);
assert!(announcer.synced.is_empty());
}
#[test]
fn construct_node_appears_in_multiple_input_sets() {
let local = arbitrary::gen::<NodeId>(0);
let alice = arbitrary::gen::<NodeId>(1);
let bob = arbitrary::gen::<NodeId>(2);
let eve = arbitrary::gen::<NodeId>(3);
// alice will appear in synced and unsynced
let synced = [alice].iter().copied().collect::<BTreeSet<_>>();
let unsynced = [alice, bob, eve].iter().copied().collect::<BTreeSet<_>>();
let config = AnnouncerConfig::public(
local,
ReplicationFactor::must_reach(2),
BTreeSet::new(),
synced,
unsynced,
);
let announcer = Announcer::new(config).unwrap();
// synced takes precedence over to_sync when constructing
assert!(
announcer.synced.contains_key(&alice),
"alice should be synced"
);
assert!(
!announcer.to_sync.contains(&alice),
"alice should not appear in to_sync"
);
// bob and eve should appear in to_sync
assert!(
announcer.to_sync.contains(&bob) && announcer.to_sync.contains(&eve),
"Other node should be in to_sync"
);
}
#[test]
fn cannot_construct_announcer() {
let local = arbitrary::gen::<NodeId>(0);