Add deterministic `TestDispatcher`

Antonio Scandurra , Conrad Irwin , Max Brunsfeld , and Kyle Caverly created

Co-Authored-By: Conrad Irwin <conrad@zed.dev>
Co-Authored-By: Max Brunsfeld <max@zed.dev>
Co-Authored-By: Kyle Caverly <kyle@zed.dev>

Change summary

crates/gpui2/src/app.rs                      |   6 
crates/gpui2/src/executor.rs                 |   6 
crates/gpui2/src/platform.rs                 |   7 
crates/gpui2/src/platform/mac/dispatcher.rs  |   8 
crates/gpui2/src/platform/test.rs            | 191 ---------------------
crates/gpui2/src/platform/test/dispatcher.rs | 150 +++++++++++++++++
crates/gpui2/src/platform/test/platform.rs   | 194 ++++++++++++++++++++++
crates/gpui2/src/text_system/line_wrapper.rs |   2 
crates/gpui2/src/util.rs                     |   4 
crates/gpui2/src/window.rs                   |   2 
10 files changed, 372 insertions(+), 198 deletions(-)

Detailed changes

crates/gpui2/src/app.rs 🔗

@@ -39,9 +39,9 @@ impl App {
         Self::new(current_platform(), asset_source, http_client)
     }
 
