From 3566d920c5de7417b73b881d632c2f83facc107a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 25 Oct 2023 17:33:16 +0200 Subject: [PATCH] Add deterministic `TestDispatcher` Co-Authored-By: Conrad Irwin Co-Authored-By: Max Brunsfeld Co-Authored-By: Kyle Caverly --- 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(-) create mode 100644 crates/gpui2/src/platform/test/dispatcher.rs create mode 100644 crates/gpui2/src/platform/test/platform.rs diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 2ed10469409191546f7807195801ec50233e5a43..9d4c7b62528bc654c0fc5a746d69da4f6b07e5d5 100644 --- a/crates/gpui2/src/app.rs +++ b/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) diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index 79582c11ad25bfcb3081627bc1243082a2690186..cd97b7b24473d19476f3f8b3487765beaf3a5fd7 100644 --- a/crates/gpui2/src/executor.rs +++ b/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(); + } } } } diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index fbae6c898b7925c79356732de00c4ca166ffbc32..bcc9da5890f7a725455f906613616ba9698ee4fa 100644 --- a/crates/gpui2/src/platform.rs +++ b/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 { diff --git a/crates/gpui2/src/platform/mac/dispatcher.rs b/crates/gpui2/src/platform/mac/dispatcher.rs index 06b8d35997adf014d64ebe85776b10d312d2e04c..8a0d860ef845f36dccf3a967be94928ed0987e0c 100644 --- a/crates/gpui2/src/platform/mac/dispatcher.rs +++ b/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) { diff --git a/crates/gpui2/src/platform/test.rs b/crates/gpui2/src/platform/test.rs index 4d63bb415af369eb2f1158ed80397e9a7bb7c868..7e59ced7301d75b7784c67bb5de0fe8893074b3f 100644 --- a/crates/gpui2/src/platform/test.rs +++ b/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 { - unimplemented!() - } - - fn run(&self, _on_finish_launching: Box) { - 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> { - unimplemented!() - } - - fn display(&self, _id: DisplayId) -> Option> { - unimplemented!() - } - - fn main_window(&self) -> Option { - unimplemented!() - } - - fn open_window( - &self, - _handle: crate::AnyWindowHandle, - _options: crate::WindowOptions, - ) -> Box { - unimplemented!() - } - - fn set_display_link_output_callback( - &self, - _display_id: DisplayId, - _callback: Box, - ) { - 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)>) { - unimplemented!() - } - - fn prompt_for_paths( - &self, - _options: crate::PathPromptOptions, - ) -> futures::channel::oneshot::Receiver>> { - unimplemented!() - } - - fn prompt_for_new_path( - &self, - _directory: &std::path::Path, - ) -> futures::channel::oneshot::Receiver> { - unimplemented!() - } - - fn reveal_path(&self, _path: &std::path::Path) { - unimplemented!() - } - - fn on_become_active(&self, _callback: Box) { - unimplemented!() - } - - fn on_resign_active(&self, _callback: Box) { - unimplemented!() - } - - fn on_quit(&self, _callback: Box) { - unimplemented!() - } - - fn on_reopen(&self, _callback: Box) { - unimplemented!() - } - - fn on_event(&self, _callback: Box bool>) { - unimplemented!() - } - - fn os_name(&self) -> &'static str { - unimplemented!() - } - - fn os_version(&self) -> anyhow::Result { - unimplemented!() - } - - fn app_version(&self) -> anyhow::Result { - unimplemented!() - } - - fn app_path(&self) -> anyhow::Result { - unimplemented!() - } - - fn local_timezone(&self) -> time::UtcOffset { - unimplemented!() - } - - fn path_for_auxiliary_executable(&self, _name: &str) -> anyhow::Result { - 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 { - unimplemented!() - } - - fn write_credentials( - &self, - _url: &str, - _username: &str, - _password: &[u8], - ) -> anyhow::Result<()> { - unimplemented!() - } - - fn read_credentials(&self, _url: &str) -> anyhow::Result)>> { - unimplemented!() - } - - fn delete_credentials(&self, _url: &str) -> anyhow::Result<()> { - unimplemented!() - } -} +pub use dispatcher::*; +pub use platform::*; diff --git a/crates/gpui2/src/platform/test/dispatcher.rs b/crates/gpui2/src/platform/test/dispatcher.rs new file mode 100644 index 0000000000000000000000000000000000000000..8aef3b9cd307c4171425eb2cfd94da70da6d09d3 --- /dev/null +++ b/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); + +struct TestDispatcherState { + random: StdRng, + foreground: VecDeque, + background: Vec, + delayed: BTreeMap, + 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); + } +} diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs new file mode 100644 index 0000000000000000000000000000000000000000..f4e88756c08221341d9804c99a226259bfedd700 --- /dev/null +++ b/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 { + Arc::new(crate::platform::mac::MacTextSystem::new()) + } + + fn run(&self, _on_finish_launching: Box) { + 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> { + unimplemented!() + } + + fn display(&self, _id: DisplayId) -> Option> { + unimplemented!() + } + + fn main_window(&self) -> Option { + unimplemented!() + } + + fn open_window( + &self, + _handle: crate::AnyWindowHandle, + _options: crate::WindowOptions, + ) -> Box { + unimplemented!() + } + + fn set_display_link_output_callback( + &self, + _display_id: DisplayId, + _callback: Box, + ) { + 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)>) { + unimplemented!() + } + + fn prompt_for_paths( + &self, + _options: crate::PathPromptOptions, + ) -> futures::channel::oneshot::Receiver>> { + unimplemented!() + } + + fn prompt_for_new_path( + &self, + _directory: &std::path::Path, + ) -> futures::channel::oneshot::Receiver> { + unimplemented!() + } + + fn reveal_path(&self, _path: &std::path::Path) { + unimplemented!() + } + + fn on_become_active(&self, _callback: Box) { + unimplemented!() + } + + fn on_resign_active(&self, _callback: Box) { + unimplemented!() + } + + fn on_quit(&self, _callback: Box) { + unimplemented!() + } + + fn on_reopen(&self, _callback: Box) { + unimplemented!() + } + + fn on_event(&self, _callback: Box bool>) { + unimplemented!() + } + + fn os_name(&self) -> &'static str { + unimplemented!() + } + + fn os_version(&self) -> anyhow::Result { + unimplemented!() + } + + fn app_version(&self) -> anyhow::Result { + unimplemented!() + } + + fn app_path(&self) -> anyhow::Result { + unimplemented!() + } + + fn local_timezone(&self) -> time::UtcOffset { + unimplemented!() + } + + fn path_for_auxiliary_executable(&self, _name: &str) -> anyhow::Result { + 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 { + unimplemented!() + } + + fn write_credentials( + &self, + _url: &str, + _username: &str, + _password: &[u8], + ) -> anyhow::Result<()> { + unimplemented!() + } + + fn read_credentials(&self, _url: &str) -> anyhow::Result)>> { + unimplemented!() + } + + fn delete_credentials(&self, _url: &str) -> anyhow::Result<()> { + unimplemented!() + } +} diff --git a/crates/gpui2/src/text_system/line_wrapper.rs b/crates/gpui2/src/text_system/line_wrapper.rs index 3dceec057288af318a36bd5bb9a6050a40f850b9..3420ed1be495f3aaef74176cad01cf6266ec8cb1 100644 --- a/crates/gpui2/src/text_system/line_wrapper.rs +++ b/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(), diff --git a/crates/gpui2/src/util.rs b/crates/gpui2/src/util.rs index 5e60ab091aaada1d43b5d8e4055b27a103ead1bc..1000800881e911d5e1d306d37fff88197e0c22d2 100644 --- a/crates/gpui2/src/util.rs +++ b/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}; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 55efe124b0d740cae4df5adb0c041c693334714b..2822e44d3fc83ce0c330954db5ee3e5450890371 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1819,7 +1819,7 @@ impl AnyWindowHandle { } } -#[cfg(any(test, feature = "test"))] +#[cfg(any(test, feature = "test-support"))] impl From> for StackingOrder { fn from(small_vec: SmallVec<[u32; 16]>) -> Self { StackingOrder(small_vec)