#![cfg_attr(target_family = "wasm", no_main)] //! Example demonstrating GPUI's testing infrastructure. //! //! When run normally, this displays an interactive counter window. //! The tests below demonstrate various GPUI testing patterns. //! //! Run the app: cargo run -p gpui --example testing //! Run tests: cargo test -p gpui --example testing --features test-support use gpui::{ App, Bounds, Context, FocusHandle, Focusable, Render, Task, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size, }; use gpui_platform::application; actions!(counter, [Increment, Decrement]); struct Counter { count: i32, focus_handle: FocusHandle, _subscription: gpui::Subscription, } /// Event emitted by Counter struct CounterEvent; impl gpui::EventEmitter for Counter {} impl Counter { fn new(cx: &mut Context) -> Self { let subscription = cx.subscribe_self(|this: &mut Self, _event: &CounterEvent, _cx| { this.count = 999; }); Self { count: 0, focus_handle: cx.focus_handle(), _subscription: subscription, } } fn increment(&mut self, _: &Increment, _window: &mut Window, cx: &mut Context) { self.count += 1; cx.notify(); } fn decrement(&mut self, _: &Decrement, _window: &mut Window, cx: &mut Context) { self.count -= 1; cx.notify(); } fn load(&self, cx: &mut Context) -> Task<()> { cx.spawn(async move |this, cx| { // Simulate loading data (e.g., from disk or network) this.update(cx, |counter, _| { counter.count = 100; }) .ok(); }) } fn reload(&self, cx: &mut Context) { cx.spawn(async move |this, cx| { // Simulate reloading data in the background this.update(cx, |counter, _| { counter.count += 50; }) .ok(); }) .detach(); } } impl Focusable for Counter { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for Counter { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .id("counter") .key_context("Counter") .on_action(cx.listener(Self::increment)) .on_action(cx.listener(Self::decrement)) .track_focus(&self.focus_handle) .flex() .flex_col() .gap_4() .bg(rgb(0x1e1e2e)) .size_full() .justify_center() .items_center() .child( div() .text_3xl() .text_color(rgb(0xcdd6f4)) .child(format!("{}", self.count)), ) .child( div() .flex() .gap_2() .child( div() .id("decrement") .px_4() .py_2() .bg(rgb(0x313244)) .hover(|s| s.bg(rgb(0x45475a))) .rounded_md() .cursor_pointer() .text_color(rgb(0xcdd6f4)) .on_click(cx.listener(|this, _, window, cx| { this.decrement(&Decrement, window, cx) })) .child("−"), ) .child( div() .id("increment") .px_4() .py_2() .bg(rgb(0x313244)) .hover(|s| s.bg(rgb(0x45475a))) .rounded_md() .cursor_pointer() .text_color(rgb(0xcdd6f4)) .on_click(cx.listener(|this, _, window, cx| { this.increment(&Increment, window, cx) })) .child("+"), ), ) .child( div() .flex() .gap_2() .child( div() .id("load") .px_4() .py_2() .bg(rgb(0x313244)) .hover(|s| s.bg(rgb(0x45475a))) .rounded_md() .cursor_pointer() .text_color(rgb(0xcdd6f4)) .on_click(cx.listener(|this, _, _, cx| { this.load(cx).detach(); })) .child("Load"), ) .child( div() .id("reload") .px_4() .py_2() .bg(rgb(0x313244)) .hover(|s| s.bg(rgb(0x45475a))) .rounded_md() .cursor_pointer() .text_color(rgb(0xcdd6f4)) .on_click(cx.listener(|this, _, _, cx| { this.reload(cx); })) .child("Reload"), ), ) .child( div() .text_sm() .text_color(rgb(0x6c7086)) .child("Press ↑/↓ or click buttons"), ) } } fn run_example() { application().run(|cx: &mut App| { cx.bind_keys([ gpui::KeyBinding::new("up", Increment, Some("Counter")), gpui::KeyBinding::new("down", Decrement, Some("Counter")), ]); let bounds = Bounds::centered(None, size(px(300.), px(200.)), cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), ..Default::default() }, |window, cx| { let counter = cx.new(|cx| Counter::new(cx)); counter.focus_handle(cx).focus(window, cx); counter }, ) .unwrap(); }); } #[cfg(not(target_family = "wasm"))] fn main() { run_example(); } #[cfg(target_family = "wasm")] #[wasm_bindgen::prelude::wasm_bindgen(start)] pub fn start() { gpui_platform::web_init(); run_example(); } #[cfg(test)] mod tests { use super::*; use gpui::{TestAppContext, VisualTestContext}; use rand::prelude::*; /// Here's a basic GPUI test. Just add the macro and take a TestAppContext as an argument! /// /// Note that synchronous side effects run immediately after your "update*" calls complete. #[gpui::test] fn basic_testing(cx: &mut TestAppContext) { let counter = cx.new(|cx| Counter::new(cx)); counter.update(cx, |counter, _| { counter.count = 42; }); // Note that TestAppContext doesn't support `read(cx)` let updated = counter.read_with(cx, |counter, _| counter.count); assert_eq!(updated, 42); // Emit an event - the subscriber will run immediately after the update finishes counter.update(cx, |_, cx| { cx.emit(CounterEvent); }); let count_after_update = counter.read_with(cx, |counter, _| counter.count); assert_eq!( count_after_update, 999, "Side effects should run after update completes" ); } /// Tests which involve the window require you to construct a VisualTestContext. /// Just like synchronous side effects, the window will be drawn after every "update*" /// call, so you can test render-dependent behavior. #[gpui::test] fn test_counter_in_window(cx: &mut TestAppContext) { let window = cx.update(|cx| { cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx))) .unwrap() }); let mut cx = VisualTestContext::from_window(window.into(), cx); let counter = window.root(&mut cx).unwrap(); // Action dispatch depends on the element tree to resolve which action handler // to call, and this works exactly as you'd expect in a test. let focus_handle = counter.read_with(&cx, |counter, _| counter.focus_handle.clone()); cx.update(|window, cx| { focus_handle.dispatch_action(&Increment, window, cx); }); let count_after = counter.read_with(&cx, |counter, _| counter.count); assert_eq!( count_after, 1, "Action dispatched via focus handle should increment" ); } /// GPUI tests can also be async, simply add the async keyword before the test. /// Note that the test executor is single thread, so async side effects (including /// background tasks) won't run until you explicitly yield control. #[gpui::test] async fn test_async_operations(cx: &mut TestAppContext) { let counter = cx.new(|cx| Counter::new(cx)); // Tasks can be awaited directly counter.update(cx, |counter, cx| counter.load(cx)).await; let count = counter.read_with(cx, |counter, _| counter.count); assert_eq!(count, 100, "Load task should have set count to 100"); // But side effects don't run until you yield control counter.update(cx, |counter, cx| counter.reload(cx)); let count = counter.read_with(cx, |counter, _| counter.count); assert_eq!(count, 100, "Detached reload task shouldn't have run yet"); // This runs all pending tasks cx.run_until_parked(); let count = counter.read_with(cx, |counter, _| counter.count); assert_eq!(count, 150, "Reload task should have run after parking"); } /// Note that the test executor panics if you await a future that waits on /// something outside GPUI's control, like a reading a file or network IO. /// You should mock external systems where possible, as this feature can be used /// to detect potential deadlocks in your async code. /// /// However, if you want to disable this check use `allow_parking()` #[gpui::test] async fn test_allow_parking(cx: &mut TestAppContext) { // Allow the thread to park cx.executor().allow_parking(); // Simulate an external system (like a file system) with an OS thread let (tx, rx) = futures::channel::oneshot::channel(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_millis(5)); tx.send(42).ok(); }); // Without allow_parking(), this await would panic because GPUI's // scheduler runs out of tasks while waiting for the external thread. let result = rx.await.unwrap(); assert_eq!(result, 42); } /// GPUI also provides support for property testing, via the iterations flag #[gpui::test(iterations = 10)] fn test_counter_random_operations(cx: &mut TestAppContext, mut rng: StdRng) { let window = cx.update(|cx| { cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx))) .unwrap() }); let mut cx = VisualTestContext::from_window(window.into(), cx); let counter = cx.new(|cx| Counter::new(cx)); // Perform random increments/decrements let mut expected = 0i32; for _ in 0..100 { if rng.random_bool(0.5) { expected += 1; counter.update_in(&mut cx, |counter, window, cx| { counter.increment(&Increment, window, cx) }); } else { expected -= 1; counter.update_in(&mut cx, |counter, window, cx| { counter.decrement(&Decrement, window, cx) }); } } let actual = counter.read_with(&cx, |counter, _| counter.count); assert_eq!( actual, expected, "Counter should match expected after random ops" ); } /// Now, all of those tests are good, but GPUI also provides strong support for testing distributed systems. /// Let's setup a mock network and enhance the counter to send messages over it. mod distributed_systems { use std::sync::{Arc, Mutex}; /// The state of the mock network. struct MockNetworkState { ordering: Vec, a_to_b: Vec, b_to_a: Vec, } /// A mock network that delivers messages between two peers. #[derive(Clone)] struct MockNetwork { state: Arc>, } impl MockNetwork { fn new() -> Self { Self { state: Arc::new(Mutex::new(MockNetworkState { ordering: Vec::new(), a_to_b: Vec::new(), b_to_a: Vec::new(), })), } } fn a_client(&self) -> NetworkClient { NetworkClient { network: self.clone(), is_a: true, } } fn b_client(&self) -> NetworkClient { NetworkClient { network: self.clone(), is_a: false, } } } /// A client handle for sending/receiving messages over the mock network. #[derive(Clone)] struct NetworkClient { network: MockNetwork, is_a: bool, } // See, networking is easy! impl NetworkClient { fn send(&self, value: i32) { let mut network = self.network.state.lock().unwrap(); network.ordering.push(value); if self.is_a { network.b_to_a.push(value); } else { network.a_to_b.push(value); } } fn receive_all(&self) -> Vec { let mut network = self.network.state.lock().unwrap(); if self.is_a { network.a_to_b.drain(..).collect() } else { network.b_to_a.drain(..).collect() } } } use gpui::Context; /// A networked counter that can send/receive over a mock network. struct NetworkedCounter { count: i32, client: NetworkClient, } impl NetworkedCounter { fn new(client: NetworkClient) -> Self { Self { count: 0, client } } /// Increment the counter and broadcast the change. fn increment(&mut self, delta: i32, cx: &mut Context) { self.count += delta; cx.background_spawn({ let client = self.client.clone(); async move { client.send(delta); } }) .detach(); } /// Process incoming increment requests. fn sync(&mut self) { for delta in self.client.receive_all() { self.count += delta; } } } use super::*; /// You can simulate distributed systems with multiple app contexts, simply by adding /// additional parameters. #[gpui::test] fn test_app_sync(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let network = MockNetwork::new(); let a = cx_a.new(|_| NetworkedCounter::new(network.a_client())); let b = cx_b.new(|_| NetworkedCounter::new(network.b_client())); // B increments locally and broadcasts the delta b.update(cx_b, |b, cx| b.increment(42, cx)); b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // B's count is set immediately a.read_with(cx_a, |a, _| assert_eq!(a.count, 0)); // A's count is in a side effect cx_b.run_until_parked(); // Send the delta from B a.update(cx_a, |a, _| a.sync()); // Receive the delta at A b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // Both counts now match a.read_with(cx_a, |a, _| assert_eq!(a.count, 42)); } /// Multiple apps can run concurrently, and to capture this each test app shares /// a dispatcher. Whenever you call `run_until_parked`, the dispatcher will randomly /// pick which app's tasks to run next. This allows you to test that your distributed code /// is robust to different execution orderings. #[gpui::test(iterations = 10)] fn test_random_interleaving( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, mut rng: StdRng, ) { let network = MockNetwork::new(); // Track execution order let mut original_order = Vec::new(); let a = cx_a.new(|_| NetworkedCounter::new(MockNetwork::a_client(&network))); let b = cx_b.new(|_| NetworkedCounter::new(MockNetwork::b_client(&network))); let num_operations: usize = rng.random_range(3..8); for i in 0..num_operations { let i = i as i32; let which = rng.random_bool(0.5); original_order.push(i); if which { b.update(cx_b, |b, cx| b.increment(i, cx)); } else { a.update(cx_a, |a, cx| a.increment(i, cx)); } } // This will send all of the pending increment messages, from both a and b cx_a.run_until_parked(); a.update(cx_a, |a, _| a.sync()); b.update(cx_b, |b, _| b.sync()); let a_count = a.read_with(cx_a, |a, _| a.count); let b_count = b.read_with(cx_b, |b, _| b.count); assert_eq!(a_count, b_count, "A and B should have the same count"); // Nicely format the execution order output. // Run this test with `-- --nocapture` to see it! let actual = network.state.lock().unwrap().ordering.clone(); let spawned: Vec<_> = original_order.iter().map(|n| format!("{}", n)).collect(); let ran: Vec<_> = actual.iter().map(|n| format!("{}", n)).collect(); let diff: Vec<_> = original_order .iter() .zip(actual.iter()) .map(|(o, a)| { if o == a { " ".to_string() } else { "^".to_string() } }) .collect(); println!("spawned: [{}]", spawned.join(", ")); println!("ran: [{}]", ran.join(", ")); println!(" [{}]", diff.join(", ")); } } }