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 counter = cx.new(|cx| Counter::new(cx));
315
316        // Perform random increments/decrements
317        let mut expected = 0i32;
318        for _ in 0..100 {
319            if rng.random_bool(0.5) {
320                expected += 1;
321                counter.update(cx, |counter, _| counter.count += 1);
322            } else {
323                expected -= 1;
324                counter.update(cx, |counter, _| counter.count -= 1);
325            }
326        }
327
328        let actual = counter.read_with(cx, |counter, _| counter.count);
329        assert_eq!(
330            actual, expected,
331            "Counter should match expected after random ops"
332        );
333    }
334
335    /// Now, all of those tests are good, but GPUI also provides strong support for testing distributed systems.
336    /// Let's setup a mock network and enhance the counter to send messages over it.
337    mod distributed_systems {
338        use std::sync::{Arc, Mutex};
339
340        /// A mock network that delivers messages between two peers.
341        struct MockNetwork {
342            a_to_b: Vec<i32>,
343            b_to_a: Vec<i32>,
344        }
345
346        impl MockNetwork {
347            fn new() -> Arc<Mutex<Self>> {
348                Arc::new(Mutex::new(Self {
349                    a_to_b: Vec::new(),
350                    b_to_a: Vec::new(),
351                }))
352            }
353
354            fn a_client(network: &Arc<Mutex<Self>>) -> NetworkClient {
355                NetworkClient {
356                    network: network.clone(),
357                    is_a: true,
358                }
359            }
360
361            fn b_client(network: &Arc<Mutex<Self>>) -> NetworkClient {
362                NetworkClient {
363                    network: network.clone(),
364                    is_a: false,
365                }
366            }
367        }
368
369        /// A client handle for sending/receiving messages over the mock network.
370        #[derive(Clone)]
371        struct NetworkClient {
372            network: Arc<Mutex<MockNetwork>>,
373            is_a: bool,
374        }
375
376        impl NetworkClient {
377            fn send(&self, value: i32) {
378                let mut network = self.network.lock().unwrap();
379                if self.is_a {
380                    network.b_to_a.push(value);
381                } else {
382                    network.a_to_b.push(value);
383                }
384            }
385
386            fn receive_all(&self) -> Vec<i32> {
387                let mut network = self.network.lock().unwrap();
388                if self.is_a {
389                    network.a_to_b.drain(..).collect()
390                } else {
391                    network.b_to_a.drain(..).collect()
392                }
393            }
394        }
395
396        use gpui::Context;
397
398        /// A networked counter that can send/receive over a mock network.
399        struct NetworkedCounter {
400            count: i32,
401            client: NetworkClient,
402        }
403
404        impl NetworkedCounter {
405            fn new(client: NetworkClient) -> Self {
406                Self { count: 0, client }
407            }
408
409            /// Increment the counter and broadcast the change.
410            fn increment(&mut self, delta: i32, cx: &mut Context<Self>) {
411                self.count += delta;
412
413                cx.background_spawn({
414                    let client = self.client.clone();
415                    async move {
416                        client.send(delta);
417                    }
418                })
419                .detach();
420            }
421
422            /// Process incoming increment requests.
423            fn sync(&mut self) {
424                for delta in self.client.receive_all() {
425                    self.count += delta;
426                }
427            }
428
429            /// Like increment, but tracks when the background send executes.
430            fn increment_tracked(
431                &mut self,
432                delta: i32,
433                cx: &mut Context<Self>,
434                order: Arc<Mutex<Vec<i32>>>,
435            ) {
436                self.count += delta;
437
438                cx.background_spawn({
439                    let client = self.client.clone();
440                    async move {
441                        order.lock().unwrap().push(delta);
442                        client.send(delta);
443                    }
444                })
445                .detach();
446            }
447        }
448
449        use super::*;
450
451        /// You can simulate distributed systems with multiple app contexts, simply by adding
452        /// additional parameters.
453        #[gpui::test]
454        fn test_app_sync(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
455            let network = MockNetwork::new();
456
457            let a = cx_a.new(|_| NetworkedCounter::new(MockNetwork::a_client(&network)));
458            let b = cx_b.new(|_| NetworkedCounter::new(MockNetwork::b_client(&network)));
459
460            // B increments locally and broadcasts the delta
461            b.update(cx_b, |b, cx| b.increment(42, cx));
462            b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // B's count is set immediately
463            a.read_with(cx_a, |a, _| assert_eq!(a.count, 0)); // A's count is in a side effect
464
465            cx_b.run_until_parked(); // Send the delta from B
466            a.update(cx_a, |a, _| a.sync()); // Receive the delta at A
467
468            b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // Both counts now match
469            a.read_with(cx_a, |a, _| assert_eq!(a.count, 42));
470        }
471
472        /// Multiple apps can run concurrently, and to capture this each test app shares
473        /// a dispatcher. Whenever you call `run_until_parked`, the dispatcher will randomly
474        /// pick which app's tasks to run next. This allows you to test that your distributed code
475        /// is robust to different execution orderings.
476        #[gpui::test(iterations = 10)]
477        fn test_random_interleaving(
478            cx_a: &mut TestAppContext,
479            cx_b: &mut TestAppContext,
480            mut rng: StdRng,
481        ) {
482            let network = MockNetwork::new();
483
484            // Track execution order
485            let actual_order = Arc::new(Mutex::new(Vec::new()));
486            let mut original_order = Vec::new();
487            let a = cx_a.new(|_| NetworkedCounter::new(MockNetwork::a_client(&network)));
488            let b = cx_b.new(|_| NetworkedCounter::new(MockNetwork::b_client(&network)));
489
490            let num_operations: usize = rng.random_range(3..8);
491
492            for i in 0..num_operations {
493                let id = i as i32;
494                let which = rng.random_bool(0.5);
495
496                original_order.push(id);
497                if which {
498                    b.update(cx_b, |b, cx| {
499                        b.increment_tracked(id, cx, actual_order.clone())
500                    });
501                } else {
502                    a.update(cx_a, |a, cx| {
503                        a.increment_tracked(id, cx, actual_order.clone())
504                    });
505                }
506            }
507
508            // This will send all of the pending increment messages, from both a and b
509            cx_a.run_until_parked();
510
511            a.update(cx_a, |a, _| a.sync());
512            b.update(cx_b, |b, _| b.sync());
513
514            let a_count = a.read_with(cx_a, |a, _| a.count);
515            let b_count = b.read_with(cx_b, |b, _| b.count);
516
517            assert_eq!(a_count, b_count, "A and B should have the same count");
518
519            // Nicely format the execution order output.
520            // Run this test with `-- --nocapture` to see it!
521            let actual = actual_order.lock().unwrap();
522            let spawned: Vec<_> = original_order.iter().map(|n| format!("{}", n)).collect();
523            let ran: Vec<_> = actual.iter().map(|n| format!("{}", n)).collect();
524            let diff: Vec<_> = original_order
525                .iter()
526                .zip(actual.iter())
527                .map(|(o, a)| {
528                    if o == a {
529                        " ".to_string()
530                    } else {
531                        "^".to_string()
532                    }
533                })
534                .collect();
535            println!("spawned: [{}]", spawned.join(", "));
536            println!("ran:     [{}]", ran.join(", "));
537            println!("         [{}]", diff.join(", "));
538        }
539    }
540}