1//! Test support for GPUI.
2//!
3//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context,
4//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run
5//! deterministically even in the face of arbitrary parallelism.
6//!
7//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test`
8//! or `cargo-nextest`, or another runner of your choice.
9//!
10//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts
11//! as you need.
12//!
13//! ## Example
14//!
15//! ```
16//! use gpui;
17//!
18//! #[gpui::test]
19//! async fn test_example(cx: &TestAppContext) {
20//! assert!(true)
21//! }
22//!
23//! #[gpui::test]
24//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) {
25//! assert!(true)
26//! }
27//! ```
28use crate::{Entity, Subscription, TestAppContext};
29use futures::StreamExt as _;
30use scheduler::{TestScheduler, TestSchedulerConfig};
31use smol::channel;
32use std::{
33 env,
34 panic::{self, RefUnwindSafe},
35 pin::Pin,
36 sync::Arc,
37};
38
39/// Run the given test function with the configured parameters.
40/// This is intended for use with the `gpui::test` macro
41/// and generally should not be used directly.
42pub fn run_test(
43 num_iterations: usize,
44 explicit_seeds: &[u64],
45 max_retries: usize,
46 test_fn: &mut (dyn RefUnwindSafe + Fn(Arc<TestScheduler>, u64)),
47 on_fail_fn: Option<fn()>,
48) {
49 let (seeds, is_multiple_runs) = calculate_seeds(num_iterations as u64, explicit_seeds);
50
51 for seed in seeds {
52 let mut attempt = 0;
53 loop {
54 if is_multiple_runs {
55 eprintln!("seed = {seed}");
56 }
57 let result = panic::catch_unwind(|| {
58 let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed(seed)));
59 test_fn(scheduler, seed);
60 });
61
62 match result {
63 Ok(_) => break,
64 Err(error) => {
65 if attempt < max_retries {
66 println!("attempt {} failed, retrying", attempt);
67 attempt += 1;
68 // The panic payload might itself trigger an unwind on drop:
69 // https://doc.rust-lang.org/std/panic/fn.catch_unwind.html#notes
70 std::mem::forget(error);
71 } else {
72 if is_multiple_runs {
73 eprintln!("failing seed: {}", seed);
74 }
75 if let Some(on_fail_fn) = on_fail_fn {
76 on_fail_fn()
77 }
78 panic::resume_unwind(error);
79 }
80 }
81 }
82 }
83 }
84}
85
86fn calculate_seeds(
87 iterations: u64,
88 explicit_seeds: &[u64],
89) -> (impl Iterator<Item = u64> + '_, bool) {
90 let iterations = env::var("ITERATIONS")
91 .ok()
92 .map(|var| var.parse().expect("invalid ITERATIONS variable"))
93 .unwrap_or(iterations);
94
95 let env_num = env::var("SEED")
96 .map(|seed| seed.parse().expect("invalid SEED variable as integer"))
97 .ok();
98
99 let empty_range = || 0..0;
100
101 let iter = {
102 let env_range = if let Some(env_num) = env_num {
103 env_num..env_num + 1
104 } else {
105 empty_range()
106 };
107
108 // if `iterations` is 1 and !(`explicit_seeds` is non-empty || `SEED` is set), then add the run `0`
109 // if `iterations` is 1 and (`explicit_seeds` is non-empty || `SEED` is set), then discard the run `0`
110 // if `iterations` isn't 1 and `SEED` is set, do `SEED..SEED+iterations`
111 // otherwise, do `0..iterations`
112 let iterations_range = match (iterations, env_num) {
113 (1, None) if explicit_seeds.is_empty() => 0..1,
114 (1, None) | (1, Some(_)) => empty_range(),
115 (iterations, Some(env)) => env..env + iterations,
116 (iterations, None) => 0..iterations,
117 };
118
119 // if `SEED` is set, ignore `explicit_seeds`
120 let explicit_seeds = if env_num.is_some() {
121 &[]
122 } else {
123 explicit_seeds
124 };
125
126 env_range
127 .chain(iterations_range)
128 .chain(explicit_seeds.iter().copied())
129 };
130 let is_multiple_runs = iter.clone().nth(1).is_some();
131 (iter, is_multiple_runs)
132}
133
134/// A test struct for converting an observation callback into a stream.
135pub struct Observation<T> {
136 rx: Pin<Box<channel::Receiver<T>>>,
137 _subscription: Subscription,
138}
139
140impl<T: 'static> futures::Stream for Observation<T> {
141 type Item = T;
142
143 fn poll_next(
144 mut self: std::pin::Pin<&mut Self>,
145 cx: &mut std::task::Context<'_>,
146 ) -> std::task::Poll<Option<Self::Item>> {
147 self.rx.poll_next_unpin(cx)
148 }
149}
150
151/// observe returns a stream of the change events from the given `Entity`
152pub fn observe<T: 'static>(entity: &Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
153 let (tx, rx) = smol::channel::unbounded();
154 let _subscription = cx.update(|cx| {
155 cx.observe(entity, move |_, _| {
156 let _ = smol::block_on(tx.send(()));
157 })
158 });
159 let rx = Box::pin(rx);
160
161 Observation { rx, _subscription }
162}