zebra_network/peer/client/
tests.rs1#![allow(clippy::unwrap_in_result)]
5#![cfg_attr(feature = "proptest-impl", allow(dead_code))]
6
7use std::{
8 net::{Ipv4Addr, SocketAddrV4},
9 sync::Arc,
10};
11
12use chrono::Utc;
13use futures::{
14 channel::{mpsc, oneshot},
15 future::{self, AbortHandle, Future, FutureExt},
16};
17use tokio::{
18 sync::broadcast::{self, error::TryRecvError},
19 task::JoinHandle,
20};
21
22use zebra_chain::block::Height;
23
24use crate::{
25 constants,
26 peer::{
27 error::SharedPeerError, CancelHeartbeatTask, Client, ClientRequest, ConnectedAddr,
28 ConnectionInfo, ErrorSlot,
29 },
30 peer_set::InventoryChange,
31 protocol::{
32 external::{types::Version, AddrInVersion},
33 types::{Nonce, PeerServices},
34 },
35 BoxError, VersionMessage,
36};
37
38#[cfg(test)]
39mod vectors;
40
41pub struct ClientTestHarness {
43 client_request_receiver: Option<mpsc::Receiver<ClientRequest>>,
44 shutdown_receiver: Option<oneshot::Receiver<CancelHeartbeatTask>>,
45 #[allow(dead_code)]
46 inv_receiver: Option<broadcast::Receiver<InventoryChange>>,
47 error_slot: ErrorSlot,
48 remote_version: Version,
49 connection_aborter: AbortHandle,
50 heartbeat_aborter: AbortHandle,
51}
52
53impl ClientTestHarness {
54 pub fn build() -> ClientTestHarnessBuilder {
57 ClientTestHarnessBuilder {
58 version: None,
59 connection_task: None,
60 heartbeat_task: None,
61 connected_addr: None,
62 }
63 }
64
65 pub fn remote_version(&self) -> Version {
67 self.remote_version
68 }
69
70 pub fn wants_connection_heartbeats(&mut self) -> bool {
77 let receive_result = self
78 .shutdown_receiver
79 .as_mut()
80 .expect("heartbeat shutdown receiver endpoint has been dropped")
81 .try_recv();
82
83 match receive_result {
84 Ok(None) => true,
85 Ok(Some(CancelHeartbeatTask)) | Err(oneshot::Canceled) => false,
86 }
87 }
88
89 pub fn drop_heartbeat_shutdown_receiver(&mut self) {
91 let hearbeat_future = self
92 .shutdown_receiver
93 .take()
94 .expect("unexpected test failure: heartbeat shutdown receiver endpoint has already been dropped");
95
96 std::mem::drop(hearbeat_future);
97 }
98
99 pub fn close_outbound_client_request_receiver(&mut self) {
104 self.client_request_receiver
105 .as_mut()
106 .expect("request receiver endpoint has been dropped")
107 .close();
108 }
109
110 pub fn drop_outbound_client_request_receiver(&mut self) {
114 self.client_request_receiver
115 .take()
116 .expect("request receiver endpoint has already been dropped");
117 }
118
119 pub(crate) fn try_to_receive_outbound_client_request(&mut self) -> ReceiveRequestAttempt {
123 let receive_result = self
124 .client_request_receiver
125 .as_mut()
126 .expect("request receiver endpoint has been dropped")
127 .try_recv();
128
129 match receive_result {
130 Ok(request) => ReceiveRequestAttempt::Request(request),
131 Err(mpsc::TryRecvError::Closed) => ReceiveRequestAttempt::Closed,
132 Err(mpsc::TryRecvError::Empty) => ReceiveRequestAttempt::Empty,
133 }
134 }
135
136 #[allow(dead_code)]
142 pub fn drop_inventory_change_receiver(&mut self) {
143 self.inv_receiver
144 .take()
145 .expect("inventory change receiver endpoint has already been dropped");
146 }
147
148 #[allow(dead_code)]
154 #[allow(clippy::unwrap_in_result)]
155 pub(crate) fn try_to_receive_inventory_change(&mut self) -> Option<InventoryChange> {
156 let receive_result = self
157 .inv_receiver
158 .as_mut()
159 .expect("inventory change receiver endpoint has been dropped")
160 .try_recv();
161
162 match receive_result {
163 Ok(change) => Some(change),
164 Err(TryRecvError::Empty) => None,
165 Err(TryRecvError::Closed) => None,
166 Err(TryRecvError::Lagged(skipped_messages)) => unreachable!(
167 "unexpected lagged inventory receiver in tests, skipped {} messages",
168 skipped_messages,
169 ),
170 }
171 }
172
173 pub fn current_error(&self) -> Option<SharedPeerError> {
175 self.error_slot.try_get_error()
176 }
177
178 pub fn set_error(&self, error: impl Into<SharedPeerError>) {
184 self.error_slot
185 .try_update_error(error.into())
186 .expect("unexpected earlier error in error slot")
187 }
188
189 pub async fn stop_connection_task(&self) {
191 self.connection_aborter.abort();
192
193 tokio::task::yield_now().await;
195 }
196
197 pub async fn stop_heartbeat_task(&self) {
199 self.heartbeat_aborter.abort();
200
201 tokio::task::yield_now().await;
203 }
204}
205
206#[allow(clippy::large_enum_variant)]
213pub(crate) enum ReceiveRequestAttempt {
214 Closed,
216
217 Empty,
219
220 Request(ClientRequest),
222}
223
224impl ReceiveRequestAttempt {
225 pub fn is_closed(&self) -> bool {
228 matches!(self, ReceiveRequestAttempt::Closed)
229 }
230
231 pub fn is_empty(&self) -> bool {
233 matches!(self, ReceiveRequestAttempt::Empty)
234 }
235
236 #[allow(dead_code)]
238 pub fn request(self) -> Option<ClientRequest> {
239 match self {
240 ReceiveRequestAttempt::Request(request) => Some(request),
241 ReceiveRequestAttempt::Closed | ReceiveRequestAttempt::Empty => None,
242 }
243 }
244}
245
246pub struct ClientTestHarnessBuilder<C = future::Ready<()>, H = future::Ready<()>> {
252 connection_task: Option<C>,
253 heartbeat_task: Option<H>,
254 version: Option<Version>,
255 connected_addr: Option<ConnectedAddr>,
256}
257
258impl<C, H> ClientTestHarnessBuilder<C, H>
259where
260 C: Future<Output = ()> + Send + 'static,
261 H: Future<Output = ()> + Send + 'static,
262{
263 pub fn with_version(mut self, version: Version) -> Self {
265 self.version = Some(version);
266 self
267 }
268
269 pub fn with_connected_addr(mut self, connected_addr: ConnectedAddr) -> Self {
271 self.connected_addr = Some(connected_addr);
272 self
273 }
274
275 pub fn with_connection_task<NewC>(
277 self,
278 connection_task: NewC,
279 ) -> ClientTestHarnessBuilder<NewC, H> {
280 ClientTestHarnessBuilder {
281 connection_task: Some(connection_task),
282 heartbeat_task: self.heartbeat_task,
283 version: self.version,
284 connected_addr: self.connected_addr,
285 }
286 }
287
288 pub fn with_heartbeat_task<NewH>(
290 self,
291 heartbeat_task: NewH,
292 ) -> ClientTestHarnessBuilder<C, NewH> {
293 ClientTestHarnessBuilder {
294 connection_task: self.connection_task,
295 heartbeat_task: Some(heartbeat_task),
296 version: self.version,
297 connected_addr: self.connected_addr,
298 }
299 }
300
301 pub fn finish(self) -> (Client, ClientTestHarness) {
303 let (shutdown_sender, shutdown_receiver) = oneshot::channel();
304 let (client_request_sender, client_request_receiver) = mpsc::channel(1);
305 let (inv_sender, inv_receiver) = broadcast::channel(5);
306
307 let error_slot = ErrorSlot::default();
308 let remote_version = self.version.unwrap_or(Version(0));
309
310 let (connection_task, connection_aborter) =
311 Self::spawn_background_task_or_fallback(self.connection_task);
312 let (heartbeat_task, heartbeat_aborter) =
313 Self::spawn_background_task_or_fallback_with_result(self.heartbeat_task);
314
315 let negotiated_version =
316 std::cmp::min(remote_version, constants::CURRENT_NETWORK_PROTOCOL_VERSION);
317
318 let remote = VersionMessage {
319 version: remote_version,
320 services: PeerServices::default(),
321 timestamp: Utc::now(),
322 address_recv: AddrInVersion::new(
323 SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1),
324 PeerServices::default(),
325 ),
326 address_from: AddrInVersion::new(
327 SocketAddrV4::new(Ipv4Addr::LOCALHOST, 2),
328 PeerServices::default(),
329 ),
330 nonce: Nonce::default(),
331 user_agent: "client test harness".to_string(),
332 start_height: Height(0),
333 relay: true,
334 };
335
336 let connection_info = Arc::new(ConnectionInfo {
337 connected_addr: self.connected_addr.unwrap_or(ConnectedAddr::Isolated),
338 remote,
339 negotiated_version,
340 });
341
342 let client = Client {
343 connection_info,
344 shutdown_tx: Some(shutdown_sender),
345 server_tx: client_request_sender,
346 inv_collector: inv_sender,
347 error_slot: error_slot.clone(),
348 connection_task,
349 heartbeat_task,
350 };
351
352 let harness = ClientTestHarness {
353 client_request_receiver: Some(client_request_receiver),
354 shutdown_receiver: Some(shutdown_receiver),
355 inv_receiver: Some(inv_receiver),
356 error_slot,
357 remote_version,
358 connection_aborter,
359 heartbeat_aborter,
360 };
361
362 (client, harness)
363 }
364
365 fn spawn_background_task_or_fallback<T>(task_future: Option<T>) -> (JoinHandle<()>, AbortHandle)
370 where
371 T: Future<Output = ()> + Send + 'static,
372 {
373 match task_future {
374 Some(future) => Self::spawn_background_task(future),
375 None => Self::spawn_background_task(future::pending()),
376 }
377 }
378
379 fn spawn_background_task<T>(task_future: T) -> (JoinHandle<()>, AbortHandle)
381 where
382 T: Future<Output = ()> + Send + 'static,
383 {
384 let (task, abort_handle) = future::abortable(task_future);
385 let task_handle = tokio::spawn(task.map(|_result| ()));
386
387 (task_handle, abort_handle)
388 }
389
390 fn spawn_background_task_or_fallback_with_result<T>(
397 task_future: Option<T>,
398 ) -> (JoinHandle<Result<(), BoxError>>, AbortHandle)
399 where
400 T: Future<Output = ()> + Send + 'static,
401 {
402 match task_future {
403 Some(future) => Self::spawn_background_task_with_result(future),
404 None => Self::spawn_background_task_with_result(future::pending()),
405 }
406 }
407
408 fn spawn_background_task_with_result<T>(
410 task_future: T,
411 ) -> (JoinHandle<Result<(), BoxError>>, AbortHandle)
412 where
413 T: Future<Output = ()> + Send + 'static,
414 {
415 let (task, abort_handle) = future::abortable(task_future);
416 let task_handle = tokio::spawn(task.map(|_result| Ok(())));
417
418 (task_handle, abort_handle)
419 }
420}