Skip to main content

fast_pull/core/
single.rs

1//! Single-threaded sequential download.
2
3use 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/// Options for a single-threaded download.
14#[derive(Debug, Clone, Copy)]
15pub struct DownloadOptions {
16    /// Delay before retrying a failed pull or push.
17    pub retry_gap: Duration,
18    /// Capacity of the bounded pull→push queue; backpressure when full.
19    pub push_queue_cap: usize,
20}
21
22/// Start a single-threaded sequential download.
23///
24/// The puller fetches the entire file sequentially, chunk by chunk.
25/// Supports retries and progress events via [`DownloadResult`].
26/// # Completion
27///
28/// The download is finished once the push driver has drained `rx_push` and
29/// flushed; the `event_chain` sender lives inside that driver, so it is dropped
30/// when the driver returns and the receiver disconnects. Draining
31/// `event_chain` is therefore the way to await completion. A panic in the
32/// blocking push driver also drops the sender and ends the session, but is
33/// otherwise swallowed. Normal cancellation (via
34/// `abort()`) is observed: the pull task is force-aborted and the push driver
35/// returns without flushing, so any buffered bytes are discarded and the file
36/// is left incomplete. Completion is still detected by draining `event_chain`,
37/// because the senders are dropped when both sides exit.
38#[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        // Keep only the data handle for the final assertion; the whole `pusher`
175        // (whose listener holds a clone of the `event_chain` sender) is moved into
176        // the download, so `event_chain` closes once the push thread finishes,
177        // terminating the single drain loop below.
178        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        // Keep the data handle for the post-abort length assertion; the whole
213        // `pusher` (whose listener holds a clone of the `event_chain` sender) is
214        // moved into the download.
215        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        // Abort immediately, before the pull/push machinery has a chance to
226        // finish. The push driver must observe the shared flag, break out
227        // WITHOUT flushing, and let `join()` return promptly.
228        result.abort();
229        assert!(result.is_aborted());
230
231        // The event loop must end promptly and never hang.
232        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        // The pusher layer is unchanged; for `MemPusher` each `push` is committed
239        // immediately, so abort simply stops further writes. The received bytes
240        // are a (possibly empty) prefix of the source and never exceed it — i.e.
241        // the download was cut short rather than completed.
242        let written = receive.lock().len();
243        assert!(
244            written <= mock_data.len(),
245            "abort must not write beyond the source"
246        );
247    }
248
249    // -------------------------------------------------------------------------
250    // Strengthened abort coverage.
251    //
252    // `BufWriterPusher` only forwards buffered bytes to its inner sink on
253    // `flush()` (or overflow). Wrapping `MemPusher` with capacity > source lets
254    // us prove that, on abort, the *un-flushed* buffer is discarded and nothing
255    // reaches the inner sink — an invariant the bare `MemPusher` test above
256    // cannot establish (for `MemPusher` every `push` is already committed).
257    // -------------------------------------------------------------------------
258
259    /// A [`Puller`] that stalls for `delay` before yielding any data, so a test
260    /// can deterministically abort *mid-flight* — after the push driver has
261    /// buffered bytes but before `pusher.flush()` would run.
262    #[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        // 64 KiB source. The slow puller stalls before yielding any data so the
296        // test can abort mid-flight (buffer filled, flush skipped).
297        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        // BufWriterPusher coalesces the contiguous single-stream writes; with
303        // capacity > source nothing reaches `MemPusher` until `flush()`.
304        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        // Abort as soon as the push driver starts processing (first `Pushing`
317        // event). This lands reliably before completion because the puller is
318        // slow.
319        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        // The event loop must end promptly and never hang.
331        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        // The buffered (un-flushed) bytes must have been discarded: the inner
338        // sink received nothing. This is the stronger invariant the bare
339        // `MemPusher` test could not prove.
340        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        // 64 KiB source; real file sink pre-sized by `StdFilePusher::new`, then
352        // wrapped in a buffer. Abort must leave the file untouched (all zeros):
353        // the buffered bytes are discarded and never written to disk.
354        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        // No actual bytes were written: the file is still the zero-filled
393        // pre-sized region. This proves the buffered data was discarded rather
394        // than flushed to disk.
395        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    // -------------------------------------------------------------------------
406    // Coverage for the push-error retry path (lines 54-59), flush-error retry path
407    // (lines 69-73), pull-error path (lines 83-85), stream-error path (lines 99-105)
408    // and the `SlowMockPuller` `Some` branch (line 247).
409    // -------------------------------------------------------------------------
410
411    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    /// In-memory sink that can be told to fail the next `push` (lines 54-59) or
444    /// `flush` (lines 69-73).
445    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    /// Like [`StreamErrOncePuller`] but yields a *recoverable* stream error first,
557    /// so the `is_irrecoverable == false` fall-through (line 105) is exercised.
558    #[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            // The first `pull` yields one recoverable stream error *followed by* the
572            // real data in the same stream. In `single` a recoverable stream error
573            // falls through (line 105) and then keeps reading the same stream, so the
574            // download still completes successfully.
575            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        // Lines 54-59: a failing inner push is retried after `park_timeout`.
596        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        // Lines 69-73: a failing inner flush is retried after `park_timeout`.
616        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        // Lines 83-85: a `pull` error (recoverable) is retried.
636        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        // Lines 99-105: a stream error whose `is_irrecoverable` is true triggers a
658        // `continue 'redownload` and a re-pull, which then succeeds.
659        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        // Lines 99-105: a stream error whose `is_irrecoverable` is false does NOT
681        // `continue 'redownload`; instead it falls through (line 105) and retries the
682        // pull, which then succeeds.
683        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        // Exercise `Display` for the test error types (lines 386-388, 400-403) and
705        // both arms of each test puller's `match range` (lines 483, 511, 541, 550-551).
706        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; // first call errors, sets `failed`
714        let _ = pull_err.pull(Some(&(0..2u64))).await; // success path, `Some` arm
715        let _ = pull_err.pull(None).await; // success path, `None` arm
716
717        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; // success path, `Some` arm
723        let _ = stream_err.pull(None).await; // success path, `None` arm
724
725        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; // first: `Some` arm + error branch
730        let _ = rec_stream_err.pull(Some(&(0..2u64))).await; // not-first: `Some` arm
731        let _ = rec_stream_err.pull(None).await; // `None` arm
732    }
733
734    #[test]
735    fn flaky_sink_noncontiguous_write_rebuffers() {
736        // Lines 446-449: a write whose start is not flush against the current end of
737        // the sink takes the `else` branch (resize + scatter copy) instead of `extend`.
738        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        // Line 247: the `Some` branch of `SlowMockPuller::pull` (the sequential
746        // download always passes `None`, so this branch is otherwise uncovered).
747        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        // 0-byte source: the pull stream yields `Ok(None)` immediately, the
757        // `downloaded` counter stays 0, and the push worker must still flush and
758        // exit without hanging or writing anything.
759        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        // Drain events so `event_chain` does not pin the task open.
772        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    /// Like `PullErrOncePuller` but the first `pull` fails with `None` as the retry
782    /// gap, exercising the `retry_gap.unwrap_or(options.retry_gap)` fallback.
783    #[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        // Lines 83-85: a `pull` error carrying `None` as the retry gap must fall
814        // back to `options.retry_gap` rather than panicking or stalling.
815        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    // -------------------------------------------------------------------------
835    // Regression baseline for the stalled-body hang (FluxDown-style #545).
836    // -------------------------------------------------------------------------
837
838    use crate::{PullResult, PullStream};
839    use std::pin::Pin;
840    use std::task::{Context, Poll};
841
842    /// A stream whose `next` never resolves — models a server that answers
843    /// the request (headers) but never delivers the body (a "stall"/"slowloris"
844    /// server). `TryStream` is satisfied automatically because `Item` is a
845    /// `Result`, so we only need to implement the underlying `Stream`.
846    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    /// A puller that returns a never-resolving stream (see [`PendingStream`]).
856    #[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    /// `download_single` reads the body with a bare `stream.try_next().await`
870    /// (single.rs) and its `DownloadOptions` has no `pull_timeout` field, so a
871    /// stalled body blocks forever. This asserts the session ends (a
872    /// `PullTimeout`/error surfaces and the event channel closes) within 3s.
873    ///
874    /// `#[ignore]`d because the fix (adding `pull_timeout` to `DownloadOptions`
875    /// and giving up with an error instead of hanging) does not exist yet; remove
876    /// `#[ignore]` once that lands.
877    #[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        // The session must not block forever: within 3s the event channel must
891        // close (a timeout/error must surface and end the download). Today it
892        // hangs, so the outer timeout fires and the assertion fails.
893        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}