-    #[cfg(any(test, feature = "test"))]
-    pub fn test() -> Self {
-        let platform = Arc::new(super::TestPlatform::new());
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test(seed: u64) -> Self {
+        let platform = Arc::new(crate::TestPlatform::new(seed));
         let asset_source = Arc::new(());
         let http_client = util::http::FakeHttpClient::with_404_response();
         Self::new(platform, asset_source, http_client)

crates/gpui2/src/executor.rs 🔗

@@ -145,8 +145,10 @@ impl Executor {
             match future.as_mut().poll(&mut cx) {
                 Poll::Ready(result) => return result,
                 Poll::Pending => {
-                    // todo!("call tick on test dispatcher")
-                    parker.park();
+                    if !self.dispatcher.poll() {
+                        // todo!("forbid_parking")
+                        parker.park();
+                    }
                 }
             }
         }

crates/gpui2/src/platform.rs 🔗

@@ -1,7 +1,7 @@
 mod keystroke;
 #[cfg(target_os = "macos")]
 mod mac;
-#[cfg(any(test, feature = "test"))]
+#[cfg(any(test, feature = "test-support"))]
 mod test;
 
 use crate::{
@@ -30,7 +30,7 @@ use std::{
 pub use keystroke::*;
 #[cfg(target_os = "macos")]
 pub use mac::*;
-#[cfg(any(test, feature = "test"))]
+#[cfg(any(test, feature = "test-support"))]
 pub use test::*;
 pub use time::UtcOffset;
 
@@ -161,6 +161,9 @@ pub trait PlatformDispatcher: Send + Sync {
     fn dispatch(&self, runnable: Runnable);
     fn dispatch_on_main_thread(&self, runnable: Runnable);
     fn dispatch_after(&self, duration: Duration, runnable: Runnable);
+    fn poll(&self) -> bool;
+    #[cfg(any(test, feature = "test-support"))]
+    fn advance_clock(&self, duration: Duration);
 }
 
 pub trait PlatformTextSystem: Send + Sync {

crates/gpui2/src/platform/mac/dispatcher.rs 🔗

@@ -67,6 +67,14 @@ impl PlatformDispatcher for MacDispatcher {
             );
         }
     }
+
+    fn poll(&self) -> bool {
+        false
+    }
+
+    fn advance_clock(&self, _: Duration) {
+        unimplemented!()
+    }
 }
 
 extern "C" fn trampoline(runnable: *mut c_void) {

crates/gpui2/src/platform/test.rs 🔗

@@ -1,188 +1,5 @@
-use super::Platform;
-use crate::{DisplayId, Executor};
+mod dispatcher;
+mod platform;
 
-pub struct TestPlatform;
-
-impl TestPlatform {
-    pub fn new() -> Self {
-        TestPlatform
-    }
-}
-
-// todo!("implement out what our tests needed in GPUI 1")
-impl Platform for TestPlatform {
-    fn executor(&self) -> Executor {
-        unimplemented!()
-    }
-
-    fn text_system(&self) -> std::sync::Arc<dyn crate::PlatformTextSystem> {
-        unimplemented!()
-    }
-
-    fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
-        unimplemented!()
-    }
-
-    fn quit(&self) {
-        unimplemented!()
-    }
-
-    fn restart(&self) {
-        unimplemented!()
-    }
-
-    fn activate(&self, _ignoring_other_apps: bool) {
-        unimplemented!()
-    }
-
-    fn hide(&self) {
-        unimplemented!()
-    }
-
-    fn hide_other_apps(&self) {
-        unimplemented!()
-    }
-
-    fn unhide_other_apps(&self) {
-        unimplemented!()
-    }
-
-    fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
-        unimplemented!()
-    }
-
-    fn display(&self, _id: DisplayId) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
-        unimplemented!()
-    }
-
-    fn main_window(&self) -> Option<crate::AnyWindowHandle> {
-        unimplemented!()
-    }
-
-    fn open_window(
-        &self,
-        _handle: crate::AnyWindowHandle,
-        _options: crate::WindowOptions,
-    ) -> Box<dyn crate::PlatformWindow> {
-        unimplemented!()
-    }
-
-    fn set_display_link_output_callback(
-        &self,
-        _display_id: DisplayId,
-        _callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp)>,
-    ) {
-        unimplemented!()
-    }
-
-    fn start_display_link(&self, _display_id: DisplayId) {
-        unimplemented!()
-    }
-
-    fn stop_display_link(&self, _display_id: DisplayId) {
-        unimplemented!()
-    }
-
-    fn open_url(&self, _url: &str) {
-        unimplemented!()
-    }
-
-    fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
-        unimplemented!()
-    }
-
-    fn prompt_for_paths(
-        &self,
-        _options: crate::PathPromptOptions,
-    ) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
-        unimplemented!()
-    }
-
-    fn prompt_for_new_path(
-        &self,
-        _directory: &std::path::Path,
-    ) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
-        unimplemented!()
-    }
-
-    fn reveal_path(&self, _path: &std::path::Path) {
-        unimplemented!()
-    }
-
-    fn on_become_active(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
-
-    fn on_resign_active(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
-
-    fn on_quit(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
-
-    fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
-
-    fn on_event(&self, _callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
-        unimplemented!()
-    }
-
-    fn os_name(&self) -> &'static str {
-        unimplemented!()
-    }
-
-    fn os_version(&self) -> anyhow::Result<crate::SemanticVersion> {
-        unimplemented!()
-    }
-
-    fn app_version(&self) -> anyhow::Result<crate::SemanticVersion> {
-        unimplemented!()
-    }
-
-    fn app_path(&self) -> anyhow::Result<std::path::PathBuf> {
-        unimplemented!()
-    }
-
-    fn local_timezone(&self) -> time::UtcOffset {
-        unimplemented!()
-    }
-
-    fn path_for_auxiliary_executable(&self, _name: &str) -> anyhow::Result<std::path::PathBuf> {
-        unimplemented!()
-    }
-
-    fn set_cursor_style(&self, _style: crate::CursorStyle) {
-        unimplemented!()
-    }
-
-    fn should_auto_hide_scrollbars(&self) -> bool {
-        unimplemented!()
-    }
-
-    fn write_to_clipboard(&self, _item: crate::ClipboardItem) {
-        unimplemented!()
-    }
-
-    fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
-        unimplemented!()
-    }
-
-    fn write_credentials(
-        &self,
-        _url: &str,
-        _username: &str,
-        _password: &[u8],
-    ) -> anyhow::Result<()> {
-        unimplemented!()
-    }
-
-    fn read_credentials(&self, _url: &str) -> anyhow::Result<Option<(String, Vec<u8>)>> {
-        unimplemented!()
-    }
-
-    fn delete_credentials(&self, _url: &str) -> anyhow::Result<()> {
-        unimplemented!()
-    }
-}
+pub use dispatcher::*;
+pub use platform::*;

crates/gpui2/src/platform/test/dispatcher.rs 🔗

