ZIm/crates/gpui/src/test.rs
João Marcos f587e95a7e
Add seed argument to #[gpui::test] attribute macro (#26764)
This PR introduces the arguments `seed` and `seeds` to `gpui::test`,
e.g.:
- `#[gpui::test(seed = 10)]`
- `#[gpui::test(seeds(10, 20, 30, 40))]`

Which allows us to run a test against a specific seed value without
slowing
down our tests like `iterations` does with high values.

This was motivated by a diff hunk test that only fails in a 400+ seed,
but is
slow to run 400+ times for every `cargo test`.

If your test failed with a specific seed, you can now add the `seed` arg
to
increase the chances of detecting a regression.

There are now three ways of setting seeds, the `SEED` env var,
`iterations`,
and the args this PR adds. See docs in `gpui::test`.

---

I also relaxed the limitation on `retries` not working with
`iterations`, as
that seemed unnecessary.

Release Notes:

- N/A
2025-03-14 13:40:02 -03:00

158 lines
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 rand::prelude::*;
use smol::channel;
use std::{
env,
panic::{self, RefUnwindSafe},
pin::Pin,
};
/// 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(StdRng::seed_from_u64(seed));
test_fn(dispatcher, seed);
});
match result {
Ok(_) => break,
Err(error) => {
if attempt < max_retries {
println!("attempt {} failed, retrying", attempt);
attempt += 1;
} else {
if is_multiple_runs {
eprintln!("failing seed: {}", 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<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) = smol::channel::unbounded();
let _subscription = cx.update(|cx| {
cx.observe(entity, move |_, _| {
let _ = smol::block_on(tx.send(()));
})
});
let rx = Box::pin(rx);
Observation { rx, _subscription }
}