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}