Skip to main content

gpui/
test.rs

1//! Support for the `#[gpui::test]` macro, and streams for observing entities in tests.
2//!
3//! `#[gpui::test]` runs a test function once per seed. Each `&TestAppContext`
4//! parameter gets its own app, and all the apps share one deterministic scheduler,
5//! so a failing seed replays the same interleaving. The functions in this module
6//! are used by the macro; you rarely need to call them directly. See the
7//! [testing guide](crate::_documentation::guides::testing).
8//!
9//! # Example
10//!
11//! ```
12//! use gpui::{AppContext, Global, TestAppContext};
13//!
14//! struct Name(&'static str);
15//! impl Global for Name {}
16//!
17//! #[gpui::test]
18//! fn test_counts_are_positive(cx: &mut TestAppContext) {
19//!     let count = cx.new(|_| 1usize);
20//!     cx.run_until_parked();
21//!     cx.read(|cx| assert!(*count.read(cx) > 0));
22//! }
23//!
24//! #[gpui::test]
25//! fn test_each_app_has_its_own_globals(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
26//!     cx_a.update(|cx| cx.set_global(Name("alice")));
27//!     cx_b.update(|cx| cx.set_global(Name("bob")));
28//!     cx_a.read_global::<Name, _>(|name, _| assert_eq!(name.0, "alice"));
29//!     cx_b.read_global::<Name, _>(|name, _| assert_eq!(name.0, "bob"));
30//! }
31//! ```
32use crate::{Entity, Subscription, TestAppContext, TestDispatcher, calculate_seeds};
33use futures::StreamExt as _;
34use proptest::prelude::{Just, Strategy, any};
35use std::{
36    env,
37    panic::{self, RefUnwindSafe, UnwindSafe},
38    pin::Pin,
39};
40
41/// Returns a proptest strategy for scheduler seeds.
42///
43/// When `SEED` is set, the strategy always produces that value. Otherwise it
44/// produces any `u64`. Seeds are not shrunk, so a failing seed is replayed as
45/// recorded rather than replaced with a smaller seed.
46/// This is for the `#[gpui::property_test]` macro.
47///
48/// # Panics
49///
50/// Panics if `SEED` is set but isn't a `u64`.
51pub fn seed_strategy() -> impl Strategy<Value = u64> {
52    match std::env::var("SEED") {
53        Ok(val) => Just(val.parse().unwrap()).boxed(),
54        Err(_) => any::<u64>().no_shrink().boxed(),
55    }
56}
57
58/// Sets the RNG seed of a proptest config from `SEED`, or from `0` when it's unset.
59///
60/// The same variable then controls both the scheduler seed and proptest's case
61/// generation. The `#[gpui::property_test]` macro uses this helper.
62///
63/// If `SEED` is not a `u64`, this function uses `0`. [`seed_strategy`] panics for
64/// the same input.
65pub fn apply_seed_to_proptest_config(
66    mut config: proptest::test_runner::Config,
67) -> proptest::test_runner::Config {
68    let seed = env::var("SEED")
69        .ok()
70        .and_then(|val| val.parse::<u64>().ok())
71        .unwrap_or(0);
72    config.rng_seed = proptest::test_runner::RngSeed::Fixed(seed);
73    config
74}
75
76/// Runs `test_fn` once on a dispatcher for `seed`, and returns its result.
77///
78/// Unlike [`run_test`], this runs once without retries, and `test_fn` can be an
79/// `FnOnce`. The `#[gpui::property_test]` macro uses it. If `test_fn` panics,
80/// the panic is re-raised after the dispatcher is dropped.
81pub fn run_test_once<R>(
82    seed: u64,
83    test_fn: Box<dyn UnwindSafe + FnOnce(TestDispatcher) -> R>,
84) -> R {
85    let result = panic::catch_unwind(|| {
86        let dispatcher = TestDispatcher::new(seed);
87        let scheduler = dispatcher.scheduler().clone();
88        let res = test_fn(dispatcher);
89        scheduler.end_test();
90        res
91    });
92
93    match result {
94        Ok(r) => r,
95        Err(e) => panic::resume_unwind(e),
96    }
97}
98
99/// Runs `test_fn` for each seed, retrying a failing seed up to `max_retries` times.
100///
101/// The `#[gpui::test]` macro calls this function. It chooses seeds from the
102/// macro's arguments and these environment variables:
103///
104/// - `ITERATIONS=n` overrides the number of iterations.
105/// - `SEED=n` runs only seed `n`, or, with more than one iteration, the seeds
106///   `n` to `n + iterations - 1`. It replaces `explicit_seeds`.
107///
108/// When more than one seed runs, each seed is printed before it runs, and a
109/// failing seed is printed when its last attempt fails. After the last failure,
110/// `on_fail_fn` runs, then the panic is re-raised.
111pub fn run_test(
112    num_iterations: usize,
113    explicit_seeds: &[u64],
114    max_retries: usize,
115    test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)),
116    on_fail_fn: Option<fn()>,
117) {
118    let (seeds, is_multiple_runs) = calculate_seeds(num_iterations as u64, explicit_seeds);
119
120    for seed in seeds {
121        let mut attempt = 0;
122        loop {
123            if is_multiple_runs {
124                eprintln!("seed = {seed}");
125            }
126            let result = panic::catch_unwind(|| {
127                let dispatcher = TestDispatcher::new(seed);
128                let scheduler = dispatcher.scheduler().clone();
129                test_fn(dispatcher, seed);
130                scheduler.end_test();
131            });
132
133            match result {
134                Ok(_) => break,
135                Err(error) => {
136                    if attempt < max_retries {
137                        println!("attempt {} failed, retrying", attempt);
138                        attempt += 1;
139                        // The panic payload might itself trigger an unwind on drop:
140                        // https://doc.rust-lang.org/std/panic/fn.catch_unwind.html#notes
141                        std::mem::forget(error);
142                    } else {
143                        if is_multiple_runs {
144                            eprintln!("failing seed: {seed}");
145                            eprintln!(
146                                "You can rerun from this seed by setting the environmental variable SEED to {seed}"
147                            );
148                        }
149                        if let Some(on_fail_fn) = on_fail_fn {
150                            on_fail_fn()
151                        }
152                        panic::resume_unwind(error);
153                    }
154                }
155            }
156        }
157    }
158}
159
160/// A stream of unit values from an entity observation, created by [`observe`].
161///
162/// Dropping the stream stops the observation.
163pub struct Observation<T> {
164    rx: Pin<Box<async_channel::Receiver<T>>>,
165    _subscription: Subscription,
166}
167
168impl<T: 'static> futures::Stream for Observation<T> {
169    type Item = T;
170
171    fn poll_next(
172        mut self: std::pin::Pin<&mut Self>,
173        cx: &mut std::task::Context<'_>,
174    ) -> std::task::Poll<Option<Self::Item>> {
175        self.rx.poll_next_unpin(cx)
176    }
177}
178
179/// Returns a stream that yields `()` each time `entity` notifies.
180///
181/// Dropping the returned stream unsubscribes. For a stream that also ends when the
182/// entity is released, use [`TestAppContext::notifications`].
183pub fn observe<T: 'static>(entity: &Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
184    let (tx, rx) = async_channel::unbounded();
185    let _subscription = cx.update(|cx| {
186        cx.observe(entity, move |_, _| {
187            let _ = gpui::block_on(tx.send(()));
188        })
189    });
190    let rx = Box::pin(rx);
191
192    Observation { rx, _subscription }
193}