From 140c8833fef4c82f916a6d6fbbbda1375b01f16b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 8 Jul 2021 12:03:00 -0700 Subject: [PATCH] Start work on a deterministic executor for tests Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 12 ++++- gpui/src/executor.rs | 113 ++++++++++++++++++++++++++++++++++++++--- gpui/src/lib.rs | 1 - gpui_macros/src/lib.rs | 59 ++++++++++++--------- 4 files changed, 151 insertions(+), 34 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index c0a2bc4876..5bd2a331f6 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -119,6 +119,7 @@ impl App { let foreground = Rc::new(executor::Foreground::test()); let cx = Rc::new(RefCell::new(MutableAppContext::new( foreground, + Arc::new(executor::Background::new()), Arc::new(platform), Rc::new(foreground_platform), (), @@ -134,6 +135,7 @@ impl App { let foreground = Rc::new(executor::Foreground::platform(platform.dispatcher())?); let app = Self(Rc::new(RefCell::new(MutableAppContext::new( foreground, + Arc::new(executor::Background::new()), platform.clone(), foreground_platform.clone(), asset_source, @@ -237,11 +239,16 @@ impl App { } impl TestAppContext { - pub fn new(foreground: Rc, first_entity_id: usize) -> Self { + pub fn new( + foreground: Rc, + background: Arc, + first_entity_id: usize, + ) -> Self { let platform = Arc::new(platform::test::platform()); let foreground_platform = Rc::new(platform::test::foreground_platform()); let mut cx = MutableAppContext::new( foreground.clone(), + background, platform, foreground_platform.clone(), (), @@ -566,6 +573,7 @@ pub struct MutableAppContext { impl MutableAppContext { fn new( foreground: Rc, + background: Arc, platform: Arc, foreground_platform: Rc, asset_source: impl AssetSource, @@ -582,7 +590,7 @@ impl MutableAppContext { windows: Default::default(), values: Default::default(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), - background: Arc::new(executor::Background::new()), + background, thread_pool: scoped_pool::Pool::new(num_cpus::get(), "app"), font_cache: Arc::new(FontCache::new(fonts)), }, diff --git a/gpui/src/executor.rs b/gpui/src/executor.rs index 6aab51fe1f..75a36f8088 100644 --- a/gpui/src/executor.rs +++ b/gpui/src/executor.rs @@ -1,9 +1,12 @@ use anyhow::{anyhow, Result}; use async_task::Runnable; pub use async_task::Task; +use parking_lot::Mutex; +use rand::prelude::*; use smol::prelude::*; use smol::{channel, Executor}; use std::rc::Rc; +use std::sync::mpsc::SyncSender; use std::sync::Arc; use std::{marker::PhantomData, thread}; @@ -15,11 +18,94 @@ pub enum Foreground { _not_send_or_sync: PhantomData>, }, Test(smol::LocalExecutor<'static>), + Deterministic(Arc), } -pub struct Background { - executor: Arc>, - _stop: channel::Sender<()>, +pub enum Background { + Deterministic(Arc), + Production { + executor: Arc>, + _stop: channel::Sender<()>, + }, +} + +pub struct Deterministic { + seed: u64, + runnables: Arc, Option>)>>, +} + +impl Deterministic { + fn new(seed: u64) -> Self { + Self { + seed, + runnables: Default::default(), + } + } + + pub fn spawn_local(&self, future: F) -> Task + where + T: 'static, + F: Future + 'static, + { + let runnables = self.runnables.clone(); + let (runnable, task) = async_task::spawn_local(future, move |runnable| { + let mut runnables = runnables.lock(); + runnables.0.push(runnable); + runnables.1.as_ref().unwrap().send(()).ok(); + }); + runnable.schedule(); + task + } + + pub fn spawn(&self, future: F) -> Task + where + T: 'static + Send, + F: 'static + Send + Future, + { + let runnables = self.runnables.clone(); + let (runnable, task) = async_task::spawn(future, move |runnable| { + let mut runnables = runnables.lock(); + runnables.0.push(runnable); + runnables.1.as_ref().unwrap().send(()).ok(); + }); + runnable.schedule(); + task + } + + pub fn run(&self, future: F) -> T + where + T: 'static, + F: Future + 'static, + { + let (wake_tx, wake_rx) = std::sync::mpsc::sync_channel(32); + let runnables = self.runnables.clone(); + runnables.lock().1 = Some(wake_tx); + + let (output_tx, output_rx) = std::sync::mpsc::channel(); + self.spawn_local(async move { + let output = future.await; + output_tx.send(output).unwrap(); + }) + .detach(); + + let mut rng = StdRng::seed_from_u64(self.seed); + loop { + if let Ok(value) = output_rx.try_recv() { + runnables.lock().1 = None; + return value; + } + + wake_rx.recv().unwrap(); + let runnable = { + let mut runnables = runnables.lock(); + let runnables = &mut runnables.0; + let index = rng.gen_range(0..runnables.len()); + runnables.remove(index) + }; + + runnable.run(); + } + } } impl Foreground { @@ -48,13 +134,15 @@ impl Foreground { task } Self::Test(executor) => executor.spawn(future), + Self::Deterministic(executor) => executor.spawn_local(future), } } - pub async fn run(&self, future: impl Future) -> T { + pub fn run(&self, future: impl 'static + Future) -> T { match self { Self::Platform { .. } => panic!("you can't call run on a platform foreground executor"), - Self::Test(executor) => executor.run(future).await, + Self::Test(executor) => smol::block_on(executor.run(future)), + Self::Deterministic(executor) => executor.run(future), } } } @@ -73,7 +161,7 @@ impl Background { .unwrap(); } - Self { + Self::Production { executor, _stop: stop.0, } @@ -84,6 +172,17 @@ impl Background { T: 'static + Send, F: Send + Future + 'static, { - self.executor.spawn(future) + match self { + Self::Production { executor, .. } => executor.spawn(future), + Self::Deterministic(executor) => executor.spawn(future), + } } } + +pub fn deterministic(seed: u64) -> (Rc, Arc) { + let executor = Arc::new(Deterministic::new(seed)); + ( + Rc::new(Foreground::Deterministic(executor.clone())), + Arc::new(Background::Deterministic(executor)), + ) +} diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index 29304b2f1a..216ed79b32 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -31,4 +31,3 @@ pub use presenter::{ SizeConstraint, Vector2FExt, }; pub use scoped_pool; -pub use smol::block_on; diff --git a/gpui_macros/src/lib.rs b/gpui_macros/src/lib.rs index 4c2087d754..3c988f1797 100644 --- a/gpui_macros/src/lib.rs +++ b/gpui_macros/src/lib.rs @@ -11,6 +11,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { let args = syn::parse_macro_input!(args as AttributeArgs); let mut max_retries = 0; + let mut iterations = 1; for arg in args { match arg { NestedMeta::Meta(Meta::Path(name)) @@ -19,9 +20,14 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { namespace = format_ident!("crate"); } NestedMeta::Meta(Meta::NameValue(meta)) => { - if let Some(result) = parse_retries(&meta) { + if let Some(result) = parse_int_meta(&meta, "retries") { match result { - Ok(retries) => max_retries = retries, + Ok(value) => max_retries = value, + Err(error) => return TokenStream::from(error.into_compile_error()), + } + } else if let Some(result) = parse_int_meta(&meta, "iterations") { + match result { + Ok(value) => iterations = value, Err(error) => return TokenStream::from(error.into_compile_error()), } } @@ -44,7 +50,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { let inner_fn_args = (0..inner_fn.sig.inputs.len()) .map(|i| { let first_entity_id = i * 100_000; - quote!(#namespace::TestAppContext::new(foreground.clone(), #first_entity_id),) + quote!(#namespace::TestAppContext::new(foreground.clone(), background.clone(), #first_entity_id),) }) .collect::(); @@ -54,29 +60,34 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { fn #outer_fn_name() { #inner_fn - if #max_retries > 0 { - let mut retries = 0; - loop { - let result = std::panic::catch_unwind(|| { - let foreground = ::std::rc::Rc::new(#namespace::executor::Foreground::test()); - #namespace::block_on(foreground.run(#inner_fn_name(#inner_fn_args))); - }); + let mut retries = 0; + let mut seed = 0; + loop { + let result = std::panic::catch_unwind(|| { + let (foreground, background) = #namespace::executor::deterministic(seed as u64); + foreground.run(#inner_fn_name(#inner_fn_args)); + }); - match result { - Ok(result) => return result, - Err(error) => { - if retries < #max_retries { - retries += 1; - println!("retrying: attempt {}", retries); - } else { - std::panic::resume_unwind(error); + match result { + Ok(result) => { + seed += 1; + retries = 0; + if seed == #iterations { + return result + } + } + Err(error) => { + if retries < #max_retries { + retries += 1; + println!("retrying: attempt {}", retries); + } else { + if #iterations > 1 { + eprintln!("failing seed: {}", seed); } + std::panic::resume_unwind(error); } } } - } else { - let foreground = ::std::rc::Rc::new(#namespace::executor::Foreground::test()); - #namespace::block_on(foreground.run(#inner_fn_name(#inner_fn_args))); } } } @@ -120,15 +131,15 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { TokenStream::from(quote!(#outer_fn)) } -fn parse_retries(meta: &MetaNameValue) -> Option> { +fn parse_int_meta(meta: &MetaNameValue, name: &str) -> Option> { let ident = meta.path.get_ident(); - if ident.map_or(false, |n| n == "retries") { + if ident.map_or(false, |n| n == name) { if let Lit::Int(int) = &meta.lit { Some(int.base10_parse()) } else { Some(Err(syn::Error::new( meta.lit.span(), - "retries mut be an integer", + format!("{} mut be an integer", name), ))) } } else {