testing.rs

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