WIP: Add a condition method to model and view handles for use in tests

Nathan Sobo created

It returns a future that resolves when the provided predicate returns true. The predicate is called any time the handle's targeted entity calls notify.

Still need to add a timeout and completely remove finsih_pending_tasks.

Change summary

Cargo.lock                          |  6 +
gpui/Cargo.toml                     |  1 
gpui/src/app.rs                     | 73 ++++++++++++++++++++++++++++++
zed/Cargo.toml                      |  5 +
zed/src/test.rs                     |  9 +++
zed/src/workspace/workspace_view.rs | 71 ++++++++++++++++--------------
6 files changed, 126 insertions(+), 39 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -568,9 +568,9 @@ dependencies = [
 
 [[package]]
 name = "ctor"
-version = "0.1.19"
+version = "0.1.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8f45d9ad417bcef4817d614a501ab55cdd96a6fdb24f49aab89a54acfd66b19"
+checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
 dependencies = [
  "quote",
  "syn",
@@ -1005,6 +1005,7 @@ dependencies = [
  "pathfinder_color",
  "pathfinder_geometry",
  "png",
+ "postage",
  "rand 0.8.3",
  "replace_with",
  "resvg",
@@ -2449,6 +2450,7 @@ dependencies = [
  "anyhow",
  "arrayvec",
  "crossbeam-channel 0.5.0",
+ "ctor",
  "dirs",
  "easy-parallel",
  "fsevent",

gpui/Cargo.toml 🔗

@@ -15,6 +15,7 @@ ordered-float = "2.1.1"
 parking_lot = "0.11.1"
 pathfinder_color = "0.5"
 pathfinder_geometry = "0.5"
+postage = {version = "0.4.1", features = ["futures-traits"]}
 rand = "0.8.3"
 replace_with = "0.1.7"
 resvg = "0.14"

gpui/src/app.rs 🔗

@@ -13,11 +13,12 @@ use keymap::MatchResult;
 use parking_lot::Mutex;
 use pathfinder_geometry::{rect::RectF, vector::vec2f};
 use platform::Event;
+use postage::{sink::Sink as _, stream::Stream as _};
 use smol::prelude::*;
 use std::{
     any::{type_name, Any, TypeId},
     cell::RefCell,
-    collections::{HashMap, HashSet, VecDeque},
+    collections::{hash_map::Entry, HashMap, HashSet, VecDeque},
     fmt::{self, Debug},
     hash::{Hash, Hasher},
     marker::PhantomData,
@@ -384,6 +385,7 @@ pub struct MutableAppContext {
     next_task_id: usize,
     subscriptions: HashMap<usize, Vec<Subscription>>,
     observations: HashMap<usize, Vec<Observation>>,
+    async_observations: HashMap<usize, postage::broadcast::Sender<()>>,
     window_invalidations: HashMap<usize, WindowInvalidation>,
     presenters_and_platform_windows:
         HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
@@ -424,6 +426,7 @@ impl MutableAppContext {
             next_task_id: 0,
             subscriptions: HashMap::new(),
             observations: HashMap::new(),
+            async_observations: HashMap::new(),
             window_invalidations: HashMap::new(),
             presenters_and_platform_windows: HashMap::new(),
             debug_elements_callbacks: HashMap::new(),
@@ -877,11 +880,13 @@ impl MutableAppContext {
                 self.ctx.models.remove(&model_id);
                 self.subscriptions.remove(&model_id);
                 self.observations.remove(&model_id);
+                self.async_observations.remove(&model_id);
             }
 
             for (window_id, view_id) in dropped_views {
                 self.subscriptions.remove(&view_id);
                 self.observations.remove(&view_id);
+                self.async_observations.remove(&view_id);
                 if let Some(window) = self.ctx.windows.get_mut(&window_id) {
                     self.window_invalidations
                         .entry(window_id)
@@ -1047,6 +1052,12 @@ impl MutableAppContext {
                 }
             }
         }
+
+        if let Entry::Occupied(mut entry) = self.async_observations.entry(observed_id) {
+            if entry.get_mut().blocking_send(()).is_err() {
+                entry.remove_entry();
+            }
+        }
     }
 
     fn notify_view_observers(&mut self, window_id: usize, view_id: usize) {
@@ -2012,6 +2023,36 @@ impl<T: Entity> ModelHandle<T> {
     {
         app.update_model(self, update)
     }
+
+    pub fn condition(
+        &self,
+        ctx: &TestAppContext,
+        mut predicate: impl 'static + FnMut(&T, &AppContext) -> bool,
+    ) -> impl 'static + Future<Output = ()> {
+        let mut ctx = ctx.0.borrow_mut();
+        let tx = ctx
+            .async_observations
+            .entry(self.id())
+            .or_insert_with(|| postage::broadcast::channel(128).0);
+        let mut rx = tx.subscribe();
+        let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap();
+        let handle = self.clone();
+
+        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() {
+                    break;
+                }
+            }
+        }
+    }
 }
 
 impl<T> Clone for ModelHandle<T> {
@@ -2145,6 +2186,36 @@ impl<T: View> ViewHandle<T> {
         app.focused_view_id(self.window_id)
             .map_or(false, |focused_id| focused_id == self.view_id)
     }
+
+    pub fn condition(
+        &self,
+        ctx: &TestAppContext,
+        mut predicate: impl 'static + FnMut(&T, &AppContext) -> bool,
+    ) -> impl 'static + Future<Output = ()> {
+        let mut ctx = ctx.0.borrow_mut();
+        let tx = ctx
+            .async_observations
+            .entry(self.id())
+            .or_insert_with(|| postage::broadcast::channel(128).0);
+        let mut rx = tx.subscribe();
+        let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap();
+        let handle = self.clone();
+
+        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() {
+                    break;
+                }
+            }
+        }
+    }
 }
 
 impl<T> Clone for ViewHandle<T> {

zed/Cargo.toml 🔗

@@ -16,10 +16,11 @@ path = "src/main.rs"
 anyhow = "1.0.38"
 arrayvec = "0.5.2"
 crossbeam-channel = "0.5.0"
+ctor = "0.1.20"
 dirs = "3.0"
 easy-parallel = "3.1.0"
-futures-core = "0.3"
 fsevent = {path = "../fsevent"}
+futures-core = "0.3"
 gpui = {path = "../gpui"}
 ignore = {git = "https://github.com/zed-industries/ripgrep", rev = "1d152118f35b3e3590216709b86277062d79b8a0"}
 lazy_static = "1.4.0"
@@ -31,8 +32,8 @@ postage = {version = "0.4.1", features = ["futures-traits"]}
 rand = "0.8.3"
 rust-embed = "5.9.0"
 seahash = "4.1"
+serde = {version = "1", features = ["derive"]}
 simplelog = "0.9"
-serde = { version = "1", features = ["derive"] }
 smallvec = "1.6.1"
 smol = "1.2.5"
 

zed/src/test.rs 🔗

@@ -1,4 +1,8 @@
+use crate::time::ReplicaId;
+use ctor::ctor;
+use log::LevelFilter;
 use rand::Rng;
+use simplelog::SimpleLogger;
 use std::{
     collections::BTreeMap,
     path::{Path, PathBuf},
@@ -6,7 +10,10 @@ use std::{
 };
 use tempdir::TempDir;
 
-use crate::time::ReplicaId;
+#[ctor]
+fn init_logger() {
+    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+}
 
 #[derive(Clone)]
 struct Envelope<T: Clone> {

zed/src/workspace/workspace_view.rs 🔗

@@ -402,44 +402,56 @@ mod tests {
 
             // Open the first entry
             workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx));
-            app.finish_pending_tasks().await;
 
-            app.read(|ctx| {
-                assert_eq!(
-                    workspace_view
-                        .read(ctx)
-                        .active_pane()
-                        .read(ctx)
-                        .items()
-                        .len(),
-                    1
-                )
-            });
+            workspace_view
+                .condition(&app, |workspace_view, ctx| {
+                    workspace_view.active_pane().read(ctx).items().len() == 1
+                })
+                .await;
 
             // Open the second entry
             workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[1], ctx));
-            app.finish_pending_tasks().await;
+
+            workspace_view
+                .condition(&app, |workspace_view, ctx| {
+                    workspace_view.active_pane().read(ctx).items().len() == 2
+                })
+                .await;
 
             app.read(|ctx| {
-                let active_pane = workspace_view.read(ctx).active_pane().read(ctx);
-                assert_eq!(active_pane.items().len(), 2);
                 assert_eq!(
-                    active_pane.active_item().unwrap().entry_id(ctx),
+                    workspace_view
+                        .read(ctx)
+                        .active_pane()
+                        .read(ctx)
+                        .active_item()
+                        .unwrap()
+                        .entry_id(ctx),
                     Some(entries[1])
                 );
             });
 
             // Open the first entry again
             workspace_view.update(&mut app, |w, ctx| w.open_entry(entries[0], ctx));
-            app.finish_pending_tasks().await;
+
+            {
+                let entries = entries.clone();
+                workspace_view
+                    .condition(&app, move |workspace_view, ctx| {
+                        workspace_view
+                            .active_pane()
+                            .read(ctx)
+                            .active_item()
+                            .unwrap()
+                            .entry_id(ctx)
+                            == Some(entries[0])
+                    })
+                    .await;
+            }
 
             app.read(|ctx| {
                 let active_pane = workspace_view.read(ctx).active_pane().read(ctx);
                 assert_eq!(active_pane.items().len(), 2);
-                assert_eq!(
-                    active_pane.active_item().unwrap().entry_id(ctx),
-                    Some(entries[0])
-                );
             });
 
             // Open the third entry twice concurrently
@@ -447,19 +459,12 @@ mod tests {
                 w.open_entry(entries[2], ctx);
                 w.open_entry(entries[2], ctx);
             });
-            app.finish_pending_tasks().await;
 
-            app.read(|ctx| {
-                assert_eq!(
-                    workspace_view
-                        .read(ctx)
-                        .active_pane()
-                        .read(ctx)
-                        .items()
-                        .len(),
-                    3
-                );
-            });
+            workspace_view
+                .condition(&app, |workspace_view, ctx| {
+                    workspace_view.active_pane().read(ctx).items().len() == 3
+                })
+                .await;
         });
     }