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}