testing.rs

  1//! Example demonstrating GPUI's testing infrastructure.
  2//!
  3//! When run normally, this displays an interactive counter window.
  4//! The tests below demonstrate various GPUI testing patterns.
  5//!
  6//! Run the app: cargo run -p gpui --example testing
  7//! Run tests:   cargo test -p gpui --example testing --features test-support
  8
  9use gpui::{
 10    App, Application, Bounds, Context, FocusHandle, Focusable, Render, Task, Window, WindowBounds,
 11    WindowOptions, actions, div, prelude::*, px, rgb, size,
 12};
 13
 14actions!(counter, [Increment, Decrement]);
 15
 16struct Counter {
 17    count: i32,
 18    focus_handle: FocusHandle,
 19    _subscription: gpui::Subscription,
 20}
 21
 22/// Event emitted by Counter
 23struct CounterEvent;
 24
 25impl gpui::EventEmitter<CounterEvent> for Counter {}
 26
 27impl Counter {
 28    fn new(cx: &mut Context<Self>) -> Self {
 29        let subscription = cx.subscribe_self(|this: &mut Self, _event: &CounterEvent, _cx| {
 30            this.count = 999;
 31        });
 32
 33        Self {
 34            count: 0,
 35            focus_handle: cx.focus_handle(),
 36            _subscription: subscription,
 37        }
 38    }
 39
 40    fn increment(&mut self, _: &Increment, _window: &mut Window, cx: &mut Context<Self>) {
 41        self.count += 1;
 42        cx.notify();
 43    }
 44
 45    fn decrement(&mut self, _: &Decrement, _window: &mut Window, cx: &mut Context<Self>) {
 46        self.count -= 1;
 47        cx.notify();
 48    }
 49
 50    fn load(&self, cx: &mut Context<Self>) -> Task<()> {
 51        cx.spawn(async move |this, cx| {
 52            // Simulate loading data (e.g., from disk or network)
 53            this.update(cx, |counter, _| {
 54                counter.count = 100;
 55            })
 56            .ok();
 57        })
 58    }
 59
 60    fn reload(&self, cx: &mut Context<Self>) {
 61        cx.spawn(async move |this, cx| {
 62            // Simulate reloading data in the background
 63            this.update(cx, |counter, _| {
 64                counter.count += 50;
 65            })
 66            .ok();
 67        })
 68        .detach();
 69    }
 70}
 71
 72impl Focusable for Counter {
 73    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 74        self.focus_handle.clone()
 75    }
 76}
 77
 78impl Render for Counter {
 79    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 80        div()
 81            .id("counter")
 82            .key_context("Counter")
 83            .on_action(cx.listener(Self::increment))
 84            .on_action(cx.listener(Self::decrement))
 85            .track_focus(&self.focus_handle)
 86            .flex()
 87            .flex_col()
 88            .gap_4()
 89            .bg(rgb(0x1e1e2e))
 90            .size_full()
 91            .justify_center()
 92            .items_center()
 93            .child(
 94                div()
 95                    .text_3xl()
 96                    .text_color(rgb(0xcdd6f4))
 97                    .child(format!("{}", self.count)),
 98            )
 99            .child(
100                div()
101                    .flex()
102                    .gap_2()
103                    .child(
104                        div()
105                            .id("decrement")
106                            .px_4()
107                            .py_2()
108                            .bg(rgb(0x313244))
109                            .hover(|s| s.bg(rgb(0x45475a)))
110                            .rounded_md()
111                            .cursor_pointer()
112                            .text_color(rgb(0xcdd6f4))
113                            .on_click(cx.listener(|this, _, window, cx| {
114                                this.decrement(&Decrement, window, cx)
115                            }))
116                            .child(""),
117                    )
118                    .child(
119                        div()
120                            .id("increment")
121                            .px_4()
122                            .py_2()
123                            .bg(rgb(0x313244))
124                            .hover(|s| s.bg(rgb(0x45475a)))
125                            .rounded_md()
126                            .cursor_pointer()
127                            .text_color(rgb(0xcdd6f4))
128                            .on_click(cx.listener(|this, _, window, cx| {
129                                this.increment(&Increment, window, cx)
130                            }))
131                            .child("+"),
132                    ),
133            )
134            .child(
135                div()
136                    .flex()
137                    .gap_2()
138                    .child(
139                        div()
140                            .id("load")
141                            .px_4()
142                            .py_2()
143                            .bg(rgb(0x313244))
144                            .hover(|s| s.bg(rgb(0x45475a)))
145                            .rounded_md()
146                            .cursor_pointer()
147                            .text_color(rgb(0xcdd6f4))
148                            .on_click(cx.listener(|this, _, _, cx| {
149                                this.load(cx).detach();
150                            }))
151                            .child("Load"),
152                    )
153                    .child(
154                        div()
155                            .id("reload")
156                            .px_4()
157                            .py_2()
158                            .bg(rgb(0x313244))
159                            .hover(|s| s.bg(rgb(0x45475a)))
160                            .rounded_md()
161                            .cursor_pointer()
162                            .text_color(rgb(0xcdd6f4))
163                            .on_click(cx.listener(|this, _, _, cx| {
164                                this.reload(cx);
165                            }))
166                            .child("Reload"),
167                    ),
168            )
169            .child(
170                div()
171                    .text_sm()
172                    .text_color(rgb(0x6c7086))
173                    .child("Press ↑/↓ or click buttons"),
174            )
175    }
176}
177
178fn main() {
179    Application::new().run(|cx: &mut App| {
180        cx.bind_keys([
181            gpui::KeyBinding::new("up", Increment, Some("Counter")),
182            gpui::KeyBinding::new("down", Decrement, Some("Counter")),
183        ]);
184
185        let bounds = Bounds::centered(None, size(px(300.), px(200.)), cx);
186        cx.open_window(
187            WindowOptions {
188                window_bounds: Some(WindowBounds::Windowed(bounds)),
189                ..Default::default()
190            },
191            |window, cx| {
192                let counter = cx.new(|cx| Counter::new(cx));
193                counter.focus_handle(cx).focus(window, cx);
194                counter
195            },
196        )
197        .unwrap();
198    });
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use gpui::{TestAppContext, VisualTestContext};
205    use rand::prelude::*;
206
207    /// Here's a basic GPUI test. Just add the macro and take a TestAppContext as an argument!
208    ///
209    /// Note that synchronous side effects run immediately after your "update*" calls complete.
210    #[gpui::test]
211    fn basic_testing(cx: &mut TestAppContext) {
212        let counter = cx.new(|cx| Counter::new(cx));
213
214        counter.update(cx, |counter, _| {
215            counter.count = 42;
216        });
217
218        // Note that TestAppContext doesn't support `read(cx)`
219        let updated = counter.read_with(cx, |counter, _| counter.count);
220        assert_eq!(updated, 42);
221
222        // Emit an event - the subscriber will run immediately after the update finishes
223        counter.update(cx, |_, cx| {
224            cx.emit(CounterEvent);
225        });
226
227        let count_after_update = counter.read_with(cx, |counter, _| counter.count);
228        assert_eq!(
229            count_after_update, 999,
230            "Side effects should run after update completes"
231        );
232    }
233
234    /// Tests which involve the window require you to construct a VisualTestContext.
235    /// Just like synchronous side effects, the window will be drawn after every "update*"
236    /// call, so you can test render-dependent behavior.
237    #[gpui::test]
238    fn test_counter_in_window(cx: &mut TestAppContext) {
239        let window = cx.update(|cx| {
240            cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx)))
241                .unwrap()
242        });
243
244        let mut cx = VisualTestContext::from_window(window.into(), cx);
245        let counter = window.root(&mut cx).unwrap();
246
247        // Action dispatch depends on the element tree to resolve which action handler
248        // to call, and this works exactly as you'd expect in a test.
249        let focus_handle = counter.read_with(&cx, |counter, _| counter.focus_handle.clone());
250        cx.update(|window, cx| {
251            focus_handle.dispatch_action(&Increment, window, cx);
252        });
253
254        let count_after = counter.read_with(&cx, |counter, _| counter.count);
255        assert_eq!(
256            count_after, 1,
257            "Action dispatched via focus handle should increment"
258        );
259    }
260
261    /// GPUI tests can also be async, simply add the async keyword before the test.
262    /// Note that the test executor is single thread, so async side effects (including
263    /// background tasks) won't run until you explicitly yield control.
264    #[gpui::test]
265    async fn test_async_operations(cx: &mut TestAppContext) {
266        let counter = cx.new(|cx| Counter::new(cx));
267
268        // Tasks can be awaited directly
269        counter.update(cx, |counter, cx| counter.load(cx)).await;
270
271        let count = counter.read_with(cx, |counter, _| counter.count);
272        assert_eq!(count, 100, "Load task should have set count to 100");
273
274        // But side effects don't run until you yield control
275        counter.update(cx, |counter, cx| counter.reload(cx));
276
277        let count = counter.read_with(cx, |counter, _| counter.count);
278        assert_eq!(count, 100, "Detached reload task shouldn't have run yet");
279
280        // This runs all pending tasks
281        cx.run_until_parked();
282
283        let count = counter.read_with(cx, |counter, _| counter.count);
284        assert_eq!(count, 150, "Reload task should have run after parking");
285    }
286
287    /// Note that the test executor panics if you await a future that waits on
288    /// something outside GPUI's control, like a reading a file or network IO.
289    /// You should mock external systems where possible, as this feature can be used
290    /// to detect potential deadlocks in your async code.
291    ///
292    /// However, if you want to disable this check use `allow_parking()`
293    #[gpui::test]
294    async fn test_allow_parking(cx: &mut TestAppContext) {
295        // Allow the thread to park
296        cx.executor().allow_parking();
297
298        // Simulate an external system (like a file system) with an OS thread
299        let (tx, rx) = futures::channel::oneshot::channel();
300        std::thread::spawn(move || {
301            std::thread::sleep(std::time::Duration::from_millis(5));
302            tx.send(42).ok();
303        });
304
305        // Without allow_parking(), this await would panic because GPUI's
306        // scheduler runs out of tasks while waiting for the external thread.
307        let result = rx.await.unwrap();
308        assert_eq!(result, 42);
309    }
310
311    /// GPUI also provides support for property testing, via the iterations flag
312    #[gpui::test(iterations = 10)]
313    fn test_counter_random_operations(cx: &mut TestAppContext, mut rng: StdRng) {
314        let window = cx.update(|cx| {
315            cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx)))
316                .unwrap()
317        });
318        let mut cx = VisualTestContext::from_window(window.into(), cx);
319
320        let counter = cx.new(|cx| Counter::new(cx));
321
322        // Perform random increments/decrements
323        let mut expected = 0i32;
324        for _ in 0..100 {
325            if rng.random_bool(0.5) {
326                expected += 1;
327                counter.update_in(&mut cx, |counter, window, cx| {
328                    counter.increment(&Increment, window, cx)
329                });
330            } else {
331                expected -= 1;
332                counter.update_in(&mut cx, |counter, window, cx| {
333                    counter.decrement(&Decrement, window, cx)
334                });
335            }
336        }
337
338        let actual = counter.read_with(&cx, |counter, _| counter.count);
339        assert_eq!(
340            actual, expected,
341            "Counter should match expected after random ops"
342        );
343    }
344
345    /// Now, all of those tests are good, but GPUI also provides strong support for testing distributed systems.
346    /// Let's setup a mock network and enhance the counter to send messages over it.
347    mod distributed_systems {
348        use std::sync::{Arc, Mutex};
349
350        /// The state of the mock network.
351        struct MockNetworkState {
352            ordering: Vec<i32>,
353            a_to_b: Vec<i32>,
354            b_to_a: Vec<i32>,
355        }
356
357        /// A mock network that delivers messages between two peers.
358        #[derive(Clone)]
359        struct MockNetwork {
360            state: Arc<Mutex<MockNetworkState>>,
361        }
362
363        impl MockNetwork {
364            fn new() -> Self {
365                Self {
366                    state: Arc::new(Mutex::new(MockNetworkState {
367                        ordering: Vec::new(),
368                        a_to_b: Vec::new(),
369                        b_to_a: Vec::new(),
370                    })),
371                }
372            }
373
374            fn a_client(&self) -> NetworkClient {
375                NetworkClient {
376                    network: self.clone(),
377                    is_a: true,
378                }
379            }
380
381            fn b_client(&self) -> NetworkClient {
382                NetworkClient {
383                    network: self.clone(),
384                    is_a: false,
385                }
386            }
387        }
388
389        /// A client handle for sending/receiving messages over the mock network.
390        #[derive(Clone)]
391        struct NetworkClient {
392            network: MockNetwork,
393            is_a: bool,
394        }
395
396        // See, networking is easy!
397        impl NetworkClient {
398            fn send(&self, value: i32) {
399                let mut network = self.network.state.lock().unwrap();
400                network.ordering.push(value);
401                if self.is_a {
402                    network.b_to_a.push(value);
403                } else {
404                    network.a_to_b.push(value);
405                }
406            }
407
408            fn receive_all(&self) -> Vec<i32> {
409                let mut network = self.network.state.lock().unwrap();
410                if self.is_a {
411                    network.a_to_b.drain(..).collect()
412                } else {
413                    network.b_to_a.drain(..).collect()
414                }
415            }
416        }
417
418        use gpui::Context;
419
420        /// A networked counter that can send/receive over a mock network.
421        struct NetworkedCounter {
422            count: i32,
423            client: NetworkClient,
424        }
425
426        impl NetworkedCounter {
427            fn new(client: NetworkClient) -> Self {
428                Self { count: 0, client }
429            }
430
431            /// Increment the counter and broadcast the change.
432            fn increment(&mut self, delta: i32, cx: &mut Context<Self>) {
433                self.count += delta;
434
435                cx.background_spawn({
436                    let client = self.client.clone();
437                    async move {
438                        client.send(delta);
439                    }
440                })
441                .detach();
442            }
443
444            /// Process incoming increment requests.
445            fn sync(&mut self) {
446                for delta in self.client.receive_all() {
447                    self.count += delta;
448                }
449            }
450        }
451
452        use super::*;
453
454        /// You can simulate distributed systems with multiple app contexts, simply by adding
455        /// additional parameters.
456        #[gpui::test]
457        fn test_app_sync(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
458            let network = MockNetwork::new();
459
460            let a = cx_a.new(|_| NetworkedCounter::new(network.a_client()));
461            let b = cx_b.new(|_| NetworkedCounter::new(network.b_client()));
462
463            // B increments locally and broadcasts the delta
464            b.update(cx_b, |b, cx| b.increment(42, cx));
465            b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // B's count is set immediately
466            a.read_with(cx_a, |a, _| assert_eq!(a.count, 0)); // A's count is in a side effect
467
468            cx_b.run_until_parked(); // Send the delta from B
469            a.update(cx_a, |a, _| a.sync()); // Receive the delta at A
470
471            b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // Both counts now match
472            a.read_with(cx_a, |a, _| assert_eq!(a.count, 42));
473        }
474
475        /// Multiple apps can run concurrently, and to capture this each test app shares
476        /// a dispatcher. Whenever you call `run_until_parked`, the dispatcher will randomly
477        /// pick which app's tasks to run next. This allows you to test that your distributed code
478        /// is robust to different execution orderings.
479        #[gpui::test(iterations = 10)]
480        fn test_random_interleaving(
481            cx_a: &mut TestAppContext,
482            cx_b: &mut TestAppContext,
483            mut rng: StdRng,
484        ) {
485            let network = MockNetwork::new();
486
487            // Track execution order
488            let mut original_order = Vec::new();
489            let a = cx_a.new(|_| NetworkedCounter::new(MockNetwork::a_client(&network)));
490            let b = cx_b.new(|_| NetworkedCounter::new(MockNetwork::b_client(&network)));
491
492            let num_operations: usize = rng.random_range(3..8);
493
494            for i in 0..num_operations {
495                let i = i as i32;
496                let which = rng.random_bool(0.5);
497
498                original_order.push(i);
499                if which {
500                    b.update(cx_b, |b, cx| b.increment(i, cx));
501                } else {
502                    a.update(cx_a, |a, cx| a.increment(i, cx));
503                }
504            }
505
506            // This will send all of the pending increment messages, from both a and b
507            cx_a.run_until_parked();
508
509            a.update(cx_a, |a, _| a.sync());
510            b.update(cx_b, |b, _| b.sync());
511
512            let a_count = a.read_with(cx_a, |a, _| a.count);
513            let b_count = b.read_with(cx_b, |b, _| b.count);
514
515            assert_eq!(a_count, b_count, "A and B should have the same count");
516
517            // Nicely format the execution order output.
518            // Run this test with `-- --nocapture` to see it!
519            let actual = network.state.lock().unwrap().ordering.clone();
520            let spawned: Vec<_> = original_order.iter().map(|n| format!("{}", n)).collect();
521            let ran: Vec<_> = actual.iter().map(|n| format!("{}", n)).collect();
522            let diff: Vec<_> = original_order
523                .iter()
524                .zip(actual.iter())
525                .map(|(o, a)| {
526                    if o == a {
527                        " ".to_string()
528                    } else {
529                        "^".to_string()
530                    }
531                })
532                .collect();
533            println!("spawned: [{}]", spawned.join(", "));
534            println!("ran:     [{}]", ran.join(", "));
535            println!("         [{}]", diff.join(", "));
536        }
537    }
538}