@@ -0,0 +1,150 @@
+use crate::PlatformDispatcher;
+use async_task::Runnable;
+use collections::{BTreeMap, VecDeque};
+use parking_lot::Mutex;
+use rand::prelude::*;
+use std::time::{Duration, Instant};
+
+pub struct TestDispatcher(Mutex<TestDispatcherState>);
+
+struct TestDispatcherState {
+    random: StdRng,
+    foreground: VecDeque<Runnable>,
+    background: Vec<Runnable>,
+    delayed: BTreeMap<Instant, Runnable>,
+    time: Instant,
+    is_main_thread: bool,
+}
+
+impl TestDispatcher {
+    pub fn new(random: StdRng) -> Self {
+        let state = TestDispatcherState {
+            random,
+            foreground: VecDeque::new(),
+            background: Vec::new(),
+            delayed: BTreeMap::new(),
+            time: Instant::now(),
+            is_main_thread: true,
+        };
+
+        TestDispatcher(Mutex::new(state))
+    }
+}
+
+impl PlatformDispatcher for TestDispatcher {
+    fn is_main_thread(&self) -> bool {
+        self.0.lock().is_main_thread
+    }
+
+    fn dispatch(&self, runnable: Runnable) {
+        self.0.lock().background.push(runnable);
+    }
+
+    fn dispatch_on_main_thread(&self, runnable: Runnable) {
+        self.0.lock().foreground.push_back(runnable);
+    }
+
+    fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) {
+        let mut state = self.0.lock();
+        let next_time = state.time + duration;
+        state.delayed.insert(next_time, runnable);
+    }
+
+    fn poll(&self) -> bool {
+        let mut state = self.0.lock();
+
+        while let Some((deadline, _)) = state.delayed.first_key_value() {
+            if *deadline > state.time {
+                break;
+            }
+            let (_, runnable) = state.delayed.pop_first().unwrap();
+            state.background.push(runnable);
+        }
+
+        if state.foreground.is_empty() && state.background.is_empty() {
+            return false;
+        }
+
+        let foreground_len = state.foreground.len();
+        let background_len = state.background.len();
+        let main_thread = background_len == 0
+            || state
+                .random
+                .gen_ratio(foreground_len as u32, background_len as u32);
+        let was_main_thread = state.is_main_thread;
+        state.is_main_thread = main_thread;
+
+        let runnable = if main_thread {
+            state.foreground.pop_front().unwrap()
+        } else {
+            let ix = state.random.gen_range(0..background_len);
+            state.background.remove(ix)
+        };
+
+        drop(state);
+        runnable.run();
+
+        self.0.lock().is_main_thread = was_main_thread;
+
+        true
+    }
+
+    fn advance_clock(&self, by: Duration) {
+        self.0.lock().time += by;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::Executor;
+    use std::sync::Arc;
+
+    #[test]
+    fn test_dispatch() {
+        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
+        let executor = Executor::new(Arc::new(dispatcher));
+
+        let result = executor.block(async { executor.run_on_main(|| 1).await });
+        assert_eq!(result, 1);
+
+        let result = executor.block({
+            let executor = executor.clone();
+            async move {
+                executor
+                    .spawn_on_main({
+                        let executor = executor.clone();
+                        assert!(executor.is_main_thread());
+                        || async move {
+                            assert!(executor.is_main_thread());
+                            let result = executor
+                                .spawn({
+                                    let executor = executor.clone();
+                                    async move {
+                                        assert!(!executor.is_main_thread());
+
+                                        let result = executor
+                                            .spawn_on_main({
+                                                let executor = executor.clone();
+                                                || async move {
+                                                    assert!(executor.is_main_thread());
+                                                    2
+                                                }
+                                            })
+                                            .await;
+
+                                        assert!(!executor.is_main_thread());
+                                        result
+                                    }
+                                })
+                                .await;
+                            assert!(executor.is_main_thread());
+                            result
+                        }
+                    })
+                    .await
+            }
+        });
+        assert_eq!(result, 2);
+    }
+}

crates/gpui2/src/platform/test/platform.rs 🔗

