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 window = cx.update(|cx| {
315 cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx)))
316 .unwrap()
317 });
318 let mut cx = VisualTestContext::from_window(window.into(), cx);
319
320 let counter = cx.new(|cx| Counter::new(cx));
321
322 // Perform random increments/decrements
323 let mut expected = 0i32;
324 for _ in 0..100 {
325 if rng.random_bool(0.5) {
326 expected += 1;
327 counter.update_in(&mut cx, |counter, window, cx| {
328 counter.increment(&Increment, window, cx)
329 });
330 } else {
331 expected -= 1;
332 counter.update_in(&mut cx, |counter, window, cx| {
333 counter.decrement(&Decrement, window, cx)
334 });
335 }
336 }
337
338 let actual = counter.read_with(&cx, |counter, _| counter.count);
339 assert_eq!(
340 actual, expected,
341 "Counter should match expected after random ops"
342 );
343 }
344
345 /// Now, all of those tests are good, but GPUI also provides strong support for testing distributed systems.
346 /// Let's setup a mock network and enhance the counter to send messages over it.
347 mod distributed_systems {
348 use std::sync::{Arc, Mutex};
349
350 /// The state of the mock network.
351 struct MockNetworkState {
352 ordering: Vec<i32>,
353 a_to_b: Vec<i32>,
354 b_to_a: Vec<i32>,
355 }
356
357 /// A mock network that delivers messages between two peers.
358 #[derive(Clone)]
359 struct MockNetwork {
360 state: Arc<Mutex<MockNetworkState>>,
361 }
362
363 impl MockNetwork {
364 fn new() -> Self {
365 Self {
366 state: Arc::new(Mutex::new(MockNetworkState {
367 ordering: Vec::new(),
368 a_to_b: Vec::new(),
369 b_to_a: Vec::new(),
370 })),
371 }
372 }
373
374 fn a_client(&self) -> NetworkClient {
375 NetworkClient {
376 network: self.clone(),
377 is_a: true,
378 }
379 }
380
381 fn b_client(&self) -> NetworkClient {
382 NetworkClient {
383 network: self.clone(),
384 is_a: false,
385 }
386 }
387 }
388
389 /// A client handle for sending/receiving messages over the mock network.
390 #[derive(Clone)]
391 struct NetworkClient {
392 network: MockNetwork,
393 is_a: bool,
394 }
395
396 // See, networking is easy!
397 impl NetworkClient {
398 fn send(&self, value: i32) {
399 let mut network = self.network.state.lock().unwrap();
400 network.ordering.push(value);
401 if self.is_a {
402 network.b_to_a.push(value);
403 } else {
404 network.a_to_b.push(value);
405 }
406 }
407
408 fn receive_all(&self) -> Vec<i32> {
409 let mut network = self.network.state.lock().unwrap();
410 if self.is_a {
411 network.a_to_b.drain(..).collect()
412 } else {
413 network.b_to_a.drain(..).collect()
414 }
415 }
416 }
417
418 use gpui::Context;
419
420 /// A networked counter that can send/receive over a mock network.
421 struct NetworkedCounter {
422 count: i32,
423 client: NetworkClient,
424 }
425
426 impl NetworkedCounter {
427 fn new(client: NetworkClient) -> Self {
428 Self { count: 0, client }
429 }
430
431 /// Increment the counter and broadcast the change.
432 fn increment(&mut self, delta: i32, cx: &mut Context<Self>) {
433 self.count += delta;
434
435 cx.background_spawn({
436 let client = self.client.clone();
437 async move {
438 client.send(delta);
439 }
440 })
441 .detach();
442 }
443
444 /// Process incoming increment requests.
445 fn sync(&mut self) {
446 for delta in self.client.receive_all() {
447 self.count += delta;
448 }
449 }
450 }
451
452 use super::*;
453
454 /// You can simulate distributed systems with multiple app contexts, simply by adding
455 /// additional parameters.
456 #[gpui::test]
457 fn test_app_sync(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
458 let network = MockNetwork::new();
459
460 let a = cx_a.new(|_| NetworkedCounter::new(network.a_client()));
461 let b = cx_b.new(|_| NetworkedCounter::new(network.b_client()));
462
463 // B increments locally and broadcasts the delta
464 b.update(cx_b, |b, cx| b.increment(42, cx));
465 b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // B's count is set immediately
466 a.read_with(cx_a, |a, _| assert_eq!(a.count, 0)); // A's count is in a side effect
467
468 cx_b.run_until_parked(); // Send the delta from B
469 a.update(cx_a, |a, _| a.sync()); // Receive the delta at A
470
471 b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // Both counts now match
472 a.read_with(cx_a, |a, _| assert_eq!(a.count, 42));
473 }
474
475 /// Multiple apps can run concurrently, and to capture this each test app shares
476 /// a dispatcher. Whenever you call `run_until_parked`, the dispatcher will randomly
477 /// pick which app's tasks to run next. This allows you to test that your distributed code
478 /// is robust to different execution orderings.
479 #[gpui::test(iterations = 10)]
480 fn test_random_interleaving(
481 cx_a: &mut TestAppContext,
482 cx_b: &mut TestAppContext,
483 mut rng: StdRng,
484 ) {
485 let network = MockNetwork::new();
486
487 // Track execution order
488 let mut original_order = Vec::new();
489 let a = cx_a.new(|_| NetworkedCounter::new(MockNetwork::a_client(&network)));
490 let b = cx_b.new(|_| NetworkedCounter::new(MockNetwork::b_client(&network)));
491
492 let num_operations: usize = rng.random_range(3..8);
493
494 for i in 0..num_operations {
495 let i = i as i32;
496 let which = rng.random_bool(0.5);
497
498 original_order.push(i);
499 if which {
500 b.update(cx_b, |b, cx| b.increment(i, cx));
501 } else {
502 a.update(cx_a, |a, cx| a.increment(i, cx));
503 }
504 }
505
506 // This will send all of the pending increment messages, from both a and b
507 cx_a.run_until_parked();
508
509 a.update(cx_a, |a, _| a.sync());
510 b.update(cx_b, |b, _| b.sync());
511
512 let a_count = a.read_with(cx_a, |a, _| a.count);
513 let b_count = b.read_with(cx_b, |b, _| b.count);
514
515 assert_eq!(a_count, b_count, "A and B should have the same count");
516
517 // Nicely format the execution order output.
518 // Run this test with `-- --nocapture` to see it!
519 let actual = network.state.lock().unwrap().ordering.clone();
520 let spawned: Vec<_> = original_order.iter().map(|n| format!("{}", n)).collect();
521 let ran: Vec<_> = actual.iter().map(|n| format!("{}", n)).collect();
522 let diff: Vec<_> = original_order
523 .iter()
524 .zip(actual.iter())
525 .map(|(o, a)| {
526 if o == a {
527 " ".to_string()
528 } else {
529 "^".to_string()
530 }
531 })
532 .collect();
533 println!("spawned: [{}]", spawned.join(", "));
534 println!("ran: [{}]", ran.join(", "));
535 println!(" [{}]", diff.join(", "));
536 }
537 }
538}