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