//! Test support for GPUI. //! //! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context, //! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run //! deterministically even in the face of arbitrary parallelism. //! //! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test` //! or `cargo-nextest`, or another runner of your choice. //! //! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts //! as you need. //! //! ## Example //! //! ``` //! use gpui; //! //! #[gpui::test] //! async fn test_example(cx: &TestAppContext) { //! assert!(true) //! } //! //! #[gpui::test] //! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) { //! assert!(true) //! } //! ``` use crate::{Entity, Subscription, TestAppContext, TestDispatcher}; use futures::StreamExt as _; use rand::prelude::*; use smol::channel; use std::{ env, panic::{self, RefUnwindSafe}, pin::Pin, }; /// Run the given test function with the configured parameters. /// This is intended for use with the `gpui::test` macro /// and generally should not be used directly. pub fn run_test( num_iterations: usize, explicit_seeds: &[u64], max_retries: usize, test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)), on_fail_fn: Option, ) { let (seeds, is_multiple_runs) = calculate_seeds(num_iterations as u64, explicit_seeds); for seed in seeds { let mut attempt = 0; loop { if is_multiple_runs { eprintln!("seed = {seed}"); } let result = panic::catch_unwind(|| { let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed)); test_fn(dispatcher, seed); }); match result { Ok(_) => break, Err(error) => { if attempt < max_retries { println!("attempt {} failed, retrying", attempt); attempt += 1; } else { if is_multiple_runs { eprintln!("failing seed: {}", seed); } if let Some(on_fail_fn) = on_fail_fn { on_fail_fn() } panic::resume_unwind(error); } } } } } } fn calculate_seeds( iterations: u64, explicit_seeds: &[u64], ) -> (impl Iterator + '_, bool) { let iterations = env::var("ITERATIONS") .ok() .map(|var| var.parse().expect("invalid ITERATIONS variable")) .unwrap_or(iterations); let env_num = env::var("SEED") .map(|seed| seed.parse().expect("invalid SEED variable as integer")) .ok(); let empty_range = || 0..0; let iter = { let env_range = if let Some(env_num) = env_num { env_num..env_num + 1 } else { empty_range() }; // if `iterations` is 1 and !(`explicit_seeds` is non-empty || `SEED` is set), then add the run `0` // if `iterations` is 1 and (`explicit_seeds` is non-empty || `SEED` is set), then discard the run `0` // if `iterations` isn't 1 and `SEED` is set, do `SEED..SEED+iterations` // otherwise, do `0..iterations` let iterations_range = match (iterations, env_num) { (1, None) if explicit_seeds.is_empty() => 0..1, (1, None) | (1, Some(_)) => empty_range(), (iterations, Some(env)) => env..env + iterations, (iterations, None) => 0..iterations, }; // if `SEED` is set, ignore `explicit_seeds` let explicit_seeds = if env_num.is_some() { &[] } else { explicit_seeds }; env_range .chain(iterations_range) .chain(explicit_seeds.iter().copied()) }; let is_multiple_runs = iter.clone().nth(1).is_some(); (iter, is_multiple_runs) } /// A test struct for converting an observation callback into a stream. pub struct Observation { rx: Pin>>, _subscription: Subscription, } impl futures::Stream for Observation { type Item = T; fn poll_next( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { self.rx.poll_next_unpin(cx) } } /// observe returns a stream of the change events from the given `Entity` pub fn observe(entity: &Entity, cx: &mut TestAppContext) -> Observation<()> { let (tx, rx) = smol::channel::unbounded(); let _subscription = cx.update(|cx| { cx.observe(entity, move |_, _| { let _ = smol::block_on(tx.send(())); }) }); let rx = Box::pin(rx); Observation { rx, _subscription } }