Extract a scheduler crate from GPUI to enable unified integration testing of client and server code (#37326)

Extracts and cleans up GPUI's scheduler code into a new `scheduler`
crate, making it pluggable by external runtimes. This will enable
deterministic integration testing with cloud components by providing a
unified test scheduler across Zed and backend code. In Zed, it will
replace the existing GPUI scheduler for consistent async task management
across platforms.

## Changes

- **Core Implementation**: `TestScheduler` with seed-based
randomization, session tracking (`SessionId`), and foreground/background
task separation for reproducible testing.
- **Executors**: `ForegroundExecutor` (!Send, thread-local) and
`BackgroundExecutor` (Send, with blocking/timeout support) as
GPUI-compatible wrappers.
- **Clock and Timer**: Controllable `TestClock` and future-based `Timer`
for time-sensitive tests.
- **Testing APIs**: `once()`, `with_seed()`, and `many()` methods for
configurable test runs.
- **Dependencies**: Added `async-task`, `chrono`, `futures`, etc., with
updates to `Cargo.toml` and lock file.

## Benefits

- **Integration Testing**: Facilitates reliable async tests involving
cloud sessions, reducing flakiness via deterministic execution.
- **Pluggability**: Trait-based design (`Scheduler`) allows easy
integration into non-GPUI runtimes while maintaining GPUI compatibility.
- **Cleanup**: Refactors GPUI scheduler logic for clarity, correctness
(no `unwrap()`, proper error handling), and extensibility.

Follows Rust guidelines; run `./script/clippy` for verification.

- [x] Define and test a core scheduler that we think can power our cloud
code and GPUI
- [ ] Replace GPUI's scheduler


Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Nathan Sobo
2025-09-04 17:14:53 +02:00
committed by GitHub
co-authored by Antonio Scandurra
parent a05f86f97b
commit 1ae326432e
64 changed files with 1569 additions and 473 deletions
+16 -16
View File
@@ -3013,7 +3013,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let base_text_len = rng.gen_range(0..10);
let base_text_len = rng.random_range(0..10);
let base_text = RandomCharIter::new(&mut rng)
.take(base_text_len)
.collect::<String>();
@@ -3022,7 +3022,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
let network = Arc::new(Mutex::new(Network::new(rng.clone())));
let base_buffer = cx.new(|cx| Buffer::local(base_text.as_str(), cx));
for i in 0..rng.gen_range(min_peers..=max_peers) {
for i in 0..rng.random_range(min_peers..=max_peers) {
let buffer = cx.new(|cx| {
let state = base_buffer.read(cx).to_proto(cx);
let ops = cx
@@ -3035,7 +3035,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
.map(|op| proto::deserialize_operation(op).unwrap()),
cx,
);
buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200)));
let network = network.clone();
cx.subscribe(&cx.entity(), move |buffer, _, event, _| {
if let BufferEvent::Operation {
@@ -3066,11 +3066,11 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
let mut next_diagnostic_id = 0;
let mut active_selections = BTreeMap::default();
loop {
let replica_index = rng.gen_range(0..replica_ids.len());
let replica_index = rng.random_range(0..replica_ids.len());
let replica_id = replica_ids[replica_index];
let buffer = &mut buffers[replica_index];
let mut new_buffer = None;
match rng.gen_range(0..100) {
match rng.random_range(0..100) {
0..=29 if mutation_count != 0 => {
buffer.update(cx, |buffer, cx| {
buffer.start_transaction_at(now);
@@ -3082,13 +3082,13 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
}
30..=39 if mutation_count != 0 => {
buffer.update(cx, |buffer, cx| {
if rng.gen_bool(0.2) {
if rng.random_bool(0.2) {
log::info!("peer {} clearing active selections", replica_id);
active_selections.remove(&replica_id);
buffer.remove_active_selections(cx);
} else {
let mut selections = Vec::new();
for id in 0..rng.gen_range(1..=5) {
for id in 0..rng.random_range(1..=5) {
let range = buffer.random_byte_range(0, &mut rng);
selections.push(Selection {
id,
@@ -3111,7 +3111,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
mutation_count -= 1;
}
40..=49 if mutation_count != 0 && replica_id == 0 => {
let entry_count = rng.gen_range(1..=5);
let entry_count = rng.random_range(1..=5);
buffer.update(cx, |buffer, cx| {
let diagnostics = DiagnosticSet::new(
(0..entry_count).map(|_| {
@@ -3166,7 +3166,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
new_buffer.replica_id(),
new_buffer.text()
);
new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
new_buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200)));
let network = network.clone();
cx.subscribe(&cx.entity(), move |buffer, _, event, _| {
if let BufferEvent::Operation {
@@ -3238,7 +3238,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
_ => {}
}
now += Duration::from_millis(rng.gen_range(0..=200));
now += Duration::from_millis(rng.random_range(0..=200));
buffers.extend(new_buffer);
for buffer in &buffers {
@@ -3320,23 +3320,23 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
// Generate a random multi-line string containing
// some lines with trailing whitespace.
let mut text = String::new();
for _ in 0..rng.gen_range(0..16) {
for _ in 0..rng.gen_range(0..36) {
text.push(match rng.gen_range(0..10) {
for _ in 0..rng.random_range(0..16) {
for _ in 0..rng.random_range(0..36) {
text.push(match rng.random_range(0..10) {
0..=1 => ' ',
3 => '\t',
_ => rng.gen_range('a'..='z'),
_ => rng.random_range('a'..='z'),
});
}
text.push('\n');
}
match rng.gen_range(0..10) {
match rng.random_range(0..10) {
// sometimes remove the last newline
0..=1 => drop(text.pop()), //
// sometimes add extra newlines
2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))),
2..=3 => text.push_str(&"\n".repeat(rng.random_range(1..5))),
_ => {}
}