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}