Time out `condition` after 200ms and add basic unit tests for it

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/app.rs     | 174 +++++++++++++++++++++++++++++++++++++++++-----
gpui/src/util.rs    |  15 ++++
zed/src/worktree.rs |   6 
3 files changed, 171 insertions(+), 24 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -4,7 +4,7 @@ use crate::{
     keymap::{self, Keystroke},
     platform::{self, WindowOptions},
     presenter::Presenter,
-    util::post_inc,
+    util::{post_inc, timeout},
     AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
 };
 use anyhow::{anyhow, Result};
@@ -25,6 +25,7 @@ use std::{
     path::PathBuf,
     rc::{self, Rc},
     sync::{Arc, Weak},
+    time::Duration,
 };
 
 pub trait Entity: 'static + Send + Sync {
@@ -2007,18 +2008,23 @@ impl<T: Entity> ModelHandle<T> {
         let handle = self.clone();
 
         async move {
-            loop {
-                {
-                    let ctx = ctx.borrow();
-                    let ctx = ctx.as_ref();
-                    if predicate(handle.read(ctx), ctx) {
-                        break;
+            timeout(Duration::from_millis(200), async move {
+                loop {
+                    {
+                        let ctx = ctx.borrow();
+                        let ctx = ctx.as_ref();
+                        if predicate(handle.read(ctx), ctx) {
+                            break;
+                        }
+                    }
+
+                    if rx.recv().await.is_none() {
+                        panic!("model dropped with pending condition");
                     }
                 }
-                if rx.recv().await.is_none() {
-                    break;
-                }
-            }
+            })
+            .await
+            .expect("condition timed out");
         }
     }
 }
@@ -2170,18 +2176,23 @@ impl<T: View> ViewHandle<T> {
         let handle = self.clone();
 
         async move {
-            loop {
-                {
-                    let ctx = ctx.borrow();
-                    let ctx = ctx.as_ref();
-                    if predicate(handle.read(ctx), ctx) {
-                        break;
+            timeout(Duration::from_millis(200), async move {
+                loop {
+                    {
+                        let ctx = ctx.borrow();
+                        let ctx = ctx.as_ref();
+                        if predicate(handle.read(ctx), ctx) {
+                            break;
+                        }
+                    }
+
+                    if rx.recv().await.is_none() {
+                        panic!("model dropped with pending condition");
                     }
                 }
-                if rx.recv().await.is_none() {
-                    break;
-                }
-            }
+            })
+            .await
+            .expect("condition timed out");
         }
     }
 }
@@ -2475,6 +2486,7 @@ impl<T> Drop for EntityTask<T> {
 mod tests {
     use super::*;
     use crate::elements::*;
+    use smol::future::poll_once;
 
     #[test]
     fn test_model_handles() {
@@ -3276,6 +3288,126 @@ mod tests {
         });
     }
 
+    #[test]
+    fn test_model_condition() {
+        struct Counter(usize);
+
+        impl super::Entity for Counter {
+            type Event = ();
+        }
+
+        impl Counter {
+            fn inc(&mut self, ctx: &mut ModelContext<Self>) {
+                self.0 += 1;
+                ctx.notify();
+            }
+        }
+
+        App::test_async((), |mut app| async move {
+            let model = app.add_model(|_| Counter(0));
+
+            let condition1 = model.condition(&app, |model, _| model.0 == 2);
+            let condition2 = model.condition(&app, |model, _| model.0 == 3);
+            smol::pin!(condition1, condition2);
+
+            model.update(&mut app, |model, ctx| model.inc(ctx));
+            assert_eq!(poll_once(&mut condition1).await, None);
+            assert_eq!(poll_once(&mut condition2).await, None);
+
+            model.update(&mut app, |model, ctx| model.inc(ctx));
+            assert_eq!(poll_once(&mut condition1).await, Some(()));
+            assert_eq!(poll_once(&mut condition2).await, None);
+
+            model.update(&mut app, |model, ctx| model.inc(ctx));
+            assert_eq!(poll_once(&mut condition2).await, Some(()));
+        });
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_model_condition_timeout() {
+        struct Model;
+
+        impl super::Entity for Model {
+            type Event = ();
+        }
+
+        App::test_async((), |mut app| async move {
+            let model = app.add_model(|_| Model);
+            model.condition(&app, |_, _| false).await;
+        });
+    }
+
+    #[test]
+    fn test_view_condition() {
+        struct Counter(usize);
+
+        impl super::Entity for Counter {
+            type Event = ();
+        }
+
+        impl super::View for Counter {
+            fn ui_name() -> &'static str {
+                "test view"
+            }
+
+            fn render(&self, _: &AppContext) -> ElementBox {
+                Empty::new().boxed()
+            }
+        }
+
+        impl Counter {
+            fn inc(&mut self, ctx: &mut ViewContext<Self>) {
+                self.0 += 1;
+                ctx.notify();
+            }
+        }
+
+        App::test_async((), |mut app| async move {
+            let (_, view) = app.add_window(|_| Counter(0));
+
+            let condition1 = view.condition(&app, |view, _| view.0 == 2);
+            let condition2 = view.condition(&app, |view, _| view.0 == 3);
+            smol::pin!(condition1, condition2);
+
+            view.update(&mut app, |view, ctx| view.inc(ctx));
+            assert_eq!(poll_once(&mut condition1).await, None);
+            assert_eq!(poll_once(&mut condition2).await, None);
+
+            view.update(&mut app, |view, ctx| view.inc(ctx));
+            assert_eq!(poll_once(&mut condition1).await, Some(()));
+            assert_eq!(poll_once(&mut condition2).await, None);
+
+            view.update(&mut app, |view, ctx| view.inc(ctx));
+            assert_eq!(poll_once(&mut condition2).await, Some(()));
+        });
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_view_condition_timeout() {
+        struct View;
+
+        impl super::Entity for View {
+            type Event = ();
+        }
+
+        impl super::View for View {
+            fn ui_name() -> &'static str {
+                "test view"
+            }
+
+            fn render(&self, _: &AppContext) -> ElementBox {
+                Empty::new().boxed()
+            }
+        }
+
+        App::test_async((), |mut app| async move {
+            let (_, view) = app.add_window(|_| View);
+            view.condition(&app, |_, _| false).await;
+        });
+    }
+
     // #[test]
     // fn test_ui_and_window_updates() {
     //     struct View {

gpui/src/util.rs 🔗

@@ -1,5 +1,20 @@
+use smol::future::FutureExt;
+use std::{future::Future, time::Duration};
+
 pub fn post_inc(value: &mut usize) -> usize {
     let prev = *value;
     *value += 1;
     prev
 }
+
+pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
+where
+    F: Future<Output = T>,
+{
+    let timer = async {
+        smol::Timer::after(timeout).await;
+        Err(())
+    };
+    let future = async move { Ok(f.await) };
+    timer.race(future).await
+}

zed/src/worktree.rs 🔗

@@ -77,9 +77,9 @@ impl Worktree {
     pub fn scan_complete(&self) -> impl Future<Output = ()> {
         let mut scan_state_rx = self.scan_state.1.clone();
         async move {
-            let mut next_scan_state = Some(scan_state_rx.borrow().clone());
-            while let Some(ScanState::Scanning) = next_scan_state {
-                next_scan_state = scan_state_rx.recv().await;
+            let mut scan_state = Some(scan_state_rx.borrow().clone());
+            while let Some(ScanState::Scanning) = scan_state {
+                scan_state = scan_state_rx.recv().await;
             }
         }
     }