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
53pub struct ReceivedClientRequest(super::InProgressClientRequest);
55
56impl ReceivedClientRequest {
57 pub fn request(&self) -> &crate::Request {
59 &self.0.request
60 }
61
62 pub fn respond(self, response: Result<crate::Response, SharedPeerError>) {
64 let _ = self.0.tx.send(response);
65 }
66
67 pub fn is_canceled(&self) -> bool {
69 self.0.tx.is_canceled()
70 }
71}
72
73impl ClientTestHarness {
74 pub fn build() -> ClientTestHarnessBuilder {
77 ClientTestHarnessBuilder {
78 version: None,
79 services: None,
80 start_height: None,
81 connection_task: None,
82 heartbeat_task: None,
83 connected_addr: None,
84 }
85 }
86
87 pub fn remote_version(&self) -> Version {
89 self.remote_version
90 }
91
92 pub fn wants_connection_heartbeats(&mut self) -> bool {
99 let receive_result = self
100 .shutdown_receiver
101 .as_mut()
102 .expect("heartbeat shutdown receiver endpoint has been dropped")
103 .try_recv();
104
105 match receive_result {
106 Ok(None) => true,
107 Ok(Some(CancelHeartbeatTask)) | Err(oneshot::Canceled) => false,
108 }
109 }
110
111 pub fn drop_heartbeat_shutdown_receiver(&mut self) {
113 let hearbeat_future = self
114 .shutdown_receiver
115 .take()
116 .expect("unexpected test failure: heartbeat shutdown receiver endpoint has already been dropped");
117
118 std::mem::drop(hearbeat_future);
119 }
120
121 pub fn close_outbound_client_request_receiver(&mut self) {
126 self.client_request_receiver
127 .as_mut()
128 .expect("request receiver endpoint has been dropped")
129 .close();
130 }
131
132 pub fn drop_outbound_client_request_receiver(&mut self) {
136 self.client_request_receiver
137 .take()
138 .expect("request receiver endpoint has already been dropped");
139 }
140
141 pub fn take_request(&mut self) -> Option<ReceivedClientRequest> {
143 self.try_to_receive_outbound_client_request()
144 .request()
145 .map(|request| ReceivedClientRequest(request.into()))
146 }
147
148 pub(crate) fn try_to_receive_outbound_client_request(&mut self) -> ReceiveRequestAttempt {
152 let receive_result = self
153 .client_request_receiver
154 .as_mut()
155 .expect("request receiver endpoint has been dropped")
156 .try_recv();
157
158 match receive_result {
159 Ok(request) => ReceiveRequestAttempt::Request(request),
160 Err(mpsc::TryRecvError::Closed) => ReceiveRequestAttempt::Closed,
161 Err(mpsc::TryRecvError::Empty) => ReceiveRequestAttempt::Empty,
162 }
163 }
164
165 #[allow(dead_code)]
171 pub fn drop_inventory_change_receiver(&mut self) {
172 self.inv_receiver
173 .take()
174 .expect("inventory change receiver endpoint has already been dropped");
175 }
176
177 #[allow(dead_code)]
183 #[allow(clippy::unwrap_in_result)]
184 pub(crate) fn try_to_receive_inventory_change(&mut self) -> Option<InventoryChange> {
185 let receive_result = self
186 .inv_receiver
187 .as_mut()
188 .expect("inventory change receiver endpoint has been dropped")
189 .try_recv();
190
191 match receive_result {
192 Ok(change) => Some(change),
193 Err(TryRecvError::Empty) => None,
194 Err(TryRecvError::Closed) => None,
195 Err(TryRecvError::Lagged(skipped_messages)) => unreachable!(
196 "unexpected lagged inventory receiver in tests, skipped {} messages",
197 skipped_messages,
198 ),
199 }
200 }
201
202 pub fn current_error(&self) -> Option<SharedPeerError> {
204 self.error_slot.try_get_error()
205 }
206
207 pub fn set_error(&self, error: impl Into<SharedPeerError>) {
213 self.error_slot
214 .try_update_error(error.into())
215 .expect("unexpected earlier error in error slot")
216 }
217
218 pub async fn stop_connection_task(&self) {
220 self.connection_aborter.abort();
221
222 tokio::task::yield_now().await;
224 }
225
226 pub async fn stop_heartbeat_task(&self) {
228 self.heartbeat_aborter.abort();
229
230 tokio::task::yield_now().await;
232 }
233}
234
235#[allow(clippy::large_enum_variant)]
242pub(crate) enum ReceiveRequestAttempt {
243 Closed,
245
246 Empty,
248
249 Request(ClientRequest),
251}
252
253impl ReceiveRequestAttempt {
254 pub fn is_closed(&self) -> bool {
257 matches!(self, ReceiveRequestAttempt::Closed)
258 }
259
260 pub fn is_empty(&self) -> bool {
262 matches!(self, ReceiveRequestAttempt::Empty)
263 }
264
265 #[allow(dead_code)]
267 pub fn request(self) -> Option<ClientRequest> {
268 match self {
269 ReceiveRequestAttempt::Request(request) => Some(request),
270 ReceiveRequestAttempt::Closed | ReceiveRequestAttempt::Empty => None,
271 }
272 }
273}
274
275pub struct ClientTestHarnessBuilder<C = future::Ready<()>, H = future::Ready<()>> {
281 connection_task: Option<C>,
282 heartbeat_task: Option<H>,
283 version: Option<Version>,
284 services: Option<PeerServices>,
285 connected_addr: Option<ConnectedAddr>,
286 start_height: Option<Height>,
287}
288
289impl<C, H> ClientTestHarnessBuilder<C, H>
290where
291 C: Future<Output = ()> + Send + 'static,
292 H: Future<Output = ()> + Send + 'static,
293{
294 pub fn with_version(mut self, version: Version) -> Self {
296 self.version = Some(version);
297 self
298 }
299
300 pub fn with_services(mut self, services: PeerServices) -> Self {
302 self.services = Some(services);
303 self
304 }
305
306 pub fn with_connected_addr(mut self, connected_addr: ConnectedAddr) -> Self {
308 self.connected_addr = Some(connected_addr);
309 self
310 }
311
312 pub fn with_start_height(mut self, start_height: Height) -> Self {
314 self.start_height = Some(start_height);
315 self
316 }
317
318 pub fn with_connection_task<NewC>(
320 self,
321 connection_task: NewC,
322 ) -> ClientTestHarnessBuilder<NewC, H> {
323 ClientTestHarnessBuilder {
324 connection_task: Some(connection_task),
325 heartbeat_task: self.heartbeat_task,
326 version: self.version,
327 services: self.services,
328 connected_addr: self.connected_addr,
329 start_height: self.start_height,
330 }
331 }
332
333 pub fn with_heartbeat_task<NewH>(
335 self,
336 heartbeat_task: NewH,
337 ) -> ClientTestHarnessBuilder<C, NewH> {
338 ClientTestHarnessBuilder {
339 connection_task: self.connection_task,
340 heartbeat_task: Some(heartbeat_task),
341 version: self.version,
342 services: self.services,
343 connected_addr: self.connected_addr,
344 start_height: self.start_height,
345 }
346 }
347
348 pub fn finish(self) -> (Client, ClientTestHarness) {
350 let (shutdown_sender, shutdown_receiver) = oneshot::channel();
351 let (client_request_sender, client_request_receiver) = mpsc::channel(1);
352 let (inv_sender, inv_receiver) = broadcast::channel(5);
353
354 let error_slot = ErrorSlot::default();
355 let remote_version = self.version.unwrap_or(Version(0));
356
357 let (connection_task, connection_aborter) =
358 Self::spawn_background_task_or_fallback(self.connection_task);
359 let (heartbeat_task, heartbeat_aborter) =
360 Self::spawn_background_task_or_fallback_with_result(self.heartbeat_task);
361
362 let negotiated_version =
363 std::cmp::min(remote_version, constants::CURRENT_NETWORK_PROTOCOL_VERSION);
364
365 let remote = VersionMessage {
366 version: remote_version,
367 services: self.services.unwrap_or_default(),
368 timestamp: Utc::now(),
369 address_recv: AddrInVersion::new(
370 SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1),
371 PeerServices::default(),
372 ),
373 address_from: AddrInVersion::new(
374 SocketAddrV4::new(Ipv4Addr::LOCALHOST, 2),
375 PeerServices::default(),
376 ),
377 nonce: Nonce::default(),
378 user_agent: "client test harness".to_string(),
379 start_height: self.start_height.unwrap_or(Height(0)),
380 relay: true,
381 };
382
383 let connection_info = Arc::new(ConnectionInfo {
384 connected_addr: self.connected_addr.unwrap_or(ConnectedAddr::Isolated),
385 remote,
386 negotiated_version,
387 });
388
389 let client = Client {
390 connection_info,
391 shutdown_tx: Some(shutdown_sender),
392 server_tx: client_request_sender,
393 inv_collector: inv_sender,
394 error_slot: error_slot.clone(),
395 connection_task,
396 heartbeat_task,
397 };
398
399 let harness = ClientTestHarness {
400 client_request_receiver: Some(client_request_receiver),
401 shutdown_receiver: Some(shutdown_receiver),
402 inv_receiver: Some(inv_receiver),
403 error_slot,
404 remote_version,
405 connection_aborter,
406 heartbeat_aborter,
407 };
408
409 (client, harness)
410 }
411
412 fn spawn_background_task_or_fallback<T>(task_future: Option<T>) -> (JoinHandle<()>, AbortHandle)
417 where
418 T: Future<Output = ()> + Send + 'static,
419 {
420 match task_future {
421 Some(future) => Self::spawn_background_task(future),
422 None => Self::spawn_background_task(future::pending()),
423 }
424 }
425
426 fn spawn_background_task<T>(task_future: T) -> (JoinHandle<()>, AbortHandle)
428 where
429 T: Future<Output = ()> + Send + 'static,
430 {
431 let (task, abort_handle) = future::abortable(task_future);
432 let task_handle = tokio::spawn(task.map(|_result| ()));
433
434 (task_handle, abort_handle)
435 }
436
437 fn spawn_background_task_or_fallback_with_result<T>(
444 task_future: Option<T>,
445 ) -> (JoinHandle<Result<(), BoxError>>, AbortHandle)
446 where
447 T: Future<Output = ()> + Send + 'static,
448 {
449 match task_future {
450 Some(future) => Self::spawn_background_task_with_result(future),
451 None => Self::spawn_background_task_with_result(future::pending()),
452 }
453 }
454
455 fn spawn_background_task_with_result<T>(
457 task_future: T,
458 ) -> (JoinHandle<Result<(), BoxError>>, AbortHandle)
459 where
460 T: Future<Output = ()> + Send + 'static,
461 {
462 let (task, abort_handle) = future::abortable(task_future);
463 let task_handle = tokio::spawn(task.map(|_result| Ok(())));
464
465 (task_handle, abort_handle)
466 }
467}