Files
zed/crates/gpui/src/test.rs
T
Cameron McloughlinandLukas Wirth f482f9e18c agent: LSP tools (#55744)
Adds 4 (technically 5) new tools to the zed agent, corresponding to LSP
actions:
- `find_references`
- `goto_definition`
- `rename_symbol`
- `get_code_actions` and `apply_code_actions`

Notes:
- `rename_symbol` skips doing a `prepare_rename`. If there is nothing to
rename at the position, it will forward the error to the agent
- The code action tools are stateful. The state is stored in the
`get_code_actions` tool itself as a `PendingCodeActions`. It is not
passed into/out of subagents. Calling `apply_code_actions` without
calling `get_code_actions` first is an error, but I've never seen an
agent do this

Symbols are identified by:
- file name
- line number
- symbol

If there is no substring match on that line for the symbol text, it is
an error. If there are multiple, it chooses the first. This may not be
great if you have a line like: `fn convert(x: foo::Something) ->
bar::Something` - the second `Something` is a different symbol, but is
inacessible to these tools. Probably fine for now, but we can look into
improving


Release Notes:

- Added: New tools for the Zed Agent for interacting with language
servers

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
2026-05-05 09:35:08 +00:00

221 lines
7.5 KiB
Rust

//! Test support for GPUI.
//!
//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context,
//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run
//! deterministically even in the face of arbitrary parallelism.
//!
//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test`
//! or `cargo-nextest`, or another runner of your choice.
//!
//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts
//! as you need.
//!
//! ## Example
//!
//! ```
//! use gpui;
//!
//! #[gpui::test]
//! async fn test_example(cx: &TestAppContext) {
//! assert!(true)
//! }
//!
//! #[gpui::test]
//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) {
//! assert!(true)
//! }
//! ```
use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
use futures::StreamExt as _;
use proptest::prelude::{Just, Strategy, any};
use std::{
env,
panic::{self, RefUnwindSafe, UnwindSafe},
pin::Pin,
};
/// Strategy injected into `#[gpui::property_test]` tests to control the seed
/// given to the scheduler. Doesn't shrink, since all scheduler seeds are
/// equivalent in complexity. If `$SEED` is set, it always uses that value.
///
/// Note: this function is not intended to be used directly. Rather, it is
/// public so that it can be used from the `property_test` macro.
pub fn seed_strategy() -> impl Strategy<Value = u64> {
match std::env::var("SEED") {
Ok(val) => Just(val.parse().unwrap()).boxed(),
Err(_) => any::<u64>().no_shrink().boxed(),
}
}
/// Applies a fixed RNG seed to a proptest config so that case generation
/// is deterministic. Uses `$SEED` if set, otherwise defaults to `0`.
/// This bridges the GPUI `SEED` env var to proptest's RNG seed, so that
/// a single variable controls both the scheduler seed and case generation.
///
/// Note: this function is not intended to be used directly. Rather, it is
/// public so that it can be used from the `property_test` macro.
pub fn apply_seed_to_proptest_config(
mut config: proptest::test_runner::Config,
) -> proptest::test_runner::Config {
let seed = env::var("SEED")
.ok()
.and_then(|val| val.parse::<u64>().ok())
.unwrap_or(0);
config.rng_seed = proptest::test_runner::RngSeed::Fixed(seed);
config
}
/// Similar to [`run_test`], but only runs the callback once, allowing
/// [`FnOnce`] callbacks. This is intended for use with the
/// `gpui::property_test` macro and generally should not be used directly.
///
/// Doesn't support many features of [`run_test`], since these are provided by
/// proptest.
pub fn run_test_once<R>(
seed: u64,
test_fn: Box<dyn UnwindSafe + FnOnce(TestDispatcher) -> R>,
) -> R {
let result = panic::catch_unwind(|| {
let dispatcher = TestDispatcher::new(seed);
let scheduler = dispatcher.scheduler().clone();
let res = test_fn(dispatcher);
scheduler.end_test();
res
});
match result {
Ok(r) => r,
Err(e) => panic::resume_unwind(e),
}
}
/// Run the given test function with the configured parameters.
/// This is intended for use with the `gpui::test` macro
/// and generally should not be used directly.
pub fn run_test(
num_iterations: usize,
explicit_seeds: &[u64],
max_retries: usize,
test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)),
on_fail_fn: Option<fn()>,
) {
let (seeds, is_multiple_runs) = calculate_seeds(num_iterations as u64, explicit_seeds);
for seed in seeds {
let mut attempt = 0;
loop {
if is_multiple_runs {
eprintln!("seed = {seed}");
}
let result = panic::catch_unwind(|| {
let dispatcher = TestDispatcher::new(seed);
let scheduler = dispatcher.scheduler().clone();
test_fn(dispatcher, seed);
scheduler.end_test();
});
match result {
Ok(_) => break,
Err(error) => {
if attempt < max_retries {
println!("attempt {} failed, retrying", attempt);
attempt += 1;
// The panic payload might itself trigger an unwind on drop:
// https://doc.rust-lang.org/std/panic/fn.catch_unwind.html#notes
std::mem::forget(error);
} else {
if is_multiple_runs {
eprintln!("failing seed: {seed}");
eprintln!(
"You can rerun from this seed by setting the environmental variable SEED to {seed}"
);
}
if let Some(on_fail_fn) = on_fail_fn {
on_fail_fn()
}
panic::resume_unwind(error);
}
}
}
}
}
}
fn calculate_seeds(
iterations: u64,
explicit_seeds: &[u64],
) -> (impl Iterator<Item = u64> + '_, bool) {
let iterations = env::var("ITERATIONS")
.ok()
.map(|var| var.parse().expect("invalid ITERATIONS variable"))
.unwrap_or(iterations);
let env_num = env::var("SEED")
.map(|seed| seed.parse().expect("invalid SEED variable as integer"))
.ok();
let empty_range = || 0..0;
let iter = {
let env_range = if let Some(env_num) = env_num {
env_num..env_num + 1
} else {
empty_range()
};
// if `iterations` is 1 and !(`explicit_seeds` is non-empty || `SEED` is set), then add the run `0`
// if `iterations` is 1 and (`explicit_seeds` is non-empty || `SEED` is set), then discard the run `0`
// if `iterations` isn't 1 and `SEED` is set, do `SEED..SEED+iterations`
// otherwise, do `0..iterations`
let iterations_range = match (iterations, env_num) {
(1, None) if explicit_seeds.is_empty() => 0..1,
(1, None) | (1, Some(_)) => empty_range(),
(iterations, Some(env)) => env..env + iterations,
(iterations, None) => 0..iterations,
};
// if `SEED` is set, ignore `explicit_seeds`
let explicit_seeds = if env_num.is_some() {
&[]
} else {
explicit_seeds
};
env_range
.chain(iterations_range)
.chain(explicit_seeds.iter().copied())
};
let is_multiple_runs = iter.clone().nth(1).is_some();
(iter, is_multiple_runs)
}
/// A test struct for converting an observation callback into a stream.
pub struct Observation<T> {
rx: Pin<Box<async_channel::Receiver<T>>>,
_subscription: Subscription,
}
impl<T: 'static> futures::Stream for Observation<T> {
type Item = T;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.rx.poll_next_unpin(cx)
}
}
/// observe returns a stream of the change events from the given `Entity`
pub fn observe<T: 'static>(entity: &Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
let (tx, rx) = async_channel::unbounded();
let _subscription = cx.update(|cx| {
cx.observe(entity, move |_, _| {
let _ = gpui::block_on(tx.send(()));
})
});
let rx = Box::pin(rx);
Observation { rx, _subscription }
}