1use crate::{
4 DownloadResult, Event, ProgressEntry, Puller, PullerError, Pusher, multi::TokioExecutor,
5};
6use bytes::Bytes;
7use core::time::Duration;
8use crossfire::{mpmc, spsc};
9use futures::TryStreamExt;
10use std::sync::Arc;
11use tokio_util::sync::CancellationToken;
12
13#[derive(Debug, Clone, Copy)]
15pub struct DownloadOptions {
16 pub retry_gap: Duration,
17 pub push_queue_cap: usize,
18}
19
20#[allow(clippy::too_many_lines)]
37pub fn download_single<R: Puller, W: Pusher>(
38 mut puller: R,
39 mut pusher: W,
40 options: DownloadOptions,
41) -> DownloadResult<TokioExecutor<R, W::Error>, R::Error, W::Error> {
42 const ID: usize = 0;
43 let token = CancellationToken::new();
44 let (tx, event_chain) = mpmc::unbounded_async();
45 pusher.set_listener({
46 let tx = tx.clone();
47 Box::new(move |p| {
48 let _ = tx.send(Event::PushProgress(p));
49 })
50 });
51
52 let (tx_push, rx_push) =
53 spsc::bounded_async_blocking::<(ProgressEntry, Bytes)>(options.push_queue_cap);
54 let push_thread = Arc::new(std::sync::OnceLock::new());
55 let push_handle = tokio::task::spawn_blocking({
56 let push_thread = push_thread.clone();
57 let token = token.clone();
58 let tx = tx.clone();
59 move || {
60 let _ = push_thread.set(std::thread::current());
61 while let Ok((mut spin, mut data)) = rx_push.recv() {
62 loop {
63 if token.is_cancelled() {
64 return;
65 }
66 let _ = tx.send(Event::Pushing(ID, spin.clone()));
67 let len_before_push = data.len();
68 match pusher.push(&spin, data) {
69 Ok(()) => break,
70 Err((err, bytes)) => {
71 let _ = tx.send(Event::PushError(ID, spin.clone(), err));
72 let written = len_before_push.saturating_sub(bytes.len());
73 data = bytes;
74 spin.start += written as u64;
75 }
76 }
77 std::thread::park_timeout(options.retry_gap);
78 }
79 }
80 loop {
81 if token.is_cancelled() {
82 break;
83 }
84 let _ = tx.send(Event::Flushing);
85 match pusher.flush() {
86 Ok(()) => break,
87 Err(err) => {
88 let _ = tx.send(Event::FlushError(err));
89 }
90 }
91 std::thread::park_timeout(options.retry_gap);
92 }
93 }
94 });
95
96 let pull_handle = tokio::spawn(async move {
97 'redownload: loop {
98 let _ = tx.send(Event::Pulling(ID));
99 let mut downloaded: u64 = 0;
100 let mut stream = loop {
101 match puller.pull(None).await {
102 Ok(t) => break t,
103 Err((e, retry_gap)) => {
104 let _ = tx.send(Event::PullError(ID, e));
105 tokio::time::sleep(retry_gap.unwrap_or(options.retry_gap)).await;
106 }
107 }
108 };
109 loop {
110 match stream.try_next().await {
111 Ok(Some(chunk)) => {
112 if chunk.is_empty() {
113 continue;
114 }
115 let len = chunk.len() as u64;
116 let span = downloaded..(downloaded + len);
117 let _ = tx.send(Event::PullProgress(ID, span.clone()));
118 let _ = tx_push.send((span, chunk)).await;
119 downloaded += len;
120 }
121 Ok(None) => break 'redownload,
122 Err((e, retry_gap)) => {
123 let is_irrecoverable = e.is_irrecoverable();
124 let _ = tx.send(Event::PullError(ID, e));
125 tokio::time::sleep(retry_gap.unwrap_or(options.retry_gap)).await;
126 if is_irrecoverable {
127 continue 'redownload;
128 }
129 }
130 }
131 }
132 }
133 let _ = tx.send(Event::Finished(ID));
134 });
135
136 tokio::spawn({
137 let token = token.clone();
138 async move {
139 tokio::select! {
140 _ = push_handle => {},
141 () = token.cancelled() => {
142 pull_handle.abort();
143 if let Some(t) = push_thread.get() {
144 t.unpark();
145 }
146 }
147 }
148 }
149 });
150 DownloadResult::new(event_chain, None, token)
151}
152
153#[cfg(test)]
154mod tests {
155 #![allow(clippy::cast_possible_truncation)]
156 use super::*;
157 use crate::BufWriterPusher;
158 use crate::{
159 MemPusher, Merge, ProgressEntry,
160 mock::{MockPuller, build_mock_data},
161 };
162 use futures::stream;
163 use std::{dbg, vec};
164 use tokio::time::{sleep, timeout};
165 use vec::Vec;
166
167 #[tokio::test]
168 async fn test_sequential_download() {
169 let mock_data = build_mock_data(3 * 1024);
170 let puller = MockPuller::new(&mock_data);
171 let pusher = MemPusher::with_capacity(mock_data.len());
172 let receive = pusher.receive.clone();
177 #[allow(clippy::single_range_in_vec_init)]
178 let download_chunks = [0..mock_data.len() as u64];
179 let result = download_single(
180 puller,
181 pusher,
182 DownloadOptions {
183 retry_gap: Duration::from_secs(1),
184 push_queue_cap: 1024,
185 },
186 );
187
188 let mut pull_progress: Vec<ProgressEntry> = Vec::new();
189 let mut push_progress: Vec<ProgressEntry> = Vec::new();
190 while let Ok(e) = result.event_chain().recv().await {
191 match e {
192 Event::PullProgress(_, p) => pull_progress.merge_progress(p),
193 Event::PushProgress(p) => push_progress.merge_progress(p),
194 _ => {}
195 }
196 }
197 dbg!(&pull_progress);
198 dbg!(&push_progress);
199 assert_eq!(pull_progress, download_chunks);
200 assert_eq!(push_progress, download_chunks);
201
202 assert_eq!(&**receive.lock(), mock_data);
203 }
204
205 #[tokio::test]
206 async fn test_sequential_download_abort_discards() {
207 let mock_data = build_mock_data(3 * 1024);
208 let puller = MockPuller::new(&mock_data);
209 let pusher = MemPusher::with_capacity(mock_data.len());
210 let receive = pusher.receive.clone();
214 let result = download_single(
215 puller,
216 pusher,
217 DownloadOptions {
218 retry_gap: Duration::from_secs(1),
219 push_queue_cap: 1024,
220 },
221 );
222
223 result.abort();
227 assert!(result.is_aborted());
228
229 tokio::time::timeout(Duration::from_secs(10), async {
231 while result.event_chain().recv().await.is_ok() {}
232 })
233 .await
234 .expect("event loop hung after abort");
235
236 let written = receive.lock().len();
241 assert!(
242 written <= mock_data.len(),
243 "abort must not write beyond the source"
244 );
245 }
246
247 #[derive(Debug, Clone)]
261 struct SlowMockPuller {
262 data: Arc<[u8]>,
263 delay: Duration,
264 }
265 impl Puller for SlowMockPuller {
266 type Error = std::convert::Infallible;
267 #[allow(clippy::cast_possible_truncation)]
268 fn pull(
269 &mut self,
270 range: Option<&ProgressEntry>,
271 ) -> impl Future<
272 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
273 > + Send {
274 type PullItem = crate::PullResult<Bytes, std::convert::Infallible>;
275 let owned: Vec<u8> = match range {
276 Some(r) => self.data[r.start as usize..r.end as usize].to_vec(),
277 None => self.data.to_vec(),
278 };
279 let delay = self.delay;
280 async move {
281 sleep(delay).await;
282 let items: Vec<PullItem> = owned
283 .chunks(2)
284 .map(|c| Ok(Bytes::from(c.to_vec())))
285 .collect();
286 Ok(stream::iter(items))
287 }
288 }
289 }
290
291 #[tokio::test(flavor = "multi_thread")]
292 async fn test_sequential_download_abort_discards_buffered() {
293 let mock_data = build_mock_data(64 * 1024);
296 let puller = SlowMockPuller {
297 data: Arc::from(mock_data.as_slice()),
298 delay: Duration::from_millis(50),
299 };
300 let inner = MemPusher::with_capacity(mock_data.len());
303 let receive = inner.receive.clone();
304 let pusher = BufWriterPusher::new(inner, mock_data.len() + 1);
305 let result = download_single(
306 puller,
307 pusher,
308 DownloadOptions {
309 retry_gap: Duration::from_secs(1),
310 push_queue_cap: 1024,
311 },
312 );
313
314 let mut aborted = false;
318 while let Ok(e) = result.event_chain().recv().await {
319 if matches!(e, Event::Pushing(_, _)) {
320 result.abort();
321 assert!(result.is_aborted());
322 aborted = true;
323 break;
324 }
325 }
326 assert!(aborted, "expected a Pushing event before aborting");
327
328 timeout(Duration::from_secs(10), async {
330 while result.event_chain().recv().await.is_ok() {}
331 })
332 .await
333 .expect("event loop hung after abort");
334
335 let written = receive.lock().len();
339 assert_eq!(
340 written, 0,
341 "abort must discard buffered bytes, not write them to the sink"
342 );
343 }
344
345 #[cfg(feature = "file")]
346 #[tokio::test(flavor = "multi_thread")]
347 async fn test_sequential_download_abort_discards_file() {
348 use std::io::Read;
349 let mock_data = build_mock_data(64 * 1024);
353 let puller = SlowMockPuller {
354 data: Arc::from(mock_data.as_slice()),
355 delay: Duration::from_millis(50),
356 };
357 let tmp = tempfile::NamedTempFile::new().unwrap();
358 let path = tmp.path().to_path_buf();
359 let file = tokio::fs::File::from(tmp.reopen().unwrap());
360 let inner = crate::StdFilePusher::new(file, mock_data.len() as u64, false)
361 .await
362 .unwrap();
363 let pusher = BufWriterPusher::new(inner, mock_data.len() + 1);
364 let result = download_single(
365 puller,
366 pusher,
367 DownloadOptions {
368 retry_gap: Duration::from_secs(1),
369 push_queue_cap: 1024,
370 },
371 );
372
373 let mut aborted = false;
374 while let Ok(e) = result.event_chain().recv().await {
375 if matches!(e, Event::Pushing(_, _)) {
376 result.abort();
377 assert!(result.is_aborted());
378 aborted = true;
379 break;
380 }
381 }
382 assert!(aborted, "expected a Pushing event before aborting");
383
384 timeout(Duration::from_secs(10), async {
385 while result.event_chain().recv().await.is_ok() {}
386 })
387 .await
388 .expect("event loop hung after abort");
389
390 let mut f = std::fs::File::open(&path).unwrap();
394 let mut buf = Vec::new();
395 f.read_to_end(&mut buf).unwrap();
396 assert_eq!(buf.len(), mock_data.len(), "file should remain pre-sized");
397 assert!(
398 buf.iter().all(|&b| b == 0),
399 "abort must not write buffered bytes to the file"
400 );
401 }
402
403 use parking_lot::Mutex;
410 use std::sync::Arc;
411 use std::sync::atomic::{AtomicBool, Ordering};
412
413 #[derive(Debug)]
414 struct FatalErr;
415 impl std::fmt::Display for FatalErr {
416 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417 f.write_str("fatal")
418 }
419 }
420 impl std::error::Error for FatalErr {}
421 impl crate::PullerError for FatalErr {
422 fn is_irrecoverable(&self) -> bool {
423 true
424 }
425 }
426
427 #[derive(Debug)]
428 struct RecoverableErr;
429 impl std::fmt::Display for RecoverableErr {
430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431 f.write_str("recoverable")
432 }
433 }
434 impl std::error::Error for RecoverableErr {}
435 impl crate::PullerError for RecoverableErr {
436 fn is_irrecoverable(&self) -> bool {
437 false
438 }
439 }
440
441 struct FlakySink {
444 fail_push: Arc<AtomicBool>,
445 fail_flush: Arc<AtomicBool>,
446 receive: Arc<Mutex<Vec<u8>>>,
447 listener: Option<crate::ProgressListener>,
448 }
449 impl FlakySink {
450 fn new() -> Self {
451 Self {
452 fail_push: Arc::new(AtomicBool::new(false)),
453 fail_flush: Arc::new(AtomicBool::new(false)),
454 receive: Arc::new(Mutex::new(Vec::new())),
455 listener: None,
456 }
457 }
458 }
459 impl crate::Pusher for FlakySink {
460 type Error = std::io::Error;
461 fn set_listener(&mut self, cb: crate::ProgressListener) {
462 self.listener = Some(cb);
463 }
464 fn push(
465 &mut self,
466 range: &crate::ProgressEntry,
467 bytes: Bytes,
468 ) -> Result<(), (Self::Error, Bytes)> {
469 if self.fail_push.swap(false, Ordering::SeqCst) {
470 return Err((std::io::Error::other("push"), bytes));
471 }
472 let mut g = self.receive.lock();
473 if range.start as usize == g.len() {
474 g.extend_from_slice(&bytes);
475 } else {
476 if g.len() < range.end as usize {
477 g.resize(range.end as usize, 0);
478 }
479 g[range.start as usize..range.end as usize].copy_from_slice(&bytes);
480 }
481 drop(g);
482 if let Some(l) = &mut self.listener {
483 l(range.clone());
484 }
485 Ok(())
486 }
487 fn flush(&mut self) -> Result<(), Self::Error> {
488 if self.fail_flush.swap(false, Ordering::SeqCst) {
489 Err(std::io::Error::other("flush"))
490 } else {
491 Ok(())
492 }
493 }
494 }
495
496 #[derive(Debug, Clone)]
497 struct PullErrOncePuller {
498 data: Arc<[u8]>,
499 failed: Arc<AtomicBool>,
500 }
501 impl crate::Puller for PullErrOncePuller {
502 type Error = RecoverableErr;
503 fn pull(
504 &mut self,
505 range: Option<&crate::ProgressEntry>,
506 ) -> impl Future<
507 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
508 > + Send {
509 if !self.failed.swap(true, Ordering::SeqCst) {
510 return std::future::ready(Err((RecoverableErr, Some(Duration::ZERO))));
511 }
512 let data = match range {
513 Some(r) => &self.data[r.start as usize..r.end as usize],
514 None => &self.data,
515 };
516 let items: Vec<crate::PullResult<Bytes, RecoverableErr>> = data
517 .chunks(2)
518 .map(|c| Ok(Bytes::copy_from_slice(c)))
519 .collect();
520 std::future::ready(Ok(stream::iter(items)))
521 }
522 }
523
524 #[derive(Debug, Clone)]
525 struct StreamErrOncePuller {
526 data: Arc<[u8]>,
527 failed: Arc<AtomicBool>,
528 }
529 impl crate::Puller for StreamErrOncePuller {
530 type Error = FatalErr;
531 fn pull(
532 &mut self,
533 range: Option<&crate::ProgressEntry>,
534 ) -> impl Future<
535 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
536 > + Send {
537 if !self.failed.swap(true, Ordering::SeqCst) {
538 let items: Vec<crate::PullResult<Bytes, FatalErr>> =
539 vec![Err((FatalErr, Some(Duration::ZERO)))];
540 return std::future::ready(Ok(stream::iter(items)));
541 }
542 let data = match range {
543 Some(r) => &self.data[r.start as usize..r.end as usize],
544 None => &self.data,
545 };
546 let items: Vec<crate::PullResult<Bytes, FatalErr>> = data
547 .chunks(2)
548 .map(|c| Ok(Bytes::copy_from_slice(c)))
549 .collect();
550 std::future::ready(Ok(stream::iter(items)))
551 }
552 }
553
554 #[derive(Debug, Clone)]
557 struct RecoverableStreamErrOncePuller {
558 data: Arc<[u8]>,
559 failed: Arc<AtomicBool>,
560 }
561 impl crate::Puller for RecoverableStreamErrOncePuller {
562 type Error = RecoverableErr;
563 fn pull(
564 &mut self,
565 range: Option<&crate::ProgressEntry>,
566 ) -> impl Future<
567 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
568 > + Send {
569 let first = !self.failed.swap(true, Ordering::SeqCst);
574 let data = match range {
575 Some(r) => &self.data[r.start as usize..r.end as usize],
576 None => &self.data,
577 };
578 let mut items: Vec<crate::PullResult<Bytes, RecoverableErr>> = data
579 .chunks(2)
580 .map(|c| Ok(Bytes::copy_from_slice(c)))
581 .collect();
582 if first {
583 let mut with_err = vec![Err((RecoverableErr, Some(Duration::ZERO)))];
584 with_err.append(&mut items);
585 return std::future::ready(Ok(stream::iter(with_err)));
586 }
587 std::future::ready(Ok(stream::iter(items)))
588 }
589 }
590
591 #[tokio::test]
592 async fn test_single_push_error_retries() {
593 let mock_data = build_mock_data(3 * 1024);
595 let puller = MockPuller::new(&mock_data);
596 let sink = FlakySink::new();
597 sink.fail_push.store(true, Ordering::SeqCst);
598 let receive = sink.receive.clone();
599 let result = download_single(
600 puller,
601 sink,
602 DownloadOptions {
603 retry_gap: Duration::ZERO,
604 push_queue_cap: 1024,
605 },
606 );
607 while result.event_chain().recv().await.is_ok() {}
608 assert_eq!(&**receive.lock(), mock_data);
609 }
610
611 #[tokio::test]
612 async fn test_single_flush_error_retries() {
613 let mock_data = build_mock_data(3 * 1024);
615 let puller = MockPuller::new(&mock_data);
616 let sink = FlakySink::new();
617 sink.fail_flush.store(true, Ordering::SeqCst);
618 let receive = sink.receive.clone();
619 let result = download_single(
620 puller,
621 sink,
622 DownloadOptions {
623 retry_gap: Duration::ZERO,
624 push_queue_cap: 1024,
625 },
626 );
627 while result.event_chain().recv().await.is_ok() {}
628 assert_eq!(&**receive.lock(), mock_data);
629 }
630
631 #[tokio::test]
632 async fn test_single_pull_error_retries() {
633 let mock_data = build_mock_data(3 * 1024);
635 let puller = PullErrOncePuller {
636 data: Arc::from(mock_data.as_slice()),
637 failed: Arc::new(AtomicBool::new(false)),
638 };
639 let pusher = MemPusher::with_capacity(mock_data.len());
640 let receive = pusher.receive.clone();
641 let result = download_single(
642 puller,
643 pusher,
644 DownloadOptions {
645 retry_gap: Duration::ZERO,
646 push_queue_cap: 1024,
647 },
648 );
649 while result.event_chain().recv().await.is_ok() {}
650 assert_eq!(&**receive.lock(), mock_data);
651 }
652
653 #[tokio::test]
654 async fn test_single_stream_error_irrecoverable_retries() {
655 let mock_data = build_mock_data(3 * 1024);
658 let puller = StreamErrOncePuller {
659 data: Arc::from(mock_data.as_slice()),
660 failed: Arc::new(AtomicBool::new(false)),
661 };
662 let pusher = MemPusher::with_capacity(mock_data.len());
663 let receive = pusher.receive.clone();
664 let result = download_single(
665 puller,
666 pusher,
667 DownloadOptions {
668 retry_gap: Duration::ZERO,
669 push_queue_cap: 1024,
670 },
671 );
672 while result.event_chain().recv().await.is_ok() {}
673 assert_eq!(&**receive.lock(), mock_data);
674 }
675
676 #[tokio::test]
677 async fn test_single_stream_error_recoverable_retries() {
678 let mock_data = build_mock_data(3 * 1024);
682 let puller = RecoverableStreamErrOncePuller {
683 data: Arc::from(mock_data.as_slice()),
684 failed: Arc::new(AtomicBool::new(false)),
685 };
686 let pusher = MemPusher::with_capacity(mock_data.len());
687 let receive = pusher.receive.clone();
688 let result = download_single(
689 puller,
690 pusher,
691 DownloadOptions {
692 retry_gap: Duration::ZERO,
693 push_queue_cap: 1024,
694 },
695 );
696 while result.event_chain().recv().await.is_ok() {}
697 assert_eq!(&**receive.lock(), mock_data);
698 }
699
700 #[tokio::test]
701 async fn puller_and_error_coverage() {
702 assert_eq!(format!("{FatalErr}"), "fatal");
705 assert_eq!(format!("{RecoverableErr}"), "recoverable");
706
707 let mut pull_err = PullErrOncePuller {
708 data: Arc::from(b"abcdef".as_slice()),
709 failed: Arc::new(AtomicBool::new(false)),
710 };
711 let _ = pull_err.pull(Some(&(0..2u64))).await; let _ = pull_err.pull(Some(&(0..2u64))).await; let _ = pull_err.pull(None).await; let mut stream_err = StreamErrOncePuller {
716 data: Arc::from(b"abcdef".as_slice()),
717 failed: Arc::new(AtomicBool::new(false)),
718 };
719 let _ = stream_err.pull(Some(&(0..2u64))).await;
720 let _ = stream_err.pull(Some(&(0..2u64))).await; let _ = stream_err.pull(None).await; let mut rec_stream_err = RecoverableStreamErrOncePuller {
724 data: Arc::from(b"abcdef".as_slice()),
725 failed: Arc::new(AtomicBool::new(false)),
726 };
727 let _ = rec_stream_err.pull(Some(&(0..2u64))).await; let _ = rec_stream_err.pull(Some(&(0..2u64))).await; let _ = rec_stream_err.pull(None).await; }
731
732 #[test]
733 fn flaky_sink_noncontiguous_write_rebuffers() {
734 let mut sink = FlakySink::new();
737 sink.push(&(5..8u64), Bytes::from_static(b"xyz")).unwrap();
738 assert_eq!(&**sink.receive.lock(), b"\0\0\0\0\0xyz");
739 }
740
741 #[tokio::test]
742 async fn test_slow_mock_puller_some_range() {
743 let mut p = SlowMockPuller {
746 data: Arc::from(b"hello world".as_slice()),
747 delay: Duration::ZERO,
748 };
749 assert!(p.pull(Some(&(0..5))).await.is_ok());
750 }
751
752 #[tokio::test]
753 async fn test_sequential_download_empty_file() {
754 let mock_data: Vec<u8> = Vec::new();
758 let puller = MockPuller::new(&mock_data);
759 let pusher = MemPusher::with_capacity(0);
760 let receive = pusher.receive.clone();
761 let result = download_single(
762 puller,
763 pusher,
764 DownloadOptions {
765 retry_gap: Duration::from_secs(1),
766 push_queue_cap: 1024,
767 },
768 );
769 while result.event_chain().recv().await.is_ok() {}
771 timeout(Duration::from_secs(10), async {
772 while result.event_chain().recv().await.is_ok() {}
773 })
774 .await
775 .expect("event loop hung on empty file");
776 assert_eq!(receive.lock().len(), 0);
777 }
778
779 #[derive(Debug, Clone)]
782 struct PullErrNoGapPuller {
783 data: Arc<[u8]>,
784 failed: Arc<AtomicBool>,
785 }
786 impl crate::Puller for PullErrNoGapPuller {
787 type Error = RecoverableErr;
788 fn pull(
789 &mut self,
790 range: Option<&crate::ProgressEntry>,
791 ) -> impl Future<
792 Output = crate::PullResult<impl crate::PullStream<Self::Error>, Self::Error>,
793 > + Send {
794 if !self.failed.swap(true, Ordering::SeqCst) {
795 return std::future::ready(Err((RecoverableErr, None)));
796 }
797 let data = match range {
798 Some(r) => &self.data[r.start as usize..r.end as usize],
799 None => &self.data,
800 };
801 let items: Vec<crate::PullResult<Bytes, RecoverableErr>> = data
802 .chunks(2)
803 .map(|c| Ok(Bytes::copy_from_slice(c)))
804 .collect();
805 std::future::ready(Ok(stream::iter(items)))
806 }
807 }
808
809 #[tokio::test]
810 async fn test_single_pull_error_without_retry_gap_uses_options_default() {
811 let mock_data = build_mock_data(3 * 1024);
814 let puller = PullErrNoGapPuller {
815 data: Arc::from(mock_data.as_slice()),
816 failed: Arc::new(AtomicBool::new(false)),
817 };
818 let pusher = MemPusher::with_capacity(mock_data.len());
819 let receive = pusher.receive.clone();
820 let result = download_single(
821 puller,
822 pusher,
823 DownloadOptions {
824 retry_gap: Duration::ZERO,
825 push_queue_cap: 1024,
826 },
827 );
828 while result.event_chain().recv().await.is_ok() {}
829 assert_eq!(&**receive.lock(), mock_data);
830 }
831
832 use crate::{PullResult, PullStream};
837 use std::pin::Pin;
838 use std::task::{Context, Poll};
839
840 struct PendingStream;
845 impl futures::Stream for PendingStream {
846 type Item = Result<Bytes, (std::convert::Infallible, Option<Duration>)>;
847 fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
848 Poll::Pending
849 }
850 }
851 impl Unpin for PendingStream {}
852
853 #[derive(Clone)]
855 struct StallPuller;
856 impl Puller for StallPuller {
857 type Error = std::convert::Infallible;
858 fn pull(
859 &mut self,
860 _range: Option<&ProgressEntry>,
861 ) -> impl Future<Output = PullResult<impl PullStream<Self::Error>, Self::Error>> + Send
862 {
863 std::future::ready(Ok(PendingStream))
864 }
865 }
866
867 #[tokio::test]
876 #[ignore = "regression baseline: download_single has no pull_timeout, a stalled body hangs forever; enable after adding pull_timeout to DownloadOptions and surfacing a PullTimeout/error"]
877 async fn test_single_stall_body_hangs_without_timeout() {
878 let puller = StallPuller;
879 let pusher = MemPusher::with_capacity(0);
880 let result = download_single(
881 puller,
882 pusher,
883 DownloadOptions {
884 retry_gap: Duration::from_secs(1),
885 push_queue_cap: 1024,
886 },
887 );
888 let drained = tokio::time::timeout(Duration::from_secs(3), async {
892 while result.event_chain().recv().await.is_ok() {}
893 })
894 .await;
895 assert!(
896 drained.is_ok(),
897 "download_single must not hang forever on a stalled body; a pull_timeout should surface a PullTimeout or error and end the session"
898 );
899 }
900}