1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4use std::time::{Duration, Instant};
5
6use tokio::task::JoinHandle;
7use tokio_stream::wrappers::UnboundedReceiverStream;
8use tokio_stream::Stream;
9use tokio_util::sync::CancellationToken;
10
11use crate::error::Result;
12use crate::progress::Progress;
13
14pub struct Handle<T> {
18 join_handle: JoinHandle<Result<T>>,
19 progress: UnboundedReceiverStream<Progress>,
20 cancel: CancellationToken,
21 started: Instant,
22 finished: Option<Instant>,
27}
28
29impl<T> Handle<T> {
30 pub(crate) fn new(
31 join_handle: JoinHandle<Result<T>>,
32 progress: tokio::sync::mpsc::UnboundedReceiver<Progress>,
33 cancel: CancellationToken,
34 ) -> Self {
35 Self {
36 join_handle,
37 progress: UnboundedReceiverStream::new(progress),
38 cancel,
39 started: Instant::now(),
43 finished: None,
44 }
45 }
46
47 pub fn progress(&mut self) -> &mut (impl Stream<Item = Progress> + Unpin) {
48 &mut self.progress
49 }
50
51 pub fn elapsed(&self) -> Duration {
60 match self.finished {
61 Some(finished) => finished.saturating_duration_since(self.started),
62 None => self.started.elapsed(),
63 }
64 }
65
66 pub fn cancel(&self) {
70 self.cancel.cancel();
71 }
72}
73
74impl<T> Future for Handle<T> {
75 type Output = Result<T>;
76
77 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
78 let this = self.get_mut();
79 match Pin::new(&mut this.join_handle).poll(cx) {
80 Poll::Ready(Ok(result)) => {
81 this.finished.get_or_insert_with(Instant::now);
82 Poll::Ready(result)
83 }
84 Poll::Ready(Err(join_err)) => std::panic::resume_unwind(join_err.into_panic()),
89 Poll::Pending => Poll::Pending,
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use std::sync::{Arc, Mutex};
97 use std::time::Duration;
98
99 use tokio::sync::mpsc;
100
101 use super::*;
102
103 #[tokio::test]
104 async fn awaiting_yields_the_wrapped_ok_value() {
105 let (_tx, rx) = mpsc::unbounded_channel();
106 let join_handle = tokio::spawn(async { Ok::<_, crate::error::Error>(42) });
107 let handle = Handle::new(join_handle, rx, CancellationToken::new());
108
109 assert_eq!(handle.await.unwrap(), 42);
110 }
111
112 #[tokio::test]
113 async fn awaiting_yields_the_wrapped_err_value() {
114 let (_tx, rx) = mpsc::unbounded_channel();
115 let join_handle = tokio::spawn(async { Err::<i32, _>(crate::error::Error::Cancelled) });
116 let handle = Handle::new(join_handle, rx, CancellationToken::new());
117
118 assert!(matches!(handle.await, Err(crate::error::Error::Cancelled)));
119 }
120
121 #[tokio::test]
122 async fn progress_stream_yields_events_in_send_order_then_ends() {
123 let (tx, rx) = mpsc::unbounded_channel();
124 let reporter = crate::progress::ProgressReporter::new(tx);
125 let join_handle = tokio::spawn(async move {
126 reporter.send(Progress::Started {
127 bytes_total: None,
128 entries_total: 1,
129 });
130 reporter.send(Progress::EntryStarted {
131 entry: test_entry(),
132 });
133 Ok::<_, crate::error::Error>(())
135 });
136 let mut handle = Handle::new(join_handle, rx, CancellationToken::new());
137
138 use tokio_stream::StreamExt;
139 assert!(matches!(
140 handle.progress().next().await,
141 Some(Progress::Started { .. })
142 ));
143 assert!(matches!(
144 handle.progress().next().await,
145 Some(Progress::EntryStarted { .. })
146 ));
147 assert!(handle.progress().next().await.is_none());
148
149 handle.await.unwrap();
150 }
151
152 #[tokio::test]
153 async fn cancel_triggers_the_wrapped_cancellation_token() {
154 let (_tx, rx) = mpsc::unbounded_channel();
155 let cancel = CancellationToken::new();
156 let join_handle = tokio::spawn(async { Ok::<_, crate::error::Error>(()) });
157 let handle = Handle::new(join_handle, rx, cancel.clone());
158
159 handle.cancel();
160 assert!(cancel.is_cancelled());
161 }
162
163 #[tokio::test]
164 async fn dropping_the_handle_does_not_stop_the_wrapped_task() {
165 let (_tx, rx) = mpsc::unbounded_channel();
166 let sentinel = Arc::new(Mutex::new(0));
167 let sentinel_for_task = Arc::clone(&sentinel);
168
169 let join_handle = tokio::spawn(async move {
170 *sentinel_for_task.lock().unwrap() = 1;
171 tokio::time::sleep(Duration::from_millis(30)).await;
172 *sentinel_for_task.lock().unwrap() = 2;
173 Ok::<_, crate::error::Error>(())
174 });
175 let handle = Handle::new(join_handle, rx, CancellationToken::new());
176
177 tokio::time::sleep(Duration::from_millis(10)).await;
178 assert_eq!(*sentinel.lock().unwrap(), 1);
179 drop(handle);
180
181 tokio::time::sleep(Duration::from_millis(50)).await;
182 assert_eq!(
183 *sentinel.lock().unwrap(),
184 2,
185 "task should have run to completion despite the handle being dropped"
186 );
187 }
188
189 #[tokio::test]
190 async fn elapsed_grows_while_the_operation_runs() {
191 let (_tx, rx) = mpsc::unbounded_channel();
192 let join_handle = tokio::spawn(async {
193 tokio::time::sleep(Duration::from_millis(50)).await;
194 Ok::<_, crate::error::Error>(())
195 });
196 let handle = Handle::new(join_handle, rx, CancellationToken::new());
197
198 tokio::time::sleep(Duration::from_millis(20)).await;
199 let first = handle.elapsed();
200 tokio::time::sleep(Duration::from_millis(20)).await;
201
202 assert!(first >= Duration::from_millis(20));
203 assert!(handle.elapsed() > first);
204 }
205
206 #[tokio::test]
207 async fn elapsed_freezes_once_the_task_has_completed() {
208 let (_tx, rx) = mpsc::unbounded_channel();
209 let join_handle = tokio::spawn(async {
210 tokio::time::sleep(Duration::from_millis(20)).await;
211 Ok::<_, crate::error::Error>(())
212 });
213 let mut handle = Handle::new(join_handle, rx, CancellationToken::new());
214
215 Pin::new(&mut handle).await.unwrap();
218 let at_completion = handle.elapsed();
219
220 tokio::time::sleep(Duration::from_millis(30)).await;
221
222 assert!(at_completion >= Duration::from_millis(20));
223 assert_eq!(handle.elapsed(), at_completion);
224 }
225
226 fn test_entry() -> crate::profiler::Entry {
227 crate::profiler::Entry {
228 path: std::path::PathBuf::from("a"),
229 relative_path: std::path::PathBuf::from("a"),
230 size: 1,
231 modified: None,
232 }
233 }
234}