1#![cfg_attr(target_family = "wasm", no_main)]
2//! Example demonstrating GPUI's testing infrastructure.
3//!
4//! When run normally, this displays an interactive counter window.
5//! The tests below demonstrate various GPUI testing patterns.
6//!
7//! Run the app: cargo run -p gpui --example testing
8//! Run tests: cargo test -p gpui --example testing --features test-support
9
10use gpui::{
11 App, Bounds, Context, FocusHandle, Focusable, Render, Task, Window, WindowBounds,
12 WindowOptions, actions, div, prelude::*, px, rgb, size,
13};
14use gpui_platform::application;
15
16actions!(counter, [Increment, Decrement]);
17
18struct Counter {
19 count: i32,
20 focus_handle: FocusHandle,
21 _subscription: gpui::Subscription,
22}
23
24/// Event emitted by Counter
25struct CounterEvent;
26
27impl gpui::EventEmitter<CounterEvent> for Counter {}
28
29impl Counter {
30 fn new(cx: &mut Context<Self>) -> Self {
31 let subscription = cx.subscribe_self(|this: &mut Self, _event: &CounterEvent, _cx| {
32 this.count = 999;
33 });
34
35 Self {
36 count: 0,
37 focus_handle: cx.focus_handle(),
38 _subscription: subscription,
39 }
40 }
41
42 fn increment(&mut self, _: &Increment, _window: &mut Window, cx: &mut Context<Self>) {
43 self.count += 1;
44 cx.notify();
45 }
46
47 fn decrement(&mut self, _: &Decrement, _window: &mut Window, cx: &mut Context<Self>) {
48 self.count -= 1;
49 cx.notify();
50 }
51
52 fn load(&self, cx: &mut Context<Self>) -> Task<()> {
53 cx.spawn(async move |this, cx| {
54 // Simulate loading data (e.g., from disk or network)
55 this.update(cx, |counter, _| {
56 counter.count = 100;
57 })
58 .ok();
59 })
60 }
61
62 fn reload(&self, cx: &mut Context<Self>) {
63 cx.spawn(async move |this, cx| {
64 // Simulate reloading data in the background
65 this.update(cx, |counter, _| {
66 counter.count += 50;
67 })
68 .ok();
69 })
70 .detach();
71 }
72}
73
74impl Focusable for Counter {
75 fn focus_handle(&self, _cx: &App) -> FocusHandle {
76 self.focus_handle.clone()
77 }
78}
79
80impl Render for Counter {
81 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
82 div()
83 .id("counter")
84 .key_context("Counter")
85 .on_action(cx.listener(Self::increment))
86 .on_action(cx.listener(Self::decrement))
87 .track_focus(&self.focus_handle)
88 .flex()
89 .flex_col()
90 .gap_4()
91 .bg(rgb(0x1e1e2e))
92 .size_full()
93 .justify_center()
94 .items_center()
95 .child(
96 div()
97 .text_3xl()
98 .text_color(rgb(0xcdd6f4))
99 .child(format!("{}", self.count)),
100 )
101 .child(
102 div()
103 .flex()
104 .gap_2()
105 .child(
106 div()
107 .id("decrement")
108 .px_4()
109 .py_2()
110 .bg(rgb(0x313244))
111 .hover(|s| s.bg(rgb(0x45475a)))
112 .rounded_md()
113 .cursor_pointer()
114 .text_color(rgb(0xcdd6f4))
115 .on_click(cx.listener(|this, _, window, cx| {
116 this.decrement(&Decrement, window, cx)
117 }))
118 .child("−"),
119 )
120 .child(
121 div()
122 .id("increment")
123 .px_4()
124 .py_2()
125 .bg(rgb(0x313244))
126 .hover(|s| s.bg(rgb(0x45475a)))
127 .rounded_md()
128 .cursor_pointer()
129 .text_color(rgb(0xcdd6f4))
130 .on_click(cx.listener(|this, _, window, cx| {
131 this.increment(&Increment, window, cx)
132 }))
133 .child("+"),
134 ),
135 )
136 .child(
137 div()
138 .flex()
139 .gap_2()
140 .child(
141 div()
142 .id("load")
143 .px_4()
144 .py_2()
145 .bg(rgb(0x313244))
146 .hover(|s| s.bg(rgb(0x45475a)))
147 .rounded_md()
148 .cursor_pointer()
149 .text_color(rgb(0xcdd6f4))
150 .on_click(cx.listener(|this, _, _, cx| {
151 this.load(cx).detach();
152 }))
153 .child("Load"),
154 )
155 .child(
156 div()
157 .id("reload")
158 .px_4()
159 .py_2()
160 .bg(rgb(0x313244))
161 .hover(|s| s.bg(rgb(0x45475a)))
162 .rounded_md()
163 .cursor_pointer()
164 .text_color(rgb(0xcdd6f4))
165 .on_click(cx.listener(|this, _, _, cx| {
166 this.reload(cx);
167 }))
168 .child("Reload"),
169 ),
170 )
171 .child(
172 div()
173 .text_sm()
174 .text_color(rgb(0x6c7086))
175 .child("Press ↑/↓ or click buttons"),
176 )
177 }
178}
179
180fn run_example() {
181 application().run(|cx: &mut App| {
182 cx.bind_keys([
183 gpui::KeyBinding::new("up", Increment, Some("Counter")),
184 gpui::KeyBinding::new("down", Decrement, Some("Counter")),
185 ]);
186
187 let bounds = Bounds::centered(None, size(px(300.), px(200.)), cx);
188 cx.open_window(
189 WindowOptions {
190 window_bounds: Some(WindowBounds::Windowed(bounds)),
191 ..Default::default()
192 },
193 |window, cx| {
194 let counter = cx.new(|cx| Counter::new(cx));
195 counter.focus_handle(cx).focus(window, cx);
196 counter
197 },
198 )
199 .unwrap();
200 });
201}
202
203#[cfg(not(target_family = "wasm"))]
204fn main() {
205 run_example();
206}
207
208#[cfg(target_family = "wasm")]
209#[wasm_bindgen::prelude::wasm_bindgen(start)]
210pub fn start() {
211 gpui_platform::web_init();
212 run_example();
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use gpui::{TestAppContext, VisualTestContext};
219 use rand::prelude::*;
220
221 /// Here's a basic GPUI test. Just add the macro and take a TestAppContext as an argument!
222 ///
223 /// Note that synchronous side effects run immediately after your "update*" calls complete.
224 #[gpui::test]
225 fn basic_testing(cx: &mut TestAppContext) {
226 let counter = cx.new(|cx| Counter::new(cx));
227
228 counter.update(cx, |counter, _| {
229 counter.count = 42;
230 });
231
232 // Note that TestAppContext doesn't support `read(cx)`
233 let updated = counter.read_with(cx, |counter, _| counter.count);
234 assert_eq!(updated, 42);
235
236 // Emit an event - the subscriber will run immediately after the update finishes
237 counter.update(cx, |_, cx| {
238 cx.emit(CounterEvent);
239 });
240
241 let count_after_update = counter.read_with(cx, |counter, _| counter.count);
242 assert_eq!(
243 count_after_update, 999,
244 "Side effects should run after update completes"
245 );
246 }
247
248 /// Tests which involve the window require you to construct a VisualTestContext.
249 /// Just like synchronous side effects, the window will be drawn after every "update*"
250 /// call, so you can test render-dependent behavior.
251 #[gpui::test]
252 fn test_counter_in_window(cx: &mut TestAppContext) {
253 let window = cx.update(|cx| {
254 cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx)))
255 .unwrap()
256 });
257
258 let mut cx = VisualTestContext::from_window(window.into(), cx);
259 let counter = window.root(&mut cx).unwrap();
260
261 // Action dispatch depends on the element tree to resolve which action handler
262 // to call, and this works exactly as you'd expect in a test.
263 let focus_handle = counter.read_with(&cx, |counter, _| counter.focus_handle.clone());
264 cx.update(|window, cx| {
265 focus_handle.dispatch_action(&Increment, window, cx);
266 });
267
268 let count_after = counter.read_with(&cx, |counter, _| counter.count);
269 assert_eq!(
270 count_after, 1,
271 "Action dispatched via focus handle should increment"
272 );
273 }
274
275 /// GPUI tests can also be async, simply add the async keyword before the test.
276 /// Note that the test executor is single thread, so async side effects (including
277 /// background tasks) won't run until you explicitly yield control.
278 #[gpui::test]
279 async fn test_async_operations(cx: &mut TestAppContext) {
280 let counter = cx.new(|cx| Counter::new(cx));
281
282 // Tasks can be awaited directly
283 counter.update(cx, |counter, cx| counter.load(cx)).await;
284
285 let count = counter.read_with(cx, |counter, _| counter.count);
286 assert_eq!(count, 100, "Load task should have set count to 100");
287
288 // But side effects don't run until you yield control
289 counter.update(cx, |counter, cx| counter.reload(cx));
290
291 let count = counter.read_with(cx, |counter, _| counter.count);
292 assert_eq!(count, 100, "Detached reload task shouldn't have run yet");
293
294 // This runs all pending tasks
295 cx.run_until_parked();
296
297 let count = counter.read_with(cx, |counter, _| counter.count);
298 assert_eq!(count, 150, "Reload task should have run after parking");
299 }
300
301 /// Note that the test executor panics if you await a future that waits on
302 /// something outside GPUI's control, like a reading a file or network IO.
303 /// You should mock external systems where possible, as this feature can be used
304 /// to detect potential deadlocks in your async code.
305 ///
306 /// However, if you want to disable this check use `allow_parking()`
307 #[gpui::test]
308 async fn test_allow_parking(cx: &mut TestAppContext) {
309 // Allow the thread to park
310 cx.executor().allow_parking();
311
312 // Simulate an external system (like a file system) with an OS thread
313 let (tx, rx) = futures::channel::oneshot::channel();
314 std::thread::spawn(move || {
315 std::thread::sleep(std::time::Duration::from_millis(5));
316 tx.send(42).ok();
317 });
318
319 // Without allow_parking(), this await would panic because GPUI's
320 // scheduler runs out of tasks while waiting for the external thread.
321 let result = rx.await.unwrap();
322 assert_eq!(result, 42);
323 }
324
325 /// GPUI also provides support for property testing, via the iterations flag
326 #[gpui::test(iterations = 10)]
327 fn test_counter_random_operations(cx: &mut TestAppContext, mut rng: StdRng) {
328 let window = cx.update(|cx| {
329 cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx)))
330 .unwrap()
331 });
332 let mut cx = VisualTestContext::from_window(window.into(), cx);
333
334 let counter = cx.new(|cx| Counter::new(cx));
335
336 // Perform random increments/decrements
337 let mut expected = 0i32;
338 for _ in 0..100 {
339 if rng.random_bool(0.5) {
340 expected += 1;
341 counter.update_in(&mut cx, |counter, window, cx| {
342 counter.increment(&Increment, window, cx)
343 });
344 } else {
345 expected -= 1;
346 counter.update_in(&mut cx, |counter, window, cx| {
347 counter.decrement(&Decrement, window, cx)
348 });
349 }
350 }
351
352 let actual = counter.read_with(&cx, |counter, _| counter.count);
353 assert_eq!(
354 actual, expected,
355 "Counter should match expected after random ops"
356 );
357 }
358
359 /// Now, all of those tests are good, but GPUI also provides strong support for testing distributed systems.
360 /// Let's setup a mock network and enhance the counter to send messages over it.
361 mod distributed_systems {
362 use std::sync::{Arc, Mutex};
363
364 /// The state of the mock network.
365 struct MockNetworkState {
366 ordering: Vec<i32>,
367 a_to_b: Vec<i32>,
368 b_to_a: Vec<i32>,
369 }
370
371 /// A mock network that delivers messages between two peers.
372 #[derive(Clone)]
373 struct MockNetwork {
374 state: Arc<Mutex<MockNetworkState>>,
375 }
376
377 impl MockNetwork {
378 fn new() -> Self {
379 Self {
380 state: Arc::new(Mutex::new(MockNetworkState {
381 ordering: Vec::new(),
382 a_to_b: Vec::new(),
383 b_to_a: Vec::new(),
384 })),
385 }
386 }
387
388 fn a_client(&self) -> NetworkClient {
389 NetworkClient {
390 network: self.clone(),
391 is_a: true,
392 }
393 }
394
395 fn b_client(&self) -> NetworkClient {
396 NetworkClient {
397 network: self.clone(),
398 is_a: false,
399 }
400 }
401 }
402
403 /// A client handle for sending/receiving messages over the mock network.
404 #[derive(Clone)]
405 struct NetworkClient {
406 network: MockNetwork,
407 is_a: bool,
408 }
409
410 // See, networking is easy!
411 impl NetworkClient {
412 fn send(&self, value: i32) {
413 let mut network = self.network.state.lock().unwrap();
414 network.ordering.push(value);
415 if self.is_a {
416 network.b_to_a.push(value);
417 } else {
418 network.a_to_b.push(value);
419 }
420 }
421
422 fn receive_all(&self) -> Vec<i32> {
423 let mut network = self.network.state.lock().unwrap();
424 if self.is_a {
425 network.a_to_b.drain(..).collect()
426 } else {
427 network.b_to_a.drain(..).collect()
428 }
429 }
430 }
431
432 use gpui::Context;
433
434 /// A networked counter that can send/receive over a mock network.
435 struct NetworkedCounter {
436 count: i32,
437 client: NetworkClient,
438 }
439
440 impl NetworkedCounter {
441 fn new(client: NetworkClient) -> Self {
442 Self { count: 0, client }
443 }
444
445 /// Increment the counter and broadcast the change.
446 fn increment(&mut self, delta: i32, cx: &mut Context<Self>) {
447 self.count += delta;
448
449 cx.background_spawn({
450 let client = self.client.clone();
451 async move {
452 client.send(delta);
453 }
454 })
455 .detach();
456 }
457
458 /// Process incoming increment requests.
459 fn sync(&mut self) {
460 for delta in self.client.receive_all() {
461 self.count += delta;
462 }
463 }
464 }
465
466 use super::*;
467
468 /// You can simulate distributed systems with multiple app contexts, simply by adding
469 /// additional parameters.
470 #[gpui::test]
471 fn test_app_sync(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
472 let network = MockNetwork::new();
473
474 let a = cx_a.new(|_| NetworkedCounter::new(network.a_client()));
475 let b = cx_b.new(|_| NetworkedCounter::new(network.b_client()));
476
477 // B increments locally and broadcasts the delta
478 b.update(cx_b, |b, cx| b.increment(42, cx));
479 b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // B's count is set immediately
480 a.read_with(cx_a, |a, _| assert_eq!(a.count, 0)); // A's count is in a side effect
481
482 cx_b.run_until_parked(); // Send the delta from B
483 a.update(cx_a, |a, _| a.sync()); // Receive the delta at A
484
485 b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // Both counts now match
486 a.read_with(cx_a, |a, _| assert_eq!(a.count, 42));
487 }
488
489 /// Multiple apps can run concurrently, and to capture this each test app shares
490 /// a dispatcher. Whenever you call `run_until_parked`, the dispatcher will randomly
491 /// pick which app's tasks to run next. This allows you to test that your distributed code
492 /// is robust to different execution orderings.
493 #[gpui::test(iterations = 10)]
494 fn test_random_interleaving(
495 cx_a: &mut TestAppContext,
496 cx_b: &mut TestAppContext,
497 mut rng: StdRng,
498 ) {
499 let network = MockNetwork::new();
500
501 // Track execution order
502 let mut original_order = Vec::new();
503 let a = cx_a.new(|_| NetworkedCounter::new(MockNetwork::a_client(&network)));
504 let b = cx_b.new(|_| NetworkedCounter::new(MockNetwork::b_client(&network)));
505
506 let num_operations: usize = rng.random_range(3..8);
507
508 for i in 0..num_operations {
509 let i = i as i32;
510 let which = rng.random_bool(0.5);
511
512 original_order.push(i);
513 if which {
514 b.update(cx_b, |b, cx| b.increment(i, cx));
515 } else {
516 a.update(cx_a, |a, cx| a.increment(i, cx));
517 }
518 }
519
520 // This will send all of the pending increment messages, from both a and b
521 cx_a.run_until_parked();
522
523 a.update(cx_a, |a, _| a.sync());
524 b.update(cx_b, |b, _| b.sync());
525
526 let a_count = a.read_with(cx_a, |a, _| a.count);
527 let b_count = b.read_with(cx_b, |b, _| b.count);
528
529 assert_eq!(a_count, b_count, "A and B should have the same count");
530
531 // Nicely format the execution order output.
532 // Run this test with `-- --nocapture` to see it!
533 let actual = network.state.lock().unwrap().ordering.clone();
534 let spawned: Vec<_> = original_order.iter().map(|n| format!("{}", n)).collect();
535 let ran: Vec<_> = actual.iter().map(|n| format!("{}", n)).collect();
536 let diff: Vec<_> = original_order
537 .iter()
538 .zip(actual.iter())
539 .map(|(o, a)| {
540 if o == a {
541 " ".to_string()
542 } else {
543 "^".to_string()
544 }
545 })
546 .collect();
547 println!("spawned: [{}]", spawned.join(", "));
548 println!("ran: [{}]", ran.join(", "));
549 println!(" [{}]", diff.join(", "));
550 }
551 }
552}