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