mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 17:28:40 +00:00
Introduce a new detect_nondeterminism = true
attribute to gpui::test
This commit is contained in:
parent
f0a721032d
commit
fa3f100eff
3 changed files with 150 additions and 33 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue