1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use tokio::task::JoinHandle;
6use tokio_stream::wrappers::UnboundedReceiverStream;
7use tokio_stream::Stream;
8use tokio_util::sync::CancellationToken;
9
10use crate::error::Result;
11use crate::progress::Progress;
12
13pub struct Handle<T> {
17 join_handle: JoinHandle<Result<T>>,
18 progress: UnboundedReceiverStream<Progress>,
19 cancel: CancellationToken,
20}
21
22impl<T> Handle<T> {
23 pub(crate) fn new(
24 join_handle: JoinHandle<Result<T>>,
25 progress: tokio::sync::mpsc::UnboundedReceiver<Progress>,
26 cancel: CancellationToken,
27 ) -> Self {
28 Self { join_handle, progress: UnboundedReceiverStream::new(progress), cancel }
29 }
30
31 pub fn progress(&mut self) -> &mut (impl Stream<Item = Progress> + Unpin) {
32 &mut self.progress
33 }
34
35 pub fn cancel(&self) {
40 self.cancel.cancel();
41 }
42}
43
44impl<T> Future for Handle<T> {
45 type Output = Result<T>;
46
47 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
48 let this = self.get_mut();
49 match Pin::new(&mut this.join_handle).poll(cx) {
50 Poll::Ready(Ok(result)) => Poll::Ready(result),
51 Poll::Ready(Err(join_err)) => std::panic::resume_unwind(join_err.into_panic()),
56 Poll::Pending => Poll::Pending,
57 }
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use std::sync::{Arc, Mutex};
64 use std::time::Duration;
65
66 use tokio::sync::mpsc;
67
68 use super::*;
69
70 #[tokio::test]
71 async fn awaiting_yields_the_wrapped_ok_value() {
72 let (_tx, rx) = mpsc::unbounded_channel();
73 let join_handle = tokio::spawn(async { Ok::<_, crate::error::Error>(42) });
74 let handle = Handle::new(join_handle, rx, CancellationToken::new());
75
76 assert_eq!(handle.await.unwrap(), 42);
77 }
78
79 #[tokio::test]
80 async fn awaiting_yields_the_wrapped_err_value() {
81 let (_tx, rx) = mpsc::unbounded_channel();
82 let join_handle = tokio::spawn(async { Err::<i32, _>(crate::error::Error::Cancelled) });
83 let handle = Handle::new(join_handle, rx, CancellationToken::new());
84
85 assert!(matches!(handle.await, Err(crate::error::Error::Cancelled)));
86 }
87
88 #[tokio::test]
89 async fn progress_stream_yields_events_in_send_order_then_ends() {
90 let (tx, rx) = mpsc::unbounded_channel();
91 let reporter = crate::progress::ProgressReporter::new(tx);
92 let join_handle = tokio::spawn(async move {
93 reporter.send(Progress::Started { bytes_total: None, entries_total: 1 });
94 reporter.send(Progress::EntryStarted {
95 entry: test_entry(),
96 });
97 Ok::<_, crate::error::Error>(())
99 });
100 let mut handle = Handle::new(join_handle, rx, CancellationToken::new());
101
102 use tokio_stream::StreamExt;
103 assert!(matches!(handle.progress().next().await, Some(Progress::Started { .. })));
104 assert!(matches!(handle.progress().next().await, Some(Progress::EntryStarted { .. })));
105 assert!(handle.progress().next().await.is_none());
106
107 handle.await.unwrap();
108 }
109
110 #[tokio::test]
111 async fn cancel_triggers_the_wrapped_cancellation_token() {
112 let (_tx, rx) = mpsc::unbounded_channel();
113 let cancel = CancellationToken::new();
114 let join_handle = tokio::spawn(async { Ok::<_, crate::error::Error>(()) });
115 let handle = Handle::new(join_handle, rx, cancel.clone());
116
117 handle.cancel();
118 assert!(cancel.is_cancelled());
119 }
120
121 #[tokio::test]
122 async fn dropping_the_handle_does_not_stop_the_wrapped_task() {
123 let (_tx, rx) = mpsc::unbounded_channel();
124 let sentinel = Arc::new(Mutex::new(0));
125 let sentinel_for_task = Arc::clone(&sentinel);
126
127 let join_handle = tokio::spawn(async move {
128 *sentinel_for_task.lock().unwrap() = 1;
129 tokio::time::sleep(Duration::from_millis(30)).await;
130 *sentinel_for_task.lock().unwrap() = 2;
131 Ok::<_, crate::error::Error>(())
132 });
133 let handle = Handle::new(join_handle, rx, CancellationToken::new());
134
135 tokio::time::sleep(Duration::from_millis(10)).await;
136 assert_eq!(*sentinel.lock().unwrap(), 1);
137 drop(handle);
138
139 tokio::time::sleep(Duration::from_millis(50)).await;
140 assert_eq!(*sentinel.lock().unwrap(), 2, "task should have run to completion despite the handle being dropped");
141 }
142
143 fn test_entry() -> crate::profiler::Entry {
144 crate::profiler::Entry {
145 path: std::path::PathBuf::from("a"),
146 relative_path: std::path::PathBuf::from("a"),
147 size: 1,
148 modified: None,
149 }
150 }
151}