@@ -0,0 +1,194 @@
+use crate::{DisplayId, Executor, Platform, PlatformTextSystem, TestDispatcher};
+use rand::prelude::*;
+use std::sync::Arc;
+
+pub struct TestPlatform {
+    executor: Executor,
+}
+
+impl TestPlatform {
+    pub fn new(seed: u64) -> Self {
+        let rng = StdRng::seed_from_u64(seed);
+        TestPlatform {
+            executor: Executor::new(Arc::new(TestDispatcher::new(rng))),
+        }
+    }
+}
+
+// todo!("implement out what our tests needed in GPUI 1")
+impl Platform for TestPlatform {
+    fn executor(&self) -> Executor {
+        self.executor.clone()
+    }
+
+    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
+        Arc::new(crate::platform::mac::MacTextSystem::new())
+    }
+
+    fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
+        unimplemented!()
+    }
+
+    fn quit(&self) {
+        unimplemented!()
+    }
+
+    fn restart(&self) {
+        unimplemented!()
+    }
+
+    fn activate(&self, _ignoring_other_apps: bool) {
+        unimplemented!()
+    }
+
+    fn hide(&self) {
+        unimplemented!()
+    }
+
+    fn hide_other_apps(&self) {
+        unimplemented!()
+    }
+
+    fn unhide_other_apps(&self) {
+        unimplemented!()
+    }
+
+    fn displays(&self) -> Vec<std::rc::Rc<dyn crate::PlatformDisplay>> {
+        unimplemented!()
+    }
+
+    fn display(&self, _id: DisplayId) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
+        unimplemented!()
+    }
+
+    fn main_window(&self) -> Option<crate::AnyWindowHandle> {
+        unimplemented!()
+    }
+
+    fn open_window(
+        &self,
+        _handle: crate::AnyWindowHandle,
+        _options: crate::WindowOptions,
+    ) -> Box<dyn crate::PlatformWindow> {
+        unimplemented!()
+    }
+
+    fn set_display_link_output_callback(
+        &self,
+        _display_id: DisplayId,
+        _callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp)>,
+    ) {
+        unimplemented!()
+    }
+
+    fn start_display_link(&self, _display_id: DisplayId) {
+        unimplemented!()
+    }
+
+    fn stop_display_link(&self, _display_id: DisplayId) {
+        unimplemented!()
+    }
+
+    fn open_url(&self, _url: &str) {
+        unimplemented!()
+    }
+
+    fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
+        unimplemented!()
+    }
+
+    fn prompt_for_paths(
+        &self,
+        _options: crate::PathPromptOptions,
+    ) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
+        unimplemented!()
+    }
+
+    fn prompt_for_new_path(
+        &self,
+        _directory: &std::path::Path,
+    ) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
+        unimplemented!()
+    }
+
+    fn reveal_path(&self, _path: &std::path::Path) {
+        unimplemented!()
+    }
+
+    fn on_become_active(&self, _callback: Box<dyn FnMut()>) {
+        unimplemented!()
+    }
+
+    fn on_resign_active(&self, _callback: Box<dyn FnMut()>) {
+        unimplemented!()
+    }
+
+    fn on_quit(&self, _callback: Box<dyn FnMut()>) {
+        unimplemented!()
+    }
+
+    fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
+        unimplemented!()
+    }
+
+    fn on_event(&self, _callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
+        unimplemented!()
+    }
+
+    fn os_name(&self) -> &'static str {
+        unimplemented!()
+    }
+
+    fn os_version(&self) -> anyhow::Result<crate::SemanticVersion> {
+        unimplemented!()
+    }
+
+    fn app_version(&self) -> anyhow::Result<crate::SemanticVersion> {
+        unimplemented!()
+    }
+
+    fn app_path(&self) -> anyhow::Result<std::path::PathBuf> {
+        unimplemented!()
+    }
+
+    fn local_timezone(&self) -> time::UtcOffset {
+        unimplemented!()
+    }
+
+    fn path_for_auxiliary_executable(&self, _name: &str) -> anyhow::Result<std::path::PathBuf> {
+        unimplemented!()
+    }
+
+    fn set_cursor_style(&self, _style: crate::CursorStyle) {
+        unimplemented!()
+    }
+
+    fn should_auto_hide_scrollbars(&self) -> bool {
+        unimplemented!()
+    }
+
+    fn write_to_clipboard(&self, _item: crate::ClipboardItem) {
+        unimplemented!()
+    }
+
+    fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
+        unimplemented!()
+    }
+
+    fn write_credentials(
+        &self,
+        _url: &str,
+        _username: &str,
+        _password: &[u8],
+    ) -> anyhow::Result<()> {
+        unimplemented!()
+    }
+
+    fn read_credentials(&self, _url: &str) -> anyhow::Result<Option<(String, Vec<u8>)>> {
+        unimplemented!()
+    }
+
+    fn delete_credentials(&self, _url: &str) -> anyhow::Result<()> {
+        unimplemented!()
+    }
+}

crates/gpui2/src/text_system/line_wrapper.rs 🔗

@@ -143,7 +143,7 @@ mod tests {
 
     #[test]
     fn test_wrap_line() {
-        App::test().run(|cx| {
+        App::test(0).run(|cx| {
             let text_system = cx.text_system().clone();
             let mut wrapper = LineWrapper::new(
                 text_system.font_id(&font("Courier")).unwrap(),

crates/gpui2/src/util.rs 🔗

@@ -12,10 +12,10 @@ pub use util::*;
 //     timer.race(future).await
 // }
 
-#[cfg(any(test, feature = "test"))]
+#[cfg(any(test, feature = "test-support"))]
 pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace);
 
-#[cfg(any(test, feature = "test"))]
+#[cfg(any(test, feature = "test-support"))]
 impl<'a> std::fmt::Debug for CwdBacktrace<'a> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         use backtrace::{BacktraceFmt, BytesOrWideString};

crates/gpui2/src/window.rs 🔗

@@ -1819,7 +1819,7 @@ impl AnyWindowHandle {
     }
 }
 
-#[cfg(any(test, feature = "test"))]
+#[cfg(any(test, feature = "test-support"))]
 impl From<SmallVec<[u32; 16]>> for StackingOrder {
     fn from(small_vec: SmallVec<[u32; 16]>) -> Self {
         StackingOrder(small_vec)