Introduce a new detect_nondeterminism = true attribute to gpui::test

This commit is contained in:
Antonio Scandurra 2022-11-28 19:01:28 +01:00
parent f0a721032d
commit fa3f100eff
3 changed files with 150 additions and 33 deletions

View file

@ -66,21 +66,31 @@ struct DeterministicState {
rng: rand::prelude::StdRng, rng: rand::prelude::StdRng,
seed: u64, seed: u64,
scheduled_from_foreground: collections::HashMap<usize, Vec<ForegroundRunnable>>, scheduled_from_foreground: collections::HashMap<usize, Vec<ForegroundRunnable>>,
scheduled_from_background: Vec<Runnable>, scheduled_from_background: Vec<BackgroundRunnable>,
forbid_parking: bool, forbid_parking: bool,
block_on_ticks: std::ops::RangeInclusive<usize>, block_on_ticks: std::ops::RangeInclusive<usize>,
now: std::time::Instant, now: std::time::Instant,
next_timer_id: usize, next_timer_id: usize,
pending_timers: Vec<(usize, std::time::Instant, postage::barrier::Sender)>, pending_timers: Vec<(usize, std::time::Instant, postage::barrier::Sender)>,
waiting_backtrace: Option<backtrace::Backtrace>, waiting_backtrace: Option<backtrace::Backtrace>,
next_runnable_id: usize,
poll_history: Vec<usize>,
runnable_backtraces: collections::HashMap<usize, backtrace::Backtrace>,
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
struct ForegroundRunnable { struct ForegroundRunnable {
id: usize,
runnable: Runnable, runnable: Runnable,
main: bool, main: bool,
} }
#[cfg(any(test, feature = "test-support"))]
struct BackgroundRunnable {
id: usize,
runnable: Runnable,
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub struct Deterministic { pub struct Deterministic {
state: Arc<parking_lot::Mutex<DeterministicState>>, state: Arc<parking_lot::Mutex<DeterministicState>>,
@ -117,11 +127,24 @@ impl Deterministic {
next_timer_id: Default::default(), next_timer_id: Default::default(),
pending_timers: Default::default(), pending_timers: Default::default(),
waiting_backtrace: None, waiting_backtrace: None,
next_runnable_id: 0,
poll_history: Default::default(),
runnable_backtraces: Default::default(),
})), })),
parker: Default::default(), parker: Default::default(),
}) })
} }
pub fn runnable_history(&self) -> Vec<usize> {
self.state.lock().poll_history.clone()
}
pub fn runnable_backtrace(&self, runnable_id: usize) -> backtrace::Backtrace {
let mut backtrace = self.state.lock().runnable_backtraces[&runnable_id].clone();
backtrace.resolve();
backtrace
}
pub fn build_background(self: &Arc<Self>) -> Arc<Background> { pub fn build_background(self: &Arc<Self>) -> Arc<Background> {
Arc::new(Background::Deterministic { Arc::new(Background::Deterministic {
executor: self.clone(), executor: self.clone(),
@ -142,6 +165,15 @@ impl Deterministic {
main: bool, main: bool,
) -> AnyLocalTask { ) -> AnyLocalTask {
let state = self.state.clone(); let state = self.state.clone();
let id;
{
let mut state = state.lock();
id = util::post_inc(&mut state.next_runnable_id);
state
.runnable_backtraces
.insert(id, backtrace::Backtrace::new_unresolved());
}
let unparker = self.parker.lock().unparker(); let unparker = self.parker.lock().unparker();
let (runnable, task) = async_task::spawn_local(future, move |runnable| { let (runnable, task) = async_task::spawn_local(future, move |runnable| {
let mut state = state.lock(); let mut state = state.lock();
@ -149,7 +181,7 @@ impl Deterministic {
.scheduled_from_foreground .scheduled_from_foreground
.entry(cx_id) .entry(cx_id)
.or_default() .or_default()
.push(ForegroundRunnable { runnable, main }); .push(ForegroundRunnable { id, runnable, main });
unparker.unpark(); unparker.unpark();
}); });
runnable.schedule(); runnable.schedule();
@ -158,10 +190,21 @@ impl Deterministic {
fn spawn(&self, future: AnyFuture) -> AnyTask { fn spawn(&self, future: AnyFuture) -> AnyTask {
let state = self.state.clone(); let state = self.state.clone();
let id;
{
let mut state = state.lock();
id = util::post_inc(&mut state.next_runnable_id);
state
.runnable_backtraces
.insert(id, backtrace::Backtrace::new_unresolved());
}
let unparker = self.parker.lock().unparker(); let unparker = self.parker.lock().unparker();
let (runnable, task) = async_task::spawn(future, move |runnable| { let (runnable, task) = async_task::spawn(future, move |runnable| {
let mut state = state.lock(); let mut state = state.lock();
state.scheduled_from_background.push(runnable); state
.scheduled_from_background
.push(BackgroundRunnable { id, runnable });
unparker.unpark(); unparker.unpark();
}); });
runnable.schedule(); runnable.schedule();
@ -178,15 +221,25 @@ impl Deterministic {
let woken = Arc::new(AtomicBool::new(false)); let woken = Arc::new(AtomicBool::new(false));
let state = self.state.clone(); let state = self.state.clone();
let id;
{
let mut state = state.lock();
id = util::post_inc(&mut state.next_runnable_id);
state
.runnable_backtraces
.insert(id, backtrace::Backtrace::new());
}
let unparker = self.parker.lock().unparker(); let unparker = self.parker.lock().unparker();
let (runnable, mut main_task) = unsafe { let (runnable, mut main_task) = unsafe {
async_task::spawn_unchecked(main_future, move |runnable| { async_task::spawn_unchecked(main_future, move |runnable| {
let mut state = state.lock(); let state = &mut *state.lock();
state state
.scheduled_from_foreground .scheduled_from_foreground
.entry(cx_id) .entry(cx_id)
.or_default() .or_default()
.push(ForegroundRunnable { .push(ForegroundRunnable {
id: util::post_inc(&mut state.next_runnable_id),
runnable, runnable,
main: true, main: true,
}); });
@ -248,9 +301,10 @@ impl Deterministic {
if !state.scheduled_from_background.is_empty() && state.rng.gen() { if !state.scheduled_from_background.is_empty() && state.rng.gen() {
let background_len = state.scheduled_from_background.len(); let background_len = state.scheduled_from_background.len();
let ix = state.rng.gen_range(0..background_len); let ix = state.rng.gen_range(0..background_len);
let runnable = state.scheduled_from_background.remove(ix); let background_runnable = state.scheduled_from_background.remove(ix);
state.poll_history.push(background_runnable.id);
drop(state); drop(state);
runnable.run(); background_runnable.runnable.run();
} else if !state.scheduled_from_foreground.is_empty() { } else if !state.scheduled_from_foreground.is_empty() {
let available_cx_ids = state let available_cx_ids = state
.scheduled_from_foreground .scheduled_from_foreground
@ -266,6 +320,7 @@ impl Deterministic {
if scheduled_from_cx.is_empty() { if scheduled_from_cx.is_empty() {
state.scheduled_from_foreground.remove(&cx_id_to_run); state.scheduled_from_foreground.remove(&cx_id_to_run);
} }
state.poll_history.push(foreground_runnable.id);
drop(state); drop(state);
@ -298,9 +353,10 @@ impl Deterministic {
let runnable_count = state.scheduled_from_background.len(); let runnable_count = state.scheduled_from_background.len();
let ix = state.rng.gen_range(0..=runnable_count); let ix = state.rng.gen_range(0..=runnable_count);
if ix < state.scheduled_from_background.len() { if ix < state.scheduled_from_background.len() {
let runnable = state.scheduled_from_background.remove(ix); let background_runnable = state.scheduled_from_background.remove(ix);
state.poll_history.push(background_runnable.id);
drop(state); drop(state);
runnable.run(); background_runnable.runnable.run();
} else { } else {
drop(state); drop(state);
if let Poll::Ready(result) = future.poll(&mut cx) { if let Poll::Ready(result) = future.poll(&mut cx) {

View file

@ -1,11 +1,13 @@
use crate::{ use crate::{
elements::Empty, executor, platform, Element, ElementBox, Entity, FontCache, Handle, elements::Empty, executor, platform, util::CwdBacktrace, Element, ElementBox, Entity,
LeakDetector, MutableAppContext, Platform, RenderContext, Subscription, TestAppContext, View, FontCache, Handle, LeakDetector, MutableAppContext, Platform, RenderContext, Subscription,
TestAppContext, View,
}; };
use futures::StreamExt; use futures::StreamExt;
use parking_lot::Mutex; use parking_lot::Mutex;
use smol::channel; use smol::channel;
use std::{ use std::{
fmt::Write,
panic::{self, RefUnwindSafe}, panic::{self, RefUnwindSafe},
rc::Rc, rc::Rc,
sync::{ sync::{
@ -29,13 +31,13 @@ pub fn run_test(
mut num_iterations: u64, mut num_iterations: u64,
mut starting_seed: u64, mut starting_seed: u64,
max_retries: usize, max_retries: usize,
detect_nondeterminism: bool,
test_fn: &mut (dyn RefUnwindSafe test_fn: &mut (dyn RefUnwindSafe
+ Fn( + Fn(
&mut MutableAppContext, &mut MutableAppContext,
Rc<platform::test::ForegroundPlatform>, Rc<platform::test::ForegroundPlatform>,
Arc<executor::Deterministic>, Arc<executor::Deterministic>,
u64, u64,
bool,
)), )),
fn_name: String, fn_name: String,
) { ) {
@ -60,10 +62,10 @@ pub fn run_test(
let platform = Arc::new(platform::test::platform()); let platform = Arc::new(platform::test::platform());
let font_system = platform.fonts(); let font_system = platform.fonts();
let font_cache = Arc::new(FontCache::new(font_system)); let font_cache = Arc::new(FontCache::new(font_system));
let mut prev_runnable_history: Option<Vec<usize>> = None;
loop { for _ in 0..num_iterations {
let seed = atomic_seed.fetch_add(1, SeqCst); let seed = atomic_seed.load(SeqCst);
let is_last_iteration = seed + 1 >= starting_seed + num_iterations;
if is_randomized { if is_randomized {
dbg!(seed); dbg!(seed);
@ -82,13 +84,7 @@ pub fn run_test(
fn_name.clone(), fn_name.clone(),
); );
cx.update(|cx| { cx.update(|cx| {
test_fn( test_fn(cx, foreground_platform.clone(), deterministic.clone(), seed);
cx,
foreground_platform.clone(),
deterministic.clone(),
seed,
is_last_iteration,
);
}); });
cx.update(|cx| cx.remove_all_windows()); cx.update(|cx| cx.remove_all_windows());
@ -96,8 +92,64 @@ pub fn run_test(
cx.update(|cx| cx.clear_globals()); cx.update(|cx| cx.clear_globals());
leak_detector.lock().detect(); leak_detector.lock().detect();
if is_last_iteration {
break; if detect_nondeterminism {
let curr_runnable_history = deterministic.runnable_history();
if let Some(prev_runnable_history) = prev_runnable_history {
let mut prev_entries = prev_runnable_history.iter().fuse();
let mut curr_entries = curr_runnable_history.iter().fuse();
let mut nondeterministic = false;
let mut common_history_prefix = Vec::new();
let mut prev_history_suffix = Vec::new();
let mut curr_history_suffix = Vec::new();
loop {
match (prev_entries.next(), curr_entries.next()) {
(None, None) => break,
(None, Some(curr_id)) => curr_history_suffix.push(*curr_id),
(Some(prev_id), None) => prev_history_suffix.push(*prev_id),
(Some(prev_id), Some(curr_id)) => {
if nondeterministic {
prev_history_suffix.push(*prev_id);
curr_history_suffix.push(*curr_id);
} else if prev_id == curr_id {
common_history_prefix.push(*curr_id);
} else {
nondeterministic = true;
prev_history_suffix.push(*prev_id);
curr_history_suffix.push(*curr_id);
}
}
}
}
if nondeterministic {
let mut error = String::new();
writeln!(&mut error, "Common prefix: {:?}", common_history_prefix)
.unwrap();
writeln!(&mut error, "Previous suffix: {:?}", prev_history_suffix)
.unwrap();
writeln!(&mut error, "Current suffix: {:?}", curr_history_suffix)
.unwrap();
let last_common_backtrace = common_history_prefix
.last()
.map(|runnable_id| deterministic.runnable_backtrace(*runnable_id));
writeln!(
&mut error,
"Last future that ran on both executions: {:?}",
last_common_backtrace.as_ref().map(CwdBacktrace)
)
.unwrap();
panic!("Detected non-determinism.\n{}", error);
}
}
prev_runnable_history = Some(curr_runnable_history);
}
if !detect_nondeterminism {
atomic_seed.fetch_add(1, SeqCst);
} }
} }
}); });
@ -112,7 +164,7 @@ pub fn run_test(
println!("retrying: attempt {}", retries); println!("retrying: attempt {}", retries);
} else { } else {
if is_randomized { if is_randomized {
eprintln!("failing seed: {}", atomic_seed.load(SeqCst) - 1); eprintln!("failing seed: {}", atomic_seed.load(SeqCst));
} }
panic::resume_unwind(error); panic::resume_unwind(error);
} }

View file

@ -14,6 +14,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
let mut max_retries = 0; let mut max_retries = 0;
let mut num_iterations = 1; let mut num_iterations = 1;
let mut starting_seed = 0; let mut starting_seed = 0;
let mut detect_nondeterminism = false;
for arg in args { for arg in args {
match arg { match arg {
@ -26,6 +27,9 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
let key_name = meta.path.get_ident().map(|i| i.to_string()); let key_name = meta.path.get_ident().map(|i| i.to_string());
let result = (|| { let result = (|| {
match key_name.as_deref() { match key_name.as_deref() {
Some("detect_nondeterminism") => {
detect_nondeterminism = parse_bool(&meta.lit)?
}
Some("retries") => max_retries = parse_int(&meta.lit)?, Some("retries") => max_retries = parse_int(&meta.lit)?,
Some("iterations") => num_iterations = parse_int(&meta.lit)?, Some("iterations") => num_iterations = parse_int(&meta.lit)?,
Some("seed") => starting_seed = parse_int(&meta.lit)?, Some("seed") => starting_seed = parse_int(&meta.lit)?,
@ -77,10 +81,6 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),)); inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
continue; continue;
} }
Some("bool") => {
inner_fn_args.extend(quote!(is_last_iteration,));
continue;
}
Some("Arc") => { Some("Arc") => {
if let syn::PathArguments::AngleBracketed(args) = if let syn::PathArguments::AngleBracketed(args) =
&last_segment.unwrap().arguments &last_segment.unwrap().arguments
@ -146,7 +146,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
#num_iterations as u64, #num_iterations as u64,
#starting_seed as u64, #starting_seed as u64,
#max_retries, #max_retries,
&mut |cx, foreground_platform, deterministic, seed, is_last_iteration| { #detect_nondeterminism,
&mut |cx, foreground_platform, deterministic, seed| {
#cx_vars #cx_vars
cx.foreground().run(#inner_fn_name(#inner_fn_args)); cx.foreground().run(#inner_fn_name(#inner_fn_args));
#cx_teardowns #cx_teardowns
@ -165,9 +166,6 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
Some("StdRng") => { Some("StdRng") => {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),)); inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
} }
Some("bool") => {
inner_fn_args.extend(quote!(is_last_iteration,));
}
_ => {} _ => {}
} }
} else { } else {
@ -189,7 +187,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
#num_iterations as u64, #num_iterations as u64,
#starting_seed as u64, #starting_seed as u64,
#max_retries, #max_retries,
&mut |cx, _, _, seed, is_last_iteration| #inner_fn_name(#inner_fn_args), #detect_nondeterminism,
&mut |cx, _, _, seed| #inner_fn_name(#inner_fn_args),
stringify!(#outer_fn_name).to_string(), stringify!(#outer_fn_name).to_string(),
); );
} }
@ -209,3 +208,13 @@ fn parse_int(literal: &Lit) -> Result<usize, TokenStream> {
result.map_err(|err| TokenStream::from(err.into_compile_error())) result.map_err(|err| TokenStream::from(err.into_compile_error()))
} }
fn parse_bool(literal: &Lit) -> Result<bool, TokenStream> {
let result = if let Lit::Bool(result) = &literal {
Ok(result.value)
} else {
Err(syn::Error::new(literal.span(), "must be a boolean"))
};
result.map_err(|err| TokenStream::from(err.into_compile_error()))
}