Skip to content

Commit 1c9fd89

Browse files
authored
chore(gossipsub): improve test structure
Improve gossipsub behavior tests structure. Split the already big enough file into multiple test files according to their `Behaviour` tests category. Document the shared test primitives and rename structures, `InjectNodes` to `BehaviourTestBuilder` and remove the `inject_nodes` functions to use `Default`. This will help with the new tests for partial messages on sigp#577 which will be eventually moved here Pull-Request: #6232.
1 parent 93a63d9 commit 1c9fd89

File tree

13 files changed

+7174
-6643
lines changed

13 files changed

+7174
-6643
lines changed

protocols/gossipsub/src/behaviour/tests.rs

Lines changed: 0 additions & 6643 deletions
This file was deleted.
Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
// Copyright 2025 Sigma Prime Pty Ltd.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the "Software"),
5+
// to deal in the Software without restriction, including without limitation
6+
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
7+
// and/or sell copies of the Software, and to permit persons to whom the
8+
// Software is furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
// DEALINGS IN THE SOFTWARE.
20+
21+
//! Tests for explicit peer handling.
22+
23+
use std::collections::BTreeSet;
24+
25+
use libp2p_identity::PeerId;
26+
use libp2p_swarm::ToSwarm;
27+
28+
use super::{count_control_msgs, disconnect_peer, flush_events, DefaultBehaviourTestBuilder};
29+
use crate::{
30+
config::{Config, ConfigBuilder},
31+
types::{Prune, RawMessage, RpcOut, Subscription, SubscriptionAction},
32+
IdentTopic as Topic,
33+
};
34+
35+
/// tests that a peer added as explicit peer gets connected to
36+
#[test]
37+
fn test_explicit_peer_gets_connected() {
38+
let (mut gs, _, _, _) = DefaultBehaviourTestBuilder::default()
39+
.peer_no(0)
40+
.topics(Vec::new())
41+
.to_subscribe(true)
42+
.create_network();
43+
44+
// create new peer
45+
let peer = PeerId::random();
46+
47+
// add peer as explicit peer
48+
gs.add_explicit_peer(&peer);
49+
50+
let num_events = gs
51+
.events
52+
.iter()
53+
.filter(|e| match e {
54+
ToSwarm::Dial { opts } => opts.get_peer_id() == Some(peer),
55+
_ => false,
56+
})
57+
.count();
58+
59+
assert_eq!(
60+
num_events, 1,
61+
"There was no dial peer event for the explicit peer"
62+
);
63+
}
64+
65+
#[test]
66+
fn test_explicit_peer_reconnects() {
67+
let config = ConfigBuilder::default()
68+
.check_explicit_peers_ticks(2)
69+
.build()
70+
.unwrap();
71+
let (mut gs, others, queues, _) = DefaultBehaviourTestBuilder::default()
72+
.peer_no(1)
73+
.topics(Vec::new())
74+
.to_subscribe(true)
75+
.gs_config(config)
76+
.create_network();
77+
78+
let peer = others.first().unwrap();
79+
80+
// add peer as explicit peer
81+
gs.add_explicit_peer(peer);
82+
83+
flush_events(&mut gs, queues);
84+
85+
// disconnect peer
86+
disconnect_peer(&mut gs, peer);
87+
88+
gs.heartbeat();
89+
90+
// check that no reconnect after first heartbeat since `explicit_peer_ticks == 2`
91+
assert_eq!(
92+
gs.events
93+
.iter()
94+
.filter(|e| match e {
95+
ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer),
96+
_ => false,
97+
})
98+
.count(),
99+
0,
100+
"There was a dial peer event before explicit_peer_ticks heartbeats"
101+
);
102+
103+
gs.heartbeat();
104+
105+
// check that there is a reconnect after second heartbeat
106+
assert!(
107+
gs.events
108+
.iter()
109+
.filter(|e| match e {
110+
ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer),
111+
_ => false,
112+
})
113+
.count()
114+
>= 1,
115+
"There was no dial peer event for the explicit peer"
116+
);
117+
}
118+
119+
#[test]
120+
fn test_handle_graft_explicit_peer() {
121+
let (mut gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default()
122+
.peer_no(1)
123+
.topics(vec![String::from("topic1"), String::from("topic2")])
124+
.to_subscribe(true)
125+
.gs_config(Config::default())
126+
.explicit(1)
127+
.create_network();
128+
129+
let peer = peers.first().unwrap();
130+
131+
gs.handle_graft(peer, topic_hashes.clone());
132+
133+
// peer got not added to mesh
134+
assert!(gs.mesh[&topic_hashes[0]].is_empty());
135+
assert!(gs.mesh[&topic_hashes[1]].is_empty());
136+
137+
// check prunes
138+
let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| {
139+
peer_id == peer
140+
&& match m {
141+
RpcOut::Prune(Prune { topic_hash, .. }) => {
142+
topic_hash == &topic_hashes[0] || topic_hash == &topic_hashes[1]
143+
}
144+
_ => false,
145+
}
146+
});
147+
assert!(
148+
control_msgs >= 2,
149+
"Not enough prunes sent when grafting from explicit peer"
150+
);
151+
}
152+
153+
#[test]
154+
fn explicit_peers_not_added_to_mesh_on_receiving_subscription() {
155+
let (gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default()
156+
.peer_no(2)
157+
.topics(vec![String::from("topic1")])
158+
.to_subscribe(true)
159+
.gs_config(Config::default())
160+
.explicit(1)
161+
.create_network();
162+
163+
// only peer 1 is in the mesh not peer 0 (which is an explicit peer)
164+
assert_eq!(
165+
gs.mesh[&topic_hashes[0]],
166+
vec![peers[1]].into_iter().collect()
167+
);
168+
169+
// assert that graft gets created to non-explicit peer
170+
let (control_msgs, queues) = count_control_msgs(queues, |peer_id, m| {
171+
peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. })
172+
});
173+
assert!(
174+
control_msgs >= 1,
175+
"No graft message got created to non-explicit peer"
176+
);
177+
178+
// assert that no graft gets created to explicit peer
179+
let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| {
180+
peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. })
181+
});
182+
assert_eq!(
183+
control_msgs, 0,
184+
"A graft message got created to an explicit peer"
185+
);
186+
}
187+
188+
#[test]
189+
fn do_not_graft_explicit_peer() {
190+
let (mut gs, others, queues, topic_hashes) = DefaultBehaviourTestBuilder::default()
191+
.peer_no(1)
192+
.topics(vec![String::from("topic")])
193+
.to_subscribe(true)
194+
.gs_config(Config::default())
195+
.explicit(1)
196+
.create_network();
197+
198+
gs.heartbeat();
199+
200+
// mesh stays empty
201+
assert_eq!(gs.mesh[&topic_hashes[0]], BTreeSet::new());
202+
203+
// assert that no graft gets created to explicit peer
204+
let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| {
205+
peer_id == &others[0] && matches!(m, RpcOut::Graft { .. })
206+
});
207+
assert_eq!(
208+
control_msgs, 0,
209+
"A graft message got created to an explicit peer"
210+
);
211+
}
212+
213+
#[test]
214+
fn do_forward_messages_to_explicit_peers() {
215+
let (mut gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default()
216+
.peer_no(2)
217+
.topics(vec![String::from("topic1"), String::from("topic2")])
218+
.to_subscribe(true)
219+
.gs_config(Config::default())
220+
.explicit(1)
221+
.create_network();
222+
223+
let local_id = PeerId::random();
224+
225+
let message = RawMessage {
226+
source: Some(peers[1]),
227+
data: vec![12],
228+
sequence_number: Some(0),
229+
topic: topic_hashes[0].clone(),
230+
signature: None,
231+
key: None,
232+
validated: true,
233+
};
234+
gs.handle_received_message(message.clone(), &local_id);
235+
assert_eq!(
236+
queues.into_iter().fold(0, |mut fwds, (peer_id, mut queue)| {
237+
while !queue.is_empty() {
238+
if matches!(queue.try_pop(), Some(RpcOut::Forward{message: m, ..}) if peer_id == peers[0] && m.data == message.data) {
239+
fwds +=1;
240+
}
241+
}
242+
fwds
243+
}),
244+
1,
245+
"The message did not get forwarded to the explicit peer"
246+
);
247+
}
248+
249+
#[test]
250+
fn explicit_peers_not_added_to_mesh_on_subscribe() {
251+
let (mut gs, peers, queues, _) = DefaultBehaviourTestBuilder::default()
252+
.peer_no(2)
253+
.topics(Vec::new())
254+
.to_subscribe(true)
255+
.gs_config(Config::default())
256+
.explicit(1)
257+
.create_network();
258+
259+
// create new topic, both peers subscribing to it but we do not subscribe to it
260+
let topic = Topic::new(String::from("t"));
261+
let topic_hash = topic.hash();
262+
for peer in peers.iter().take(2) {
263+
gs.handle_received_subscriptions(
264+
&[Subscription {
265+
action: SubscriptionAction::Subscribe,
266+
topic_hash: topic_hash.clone(),
267+
}],
268+
peer,
269+
);
270+
}
271+
272+
// subscribe now to topic
273+
gs.subscribe(&topic).unwrap();
274+
275+
// only peer 1 is in the mesh not peer 0 (which is an explicit peer)
276+
assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect());
277+
278+
// assert that graft gets created to non-explicit peer
279+
let (control_msgs, queues) = count_control_msgs(queues, |peer_id, m| {
280+
peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. })
281+
});
282+
assert!(
283+
control_msgs > 0,
284+
"No graft message got created to non-explicit peer"
285+
);
286+
287+
// assert that no graft gets created to explicit peer
288+
let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| {
289+
peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. })
290+
});
291+
assert_eq!(
292+
control_msgs, 0,
293+
"A graft message got created to an explicit peer"
294+
);
295+
}
296+
297+
#[test]
298+
fn explicit_peers_not_added_to_mesh_from_fanout_on_subscribe() {
299+
let (mut gs, peers, queues, _) = DefaultBehaviourTestBuilder::default()
300+
.peer_no(2)
301+
.topics(Vec::new())
302+
.to_subscribe(true)
303+
.gs_config(Config::default())
304+
.explicit(1)
305+
.create_network();
306+
307+
// create new topic, both peers subscribing to it but we do not subscribe to it
308+
let topic = Topic::new(String::from("t"));
309+
let topic_hash = topic.hash();
310+
for peer in peers.iter().take(2) {
311+
gs.handle_received_subscriptions(
312+
&[Subscription {
313+
action: SubscriptionAction::Subscribe,
314+
topic_hash: topic_hash.clone(),
315+
}],
316+
peer,
317+
);
318+
}
319+
320+
// we send a message for this topic => this will initialize the fanout
321+
gs.publish(topic.clone(), vec![1, 2, 3]).unwrap();
322+
323+
// subscribe now to topic
324+
gs.subscribe(&topic).unwrap();
325+
326+
// only peer 1 is in the mesh not peer 0 (which is an explicit peer)
327+
assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect());
328+
329+
// assert that graft gets created to non-explicit peer
330+
let (control_msgs, queues) = count_control_msgs(queues, |peer_id, m| {
331+
peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. })
332+
});
333+
assert!(
334+
control_msgs >= 1,
335+
"No graft message got created to non-explicit peer"
336+
);
337+
338+
// assert that no graft gets created to explicit peer
339+
let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| {
340+
peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. })
341+
});
342+
assert_eq!(
343+
control_msgs, 0,
344+
"A graft message got created to an explicit peer"
345+
);
346+
}
347+
348+
#[test]
349+
fn no_gossip_gets_sent_to_explicit_peers() {
350+
let (mut gs, peers, mut queues, topic_hashes) = DefaultBehaviourTestBuilder::default()
351+
.peer_no(2)
352+
.topics(vec![String::from("topic1"), String::from("topic2")])
353+
.to_subscribe(true)
354+
.gs_config(Config::default())
355+
.explicit(1)
356+
.create_network();
357+
358+
let local_id = PeerId::random();
359+
360+
let message = RawMessage {
361+
source: Some(peers[1]),
362+
data: vec![],
363+
sequence_number: Some(0),
364+
topic: topic_hashes[0].clone(),
365+
signature: None,
366+
key: None,
367+
validated: true,
368+
};
369+
370+
// forward the message
371+
gs.handle_received_message(message, &local_id);
372+
373+
// simulate multiple gossip calls (for randomness)
374+
for _ in 0..3 {
375+
gs.emit_gossip();
376+
}
377+
378+
// assert that no gossip gets sent to explicit peer
379+
let mut receiver_queue = queues.remove(&peers[0]).unwrap();
380+
let mut gossips = 0;
381+
while !receiver_queue.is_empty() {
382+
if let Some(RpcOut::IHave(_)) = receiver_queue.try_pop() {
383+
gossips += 1;
384+
}
385+
}
386+
assert_eq!(gossips, 0, "Gossip got emitted to explicit peer");
387+
}

0 commit comments

Comments
 (0)