538 lines · 18.8 KB · Rust
RawDownload
//! 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, Application, Bounds, Context, FocusHandle, Focusable, Render, Task, Window, WindowBounds,    WindowOptions, actions, div, prelude::*, px, rgb, size,}; actions!(counter, [Increment, Decrement]); struct Counter {    count: i32,    focus_handle: FocusHandle,    _subscription: gpui::Subscription,} /// Event emitted by Counterstruct CounterEvent; impl gpui::EventEmitter<CounterEvent> for Counter {} impl Counter {    fn new(cx: &mut Context<Self>) -> 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>) {        self.count += 1;        cx.notify();    }     fn decrement(&mut self, _: &Decrement, _window: &mut Window, cx: &mut Context<Self>) {        self.count -= 1;        cx.notify();    }     fn load(&self, cx: &mut Context<Self>) -> 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<Self>) {        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<Self>) -> 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 main() {    Application::new().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(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<i32>,            a_to_b: Vec<i32>,            b_to_a: Vec<i32>,        }         /// A mock network that delivers messages between two peers.        #[derive(Clone)]        struct MockNetwork {            state: Arc<Mutex<MockNetworkState>>,        }         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<i32> {                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>) {                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));

Lines 1–500 of 538.