logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
552
crates/gpui/examples/testing.rs
Normal file
552
crates/gpui/examples/testing.rs
Normal file
@@ -0,0 +1,552 @@
|
||||
#![cfg_attr(target_family = "wasm", no_main)]
|
||||
//! 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, Bounds, Context, FocusHandle, Focusable, Render, Task, Window, WindowBounds,
|
||||
WindowOptions, actions, div, prelude::*, px, rgb, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
|
||||
actions!(counter, [Increment, Decrement]);
|
||||
|
||||
struct Counter {
|
||||
count: i32,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
/// Event emitted by Counter
|
||||
struct 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 run_example() {
|
||||
application().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(not(target_family = "wasm"))]
|
||||
fn main() {
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
gpui_platform::web_init();
|
||||
run_example();
|
||||
}
|
||||
|
||||
#[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));
|
||||
} else {
|
||||
a.update(cx_a, |a, cx| a.increment(i, cx));
|
||||
}
|
||||
}
|
||||
|
||||
// This will send all of the pending increment messages, from both a and b
|
||||
cx_a.run_until_parked();
|
||||
|
||||
a.update(cx_a, |a, _| a.sync());
|
||||
b.update(cx_b, |b, _| b.sync());
|
||||
|
||||
let a_count = a.read_with(cx_a, |a, _| a.count);
|
||||
let b_count = b.read_with(cx_b, |b, _| b.count);
|
||||
|
||||
assert_eq!(a_count, b_count, "A and B should have the same count");
|
||||
|
||||
// Nicely format the execution order output.
|
||||
// Run this test with `-- --nocapture` to see it!
|
||||
let actual = network.state.lock().unwrap().ordering.clone();
|
||||
let spawned: Vec<_> = original_order.iter().map(|n| format!("{}", n)).collect();
|
||||
let ran: Vec<_> = actual.iter().map(|n| format!("{}", n)).collect();
|
||||
let diff: Vec<_> = original_order
|
||||
.iter()
|
||||
.zip(actual.iter())
|
||||
.map(|(o, a)| {
|
||||
if o == a {
|
||||
" ".to_string()
|
||||
} else {
|
||||
"^".to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
println!("spawned: [{}]", spawned.join(", "));
|
||||
println!("ran: [{}]", ran.join(", "));
|
||||
println!(" [{}]", diff.join(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user